前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >5. 「snabbdom@3.5.1 源码分析」Thunks 函数

5. 「snabbdom@3.5.1 源码分析」Thunks 函数

作者头像
tinyant
发布2023-02-24 10:04:23
1980
发布2023-02-24 10:04:23
举报

介绍和使用

thunk 函数传入 一个选择器,一个 key 作为 thunk 的身份标识,一个返回 vnode 的函数,和一个 state 数组参数。如果调用,那么 render 函数将会接收 state 作为参数传入。

thunk(selector, key, renderFn, [stateArguments])

当 renderFn 改变 或 [state] 数组长度改变 亦或者 元素改变时 将调用 renderFn

key 是可选的,但是当 selector 在同级 thunks 中不是唯一的时候则需要提供,这确保了在 diff 过程中 thunk 始终能正确匹配。

Thunks 是一种优化方法,用于【数据的不可变性】。

参考这个基于数字创建虚拟节点的函数。

代码语言:javascript
复制
function numberView(n) {
  return h("div", "Number is: " + n);
}

这里的视图仅仅依赖于n,这意味着如果 n 未改变,随后又通过创建虚拟 DOM 节点来 patch 旧节点,这种操作是不必要的,我们可以使用 thunk 函数来避免上述操作。

代码语言:javascript
复制
function render(state) {
  return thunk("num", numberView, [state.number]);
}

这与直接调用 numberView 函数不同的是,这只会在虚拟树中添加一个 伪节点,当 Snabbdom 对照旧节点 patch 这个伪节点时,它会比较 n 的值,如果 n 不变则复用旧的 vnode。这避免了在 diff 过程中重复创建数字视图。

这里的 view 函数仅仅是一个简单的示例,在实际使用中,thunks 在渲染一个需要耗费大量计算才能生成的复杂的视图时才能充分发挥它的价值。

demo

先在源码patchVnode方法中添加日志,因为thunk优化的角度就是减少不必要的diff(只在fn/args发生变化进行对比)

代码语言:javascript
复制
function patchVnode(oldVnode, vnode, insertedVnodeQueue) {
    console.log('patchVnode---', oldVnode,vnode)
    //...
}

index.html

代码语言:javascript
复制
<!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); }

代码语言:javascript
复制
## 用法1,常规用法
```js
const one = numberView(1)
patch(container, one);

setTimeout(() => {
    console.log('---diff---')
    const two = numberView(1)
    patch(one, two);
}, 1 * 1000)

用法2,thunk用法

代码语言:javascript
复制
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)

执行结果:

image.png
image.png

小结

  1. 实际上用法1和用法2的内容都没有发生过变更
  2. 但是第一种用法需要完全对比整颗虚拟DOM树,才能验证整颗树未发生变化
  3. 而第二种用法只是对比了最外层的虚拟DOM节点,这是thunk起的作用,其内部会判断fn和args是否发生变化(fn应该是纯函数,即只能依赖args),如果fn/args没有发生变化则认为返回的虚拟DOM树未变化,因此不再继续对比。

下面看下thunk的具体实现。

分析

代码语言:javascript
复制
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,只有最外面一层。下面重点看下initprepatch做了什么神奇的事情

init

当创建DOM元素时走createElm会调用 vnode.data.hook.init 钩子,看到会调用fn(...args)返回真正的vnode树,而后调用copyToThunk方法拷贝vnode信息给thunk节点

代码语言:javascript
复制
function init(thunk: VNode): void {
  const cur = thunk.data as VNodeData;
  const vnode = (cur.fn as any)(...cur.args!);
  copyToThunk(vnode, thunk);
}

copyToThunk

  1. 显然这里是假定thunk和fn返回的根虚拟DOM是相同的,即使不同,这里也会忽略fn返回的根DOM,只关注children
  2. 关键之处在于children的拷贝 ```js 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); }

代码语言:javascript
复制
## 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逻辑

代码语言:javascript
复制
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

总结

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-01-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 介绍和使用
  • demo
    • 用法2,thunk用法
      • 小结
      • 分析
        • init
          • copyToThunk
      • 总结
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档