在Preact源码阅读(一)我们分析了render函数、diff函数的功能,分析了React Component的生命周期映射、父子组件的周期执行顺序,在本节中,我们将继续(一)中未分析的内容,介绍Preact的diff流程的处理流程、vnode到dom的转换及插入过程。let we go, 继续我们的源码阅读之旅。
1、关键子函数分析
在分析整体的流程之前,我们先看看几个关键的子函数,这可以帮助我们更好的理解整体的diff流程。我们主要分析与diff流程之外的函数,方便后续diff流程时,我们理解子函数的功能。
1.1 props
Preact props文件定义属性添加、比较、事件相关的一系列子功能,我们首先看下其包括的具体功能。
1.1.1 eventProxy
我们首先看下事件代理的功能,可以看到,当我们定义了options.event hooks时,会首先执行options.event hook,完成事件的代理。
function eventProxy(e) {
this._listeners[e.type](options.event ? options.event(e) : e);
}
在(一)中我们也看到很多的options.*的hook功能,这里就分析下它的功能。options作为一个基础的工具库,可以自定义一系列的功能,比较典型的两种场景如下:
- Preact官方的使用。一是,Preact到React的功能映射及转换,如compat/src/render.js里,定义了React的Event处理;二是,debug/test时的调用,方便debug、性能的测试。
- 开发者自定义。当然,我们也可以自己定义options hook,具体的功能,可以参见preactjs options。
1.1.2 setStyle
setStyle,用来设置元素的style属性。Preact对style属性分了4类情况:
- 以'-'开头的元素。直接使用setProperty设置style的属性。
- value为数值且需要设置px的属性,典型的如height、width等。这里的IS_NON_DIMENSIONAL包含一些不需要添加px的属性,如zoom、ine等。
- value为null时,设置为''。
- 默认逻辑,style[key]等于value。
function setStyle(style, key, value) {
if (key[0] === '-') {
style.setProperty(key, value);
} else if (
typeof value == 'number' &&
IS_NON_DIMENSIONAL.test(key) === false
) {
style[key] = value + 'px';
} else if (value == null) {
style[key] = '';
} else {
style[key] = value;
}
}
1.1.3 setProperty
dom Property函数用来处理dom element的property, 其具体的处理流程如下:
- className初始处理。svg图片,className => class, 非svg, class => className。
if (isSvg) {
if (name === 'className') {
name = 'class';
}
} else if (name === 'class') {
name = 'className';
}
- Style。分为两种类型,string/object。
- string时,dom.style.cssText = style;
- object时。
- oldValue string时,dom.style.cssText清空、oldValue为null。
- oldValue为对象时。
- oldValue有但value没有的属性,设置为''.
- value设置新的style属性。
if (name === 'style') {
s = dom.style;
// string处理
if (typeof value == 'string') {
s.cssText = value;
} else {
// oldValue string
if (typeof oldValue == 'string') {
s.cssText = '';
oldValue = null;
}
// oldValue object
// oldValue有但value没有的属性,设置为''
if (oldValue) {
for (let i in oldValue) {
if (!(value && i in value)) {
setStyle(s, i, '');
}
}
}
// 设置新的value属性
if (value) {
for (let i in value) {
if (!oldValue || value[i] !== oldValue[i]) {
setStyle(s, i, value[i]);
}
}
}
}
}
- 以on开头的属性,event的处理。区分onClick、onClickCapture两种场景。
- value不存在时 ,移除name的事件。
- value存在,dom添加event的listener。
else if (name[0] === 'o' && name[1] === 'n') {
useCapture = name !== (name = name.replace(/Capture$/, ''));
nameLower = name.toLowerCase();
name = (nameLower in dom ? nameLower : name).slice(2);
if (value) {
if (!oldValue) dom.addEventListener(name, eventProxy, useCapture);
(dom._listeners || (dom._listeners = {}))[name] = value;
} else {
dom.removeEventListener(name, eventProxy, useCapture);
}
}
- 正常操作。非SVG且name在dom且name!=list/tagName/form/type/size时,设置dom.name等于value。由于tagName、form、list等属性,在dom里只读属性,是不可以设置的,因此更新时,过滤这些属性的操作。
else if (
name !== 'list' &&
name !== 'tagName' &&
name !== 'form' &&
name !== 'type' &&
name !== 'size' &&
!isSvg &&
name in dom
) {
dom[name] = value == null ? '' : value;
}
- 除以上及value非函数、name不等于"dangerouslySetInnerHTML"的处理。
- www.w3.org/1999/xlink 属性处理,value为null/false时,removeAttributeNS对应的属性;否则,setAttributeNS对应的属性。
- value为null、value为false且非ARIA-attributes属性节点时,removeAttribute该属性。
- ARIA属性存在为false的场景,因此,需要过滤该类型的属性。
- 除以上的情况,直接设置setAttribute(name, value).
else if (typeof value != 'function' && name !== 'dangerouslySetInnerHTML') {
if (name !== (name = name.replace(/^xlink:?/, ''))) {
if (value == null || value === false) {
dom.removeAttributeNS(
'http://www.w3.org/1999/xlink',
name.toLowerCase()
);
} else {
dom.setAttributeNS(
'http://www.w3.org/1999/xlink',
name.toLowerCase(),
value
);
}
} else if (
value == null ||
(value === false &&
!/^ar/.test(name))
) {
dom.removeAttribute(name);
} else {
dom.setAttribute(name, value);
}
}
}
1.1.4 diffProps
在分析完基础的子函数后,我们看下diffProps函数的核心功能,其主要是vnode节点的props diff并将变更应用到对应的dom节点上。
- 除children/key外,oldProps有但newProps没有的属性,调用setProperty设置为null.
- 递归处理newProps的属性,满足如下三种条件下,setProperty i更新为新的newProps[i]。
- 除children/value/key/checked外
- 非hydrate模式,或hydrate下newProps[i]为函数
- oldProps、newProps属性不想等。
export function diffProps(dom, newProps, oldProps, isSvg, hydrate) {
let i;
// 处理oldProps有但newProps没有的props
for (i in oldProps) {
// 除children/key外,i不在newProps,设置为null
if (i !== 'children' && i !== 'key' && !(i in newProps)) {
setProperty(dom, i, null, oldProps[i], isSvg);
}
}
// 处理新的props
for (i in newProps) {
// 除children/value/key/checked属性外
// 非hydrate模式,或hydrate下newProps[i]为函数
// oldProps[i] !== newProps[i]
if (
(!hydrate || typeof newProps[i] == 'function') &&
i !== 'children' &&
i !== 'key' &&
i !== 'value' &&
i !== 'checked' &&
oldProps[i] !== newProps[i]
) {
// 设置i属性,更新为newProps[i]
setProperty(dom, i, newProps[i], oldProps[i], isSvg);
}
}
}
1.2 diffchildren
diffChildren包含了核心的diffChildren功能,这里主要看一下toChildArray的功能。
1.2.1 toChildArray
toChildArray功能主要是将children转换为数组,并且对children的子元素做toChildArray的递归处理。
export function toChildArray(children) {
if (children == null || typeof children == 'boolean') {
return [];
} else if (Array.isArray(children)) {
return EMPTY_ARR.concat.apply([], children.map(toChildArray));
}
return [children];
}
1.3 applyRef/unmount/removeNode
1.3.1 applyRef
触发/更新ref对象,若为函数,则执行ref(value), 否则设置ref.current = value.
export function applyRef(ref, value, vnode) {
try {
if (typeof ref == 'function') ref(value);
else ref.current = value;
} catch (e) {
options._catchError(e, vnode);
}
}
1.3.2 unmount
unmount为节点的卸载函数,skipRemove标识父节点,是否从当前DOM分离。
- 清空当前节点/组件的refs。
- vnode.type不为组件时,重置skipRemove。
- 设置vnode.dom/vnode._nextDom为undefined。
- 若当前节点为component时,执行componentWillUnmount函数、设置r.base = r._parentDom = null。
- 递归处理vnode._children, 卸载当前元素的子节点。
- dom存在时,remove当前Dom Element。
export function unmount(vnode, parentVNode, skipRemove) {
let r;
if (options.unmount) options.unmount(vnode);
if ((r = vnode.ref)) {
if (!r.current || r.current === vnode._dom) applyRef(r, null, parentVNode);
}
let dom;
if (!skipRemove && typeof vnode.type != 'function') {
skipRemove = (dom = vnode._dom) != null;
}
vnode._dom = vnode._nextDom = undefined;
if ((r = vnode._component) != null) {
if (r.componentWillUnmount) {
try {
r.componentWillUnmount();
} catch (e) {
options._catchError(e, parentVNode);
}
}
r.base = r._parentDom = null;
}
if ((r = vnode._children)) {
for (let i = 0; i < r.length; i++) {
if (r[i]) unmount(r[i], parentVNode, skipRemove);
}
}
if (dom != null) removeNode(dom);
}
1.3.3 removeNode
调用DOM removeChildren,完成node节点的删除。
export function removeNode(node) {
let parentNode = node.parentNode;
if (parentNode) parentNode.removeChild(node);
}
1.4 getDomSibling
getDomSibling函数用来获取索引为index的兄弟节点、当前节点的相邻节点。
- childIndex等于null时,获取当前节点的相邻节点。
- childIndex存在时,从索引开始寻找children[i]._dom存在的节点,返回该节点的_dom.
- 在上一步,若仍然未找到_dom,只能从当前节点的父节点继续查询,寻找关联的节点。
export function getDomSibling(vnode, childIndex) {
if (childIndex == null) {
return vnode._parent
? getDomSibling(vnode._parent, vnode._parent._children.indexOf(vnode) + 1)
: null;
}
let sibling;
for (; childIndex < vnode._children.length; childIndex++) {
sibling = vnode._children[childIndex];
if (sibling != null && sibling._dom != null) {
return sibling._dom;
}
}
return typeof vnode.type == 'function' ? getDomSibling(vnode) : null;
}
2、整体的diff流程
preact的diff流程采用深度搜索的方法,从跟节点出发,递归遍历children节点/组件,在叶节点完成dom的生成与转换,从而完成diff的流程。我们先看下diffElementNodes/diffChildren的功能,后续在分析preact整体的diff流程。
2.1 diffElementNodes
通过函数的名称,我们就能知道diffElementNodes用来进行element node的比较,生成dom节点。
diffElementNodes的入参数如下:
diffElementNodes的函数处理流程如下:
2.1.1 初始化
参数初始化。初始化oldProps、newProps、isSvg等属性。
let i;
let oldProps = oldVNode.props;
let newProps = newVNode.props;
isSvg = newVNode.type === 'svg' || isSvg;
2.1.2 excessDomChildren
excessDomChildren属性用于hydration模式下现有DOM节点的复用,现从整体的角度去看其基本功能及作用。
如下,现以一个简单的Demo,说明excessDomChildren的整体功能。
当excessDomChildren存在时,寻找当前nodes的值,与dom比较,复用现有的dom节点(diffElementNodes)。依据节点类型分为三类:
- 节点类型为文本节时(nodeType === 3), 复用现有的文本节点。
- 节点类型为非文本节点时, child.localName === newVNode.type, 即标签名称相同时, 如div、span元素。
- dom节点与child相同时,复用现有的节点。
if (excessDomChildren != null) {
for (i = 0; i < excessDomChildren.length; i++) {
const child = excessDomChildren[i];
if (
child != null &&
((newVNode.type === null
? child.nodeType === 3
: child.localName === newVNode.type) ||
dom == child)
) {
dom = child;
excessDomChildren[i] = null;
break;
}
}
}
excessDomChildren存在时,当前节点的children diff时,会设置excessDomChildren为当前节点的childNodes。
if (excessDomChildren != null) {
excessDomChildren = EMPTY_ARR.slice.call(dom.childNodes);
}
excessDomChildren的重置,在diffchildren函数里,清除excessDomChildren[i]的子节点。
// Remove children that are not part of any vnode.
if (excessDomChildren != null && typeof newParentVNode.type != 'function') {
for (i = excessDomChildren.length; i--; ) {
if (excessDomChildren[i] != null) removeNode(excessDomChildren[i]);
}
}
现在以某个demo,我们看下excessDomChildren是怎么复用现有的dom,完成节点的更新及替换的。默认插入的节点为body,render时excessDomChildren的结构[text、div.hello-world、script、text]。
如下图,demo excessDomChildren节点的处理与比较流程如下:
- 比较div.hello-world节点时,excessDomChildren为初始化的document.body.childNodes节点。
- excessDomChildren不为null, 匹配中child.localName(div) === newVNode.type div匹配,dom = document.body.childNodes.div节点,设置对应excessDomChildren[i]为null。
- excessDomChildren不为null,当前子节点的excessDomChildren设置为[text, div, text('world')], 调用diffChildren完成childredn节点的匹配。
- 比较div节点,适配div元素。
- 此时excessDomChildrenan按照匹配规则,适配div节点,dom = excessDomChildren.div。
- excessDomChildren不为null,当前子节点的excessDomChildren设置为[text('world')], 调用diffChildren完成childredn节点的匹配。
- 比较world文本节点,适配文本节点。
- 此时excessDomChildrenan按照匹配规则,适配文本节点, dom = excessDomChildren[0], 完成文本节点的适配。
- world文本节点无子节点,返回到上一轮的节点diff。
- div兄弟节点world, 适配excessDomChildrenan的文本节点。
- 按照适配规则,文本节点适配(text('')),并没有适配最后一个节点。 excessDomChildren主要目的就是更好的复用已有的dom元素,提高节点创建的损耗,更快的渲染出页面。
2.1.3 新节点的创建
当dom为null时,表明当前的节点为新添加的节点,如isA & <A />的场景,此时依据节点的类型,分三类进行处理:
if (dom == null) {
if (newVNode.type === null) {
return document.createTextNode(newProps);
}
dom = isSvg
? document.createElementNS('http://www.w3.org/2000/svg', newVNode.type)
: document.createElement(
newVNode.type,
newProps.is && { is: newProps.is }
);
excessDomChildren = null;
isHydrating = false;
}
- 文本节点。type 为null时,标识为文本节点,使用document.createTextNode(newProps)完成文本节点的创建。
- SVG。调用document.createElementNS('www.w3.org/2000/svg', newVNode.type)创建SVG元素。
- element,如div/span/input等。调用document.createElement完成节点的创建。
- 重置excessDomChildren、isHydrating。dom为新增的元素,没必要复用现有的节点,所有设置excessDomChildren为null。创建新的节点,现有的元素节点无法复用,isHydrating设置为false。
2.1.4 props及children的处理
diffElementNodes在props/children的处理,可以分为如下的两个方面:
- 文本节点,props的直接替换。
if (newVNode.type === null) {
if (oldProps !== newProps && dom.data != newProps) {
dom.data = newProps;
}
}
- 非文本节点,依据dangerouslySetInnerHTML分成两种模式,处理props及children.
- 若使用dangerouslySetInnerHTML, 计算生成新的html。
let oldHtml = oldProps.dangerouslySetInnerHTML;
let newHtml = newProps.dangerouslySetInnerHTML;
// 非isHydrating模式下,获取旧的props,isHydrating模式下不处理旧props。
if (!isHydrating) {
// 获取dom attributes,拷贝到oldProps里。
if (excessDomChildren != null) {
oldProps = {};
for (let i = 0; i < dom.attributes.length; i++) {
oldProps[dom.attributes[i].name] = dom.attributes[i].value;
}
}
// newHtml与newHtml存在且不想等,设置dom.innerHTML
if (newHtml || oldHtml) {
if (!newHtml || !oldHtml || newHtml.__html != oldHtml.__html) {
dom.innerHTML = (newHtml && newHtml.__html) || '';
}
}
}
- newProps与oldProps比较。
diffProps(dom, newProps, oldProps, isSvg, isHydrating);
- Children diff。dangerouslySetInnerHTML模式下,设置_chilren=[],否则比较当前节点的children。
if (newHtml) {
newVNode._children = [];
} else {
i = newVNode.props.children;
// children的diff,开启新一轮递归
diffChildren(
dom,
Array.isArray(i) ? i : [i],
newVNode,
oldVNode,
globalContext,
newVNode.type === 'foreignObject' ? false : isSvg,
excessDomChildren,
commitQueue,
EMPTY_OBJ,
isHydrating
);
}
- 非isHydrating模式下,比较属性的value、checked属性,一般用于input/textarea等节点。
if (!isHydrating) {
if (
'value' in newProps &&
(i = newProps.value) !== undefined &&
i !== dom.value
) {
setProperty(dom, 'value', i, oldProps.value, false);
}
if (
'checked' in newProps &&
(i = newProps.checked) !== undefined &&
i !== dom.checked
) {
setProperty(dom, 'checked', i, oldProps.checked, false);
}
}
}
- 返回生成的dom节点。
2.2 diffChildren
diffChildren的入参与diff、diffElementNodes类似,其具体的参数如下:
2.2.1 初始化参数
diffChildren定义了常用的参数,包括oldNode、childVNode、newDom、firstChildDom、refs等。
let i, j, oldVNode, childVNode, newDom, firstChildDom, refs;
// 获取旧节点的_children, 不存在时,设置为空数组。
let oldChildren = (oldParentVNode && oldParentVNode._children) || EMPTY_ARR;
// 获取旧节点的children长度
let oldChildrenLength = oldChildren.length;
2.2.2 oldDom设置
oldDom不存在的场景,只在调用render或diffElementNodes才会设置为EMPTY_OBJ, 此时,需复用现有excessDomChildren、oldParentVNode,得到新的oldDom.
if (oldDom == EMPTY_OBJ) {
//excessDomChildren存在,oldDom设置为首个节点。
if (excessDomChildren != null) {
oldDom = excessDomChildren[0];
} else if (oldChildrenLength) {
// oldChildren存在时,取虚拟节点dom
oldDom = getDomSibling(oldParentVNode, 0);
} else {
// 不存在,设置为null
oldDom = null;
}
}
2.2.3 循环处理children
接下来就是循环处理renderResult,将新的子节点添加到newParentVNode._children里。
newParentVNode._children = [];
for (i = 0; i < renderResult.length; i++) {
}
下面按照节点的创建、XX的流程来介绍children节点的处理流程。
2.2.3.1 节点的创建
针对生成的节点,依据不同的类型,创建生成不同的vnode。
- childNode为null、typeof childVNode == 'boolean'。该类型的节点,设置为null,不渲染。
- typeof childVNode 为string/number的节点,将创建为文本节点(vnode.type = null)。
- childVNode为数组时,将创建为Fragment节点(vnode.type = Fragment)。
- childVNode._dom存在或者childVNode._component不为null(组件)时,重新创建childVNode.type节点。
- childVNode已创建时,直接复制到newParentVNode._children上即可。
childVNode = renderResult[i];
if (childVNode == null || typeof childVNode == 'boolean') {
childVNode = newParentVNode._children[i] = null;
} else if (typeof childVNode == 'string' || typeof childVNode == 'number') {
childVNode = newParentVNode._children[i] = createVNode(
null,
childVNode,
null,
null,
childVNode
);
} else if (Array.isArray(childVNode)) {
childVNode = newParentVNode._children[i] = createVNode(
Fragment,
{ children: childVNode },
null,
null,
null
);
} else if (childVNode._dom != null || childVNode._component != null) {
childVNode = newParentVNode._children[i] = createVNode(
childVNode.type,
childVNode.props,
childVNode.key,
null,
childVNode._original
);
} else {
childVNode = newParentVNode._children[i] = childVNode;
}
完成节点的创建后, 若为null,跳过处理。不为null,绑定parentNode和设置_depth。
// childVNode节点为null,跳过处理
if (childVNode == null) {
continue;
}
// 绑定父节点和设置_depth。
childVNode._parent = newParentVNode;
childVNode._depth = newParentVNode._depth + 1;
2.2.3.2 key节点处理
针对设置了key值的节点,preact会走专门的处理逻辑。
- oldNode存在且新旧节点key/type相等时,设置oldChildren[i] = undefined(后续删除使用)。
- 其他场景下,递归处理oldChildren, 寻找key/value适配的节点,若找到,设置该节点为undefined。
oldVNode = oldChildren[i];
if (
oldVNode === null ||
(oldVNode &&
childVNode.key == oldVNode.key &&
childVNode.type === oldVNode.type)
) {
oldChildren[i] = undefined;
} else {
for (j = 0; j < oldChildrenLength; j++) {
oldVNode = oldChildren[j];
if (
oldVNode &&
childVNode.key == oldVNode.key &&
childVNode.type === oldVNode.type
) {
oldChildren[j] = undefined;
break;
}
oldVNode = null;
}
}
2.2.3.3 diff的调用
oldNode为null/undefined时,重置为EMPTY_OBJ,调用diff函数生成newDom。 oldVNode = oldVNode || EMPTY_OBJ;
newDom = diff(
parentDom,
childVNode,
oldVNode,
globalContext,
isSvg,
excessDomChildren,
commitQueue,
oldDom,
isHydrating
);
2.2.3.4 refs的处理
childVNode的ref添加到refs里,oldVNode的ref绑定节点设置null。
if ((j = childVNode.ref) && oldVNode.ref != j) {
if (!refs) refs = [];
if (oldVNode.ref) refs.push(oldVNode.ref, null, childVNode);
refs.push(j, childVNode._component || newDom, childVNode);
}
2.2.3.5 newDom的插入
diff函数返回的新dom节点, 依据是否为null, 分为两类:
- newDom不为null时,有如下的处理步骤:
- 设置firstChildDom等于首个不为null的dom节点。
- 调用placeChild,将dom节点插入到parentDom中。
- 针对select.options特殊处理,设置其value为''。
- newParentVNode.type为函数时,设置newParentVNode._nextDom。
- newDom为null时,针对oldDom的某个场景特殊处理,oldDom获取下一个节点的dom。
if (newDom != null) {
if (firstChildDom == null) {
firstChildDom = newDom;
}
oldDom = placeChild(
parentDom,
childVNode,
oldVNode,
oldChildren,
excessDomChildren,
newDom,
oldDom
);
if (newParentVNode.type == 'option') {
parentDom.value = '';
} else if (typeof newParentVNode.type == 'function') {
newParentVNode._nextDom = oldDom;
}
} else if (
oldDom &&
oldVNode._dom == oldDom &&
oldDom.parentNode != parentDom
) {
oldDom = getDomSibling(oldVNode);
}
placeChild主要负责将新生成的节点插入、移动,完成parentDom的生成。
function placeChild(
parentDom,
childVNode,
oldVNode,
oldChildren,
excessDomChildren,
newDom,
oldDom
) {
let nextDom;
// Fragment下,_nextDom不为null。
if (childVNode._nextDom !== undefined) {
nextDom = childVNode._nextDom;
childVNode._nextDom = undefined;
// 当前仅当excessDomChildren、oldNode都为null时,两者才相等
// 新旧dom不相等、newParent.parentNode为null
} else if (
excessDomChildren == oldVNode ||
newDom != oldDom ||
newDom.parentNode == null
) {
// oldDom为null时,oldDomParentNode != parentDom时,将newDom插入到newDom
outer: if (oldDom == null || oldDom.parentNode !== parentDom) {
parentDom.appendChild(newDom);
nextDom = null;
} else {
// oldChildren会存在空的text节点,因此只需要length/2的次数即可
// 当前兄弟节点是否找到newDom节点,若找到,中断执行
for (
let sibDom = oldDom, j = 0;
(sibDom = sibDom.nextSibling) && j < oldChildren.length;
j += 2
) {
if (sibDom == newDom) {
break outer;
}
}
// 在oldDom前插入newDom节点
parentDom.insertBefore(newDom, oldDom);
nextDom = oldDom;
}
}
if (nextDom !== undefined) {
oldDom = nextDom;
} else {
oldDom = newDom.nextSibling;
}
return oldDom;
}
2.2.4 卸载处理
在完成children的diff之后,将会对excessDomChildren、oldChildren、ref做处理,卸载已有的节点、更新新添加的节点。
// 更新newParentVNode._dom为更新后的dom
newParentVNode._dom = firstChildDom;
// 非组件状态下,remove excessDomChildren不为null的节点
if (excessDomChildren != null && typeof newParentVNode.type != 'function') {
for (i = excessDomChildren.length; i--; ) {
if (excessDomChildren[i] != null) removeNode(excessDomChildren[i]);
}
}
// 卸载oldChildren里的节点
for (i = oldChildrenLength; i--; ) {
if (oldChildren[i] != null) unmount(oldChildren[i], oldChildren[i]);
}
// 应用refs,卸载或者触发更新
if (refs) {
for (i = 0; i < refs.length; i++) {
applyRef(refs[i], refs[++i], refs[++i]);
}
}
2.3 diff的整体流程
Preact的diff流程,采取深度搜索比较的形式,从上到下,diff、diffChildren、diffElementNode循环调用,完整整个vnode dom tree的比较和处理。以如下的一个例子,我们看下初始化阶段其基本的diff处理流程。
如下图,其基本的调用流程如下,实线代表函数调用,虚线表示返回及dom的处理。我们可以看到,Preact从App节点开始,使用深度搜索的方式,递归调用diffChildren、diff、diffElementNodes三个函数,完成节点的diff、虚拟节点的创建、dom的生成,最终将内容插入到页面中。
3、总结
本节主要介绍了preact 10.4.6的diff流程,并以一个简单的demo,演示了render阶段节点的创建、比较、Dom的生成与创建流程。preact采取深度搜索的形式完成vnode的比较,在此基础上,使用了裁剪(key)、DOM节点复用等多种方式,优化现有的diff方式,提高dom diff的效率。