logo
程序员LEON

像素主题

分类

分类

标签

标签

cover

渲染流程:Vue vs React

经验总结
book

阅读5分钟

timer

2023年01月04日 08:05

本文主要从渲染流程中对比Vue和React的原理和结构差异。其中Vue使用的是Vue2,React是React15。

架构

Vue

Vue中,每个组件都有一个Vue实例。渲染、挂载、卸载、子组件管理、响应式都在这个实例内进行。所有功能以实例为载体实现。

每个Vue实例内,通过render函数,得到VNode树并存储下来。在实例的渲染过程中,通过深度优先遍历+双端diff算法对比新旧VNode树,并patch相应的dom变更。对于子组件,要么实例化新的,要么从组件缓存中获取。子组件的渲染交给子组件自己去执行。从而实现整个组件树的渲染。

React

React中,渲染分为Reconciler(协调)和Renderer(渲染)。16.8版本之前使用的是Stack Reconciler。包含框架从ReactDom.render入口开始,深度遍历渲染整个组件树。React在渲染过程中,会将每个jsx节点都对应创建一个内部实例,包含组合组件(CompositeComponent)和宿主组件(HostComponent)两种类型。组合组件与<App />这样的React组件节点对应。宿主组件与<div />这样的原生节点对应。内部实例里包含了每个节点需要的上下文信息。

它的工作机制类似于函数调用栈,所以称为Stack Reconciler。包括mount、unmount、更新等,都是递归调用+深度优先。

宿主组件mount时,根据当前的jsx虚拟节点(element)。创建或更新对应的原生节点(node)。然后遍历children并挨个创建内部实例,得到子内部实例列表(renderedChildren)。再调用renderedChildren.forEach.mount()往深层执行。mount函数执行过程中,存储element、node和renderedChildren。函数最后return node。

组合组件mount时,根据当前的jsx虚拟节点(element)。如果是class组件则实例化,执行生命周期,执行render函数。如果是函数组件则无需实例化(本身即render函数)。执行render后得到组件根jsx节点,对应创建内部实例(renderedComponent)。组合组件无需遍历children,因为永远只有一个根节点。函数执行过程中存储element、renderedComponent。函数最后返回值为return renderedComponent.mount(),即继续执行子内部实例的mount函数,并取其返回值。这么看来,它最终返回的是离它深度最小的子宿主组件的node。执行过程中通知renderer挂载dom。

数据结构对比

现有如下的一个模版:

复制
<App>
    <h1>App标题</h1>
    <Detail />
</App>

// App组件:

<div>{children}</div>

// Detail组件:
<button />

在Vue里的存储结构为:

Vue实例结构
Vue render struct

图中每个实线框都是一个VNode,每个Vue组件对应一个Vue实例。

在vue中,父级实例包含子级实例:App实例包含一个div根级VNode,div下包含一个h1和一个Detail的VNode,其中Detail虚拟节点对应了Detail实例,Detail实例下又包含了一个button的VNode。

同样的模版,在React里的存储结构为:

React实例结构
React render struct

图中每个圆角框都是一个内部实例。组合组件总是有一个且只有一个renderedComponent,指向另一个内部实例。宿主组件的renderedChildren可能包含多个,指向其他的内部实例。此外,宿主组件会return dom节点元素。而组合组件会return第一个宿主组件的return值。

Diff更新

Vue

Vue使用深度优先,对每个实例下的VNode tree进行递归渲染,同一级的VNode trees使用双端diff算法进行最小化的差异对比,并同步操作真实dom树。

双端diff算法,使用四个指针,通过对新旧VNode数组的头头、尾尾、头尾、尾头的俩俩对比,匹配节点。这四种对比能覆盖80%以上的列表变更的场景。如果这四种都不满足,那会直接在旧数组中查找,如果还未查找到,则说明是新增节点。四指针遍历结束后,如果新数组还有剩余,则说明旧数组中找不到,要添加dom。如果旧数组还有剩余,则说明新数组中找不到,要删除dom。

React

React在首次挂载完后,便通过内部实例之间的相互引用,得到了一个内部实例的树。渲染更新时,由根内部实例,深度递归遍历,调用实例的receive方法。receive方法接收一个可以复用的新jsx节点(element),即key和type都相同。

在组合组件的receive方法中,如果是class组件,更新props,执行生命周期,并调用render方法;如果是函数组件,执行函数。得到子jsx虚拟节点。与旧的虚拟节点对比,如果key和type都一样,则可以复用子内部实例,调用子内部实例的receive方法,形成递归。如果不可复用,则重新创建子内部实例。并unmount旧的子内部实例,mount新的子内部实例。同时通知renderer卸载dom。

在宿主组件的receive方法中,首先更新props为新的element的props。接着开始遍历处理props.children。对比旧的子内部实例数组(prevRenderedChildren)和新的数组(nextRenderedChildren)。数组遍历中,找到可以复用的,也是直接调用该子内部实例的receive方法,往下递归,如果存在位置移动,添加一个“MOVE”操作到操作队列(operationQueue)里。如果找不到,则需要新增,添加一个“ADD”操作到操作队列里。如果旧数组存在而新数组不存在,则需要删除,添加一个“REMOVE”操作到操作队列里。最终遍历操作队列,通知renderer更新dom。

总结下来就是,组合组件每次渲染更新都执行render函数,并对比renderedComponent是否可以复用。宿主组件每次渲染更新,都更新props,对比props.children考虑renderedChildren中哪些可以复用。复用的则向下递归调用receive方法。不复用的则调用renderer更新dom。

简单版源码

本文的结构分析是基于我造轮子的VuelonReacteon

有兴趣的可以同时从npm包下载,直接在npmjs.com搜索包即可:Vuelon和Reacteon。

unko