系列文章:
前一篇文章《从 Preact 源码一窥 React 原理(二):JSX 渲染》作为铺垫,简单介绍了 JSX 转化为 Preact 中虚拟 DOM 节点 VNode
的相关函数以及数据结构。
本文则将走进 Preact 中最为核心的部分之一:Diff 算法,介绍 Preact 如何渲染并更新得到的 VNode
树。
为了简单起见,本文主要内容仅集中于非组件节点的 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
翻译一下:
根据以上的假设,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 树。
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 作为 oldDom
if (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 3
for (i=0; i<newChildren.length; i++) {
// 拷贝包含 dom 的 VNode 并将非 VNode 的节点转化为 VNode
childVNode = 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 类型的 VNode
for (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++
// 事实上即使不进行判断操作,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 的下一个兄弟节点 nextDom
oldDom = 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 由对应的注释所标示)。
toChildArray
函数将 newParentVNode
中的子节点展平,并放入 newChildren
中,并获得 oldChildren
和 oldChildrenLength
;diffChildren
函数被 render
或是 diffElementNodes
函数所调用时才会进入,并取出 excessDomChildren
或是 oldChildren
中的第一个非空节点作为 oldDom
。oldDom
会被设为 oldChildren[0]._dom
。如果 excessDomChildren
不为空,则其必然是由 diffElementNodes
函数所调用,其值为 EMPTY_ARR.slice.call(dom.childNodes)
,也即是父真实 DOM 的所有子节点;diffChildren
函数的主循环,遍历 newChildren
中的所有 VNode
节点,并执行相应操作;VNode
,需要寻找到 oldChildren
中对应的节点,根据 Diff 算法所提出的假设,只需要找到拥有相同 key 值的 VNode
或者是不包含 key 值,但拥有相同类型的 VNode
。VNode
是否符合条件,在很多情况下,这一预判断操作可以帮助省略后续的遍历过程。oldChildren
数组中进行线性查找。如果找到了符合要求的 VNode
则将其从数组中删除以避免重复查找;VNode
调用 diff
函数递归判断两个 VNode
间的差异并更新真实 DOM;diff
函数执行后,有可能新的真实 DOM 并未挂载到真实 DOM 树中(例如在 diff
函数中未复用已有的真实 DOM 节点,而是创建了一个新的真实 DOM 节点)。此时就需要对该节点进行挂载,如果 oldDom
为空则将其插入父节点末尾,如果不为空则先遍历 oldDom
的 nextSibling
判断该节点是否已被挂载,否则将其插入 oldDom
前。j++ 来控制查找次数,但是其意义在于哪里呢?是否会由于并未遍历全部 oldChildren
而导致新挂载的真实 DOM 节点顺序错乱呢?还请有人能为我解答)
newChildren
的遍历操作后,需要将未被复用的真实 DOM 节点(excessDomChildren
)以及未被处理的 VNode 节点(oldChildren
)进行卸载。简单小结一下,diffChildren
函数就是为新 VNode
的子节点寻找对应的旧的 VNode
,并为其调用 diff
函数。同时,由于在 Preact 中,Diff 操作与 DOM 操作是同步进行的,因此需要一些额外的参数来提升执行效率并保证更新结果的正确性,例如 excessDomChildren
与 oldDom
。
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 上,因此直接执行 diffChildren
diffChildren(parentDom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts, c, oldDom);
// 将 dom 设为 null,因为 newVNode._children 可能为空
dom = null;
if (newVNode._children.length) {
// 此时 dom 被设定为 children 中的第一个 dom
dom = newVNode._children[0]._dom;
// _lastDomChild 通过 children 中最后一个节点取到,
p = newVNode._children[newVNode._children.length - 1];
// 当存在嵌套的 Fragment 时 _lastDomChild 的值为 p._lastDomChild
newVNode._lastDomChild = p._lastDomChild || p._dom;
}
}
// PART 3
// VNode 类型为类组件或者函数组件
else if (typeof newType==='function') {
// 省略组件相关代码
balabala...
}
// PART 4
// VNode 类型为元素节点
else {
// 元素节点就直接调用 diffElementNodes
dom = diffElementNodes(dom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts, ancestorComponent);
// 使用新的 dom 替代原先的 ref
if (newVNode.ref && (oldVNode.ref !== newVNode.ref)) {
applyRef(newVNode.ref, dom, ancestorComponent);
}
}
newVNode._dom = dom;
// 省略组件相关代码
balabala...
}
catch (e) {
// 在最近的父组件实例中捕获错误
catchErrorInComponent(e, ancestorComponent);
}
return dom;
}
同样的,将 diff
函数划分为几个部分进行介绍:
diff
函数判断 oldVNode
与 newVNode
是否为空以及 key 值或者类型是否相同,如果不匹配则根据 Diff 算法的第一条假设,将原有的真实 DOM 树抛弃;diff
函数的主体部分,其核心即是对于 newVNode
的类型进行判断,并执行相应的处理。newVNode
是否为 Fragment
节点。由于 Fragment
节点仅仅是其子节点的聚合,其本身不需要添加额外的真实 DOM ,因此直接调用 diffChildren
来递归处理其子节点。执行结束 Fragment
的子节点 Diff 后,返回的真实 DOM 为其孩子节点中的第一个 DOM,并且找到最后一个子节点设置 _lastDomChild
值。outer:
使用了 JavaScript 标签语法,这一语法规则的应用相当少见,具体规范请参见 MDN;newVNode
是否为函数类型,也即是处理 newVNode
为函数组件或者类组件的情况,具体内容将在后续文章中进行介绍;newVNode
必然为元素节点或是文本节点,因此对其调用 diffElementNodes
从而处理 newVNode
与 oldVNode
之间的差异;同时需要将 oldVNode
的 ref
转为 newVNode
;catchErrorInComponent(e, ancestorComponent)
移交给最近的父组件进行处理。通过上节的分析,我们了解到 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 创建 dom
if (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 时,使用元素节点的属性作为 oldProps
if (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;
}
}
}
// 设置 dangerouslySetInnerHTML
let 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
函数中的几个部分进行分析:
VNode
是否为 SVG 类型,其会对真实 DOM 的属性设置造成一定影响。然后函数从 excessDomChildren
中选择一个相同类型的真实 DOM 节点进行复用,以避免减少重复创建真实 DOM 带来的开支;excessDomChildren
中没有符合条件的节点,则进行创建。此时由于 DOM 被新创建,没有可以复用的子节点,因此将 excessDomChildren
置为 null
作为标示;newVNode
的类型为 null
时,说明其为文本节点,此时仅需要将真实 DOM 中的文本进行更新;newVNode
为元素节点时,如果该 DOM 并不是新创建的,则将其子节点放入数组中进行复用。对于 newVNode
中的 dangerouslySetInnerHTML
属性进行更新之后,函数调用 diffChildren
对其子节点进行更新,并调用 diffProps
对真实 DOM 的属性进行更新。根据上述函数的介绍,我们已经能够大致了解 Diff 算法执行的基本流程,剩下还有一部分内容就是如何更新 VNode
的 props
。
在 diffElementNodes
中调用了 diffProps
函数以完成这一工作,其实现如下所示:
export function diffProps(dom, newProps, oldProps, isSvg) {
// 更新 newProps 中的新 props
for (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 中未被更新的旧 props
for (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 的属性名为 className
if (name==='class' || name==='className') name = isSvg ? 'class' : 'className';
// 属性为 style 时
if (name==='style') {
let s = dom.style;
// 如果值为 String,则直接将值赋给 cssText
if (typeof value==='string') {
s.cssText = value;
}
else {
// 移除旧的样式
if (typeof oldValue==='string') s.cssText = '';
else {
// remove values not in the new list
for (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 中时,直接将该属性添加到 dom
else 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 中核心逻辑的思路。除此之外还有许多额外的辅助函数,由于精力有限,这里不再展开,还请读者自行阅读。