《Vue.js 设计与实现》-读书心得

第一章 权衡的艺术

虚拟 Dom

虚拟 DOM 到页面数据的变更的过程发生了:

  1. 创建页面时候:创建 JavaScript 对象(VNode)、在页面新建所有的 DOM
  2. 更新阶段时候:diff 算法 + 创建新的 JavaScript 对象

性能

  • 对于创建 DOM 来说,命令行代码 ① 性能优于虚拟 DOM
  • 更新 DOM 的时候,vue.js的性能优于命令行代码(innerHTML)
  • 原生的 dom 操作性能是高于用虚拟 dom 的

注:① 命令行代码是指下方的代码

// 命令行代码
const div = document.getElementById("#app");
div.innerText = "Hello world";

总结: 虚拟 DOM 的更新性能消耗为:找出差异的性能消耗(diff 实现) + 直接修改的性能消耗(修改 VNode 和映射更新实际 DOM),相对于大多数的命令式或者直接字符串拼接 innerHTML 相比心理负担更小,可维护性更高,虽然命令式修改 DOM 性能更高,但是在 diff 之后,差距会变小,并且更省编码时间和心智负担

第二章 框架设计的核心要素

  1. 开发体验:足够友好的警告和错误提示;
  2. 框架代码的体积,优化部分开发环境才会执行的代码,然后利用打包工具移除dead code(框架分为开发和生产两个环境的框架包);
  3. 合理利用 Tree-Shaking,其中 rollup 利用 esm(ES Module)进行摇树,一般对于顶级作用域调用函数时候,才会产生全局的副作用(摇树对于涉及到修改全局变量的没法确定是否需要移除代码)下方的例子中的函数foo以及执行函数foo()都会被移除出构建产物;
    // utils.js
    export const foo = (obj) => {
      obj?.foo();
    };
    
    import { foo } from "./utils";
    /*#_PURE_*/ foo();
    
  4. 构建产物的优化,对于浏览器生产环境来说(直接使用浏览器的<script type="module"></script>),不必要的打印需要移除,对于生产环境的使用(npm 方式)来说 esm 就是有必要的了,当然输出 UMD 也更为合适,但是额外的封装也会占用一点点的空间;
  5. 设置特性开关,开发、测试、生产的特性分别对应,减少代码空间;
  6. 减少用户心智负担的错误捕获。

第三章 Vue.js 3 的设计思路

我在这一章看到了渲染器是什么样子,大致的工作原理,组件的本质以及模板的工作原理,下方将会简要意义口语化描述

  1. 第一章说过 vue 是一个声明式的 UI 框架,意思就是用户可以使用声明式描述 UI,当然下方的示例第一段代码就很拉闸,在 第二段代码就稍微舒服了点,h函数取自Hyperscript,意为生成生成 HTML 结构的 script 脚本,在 vue 中简写为h函数,当然最简单的就是写成 template 模板 <h1 @click="handler"></h1>

    export default {
      render() {
        return {
          tag: "h1",
          props: { onClick: handler },
        };
      },
    };
    // 这种更舒服一点
    export default {
      render() {
        return h('h1', {onClick: handler})
      }
    }
    
  2. 渲染器 渲染器其实就是对刚刚声明的 UI 进行渲染,其实就是将虚拟的 DOM 渲染为实际 DOM

    伪代码示例
    // 伪代码,当然最主要的是diff算法的更新vnode
    function render(vnode, container) {
      const el = document.createElement(vnode.tag);
      if (vnode.attrs) {
        Object.keys(vnode.attrs).forEach((key) => {
          el.setAttribute(key, vnode.attrs[key]);
        });
      }
      for (const key in vnode.props) {
        if (/^on/.test(key)) {
          el.addEventListener(key.slice(2).toLowerCase(), vnode.props[key]);
        }
      }
      if (vnode.children) {
        if (Array.isArray(vnode.children)) {
          vnode.children.forEach((child) => {
            if (typeof child === "string") {
              el.appendChild(document.createTextNode(child));
            } else {
              render(child, el);
            }
          });
        } else {
          render(vnode.children, el);
        }
      }
      container.appendChild(el);
    }
    
  3. 组件的本质其实就是 类似一个 vnode,一层 UI 描述的封装,返回值也是 vnode,如果使用的话伪代码就是

    const vnode = {
      tag: Component, // Component 就是 const Component = () => ({tag: 'div', props:{ onClick: handler },children: ['click me']})
    };
    
  4. 模板 说到组件就不得不说模板,不管是手写 render 函数,还是使用模板都是声明式描述 UI,当然之前也提到 Vue 是运行时 + 编译时的一个渐进式 UI 框架,那么有模板就会需要涉及到编译时,当然这个编译时大多是发生在打包时,一般不会直接在代码中进行编译模板的,而是在打包时候动态解析模板(因为 Compiler 函数是会涉及到性能开销的),标记永远不会更新的 vnode,更新 dom 时候不会改动标记为静态 vnode 的 vnode(vue 对 template 模板的性能优化,这样大部分情况模板的性能会比直接使用 render 性能更高)

  5. 渲染器、编译器是协同工作的,模板会先编译成渲染函数,然后是渲染器去执行渲染操作,下方有个编译器优化阶段的小 demo,vue 也是比较推崇使用 template 模板的(配合打包工具,生产环境的模板还是不太推荐的),例如

    <div id="foo :class="cls"><span>我是静态的</span> </div>
    

    那么就会被编译成下方的代码

    render() {
     return {
       tag: 'div',
       props: {
         id: 'foo'.
         class: cls
       },
       patchFlags: 1, // 假设数字1是代表class是动态的,代表只有cls是动态的,对于渲染器来说只需要更新class,
       children: [
         {
           tag: 'span',
           children: '我是静态的',
           static: true // 代表该vnode永远不会设计到变更,在diff中直接忽略更新操作,对于渲染器来说更新时候不需要操作该vnode
         }
       ]
     }
    }
    

第四章 响应式原理和实现

Last Updated: