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
,实现类似下面的转换
Preact利用Preact.h()
函数来将转换后的js生成Virtual DOM,之后再利用自己的Virtual DOM算法将其转换为真实的DOM。
Preact.h()这个函数为hyperscript的一个简化版,有兴趣的读者可以去了解完整的hyperscript。
Preact的Virtual DOM算法
创建节点
h函数接受babel转换后的代码,生成一系列的VNode(也就是Virtual DOM),之后执行render()
函数,例如
h()函数会将Foo转为如下的VNode
之后会根据VNode.nodeName的类型分为以下几种情况
为组件创建VNode的实例
如果当前这个VNode.nodeName是一个组件,且还未被实例化,则会先去创建这个组件的实例,然后去先后调用这个组件的componentWillMount
和render
生命周期,其中render()
会多次调用h()
函数,最终让VNode变成类似下面的结构
需要注意的是,上面的节点的第二个子节点为一个组件,在这里并没有被转换。render()
函数执行完后会又重复根据VNode.nodeName类型进行判断
与组件相关的函数一共有四个,分别为
setComponentProps()
: 为组件设立propsrenderComponent()
: 渲染一个组件,在这个函数内部会触发一些生命周期的钩子以及接收高阶组件buildComponentFromVNode()
: 将来自VNode的组件变为真实DOMunmoumtComponent()
: 将组件从DOM树中移除,并回收它
为非组件直接创建真实DOM节点
当VNode.nodeName为非组件(即HTML标签),且还没有相同的真实DOM节点已经被渲染,则会去用document.createNode(node)
创建新的节点。
如果这个节点还有子节点的话,则会循环这个过程来重复创建子节点。
子节点添加到父节点
当上一步中判断节点没有子节点时,会接着判断该节点是否有父节点,若有父节点则会调用父节点的appendChild(childNode)
方法来将子节点添加到父节点。
创建完子节点A(A无子节点)后并不会立即去创建子节点B,而是会先将子节点A添加到父节点,再去创建子节点B
处理子组件
当子节点为一个组件时,流程与前面类似,先为该组件创建VNode,再对其子节点进行前面流程的操作。
最终处理完该组件(以前面的List组件为例)的VNode类似如下结构:
结束处理
多次重复前面的流程,所有的节点以及挂载之后(即当前处理的VNode没有父节点),会调用componentDidMount
函数,结束处理。
在这之后,组件会保持一个对真实DOM的引用(refs),引用会用于更新和对比,能避免重复创建相同的节点。
删除节点
删除节点的过程中,涉及到了组件的更新、删除节点和组件的卸载
更新VNode
在判断完VNode.nodeName为组件并且该组件以及存在后,会去先后调用componentWillReceiveProps
, shouldComponentUpdate
和componentWillUpdate
的生命钩子,之后再去调用render
如果在这个流程中
shouldComponentUpdate
返回了false,则会直接跳过componentWillUpdate
和render
,这也就是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