【问题标题】:How to trigger a method when Angular is done adding scope updates to the DOM?Angular 完成向 DOM 添加范围更新后如何触发方法?
【发布时间】:2013-06-20 22:48:08
【问题描述】:

我正在寻找一种在将更改添加到 $scope 变量(在本例中为 $scope.results)后执行代码的方法。我需要这样做是为了调用一些要求项目在 DOM 中才能执行的遗留代码。

我的真实代码是触发 AJAX 调用,并更新范围变量以更新 ui。所以我目前我的代码在我推送到范围后立即执行,但是由于 dom 元素尚不可用,遗留代码失败了。

我可以用 setTimeout() 添加一个难看的延迟,但这并不能保证 DOM 真的准备好了。

我的问题是,有什么方法可以绑定到类似“渲染”的事件?

var myApp = angular.module('myApp', []);

myApp.controller("myController", ['$scope', function($scope){
    var resultsToLoad = [{id: 1, name: "one"},{id: 2, name: "two"},{id: 3, name: "three"}];
    $scope.results = [];

    $scope.loadResults = function(){
        for(var i=0; i < resultsToLoad.length; i++){
            $scope.results.push(resultsToLoad[i]);
        }
    }

    function doneAddingToDom(){
        // do something awesome like trigger a service call to log
    }
}]);
angular.bootstrap(document, ['myApp']);

模拟代码链接:http://jsfiddle.net/acolchado/BhApF/5/

提前致谢!

【问题讨论】:

  • 如果你想使用 setTimeout() 为什么不试试 setInterval() 方法呢?以角度方式,有什么方法可以在代码中使用手表功能?
  • 我想避免使用 setTimeout。我发现这个小提琴声称可以做我想做的事,但它不起作用。关键似乎是$evalAsync,所以我必须想办法让它工作。我希望我走在正确的道路上。
  • 我面临着类似的问题,我的想法是使用承诺“做一些很棒的事情”,在将(最终)更新的范围变量上设置 $watch并解决 $watch 中的承诺。我还没有尝试过:刚刚进入 Angular。我很想看看你能不能让它工作!

标签: angularjs angularjs-scope


【解决方案1】:

$evalAsync 队列用于安排需要在当前堆栈框架之外发生但在浏览器视图呈现之前发生的工作。 -- http://docs.angularjs.org/guide/concepts#runtime

好的,那么什么是“堆栈帧”? Github 评论揭示了更多信息:

如果你从控制器入队,那么它将在之前,但如果你从指令入队,那么它将在之后。 -- https://github.com/angular/angular.js/issues/734#issuecomment-3675158

上面,Misko 正在讨论何时运行由 $evalAsync 排队等待执行的代码,以及 Angular 何时更新 DOM。我建议之前也阅读这两个 Github cmets,以获得完整的上下文。

所以如果代码使用指令中的 $evalAsync 进行排队,它应该在 Angular 操作 DOM 之后但在浏览器呈现之前运行。如果您需要在浏览器渲染后或控制器更新模型后运行某些内容,请使用 $timeout(..., 0);

另请参阅https://stackoverflow.com/a/13619324/215945,其中还有一个使用 $evalAsync() 的示例小提琴。

【讨论】:

  • 非常感谢您的解释。现在更有意义的是为什么需要使用 $timeout(..., 0),有或没有 $evalAsync()。添加强制等待后,它工作得非常好。现在浏览器在调用发生之前完成它的渲染。
  • DOM 更新后与浏览器呈现时是什么意思?浏览器不是总是渲染正在发生的一切吗?换句话说,如果我运行的函数只能在从指令模板中的父范围模型异步插入字符串后运行,我应该使用 evalAsync 还是 $timeout?我一直在使用 $timeout,它一直在工作,但这是正确的做事方式吗?
  • @CMCDragonkai,由于 Javascript 是单线程的,当 Angular 运行时,浏览器不会更新你看到的内容(即,它没有呈现)。请参阅Concepts page 上的第二张图片。 “DOM 渲染”发生在 Angular 摘要循环之后。我总是先尝试使用 $evalAsync,如果这不起作用,我尝试使用 $timeout。
【解决方案2】:

我叉了你的小提琴。 http://jsfiddle.net/xGCmp/7/

我添加了一个名为 emit-when 的指令。它需要两个参数。要发出的事件以及要发出的事件必须满足的条件。这是有效的,因为当在指令中执行链接函数时,我们知道元素已经在 DOM 中呈现。我的解决方案是在呈现 ng-repeat 中的最后一项时发出一个事件。

如果我们有一个全 Angular 解决方案,我不建议这样做。这有点骇人听闻。但是,对于处理您提到的遗留代码类型,它可能是一个不错的解决方案。

var myApp = angular.module('myApp', []);

myApp.controller("myController", ['$scope', function($scope){
    var resultsToLoad = [
        {id: 1, name: "one"},
        {id: 2, name: "two"},
        {id: 3, name: "three"}
    ];

    function doneAddingToDom() {
        console.log(document.getElementById('renderedList').children.length);
    }

    $scope.results = [];

    $scope.loadResults = function(){
        $scope.results = resultsToLoad;
        // If run doneAddingToDom here, we will find 0 list elements in the DOM. Check console.
        doneAddingToDom();
    }

    // If we run on doneAddingToDom here, we will find 3 list elements in the DOM.
    $scope.$on('allRendered', doneAddingToDom);
}]);

myApp.directive("emitWhen", function(){
    return {
        restrict: 'A',
        link: function(scope, element, attrs) {
            var params = scope.$eval(attrs.emitWhen),
                event = params.event,
                condition = params.condition;
            if(condition){
                scope.$emit(event);
            }
        }
    }
});

angular.bootstrap(document, ['myApp']);

【讨论】:

  • 这也是一个非常好的解决方案。真的很感激!一个快速跟进的问题。如果它是一个延迟加载的列表,我每次单击按钮时都将其附加到列表的末尾,那么每次都会触发该事件吗?
  • 如果这个小提琴描述了你的问题,那么是的。 =) jsfiddle.net/h98pH/1
【解决方案3】:

使用超时不是正确的方法。使用指令添加/操作 DOM。如果你确实使用超时,请确保使用与 Angular 挂钩的 $timeout(例如返回一个承诺)。

【讨论】:

    【解决方案4】:

    如果您像我一样,您会注意到在许多情况下等待为 0 的 $timeout 在 DOM 真正稳定和完全静态之前运行得很好。当我希望 DOM 稳定时,我希望它是 stable 天哪。所以我遇到的解决方案是为“DOMSubtreeModified”事件在元素上设置一个观察者(或在整个文档下面的示例中)。一旦我等待了 500 毫秒并且没有 DOM 更改,我就会广播一个像“domRendered”这样的事件。

    IE:

       //todo: Inject $rootScope and $window, 
    
    
       //Every call to $window.setTimeout will use this function
       var broadcast = function () {};
    
       if (document.addEventListener) {
    
           document.addEventListener("DOMSubtreeModified", function (e) {
               //If less than 500 milliseconds have passed, the previous broadcast will be cleared. 
               clearTimeout(broadcast)
               broadcast = $window.setTimeout(function () {
                   //This will only fire after 500 ms have passed with no changes
                   $rootScope.$broadcast('domRendered')
               }, 500)
    
           });
    
       //IE stupidity
       } else {
           document.attachEvent("DOMSubtreeModified", function (e) {
    
               clearTimeout(broadcast)
               broadcast = $window.setTimeout(function () {
                   $rootScope.$broadcast('domRendered')
               }, 500)
    
           });
       }
    

    这个事件可以像所有广播一样被挂钩,就像这样:

    $rootScope.$on("domRendered", function(){
       //do something
    })
    

    【讨论】:

      【解决方案5】:

      我有一个自定义指令,我需要在我的指令中生成 elementheight() 属性,这意味着我需要在 angular 运行整个 $digest 并且浏览器流出布局后读取它。

      在我的指令的link函数中;

      这并不可靠,还不够晚;

      scope.$watch(function() {}); 
      

      这还不够晚;

      scope.$evalAsync(function() {});
      

      以下似乎有效(即使在 Chrome 上使用 0ms),奇怪的是即使 ẁindow.setTimeout()scope.$apply() 也没有;

      $timeout(function() {}, 0);
      

      闪烁是一个问题,所以最后我在我的指令中使用requestAnimationFrame() 并回退到$timeout(适当的供应商前缀)。简而言之,这基本上看起来像;

      scope.$watch("someBoundPropertyIexpectWillAlterLayout", function(n,o) {
          $window.requestAnimationFrame(function() {
              scope.$apply(function() {
                  scope.height = element.height(); // OK, this seems to be accurate for the layout
              });
          });
      });
      

      那我当然可以只用一个;

      scope.$watch("height", function() {
          // Adjust view model based on new layout metrics
      });
      

      【讨论】:

        【解决方案6】:

        间隔对我有用,例如:

        interval = $interval(function() {
            if ($("#target").children().length === 0) {
                return;
            }
            doSomething();
            $interval.cancel(interval);
        }, 0);
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2023-03-21
          • 2023-04-02
          • 2016-11-06
          • 1970-01-01
          • 2023-03-16
          • 1970-01-01
          • 2022-10-25
          相关资源
          最近更新 更多