这是一个系列文章,请关注snabbdom@3.5.1 源码分析专栏、vue@2.6.11 源码分析 专栏
介绍和使用
thunk
函数传入 一个选择器,一个 key 作为 thunk 的身份标识,一个返回 vnode 的函数,和一个 state 数组参数。如果调用,那么 render 函数将会接收 state 作为参数传入。
thunk(selector, key, renderFn, [stateArguments])
当 renderFn
改变 或 [state]
数组长度改变 亦或者 元素改变时 将调用 renderFn
。
key
是可选的,但是当 selector
在同级 thunks 中不是唯一的时候则需要提供,这确保了在 diff 过程中 thunk 始终能正确匹配。
Thunks 是一种优化方法,用于【数据的不可变性】。
参考这个基于数字创建虚拟节点的函数。
function numberView(n) {
return h("div", "Number is: " + n);
}
这里的视图仅仅依赖于n
,这意味着如果 n
未改变,随后又通过创建虚拟 DOM 节点来 patch 旧节点,这种操作是不必要的,我们可以使用 thunk
函数来避免上述操作。
function render(state) {
return thunk("num", numberView, [state.number]);
}
这与直接调用 numberView
函数不同的是,这只会在虚拟树中添加一个 伪节点,当 Snabbdom 对照旧节点 patch 这个伪节点时,它会比较 n
的值,如果 n
不变则复用旧的 vnode。这避免了在 diff 过程中重复创建数字视图。
这里的 view 函数仅仅是一个简单的示例,在实际使用中,thunks 在渲染一个需要耗费大量计算才能生成的复杂的视图时才能充分发挥它的价值。
demo
先在源码patchVnode方法中添加日志,因为thunk
优化的角度就是减少不必要的diff
(只在fn/args发生变化进行对比)
function patchVnode(oldVnode, vnode, insertedVnodeQueue) {
console.log('patchVnode---', oldVnode,vnode)
//...
}
- index.html
<!DOCTYPE html>
<html>
<head>
<title>easy demo</title>
<script type="module" src="./index.js"></script>
</head>
<body>
<div id="container"></div>
</body>
</html>
- index.js(公共部分,差异部分按照两个用法区分)
import {init, classModule, propsModule, styleModule, eventListenersModule, thunk, h} from "../build/index.js";
const patch = init([// Init patch function with chosen modules
classModule, // makes it easy to toggle classes
propsModule, // for setting properties on DOM elements
styleModule, // handles styling on elements with support for animations
eventListenersModule // attaches event listeners
]);
const container = document.getElementById("container");
function numberView(n) {
const children = [h('span#b', {style: {color: 'red'}}, "Number is: "), n]
return h("span#a", {style: {border: '1px solid red', display: 'inline-block'}}, children);
}
用法1,常规用法
const one = numberView(1)
patch(container, one);
setTimeout(() => {
console.log('---diff---')
const two = numberView(1)
patch(one, two);
}, 1 * 1000)
用法2,thunk用法
function render(state) {
// 注意:这里的sel需要和numberView返回的vnode的sel是相同的
return thunk("span#a", numberView, [state.number]);
}
const one = render({number: 1})
patch(container, one);
setTimeout(() => {
console.log('---diff---')
const two = render({number: 1}); // 注意:数据未变更
patch(one, two);
}, 1 * 1000)
执行结果:
小结
- 实际上用法1和用法2的内容都没有发生过变更
- 但是第一种用法需要完全对比整颗虚拟DOM树,才能验证整颗树未发生变化
- 而第二种用法只是对比了最外层的虚拟DOM节点,这是thunk起的作用,其内部会判断fn和args是否发生变化(fn应该是纯函数,即只能依赖args),如果fn/args没有发生变化则认为返回的虚拟DOM树未变化,因此不再继续对比。
下面看下thunk的具体实现。
分析
export const thunk = function thunk( sel: string, key?: any, fn?: any, args?: any): VNode {
if (args === undefined) {
args = fn;
fn = key;
key = undefined;
}
return h(sel, {
key: key,
hook: { init, prepatch },
fn: fn,
args: args,
});
} as ThunkFn;
看到thunk
返回一个虚拟DOM节点就是后面init/prepatch
的入参thunk: VNode
,只有最外面一层。下面重点看下init
和prepatch
做了什么神奇的事情
init
当创建DOM元素时走createElm会调用 vnode.data.hook.init 钩子,看到会调用fn(...args)
返回真正的vnode树,而后调用copyToThunk
方法拷贝vnode信息给thunk节点
function init(thunk: VNode): void {
const cur = thunk.data as VNodeData;
const vnode = (cur.fn as any)(...cur.args!);
copyToThunk(vnode, thunk);
}
copyToThunk
- 显然这里是假定thunk和fn返回的根虚拟DOM是相同的,即使不同,这里也会忽略fn返回的根DOM,只关注children
- 关键之处在于children的拷贝
function copyToThunk(vnode: VNode, thunk: VNode): void {
const ns = thunk.data?.ns;
(vnode.data as VNodeData).fn = (thunk.data as VNodeData).fn;
(vnode.data as VNodeData).args = (thunk.data as VNodeData).args;
thunk.data = vnode.data;
thunk.children = vnode.children;
thunk.text = vnode.text;
thunk.elm = vnode.elm;
if (ns) addNS(thunk.data, thunk.children, thunk.sel);
}
prepatch
先看下patchVnode在这里的关键地方:调用prepatch钩子,而后对比oldCh和ch,如果有变化则递归对比孩子节点。
function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
//...
const hook = vnode.data?.hook;
hook?.prepatch?.(oldVnode, vnode);
//...
const oldCh = oldVnode.children as VNode[];
const ch = vnode.children as VNode[];
//... 由于是直接拷贝的oldVnode.children(见copyToThunk)因此不满足if不会进入updateChildren
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
}
下面看下prepatch逻辑
function prepatch(oldVnode: VNode, thunk: VNode): void {
let i: number;
const old = oldVnode.data as VNodeData;
const cur = thunk.data as VNodeData;
const oldArgs = old.args;
const args = cur.args;
if (old.fn !== cur.fn || (oldArgs as any).length !== (args as any).length) {
copyToThunk((cur.fn as any)(...args!), thunk);
return;
}
for (i = 0; i < (args as any).length; ++i) {
if ((oldArgs as any)[i] !== (args as any)[i]) {
copyToThunk((cur.fn as any)(...args!), thunk);
return;
}
}
copyToThunk(oldVnode, thunk);
}
vnode.data.hook.prepach的触发时机:patchVnode方法开始正式对比两个节点之前(pre-patch即在patch之前),会判断fn/args是否发生变化了,发生变化则重新执行fn(...args)返回新的虚拟DOM树,如果没有变化,直接将oldVnode信息拷贝给thunk(需要拷贝的,因为此时thunk节点还只是一个壳子),而后在patchVnode
方法中就不进入孩子的对比即updateChildren
(因为此时oldCh == ch
)