经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » JS/JS库/框架 » Vue.js » 查看文章
Vue编译优化实现流程详解
来源:jb51  时间:2023/1/30 15:20:01  对本文有异议

动态节点收集与补丁标志

1.传统diff算法的问题

对于一个普通模板文件,如果只是标签中的内容发生了变化,那么最简单的更新方法很明显是直接替换标签中的文本内容。但是diff算法很明显做不到这一点,它会重新生成一棵虚拟DOM树,然后对两棵虚拟DOM树进行比较。很明显,与直接替换标签中的内容相比,传统diff算法需要做很多无意义的操作,如果能够去除这些无意义的操作,将会省下一笔很大的性能开销。其实,只要在模板编译时,标记出哪些节点是动态的,哪些是静态的,然后再通过虚拟DOM传递给渲染器,渲染器就能根据这些信息,直接修改对应节点,从而提高运行时性能。

2.Block和PatchFlags

对于一个传统的模板:

  1. <div>
  2. <div>
  3. foo
  4. </div>
  5. <p>
  6. {{ bar }}
  7. </p>
  8. </div>

在这个模板中,只用{{ bar }}是动态内容,因此在bar变量发生变化时,只需要修改p标签内的内容就行了。因此我们在这个模板对于的虚拟DOM中,加入patchFlag属性,以此来标签模板中的动态内容。

  1. const vnode = {
  2. tag: 'div',
  3. children: [
  4. { tag: 'div', children: 'foo' },
  5. { tag: 'p', children: ctx.bar, patchFlag: 1 },
  6. ]
  7. }

对于不同的数值绑定,我们分别用不同的patch值来表示:

  • 数字1,代表节点有动态的textContent
  • 数字2,代表节点有动态的class绑定
  • 数字3,代表节点有动态的style绑定
  • 数字4,其他…

我们可以新建一个枚举类型来表示这些值:

  1. enum PatchFlags {
  2. TEXT: 1,
  3. CLASS,
  4. STYLE,
  5. OTHER
  6. }

这样我们就在虚拟DOM的创建阶段,将动态节点提取出来:

  1. const vnode = {
  2. tag: 'div',
  3. children: [
  4. { tag: 'div', children: 'foo' },
  5. { tag: 'p', children: ctx.bar, patchFlag: PatchFlags.TEXT },
  6. ],
  7. dynamicChildren: [
  8. { tag: 'p', children: ctx.bar, patchFlag: PatchFlags.TEXT },
  9. ]
  10. }

3.收集动态节点

首先我们创建收集动态节点的逻辑。

  1. const dynamicChildrenStack = []; // 动态节点栈
  2. let currentDynamicChildren = null; // 当前动态节点集合
  3. function openBlock() {
  4. // 创建一个新的动态节点栈
  5. dynamicChildrenStack.push((currentDynamicChildren = []));
  6. }
  7. function closeBlock() {
  8. // openBlock创建的动态节点集合弹出
  9. currentDynamicChildren = dynamicChildrenStack.pop();
  10. }

然后,我们在创建虚拟节点的时候,对动态节点进行收集。

  1. function createVNode(tag, props, children, flags) {
  2. const key = props && props.key;
  3. props && delete props.key;
  4. const vnode = {
  5. tag,
  6. props,
  7. children,
  8. key,
  9. patchFlags: flags
  10. }
  11. if(typeof flags !== 'undefined' && currentDynamicChildren) {
  12. currentDynamicChildren.push(vnode);
  13. }
  14. return vnode;
  15. }

然后我们修改组件渲染函数的逻辑。

  1. render() {
  2. return (openBlock(), createBlock('div', null, [
  3. createVNode('p', { class: 'foo' }, null, 1),
  4. createVNode('p', { class: 'bar' }, null)
  5. ]));
  6. }
  7. function createBlock(tag, props, children) {
  8. const block = createVNode(tag, props, children);
  9. block.dynamicChildren = currentDynamicChildren;
  10. closeBlock();
  11. return block;
  12. }

4.渲染器运行时支持

  1. function patchElement(n1, n2) {
  2. const el = n2.el = n1.el;
  3. const oldProps = n1.props;
  4. const newProps = n2.props;
  5. // ...
  6. if(n2.dynamicChildren) {
  7. // 如果有动态节点数组,直接更新动态节点数组
  8. patchBlockChildren(n1, n2);
  9. } else {
  10. patchChildren(n1, n2, el);
  11. }
  12. }
  13. function pathcBlockChildren(n1, n2) {
  14. for(let i = 0; i < n2.dynamicChildren.length; i++) {
  15. patchElement(n1.dynamicChildren[i], n2.dynamicChildren[i]);
  16. }
  17. }

由于我们标记了不同的动态节点类型,因此我们可以针对性的完成靶向更新。

  1. function patchElement(n1, n2) {
  2. const el = n2.el = n1.el;
  3. const oldProps = n1.props;
  4. const newProps = n2.props;
  5. if(n2.patchFlags) {
  6. if(n2.patchFlags === 1) {
  7. // 只更新内容
  8. } else if(n2.patchFlags === 2) {
  9. // 只更新class
  10. } else if(n2.patchFlags === 3) {
  11. // 只更新style
  12. } else {
  13. // 更新所有
  14. for(const k in newProps) {
  15. if(newProps[key] !== oldProps[key]) {
  16. patchProps(el, key, oldProps[k], newProps[k]);
  17. }
  18. }
  19. for(const k in oldProps) {
  20. if(!key in newProps) {
  21. patchProps(el, key, oldProps[k], null);
  22. }
  23. }
  24. }
  25. }
  26. patchChildren(n1, n2, el);
  27. }

5.Block树

组件的根节点必须作为Block角色,这样,从根节点开始的所有动态子代节点都会被收集到根节点的dynamicChildren数组中。除了根节点外,带有v-if、v-for这种结构化指令的节点,也会被作为Block角色,这些Block角色共同构成一棵Block树。

静态提升

假设有以下模板

  1. <div>
  2. <p>
  3. static text
  4. </p>
  5. <p>
  6. {{ title }}
  7. </p>
  8. </div>

默认情况下,对应的渲染函数为:

  1. function render() {
  2. return (openBlock(), createBlock('div', null, [
  3. createVNode('p', null, 'static text'),
  4. createVNode('p', null, ctx.title, 1 /* TEXT */)
  5. ]))
  6. }

在这段代码中,当ctx.title属性变化时,内容为静态文本的p标签节点也会跟着渲染一次,这很明显式不必要的。因此,我们可以使用“静态提升”,即将静态节点,提取到渲染函数之外,这样渲染函数在执行的时候,只是保持了对静态节点的引用,而不会重新创建虚拟节点。

  1. const hoist1 = createVNode('p', null, 'static text');
  2. function render() {
  3. return (openBlock(), createBlock('div', null, [
  4. hoist1,
  5. createVNode('p', null, ctx.title, 1 /* TEXT */)
  6. ]))
  7. }

除了静态节点,对于静态props我们也可以将其进行静态提升处理。

  1. const hoistProps = { foo: 'bar', a: '1' };
  2. function render() {
  3. return (openBlock(), createBlock('div', null, [
  4. hoist1,
  5. createVNode('p', hoistProps, ctx.title, 1 /* TEXT */)
  6. ]))
  7. }

预字符化

除了对节点进行静态提升外,我们还可以对于纯静态的模板进行预字符化。对于这样一个模板:

  1. <templete>
  2. <p></p>
  3. <p></p>
  4. <p></p>
  5. <p></p>
  6. <p></p>
  7. ...
  8. <p></p>
  9. <p></p>
  10. <p></p>
  11. <p></p>
  12. </templete>

我们完全可以将其预处理为:

  1. const hoistStatic = createStaticVNode('<p></p><p></p><p></p><p></p>...<p></p><p></p><p></p><p></p>');
  2. render() {
  3. return (openBlock(), createBlock('div', null, [
  4. hoistStatic
  5. ]));
  6. }

这么做的优势:

  • 大块的静态内容可以通过innerHTML直接设置,在性能上具有一定优势
  • 减少创建虚拟节点带来的额外开销
  • 减少内存占用

缓存内联事件处理函数

当为组件添加内联事件时,每次新建一个组件,都会为该组件重新创建并绑定一个新的内联事件函数,为了避免这方面的无意义开销,我们可以对内联事件处理函数进行缓存。

  1. function render(ctx, cache) {
  2. return h(Comp, {
  3. onChange: cache[0] || cache[0] = ($event) => (ctx.a + ctx.b);
  4. })
  5. }

v-once

v-once指令可以是组件只渲染一次,并且即使该组件绑定了动态参数,也不会更新。它与内联事件一样,也是使用了缓存,同时通过setBlockTracking(-1)阻止该VNode被Block收集。

v-once的优点:

  • 避免组件更新时重新创建虚拟DOM带来的性能开销
  • 避免无用的Diff开销

到此这篇关于Vue编译优化实现流程详解的文章就介绍到这了,更多相关Vue编译优化内容请搜索w3xue以前的文章或继续浏览下面的相关文章希望大家以后多多支持w3xue!

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号