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 在渲染一个需要耗费大量计算才能生成的复杂的视图时才能充分发挥它的价值。
先在源码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(公共部分,差异部分按照两个用法区分) ```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,常规用法
```js
const one = numberView(1)
patch(container, one);
setTimeout(() => {
console.log('---diff---')
const two = numberView(1)
patch(one, two);
}, 1 * 1000)
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)
执行结果:
下面看下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
做了什么神奇的事情
当创建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);
}
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,如果有变化则递归对比孩子节点。
```js
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
)