Vue2 Source

Vue2 Parser

deprecated warning

Data Flow

  • HTML Template:html string
  • Parser:ast nodes & expressions
  • Optimizer:markStatic
  • Code Generator:with(this) { createElement(...) }
  • Render:VNodes
  • Node Patching:VNode Diff
  • DOM:finnaly...

Parser

Paser 分为三类,HTML Parser、文本解析器(Text Parser)、过滤器解析器(Filter Parser)。

Vue 中的 HTML Parser 是在某个已有的 Parser 基础上改造而成的。原有 Parser 是一个不断解析传入 HTML 的有限状态机,不停地对标签开头、属性、标签结尾及文本等类型进行正则匹配,并处理匹配得到的内容。其内部维护了一个堆栈,可以很好地处理节点间的层级关系。

Vue 通过原 HTML Parser 提供的接口,传入钩子函数,分别对标签、文本等做额外处理。比如说,HTML 中的文本要按照文本解析器,将模板的插值和纯文本解析出来,这样才可以将变量动态代入,生成字符串。最终,HTML 通过 Parser 解析,得到了 AST。

Code Example

Optimizer

优化器(Optimizer)在 HTML Parser 生成的 AST 的基础上,对静态节点进行标记,以提高重渲染以及 node patching 时的性能:node patching 时静态节点完全不变,所以可以跳过比较;重渲染时意味着可以在新的渲染过程中服用第一次渲染时生成好的 DOM Node。

Generator

Generator 在这里指代码生成器。Generator 从 AST 节点的根开始遍历,把所以节点转化为类似 _c(tag,data,children) 的字符串。这些字符串最后经过运行,也就得到了货真价实的 VNodes。也就是说,_c 和实例的 _h 函数是一样的。它在 Vue 实例,也就是组件,的初始化时就被诸如。除了 _c,Generator 中还添加了许多类似的函数,用于创建 VNode,比如 _s,对应 createTextNode。这些函数在 Vue 实例的 renderMixin 时,被挂载到实例原型上。

Node Patching

VNode

虚拟节点通过一些特定的选项来表示真实的 DOM 结构。由于只涉及 JS 计算,所以在需要批量操作的情况下,操作虚拟节点要比操作 DOM 开销要小。

主要有以下几类:

  • EmptyVNode:空节点
  • TextVNode:文本节点
  • ElementVNode:元素节点
  • ComponentVNode:组件节点
  • CloneVNode:克隆节点

渲染函数通过 AST Node 生成了 VNode,这些 VNode 可能被复用,此时会拷贝出一个新节点用来渲染,这个节点就是克隆节点。

function cloneVNode(vnode) {
  const clonedChild = (vnode.children || []).map((vnode) => cloneVNode(vnode));
  const cloned = createElement(vnode.tag, vnode.data, clonedChild);
  const copyProps = [
    "text",
    "isComment",
    "isStatic",
    "key",
    "elm",
    "context",
    "ns",
    "componentOptions",
  ];
  copyProps.map((prop) => (cloned[prop] = vnode[prop]));
  return cloned;
}

关于 VNode 的属性更多解释见 vnode.js

使用 VNode 作为节点状态和 DOM 之间的中间层,可以避免性能浪费。每次渲染时,VNode 可以和上一次的 VNode 进行比较,以便重新生成 DOM 或是只改变 DOM 的一部分。

CreateElement

VueJS createElement API 解析如下:

createElement(
  'name', // HTML 元素名、组件名或者函数也行
  { // 一些可选参数
    class: { /* loaded: isLoaded */ }, // :class
    style: { /* background: isRed ? 'red' : 'white' */ }, // :style
    props: { /* name */ }, // 组件 Props
    attrs: { /* id, class */ }, // HTML attributes
    domProps: { /* innerHTML、innerText */ }, // DOM props
    on: { /* !~click: () => {} */ }, // 通过 Vue.$emit 触发的事件;感叹号和波浪号分别代表 capture 和 once
    nativeOn: { /* click: () => {} */ }, // 原生 DOM 事件
    scopedSlot: { /* default: props => h('div', {}, props.text) */ }, // 作用域插槽
    key: '/* keyName */',
    ref: '/* refName */',
  },
  []  // 子节点或者文本节点
)

Node Diff

对比节点可简述为:

  • 两个节点是否相同?结束更新。
  • 两个节点是否是静态节点?结束更新。
  • 新节点有 text 属性?
  • 两个节点都有子节点?如果子节点不同则更新子节点。
  • 只有新节点有子节点?
  • 只有旧节点有子节点?
  • 旧节点有 text 属性?

对比子节点是用一个从两边至中间的循环,分别比较新前旧前、新后旧后、新后旧前、新前旧后。

de-indent

de-indent 是 Vue 解析 SFC 是引用的一个工具包。能将代码中额外的前置缩进去掉。它做了以下三件事情:

  • 将源码拆分为行
  • 遍历代码行,找到最小缩进(或空格)数量
  • 每行都去除最小缩进(或空格)数量

简单实现

其实我在写博客时碰到过额外的前置缩进这种问题。当时,写了一个 highlight.js 的 Vue 组件封装,用来在 Markdown 的 HTML 代码中写代码高亮,如:

<p>
    <Highlight lang='js'>
        export default {
            hello: 'world'
        }
    </Highlight>
</p>

看起来没啥问题,但是这段文本传到 Vue 组件内部,就会带上一些不必要的缩进,和前后两个多余的换行。

export default {
  hello: "world",
};

对于缩进,简单处理如下。将源码拆分为行,找出最小的前置空格数量,记长度为 minSpace,然后每行去除这个等长的前置空格:

const splits = codes.split(/\n/);
const tabs = splits.map((x) => x.search(/[^\s]/));
const minSpace = Math.min(...tabs);
const reMinSpace = new RegExp(`\\s{${minSpace}}`);
codes = splits.map((x) => x.replace(reMinSpace, "")).join("\n");

源码解析

思路是一模一样的,但是 de-indent 这玩意儿性能高很多倍。

var splitRE = /\r?\n/g;
var emptyRE = /^\s*$/;
var needFixRE = /^(\r?\n)*[\t\s]/;

module.exports = function deindent(str) {
  // 如果第一行前没有空格,直接退出
  if (!needFixRE.test(str)) {
    return str;
  }
  // 将代码拆分成行
  var lines = str.split(splitRE);

  /* 这里只循环了一次就找到了最小空格数量,并且做了优化 */
  var min = Infinity;
  // type 用来记录缩进是 tab 还是空格
  var type, cur, c;
  for (var i = 0; i < lines.length; i++) {
    var line = lines[i];
    if (!emptyRE.test(line)) {
      if (!type) {
        c = line.charAt(0);
        if (c === " " || c === "\t") {
          type = c;
          cur = count(line, type);
          if (cur < min) {
            min = cur;
          }
        } else {
          return str;
        }
      } else {
        cur = count(line, type);
        if (cur < min) {
          min = cur;
        }
      }
    }
  }
  // 用 String.slice 切除前置空格,性能最好
  return lines
    .map(function (line) {
      return line.slice(min);
    })
    .join("\n");
};

function count(line, type) {
  var i = 0;
  while (line.charAt(i) === type) {
    i++;
  }
  return i;
}

Copyright © 2024 Lionad - CC-BY-NC-CD-4.0