文章内容输出来源:拉勾教育大前端高薪训练营
在拉钩训练营学习已经有一段时间了,感觉这段时间的收获远比自己独自学习强的多,自己学习的时候经常会因为惰性,无法坚持,在这里有班主任时刻关注你的学习进度(感觉对我这种懒人蛮好的),最重要的是有了一个学习的计划,不用无头苍蝇一样东一点西一点,最后什么都没记住。学习都是需要方法和技巧的,在训练营会有专业的老师为你答疑解惑,同时会教你解决问题的思路及方法,让你能够触类旁通。
Vue的源码中其虚拟dom部分主要是 借鉴了snabbdom.js,而snabbdom的代码行数比较少,可以帮助我们更好的理解相关概念,所以这里我们对snabbdom进行学习。
虚拟Dom
- vNode实际上只是一个js对象,使用普通js对象来描述dom对象,因为不是真实dom所有叫虚拟dom(创建一个真实Dom的成本非常高,属性非常多),通过新旧虚拟DOM 这两个对象的差异(Diff算法),最终只把变化的部分重新渲染,提高渲染效率的过程; diff 是通过JS层面的计算,返回一个patch对象,即补丁对象,在通过特定的操作解析patch对象,完成页面的重新渲染
export interface VNode {
sel: string | undefined; //选择器
data: VNodeData | undefined; //数据
children: Array<VNode | string> | undefined; //子节点
elm: Node | undefined;//对应的真实DOM
text: string | undefined;//文本
key: Key | undefined;//key值
}
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
}
export function vnode(sel: string | undefined,
data: any | undefined,
children: Array<VNode | string> | undefined,
text: string | undefined,
elm: Element | Text | undefined): VNode {
let key = data === undefined ? undefined : data.key;
return {sel, data, children, text, elm, key};
}
源码分析流程
我们根据一个案例开始分析:
import { init , h} from "snabbdom"
import style from "snabbdom/modules/style"
import eventlisteners from "snabbdom/modules/eventlisteners"
// const snabbdom = require("snabbdom")
const patch = init([style, eventlisteners ])
const oldNode= document.getElementById("app")
const vNode = h('span#first.class',
{//因为init时引入了style与eventListener模块所以可以使用事件及样式
on:{
click:() => console.log('click')
},
style:{
color:"red"
}
}, 'hello word')
//更新视图
patch(oldNode, vNode)
上述代码中,我们拿到了snabbdom暴露的init,h方法,通过init方法我们可以得到一个patch函数,在init的时候我们可以传入引用的modules,这样我们在使用h函数的时候就可以使用相关模块的功能,patch函数可以帮我们更新视图下面我们根据源码开始分析:
const hooks: (keyof Module)[] = ['create', 'update', 'remove', 'destroy', 'pre', 'post'];
export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
let i: number, j: number, cbs = ({} as ModuleHooks);
const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;
//一个参数接收的模块数组(attributes, props等模块),都有自己的钩子函数(pre, create等等),cbs就是用来收集这些钩子函数的收集器,cbs={pre:[updateStyle等modules中各个模块对应的钩子函数],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);
}
}
}
...相关功能函数
//返回一个用于更新视图的patch函数
return function patch(){...}
}
通过执行上面的init函数我们得到了一个用来更新新旧dom的patch函数,下面我们开始执行patch函数
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]();
//判断oldVnode是否vNode类型,如果不是就转换成vNode
if (!isVnode(oldVnode)) {
oldVnode = emptyNodeAt(oldVnode);
}
//判断新旧node的sel与key是都相同
if (sameVnode(oldVnode, vnode)) { //相同就更新
patchVnode(oldVnode, vnode, insertedVnodeQueue);
} else {//不相同
// 获取老节点的真实DOM
elm = oldVnode.elm as Node;
//获取老节点的父节点
parent = api.parentNode(elm);
// 给将vNode转化内为真实DOM挂载在vNode的elm属性上
createElm(vnode, insertedVnodeQueue);
//如果存在父节点
if (parent !== null) {
//在父节点下插入vNode的真实DOM
api.insertBefore(parent, vnode.elm as Node, api.nextSibling(elm));
//删除父节点下的老的真实DOM
removeVnodes(parent, [oldVnode], 0, 0);
}
}
//当我们使用createElm创建真实DOM时,如果vNode的data的hook上有insert钩子,就会将该vNode添加到insertedVnodeQueue队列中,在对比结束之后会对insertedVnodeQueue进行遍历 执行队列中insertedVnodeQueue[i](vNode).data.hook.insert钩子
for (i = 0; i < insertedVnodeQueue.length; ++i) {
(((insertedVnodeQueue[i].data as VNodeData).hook as Hooks).insert as any)(insertedVnodeQueue[i]);
}
//执行cbs收集的post钩子
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
//将新节点返回作为下一次对比的oldVnode
return vnode;
};
通过上述代码分析我们可以知道,我们在对比vNode时,通过判断sel及key值来选择不同的执行方式:如果不同就删除旧节点插入新节点,如果相同就通过patchVnode做进一步处理:
function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
let i: any, hook: any;
//执行在创建vNode时data上传入的prepatch钩子
if (isDef(i = vnode.data) && isDef(hook = i.hook) && isDef(i = hook.prepatch)) {
i(oldVnode, vnode);
}
//因为新老接待你的sel及key值相同因此新老节点的elm指向的真实DOM也是同一个
const elm = vnode.elm = (oldVnode.elm as Node);
let oldCh = oldVnode.children;
let ch = vnode.children;
//如果对比的新老虚拟DOM相同,就不需要更新
if (oldVnode === vnode) return;
//当data存在执行cbs及data上对应的钩子,
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);
}
//如果vNode不存在文本
if (isUndef(vnode.text)) {
//新老虚拟DOM都存在children,且不相等,更新children(diff算法核心)
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh as Array<VNode>, ch as Array<VNode>, insertedVnodeQueue);
//如果vNode存在children
} else if (isDef(ch)) {
// 判断老虚拟DOM是否存在text,存在就更新为空
if (isDef(oldVnode.text)) api.setTextContent(elm, '');
//将vNode的子节点添加更新到对应的真实DOM上
addVnodes(elm, null, ch as Array<VNode>, 0, (ch as Array<VNode>).length - 1, insertedVnodeQueue);
//如果oldVnode存在children就删除
} else if (isDef(oldCh)) {
removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
//如果oldVnode存在text更新为空
} else if (isDef(oldVnode.text)) {
api.setTextContent(elm, '');
}
} else if (oldVnode.text !== vnode.text) {//新老虚拟DOMtext不同
//olVnode存在children就清除所有子节点
if (isDef(oldCh)) {
removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
}
//更新真实DOM的文本内容
api.setTextContent(elm, vnode.text as string);
}
//执行钩子
if (isDef(hook) && isDef(i = hook.postpatch)) {
i(oldVnode, vnode);
}
}
通过以上函数我们可以知道根据olVnode,vNode中的text,children存在与否执行对应的操作,其中如果oldVnode与vNode的children同时存在且不相等还需要进一步更新,这个时候就开始执行updateChildren也是diff算法的核心实现:
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; //oldKeyToIdx为oldNode中key与index对应对象
let idxInOld: number; //newStartVnode的key在oldKeyToIdx中对应的
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 {
if (oldKeyToIdx === undefined) {//oldKeyToIdx为oldNode中key与index对应对象
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
idxInOld = oldKeyToIdx[newStartVnode.key as string]; //判断老节点中是否有新节点key
if (isUndef(idxInOld)) { // 没有说明是新节点 执行插入操作在老节点之前
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;//将旧节点中被更新的那个节点赋值为undefined,否则下次会继续对比
api.insertBefore(parentElm, (elmToMove.elm as Node), oldStartVnode.elm as Node);//将跟新节点插入到当前旧开始节点之前
}
newStartVnode = newCh[++newStartIdx];//开始下一次对比
}
}
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
if (oldStartIdx > oldEndIdx) {//老节点先遍历完成:记录当前newStartIdx与newEndIdx,将多余节点插入 newEndIdx + 1 位置节点之前
before = newCh[newEndIdx+1] == null ? null : newCh[newEndIdx+1].elm;
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
} else {//新节点先遍历完成:清除多余节点
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
}