zoukankan      html  css  js  c++  java
  • Ionic中不合理的view层级导致afterEnter没有被调用

    文章原链接:http://blog.csdn.net/zzxiang1985/article/details/66970321

    在公司的ionic项目中我们定义了如下状态:

    [javascript] view plain copy
     
    1. $stateProvider  
    2.   .state('A', {  
    3.     abstract: true,  
    4.     views: {  
    5.       root: {  
    6.         template: '<ion-nav-view id="ViewA"></ion-nav-view>'  
    7.       }  
    8.     }  
    9.   })  
    10.   .state('A.B', {  
    11.     url: '/A/B',  
    12.     templateUrl: 'A/B.tpl.html',  
    13.     controller: 'ABCtrl'  
    14.   })  
    15.   .state('A.C', {  
    16.     abstract: true,  
    17.     url: '/A/C'  
    18.   })  
    19.   .state('A.C.D', {  
    20.     url: '/D',  
    21.     views: {  
    22.       'root@': {  
    23.         templateUrl: 'A/C/D.tpl.html',  
    24.         controller: 'ACDCtrl'  
    25.       }  
    26.     }  
    27.   })  
    28.   .state('E', {  
    29.     url: '/E',  
    30.     views: {  
    31.       root: {  
    32.         templateUrl: 'E.tpl.html'  
    33.       }  
    34.     }  
    35.   })  

    其中views里面的root是在index.html里定义的ion-nav-view:

    [html] view plain copy
     
    1. <html>  
    2.   ...  
    3.   <body ng-app="starter">  
    4.     ...  
    5.     <ion-nav-view name="root"></ion-nav-view>  
    6.   </body>  
    7. </html>  

    并且ABCtrl和ACDCtrl的代码中都注册监听了afterEnter事件。

    按理说从状态A.B跳转到状态A.C.D时,ACDCtrl里的afterEnter会被执行,可实际运行的时候却没有。但是从E跳转到A.C.D则没有问题,ACDCtrl里的afterEnter会如期被调用。从E跳到A.B也没有问题,ABCtrl里的afterEnter也会执行。

    公司项目的ionic lib版本是1.3.1:

    [plain] view plain copy
     
    1. $ ionic lib  
    2. Local Ionic version: 1.3.1 (/Users/zhixiangzhu/my-ionic-project/www/lib/ionic/version.json)  
    3. Latest Ionic version: 1.3.3 (released 2017-02-24)  
    4.  * Local version is out of date  

    本文末尾附上了我自己写的一个ionic小项目专用于重现这个问题。该项目的ionic lib版本是1.3.3:

    [plain] view plain copy
     
    1. $ ionic lib  
    2. Local Ionic version: 1.3.3 (/Users/zhixiangzhu/ionic-afterEnter-test/www/lib/ionic/version.json)  
    3. Latest Ionic version: 1.3.3 (released 2017-02-24)  
    4.  * Local version up to date  

    于是我钻进了ionic的代码里研究了一番。afterEnter是在ionicViewSwitcher的transitionComplete函数中,也就是在状态跳转完成时触发的:

    [javascript] view plain copy
     
    1. function transitionComplete() {  
    2.   ...  
    3.   // the most recent transition added has completed and all the active  
    4.   // transition promises should be added to the services array of promises  
    5.   if (transitionId === transitionCounter) {  
    6.     ...  
    7.     // emit that the views have finished transitioning  
    8.     // each parent nav-view will update which views are active and cached  
    9.     switcher.emit('after', enteringData, leavingData);  // ionic在这里触发afterEnter            
    10.     ...  
    11.   }  
    12.   ...  
    13. }  


    可以看到afterEnter触发的条件是transitionId === transitionCounter。ACDCtrl的afterEnter没有被调用,正是因为这个条件没有被满足。

    于是需要理解transitionId和transitionCounter分别是什么。两者的定义在如下代码中:

    [javascript] view plain copy
     
    1. IonicModule.factory('$ionicViewSwitcher', [  
    2. ...,  
    3. function(...) {  
    4.   ...  
    5.   var transitionCounter = 0;  
    6.   ...  
    7.   
    8.   var ionicViewSwitcher = {  
    9.   
    10.     create: function(navViewCtrl, viewLocals, enteringView, leavingView, renderStart, renderEnd) {  
    11.       // get a reference to an entering/leaving element if they exist  
    12.       // loop through to see if the view is already in the navViewElement  
    13.       var enteringEle, leavingEle;  
    14.       var transitionId = ++transitionCounter;  


    可以看到transitionCounter是一个全局变量。状态跳转时每创建一次ionViewSwitcher,transitionCounter计数就会加1。上面代码里的create函数是从ionNavView的$stateChangeSuccess响应函数一路调用进来的。

    [javascript] view plain copy
     
    1. IonicModule  
    2. .directive('ionNavView', [  
    3.   ...,  
    4. function(...) {  
    5.   // IONIC's fork of Angular UI Router, v0.2.10  
    6.   // the navView handles registering views in the history and how to transition between them  
    7.   return {  
    8.     ...  
    9.     // listen for $stateChangeSuccess  
    10.     $scope.$on('$stateChangeSuccess', function() {  
    11.       updateView(false);  
    12.     });  
    13.     ...  
    14.     function updateView(firstTime) {  
    15.       // get the current local according to the $state  
    16.       var viewLocals = $state.$current && $state.$current.locals[viewData.name];  
    17.   
    18.       // do not update THIS nav-view if its is not the container for the given state  
    19.       // if the viewLocals are the same as THIS latestLocals, then nothing to do  
    20.       if (!viewLocals || (!firstTime && viewLocals === latestLocals)) return;  
    21.   
    22.       // update the latestLocals  
    23.       latestLocals = viewLocals;  
    24.       viewData.state = viewLocals.$$state;  
    25.   
    26.       // register, update and transition to the new view  
    27.       navViewCtrl.register(viewLocals);  // ionicViewSwitcher的create函数是从这里一路调用进去的  
    28.     }  
    29.     ...  


    而$stateChangeSuccess事件是在状态跳转完成时在$rootScope上广播触发的:

    [javascript] view plain copy
     
    1. var transition = $state.transition = resolved.then(function () {  
    2.   ...  
    3.   $rootScope.$broadcast('$stateChangeSuccess', to.self, toParams, from.self, fromParams);  
    4.   ...  


    经过一番研究,我发现故事正是可以从$stateChangeSuccess事件开始讲起。


    我们先以状态E跳转到状态A.B这个没有出现问题的流程为例。

    在状态跳转成功,也就是$stateChangeSuccess在$rootScope上广播触发的时候,其实跳转目标状态(A.C.D)和目标状态的祖先状态(A.C和A)对应的view还没有创建好,或是还处于非活跃状态。此时以$rootScope为根结点的scope树可以简化如下:

    [plain] view plain copy
     
    1. ion-nav-view name="root"  
    2.  |  
    3.  |-------- ion-view state="E"  


    $broadcast的算法是深度遍历,所以首先被遍历到的是位于根部的名为root的ion-nav-view。当$stateChangeSuccess在ion-nav-view的$scope上触发时,ionic会检查当前的ion-nav-view是否跳转目标状态或其祖先状态的其中任意一个view所在的容器。如果不是,那么ionic就会跳过这个ion-nav-view,遍历下一个。见上文updateView函数中的注释:

    // do not update THIS nav-view if its is not the container for the given state

    但是在E -> A.B这个例子中,ion-nav-view name="root"是状态A的view所在的容器。因此ionic会在当前的ion-nav-view中创建或唤醒相应状态(A)的view,并将transitionCounter计数器加1,赋值给相应view的transitionId。因为在状态A的定义中,A的view本身也含有一个ion-nav-view,所以现在scope树变成了这样(假设状态跳转之前transitionCounter为0):

    [plain] view plain copy
     
    1. ion-nav-view name="root"  
    2.   |  
    3.   |-------- ion-view state="E"  
    4.   |  
    5.   |-------- ion-nav-view id="ViewA"   transitionId = 1   


    当新的ion-nav-view被创建的时候,它对自身也会执行一次updateView的流程,判断自己是否为目标状态或目标祖先状态的任意一个view所在的容器。在这里ion-nav-view id="ViewA"是A.B的view所在的容器,因此它会在自己的view中创建A.B的view,并再次增加transitionCounter计数,赋值给A.B的view的transitionId。此时的scope树如下所示:

    [plain] view plain copy
     
    1. ion-nav-view name="root"  
    2.   |  
    3.   |-------- ion-view state="E"  
    4.   |  
    5.   |-------- ion-nav-view id="ViewA"   transitionId = 1   
    6.                   |  
    7.                   |-------- ion-view view-title="A.B"    transitionId = 2  


    由于A.B的view中没有ion-nav-view(详见文章末尾附件中的代码),且scope树中已没有未遍历的ion-nav-view,所以$stateChangeSuccess的广播到此结束。此时transitionCounter的值为2,而transitionId为2的正是A.B的view,于是在transitionComplete的时候afterEnter在ABCtrl上被触发。

    上面的过程可以小结如下:在状态跳转成功的时候,ionic在$rootScope上广播$stateChangeSuccess事件,从scope树的根节点开始按深度遍历所有的ion-nav-view。如果当前正在遍历的ion-nav-view是目标状态或其祖先状态的view所在的容器,那么就会在其中创建或唤醒相应状态的view,增加transitionCounter计数并赋值view的transitionId。在$stateChangeSuccess广播完成之后,ionic会在transitionId最大(即等于transitionCounter)的view上,也就是最后创建或唤醒的view上触发afterEnter。

    那么从状态A.B跳转到状态A.C.D时,为什么没有在ACDCtrl上触发afterEnter呢?让我们跟踪一下这个过程。

    在A.B -> A.C.D的$stateChangeSuccess广播之前,scope树是这样的:

    [plain] view plain copy
     
    1. ion-nav-view name="root"  
    2.   |  
    3.   |-------- ion-nav-view id="ViewA"   
    4.                   |  
    5.                   |-------- ion-view view-title="A.B"   


    和之前一样,首先遍历到的是ion-nav-view name="root"。这里要注意,在A.C.D及其祖先状态中,以root为容器的既有状态A的view,又有状态A.C.D的view(见A.C.D定义的views)。在决定在ion-nav-view中创建或唤醒哪个状态的view这个问题上,ionic会优先考虑子状态的view。所以在ion-nav-view name="root"中,ionic只会创建A.C.D的view,而不会创建A的view。

    *注:如果想深究这个优先级是如何实现的话,可研究ionic的transitionTo函数中的如下代码:

    [javascript] view plain copy
     
    1. ...  
    2. // We also set up an inheritance chain for the locals here. This allows the view directive  
    3. // to quickly look up the correct definition for each view in the current state.  
    4. ...  
    5. for (var l = keep; l < toPath.length; l++, state = toPath[l]) {  
    6.   locals = toLocals[l] = inherit(locals);  
    7.   resolved = resolveState(state, toParams, state === to, resolved, locals, options);  
    8. }  


    从中可见ionic是通过javascript的继承与原型链实现这种优先级的,子状态的view数据(locals)作为子类覆盖了父状态的数据。

    于是scope树变成了这样(假设状态跳转之前transitionCounter为0): 

    [plain] view plain copy
     
    1. ion-nav-view name="root"  
    2.   |  
    3.   |-------- ion-nav-view id="ViewA"   
    4.   |               |  
    5.   |               |-------- ion-view view-title="A.B"   
    6.   |  
    7.   |-------- ion-view view-title="A.C.D"   transitionId = 1  


    接下来注意了!A.C.D的view创建之后,遍历还没结束。scope树里还有一个$stateChangeSuccess广播之前就存在的ion-nav-view id="ViewA",而它正好是目标状态A.C.D的父状态A.C的view的容器!因此ionic会继续在ion-nav-view id="ViewA"上创建A.C的view:

    [plain] view plain copy
     
    1. ion-nav-view name="root"  
    2.   |  
    3.   |-------- ion-nav-view id="ViewA"   
    4.   |               |  
    5.   |               |-------- ion-view view-title="A.B"   
    6.   |               |  
    7.   |               |-------- div transitionId = 2  (这是A.C的view。因为A.C没有定义template,所以它的view只是一个空的div。)  
    8.   |  
    9.   |-------- ion-view view-title="A.C.D"   transitionId = 1  


    这时可以发现,最大的transitionId已经不是A.C.D的view,而是A.C的view了。这就是为什么ACDCtrl上没有触发afterEnter的原因。(注:这样说下来,按照流程afterEnter似乎会在A.C的view上触发,但实际上也没有。这是因为afterEnter的触发除了transitionId的判断以外,还有其它更多条件,这些就不在本文中阐述了。)

    为什么从状态E跳转到状态A.C.D又没问题呢?因为这种情况下在A.C.D的view创建之后,scope树如下:

    [plain] view plain copy
     
    1. ion-nav-view name="root"  
    2.   |  
    3.   |-------- ion-view view-title="E"  
    4.   |  
    5.   |-------- ion-view view-title="A.C.D"   transitionId = 1  


    这时scope树中已经没有其它还未遍历的ion-nav-view了,遍历到此结束。此时transitionId最大的正是A.C.D的view,因此afterEnter也就在ACDCtrl上触发。其实即便scope树中还有其它未遍历的ion-nav-view,只要它们不是A、A.C或A.C.D的容器,那么它们之中就不会创建或唤醒新的view,transitionCounter也就不会增大,afterEnter也还是会在ACDCtrl上触发。

    上面阐述的整个遍历和创建view的过程都发生在ionic.bundle.js的transitionTo函数中。这个函数在跳转开始前会调用resolveState记录下目标状态及其祖先状态的各个view需要的容器,然后在跳转成功后会广播$stateChangeSuccess事件,遍历scope树,在最后创建或唤醒的view上触发afterEnter。

    因此,如果我们希望A.B -> A.C.D时afterEnter能在ACDCtrl上触发,那么可以更改A.C.D的views定义,将view放在ion-nav-view id="ViewA"上:

    [javascript] view plain copy
     
    1. .state('A.C.D', {  
    2.     url: '/D',  
    3.     views: {  
    4.       '@A': {  // 'root@'改为'@A'  
    5.         templateUrl: 'A/C/D.tpl.html',  
    6.         controller: 'ACDCtrl'  
    7.       }  
    8.     }  
    9.   })  


    这样scope树遍历之后的结果如下:

    [plain] view plain copy
     
    1. ion-nav-view name="root"  
    2.   |  
    3.   |-------- ion-nav-view id="ViewA"   
    4.                   |  
    5.                   |-------- ion-view view-title="A.B"   
    6.                   |  
    7.                   |-------- ion-view view-title="A.C.D"  transitionId = 1  (A.C.D的view覆盖了A.C的view)  

    transitionId最大的就是A.C.D的view——实际上也只创建了这一个新view,于是afterEnter就会在ACDCtrl上触发。

    或者也可以给A.C的view添加一个ion-nav-view:

    [javascript] view plain copy
     
    1. .state('A.C', {  
    2.     abstract: true,  
    3.     url: '/A/C',  
    4.     template: '<ion-nav-view></ion-nav-view>'  
    5.   })  


    然后将A.C.D的view放在A.C的view中:

    [javascript] view plain copy
     
    1. .state('A.C.D', {  
    2.     url: '/D',  
    3.     views: {  
    4.       '@A.C': {  
    5.         templateUrl: 'A/C/D.tpl.html',  
    6.         controller: 'ACDCtrl'  
    7.       }  
    8.     }  
    9.   })  


    这样scope树遍历之后的结果如下:

    [plain] view plain copy
     
    1. ion-nav-view name="root"  
    2.   |  
    3.   |-------- ion-nav-view id="ViewA"   
    4.                   |  
    5.                   |-------- ion-view view-title="A.B"   
    6.                   |  
    7.                   |-------- ion-nav-view  transitionId = 1  (A.C的view)  
    8.                                   |  
    9.                                   |--------- ion-view view-title="A.C.D"  transitionId = 2  


    transitionId最大的仍然是A.C.D的view。

    注:要控制一个view放在哪个ion-nav-view需要理解view的命名法则,参见ui-router的文档

    总而言之,如果我们希望在状态跳转时afterEnter在目标状态的view上触发,那么必须合理安排view的层级,以保证在scope树的深度遍历中,目标状态的view(而不是目标状态的祖先状态的view)是最后一个被创建或唤醒的view。

    不过我仍然不太理解为何ionic要用这种方法决定在哪个view上触发afterEnter。为何不在创建view的过程中记下目标状态的view,然后在跳转完成后直接在那个view上触发呢?

    最后附上专用于重现该问题的ionic小项目。下载项目解压后,在项目目录下执行ionic serve(需要本机安装ionic)即会弹出界面。初始状态是E,可以点击按钮在A.B和A.C.D之间跳转。

    点击这里下载专用于重现本文所述问题的ionic小项目

    本文在我的独立博客上的地址:https://zxtechart.com/2017/03/26/irrational-view-hierarchy-causes-afterenter-not-firing-in-ionic/

  • 相关阅读:
    职场篇:聚焦与复盘
    职场篇:直面情绪杀手【已补更】
    .NetCore实践篇:成功解决分布式监控ZipKin聚合依赖问题(三)
    职场篇:为何我们需要思想大洗礼?
    职场篇:从温水煮青蛙说起
    .NetCore实践篇:分布式监控系统zipkin踩坑之路(二)
    postman application/json;
    yapi 个人空间 这个分组的问题
    yapi 的分组的理解!
    yapi的安装
  • 原文地址:https://www.cnblogs.com/callmeguxi/p/7542186.html
Copyright © 2011-2022 走看看