patch 要领
媒介
在最先剖析这块源码的时刻,先给人人补一个知识点。关于 两颗 Virtual Dom 树对照的战略
diff 战略
- 同级对照
对照的时刻,只针对同级的对照,削减算法复杂度。 - 就近复用
为了尽量不发生 DOM 的挪动,会就近复用雷同的 DOM 节点,复用的依据是推断是不是是同范例的 dom 元素
init 要领
在 ./src/snabbdom.ts
中,主假如 init 要领。
init
要领主假如传入 modules
,domApi
, 然后返回一个 patch
要领
注册钩子
// 钩子 ,
const hooks: (keyof Module)[] = [
'create',
'update',
'remove',
'destroy',
'pre',
'post'
];
这里主假如注册一系列的钩子,在差别的阶段触发,细节可看 钩子
将各个模块的钩子要领,挂到一致的钩子上
这里主假如将每一个 modules 下的 hook 要领提取出来存到 cbs 内里
- 初始化的时刻,将每一个 modules 下的响应的钩子都追加都一个数组内里。create、update….
- 在举行 patch 的各个阶段,触发对应的钩子去处置惩罚对应的事变
- 这类体式格局比较轻易扩大。新增钩子的时刻,不需要更改到主要的流程
// 轮回 hooks , 将每一个 modules 下的 hook 要领提取出来存到 cbs 内里
// 返回效果 eg : cbs['create'] = [modules[0]['create'],modules[1]['create'],...];
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = [];
for (j = 0; j < modules.length; ++j) {
const hook = modules[j][hooks[i]];
if (hook !== undefined) {
(cbs[hooks[i]] as Array<any>).push(hook);
}
}
}
这些模块的钩子,主要用在更新节点的时刻,会在差别的生命周期内里去触发对应的钩子,从而更新这些模块。
比方元素的
attr、props、class
之类的!细致相识请检察模块:模块
sameVnode
推断是不是是雷同的假造节点
/**
* 推断是不是是雷同的假造节点
*/
function sameVnode(vnode1: VNode, vnode2: VNode): boolean {
return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
}
patch
init
要领末了返回一个 patch
要领 。
patch
要领主要的逻辑以下 :
- 触发
pre
钩子 - 假如老节点非
vnode,
则新建立空的vnode
- 新旧节点为
sameVnode
的话,则挪用patchVnode
更新vnode
, 不然建立新节点 - 触发网络到的新元素
insert
钩子 - 触发
post
钩子
/**
* 修补节点
*/
return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
let i: number, elm: Node, parent: Node;
// 用于网络一切插进去的元素
const insertedVnodeQueue: VNodeQueue = [];
// 先挪用 pre 回调
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
// 假如老节点非 vnode , 则建立一个空的 vnode
if (!isVnode(oldVnode)) {
oldVnode = emptyNodeAt(oldVnode);
}
// 假如是同个节点,则举行修补
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue);
} else {
// 差别 Vnode 节点则新建
elm = oldVnode.elm as Node;
parent = api.parentNode(elm);
createElm(vnode, insertedVnodeQueue);
// 插进去新节点,删除老节点
if (parent !== null) {
api.insertBefore(
parent,
vnode.elm as Node,
api.nextSibling(elm)
);
removeVnodes(parent, [oldVnode], 0, 0);
}
}
// 遍历一切网络到的插进去节点,挪用插进去的钩子,
for (i = 0; i < insertedVnodeQueue.length; ++i) {
(((insertedVnodeQueue[i].data as VNodeData).hook as Hooks)
.insert as any)(insertedVnodeQueue[i]);
}
// 挪用post的钩子
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
return vnode;
};
团体的流程大体上是这模样,接下来我们来关注更多的细节!
patchVnode 要领
起首我们研讨 patchVnode
相识雷同节点是怎样更新的
patchVnode 要领主要的逻辑以下 :
- 触发
prepatch
钩子 - 触发
update
钩子, 这里主要为了更新对应的module
内容 - 非文本节点的状况 , 挪用 updateChildren 更新一切子节点
- 文本节点的状况 , 直接
api.setTextContent(elm, vnode.text as string);
这里在对照的时刻,就会直接更新元素内容了。并不会比及对照完才更新 DOM 元素
详细代码细节:
/**
* 更新节点
*/
function patchVnode(
oldVnode: VNode,
vnode: VNode,
insertedVnodeQueue: VNodeQueue
) {
let i: any, hook: any;
// 挪用 prepatch 回调
if (
isDef((i = vnode.data)) &&
isDef((hook = i.hook)) &&
isDef((i = hook.prepatch))
) {
i(oldVnode, vnode);
}
const elm = (vnode.elm = oldVnode.elm as Node);
let oldCh = oldVnode.children;
let ch = vnode.children;
if (oldVnode === vnode) return;
// 挪用 cbs 中的一切模块的update回调 更新对应的现实内容。
if (vnode.data !== undefined) {
for (i = 0; i < cbs.update.length; ++i)
cbs.update[i](oldVnode, vnode);
i = vnode.data.hook;
if (isDef(i) && isDef((i = i.update))) i(oldVnode, vnode);
}
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
// 新老子节点都存在的状况,更新 子节点
if (oldCh !== ch)
updateChildren(
elm,
oldCh as Array<VNode>,
ch as Array<VNode>,
insertedVnodeQueue
);
} else if (isDef(ch)) {
// 老节点不存在子节点,状况下,新建元素
if (isDef(oldVnode.text)) api.setTextContent(elm, '');
addVnodes(
elm,
null,
ch as Array<VNode>,
0,
(ch as Array<VNode>).length - 1,
insertedVnodeQueue
);
} else if (isDef(oldCh)) {
// 新节点不存在子节点,状况下,删除元素
removeVnodes(
elm,
oldCh as Array<VNode>,
0,
(oldCh as Array<VNode>).length - 1
);
} else if (isDef(oldVnode.text)) {
// 假如老节点存在文本节点,而新节点不存在,所以清空
api.setTextContent(elm, '');
}
} else if (oldVnode.text !== vnode.text) {
// 子节点文本不一样的状况下,更新文本
api.setTextContent(elm, vnode.text as string);
}
// 挪用 postpatch
if (isDef(hook) && isDef((i = hook.postpatch))) {
i(oldVnode, vnode);
}
}
一最先,看到这类写法总有点不习惯,不过背面看着就习惯了。
if (isDef((i = data.hook)) && isDef((i = i.init))) {i(vnode);}
约等于
if(data.hook.init){data.hook.init(vnode)}
updateChildren 要领
patchVnode
内里最主要的要领,也是全部 diff
内里的最中心要领
updateChildren
主要的逻辑以下:
优先处置惩罚特别场景,先对照两头。也就是
- 旧 vnode 头 vs 新 vnode 头
- 旧 vnode 尾 vs 新 vnode 尾
- 旧 vnode 头 vs 新 vnode 尾
- 旧 vnode 尾 vs 新 vnode 头
- 首尾不一样的状况,寻觅 key 雷同的节点,找不到则新建元素
- 假如找到 key,然则,元素挑选器变化了,也新建元素
- 假如找到 key,而且元素挑选没变, 则挪动元素
- 两个列表对照完以后,清算过剩的元素,新增增加的元素
不供应 key 的状况下,假如只是递次转变的状况,比方第一个挪动到末端。这个时刻,会致使实在更新了背面的一切元素
详细代码细节:
/**
* 更新子节点
*/
function updateChildren(
parentElm: Node,
oldCh: Array<VNode>,
newCh: Array<VNode>,
insertedVnodeQueue: VNodeQueue
) {
let oldStartIdx = 0,
newStartIdx = 0;
let oldEndIdx = oldCh.length - 1;
let oldStartVnode = oldCh[0];
let oldEndVnode = oldCh[oldEndIdx];
let newEndIdx = newCh.length - 1;
let newStartVnode = newCh[0];
let newEndVnode = newCh[newEndIdx];
let oldKeyToIdx: any;
let idxInOld: number;
let elmToMove: VNode;
let before: any;
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == null) {
// 挪动索引,由于节点处置惩罚过了会置空,所以这里向右移
oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
} else if (oldEndVnode == null) {
// 道理同上
oldEndVnode = oldCh[--oldEndIdx];
} else if (newStartVnode == null) {
// 道理同上
newStartVnode = newCh[++newStartIdx];
} else if (newEndVnode == null) {
// 道理同上
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// 从左对照
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 从右对照
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// Vnode moved right
// 最左边 对照 最右边
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
// 挪动元素到右边指针的背面
api.insertBefore(
parentElm,
oldStartVnode.elm as Node,
api.nextSibling(oldEndVnode.elm as Node)
);
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// Vnode moved left
// 最右边对照最左边
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
// 挪动元素到左边指针的背面
api.insertBefore(
parentElm,
oldEndVnode.elm as Node,
oldStartVnode.elm as Node
);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
// 首尾都不一样的状况,寻觅雷同 key 的节点,所以运用的时刻加上key能够调高效力
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(
oldCh,
oldStartIdx,
oldEndIdx
);
}
idxInOld = oldKeyToIdx[newStartVnode.key as string];
if (isUndef(idxInOld)) {
// New element
// 假如找不到 key 对应的元素,就新建元素
api.insertBefore(
parentElm,
createElm(newStartVnode, insertedVnodeQueue),
oldStartVnode.elm as Node
);
newStartVnode = newCh[++newStartIdx];
} else {
// 假如找到 key 对应的元素,就挪动元素
elmToMove = oldCh[idxInOld];
if (elmToMove.sel !== newStartVnode.sel) {
api.insertBefore(
parentElm,
createElm(newStartVnode, insertedVnodeQueue),
oldStartVnode.elm as Node
);
} else {
patchVnode(
elmToMove,
newStartVnode,
insertedVnodeQueue
);
oldCh[idxInOld] = undefined as any;
api.insertBefore(
parentElm,
elmToMove.elm as Node,
oldStartVnode.elm as Node
);
}
newStartVnode = newCh[++newStartIdx];
}
}
}
// 新老数组个中一个抵达末端
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
if (oldStartIdx > oldEndIdx) {
// 假如老数组先抵达末端,申明新数组另有更多的元素,这些元素都是新增的,说以一次性插进去
before =
newCh[newEndIdx + 1] == null
? null
: newCh[newEndIdx + 1].elm;
addVnodes(
parentElm,
before,
newCh,
newStartIdx,
newEndIdx,
insertedVnodeQueue
);
} else {
// 假如新数组先抵达末端,申明新数组比老数组少了一些元素,所以一次性删除
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
}
addVnodes 要领
addVnodes
就比较简单了,主要功能就是增加 Vnodes
到 实在 DOM 中
/**
* 增加 Vnodes 到 实在 DOM 中
*/
function addVnodes(
parentElm: Node,
before: Node | null,
vnodes: Array<VNode>,
startIdx: number,
endIdx: number,
insertedVnodeQueue: VNodeQueue
) {
for (; startIdx <= endIdx; ++startIdx) {
const ch = vnodes[startIdx];
if (ch != null) {
api.insertBefore(
parentElm,
createElm(ch, insertedVnodeQueue),
before
);
}
}
}
removeVnodes 要领
删除 VNodes 的主要逻辑以下:
- 轮回触发 destroy 钩子,递归触发子节点的钩子
- 触发 remove 钩子,应用
createRmCb
, 在一切监听器实行后,才挪用api.removeChild
,删除真正的 DOM 节点
/**
* 建立一个删除的回调,屡次挪用这个回调,直到监听器都没了,就删除元素
*/
function createRmCb(childElm: Node, listeners: number) {
return function rmCb() {
if (--listeners === 0) {
const parent = api.parentNode(childElm);
api.removeChild(parent, childElm);
}
};
}
/**
* 删除 VNodes
*/
function removeVnodes(
parentElm: Node,
vnodes: Array<VNode>,
startIdx: number,
endIdx: number
): void {
for (; startIdx <= endIdx; ++startIdx) {
let i: any,
listeners: number,
rm: () => void,
ch = vnodes[startIdx];
if (ch != null) {
if (isDef(ch.sel)) {
invokeDestroyHook(ch);
listeners = cbs.remove.length + 1;
// 一切监听删除
rm = createRmCb(ch.elm as Node, listeners);
for (i = 0; i < cbs.remove.length; ++i)
cbs.remove[i](ch, rm);
// 假如有钩子则挪用钩子后再调删除回调,假如没,则直接挪用回调
if (
isDef((i = ch.data)) &&
isDef((i = i.hook)) &&
isDef((i = i.remove))
) {
i(ch, rm);
} else {
rm();
}
} else {
// Text node
api.removeChild(parentElm, ch.elm as Node);
}
}
}
}
createElm 要领
将 vnode 转换成真正的 DOM 元素
主要逻辑以下:
- 触发 init 钩子
- 处置惩罚解释节点
- 建立元素并设置 id , class
- 触发模块 create 钩子 。
- 处置惩罚子节点
- 处置惩罚文本节点
- 触发 vnodeData 的 create 钩子
/**
* VNode ==> 实在DOM
*/
function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
let i: any,
data = vnode.data;
if (data !== undefined) {
// 假如存在 data.hook.init ,则挪用该钩子
if (isDef((i = data.hook)) && isDef((i = i.init))) {
i(vnode);
data = vnode.data;
}
}
let children = vnode.children,
sel = vnode.sel;
// ! 来代表解释
if (sel === '!') {
if (isUndef(vnode.text)) {
vnode.text = '';
}
vnode.elm = api.createComment(vnode.text as string);
} else if (sel !== undefined) {
// Parse selector
// 剖析挑选器
const hashIdx = sel.indexOf('#');
const dotIdx = sel.indexOf('.', hashIdx);
const hash = hashIdx > 0 ? hashIdx : sel.length;
const dot = dotIdx > 0 ? dotIdx : sel.length;
const tag =
hashIdx !== -1 || dotIdx !== -1
? sel.slice(0, Math.min(hash, dot))
: sel;
// 依据 tag 建立元素
const elm = (vnode.elm =
isDef(data) && isDef((i = (data as VNodeData).ns))
? api.createElementNS(i, tag)
: api.createElement(tag));
// 设置 id
if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot));
// 设置 className
if (dotIdx > 0)
elm.setAttribute('class',sel.slice(dot + 1).replace(/\./g, ' '));
// 实行一切模块的 create 钩子,建立对应的内容
for (i = 0; i < cbs.create.length; ++i)
cbs.create[i](emptyNode, vnode);
// 假如存在 children ,则建立children
if (is.array(children)) {
for (i = 0; i < children.length; ++i) {
const ch = children[i];
if (ch != null) {
api.appendChild(
elm,
createElm(ch as VNode, insertedVnodeQueue)
);
}
}
} else if (is.primitive(vnode.text)) {
// 追加文本节点
api.appendChild(elm, api.createTextNode(vnode.text));
}
// 实行 vnode.data.hook 中的 create 钩子
i = (vnode.data as VNodeData).hook; // Reuse variable
if (isDef(i)) {
if (i.create) i.create(emptyNode, vnode);
if (i.insert) insertedVnodeQueue.push(vnode);
}
} else {
// sel 不存在的状况, 即为文本节点
vnode.elm = api.createTextNode(vnode.text as string);
}
return vnode.elm;
}
其他
想相识在各个生命周期都有哪些钩子,请检察:钩子
想相识在各个生命周期内里怎样更新详细的模块请检察:模块