init 函数 exportinterfaceModule { pre: PreHook; create: CreateHook;
update: UpdateHook; destroy: DestroyHook; remove: RemoveHook; post:
PostHook;} exportfunctioninit(modules:
Array<Partial<Module>>, domApi?: DOMAPI) { // cbs 用于收集
module 中的 hookleti: number, j: number, cbs = {} asModuleHooks;
constapi: DOMAPI = domApi !== undefined? domApi : htmlDomApi; // 收集
module 中的 hookfor(i = 0; i < hooks.length; ++i) { cbs[hooks[i]]
= []; for(j = 0; j < modules.length; ++j) { consthook =
modules[j][hooks[i]]; if(hook !== undefined) { (cbs[hooks[i]]
asArray< any>).push(hook); } } } functionemptyNodeAt(elm: Element)
{ // …} functioncreateRmCb(childElm: Node, listeners: number) { //
…} // 创建真正的 dom 节点functioncreateElm(vnode: VNode,
insertedVnodeQueue: VNodeQueue): Node{ // …}
functionaddVnodes(parentElm: Node, before: Node | null, vnodes:
Array<VNode>, startIdx: number, endIdx: number,
insertedVnodeQueue: VNodeQueue ) { // …} // 调用 destory hook//
如果存在 children 递归调用functioninvokeDestroyHook(vnode: VNode) { //
…} functionremoveVnodes(parentElm: Node, vnodes: Array<VNode>,
startIdx: number, endIdx: number): void{ // …}
functionupdateChildren(parentElm: Node, oldCh: Array<VNode>,
newCh: Array<VNode>, insertedVnodeQueue: VNodeQueue) { // …}
functionpatchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue:
VNodeQueue) { // …} returnfunctionpatch(oldVnode: VNode | Element,
vnode: VNode): VNode{ // …};} 复制代码

这里意味着如果 num 没有改变的话,那对 vnode 进行 patch 就是没有意义的,
对于这种情况,snabbdom 提供了一种优化手段,也就是
thunk,该函数同样返回一个 vnode 节点,但是在 patchVnode
开始时,会对参数进行一次比较,如果相同,将结束对比,这个有点类似于 React
的 pureComponent,pureComponent 的实现上会做一次浅比较 shadowEqual,结合
immutable 数据进行使用效果更加。上面的例子可以变成这样。

  • 均存在 children 且不相同,调用 updateChildren
  • 新 vnode 存在 children,旧 vnode 不存在 children,如果旧 vnode 存在
    text 先清空,然后调用 addVnodes
  • 新 vnode 不存在 children,旧 vnode 存在 children,调用 removeVnodes
    移除 children
  • 均不存在 children,新 vnode 不存在 text,移除旧 vnode 的 text
  • 均存在 text,更新 text

到这里 snabbdom 的核心源码已经阅读完毕,剩下的还有一些内置的
module,有兴趣的可以自行阅读。返回搜狐,查看更多

h 函数

最后调用 postpatch hook。整个过程很清晰,我们需要关注的是 updateChildren
addVnodesremoveVnodes。

结语

因为 h
函数后两个参数是可选的,而且有各种传递方式,所以这里首先会对参数进行格式化,然后对
children 属性做处理,将可能不是 vnode 的项转成 vnode,如果是 svg
元素,会做一个特殊处理,最后返回一个 vnode 对象。

为什么选择 snabbdom

这两个函数主要用来添加 vnode 和移除 vnode,代码逻辑基本都能看懂。

  • 如果当前元素是注释节点,会调用 createComment
    来创建一个注释节点,然后挂载到 vnode.elm
  • 如果不存在选择器,只是单纯的文本,调用 createTextNode
    来创建文本,然后挂载到 vnode.elm
  • 如果存在选择器,会对这个选择器做解析,得到 tag、id 和
    class,然后调用 或 NS 来生成节点,并挂载到 vnode.elm。接着调用
    module 上的 create hook,如果存在 children,遍历所有子节点并递归调用
    createElm 创建 dom,通过 挂载到当前的 elm 上,不存在 children 但存在
    text,便使用 createTextNode 来创建文本。最后调用调用元素上的 create
    hook和保存存在 insert hook 的 vnode,因为 insert hook 需要等 dom
    真正挂载到 document
    上才会调用,这里用个数组来保存可以避免真正需要调用时需要对 vnode
    树做遍历。

snabbdom 是 Virtual DOM 的一种实现,所以在此之前,你需要先知道什么是
Virtual DOM。通俗的说,Virtual DOM 就是一个 js 对象,它是真实 DOM
的抽象,只保留一些有用的信息,更轻量地描述 DOM 树的结构。 比如在
snabbdom 中,是这样来定义一个 VNode 的:

functionpatch(oldVnode: VNode | Element, vnode: VNode): VNode{ leti:
number, elm: Node, parent: Node; constinsertedVnodeQueue: VNodeQueue =
[]; // 调用 module 中的 pre hookfor(i = 0; i < cbs.pre.length; ++i)
cbs.pre[i](); // 如果传入的是 Element 转成空的
vnodeif(!isVnode(oldVnode)) { oldVnode = emptyNodeAt(oldVnode); } //
sameVnode 时 (sel 和 key相同) 调用 patchVnodeif(sameVnode(oldVnode,
vnode)) { patchVnode(oldVnode, vnode, insertedVnodeQueue); } else{ elm =
oldVnode.elm asNode; parent = api.parentNode(elm); // 创建新的 dom 节点
vnode.elmcreateElm(vnode, insertedVnodeQueue); if(parent !== null) { //
插入 domapi.insertBefore(parent, vnode.elm asNode,
api.nextSibling(elm)); // 移除旧 domremoveVnodes(parent, [oldVnode],
0, 0); } } // 调用元素上的 insert hook,注意 insert hook 在 module
上不支持for(i = 0; i < insertedVnodeQueue.length; ++i) {
(((insertedVnodeQueue[i].data asVNodeData).hook asHooks).insert
asany)(insertedVnodeQueue[i]); } // 调用 module post hookfor(i = 0; i
< cbs.post.length; ++i) cbs.post[i](); returnvnode;}
functionemptyNodeAt(elm: Element) { constid = elm.id ? ‘#’+ elm.id :
”; constc = elm.className ? ‘.’+ elm.className.split( ‘ ‘).join( ‘.’) :
”; returnvnode(api.tagName(elm).toLowerCase() + id + c, {}, [],
undefined, elm);} // key 和 selector 相同functionsameVnode(vnode1:
VNode, vnode2: VNode): boolean{ returnvnode1.key === vnode2.key &&
vnode1.sel === vnode2.sel;} 复制代码

源码分析

  1. oldStartVnode 和 newStartVnode 相等,直接 patchVnode,newStartIdx 和
    oldStartIdx 向中间移动,状态更新如下

varsnabbdom = require( ‘snabbdom’); varpatch = snabbdom.init([ // Init
patch function with chosen modulesrequire(
‘snabbdom/modules/class’).default, // makes it easy to toggle
classesrequire( ‘snabbdom/modules/props’).default, // for setting
properties on DOM elementsrequire( ‘snabbdom/modules/style’).default, //
handles styling on elements with support for animationsrequire(
‘snabbdom/modules/eventlisteners’).default // attaches event
listeners]); varh = require( ‘snabbdom/h’).default; // helper function
for creating vnodesvarcontainer = document.getElementById( ‘container’);
varvnode = h( ‘div#container.two.classes’, { on: { click: someFn } },
[ h( ‘span’, { style: { fontWeight: ‘bold’} }, ‘This is bold’), ‘ and
this is just normal text’, h( ‘a’, { props: { href: ‘/foo’} }, “I’ll
take you places!”)]); // Patch into empty DOM element – this modifies
the DOM as a side effectpatch(container, vnode); varnewVnode = h(
‘div#container.two.classes’, { on: { click: anotherEventHandler } }, [
h( ‘span’, { style: { fontWeight: ‘normal’, fontStyle: ‘italic’} },
‘This is now italic type’), ‘ and this is still just normal text’, h(
‘a’, { props: { href: ‘/bar’} }, “I’ll take you places!”)]); // Second
`patch` invocationpatch(vnode, newVnode); // Snabbdom efficiently
updates the old view to the new state复制代码

接着我们来看看 snabbdom 是如何做 vnode 的 diff 的,这部分是 Virtual DOM
的核心。

首先 snabbdom 模块提供一个 init 方法,它接收一个数组,数组中是各种
module,这样的设计使得这个库更具扩展性,我们也可以实现自己的
module,而且可以根据自己的需要引入相应的 module,比如如果不需要写入
class,那你可以直接把 class 的模块移除。 调用 init 方法会返回一个 patch
函数,这个函数接受两个参数,第一个是旧的 vnode 节点或是 dom
节点,第二个参数是新的 vnode 节点,调用 patch 函数会对 dom
进行更新。vnode
可以通过使用h函数来生成。使用起来相当简单,这也是本文接下来要分析的内容。

图片 1

根据例子的流程,接下来看看h方法的实现

functionrenderNumber(num) { returnh( ‘span’, num);} functionrender(num)
{ returnthunk( ‘div’, renderNumber, [num]);} varvnode =
patch(container, render( 1)) // 由于num 相同,renderNumber
不会执行patch(vnode, render( 1)) 复制代码

随着 React Vue 等框架的流行,Virtual DOM 也越来越火,snabbdom
是其中一种实现,而且 Vue 2.x 版本的 Virtual DOM 部分也是基于 snabbdom
进行修改的。snabbdom 这个库核心代码只有 200 多行,非常适合想要深入了解
Virtual DOM 实现的读者阅读。如果您没听说过
snabbdom,可以先看看官方文档。

接下来我们来分析这整个过程的实现。

exportfunctionh(sel: string): VNode; exportfunctionh(sel: string, data:
VNodeData): VNode; exportfunctionh(sel: string, children:
VNodeChildren): VNode; exportfunctionh(sel: string, data: VNodeData,
children: VNodeChildren): VNode; exportfunctionh(sel: any, b?: any, c?:
any): VNode{ vardata: VNodeData = {}, children: any, text: any, i:
number; // 参数格式化if(c !== undefined) { data = b; if(is.array(c)) {
children = c; } elseif(is.primitive(c)) { text = c; } elseif(c && c.sel)
{ children = [c]; } } elseif(b !== undefined) { if(is.array(b)) {
children = b; } elseif(is.primitive(b)) { text = b; } elseif(b && b.sel)
{ children = [b]; } else{ data = b; } } // 如果存在 children,将不是
vnode 的项转成 vnodeif(children !== undefined) { for(i = 0; i <
children.length; ++i) { if(is.primitive(children[i])) children[i] =
vnode( undefined, undefined, undefined, children[i], undefined); } }
// svg 元素添加 namespaceif(sel[ 0] === ‘s’&& sel[ 1] === ‘v’&&
sel[ 2] === ‘g’&& (sel.length === 3|| sel[ 3] === ‘.’|| sel[ 3]
=== ‘#’)) { addNS(data, children, sel); } // 返回 vnodereturnvnode(sel,
data, children, text, undefined);} functionaddNS(data: any, children:
VNodes | undefined, sel: string| undefined): void{ data.ns =
http://www.w3.org/2000/svg‘; if(sel !== ‘foreignObject’&& children !==
undefined) { for( leti = 0; i < children.length; ++i) { letchildData
= children[i].data; if(childData !== undefined) { addNS(childData,
(children[i] asVNode).children asVNodes, children[i].sel); } } }}
exportfunctionvnode(sel: string| undefined, data: any| undefined,
children: Array<VNode | string> | undefined, text: string|
undefined, elm: Element | Text | undefined): VNode{ letkey = data ===
undefined? undefined: data.key; return{ sel: sel, data: data, children:
children, text: text, elm: elm, key: key };} 复制代码

图片 2

functionpatchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue:
VNodeQueue) { leti: any, hook: any; // 调用 prepatch hookif(isDef((i =
vnode.data)) && isDef((hook = i.hook)) && isDef((i = hook.prepatch))) {
i(oldVnode, vnode); } constelm = (vnode.elm = oldVnode.elm asNode);
letoldCh = oldVnode.children; letch = vnode.children; if(oldVnode ===
vnode) return; if(vnode.data !== undefined) { // 调用 module 上的 update
hookfor(i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode,
vnode); i = vnode.data.hook; // 调用 vnode 上的 update hookif(isDef(i)
&& isDef((i = i.update))) i(oldVnode, vnode); } if(isUndef(vnode.text))
{ if(isDef(oldCh) && isDef(ch)) { // 新旧节点均存在
children,且不一样时,对 children 进行 diffif(oldCh !== ch)
updateChildren(elm, oldCh asArray<VNode>, ch asArray<VNode>,
insertedVnodeQueue); } elseif(isDef(ch)) { // 旧节点不存在 children
新节点有 children// 旧节点存在 text 置空if(isDef(oldVnode.text))
api.setTextContent(elm, ”); // 加入新的 vnodeaddVnodes(elm, null, ch
asArray<VNode>, 0, (ch asArray<VNode>).length – 1,
insertedVnodeQueue); } elseif(isDef(oldCh)) { // 新节点不存在 children
旧节点存在 children 移除旧节点的 childrenremoveVnodes(elm, oldCh
asArray<VNode>, 0, (oldCh asArray<VNode>).length – 1); }
elseif(isDef(oldVnode.text)) { // 旧节点存在 text
置空api.setTextContent(elm, ”); } } elseif(oldVnode.text !==
vnode.text) { // 更新 textapi.setTextContent(elm, vnode.text asstring);
} // 调用 postpatch hookif(isDef(hook) && isDef((i = hook.postpatch))) {
i(oldVnode, vnode); }} 复制代码

责任编辑:

  1. oldStartIdx 已经大于
    oldEndIdx,循环结束,由于是旧节点先结束循环而且还有没处理的新节点,调用
    addVnodes 处理剩下的新节点

patch 函数是 snabbdom 的核心,调用 init 会返回这个函数,用来做 dom
相关的更新,接下来看看它的具体实现。

首先从一个简单的例子入手,一步一步分析整个代码的执行过程,下面是官方的一个简单示例:

这个函数做的事情是对传入的两个 vnode 做 diff,如果存在更新,将其反馈到
dom 上。

addVnodes 和 removeVnodes 函数 functionaddVnodes(parentElm: Node,
before: Node | null, vnodes: Array<VNode>, startIdx: number,
endIdx: number, insertedVnodeQueue: VNodeQueue) { for(; startIdx <=
endIdx; ++startIdx) { constch = vnodes[startIdx]; if(ch != null) {
api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);
} }} functionremoveVnodes(parentElm: Node, vnodes: Array<VNode>,
startIdx: number, endIdx: number): void{ for(; startIdx <= endIdx;
++startIdx) { leti: any, listeners: number, rm: ()=> void, ch =
vnodes[startIdx]; if(ch != null) { if(isDef(ch.sel)) { // 调用 destory
hookinvokeDestroyHook(ch); // 计算需要调用 removecallback 的次数
只有全部调用了才会移除 domlisteners = cbs.remove.length + 1; rm =
createRmCb(ch.elm asNode, listeners); // 调用 module 中是 remove hook
for(i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm); //
调用 vnode 的 remove hook if(isDef(i = ch.data) && isDef(i = i.hook) &&
isDef(i = i.remove)) { i(ch, rm); } else{ rm(); } } else{ // Text
nodeapi.removeChild(parentElm, ch.elm asNode); } } }} // 调用 destory
hook // 如果存在 children 递归调用 functioninvokeDestroyHook(vnode:
VNode) { leti: any, j: number, data = vnode.data; if(data !== undefined)
{ if(isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode); for(i = 0;
i < cbs.destroy.length; ++i) cbs.destroy[i](vnode);
if(vnode.children !== undefined) { for(j = 0; j <
vnode.children.length; ++j) { i = vnode.children[j]; if(i != null&&
typeofi !== “string”) { invokeDestroyHook(i); } } } }} // 只有当所有的
remove hook 都调用了 remove callback 才会移除 dom
functioncreateRmCb(childElm: Node, listeners: number) { return
functionrmCb() { if(–listeners === 0) { constparent =
api.parentNode(childElm); api.removeChild(parent, childElm); } };} 复制代码

  1. 第二轮比较:oldStartVnode 和 newStartVnode 相等,直接
    patchVnode,newStartIdx 和 oldStartIdx 向中间移动,状态更新如下

原标题:snabbdom 源码阅读分析

首先会调用 module 的 pre
hook,你可能会有疑惑,为什么没有调用来自各个元素的 pre
hook,这是因为元素上不支持 pre hook,也有一些 hook 不支持在 module
中,具体可以查看这里的文档。然后会判断传入的第一个参数是否为 vnode
类型,如果不是,会调用 emptyNodeAt 然后将其转换成一个 vnode,emptyNodeAt
的具体实现也很简单,注意这里只是保留了 class 和 style,这个和 toVnode
的实现有些区别,因为这里并不需要保存很多信息,比如 prop attribute
等。接着调用 sameVnode 来判断是否为相同的 vnode
节点,具体实现也很简单,这里只是判断了 key 和 sel
是否相同。如果相同,调用 patchVnode,如果不相同,会调用 createElm
来创建一个新的 dom 节点,然后如果存在父节点,便将其插入到 dom
上,然后移除旧的 dom 节点来完成更新。最后调用元素上的 insert hook 和
module 上的 post hook。 这里的重点是 patchVnode 和 createElm
函数,我们先看 createElm 函数,看看是如何来创建 dom 节点的。

一般我们的应用是根据 js 状态来更新的,比如下面这个例子

整个过程简单来说,对两个数组进行对比,找到相同的部分进行复用,并更新。整个逻辑可能看起来有点懵,可以结合下面这个例子理解下:

export interface VNode { sel: string | undefined; data: VNodeData |
undefined; children: Array<VNode | string> | undefined; elm: Node
| undefined; text: string | undefined; key: Key | undefined;}export
interface VNodeData { props?: Props; attrs?: Attrs; class?: Classes;
style?: VNodeStyle; dataset?: Dataset; on?: On; hero?: Hero;
attachData?: AttachData; hook?: Hooks; key?: Key; ns?: string; // for
SVGs fn?: () => VNode; // for thunks args?: Array<any>; // for
thunks [key: string]: any; // for any other 3rd party module} 复制代码

从上面的定义我们可以看到,我们可以用 js 对象来描述 dom
结构,那我们是不是可以对两个状态下的 js
对象进行对比,记录出它们的差异,然后把它应用到真正的 dom
树上呢?答案是可以的,这便是 diff 算法,算法的基本步骤如下:

patchVnode 函数

updateChildren functionupdateChildren(parentElm: Node, oldCh:
Array<VNode>, newCh: Array<VNode>, insertedVnodeQueue:
VNodeQueue) { letoldStartIdx = 0, newStartIdx = 0; letoldEndIdx =
oldCh.length – 1; letoldStartVnode = oldCh[ 0]; letoldEndVnode =
oldCh[oldEndIdx]; letnewEndIdx = newCh.length – 1; letnewStartVnode =
newCh[ 0]; letnewEndVnode = newCh[newEndIdx]; letoldKeyToIdx: any;
letidxInOld: number; letelmToMove: VNode; letbefore: any; // 遍历 oldCh
newCh,对节点进行比较和更新// 每轮比较最多处理一个节点,算法复杂度
O(n)while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 如果进行比较的 4
个节点中存在空节点,为空的节点下标向中间推进,继续下个循环if(oldStartVnode
== null) { oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have
been moved left} elseif(oldEndVnode == null) { oldEndVnode =
oldCh[–oldEndIdx]; } elseif(newStartVnode == null) { newStartVnode =
newCh[++newStartIdx]; } elseif(newEndVnode == null) { newEndVnode =
newCh[–newEndIdx]; // 新旧开始节点相同,直接调用 patchVnode
进行更新,下标向中间推进} elseif(sameVnode(oldStartVnode,
newStartVnode)) { patchVnode(oldStartVnode, newStartVnode,
insertedVnodeQueue); oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx]; // 新旧结束节点相同,逻辑同上}
elseif(sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode,
newEndVnode, insertedVnodeQueue); oldEndVnode = oldCh[–oldEndIdx];
newEndVnode = newCh[–newEndIdx]; //
旧开始节点等于新的节点节点,说明节点向右移动了,调用 patchVnode
进行更新} elseif(sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved
rightpatchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue); //
旧开始节点等于新的结束节点,说明节点向右移动了//
具体移动到哪,因为新节点处于末尾,所以添加到旧结束节点(会随着
updateChildren 左移)的后面// 注意这里需要移动
dom,因为节点右移了,而为什么是插入 oldEndVnode 的后面呢?//
可以分为两个情况来理解:// 1. 当循环刚开始,下标都还没有移动,那移动到
oldEndVnode 的后面就相当于是最后面,是合理的// 2.
循环已经执行过一部分了,因为每次比较结束后,下标都会向中间靠拢,而且每次都会处理一个节点,//
这时下标左右两边已经处理完成,可以把下标开始到结束区域当成是并未开始循环的一个整体,//
所以插入到 oldEndVnode
后面是合理的(在当前循环来说,也相当于是最后面,同
1)api.insertBefore(parentElm, oldStartVnode.elm asNode,
api.nextSibling(oldEndVnode.elm asNode)); oldStartVnode =
oldCh[++oldStartIdx]; newEndVnode = newCh[–newEndIdx]; //
旧的结束节点等于新的开始节点,说明节点是向左移动了,逻辑同上}
elseif(sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved
leftpatchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
api.insertBefore(parentElm, oldEndVnode.elm asNode, oldStartVnode.elm
asNode); oldEndVnode = oldCh[–oldEndIdx]; newStartVnode =
newCh[++newStartIdx]; // 如果以上 4 种情况都不匹配,可能存在下面 2
种情况// 1. 这个节点是新创建的// 2.
这个节点在原来的位置是处于中间的(oldStartIdx 和 endStartIdx之间)}
else{ // 如果 oldKeyToIdx 不存在,创建 key 到 index 的映射//
而且也存在各种细微的优化,只会创建一次,并且已经完成的部分不需要映射if(oldKeyToIdx
=== undefined) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx,
oldEndIdx); } // 拿到在 oldCh 下对应的下标idxInOld =
oldKeyToIdx[newStartVnode.key asstring]; //
如果下标不存在,说明这个节点是新创建的if(isUndef(idxInOld)) { // New
element// 插入到 oldStartVnode
的前面(对于当前循环来说,相当于最前面)api.insertBefore(parentElm,
createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm asNode);
newStartVnode = newCh[++newStartIdx]; } else{ // 如果是已经存在的节点
找到需要移动位置的节点elmToMove = oldCh[idxInOld]; // 虽然 key
相同了,但是 seletor 不相同,需要调用 createElm 来创建新的 dom
节点if(elmToMove.sel !== newStartVnode.sel) {
api.insertBefore(parentElm, createElm(newStartVnode,
insertedVnodeQueue), oldStartVnode.elm asNode); } else{ // 否则调用
patchVnode 对旧 vnode 做更新patchVnode(elmToMove, newStartVnode,
insertedVnodeQueue); // 在 oldCh 中将当前已经处理的 vnode
置空,等下次循环到这个下标的时候直接跳过oldCh[idxInOld] =
undefinedasany; // 插入到 oldStartVnode
的前面(对于当前循环来说,相当于最前面)api.insertBefore(parentElm,
elmToMove.elm asNode, oldStartVnode.elm asNode); } newStartVnode =
newCh[++newStartIdx]; } } } // 循环结束后,可能会存在两种情况// 1.
oldCh 已经全部处理完成,而 newCh
还有新的节点,需要对剩下的每个项都创建新的 domif(oldStartIdx <=
oldEndIdx || newStartIdx <= newEndIdx) { if(oldStartIdx >
oldEndIdx) { before = newCh[newEndIdx + 1] == null? null:
newCh[newEndIdx + 1].elm; addVnodes(parentElm, before, newCh,
newStartIdx, newEndIdx, insertedVnodeQueue); // 2. newCh
已经全部处理完成,而 oldCh 还有旧的节点,需要将多余的节点移除} else{
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); } }} 复制代码

patch 函数

这里的逻辑也很清晰,首先会调用元素的 init hook,接着这里会存在三种情况:

exportinterface ThunkFn { (sel: string, fn: Function, args:
Array<any>): Thunk; (sel: string, key: any, fn: Function, args:
Array<any>): Thunk;} // 使用 h 函数返回 vnode,为其添加 init 和
prepatch 钩子exportconstthunk = functionthunk(sel: string, key?: any,
fn?: any, args?: any): VNode{ if(args === undefined) { args = fn; fn =
key; key = undefined; } returnh(sel, { key: key, hook: { init: init,
prepatch: prepatch}, fn: fn, args: args });} asThunkFn; // 将 vnode
上的数据拷贝到 thunk 上,在 patchVnode 中会进行判断,如果相同会结束
patchVnode// 并将 thunk 的 fn 和 args 属性保存到 vnode 上,在 prepatch
时需要进行比较functioncopyToThunk(vnode: VNode, thunk: VNode): void{
thunk.elm = vnode.elm; (vnode.data asVNodeData).fn = (thunk.data
asVNodeData).fn; (vnode.data asVNodeData).args = (thunk.data
asVNodeData).args; thunk.data = vnode.data; thunk.children =
vnode.children; thunk.text = vnode.text; thunk.elm = vnode.elm;}
functioninit(thunk: VNode): void{ constcur = thunk.data asVNodeData;
constvnode = (cur.fn asany).apply( undefined, cur.args);
copyToThunk(vnode, thunk);} functionprepatch(oldVnode: VNode, thunk:
VNode): void{ leti: number, old = oldVnode.data asVNodeData, cur =
thunk.data asVNodeData; constoldArgs = old.args, args = cur.args;
if(old.fn !== cur.fn || (oldArgs asany).length !== (args asany).length)
{ // 如果 fn 不同或 args 长度不同,说明发生了变化,调用 fn 生成新的
vnode 并返回copyToThunk((cur.fn asany).apply( undefined, args), thunk);
return; } for(i = 0; i < (args asany).length; ++i) { if((oldArgs
asany)[i] !== (args asany)[i]) { //
如果每个参数发生变化,逻辑同上copyToThunk((cur.fn asany).apply(
undefined, args), thunk); return; } } copyToThunk(oldVnode, thunk);}
复制代码

  1. 第四轮比较:oldStartVnode 和 newStartVnode 相等,直接
    patchVnode,newStartIdx 和 oldStartIdx 向中间移动,状态更新如下

可以回顾下 patchVnode 的实现,在 prepatch 后,会对 vnode
的数据做比较,比如当 children 相同、text 相同都会结束 patchVnode。

  • 用 js 对象来描述 dom 树结构,然后用这个 js 对象来创建一棵真正的 dom
    树,插入到文档中
  • 当状态更新时,将新的 js 对象和旧的 js
    对象进行比较,得到两个对象之间的差异
  • 将差异应用到真正的 dom 上

图片 3

  1. 第一轮比较:开始结束节点两两并不相等,于是看 newStartVnode
    在旧节点中是否存在,最后找到了在第二个位置,调用 patchVnode
    进行更新,将 oldCh[1] 至空,将 dom 插入到 oldStartVnode
    前面,newStartIdx 向中间移动,状态更新如下

functionrenderNumber(num) { returnh( ‘span’, num);} 复制代码

上面是 init
方法的一些源码,为了阅读方便,暂时先把一些方法的具体实现给注释掉,等有用到的时候再具体分析。
通过参数可以知道,这里有接受一个 modules 数组,另外有一个可选的参数
domApi,如果没传递会使用浏览器中和 dom 相关的
api,具体可以看这里,这样的设计也很有好处,它可以让用户自定义平台相关的
api,比如可以看看weex 的相关实现 。首先这里会对 module 中的 hook
进行收集,保存到 cbs
中。然后定义了各种函数,这里可以先不管,接着就是返回一个 patch
函数了,这里也先不分析它的具体逻辑。这样 init 就结束了。

图片 4

  • 核心代码只有 200 行,丰富的测试用例
  • 强大的插件系统、hook 系统
  • vue 使用了 snabbdom,读懂 snabbdom 对理解 vue 的实现有帮助
  1. 假设旧节点顺序为[A, B, C, D],新节点为[B, A, C, D, E]
  1. 第三轮比较:oldStartVnode 为空,oldStartIdx
    向中间移动,进入下轮比较,状态更新如下

首先调用 vnode 上的 prepatch hook,如果当前的两个 vnode
完全相同,直接返回。接着调用 module 和 vnode 上的 update
hook。然后会分为以下几种情况做处理:

图片 5

它的具体实现如下:

什么是 Virtual DOM

thunk 函数

图片 6

createElm 函数 // 创建真正的 dom 节点functioncreateElm(vnode: VNode,
insertedVnodeQueue: VNodeQueue): Node{ leti: any, data = vnode.data; //
调用元素的 init hookif(data !== undefined) { if(isDef(i = data.hook) &&
isDef(i = i.init)) { i(vnode); data = vnode.data; } } letchildren =
vnode.children, sel = vnode.sel; // 注释节点if(sel === ‘!’) {
if(isUndef(vnode.text)) { vnode.text = ”; } // 创建注释节点vnode.elm =
api.createComment(vnode.text asstring); } elseif(sel !== undefined) { //
Parse selectorconsthashIdx = sel.indexOf( ‘#’); constdotIdx =
sel.indexOf( ‘.’, hashIdx); consthash = hashIdx > 0? hashIdx :
sel.length; constdot = dotIdx > 0? dotIdx : sel.length; consttag =
hashIdx !== -1|| dotIdx !== -1? sel.slice( 0, Math.min(hash, dot)) :
sel; constelm = vnode.elm = isDef(data) && isDef(i = (data
asVNodeData).ns) ? api.NS(i, tag) : api.(tag); if(hash < dot)
elm.setAttribute( ‘id’, sel.slice(hash + 1, dot)); if(dotIdx > 0)
elm.setAttribute( ‘class’, sel.slice(dot + 1).replace( /./g, ‘ ‘)); //
调用 module 中的 create hookfor(i = 0; i < cbs.create.length; ++i)
cbs.create[i](emptyNode, vnode); // 挂载子节点if(is.array(children)) {
for(i = 0; i < children.length; ++i) { constch = children[i]; if(ch
!= null) { api.(elm, createElm(ch asVNode, insertedVnodeQueue)); } } }
elseif(is.primitive(vnode.text)) { api.(elm,
api.createTextNode(vnode.text)); } i = (vnode.data asVNodeData).hook; //
Reuse variable// 调用 vnode 上的 hookif(isDef(i)) { // 调用 create
hookif(i.create) i.create(emptyNode, vnode); // insert hook 存储起来 等
dom 插入后才会调用,这里用个数组来保存能避免调用时再次对 vnode
树做遍历if(i.insert) insertedVnodeQueue.push(vnode); } } else{ //
文本节点vnode.elm = api.createTextNode(vnode.text asstring); }
returnvnode.elm;} 复制代码

相关文章