Preact Virtual DOM算法探究

Preact简介


Preact是一个React轻量化的库,整个库大小只有3kb,相比其他react轻量化的方案,如25kb的react-lite,Preact在体积上有着很大的优势。
Preact有着和React相似的API,大部分情况下能兼容React的组件,只需要绑定一个preact-compat兼容层就能解决很多兼容问题。类似React的生态圈,Preact自己也有着规模不小的生态圈。
同时官网也宣称Preact有着一个简单却高效的diff算法实现使他成为最快的虚拟DOM框架之一,官网也号称自己可能是最薄的一层虚拟DOM实现。接下来本文将简单探究一下preact的虚拟DOM算法, 同时对比一下于React的Vitrual DOM算法的区别

Preact的Virtual DOM实现

React使用React.createElement()来实现js转换为Virtual DOM的过程,Preact在这里用了自己的实现方式。
Preact的Virtual DOM实现使用了babel-plugin-transform-react-jsx,通过pragma注入一个函数preact.h,实现类似下面的转换

1
2
3
4
5
/** @jsx h */
let foo = <div id='foo'>Hello</div>
//After babel transpilation
var foo = h('div', {id: 'foo'}, 'Hello');

Preact利用Preact.h()函数来将转换后的js生成Virtual DOM,之后再利用自己的Virtual DOM算法将其转换为真实的DOM。
Preact.h()这个函数为hyperscript的一个简化版,有兴趣的读者可以去了解完整的hyperscript。

Preact的Virtual DOM算法

创建节点

h函数接受babel转换后的代码,生成一系列的VNode(也就是Virtual DOM),之后执行render()函数,例如

1
render(h(Foo), document.getElementById('app'))

h()函数会将Foo转为如下的VNode

1
2
3
4
{
"nodeName": "Foo",
"children": []
}

之后会根据VNode.nodeName的类型分为以下几种情况

为组件创建VNode的实例

如果当前这个VNode.nodeName是一个组件,且还未被实例化,则会先去创建这个组件的实例,然后去先后调用这个组件的componentWillMountrender生命周期,其中render()会多次调用h()函数,最终让VNode变成类似下面的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"nodeName": "div",
"children": [
{
"nodeName": "input",
"attribute": {
"type": "text",
"onChange": "",
"placeholder": "input here"
},
"children": []
},
{
"nodeName": "List",
"attribute": {
"items": [
"foo",
"bar",
]
},
"children": []
}
]
}

需要注意的是,上面的节点的第二个子节点为一个组件,在这里并没有被转换。
render()函数执行完后会又重复根据VNode.nodeName类型进行判断

与组件相关的函数一共有四个,分别为

  • setComponentProps(): 为组件设立props
  • renderComponent(): 渲染一个组件,在这个函数内部会触发一些生命周期的钩子以及接收高阶组件
  • buildComponentFromVNode(): 将来自VNode的组件变为真实DOM
  • unmoumtComponent(): 将组件从DOM树中移除,并回收它
为非组件直接创建真实DOM节点

当VNode.nodeName为非组件(即HTML标签),且还没有相同的真实DOM节点已经被渲染,则会去用document.createNode(node)创建新的节点。
如果这个节点还有子节点的话,则会循环这个过程来重复创建子节点。

子节点添加到父节点

当上一步中判断节点没有子节点时,会接着判断该节点是否有父节点,若有父节点则会调用父节点的appendChild(childNode)方法来将子节点添加到父节点。

创建完子节点A(A无子节点)后并不会立即去创建子节点B,而是会先将子节点A添加到父节点,再去创建子节点B

处理子组件

当子节点为一个组件时,流程与前面类似,先为该组件创建VNode,再对其子节点进行前面流程的操作。
最终处理完该组件(以前面的List组件为例)的VNode类似如下结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
"nodeName": "ul",
"children": [
{
"nodeName": "li",
"attribute": {
"key": "foo"
},
"children": [
"foo"
],
"key": "foo"
},
{
"nodeName": "li",
"attribute": {
"key": "bar"
},
"children": [
"bar"
],
"key": "bar"
}
]
}

结束处理

多次重复前面的流程,所有的节点以及挂载之后(即当前处理的VNode没有父节点),会调用componentDidMount函数,结束处理。

在这之后,组件会保持一个对真实DOM的引用(refs),引用会用于更新和对比,能避免重复创建相同的节点。

删除节点

删除节点的过程中,涉及到了组件的更新、删除节点和组件的卸载

更新VNode

在判断完VNode.nodeName为组件并且该组件以及存在后,会去先后调用componentWillReceiveProps, shouldComponentUpdatecomponentWillUpdate的生命钩子,之后再去调用render

如果在这个流程中shouldComponentUpdate返回了false,则会直接跳过componentWillUpdaterender,这也就是shouldComponentUpdate用于优化react/preact性能的原理。

引用真实DOM

先前已经创建过的组件会保持对真实DOM的引用,在更新后,他的每一个属性都会和真实DOM的属性比较,如果是没有变化,则会跳过这个节点,去处理下一个节点,从而实现避免重复创建。

完全删除节点

当节点找不到与之相同的真实DOM,也不在创建节点的情况下,则会去删除(remove)该节点。
该节点不为组件的情况下,其父节点会简单的调用removeChild(node)来删除节点,在这之后会触发ComponentDidUpdate生命钩子。

卸载组件

类似上面完全删除节点的情况,如果即将删除的为一个组件,则会先调用ComponentWillUnMount,之后去调用component.unmoumtComponent(),删除完之后调用ComponentDidMounted

DOM Diff

上面的Virtual DOM算法在具体实现中会多次用到DOM Diff算法进行对比。
在探索Preact的DOM Diff算法之前,先简单了解一下React的DOM Diff算法:

React的DOM Diff算法

React的DOM Diff算法在标准Diff算法的基础上改进,将时间复杂度从O(n^3)减少到了O(n),能做到这样是因为React的diff算法基于了两点假设:

1.Two elements of different types will produce different trees.
两个不同类型的元素会生成不同的DOM树
2.The developer can hint at which child elements may be stable across different renders with a key prop.
开发者可以用给子元素加上key这个prop来让其能在多次渲染时保持稳定
(关于第二点,网上部分资料给出的为 对于同一层次的一组子节点,它们可以通过唯一的id进行区分)

其DOM Diff算法主要分为三类情况,tree diff,component diff和element diff。
(关于React DOM Diff的文章很多,本文就不在这里赘述)

Preact的DOM Diff算法

阅读过源码的读者不难发现,前面提到的render()函数本质上是调用了diff函数,而diff函数的具体实现中会涉及到以下函数

  • idiff()diff()主要会调用这个函数,去进行具体的对比
  • diffAttributes():对比节点的具体属性
    (待补完)

    关于具体的性能测试

    REPAINT RATE CHALLENGE
    UI Benchmark

Powered by Hexo and Hexo-theme-hiker

Copyright © 2013 - 2018 香香鸡的小窝 All Rights Reserved.

SunskyXH hold copyright