模板编译

Vue 使用 Template 语法,编译时将 Template 转为 render 函数。

JSX-in-Vue

跳过编译过程,使用 JSX 语法编译 render 函数,最终都是靠 render 函数生成虚拟 DOM。

使用 JSX

export default defineComponent({
  props: {
    level: {
      type: Number,
      required: true,
    },
  },
  setup(props, { slots }) {
    const tag = "h" + props.level;
    // 利用 JSX 写 h()
    return () => <tag>{slots.default()}</tag>;
  },
});

h 函数,可以处理动态性更高的场景。h 函数内部也是调用 createVnode 来返回虚拟 DOM。对于那些创建虚拟 DOM 的函数,我们统一称为 h 函数。

JSX 的本质

本质是 createVnode 的语法糖

const element = <h1 id="app">Hello World</h1>;
const element = createVnode("h1", { id: "app" }, "Hello World");

抉择

在 TodoList 组件使用 JSX 写 render 函数 👇

return () => (
  <div>
    <input type="text" vModel={title.value} />
    <button onClick={addTodo}>click</button>
    <ul>
      {todos.value.length ? (
        todos.value.map((todo) => {
          return <li>{todo.title}</li>;
        })
      ) : (
        <li>no data</li>
      )}
    </ul>
  </div>
);
  • 对动态性要求很高的场景使用 JSX,本质还是在写 JavaScript
  • JSX 相比于 template 还有一个优势,是可以在一个文件内返回多个组件
  • 当然我们接受一些操作上的限制(Template),但同时也会获得一些系统优化的收益(Vue 编译期间进行静态标记的优化)

编译优化

Template

<div id="app">
  <div @click="()=>console.log(xx)" name="hello">{{name}}</div>
  <h1>技术摸鱼</h1>
  <p :id="name" class="app">极客时间</p>
</div>

Vue3 编译后结果 🆚

import {
  toDisplayString as _toDisplayString,
  createElementVNode as _createElementVNode,
  openBlock as _openBlock,
  createElementBlock as _createElementBlock,
} from "vue";

const _hoisted_1 = { id: "app" };
const _hoisted_2 = /*#__PURE__*/ _createElementVNode(
  "h1",
  null,
  "技术摸鱼",
  -1 /* HOISTED */
);
const _hoisted_3 = ["id"];

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (
    _openBlock(),
    _createElementBlock("div", _hoisted_1, [
      _createElementVNode(
        "div",
        {
          onClick: _cache[0] || (_cache[0] = () => _ctx.console.log(_ctx.xx)),
          name: "hello",
        },
        _toDisplayString(_ctx.name),
        1 /* TEXT */
      ),
      _hoisted_2,
      _createElementVNode(
        "p",
        {
          id: _ctx.name,
          class: "app",
        },
        "极客时间",
        8 /* PROPS */,
        _hoisted_3
      ),
    ])
  );
}
  1. 首先,静态的标签和属性会放在 _hoisted 变量中,并且放在 render 函数之外。重复执行 render 的时候,代码里的 h1 这个纯静态的标签,就不需要进行额外地计算,并且静态标签在虚拟 DOM 计算的时候,会直接跳过 Diff 过程。
  2. @click 函数增加了一个 cache 缓存层,这样实现出来的效果也是和静态提升类似,尽可能高效地利用缓存。
  3. 最后,那些带冒号的属性是动态属性,因而存在使用一个数字去标记标签的动态情况。

比如在 p 标签上,使用 8 这个数字标记当前标签时,只有 props 是动态的。而在虚拟 DOM 计算 Diff 的过程中,可以忽略掉 class 和文本的计算,这也是 Vue 3 的虚拟 DOM 能够比 Vue 2 快的一个重要原因。

PatchFlag

位运算做静态标记

export const enum PatchFlags {
  TEXT = 1,// 动态的文本节点
  CLASS = 1 << 1,  // 2 动态的 class
  STYLE = 1 << 2,  // 4 动态的 style
  PROPS = 1 << 3,  // 8 动态属性,不包括类名和样式
  FULL_PROPS = 1 << 4,  // 16 动态 key,当 key 变化时需要完整的 diff 算法做比较
  HYDRATE_EVENTS = 1 << 5,  // 32 表示带有事件监听器的节点
  STABLE_FRAGMENT = 1 << 6,   // 64 一个不会改变子节点顺序的 Fragment
  KEYED_FRAGMENT = 1 << 7, // 128 带有 key 属性的 Fragment
  UNKEYED_FRAGMENT = 1 << 8, // 256 子节点没有 key 的 Fragment
  NEED_PATCH = 1 << 9,   // 512
  DYNAMIC_SLOTS = 1 << 10,  // 动态 slot
  HOISTED = -1,  // 特殊标志是负整数表示永远不会用作 diff
  BAIL = -2 // 一个特殊的标志,指代差异算法
}

HoistStatic

静态提升

<div>
  <p>静态标记</p>
  <p>{{ message }}</p>
</div>
import {
  createElementVNode as _createElementVNode,
  toDisplayString as _toDisplayString,
  openBlock as _openBlock,
  createElementBlock as _createElementBlock,
} from "vue";

const _hoisted_1 = /*#__PURE__*/ _createElementVNode(
  "p",
  null,
  "静态提升",
  -1 /* HOISTED */
);

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (
    _openBlock(),
    _createElementBlock("div", null, [
      _hoisted_1,
      _createElementVNode(
        "p",
        null,
        _toDisplayString(_ctx.message),
        1 /* TEXT */
      ),
    ])
  );
}

CacheHandler

事件监听缓存

<div>
  <p @click="handleClick">wuli一giao</p>
</div>
import {
  createElementVNode as _createElementVNode,
  openBlock as _openBlock,
  createElementBlock as _createElementBlock,
} from "vue";

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (
    _openBlock(),
    _createElementBlock("div", null, [
      _createElementVNode(
        "p",
        {
          onClick:
            _cache[0] ||
            (_cache[0] = (...args) =>
              _ctx.handleClick && _ctx.handleClick(...args)),
        },
        "wuli一giao"
      ),
    ])
  );
}

SSR

借助服务端渲染,将静态标签直接转化为文本。省去编译生成 VNode 再转成真实 DOM 的开销。

<div>
  <p>测试</p>
  <p>测试1</p>
  <p>测试2</p>
  <p>测试3</p>
  <p>测试4</p>
  <p @click="handleClick">wuli一giao</p>
</div>
import { mergeProps as _mergeProps } from "vue";
import { ssrRenderAttrs as _ssrRenderAttrs } from "vue/server-renderer";

export function ssrRender(
  _ctx,
  _push,
  _parent,
  _attrs,
  $props,
  $setup,
  $data,
  $options
) {
  const _cssVars = { style: { color: _ctx.color } };
  _push(
    `<div${_ssrRenderAttrs(_mergeProps(_attrs, _cssVars))}>
      <p>测试</p>
      <p>测试1</p>
      <p>测试2</p>
      <p>测试3</p>
      <p>测试4</p>
      <p>wuli一giao</p>
    </div>`
  );
}

StaticNode

在客户端渲染的时候,只要标签嵌套得足够多,编译时也会将其转化为 HTML 字符串。

<div>
  <p>giao1</p>
  <p>giao2</p>
  <p>giao3</p>
  <p>giao4</p>
  <p>giao5</p>
  <p>giao6</p>
  <p>giao7</p>
  <p>giao8</p>
  <p>giao9</p>
  <p>giao10</p>
</div>
import {
  createElementVNode as _createElementVNode,
  createStaticVNode as _createStaticVNode,
  openBlock as _openBlock,
  createElementBlock as _createElementBlock,
} from "vue";

const _hoisted_1 = /*#__PURE__*/ _createStaticVNode(
  "<p>giao1</p><p>giao2</p><p>giao3</p><p>giao4</p><p>giao5</p><p>giao6</p><p>giao7</p><p>giao8</p><p>giao9</p><p>giao10</p>",
  10
);
const _hoisted_11 = [_hoisted_1];

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return _openBlock(), _createElementBlock("div", null, _hoisted_11);
}