• Vue.js 原理分析


      本文内容提炼于《Vue.js设计与实现》,全书共 501 页,对 Vue.js 的设计原理从 0 到 1,循序渐进的讲解。

      篇幅比较长,需要花些时间慢慢阅读,在合适的位置会给出在线示例以供调试。

    一、概览

      Vue.js 是一款声明式框架,注重结果;早年间流行的 jQuery 是典型的命令式框架,注重过程。命令式的代码需要维护实现目标的整个过程,例如手动完成 DOM 元素的创建、更新、删除等工作。

      Vue.js 帮我们封装了过程, 其内部是命令式的实现,而暴露给用户的则是声明式。虽然声明式代码的性能劣于命令式,但是可维护性更强,其更新性能公式如下。

    声明式的更新性能消耗 = 找出差异的性能消耗 + 直接修改的性能消耗

      虚拟 DOM,就是为了公式中找出差异的性能消耗而出现的。虽然其更新性能理论上不可能比原生的 JavaScript 直接操作 DOM 更高,但是它能保证应用程序的性能下限而不至于太差。

      虽然在更新页面时,虚拟 DOM 在 JavaScript 层面的运算要比创建页面时会多出一个 Diff 的性能消耗,但是它毕竟也是 JavaScript 层面的运算,所以不会产生数量级的差异。

    1)架构

      Vue.js 3 采用了运行时 + 编译时的架构,运行时是指用户可以直接提供数据对象从而无须编译。而编译时是指用户可以提供 HTML 字符串,我们将其编译为数据对象后再交给运行时处理。

      纯运行时没办法分析用户提供的内容,也就无法对内容做进一步优化。纯编译时虽然能直接编译成可执行的 JavaScript 代码(性能可能会更好),但是有损灵活性,即用户提供的内容必须编译后才能用。

    2)核心要素

      Vue.js 作为一款优秀的框架,其核心要素包括:

    1. 开发体验,例如提供友好的警告信息,输出更友好的信息等。
    2. 代码体积,例如支持 Tree-Shaking,构建生产环境时移除开发代码等。
    3. 特性开关,例如为框架添加新特性,支持遗留 API 等。
    4. 错误处理,例如为用户提供统一的错误处理接口。
    5. 支持TypeScript,为 JavaScript 提供类型检查等功能。

    3)声明式地描述

      Vue.js 3 是一个声明式的 UI 框架,即用户在使用 Vue.js 3 开发页面时可以声明式地描述 UI。

      除了使用模板来声明式地描述 UI 之外,我们还可以用 JavaScript 对象来描述(其实就是虚拟 DOM),而使用 JavaScript 对象描述 UI 会更加灵活。虚拟 DOM 其实就是用 JavaScript 对象来描述真实的 DOM 结构,如下所示。

    复制代码
    const vnode = {
      tag: "div",
      props: {
        onClick: () => alert("hello")
      },
      children: "click me"
    };
    复制代码

    4)渲染器

      渲染器(renderer)的作用就是把虚拟 DOM 渲染为真实 DOM,平时编写的 Vue.js 组件都是依赖渲染器来工作的,下面是一个简单的渲染器实现。

    复制代码
    function renderer(vnode, container) {
      // 使用 vnode.tag 作为标签名称创建 DOM 元素
      const el = document.createElement(vnode.tag);
      // 遍历 vnode.props,将属性、事件添加到 DOM 元素
      for (const key in vnode.props) {
        if (/^on/.test(key)) {
          // 如果 key 以 on 开头,说明它是事件
          el.addEventListener(
            key.substr(2).toLowerCase(), // 事件名称 onClick ---> click
            vnode.props[key] // 事件处理函数
          );
        }
      }
      // 处理 children
      if (typeof vnode.children === "string") {
        // 如果 children 是字符串,说明它是元素的文本子节点
        el.appendChild(document.createTextNode(vnode.children));
      } else if (Array.isArray(vnode.children)) {
        // 递归地调用 renderer 函数渲染子节点,使用当前元素 el 作为挂载点
        vnode.children.forEach((child) => renderer(child, el));
      }
      // 将元素添加到挂载点下
      container.appendChild(el);
    }
    复制代码

      现在所做的还仅仅是创建节点,渲染器的精髓是在更新节点的阶段,涉及 Diff  算法。

    复制代码
    const vnode = {
      tag: "div",
      props: {
        onClick: () => alert("hello")
      },
      children: "click again" // 从 click me 改成 click again
    };
    复制代码

      对于渲染器来说,它需要精确地找到 vnode 对象的变更点并且只更新变更的内容。就上例来说,渲染器应该只更新元素的文本内容,而不需要再走一遍完整的创建元素的流程。渲染器的工作原理其实很简单,归根结底,都是使用一些我们熟悉的 API 操作 DOM 来完成渲染工作。

    5)组件的本质

      虚拟 DOM 除了能够描述真实 DOM 之外,还能够描述组件。

      组件的本质就是一组 DOM 元素的封装,可以定义一个函数来代表组件,其返回值就是组件要渲染的内容。

    复制代码
    const MyComponent = function () {
      return {
        tag: "div",
        props: {
          onClick: () => alert("hello")
        },
        children: "click me"
      };
    };
    复制代码

      可以看到,组件的返回值也是虚拟 DOM,它代表组件要渲染的内容。搞清楚了组件的本质,就可以用虚拟 DOM 来描述组件了,用 tag 属性来存储组件函数:

    const vnode = {
      tag: MyComponent
    };

      为了能够渲染组件,需要渲染器的支持。修改前面提到的 renderer 函数。

    复制代码
    function renderer(vnode, container) {
      if (typeof vnode.tag === "string") {
        // 说明 vnode 描述的是标签元素
        mountElement(vnode, container);
      } else if (typeof vnode.tag === "function") {
        // 说明 vnode 描述的是组件
        mountComponent(vnode, container);
      }
    }
    复制代码

      mountElement 函数与上文中 renderer 函数的内容一致。如果 vnode.tag 的类型是函数,则说明它描述的是组件,此时调用 mountComponent 函数完成渲染。

    function mountComponent(vnode, container) {
      // 调用组件函数,获取组件要渲染的内容(虚拟 DOM)
      const subtree = vnode.tag();
      // 递归地调用 renderer 渲染 subtree
      renderer(subtree, container);
    }

    6)模板

      无论是手写虚拟 DOM(渲染函数)还是使用模板,都属于声明式地描述 UI,并且 Vue.js 同时支持这两种描述 UI 的方式。提到模板的工作原理,那就需要讲解一下 Vue.js 中的另外一个重要组成部分:编译器。

      编译器和渲染器一样,只是一段程序而已,不过它们的工作内容不同。编译器的作用其实就是将模板编译为渲染函数。

    <div @click="handler">
      click me
    div>

      对于编译器来说,模板就是一个普通的字符串,它会分析上述字符串并生成一个功能与之相同的渲染函数:

    render() {
      return h('div', { onClick: handler }, 'click me')
    }

      以我们熟悉的 .vue 文件为例,一个 .vue 文件就是一个组件,如下所示:

    复制代码
    <template>
      <div @click="handler">
        click me
      div>
    template>
    <script>
      export default {
        data() {/* ... */ },
        methods: {
          handler: () => {/* ... */ }
        }
    script>
    复制代码

      其中