前言
源码注释链接
snabbdom
就是Virtual DOM
实现,简单又纯粹,Vue
就是借鉴了这个库。众所周知,真实DOM
是很庞大的,属性颇多,那么我们使用JS
来定义一个DOM
对象(包含一些常用的属性),那么这个花费就会大大减小,自然而然也就提升了性能
Virtual DOM
需要解决以下几点:
- 用JS对象表示DOM
- 高性能的
diff
算法,比较俩个DOM
节点的差异 - 更新需要更新的DOM到真实的DOM上
有个前提就是比较俩棵树最小修改时间复杂度 O(n^3),这个很糟糕的性能,所以我们假设我们基本上不做跨层级的修改,那么我们就可以只做本层的对比,时间复杂度就能达到 O(n),也就是不管子节点的变化,整个节点替换即可
正文
目录
|-- snabbdom
|-- h.js 创建vnode
|-- hooks.js 钩子接口定义(仅限ts)
|-- htmldomapi.js dom操作工具方法
|-- is.js 判断工具方法
|-- snabbdom.bundle.js 入口文件
|-- snabbdom.js 初始化函数,核心方法所在
|-- thunk.js thunk实现优化性能
|-- tovnode.js 将dom转vnode方法
|-- vnode.js vnode函数对象定义
|-- helpers
| |-- attachto.js 装饰vnode,并挂载到指定节点
|-- modules 模块
|-- attributes.js 属性,只能用setAttribute设置的
|-- class.js 类
|-- dataset.js 数据
|-- eventlisteners.js 事件
|-- hero.js css-transition动效
|-- module.js 模块钩子接口定义(仅限ts)
|-- props.js props可以直接.操作符设置,eg: .src = xxx
|-- style.js 样式
流程图
![](https://img.haomeiwen.com/i4874009/00045260b9b0ee6c.png)
hook
-
全局钩子
对应modules
上,所有节点共享 -
节点钩子
对应每一个节点,节点上定义了才有,如下例所示
h('div', {
hook: {
init(vnode) {
}
}
}, 'abc')
// 全局钩子只有这六种
// 节点钩子有8种 init create insert prepatch update postpatch destroy remove
const hooks = ['create', 'update', 'remove', 'destroy', 'pre', 'post'];
Name | Triggered when | Arguments to callback |
---|---|---|
pre |
patch开始的时候(patch)
|
none |
init |
vnode要被创建为DOM的时候(createElm) | vnode |
create |
vnode已经被创建为DOM的时候(createElm) | emptyVnode, vnode |
insert |
vnode已经被插入到DOM的时候,收集要插入的节点,patch要完成时触发(patch) | vnode |
prepatch |
俩节点比对前(patchNode) | oldVnode, vnode |
update |
节点更新过程中(patchNode) | oldVnode, vnode |
postpatch |
节点对比完成(patchNode) | oldVnode, vnode |
destroy |
节点被删除时调用,子节点也会被触发 (removeVnodes) |
vnode |
remove |
节点被删除时触发,不影响子节点 ,和destroy类似(removeVnodes) |
vnode, removeCallback |
post |
patch结束的时候触发(patch) | none |
updateChildren
五条比对规则:
- 旧新首首比对,旧新首索引右移
- 旧新尾尾比对,旧新尾索引左移
- 旧新首尾比对,旧首索引右移、新尾索引左移
- 旧新尾首比对,旧尾索引左移、新首索引右移
- 旧新通过key找寻在旧节点组相同的新首节点,新首索引右移(因为新首节点已经比对过了)
demo,如下例、图示
const vnode = h('ul', [h('li', {
key: 0
}, 'A'), h('li', {
key: 1
}, 'B'), h('li', {
key: 2
}, 'C'), h('li', {
key: 3
}, 'D')])
patch(container, vnode)
const nVnode = h('ul', [h('li', {
key: 3
}, 'D'), h('li', {
key: 1
}, 'B'), h('li', {
key: 2
}, 'C'), h('li', {
key: 0
}, 'A'), h('li', {
key: 4
}, 'E')])
patch(vnode, nVnode)
![](https://img.haomeiwen.com/i4874009/88b924fb37b06c17.png)
thunk
这个其实来源于传名调用与传值调用,就像下文,是先把1+2
算出来然后传入fn
(传值),还是把1+2
整个传入fn
(传名)
这俩各有优劣,很明显传值
很简单,但是可能有性能影响,毕竟我传入的值fn
可能没用。传名
能避免这个,但是明显麻烦多了
function fn(a) {
return a
}
fn(1 + 2)
我们这里thunk
就是这意思,如下
demo
这里render
方法是返回一个thunk
函数创建的vnode
(实际上是仿造的),这时候要是通过thunk
判定,新旧的vnode
一样,那么就没必要创建vnode
去path、diff
。要是没通过,那么就是不一样的,就会调用prepatch
钩子时调用传入的renderNumber
(传入的挂在data.fn),创建一个真正的vnode
去path
function renderNumber(num) {
return h('span', num);
}
function render(num) {
return thunk('div', renderNumber, [num]);
}
// 这里会调用renderNumber创建一个vnode,因为path内肯定走createElement,所以会调用init hook钩子
const vnode1 = patch(container, render(1))
// 这里因为render(1)创建的空壳vnode(vnode2)的fn、args和vnode的一样,所以会把vnode的属性赋值(copyToThunk)给vnode2
// 这样子自然不会调用renderNumber
const vnode2 = patch(vnode1, render(1))
attachTo
demo
这个方法其实是用于挂载vnode
,被该方法处理的vnode会被挂载到指定的target
节点,也就是会脱离原本该在的位置而在原地留下一个空span
节点
props
demo
此demo
是修改input文本之后点击button
,会跳过此语句。就是因为value
太容易被改变了,也就是old
没变elm[key]
变了
if (old !== cur && (key !== 'value' || elm[key] !== cur)) {
elm[key] = cur;
}
eventlisteners
其实看懂了以上到这里就比较简单了。有点提一下,这里的复用旧listener
有点技巧
function createListener() {
return function handler(event) {
// 事件触发,触发到此函数
// 这里的handler就是之前的listener,挂载了vnode数据
handleEvent(event, handler.vnode);
};
}
function updateEventListeners(oldVnode, vnode) {
// ...
// 复用老监听器,这里很巧妙。因为这个listener都是createListener创建的,他返回一个函数对象,
// 下面赋值给它vnode,那么触发到这个函数的时候,在函数内部只需要取本函数就可以取到挂载到上面的vnode
var listener = vnode.listener = oldVnode.listener || createListener();
// update vnode for listener
// 更新监听器的vnode
listener.vnode = vnode;
// if element changed or added we add all needed listeners unconditionally
if (!oldOn) {
for (name in on) {
// add listener if element was changed or new listeners added
elm.addEventListener(name, listener, false);
}
}
// ...
}
style
- 值得注意的是
nextFrame
说的是下一帧,其实需要两帧
,即32ms
// 下两帧更改样式,浏览器16帧/ms来渲染,两帧才能看出变化
var nextFrame = function (fn) {
raf(function () {
raf(fn);
});
};
- 应用样式之前需要
强制页面回流
,不然transform
样式会直接到结束状态
function applyRemoveStyle(vnode, rm) {
// ...
// 首次remove的时候需要强制回流
if (!reflowForced) {
// elm.offsetLeft需要返回最新的布局信息,因此浏览器不得不触发回流重绘来返回正确的值
// 若是不强制回流,那么transform 3s将直接到最终,自然而然的也就没了transitionend事件的回调触发,也就节点不会被remove掉
vnode.elm.offsetLeft;
// 若是同时删除俩个节点,那么回流一次就可以了
/**
var vnode1 = h('div.parent', {}, [btn1, btn2]);
var vnode2 = h('div.parent', {}, [null, null]);
*/
reflowForced = true;
}
}
function forceReflow() {
reflowForced = false;
}
网友评论