从 Preact 源码一窥 React 原理(二):Diff 算法

  • 前言
  • Diff 算法
    • 渲染
    • diffChildren 函数
    • diff 函数
    • diffElementNodes 函数
    • diffProps 函数
  • 总结
  • 参考资料

系列文章:

  1. 从 Preact 源码一窥 React 原理(一):JSX 渲染
  2. 从 Preact 源码一窥 React 原理(二):Diff 算法(本文)
  3. 从 Preact 源码一窥 React 原理(三):组件

前言

前一篇文章《从 Preact 源码一窥 React 原理(二):JSX 渲染》作为铺垫,简单介绍了 JSX 转化为 Preact 中虚拟 DOM 节点 VNode 的相关函数以及数据结构。
本文则将走进 Preact 中最为核心的部分之一:Diff 算法,介绍 Preact 如何渲染并更新得到的 VNode 树。
为了简单起见,本文主要内容仅集中于非组件节点的 Diff 算法,对于函数组件或类组件的相关操作,将在后续文章中进行介绍。

Diff 算法

什么是 Diff 算法?为什么我们需要 Diff 算法?
我们知道 Web 页面是由 DOM 树构成的(事实上,浏览器对于 Web 页面的渲染还会生成 CSSOM 树、渲染树等结构,但是直接和前端开发者打交道的就是 DOM 树了),而 DOM 树更新所带来的操作代价(重排、重绘)是昂贵的。
Diff 算法用于比较两个 DOM 树之间的差异,并确定最小的 DOM 更新操作。给定两个需要比较的 DOM 树,标准的 Diff 算法时间复杂度为 O(n^3),这一复杂度在实际应用中是不可接受的。为此,React 提出了一种启发式算法将算法复杂度降低为 O(N)。该启发式算法基于一些假设,放宽了计算结果最小操作的限制,转向较优的结果,其内容为:

  • Two elements of different types will produce different trees.
  • The developer can hint at which child elements may be stable across different renders with a key prop.

来自Reactjs.org - reconciliation

翻译一下:

  • 两个不同类型的元素将会生成不同的树;
  • 开发者可以通过 key 值来指定在不同的树中可能稳定的子节点。

根据以上的假设,React 会对两个 DOM 树执行平级的比较,并通过 key 值来确定可能相同的节点进行递归比较;不存在 key 值时,则比较子节点中类型是否相同,对于相同类型的子节点进行递归比较,不同类型的旧的子节点将直接被删除并在父节点上插入新的子节点。

Preact 所采用的 Diff 算法基本思路与 React 一致,不同之处在于 Preact 仅仅在内存中维护一棵虚拟 DOM 树,并添加了真实 DOM 与虚拟 DOM 之间的相互引用。因此每次执行 Diff 操作时,比较的事实上是新的虚拟 DOM 树与真实 DOM 树,并在比较过程中同时执行 DOM 操作。具体的算法细节参见下文。

渲染

上一篇文章中使用了这样一个示例作为引子:

import { h, render } from 'preact';render((<div id="foo"><span>Hello, world!</span><button onClick={ e => alert("hi!") }>Click Me</button></div>
), document.body);

我们已经对其中的 h函数进行了分析,了解了其如何与 Babel 相结合生成 VNode 树。
接下来就该看看 render函数是如何执行渲染操作的了:

// src/render.js
export function render(vnode, parentDom) {if (options.root) options.root(vnode, parentDom);let oldVNode = parentDom._prevVNode;vnode = createElement(Fragment, null, [vnode]);let mounts = [];diffChildren(parentDom, parentDom._prevVNode = vnode, oldVNode, EMPTY_OBJ, parentDom.ownerSVGElement!==undefined, oldVNode ? null : EMPTY_ARR.slice.call(parentDom.childNodes), mounts, vnode, EMPTY_OBJ);commitRoot(mounts, vnode);
}

略去第一句(options 为 Preact 提供了一些调试相关的钩子,与功能实现不想关联,略去不谈),首先 render 函数获取挂载在容器节点中的 _prevVNode 然后将新的 vnode 包裹为 Fragment 节点的子节点(Fragment节点本身并没有意义,只是作为子节点集合的占位符)。
然后将新旧两个节点传入 diffChildren 函数中,通过其进行新 vnode 的渲染。其中,mount 用于保存新挂载的节点,并在 diff 执行结束后,通过调用 commitRoot 函数对新挂载的组件节点调用 componentDidMount 钩子。

render 函数的逻辑较为简单,核心就在于调用 diffChildren 函数对新的虚拟 DOM 进行渲染,也就是说:对空树和虚拟 DOM 树执行 Diff 操作,事实上等价于渲染该虚拟 DOM 树。

diffChildren 函数

render 函数中所调用的 diffChildren 函数包含大量的参数,光是看一句函数调用就能够拆成好几行教人看的迷糊。
因此,在介绍 diffChildren 具体逻辑之前,有必要对其参数进行介绍:

  • parentDom:对两个 children 进行比较的父真实 DOM 节点;
  • newParentVNode:新的父虚拟 DOM 节点;
  • oldParentVNode:旧的父虚拟 DOM 节点;
  • context:Legacy Context API 所向下传递的 context 值;
  • isSvg:标示真实 DOM 是否为 SVG 节点;
  • excessDomChildren:多余的真实 DOM 子节点,部分节点将在子节点的 DOM 操作中进行复用,剩余的部分则会在 diffChildren 执行结束后卸载;
  • mounts:存储新挂载的组件,用于在 Diff 操作执行结束后对其调用 componentDidMount 钩子;
  • ancestorComponent:Diff 发生的最近的父组件,Diff 操作中出现的错误将由该父组件进行捕获;
  • oldDom:新的真实 DOM 节点将被挂载在 oldDom 附近,当首次渲染时,其值为 null,并且在大多数情况下,其会从 oldChildren[0]._dom 值开始。

列出 diffChildren 诸多参数作为参考,能够更好的帮助我们理解函数实现中的各部分的含义。
diffChildren 的实现如下所示:

export function diffChildren(parentDom, newParentVNode, oldParentVNode, context, isSvg, excessDomChildren, mounts, ancestorComponent, oldDom) {let childVNode, i, j, p, index, oldVNode, newDom,nextDom, sibDom, focus;// PART 1// 展平所有 props.children 中的数组节点并提取到 _children 中let newChildren = newParentVNode._children || toChildArray(newParentVNode.props.children, newParentVNode._children=[], coerceToVNode);let oldChildren = oldParentVNode!=null && oldParentVNode!=EMPTY_OBJ && oldParentVNode._children || EMPTY_ARR;let oldChildrenLength = oldChildren.length;// PART 2// 只有在 render 函数和 diffElementNodes 函数调用 diffChildren 时,oldDom 才会等于 EMPTY_OBJ// 当 excessDomChildren 不为空时,将 excessDomChildren 第一个非空元素作为 oldDom;// 否则将 oldChildren 中第一个非空的 _dom 作为 oldDomif (oldDom == EMPTY_OBJ) {oldDom = null;if (excessDomChildren!=null) {for (i = 0; i < excessDomChildren.length; i++) {if (excessDomChildren[i]!=null) {oldDom = excessDomChildren[i];break;}}}else {for (i = 0; i < oldChildrenLength; i++) {if (oldChildren[i] && oldChildren[i]._dom) {oldDom = oldChildren[i]._dom;break;}}}}// PART 3for (i=0; i<newChildren.length; i++) {// 拷贝包含 dom 的 VNode 并将非 VNode 的节点转化为 VNodechildVNode = newChildren[i] = coerceToVNode(newChildren[i]);oldVNode = index = null;// PART 4// 首先判断相同下标的 VNode 是否拥有相同的 key / VNode 类型p = oldChildren[i];if (p != null && (childVNode.key==null && p.key==null ? (childVNode.type === p.type) : (childVNode.key === p.key))) {index = i;}else {// 在列表中线性查找,寻找拥有相同的 key / VNode 类型的 VNodefor (j=0; j<oldChildrenLength; j++) {p = oldChildren[j];if (p!=null) {if (childVNode.key==null && p.key==null ? (childVNode.type === p.type) : (childVNode.key === p.key)) {index = j;break;}}}}// 将查找得到的 oldChild 在列表中删除,以免重复比较浪费 CPU 资源if (index!=null) {oldVNode = oldChildren[index];oldChildren[index] = null;}nextDom = oldDom!=null && oldDom.nextSibling;// PART 5// 执行当前 VNode 与 查到得到的对应 old VNode 间的 diff 操作newDom = diff(oldVNode==null ? null : oldVNode._dom, parentDom, childVNode, oldVNode, context, isSvg, excessDomChildren, mounts, ancestorComponent, null, oldDom);// PART 6// newDom 可能未被 diff() 函数所挂载if (childVNode!=null && newDom !=null) {// 记录当前 focus 的元素focus = document.activeElement;// 只有 Fragment 或者返回 Fragment 的组件才会拥有非空的 _lastDomChild 属性// 此时,将 oldDom 设置为 _lastDomChild ,后续的节点将被该子节点之后if (childVNode._lastDomChild != null) {newDom = childVNode._lastDomChild;}else if (excessDomChildren==oldVNode || newDom!=oldDom || newDom.parentNode==null) {// 注意,excessDomChildren==oldVNode 等价于 excessDomChildren==null && oldVNode==null// 如果 oldDom 为空,或者其父节点被更新了,则将其插入父节点末尾outer: if (oldDom==null || oldDom.parentNode!==parentDom) {parentDom.appendChild(newDom);}else {// 遍历 oldDom 的 nextSibling,判断是否 newDom 是否已存在于真实 DOM 树中// 如果不存在,则将 newDom 插入 oldDom 之前sibDom = oldDom;j = 0;// 这一操作,包括 j++<oldChildrenLength/2 可能只是一个性能上的权衡,// 事实上即使不进行判断操作,insertBefore 也会把已经挂载的子节点移动到指定的位置while ((sibDom=sibDom.nextSibling) && j++<oldChildrenLength/2) {if (sibDom===newDom) {break outer;}}parentDom.insertBefore(newDom, oldDom);}}// 恢复以保存的 focus 元素if (focus!==document.activeElement) {focus.focus();}// 由于 Fragment 返回的 _lastDomChild 可能为 null,因此此时 newDom 可能为 null// 当 newDom 非空时,将 oldDom 指向 newDom 的下一个兄弟节点,否则指向 oldDom 的下一个兄弟节点 nextDomoldDom = newDom!=null ? newDom.nextSibling : nextDom;}}// PART 7// 未被复用的真实 DOM 节点则被统一卸载if (excessDomChildren!=null && newParentVNode.type!==Fragment) for (i=excessDomChildren.length; i--; ) if (excessDomChildren[i]!=null) removeNode(excessDomChildren[i]);// 在前述的过程中未受处理的 VNode 则统一进行卸载for (i=oldChildrenLength; i--; ) if (oldChildren[i]!=null) unmount(oldChildren[i], ancestorComponent);
}

由于 diffChildren 代码的实现部分较长,因此将其分为多个 PART 进行介绍(每一个 PART 由对应的注释所标示)。

  • PART 1: 通过 toChildArray 函数将 newParentVNode 中的子节点展平,并放入 newChildren 中,并获得 oldChildrenoldChildrenLength
  • PART 2:该段 if 语句只有在 diffChildren 函数被 render 或是 diffElementNodes 函数所调用时才会进入,并取出 excessDomChildren 或是 oldChildren 中的第一个非空节点作为 oldDom
    大部分情况下,oldDom 会被设为 oldChildren[0]._dom 。如果 excessDomChildren 不为空,则其必然是由 diffElementNodes 函数所调用,其值为 EMPTY_ARR.slice.call(dom.childNodes) ,也即是父真实 DOM 的所有子节点;
  • PART 3:这一部分为 diffChildren 函数的主循环,遍历 newChildren 中的所有 VNode 节点,并执行相应操作;
  • PART 4:对于当前 VNode ,需要寻找到 oldChildren 中对应的节点,根据 Diff 算法所提出的假设,只需要找到拥有相同 key 值的 VNode 或者是不包含 key 值,但拥有相同类型的 VNode
    因此函数首先判断对应下标位置的 VNode 是否符合条件,在很多情况下,这一预判断操作可以帮助省略后续的遍历过程。
    如果对应下标的节点不符合要求,则在 oldChildren 数组中进行线性查找。如果找到了符合要求的 VNode 则将其从数组中删除以避免重复查找;
  • PART 5:对于新旧两个 VNode 调用 diff 函数递归判断两个 VNode 间的差异并更新真实 DOM;
  • PART 6diff 函数执行后,有可能新的真实 DOM 并未挂载到真实 DOM 树中(例如在 diff 函数中未复用已有的真实 DOM 节点,而是创建了一个新的真实 DOM 节点)。此时就需要对该节点进行挂载,如果 oldDom 为空则将其插入父节点末尾,如果不为空则先遍历 oldDomnextSibling 判断该节点是否已被挂载,否则将其插入 oldDom 前。
    (针对该部分我仍存在疑问,函数中使用 j++<oldChildrenLength/2 来控制查找次数,但是其意义在于哪里呢?是否会由于并未遍历全部 oldChildren 而导致新挂载的真实 DOM 节点顺序错乱呢?还请有人能为我解答)
  • PART 7:在完成 newChildren 的遍历操作后,需要将未被复用的真实 DOM 节点(excessDomChildren)以及未被处理的 VNode 节点(oldChildren)进行卸载。

简单小结一下,diffChildren 函数就是为新 VNode 的子节点寻找对应的旧的 VNode,并为其调用 diff 函数。同时,由于在 Preact 中,Diff 操作与 DOM 操作是同步进行的,因此需要一些额外的参数来提升执行效率并保证更新结果的正确性,例如 excessDomChildrenoldDom

diff 函数

diff 函数与 diffChildren 所拥有的参数基本相同,其区别在于 diff 函数多了两个参数:

  • dom :正在执行 Diff 的 VNode 所指向的真实 DOM;
  • force :表示是否强制更新,其针对于组件,用于确定是否通过 shouldComponentUpdate 来判断组件更新的必要。

如本文前言所述,在介绍具体 Diff 实现中将略去组件实现相关内容,以下是 diff 函数中省略了部分组件相关代码的具体实现:

export function diff(dom, parentDom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts, ancestorComponent, force, oldDom) {// PART 1// 如果 oldVnode 与 newVnode 的类型或者 key 值不匹配则将整个原先的 DOM 树抛弃if (oldVNode==null || newVNode==null || oldVNode.type!==newVNode.type || oldVNode.key!==newVNode.key) {if (oldVNode!=null) unmount(oldVNode, ancestorComponent);if (newVNode==null) return null;dom = null;oldVNode = EMPTY_OBJ;}if (options.diff) options.diff(newVNode);let c, p, isNew = false, oldProps, oldState, snapshot,newType = newVNode.type;let clearProcessingException;try {// PART 2// outer: 是标签语法,较为少见,详见 MDN// VNode 类型为 Fragment ,其为子节点的聚合,无需向 DOM 添加额外节点outer: if (oldVNode.type===Fragment || newType===Fragment) {// 由于 Fragment 的元素实际上不挂载到 dom 上,因此直接执行 diffChildrendiffChildren(parentDom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts, c, oldDom);// 将 dom 设为 null,因为 newVNode._children 可能为空dom = null;if (newVNode._children.length) {// 此时 dom 被设定为 children 中的第一个 domdom = newVNode._children[0]._dom;// _lastDomChild 通过 children 中最后一个节点取到,p = newVNode._children[newVNode._children.length - 1];// 当存在嵌套的 Fragment 时 _lastDomChild 的值为 p._lastDomChildnewVNode._lastDomChild = p._lastDomChild || p._dom;}}// PART 3// VNode 类型为类组件或者函数组件else if (typeof newType==='function') {// 省略组件相关代码balabala...}// PART 4// VNode 类型为元素节点else {// 元素节点就直接调用 diffElementNodesdom = diffElementNodes(dom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts, ancestorComponent);// 使用新的 dom 替代原先的 refif (newVNode.ref && (oldVNode.ref !== newVNode.ref)) {applyRef(newVNode.ref, dom, ancestorComponent);}}newVNode._dom = dom;// 省略组件相关代码balabala...}catch (e) {// 在最近的父组件实例中捕获错误catchErrorInComponent(e, ancestorComponent);}return dom;
}

同样的,将 diff 函数划分为几个部分进行介绍:

  • PART 1:首先 diff 函数判断 oldVNodenewVNode 是否为空以及 key 值或者类型是否相同,如果不匹配则根据 Diff 算法的第一条假设,将原有的真实 DOM 树抛弃;
  • PART 2:从 PART 2 开始进入 diff 函数的主体部分,其核心即是对于 newVNode 的类型进行判断,并执行相应的处理。
    首先,判断 newVNode 是否为 Fragment 节点。由于 Fragment 节点仅仅是其子节点的聚合,其本身不需要添加额外的真实 DOM ,因此直接调用 diffChildren 来递归处理其子节点。执行结束 Fragment 的子节点 Diff 后,返回的真实 DOM 为其孩子节点中的第一个 DOM,并且找到最后一个子节点设置 _lastDomChild 值。
    值得一提的是,这一主逻辑的开头 outer: 使用了 JavaScript 标签语法,这一语法规则的应用相当少见,具体规范请参见 MDN;
  • PART 3:判断 newVNode 是否为函数类型,也即是处理 newVNode 为函数组件或者类组件的情况,具体内容将在后续文章中进行介绍;
  • PART 4:排除以上可能类型后,此时 newVNode 必然为元素节点或是文本节点,因此对其调用 diffElementNodes 从而处理 newVNodeoldVNode 之间的差异;同时需要将 oldVNoderef 转为 newVNode
    所有在主逻辑中执行出现的错误均由 catchErrorInComponent(e, ancestorComponent) 移交给最近的父组件进行处理。

diffElementNodes 函数

通过上节的分析,我们了解到 diff 函数实际上执行完对于 newVNode 类型的判断之后,调用了 diffElementNodes 函数来处理元素节点或文本节点的 Diff。
diffElementNodes 函数的具体实现如下所示:

function diffElementNodes(dom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts, ancestorComponent) {let d = dom;// 判断当前节点是否为 SVG 并将该信息沿着 VNode 树向下传递isSvg = newVNode.type==='svg' || isSvg;// PART 1// 从 excessDomChildren 里头选一个相同类型的作为 dom 进行复用,从而省略 dom 创建的代价if (dom==null && excessDomChildren!=null) {for (let 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;excessDomChildren[i] = null;break;}}}// PART 2// 未找到可复用的真实 dom 时为 newVNode 创建 domif (dom==null) {dom = newVNode.type===null ? document.createTextNode(newVNode.text) : isSvg ? document.createElementNS('http://www.w3.org/2000/svg', newVNode.type) : document.createElement(newVNode.type);// 当新创建 dom 时,将 excessDomChildren 设为 null 以标示节点不可复用excessDomChildren = null;}newVNode._dom = dom;// PART 3// 为文本节点时直接更新文本内容if (newVNode.type===null) {if ((d===null || dom===d) && newVNode.text!==oldVNode.text) {dom.data = newVNode.text;}}// PART 4// 为元素节点时else {// 如果 dom 不为新创建的节点时,其子节点有可能被复用if (excessDomChildren!=null && dom.childNodes!=null) {excessDomChildren = EMPTY_ARR.slice.call(dom.childNodes);}if (newVNode!==oldVNode) {let oldProps = oldVNode.props;let newProps = newVNode.props;// 在 hydrating 时,使用元素节点的属性作为 oldPropsif (oldProps==null) {oldProps = {};if (excessDomChildren!=null) {let name;for (let i=0; i<dom.attributes.length; i++) {name = dom.attributes[i].name;oldProps[name=='class' && newProps.className ? 'className' : name] = dom.attributes[i].value;}}}// 设置 dangerouslySetInnerHTMLlet oldHtml = oldProps.dangerouslySetInnerHTML;let newHtml = newProps.dangerouslySetInnerHTML;if (newHtml || oldHtml) {if (!newHtml || !oldHtml || newHtml.__html!=oldHtml.__html) {dom.innerHTML = newHtml && newHtml.__html || '';}}if (newProps.multiple) {dom.multiple = newProps.multiple;}// 递归 diff 其子节点diffChildren(dom, newVNode, oldVNode, context, newVNode.type==='foreignObject' ? false : isSvg, excessDomChildren, mounts, ancestorComponent, EMPTY_OBJ);// diff 新旧 vnode 的属性diffProps(dom, newProps, oldProps, isSvg);}}return dom;
}

分别对 diffElementNodes 函数中的几个部分进行分析:

  • PART 1:函数首先判断 VNode 是否为 SVG 类型,其会对真实 DOM 的属性设置造成一定影响。然后函数从 excessDomChildren 中选择一个相同类型的真实 DOM 节点进行复用,以避免减少重复创建真实 DOM 带来的开支;
  • PART 2:如果 excessDomChildren 中没有符合条件的节点,则进行创建。此时由于 DOM 被新创建,没有可以复用的子节点,因此将 excessDomChildren 置为 null 作为标示;
  • PART 3:当 newVNode 的类型为 null 时,说明其为文本节点,此时仅需要将真实 DOM 中的文本进行更新;
  • PART 4:当 newVNode 为元素节点时,如果该 DOM 并不是新创建的,则将其子节点放入数组中进行复用。对于 newVNode 中的 dangerouslySetInnerHTML 属性进行更新之后,函数调用 diffChildren 对其子节点进行更新,并调用 diffProps 对真实 DOM 的属性进行更新。

diffProps 函数

根据上述函数的介绍,我们已经能够大致了解 Diff 算法执行的基本流程,剩下还有一部分内容就是如何更新 VNodeprops
diffElementNodes 中调用了 diffProps 函数以完成这一工作,其实现如下所示:

export function diffProps(dom, newProps, oldProps, isSvg) {// 更新 newProps 中的新 propsfor (let i in newProps) {if (i!=='children' && i!=='key' && (!oldProps || ((i==='value' || i==='checked') ? dom : oldProps)[i]!==newProps[i])) {setProperty(dom, i, newProps[i], oldProps[i], isSvg);}}// 去除 oldProps 中未被更新的旧 propsfor (let i in oldProps) {if (i!=='children' && i!=='key' && (!newProps || !(i in newProps))) {setProperty(dom, i, null, oldProps[i], isSvg);}}
}

diffProps 函数的实现很简单,只是对 newProps 以及 oldProps 中分别进行遍历,通过调用 setProperty 函数进行 props 的更新或是去除。其中,第一次遍历是为了更新 newProps 中的 props 值,第二次遍历是为了将 oldProps 中未被 newProps 更新的无效值进行去除。

setProperty 的函数实现如下所示:

function setProperty(dom, name, value, oldValue, isSvg) {let v;// 对于 SVG 其设置 class 的属性名为 class,对于非 SVG,其设置 class 的属性名为 classNameif (name==='class' || name==='className') name = isSvg ? 'class' : 'className';// 属性为 style 时if (name==='style') {let s = dom.style;// 如果值为 String,则直接将值赋给 cssTextif (typeof value==='string') {s.cssText = value;}else {// 移除旧的样式if (typeof oldValue==='string') s.cssText = '';else {// remove values not in the new listfor (let i in oldValue) {// 通过 let CAMEL_REG = /-?(?=[A-Z])/g 的正则将驼峰表示法替换为短划线if (value==null || !(i in value)) s.setProperty(i.replace(CAMEL_REG, '-'), '');}}// 添加新的样式for (let i in value) {v = value[i];if (oldValue==null || v!==oldValue[i]) {// 通过 IS_NON_DIMENSIONAL 判断非数值属性,并对数值属性添加 'px's.setProperty(i.replace(CAMEL_REG, '-'), typeof v==='number' && IS_NON_DIMENSIONAL.test(i)===false ? (v + 'px') : v);}}}}// dangerouslySetInnerHTML 属性已在之前的 diff 操作中进行处理,此处跳过else if (name==='dangerouslySetInnerHTML') {return;}// 以 on 开头的属性,也即是各类监听事件else if (name[0]==='o' && name[1]==='n') {// 通过正则匹配判断是事件冒泡或是事件捕获let useCapture = name !== (name=name.replace(/Capture$/, ''));// 转化为小写并截去开头的 on 获得事件名let nameLower = name.toLowerCase();name = (nameLower in dom ? nameLower : name).substring(2);// 事件并不直接通过 addEventListener 进行添加,而是通过 eventProxy 进行代理// 具体的处理函数存放在 dom 的 _listeners 属性中if (value) {if (!oldValue) dom.addEventListener(name, eventProxy, useCapture);}else {dom.removeEventListener(name, eventProxy, useCapture);}(dom._listeners || (dom._listeners = {}))[name] = value;}// 非 list 和 tagName 属性,节点不为 SVG 并且属性存在 dom 中时,直接将该属性添加到 domelse if (name!=='list' && name!=='tagName' && !isSvg && (name in dom)) {dom[name] = value==null ? '' : value;}// 其他类型中,当 value 为空或者 false 时删除属性else if (value==null || value===false) {// 通过正则判断该属性是否为 XLink,并根据判断结果分别选用 removeAttributeNS 和 removeAttribute 方法删除属性if (name!==(name = name.replace(/^xlink:?/, ''))) dom.removeAttributeNS('http://www.w3.org/1999/xlink', name.toLowerCase());else dom.removeAttribute(name);}// 其他类型中,当 value 不为函数时,将该属性添加else if (typeof value!=='function') {if (name!==(name = name.replace(/^xlink:?/, ''))) dom.setAttributeNS('http://www.w3.org/1999/xlink', name.toLowerCase(), value);else dom.setAttribute(name, value);}
}// 通过事件代理可以添加一些钩子
function eventProxy(e) {return this._listeners[e.type](options.event ? options.event(e) : e);
}

setProperty 函数的实现较为明朗,核心就是判断属性名并给出对应的处理。
函数首先根据节点的 SVG 属性判断添加 class 的属性名应当为 class 或是 className。之后,函数对不同的属性名进行分别的处理。
值得一提的是, setProperty 函数在处理事件监听时,并不直接处理函数添加到 dom 上,而是将委托函数添加到 dom 上,用户给定的处理函数则存放到 dom 的 _listener 属性中。通过这一委托函数,可以对 dom 的事件处理添加一些额外的钩子。
其他属性的处理,上述代码结合注释已经相当明确,这里不再赘述。

总结

Diff 算法可以说是 Preact 中最为核心的一部分,本文也花费了大量篇幅对其进行分析。Diff 算法本身并不复杂,在了解了相关的核心概念以后对照源码来看,其代码思路还是较为明晰的。
本文对 Preact 算法中的几个核心函数进行了分析,用以帮助读者快速理清 Preact 中核心逻辑的思路。除此之外还有许多额外的辅助函数,由于精力有限,这里不再展开,还请读者自行阅读。

参考资料

  1. Preact - Github
  2. Reactjs.org - reconciliation
  3. 深入浅出 React(四):虚拟 DOM Diff 算法解析
  4. Preact:一个备胎的自我修养

从 Preact 源码一窥 React 原理(二):Diff 算法相关推荐

  1. 从 Preact 源码一窥 React 原理(一):JSX 渲染

    从 Preact 源码一窥 React 原理(一):JSX 渲染 前言 JSX 渲染 VNode createElement 函数 coerceToVNode 函数 总结 参考资料 系列文章: 从 P ...

  2. react学习笔记 react-router-dom react-redux基础使用及手写基础源码 组件反射 react原理

    vdom diff 高效的diff算法 新老vdom树比较 更新只需要更新节点 数据变化检测 batch dom读写 组件多重继承 //parent components export default ...

  3. Vue源码终笔-VNode更新与diff算法初探

    写完这个就差不多了,准备干新项目了. 确实挺不擅长写东西,感觉都是罗列代码写点注释的感觉,这篇就简单阐述一下数据变动时DOM是如何更新的,主要讲解下其中的diff算法. 先来个正常的html模板: & ...

  4. 【手写 Vue2.x 源码】第二十八篇 - diff 算法-问题分析与 patch 优化

    一,前言 首先,对 6 月的更文内容做一下简单回顾: Vue2.x 源码环境的搭建 Vue2.x 初始化流程介绍 对象的单层.深层劫持 数组的单层.深层劫持 数据代理的实现 对象.数组数据变化的观测 ...

  5. 【手写 Vue2.x 源码】第三十一篇 - diff 算法 - 比对优化(下)

    一,前言 上篇,diff 算法-比对优化(上),主要涉及以下几个点: 介绍了如何对儿子节点进行比对: 新老儿子节点可能存在的 3 种情况及代码实现: 新老节点都有儿子时,diff 的方案介绍与处理逻辑 ...

  6. 深入Preact源码分析(4.20更新)

    React的源码多达几万行,对于我们想要快速阅读并看懂是相当有难度的,而Preact是一个轻量级的类react库,几千行代码就实现了react的大部分功能.因此阅读preact源码,对于我们学习rea ...

  7. preact源码分析

    前言 前两个星期花了一些时间学习preact的源码, 并写了几篇博客.但是现在回头看看写的并不好,而且源码的有些地方(diffChildren的部分)我还理解?错了.实在是不好意思.所以这次准备重新写 ...

  8. react 组件遍历】_从 Context 源码实现谈 React 性能优化

    (给前端大全加星标,提升前端技能) 转自:魔术师卡颂 学完这篇文章,你会收获: 了解Context的实现原理 源码层面掌握React组件的render时机,从而写出高性能的React组件 源码层面了解 ...

  9. preact源码分析,有毒

    最近读了读preact源码,记录点笔记,这里采用例子的形式,把代码的执行过程带到源码里走一遍,顺便说明一些重要的点,建议对着preact源码看 vnode和h() 虚拟结点是对真实DOM元素的一个js ...

最新文章

  1. SQL函数--- SQL FIRST()
  2. 破解RSA的一些技术
  3. JavaScript中创建对象的方法
  4. js进阶 11-15 jquery过滤方法有哪些
  5. Chrome(谷歌浏览器)插件资料 !
  6. Nexus3 安装 及 配置 docker 私有、代理 仓库
  7. 分布式机器学习\分布式KMeans
  8. java 创建gbase,GBase 8t使用Java UDR的方法
  9. SpringCloud(8)— 使用ElasticSearch(RestClient)
  10. android音频驱动工程师,4.Android音频驱动(底层1)
  11. 阿里云物联网IOT平台使用案例教程(模拟智能设备)
  12. 页面提交成功后,弹窗提示
  13. 数据库码的概念,全码的例子与范式的联系
  14. TP6 TP5 Db‘ not found
  15. 计算机应用在我们生活中的哪些方面,计算机在我们生活中的应用
  16. 什么是创新,什么是发明
  17. 苹果平板如何截屏_原来苹果手机自带长截屏功能!以前一直不知道,真让人相见恨晚...
  18. java duck的屏幕保护程序
  19. React-从0到1搭建一个React项目(一)
  20. html网页设计作品初级,《HTML网页设计技术》教案.doc

热门文章

  1. 探秘“科技工业美学”,传祺影酷让设计先于时代
  2. python对用户评价内容进行语义情感分析
  3. 演出商业怎么实施RPA虚拟员工提高计费效率
  4. 接口自动化测试工具- 基础篇:postman 断言
  5. 小程序图片上传和Promise.all
  6. 想说爱你不容易——致Javascript社区的一封信
  7. vue两个卡片并排_Vue组件-卡片层叠拖拽
  8. 将无损音乐flac,ape,wav格式导入ipod方法
  9. audio与video
  10. 数据库隔离级别(四种)