从错误边界到回滚到MWI

从错误边界到回滚到MWI

React 16早期版本公开的三大特征之一,错误边界,简单来说,就是存在两种组件:错误组件与边界组件。

错误组件就是发生错误的组件,即使它本身就有componentDidCatch,这钩子也不会被唤起,这情况就像理发师不能给自己理发一样(本来我想说医生不能医自己一样,但总会有人抬杠!)

边界组件就是错误组件上方的拥有componentDidCatch的组件。componentDidCatch有两个参数,出错时throw出来的东西与一个包含位置信息的对象。componentDidCatch用来给你设置新的state,然后下一个周期中,再从getDerivedStateFromCatch得到新的props。因为不知道是 state还是props引发错误,因此让你拿到全新的state/props来做渲染。


然而事实远比眼看到的复杂。比如一个组件,它render了一大堆组件,出错了,上方很难只针对某个组件进行修复,唯一方法就是全部清空,把所有子组件移除。这时有的子组件已经将元素插入DOM 树了,也需要将它拔出来。这就涉及本文所说的回滚技术了。React16相当于把git add, git revert, git commit都实现了。


继续刚才拔出子组件元素的话题,那么我们拔掉的元素是新子组件呢?还是旧子组件呢?这可能让读者懵逼了。想像有一个Parent组件,它render了p, a, p三个子组件。由于type都是元素标签,亦即源码所说的原生组件,这种组件基本上不会出错。然后Parent 组件setState, 这次render了p, a, A, span 4个组件。虽然当中的p, a的类型相同,我们可以重用它的一部分部件,即里面的stateNode。(stateNode在原生组件里就是一个元素节点,在合成组件里为类的实例。)但是A抛错了。幸好我们的Parent是一个边界组件,于是需要回滚,将自己的狀态回滚到没出错前,即props, state为旧的狀态,孩子也是旧的,只有p, a, p。于是我们得随时保存一份副本,官方称之为alternate。


alternate,备胎,替死鬼,替身演员。你也可以视它为git的开发分支,稳定没错的那个则是master。每次 setState时,组件实例stateNode上有一个_reactInternalFiber的对象,就是它的master,立即复制一个用来踩雷开路的alternate对象。由于React16又使用另一种时间分片更新技术,这就要求它每次只会diff一部分子组件。子组件一出错,就回滚,试验新的子组件。这样有节制的更新每一级孩子。加之,React在开发环境下的调试消息是非常完备的,因此带来的开发体验超级爽快。

如果一个边界组件里面有两个孩子出错呢,这个边界组件会执行两次componentDidCatch。如果也出三次,就执行三次呗。它是把捕捉到的错误与位置信息对象放到一个数组进行for循环。但只能循环这一次,如果再次出错,这个边界组件就废了,它会自杀,执行componentWillUnmount,让父级的边界组件来处理。


子组件视其出错时机,也有不同的处理。我们知道React16现在是分为两个更新阶段,reconcile与commit。


reconcile 位于树的DFS 过程,这时发生beginWork操作与finishWork操作,beginWork会进行实例化,调用compoentWillMount等轻量钩子,finishWork就是复制替身,产生子组件,diff孩子(孩子本身没有实例化)。这时如果出错,由于孩子没有插入DOM,直接回滚就是。


commit位于批量更新的方法内部(unstable_batchedUpdates),这个在React15是通过一个非常难懂的事务机制实现的。commit会执行一些操作DOM 的方法,一些重型钩子,ref回调。这时出错就不得了,首先新孩子全部不要,旧孩子全部执行移除操作,如移出DOM, ref null, willUnmount。这些操作有时会在边界组件执行完componentDidMount/Update 后执行,有时会在componentDidCatch时执行,这是基于什么安排呢,我暂时也摸不着头脑。componentDidCatch的单元测试还没有ref出错的case……有许多case,React 16.3.2也没有跑通,注释里的片言只声,很难让人窥见全貌。


不过怎么样,现在我们知道Fiber对象不单是添加了return, child, sibling表示亲戚关系的属性,它本身就是一个连体婴。替身有alternate 属性连着本身,本身有alternate连着替身。并且每次更新时,都会产生一个firstEffect 或 firstEffect连着的另一个effect。这些effect对象也是一个个替身。由于它们都是位于unstable_batchedUpdates的setState中产生,可以在同一个时间内存在多个, 就像多元宇宙,requestIdleCallback则能让它们收束成同一宇宙。MWI(世界线理论或平行宇宙)听起来好玄,但是理论基础很硬。量子坍缩时世界按量子概率分叉到了多个世界。同样的,也可能存在坍缩的逆过程,不同的世界线收束到同一根线上。薛定谔的喵的不确定性,搞傻了爱因斯坦。编程时,我们无法确定子组件的出错,到底有多少个组件出错。 每次setState都可能出错,因此分离出替身来踩雷。最后,它们全部折叠起来,形成最终态。加之,React core team的当家人是redux的作者Dan dbramov,最爱时间旅行这一套了。Promise是目前JS 异步编程的基本范式,但对于不确定的出错处理还是弱智了一点,因此他们最终没有用Promise,也没有用基于microtasks的nextTick,而是更好探知浏览器空闲狀态的requestIdleCallback。

扯远了,这显然这会很吃内存,于是React16特别青睐于链表结构。链表在内存里不是连续的,动态分配,增删方便,轻量化(不像数组那样带一大堆方法)。React16以后也一定出新的最佳实践,鼓励使用异步更新。现在React16是使用批量更新来减少对DOM的操作,缺点是同一时间产生太多替身。

============

版权所有: 司徒正美,转载时保留

广告时间,正在努力开发基于Fiber架构的anujs RubyLouvre/anu

编辑于 2018-05-07