本文基于0.58.5分析React Native Reconciliation过程
Components、Elements和Instances
讲Virtual DOM之前,先讲下React Native几个核心概念和这样设计的目的。在面向对象的UI开发时,要渲染一个UI时,都要自己创建UI对象,并且管理对象引用,例如iOS上要渲染一个UIView:
1 | UIView *view= [UIView new]; |
这种设计模式带来的问题是开发者必须自己创建、更新UI对象,当UI复杂时,维护成本会急剧增加。
React使用一种非常巧妙的设计模式来解决上面问题,UI开发时,只需要描述UI界面,引擎会根据描述自动创建具体的实例,在更新时也只需更新UI界面描述。这样开发者就从复杂的UI对象创建、更新中解放出来,开发者只需要关注UI长怎样和核心逻辑,React帮你搞定对象创建和维护,React通过Components、Elements和Instances来实现这种模式。
Elements
Element是用来描述UI的js对象,Element只有type和props两个属性,Element创建成本很低,一旦创建就不可变,Component更新到时候会创建新的Element。Element可以嵌套,React会递归解析Element直到叶子结点,这样就得到一颗Element树,这颗树就是Virtual DOM树,React通过diff等算法后把Virtual DOM渲染到屏幕,渲染过程做了很多优化。可以通过render()或者其他方法返回Element,例如:
1 | render() { |
上述JSX语法最终会转换成以下Element:
1 | { |
Components
Component是生成Element的对象,可以是个class,也可以是简单的方法。当Component是class的时候,可以存储state和其他属性,实现复杂的逻辑;当Component是方法的时候,是不可变的Component,相当于只有render()方法的class Component。
Instances
Instance就是Component 实例,React Native开发过程不用自己管理Instance,React引擎自动创建并维护Instance,具体创建逻辑下文详细介绍。
Virtual DOM
通常所说的Virtual DOM是指相对于Real DOM的element树,Virtual DOM是最初React版本的说法,最开始React只是用在前端,引入Virtual DOM的概念是为了提升UI渲染性能,在UI变化的时候可以先比较Virtual DOM,只更新有变化的Real DOM。
Reconciliation
React之所以有这么好的渲染性能,主要是因为在UI变化的时候可以先比较Virtual DOM,只更新有变化的Real DOM,整个更新过程叫Reconciliation。
Fiber
React 16重构了Reconciliation 实现,新框架叫Fiber,Facebook团结花了两年时间实现Fiber,核心优化就是使用Vitual Stack的概念。Fiber之前更新UI的时候是通过同步递归的方式遍历Virtual DOM树,整个过程是同步的,并且在遍历结束之前无法中断,这样在动画的时候就可能导致卡顿。Fiber使用Vitual Stack的概念,把同步递归操作分解成一个个异步、可中断的操作单元,从而解决卡顿问题,并且随时可以取消不需要的操作。
UI更新
React Native UI更新主要可以分为以下两个阶段:
- Render
- Commit
Render过程计算新的element树,render()方法在这个阶段调用的;Commit过程调用diff算法,更新实际发生变化的UI。
Render 阶段核心方法调用顺序:
- componentWillReceiveProps
- getDerivedStateFromProps
- shouldComponentUpdate
- componentWillUpdate
- render()
- reconcileChildren()
Commit 阶段核心方法调用顺序:
- getSnapshotBeforeUpdate
- diff
- updateView
- componentDidUpdate
componentWillReceiveProps和componentWillUpdate已经废弃,不推荐使用了,使用getDerivedStateFromProps和getSnapshotBeforeUpdate代替
updateView方法调用RCTUIManager.updateView更新Native View。
Reconciliation源码分析
接下来以setState方法为切入点分析,分析React Native Reconciliation过程,主要分析UI更新过程,并不深入Fiber细节。
setState
1 | _callback() { |
上面代码是React Native上更新UI的最常用方法,我们知道setState是异步调用的,但state是什么时机更新?callback又什么时机调用呢?又是怎么触发Virtual DOM树和UI更新的呢?
1 | //react.development.js:333 |
1 | //ReactNativeRender-dev.js:8456 |
React可以用在Web、Node、React Native,底层updater指向具体实现,React Native上就是classComponentUpdater。
可以看到setState最终会创建一个update结构,其中payload就是更新state的匿名方法,然后插入队列,payload和callback将在后面异步执行。
element树更新
前文说过Render过程计算新的element树,render()方法在这个阶段调用的,先看一下函数调用栈:
通过函数名可以猜测更新过程会把异步处理批量更新,这样可以提高性能,接下来分析Render过程核心方法。
performWorkOnRoot
1 | //ReactNativeRender-dev.js:17168 |
performWorkOnRoot是UI更新的入口方法,React Native上isYieldy直接传的false。每次更新的时候都会从最顶端的节点开始计算新的element树,不管是哪个节点调的setState,但没变化的节点并不会重新计算,而是直接重用。但如果父节点发生变化,则所有字节的都会进行重新计算,而不管子节点是否变化,除非子节点shouldComponentUpdate返回false,或者子节点是PureReactComponent。
renderRoot就是Render阶段入口方法,completeRoot则是Commit阶段入口方法。
workLoop
1 | //ReactNativeRender-dev.js:16111 |
workLoop方法就是遍历整颗element树,React 16重构了Reconciliation 实现,新框架叫Fiber,Fiber使用Vitual Stack的概念,把同步递归操作分解成一个个异步、可中断的操作单元,每个操作单元就是一个节点计算过程。
performUnitOfWork就是具体节点计算,每次执行完会通过深度优先返回下一个需要执行的节点,这样就可以遍历整个节点树了。
performUnitOfWork
1 | //ReactNativeRender-dev.js:16049 |
performUnitOfWork主要就是调用beginWork方法,然后更新props。
beginWork
1 | //ReactNativeRender-dev.js:12601 |
可以看到beginWork主要是两个分支,每个分支都是一个很大switch case语句,第一个分支处理节点没变化的情况,这个时候不会进行计算,第二个分支处理节点发生变化的情况。每个switch case处理不同类型的节点,这里只分析ClassComponent类型。
最终会调用updateClassComponent方法更新发生变化的节点。
updateClassComponent
1 | //ReactNativeRender-dev.js:11457 |
updateClassComponent会判断Component是否实例化,如果没有实例化的话会创建Component实例,这也是前文说的React引擎自动创建Instance的时机,如果已经实例化则调用updateClassInstance更新实例,updateClassInstance会返回该实例是否真正需要更新,并更新props和state。最后会调用finishClassComponent更新element并返回下一个计算单元。
updateClassInstance
1 | //ReactNativeRender-dev.js:9282 |
updateClassInstance判断节点是否需要跟新,调用以下life-cycles方法:
- componentWillReceiveProps
- getDerivedStateFromProps
- shouldComponentUpdate
- componentWillUpdate
前文已经知道调用setState时会创建一个update结构,updateClassInstance 会调用processUpdateQueue方法计算新的state,processUpdateQueue方法里面会调用setState传的匿名函数
1 | //ReactNativeRender-dev.js:8517 |
updateClassInstance调用checkShouldComponentUpdate判断是否需要更新,checkShouldComponentUpdate实现比较简单,
- 如果Component实现shouldComponentUpdate方法,则调用shouldComponentUpdate;
- 如果是PureReactComponent,则调用shallowEqual比较props和state是否变化;
- 否则返回true
这里要注意checkShouldComponentUpdate默认返回true,所以只要父节点更新,默认就会更新所有子节点,这就是为什么可以通过shouldComponentUpdate返回false或使用PureReactComponent来提升性能。
finishClassComponent
Render阶段最后就是调用finishClassComponent方法计算新的element,并且调用reconcileChildren遍历子节点,具体代码如下:
1 | //ReactNativeRender-dev.js:11562 |
如果shouldUpdate为false,则直接重用现有节点,跟beginWork处理没变化的节点一样。
如果shouldUpdate为true,则调用render方法计算新的element,然后调用reconcileChildren遍历子节点。
completeUnitOfWork
在遍历到叶子节点后performUnitOfWork会调用completeUnitOfWork
1 | //ReactNativeRender-dev.js:16101 |
completeUnitOfWork调用completeWork标记需要更新的节点,如果有兄弟节点则返回兄弟节点,继续遍历兄弟节点,否则标记父节点。
Commit 阶段源码分析
前文分析来Render阶段核心方法,Render阶段会生成一颗新的element树,并且生成一个Effect list,Effect list是一个线性列表,包含真正需要更新的操作,Commit 阶段则通过Effect list更新具体的UI,首先看下Commit 阶段的函数调用栈:
commitAllHostEffects
1 | //ReactNativeRender-dev.js:15349 |
commitAllHostEffects源码比较好理解,循环执行effect操作,effect操作可能是替换、删除、更新等
commitWork
更新操作会调用commitWork,源码如下:
1 | //ReactNativeRender-dev.js:14628 |
React Native上Component都是由View、Text等基础Component组成的,所以最终实际更新的是View、Text等基础Component,最后会调用commitUpdate和commitTextUpdate完成实际的更新操作。
commitUpdate
1 | //ReactNativeRender-dev.js:4153 |
commitUpdate源码比较好理解,首先调用diff判断是否需要更新,如果需要更新的话调用UIManager.updateView更新Native UI,其中UIManager.updateView是Native 暴露的module。diff方法主要是比较props是否变化:
1 | //ReactNativeRender-dev.js:3638 |
commitTextUpdate
1 | //ReactNativeRender-dev.js:14659 |
commitTextUpdate实现比较简单,因为completeWork方法标记TextComponent的时候已经判断了text是否变化,所以直接调用UIManager.updateView。
life-cycles方法调用
在更新完UI后将调用componentDidUpdate方法和setState callback方法,具体调用栈如下:
总结
React Native Reconciliation过程比较复杂,Fiber框架把递归操作分解成一个个异步、可中断的操作单元后进一步复杂度。
Reconciliation主要可以分为以下两个阶段:
- Render
- Commit
Render阶段从根节点开始遍历element树,对于不需要更新的节点直接重用fiber node,对于需要更新的节点,调用life-cycle方法,然后调用render方法计算新的element,最后调用reconcileChildren遍历子节点。
Render阶段还会标记更新,并且生成一个Effect list,Effect list是一个线性列表,包含真正需要更新的操作。
Commit 阶段通过Effect list更新具体的UI,这阶段会调用diff,然后调用UIManager.updateView更新Native View,最后调用componentDidUpdate方法和setState callback方法。
#引用
Inside Fiber: in-depth overview of the new reconciliation algorithm in React
React Fiber Architecture
React Components, Elements, and Instances
Reconciliation