ng-repeat 完成时调用函数

Posted

技术标签:

【中文标题】ng-repeat 完成时调用函数【英文标题】:Calling a function when ng-repeat has finished 【发布时间】:2013-02-18 21:48:47 【问题描述】:

我想要实现的基本上是一个“on ng 重复完成渲染”处理程序。我能够检测到它何时完成,但我不知道如何从中触发函数。

检查小提琴:http://jsfiddle.net/paulocoelho/BsMqq/3/

JS

var module = angular.module('testApp', [])
    .directive('onFinishRender', function () 
    return 
        restrict: 'A',
        link: function (scope, element, attr) 
            if (scope.$last === true) 
                element.ready(function () 
                    console.log("calling:"+attr.onFinishRender);
                    // CALL TEST HERE!
                );
            
        
    
);

function myC($scope) 
    $scope.ta = [1, 2, 3, 4, 5, 6];
    function test() 
        console.log("test executed");
    

html

<div ng-app="testApp" ng-controller="myC">
    <p ng-repeat="t in ta" on-finish-render="test()">t</p>
</div>

回答: 完成移动的工作小提琴:http://jsfiddle.net/paulocoelho/BsMqq/4/

【问题讨论】:

出于好奇,element.ready()sn-p 的目的是什么?我的意思是..它是您拥有的某种 jQuery 插件,还是应该在元素准备好时触发它? 可以使用 ng-init 等内置指令来实现 ng-repeat finish event的可能重复 ***.com/questions/13471129/… 的副本,在此处查看我的答案 【参考方案1】:

请看一下小提琴http://jsfiddle.net/yNXS2/。由于您创建的指令没有创建新的范围,我继续这样做。

$scope.test = function()... 做到了。

【讨论】:

错误的小提琴?与问题中的相同。 嗯,你更新小提琴了吗?它目前显示的是我的原始代码的副本。【参考方案2】:
var module = angular.module('testApp', [])
    .directive('onFinishRender', function ($timeout) 
    return 
        restrict: 'A',
        link: function (scope, element, attr) 
            if (scope.$last === true) 
                $timeout(function () 
                    scope.$emit(attr.onFinishRender);
                );
            
        
    
);

请注意,我没有使用.ready(),而是将其包装在$timeout 中。 $timeout 确保在 ng-repeated 元素真正完成渲染时执行它(因为 $timeout 将在当前摘要周期结束时执行 -- 并且它还将在内部调用 $apply,不像 @ 987654329@)。所以在ng-repeat 完成后,我们使用$emit 向外部作用域(兄弟作用域和父作用域)发出事件。

然后在您的控制器中,您可以使用$on 捕获它:

$scope.$on('ngRepeatFinished', function(ngRepeatFinishedEvent) 
    //you also get the actual event object
    //do stuff, execute functions -- whatever...
);

使用看起来像这样的 html:

<div ng-repeat="item in items" on-finish-render="ngRepeatFinished">
    <div>item.name<div>
</div>

【讨论】:

+1,但我会在使用事件之前使用 $eval —— 减少耦合。详情请参阅我的回答。 @PigalevPavel 我认为您将$timeout(基本上是setTimeout + Angular 的$scope.$apply())与setInterval 混淆了。 $timeout 将在每个 if 条件下仅执行一次,这将在下一个 $digest 循环的开始。有关 javascript 超时的更多信息,请参阅:ejohn.org/blog/how-javascript-timers-work @finishingmove 也许我不明白 :) 但请参阅我在另一个 ng-repeat 中有 ng-repeat。我想确定他们什么时候完成。当我将您的脚本与 $timeout 一起用于父 ng-repeat 时,一切正常。但是如果我不使用 $timeout,我会在孩子 ng-repeats 完成之前得到响应。我想知道为什么?我可以确定,如果我将您的脚本与 $timeout 一起用于父 ng-repeat,我将始终在所有 ng-repeat 完成后得到响应? 为什么你会使用像 setTimeout() 这样的 0 延迟是 another question 虽然我必须说我从来没有在任何地方遇到过浏览器事件队列的实际规范,这正是 single- 所暗示的线程和setTimeout()的存在。 感谢您的回答。我有一个问题,为什么我们需要分配on-finish-render="ngRepeatFinished"?当我分配on-finish-render="helloworld" 时,它的工作原理是一样的。【参考方案3】:

如果您希望在构造 DOM 之后但在浏览器呈现之前执行回调(即 test()),请使用 $evalAsync。这将防止闪烁 -- ref。

if (scope.$last) 
   scope.$evalAsync(attr.onFinishRender);

Fiddle.

如果你真的想在渲染后调用你的回调,使用 $timeout:

if (scope.$last) 
   $timeout(function()  
      scope.$eval(attr.onFinishRender);
   );

我更喜欢 $eval 而不是事件。对于一个事件,我们需要知道该事件的名称并将该事件的代码添加到我们的控制器中。使用 $eval,控制器和指令之间的耦合更少。

【讨论】:

检查$last 有什么作用?在文档中找不到。它被删除了吗? @ErikAigner, 如果ng-repeat 在元素上处于活动状态,则在作用域上定义$last 看起来您的 Fiddle 正在接收不必要的 $timeout 太棒了。这应该是公认的答案,从性能的角度来看,发射/广播成本更高。【参考方案4】:

其他解决方案在初始页面加载时可以正常工作,但从控制器调用 $timeout 是确保在模型更改时调用您的函数的唯一方法。这是一个使用 $timeout 的工作 fiddle。对于您的示例,它将是:

.controller('myC', function ($scope, $timeout) 
$scope.$watch("ta", function (newValue, oldValue) 
    $timeout(function () 
       test();
    );
);

ngRepeat 只会在行内容是新的时评估指令,因此如果您从列表中删除项目,onFinishRender 将不会触发。例如,尝试在这些 fiddles emit 中输入过滤器值。

【讨论】:

同理,当模型发生变化时,evalAsynch 解决方案中并不总是调用 test() fiddle ta 中的$scope.$watch("ta",... 是什么?【参考方案5】:

到目前为止给出的答案只会在ng-repeat 被渲染时第一次起作用,但是如果你有一个动态的ng-repeat,这意味着你将要添加/deleting/filtering items,并且每次ng-repeat被渲染时都需要通知你,这些解决方案对你不起作用。

所以,如果您每次都需要通知您 ng-repeat 被重新渲染,而不仅仅是第一次,我已经找到了一种方法来做到这一点,这很“hacky” ',但如果你知道自己在做什么,它会很好地工作。在你的ng-repeat 中使用这个$filter在你使用任何其他$filter之前

.filter('ngRepeatFinish', function($timeout)
    return function(data)
        var me = this;
        var flagProperty = '__finishedRendering__';
        if(!data[flagProperty])
            Object.defineProperty(
                data, 
                flagProperty, 
                enumerable:false, configurable:true, writable: false, value:);
            $timeout(function()
                    delete data[flagProperty];                        
                    me.$emit('ngRepeatFinished');
                ,0,false);                
        
        return data;
    ;
)

每当ng-repeat 被渲染时,这将$emit 一个名为ngRepeatFinished 的事件。

使用方法:

<li ng-repeat="item in (items|ngRepeatFinish) | filter:name:namedFiltered" >

ngRepeatFinish 过滤器需要直接应用于ArrayObject 在您的$scope 中定义,您可以在之后应用其他过滤器。

如何不使用它:

<li ng-repeat="item in (items | filter:name:namedFiltered) | ngRepeatFinish" >

不要先应用其他过滤器,然后再应用ngRepeatFinish 过滤器。

我应该什么时候使用这个?

如果您想在列表完成渲染后将某些 css 样式应用到 DOM 中,因为您需要考虑由 ng-repeat 重新渲染的 DOM 元素的新尺寸。 (顺便说一句:这类操作应该在指令中完成)

在处理ngRepeatFinished 事件的函数中不要做的事情:

不要在该函数中执行$scope.$apply,否则您会将 Angular 置于 Angular 无法检测到的无限循环中。

不要使用它来更改 $scope 属性,因为这些更改在下一个 $digest 循环之前不会反映在您的视图中,并且由于您无法执行 $scope.$apply 他们不会'没有任何用处。

“但过滤器不应该那样使用!!”

不,它们不是,这是一个 hack,如果您不喜欢它,请不要使用它。如果你知道更好的方法来完成同样的事情,请告诉我。

总结

这是一个 hack,以错误的方式使用它是危险的,仅用于在 ng-repeat 完成渲染后应用样式,您应该没有任何问题。

【讨论】:

感谢 4 小费。无论如何,非常感谢有一个工作示例的 plunkr。 不幸的是,这对我不起作用,至少对于最新的 AngularJS 1.3.7 而言。所以我发现了另一种解决方法,不是最好的,但如果这对你很重要,它会起作用。我所做的是每当我添加/更改/删除元素时,我总是在列表末尾添加另一个虚拟元素,因此由于$last 元素也被更改,通过检查$last 的简单ngRepeatFinish 指令现在将起作用。一旦调用 ngRepeatFinish,我就会删除虚拟项目。 (我也用 CSS 隐藏它,所以它不会短暂出现) 这个技巧很好,但并不完美;每次过滤器启动时,都会分派五次:-/ Mark Rajcok 下面的那个在每次重新渲染 ng-repeat 时都能完美运行(例如,由于模型更改)。所以不需要这个 hacky 解决方案。 @Josep 你是对的 - 当项目被拼接/取消移动(即数组被突变)时,它不起作用。我之前的测试可能是使用重新分配的数组(使用sugarjs),因此它每次都有效......感谢您指出这一点【参考方案6】:

使用过滤的 ngRepeat 解决此问题的方法可能是使用 Mutation 事件,但它们已被弃用(没有立即替换)。

然后我想到了另一个简单的方法:

app.directive('filtered',function($timeout) 
    return 
        restrict: 'A',link: function (scope,element,attr) 
            var elm = element[0]
                ,nodePrototype = Node.prototype
                ,timeout
                ,slice = Array.prototype.slice
            ;

            elm.insertBefore = alt.bind(null,nodePrototype.insertBefore);
            elm.removeChild = alt.bind(null,nodePrototype.removeChild);

            function alt(fn)
                fn.apply(elm,slice.call(arguments,1));
                timeout&&$timeout.cancel(timeout);
                timeout = $timeout(altDone);
            

            function altDone()
                timeout = null;
                console.log('Filtered! ...fire an event or something');
            
        
    ;
);

这挂钩到父元素的 Node.prototype 方法,并带有一个 $timeout 以观察后续修改。

它的工作原理大部分是正确的,但我确实遇到了一些 altDone 会被调用两次的情况。

再次...将此指令添加到 ngRepeat 的 parent

【讨论】:

这是迄今为止效果最好的,但并不完美。例如,我从 70 个项目变为 68 个,但它没有触发。 在上面的代码中也可以添加appendChild(因为我只使用了insertBeforeremoveChild)。【参考方案7】:

如果您需要在同一个控制器上为不同的 ng-repeats 调用不同的函数,您可以尝试如下操作:

指令:

var module = angular.module('testApp', [])
    .directive('onFinishRender', function ($timeout) 
    return 
        restrict: 'A',
        link: function (scope, element, attr) 
            if (scope.$last === true) 
            $timeout(function () 
                scope.$emit(attr.broadcasteventname ? attr.broadcasteventname : 'ngRepeatFinished');
            );
            
        
    
);

在您的控制器中,使用 $on 捕获事件:

$scope.$on('ngRepeatBroadcast1', function(ngRepeatFinishedEvent) 
// Do something
);

$scope.$on('ngRepeatBroadcast2', function(ngRepeatFinishedEvent) 
// Do something
);

在您的模板中使用多个 ng-repeat

<div ng-repeat="item in collection1" on-finish-render broadcasteventname="ngRepeatBroadcast1">
    <div>item.name<div>
</div>

<div ng-repeat="item in collection2" on-finish-render broadcasteventname="ngRepeatBroadcast2">
    <div>item.name<div>
</div>

【讨论】:

【参考方案8】:

很简单,我就是这样做的。

.directive('blockOnRender', function ($blockUI) 
    return 
        restrict: 'A',
        link: function (scope, element, attrs) 
            
            if (scope.$first) 
                $blockUI.blockElement($(element).parent());
            
            if (scope.$last) 
                $blockUI.unblockElement($(element).parent());
            
        
    ;
)

【讨论】:

如果你有自己的 $blockUI 服务,这段代码当然可以工作。【参考方案9】:

如果您不反对使用双美元范围道具,并且您正在编写一个唯一内容是重复的指令,那么有一个非常简单的解决方案(假设您只关心初始渲染)。在链接函数中:

const dereg = scope.$watch('$$childTail.$last', last => 
    if (last) 
        dereg();
        // do yr stuff -- you may still need a $timeout here
    
);

这对于您有一个指令需要根据渲染列表成员的宽度或高度进行 DOM 操作的情况很有用(我认为这是最有可能提出这个问题的原因),但它是不像已经提出的其他解决方案那样通用。

【讨论】:

【参考方案10】:

我很惊讶在这个问题的答案中没有看到最简单的解决方案。 您要做的是在重复元素(带有ngRepeat 指令的元素)上添加一个ngInit 指令,检查$lastngRepeat 在范围内设置的特殊变量,表示重复元素是最后在列表中)。如果$last 为真,我们正在渲染最后一个元素,我们可以调用我们想要的函数。

ng-init="$last && test()"

HTML 标记的完整代码是:

<div ng-app="testApp" ng-controller="myC">
    <p ng-repeat="t in ta" ng-init="$last && test()">t</p>
</div>

除了要调用的作用域函数(在本例中为test)之外,您的应用程序中不需要任何额外的 JS 代码,因为 ngInit 由 Angular.js 提供。只需确保您的 test 函数在范围内,以便可以从模板访问它:

$scope.test = function test() 
    console.log("test executed");

【讨论】:

以上是关于ng-repeat 完成时调用函数的主要内容,如果未能解决你的问题,请参考以下文章

ng-repeat 完成事件

如何在 Swift Playground 中完成歌曲时调用函数

为啥 Promise 构造函数需要一个在完成时调用 'resolve' 的函数,但 'then' 不需要 - 它返回一个值?

PHP:每当调用子类中的函数时调用函数

在按下完成或uitextfield的返回键时调用函数

在Node js中完成for循环时调用函数