Virtual DOM的实现原理之snabbdom源码学习
目标:
- 了解虚拟DOM及其作用
- snabbdom的基本使用
- snabbdom源码解析
1 What is Virtual DOM
Virtual Dom(虚拟DOM),是由普通的JS对象来描述DOM对象,我们可以对两个状态下的 js 对象进行对比,记录出它们的差异,然后把它应用到真正的dom树。因为不是真实的DOM对象,所以叫做Virtual DOM.
使用虚拟DOM来模拟真实的DOM:
因为我们知道一个DOM对象中的成员是非常多。所以创建Dom对象的成本非常高。
如果使用虚拟Dom来描述真实Dom,就会发现创建的成员少,成本也就低了
2 Why is Virtual DOM
- 手动操作
Dom比较麻烦,还需要考虑浏览器兼容性问题,虽然有Jquery等库简化DOM操作,但是随着项目的复杂度越来越高,DOM操作复杂提升,既要考虑Dom操作,还要考虑数据的操作。 - 为了简化
DOM的复杂操作于是出现了各种的MVVM框架,MVVM框架解决了视图和状态的同步问题,也就是当数据发生变化,更新视图,当视图发生变化更新数据。 - 为了简化视图的操作我们可以使用模板引擎,但是模板引擎没有解决跟踪状态变化的问题(当数据发生了变化后,无法获取上一次的状态,只有将页面上的元素删除,然后在重新创建,这时页面有刷新的问题,同时频繁操作
Dom,性能也会非常低),于是Virtual Dom出现了。 Virtual Dom的好处就是当状态改变时不需要立即更新DOM,只需要创建一个虚拟树来描述DOM,Virtual Dom内部将弄清楚如何有效(diff)的更新DOM.(例如:向用户添加列表中添加一个用户,只添加新的内容,原有的结构会被重用) 我们使用Jquery来实现数据展示与排序:
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta charset="utf-8">
<script type="text/javascript" src="https://code.jquery.com/jquery-1.11.3.js"></script>
</head>
<body>
<div id="app"></div>
<div id="sort" style="margin-top: 20px;">按年龄排序</div>
<script type="text/javascript">
let datas = [
{ 'name': 'xsm001', 'age': 32 },
{ 'name': 'xsm002', 'age': 29 },
{ 'name': 'xsm003', 'age': 31 },
{ 'name': 'xsm004', 'age': 30 }
];
let render = function() {
let html = '';
datas.forEach(function(item, index) {
html += `<li>
<div class="u-cls">
<span class="name">姓名:${item.name}</span>
<span class="age" style="margin-left:20px;">年龄:${item.age}</span>
<span class="closed">x</span>
</div>
</li>`;
});
return html;
};
$("#app").html(render());
$('#sort').on('click', function() {
datas = datas.sort(function(a, b) {
return a.age - b.age;
});
$('#app').html(render());
})
</script>
</body>
</html>
可以看出,它虽然能实现排序功能,但是比较暴力,不论数据有没有发生改变,都会将之前的DOM全部从页面干掉,然后重新去渲染新的dom节点,如果是小页面还好,要是负责的页面,就会有大量的dom操作,进而影响性能
虚拟DOM的思想是先控制数据再到视图,但是数据状态是通过diff比对,它会比对新旧虚拟DOM节点,然后找出两者之前的不同,然后再把不同的节点再发生渲染操作。
3 虚拟DOM的作用
维护视图和状态的关系(虚拟DOM会记录状态的变化,只需要更新状态变化的内容就可以了)
复杂视图情况下提升渲染性能。
虚拟DOM除了渲染DOM以外,还可以实现渲染到其它的平台,例如可以实现服务端渲染(ssr),原生应用(React Native),小程序(uni-app等)。以上列举的案例中,内部都使用了虚拟DOM.
4 Snabbdom基本使用
//创建项目目录
md snabbdom-demo
// 进入项目目录
cd snabbdom-demo
// 创建package.json
npm init -y
//本地安装parcel
npm install parcel-bundler
配置package.json
"srcipts":{ "dev":"parcel index.html --open" , //open打开浏览器 "build":"parcel build index.html" }
github链接: github.com/snabbdom/sn…
4.1 基本使用
import { h, thunk, init } from "snabbdom";
// init方法返回值为patch函数,patch函数作用是对比两个vndoe的差异并更新到真实的DOM中。init函数的参数是一个数组,数组中的内容是模块,关于模块内容后面还会讲解
let patch = init([]);
//创建虚拟DOM
// 第一个参数:标签+选择器(id选择器或者是类选择器)
// 第二个参数:如果是字符串的话就是标签中的内容
let vnode = h("div#container.cls", "Hello World");
//我们这里需要将创建的虚拟dom,最终要渲染到`index.html`中`app`这个div中,所以这里需要获取一下该div
let app = document.querySelector("#app");
//要想将虚拟DOM渲染到`app`中,需要用到patch函数。
// 我们知道patch函数的作用是对比两个vnode的差异来更新到真实的`DOM`中。
//但是我们目前没有两个虚拟DOM.那么patch方法的第一个参数也可以是真实的DOM.patch方法会将真实的DOM转换成VNode.
// 第二个参数:为VNode
//返回值为VNode
let oldNode = patch(app, vnode);
vnode = h("div","Hello Snabbdom")
patch(oldNode,vnode)
4.2 模块
Snabbdom的核心库并不能处理元素的属性/样式/事件等,如果需要处理,可以使用模块.
常用模块
官方提供了6个模块
attributes:设置DOM元素的属性,内部使用setAttribute()来设置属性,处理布尔类型的属性(可以对布尔类型的属性作相应的判断处理,布尔类型的属性,我们比较熟悉的有selected,checked`等)。
props: 和attributes模块类似,设置DOM元素的属性element[attr]=value,不处理布尔类型的属性。
class: 切换样式类,注意:给元素设置类样式是通过sel选择器。··
dataset:设置 HTML5 中的 data-* 的自定义属性
eventlisteners: 注册和移除事件
style:设置行内样式,支持动画(内部创建transitionend事件),会增加额外的属性:delayed / remove / destroy
下面看一下模块的使用
使用模块的步骤:
第一步:导入需要的模块
第二步:在init()中注册模块
第三步:使用h函数创建VNode的时候,可以把第二个参数设置为对象(对象中是模块需要的数据,可以设置行内样式、事件等),其它参数往后移。
下面我们要实现的案例,就是给div添加一个背景,同时为其添加一个单击事件,当然在div中还要创建两个元素分别是h1与p.
具体实现的代码如下:
//导入模块
import style from "snabbdom/modules/style";
import eventlisteners from "snabbdom/modules/eventlisteners";
//注册模块
let patch = init([style, eventlisteners]);
// 使用h函数的第二个参数传入模块所需要的数据(对象)
let vnode = h(
"div",
{
style: {
backgroundColor: "red",
},
on: {
click: eventHandler,
},
},
[h("h1", "Hello Vue"), h("p", "这是p标签")]
);
function eventHandler() {
console.log("点击了我");
}
let app = document.querySelector("#app");
patch(app, vnode);
5 源码解读
5.1 h函数
我们都知道,在Vue中h函数支持组件内容,在Snabbdom中的h函数是用来创建VNode
// h函数的重载
export function h(sel: string): VNode;
export function h(sel: string, data: VNodeData): VNode;
export function h(sel: string, children: VNodeChildren): VNode;
export function h(sel: string, data: VNodeData, children: VNodeChildren): VNode;
//h函数重载的具体实现
//h函数可以接收三个参数,?表示该参数可以不传递
export function h(sel: any, b?: any, c?: any): VNode {
//定义变量
var data: VNodeData = {}, children: any, text: any, i: number;
// 处理参数,实现重载的机制
//如果c这个参数的值不等于undefined,表示传递了三个参数
if (c !== undefined) {
//如果该条件成立,表示处理的就是有三个参数的情况
//参数b中存储的就是模块处理的时候,需要的数据,例如:行内样式,事件等,关于这块在前面的案例中我们也写过,在这里将b参数的值赋给了data这个变量
data = b;
//下面是对参数c进行了判断。
//关于参数c有三种情况,第一种情况为数组,第二种情况为字符串或者是数字,第三种情况为VNode.
//首先判断参数c是否为数组,如果是数组,赋值给了children这个变量,表明c是子元素。
//例如前面我们在使用模块的案例中,给h函数指定的第三个参数就为数组:[h("h1", "Hello Vue"), h("p", "这是p标签")]
if (is.array(c)) { children = c; }
//如果c参数是字符串或者是数字,将参数c赋值给了text变量,表明传递过来的内容其实就是标签中的文本内容
else if (is.primitive(c)) { text = c; }
//如果有sel属性,表明c是vnode,在这里需要转换成数组的形式,然后再赋值给children这个变量
else if (c && c.sel) { children = [c]; }
} else if (b !== undefined) {
//如果该条件成立,表明处理的是两个参数的情况
//如果b是一个数组,赋值给chilren这个变量:vnode = h("div#container", [h("h1", "Hello Vue"), h("p", "Hello p")]);
if (is.array(b)) { children = b; }
//如果b是字符串或者数字:h("div", "Hello Vue");
else if (is.primitive(b)) { text = b; }
//如果b是Vnode的情况
else if (b && b.sel) { children = [b]; }
else { data = b; }
}
//判断children中有值
if (children !== undefined) {
//对chilren进行遍历
for (i = 0; i < children.length; ++i) {
//判断从chilren中取出来的内容是否为:string/number,如果是创建文本的虚拟节点.
if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
}
}
if (
sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' &&
(sel.length === 3 || sel[3] === '.' || sel[3] === '#')
) {
//如果是svg,添加命名空间
addNS(data, children, sel);
}
//最后返回的是整个VNode.所h函数的核心就是调用vnode方法,来返回一个虚拟节点
return vnode(sel, data, children, text, undefined);
};
// 导出h函数
export default h;
addNs方法实现:
addNs方法中就是给data添加了命名空间,然后通过递归的方式给chilren中的所有子元素都添加了命名空间。
function addNS(data: any, children: VNodes | undefined, sel: string | undefined): void {
data.ns = 'http://www.w3.org/2000/svg';
if (sel !== 'foreignObject' && children !== undefined) {
for (let i = 0; i < children.length; ++i) {
let childData = children[i].data;
if (childData !== undefined) {
addNS(childData, (children[i] as VNode).children as VNodes, children[i].sel);
}
}
}
}
5.3 VNode函数
VNode见名知意,就是用来创建虚拟节点的
import {Hooks} from './hooks';
import {AttachData} from './helpers/attachto'
import {VNodeStyle} from './modules/style'
import {On} from './modules/eventlisteners'
import {Attrs} from './modules/attributes'
import {Classes} from './modules/class'
import {Props} from './modules/props'
import {Dataset} from './modules/dataset'
import {Hero} from './modules/hero'
export type Key = string | number;
export interface VNode {
//选择器,也就是调用h函数的时候传递的第一个参数
sel: string | undefined;
// 节点数据:属性/样式/事件等。
data: VNodeData | undefined;
//子节点,和text互斥 VNode是描述真实DOM的,如果所描述的真实DOM中有子节点,通过children来表示这些子节点
children: Array<VNode | string> | undefined;
// 记录vnode对应的真是DOM,将Vnode转换成真实DOM以后,会存储到elm这个属性中。关于这一点可以在将VNode转换成真实DOM的时候看到。
elm: Node | undefined;
// 节点中的内容,和children只能互斥
text: string | undefined;
//优化,关于这个属性可以在将VNode转换成真实DOM的时候看到。
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
}
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};
}
export default vnode;
5.4 虚拟dom创建的全过程
我们创建一个虚拟dom
// 构造一个虚拟dom
var vnode = h('div#app',
{style: {color: '#000'}},
[
h('span', {style: {fontWeight: 'bold'}}, "my name is zhangsan"),
' and xxxx',
h('a', {props: {href: '/foo'}}, '我是张三')
]
);
通过源码可知:
sel:'span',b:={style:{fontweight:'bold'}},c='my name is zhangsan'
第一步:判断if (c !== undefined) {} 代码,然后进入if语句内部代码
if (c !== undefined) {
data = b;
if (is.array(c)) { children = c; }
else if (is.primitive(c)) { text = c; }
}
因此 data = {style: {fontWeight: 'bold'}}; 然后判断 c 是否是一个数组,可以看到,不是,因此进入 else if语句,因此 text = "my name is zhangsan"; 从代码中可以看到,就直接跳过所有的代码了,最后执行 return VNode(sel, data, children, text, undefined); 了,因此会调用 `snabbdom/vnode.js
/*
* VNode函数如下:主要的功能是构造VNode, 把输入的参数转化为Vnode
* @param {sel} 'span'
* @param {data} {style: {fontWeight: 'bold'}}
* @param {children} undefined
* @param {text} "my name is zhangsan"
* @param {elm} undefined
*/
module.exports = function(sel, data, children, text, elm) {
var key = data === undefined ? undefined : data.key;
return {sel: sel, data: data, children: children,
text: text, elm: elm, key: key};
};
//返回值如下:
{
sel: 'span',
data: {style: {fontWeight: 'bold'}},
children: undefined,
text: "my name is zhangsan",
elm: undefined,
key: undefined
}
第二步:调用h('a', {props: {href: '/foo'}}, '我是张三');代码
同理:sel = 'a'; b = {props: {href: '/foo'}}, c = '我是张三'; 然后执行如下代码:
if (c !== undefined) {
data = b;
if (is.array(c)) { children = c; }
else if (is.primitive(c)) { text = c; }
}
因此 data = {props: {href: '/foo'}}; text = '我是张三'; children = undefined; 最后也一样执行返回:
return VNode(sel, data, children, text, undefined);
{
sel: 'a',
data: {props: {href: '/foo'}},
children: undefined,
text: "我是张三",
elm: undefined,
key: undefined
}
第三步: sel = 'div#app'; b = {style: {color: '#000'}}
c = [
{
sel: 'span',
data: {style: {fontWeight: 'bold'}},
children: undefined,
text: "my name is 张三",
elm: undefined,
key: undefined
},
' and xxxx',
{
sel: 'a',
data: {props: {href: '/foo'}},
children: undefined,
text: "我是张三",
elm: undefined,
key: undefined
}
];
接下来data = {style: {color: '#000'}},c被判断为数组,赋值给children
children = [
{
sel: 'span',
data: {style: {fontWeight: 'bold'}},
children: undefined,
text: "my name is zhangsan",
elm: undefined,
key: undefined
},
' and xxxx',
{
sel: 'a',
data: {props: {href: '/foo'}},
children: undefined,
text: "我是张三",
elm: undefined,
key: undefined
}
];
if (is.array(children)) {
for (i = 0; i < children.length; ++i) {
if (is.primitive(children[i])) children[i] = VNode(undefined, undefined, undefined, children[i]);
}
}
如上代码,判断如果 children 是一个数组的话,就循环该数组 children; 从上面我们知道 children 长度为3,因此会循环3次。进入for循环内部。判断其中一项是否是数字和字符串类型,因此只有 ' and xxxx' 符合要求,因此 children[1] = VNode(undefined, undefined, undefined, ' and xxxx'); 最后会调用 snabbdom/vnode.js 代码如下
module.exports = function(sel, data, children, text, elm) {
var key = data === undefined ? undefined : data.key;
return {sel: sel, data: data, children: children,
text: text, elm: elm, key: key};
};
//返回值:
children[1] = {
sel: undefined,
data: undefined,
children: undefined,
text: ' and xxxx',
elm: undefined,
key: undefined
};
执行完成后,我们最后返回代码:return VNode(sel, data, children, text, undefined); 因此会继续调用snabbdom/vnode.js代码如下:
/*
@param {sel} 'div#app'
@param {data} {style: {color: '#000'}}
@param {children} 值变为如下:
children = [
{
sel: 'span',
data: {style: {fontWeight: 'bold'}},
children: undefined,
text: "my name is zhangsan",
elm: undefined,
key: undefined
},
{
sel: undefined,
data: undefined,
children: undefined,
text: ' and xxxx',
elm: undefined,
key: undefined
},
{
sel: 'a',
data: {props: {href: '/foo'}},
children: undefined,
text: "我是张三",
elm: undefined,
key: undefined
}
];
@param {text} undefined
@param {elm} undefined
*/
module.exports = function(sel, data, children, text, elm) {
var key = data === undefined ? undefined : data.key;
return {sel: sel, data: data, children: children,
text: text, elm: elm, key: key};
};
//最后返回
return {
sel: sel,
data: data,
children: children,
text: text,
elm: elm,
key: key
};
我们构造虚拟dom的返回值如下:
vnode = {
sel: 'div#app',
data: {style: {color: '#000'}},
children: [
{
sel: 'span',
data: {style: {fontWeight: 'bold'}},
children: undefined,
text: "my name is zhangsan",
elm: undefined,
key: undefined
},
{
sel: undefined,
data: undefined,
children: undefined,
text: ' and xxxx',
elm: undefined,
key: undefined
},
{
sel: 'a',
data: {props: {href: '/foo'}},
children: undefined,
text: "我是张三",
elm: undefined,
key: undefined
}
],
text: undefined,
elm: undefined,
key: undefined
}
接着执行:
// 初始化容器
var app = document.getElementById('app');
// 将vnode patch 到 app 中
patch(app, vnode);
至此,虚拟dom构建完毕!
5.5 patch函数执行过程
patch函数作用:比较新旧虚拟节点,把变化的节点渲染成真实dom,最后新的节点作为下一次的旧节点 我们首先看patch中的init函数
export function init(
modules: Array<Partial<Module>>,
domApi?: DOMAPI,
options?: Options
) {
// cbs 用于收集 module 中的 hook
const cbs: ModuleHooks = {
create: [],
update: [],
remove: [],
destroy: [],
pre: [],
post: [],
};
const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;
for (const hook of hooks) {
for (const module of modules) {
const currentHook = module[hook];
if (currentHook !== undefined) {
(cbs[hook] as any[]).push(currentHook);
}
}
}
function emptyNodeAt(elm: Element) {
//......
}
function emptyDocumentFragmentAt(frag: DocumentFragment) {
//......
}
function createRmCb(childElm: Node, listeners: number) {
//......
}
//创建真实dom
function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
//......
}
function addVnodes(
parentElm: Node,
before: Node | null,
vnodes: VNode[],
startIdx: number,
endIdx: number,
insertedVnodeQueue: VNodeQueue
) {
//......
}
//有children就递归调用
function invokeDestroyHook(vnode: VNode) {
//......
}
function removeVnodes(
parentElm: Node,
vnodes: VNode[],
startIdx: number,
endIdx: number
): void {
//......
}
function updateChildren(
parentElm: Node,
oldCh: VNode[],
newCh: VNode[],
insertedVnodeQueue: VNodeQueue
) {
//......
}
function patchVnode(
oldVnode: VNode,
vnode: VNode,
insertedVnodeQueue: VNodeQueue
) {
//......
}
return function patch(
oldVnode: VNode | Element | DocumentFragment,
vnode: VNode
): VNode {
//......
};
}
可以知道init函数最后返回patch函数,init是个高阶函数 patch函数如下:
return function patch(
oldVnode: VNode | Element | DocumentFragment,
vnode: VNode
): VNode {
let i: number, elm: Node, parent: Node;
const insertedVnodeQueue: VNodeQueue = [];
//调用module中的pre hook
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
//判断传入的Element,是就转为空的 vnode
if (isElement(api, oldVnode)) {
oldVnode = emptyNodeAt(oldVnode);
} else if (isDocumentFragment(api, oldVnode)) {
oldVnode = emptyDocumentFragmentAt(oldVnode);
}
//sel和key相同,调用patchVnode
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue);
} else {
elm = oldVnode.elm!;
parent = api.parentNode(elm) as Node;
//创建新节点vnode.elm VNode(sel, data, children, elm, text, key)
//vnode(sel,data,children,text,elm)
createElm(vnode, insertedVnodeQueue);
if (parent !== null) {
api.insertBefore(parent, vnode.elm!, api.nextSibling(elm));
removeVnodes(parent, [oldVnode], 0, 0);
}
}
for (i = 0; i < insertedVnodeQueue.length; ++i) {
insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i]);
}
//调用module post hook
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
return vnode;
};
首先会调用 module 的 pre hook,然后会判断传入的第一个参数是否为 vnode 类型,如果不是,会调用 emptyNodeAt 然后将其转换成一个 vnode,接着调用 sameVnode 来判断是否为相同的 vnode 节点,
const isSameKey = vnode1.key === vnode2.key;
const isSameIs = vnode1.data?.is === vnode2.data?.is;
const isSameSel = vnode1.sel === vnode2.sel;
return isSameSel && isSameKey && isSameIs;
如果相同,调用 patchVnode,如果不相同,会调用 createElm 来创建一个新的 dom 节点,然后如果存在父节点,便将其插入到 dom 上,然后移除旧的 dom 节点来完成更新。最后调用元素上的 insert hook 和 module 上的 post hook。
5.6 createElm函数
作用:创建真实dom
function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
let i: any;
let data = vnode.data;
if (data !== undefined) {
const init = data.hook?.init;
if (isDef(init)) {
init(vnode);
data = vnode.data;
}
}
const children = vnode.children;
const sel = vnode.sel;
if (sel === "!") {//注释
if (isUndef(vnode.text)) {
vnode.text = "";
}
vnode.elm = api.createComment(vnode.text!);
} 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;
const elm = (vnode.elm =
isDef(data) && isDef((i = data.ns))
? api.createElementNS(i, tag, data)
: api.createElement(tag, data));
if (hash < dot) elm.setAttribute("id", sel.slice(hash + 1, dot));
if (dotIdx > 0)
elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " "));
for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);
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));
}
const hook = vnode.data!.hook;
if (isDef(hook)) {
hook.create?.(emptyNode, vnode);
if (hook.insert) {
insertedVnodeQueue.push(vnode);
}
}
} else if (options?.experimental?.fragments && vnode.children) {
const children = vnode.children;
vnode.elm = (
api.createDocumentFragment ?? documentFragmentIsNotSupported
)();
for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);
for (i = 0; i < children.length; ++i) {
const ch = children[i];
if (ch != null) {
api.appendChild(
vnode.elm,
createElm(ch as VNode, insertedVnodeQueue)
);
}
}
} else {
vnode.elm = api.createTextNode(vnode.text!);
}
return vnode.elm;
}
5.7 addVnodes与removeVnodes函数
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);
}
}
}
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)) {
// 调用 destory hook
invokeDestroyHook(ch);
// 计算需要调用 removecallback 的次数 只有全部调用了才会移除 dom
listeners = cbs.remove.length + 1;
rm = createRmCb(ch.elm as Node, 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 node
api.removeChild(parentElm, ch.elm as Node);
}
}
}
}
// 调用 destory hook
// 如果存在 children 递归调用
function invokeDestroyHook(vnode: VNode) {
let i: 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 && typeof i !== "string") {
invokeDestroyHook(i);
}
}
}
}
}
// 只有当所有的 remove hook 都调用了 remove callback 才会移除 dom
function createRmCb(childElm: Node, listeners: number) {
return function rmCb() {
if (--listeners === 0) {
const parent = api.parentNode(childElm);
api.removeChild(parent, childElm);
}
};
}
5.8 patchVnode函数
比较新旧两个节点,里面主要关注updateChildren函数
function patchVnode(
oldVnode: VNode,
vnode: VNode,
insertedVnodeQueue: VNodeQueue
) {
//调用prepatch
const hook = vnode.data?.hook;
hook?.prepatch?.(oldVnode, vnode);
const elm = (vnode.elm = oldVnode.elm)!;
const oldCh = oldVnode.children as VNode[];
const ch = vnode.children as VNode[];
if (oldVnode === vnode) return;
if (
vnode.data !== undefined ||
(isDef(vnode.text) && vnode.text !== oldVnode.text)
) {
vnode.data ??= {};
oldVnode.data ??= {};
for (let i = 0; i < cbs.update.length; ++i)
cbs.update[i](oldVnode, vnode);
//调用module上的update hook
vnode.data?.hook?.update?.(oldVnode, vnode);
}
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
// 新旧节点均存在 children,且不一样时,对 children 进行 diff
// thunk 中会做相关优化和这个相关
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
} else if (isDef(ch)) {
// 旧节点不存在 children 新节点有 children
// 旧节点存在 text 置空
if (isDef(oldVnode.text)) api.setTextContent(elm, "");
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
} else if (isDef(oldCh)) {
// 新节点不存在 children 旧节点存在 children 移除旧节点的 children
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
} else if (isDef(oldVnode.text)) {
// 旧节点存在 text 置空
api.setTextContent(elm, "");
}
} else if (oldVnode.text !== vnode.text) {
if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
}
api.setTextContent(elm, vnode.text!);
}
// 调用 postpatch hook
hook?.postpatch?.(oldVnode, vnode);
}
5.9 updateChildren函数
对比新旧节点的children,
function updateChildren(
parentElm: Node,
oldCh: VNode[],
newCh: VNode[],
insertedVnodeQueue: VNodeQueue
) {
let oldStartIdx = 0;
let 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: KeyToIndexMap | undefined;
let idxInOld: number;
let elmToMove: VNode;
let before: any;
// 遍历 oldCh newCh,对节点进行比较和更新
// 每轮比较最多处理一个节点,算法复杂度 O(n)
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 如果进行比较的 4 个节点中存在空节点,为空的节点下标向中间推进,继续下个循环
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];
// 新旧开始节点相同,直接调用 patchVnode 进行更新,下标向中间推进
} 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];
// 旧开始节点等于新的结束节点,说明节点向右移动了,调用 patchVnode 进行更新
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
api.insertBefore(
parentElm,
oldStartVnode.elm!,
api.nextSibling(oldEndVnode.elm!)
);
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
// 旧的结束节点等于新的开始节点,说明节点是向左移动了,逻辑同上
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
// 如果以上 4 种情况都不匹配,可能存在下面 2 种情况
// 1. 这个节点是新创建的
} else {
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
idxInOld = oldKeyToIdx[newStartVnode.key as string];
if (isUndef(idxInOld)) {
// New element
api.insertBefore(
parentElm,
createElm(newStartVnode, insertedVnodeQueue),
oldStartVnode.elm!
);
// 2. 这个节点在原来的位置是处于中间的(oldStartIdx 和 oldEndIdx之间)
} else {
// 如果是已经存在的节点 找到需要移动位置的节点
elmToMove = oldCh[idxInOld];
// 虽然 key 相同了,但是 seletor 不相同,需要调用 createElm 来创建新的 dom 节点
if (elmToMove.sel !== newStartVnode.sel) {
api.insertBefore(
parentElm,
createElm(newStartVnode, insertedVnodeQueue),
oldStartVnode.elm!
);
} else {
// 否则调用 patchVnode 对旧 vnode 做更新
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
// 在 oldCh 中将当前已经处理的 vnode 置空,等下次循环到这个下标的时候直接跳过
oldCh[idxInOld] = undefined as any;
// 插入到 oldStartVnode 的前面(对于当前循环来说,相当于最前面)
api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
}
}
newStartVnode = newCh[++newStartIdx];
}
}
// 循环结束后,可能会存在两种情况
// 1. oldCh 已经全部处理完成,而 newCh 还有新的节点,需要对剩下的每个项都创建新的 dom
if (newStartIdx <= newEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
addVnodes(
parentElm,
before,
newCh,
newStartIdx,
newEndIdx,
insertedVnodeQueue
);
}
// 2. newCh 已经全部处理完成,而 oldCh 还有旧的节点,需要将多余的节点移除
if (oldStartIdx <= oldEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
5.10 diff
- 假设旧节点顺序为[A, B, C, D],新节点为[B, A, C, D, E]
- 第一轮比较:开始结束节点两两并不相等,于是看 newStartVnode 在旧节点中是否存在,最后找到了在第二个位置,调用 patchVnode 进行更新,将 oldCh[1] 至空,将 dom 插入到 oldStartVnode 前面,newStartIdx 向中间移动,状态更新如下
- 第二轮比较:oldStartVnode 和 newStartVnode 相等,直接 patchVnode,newStartIdx 和 oldStartIdx 向中间移动,状态更新如下
4. 第三轮比较:oldStartVnode 为空,oldStartIdx 向中间移动,进入下轮比较,状态更新如下
5. 第四轮比较:oldStartVnode 和 newStartVnode 相等,直接 patchVnode,newStartIdx 和 oldStartIdx 向中间移动,状态更新如下
6. oldStartVnode 和 newStartVnode 相等,直接 patchVnode,newStartIdx 和 oldStartIdx 向中间移动,状态更新如下
7. oldStartIdx 已经大于 oldEndIdx,循环结束,由于是旧节点先结束循环而且还有没处理的新节点,调用 addVnodes 处理剩下的新节点
5.11 钩子函数
export interface Hooks {
//patch开始执行触发
pre?: PreHook;
//createElm执行之前触发,也就把vnode转化为真实dom之前触发
init?: InitHook;
//创建真实dom之后触发
create?: CreateHook;
//patch末尾执行,也就是真实dom添加到dom中触发
insert?: InsertHook;
//patchVnode函数开始执行之前触发,就是对比两个vnode的差异之前触发
prepatch?: PrePatchHook;
//两个vnode对比过程触发
update?: UpdateHook;
//patchVnode函数末尾调用,vnode对比完了
postpatch?: PostPatchHook;
//删除元素之前
destroy?: DestroyHook;
//删除元素之时
remove?: RemoveHook;
//patch最后触发
post?: PostHook;
}