Ionic中不合理的view层级导致afterEnter没有被调用

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

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

并且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:

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

于是我钻进了ionic的代码里研究了一番。

afterEnter是在ionicViewSwitcher的transitionComplete函数中,也就是在状态跳转完成时触发的:

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

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

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

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

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

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

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

$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):

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

由于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树是这样的:

和之前一样,首先遍历到的是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函数中的如下代码:

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

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

接下来注意了!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:

这时可以发现,最大的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树如下:

这时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”上:

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

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

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

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

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

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小项目

《Ionic中不合理的view层级导致afterEnter没有被调用》上有1条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注