深入浅出Vue.js读后总结-模板编译

588 阅读4分钟

前面讲了虚拟DOM,那虚拟DOM是从哪里来的呢?

其实就是vue通过模板编译将模板编译成渲染函数。

模板编译成渲染函数可以分为两个步骤,先将模板解析成AST(抽象语法树),然后在使用AST生成渲染函数。由于静态节点不需要重新渲染,所以在生成AST之后会先遍历AST,给所有静态节点做一个标记,这样在虚拟DOM更新节点是就不会重新渲染它。

所以,模板编译总体分三部分:

(1)将模板编译成AST(通过解析器)

(2)遍历AST标记静态节点(通过优化器)

(3)使用AST生成渲染函数(通过代码生成器)

下面来具体记下:

一、解释器

AST其实就是用js中的对象来描述一个节点,对象中的属性用来保存节点所需的各种数据

解析器分为好几个子解析器,比如HTML解析器,文本解析器,以及过滤解析器,其中最主要的就说HTML解析器。

HTML解析器的作用是解析HTML,在解析HTML的过程中会不断的触发各种钩子函数,这些钩子函数包括开始标签钩子函数、结束标签钩子函数、文本钩子函数以及注释钩子函数。

    parseHTML(html, options){
        //当解析当注释时,执行内部内容
        if (comment.test(html)) {
            ...
        }
        //当执行到标签结束时,执行内部内容
        if (endTagMatch) {
            ...
        }
        //当执行到标签开始时,执行内部内容
        if (startTagMatch) {
             ...
        }
    }

AST是带层级关系的,所以要维护一个栈,每次遇到开始标签时,触发钩子函数start,把当前的节点放入栈中,当遇到结束标签时候,触发钩子钩子函数end,然后从栈中弹出一个节点。这样就可以保证每次触发start钩子函数时,栈的最后一个节点就说当前节点的父节点

二、优化器

静态节点在vue2中就是永远都不会变化的节点,在AST中,静态节点就是指static属性为true的节点,静态根节点就是节点下面的所有子节点都是静态节点,它的父级是动态节点。

静态节点有两点好处:

(1)每次重新渲染时,静态子树不需要重新创建

(2)patching过程中,如果新旧两个节点都时静态子树,就不需要进行对比与更新的操作

优化器主要分为两个步骤:

(1)在AST中找出所有静态节点并打上标记

(2)在AST中造出所有静态根节点并打上标记

先标记所有静态节点,再标记所有静态根节点。

源码中是主要这样实现的:

 function optimize (root, options) {
    if (!root) { return }
    // first pass: mark all non-static nodes.
    markStatic(root);
    // second pass: mark static roots.
    markStaticRoots(root, false);
  }

三、代码生成器

代码生成器将AST转换成代码字符串,代码字符串可以在渲染函数中执行。

渲染函数执行后,就会生成VNode。

代码字符串类似于:

    'with(this){ return _c("div",{attrs:{"id":"el"}},[_v("hellow"+_s(name))])}'

这是一个函数的嵌套调用,函数_c中执行了_v, _v函数中又执行了_s。

_c其实就说createElement的别名,用于创建虚拟节点,所以渲染函数能生成Vnode的原因是因为执行了代码字符串中的_c,也就是createElement。

_c 是createElemnt 创建元素节点

_v 是createTextVNode 创建文本节点

_e 是createEmptyVNode 创建注释节点

然后生成代码字符串是一个递归的过程,每处理一个AST节点就会生成一个对应的代码字符串。

源码中使用genElement生成元素节点的代码字符串

主要代码为:

function genElement (el, state) {
   var code;
    if (el.component) {
      code = genComponent(el.component, el, state);
    } else {
      var data;
      if (!el.plain || (el.pre && state.maybeComponent(el))) {
        data = genData$2(el, state);
      }

      var children = el.inlineTemplate ? null : genChildren(el, state, true);
      code = "_c('" + (el.tag) + "'" + (data ? ("," + data) : '') + (children ? ("," + children) : '') + ")";
    }
    // module transforms
    for (var i = 0; i < state.transforms.length; i++) {
      code = state.transforms[i](el, code);
    }
    return code
}

源码中使用genText生成文本节点的代码字符串

主要源码为:

function genText (text) {
  return ("_v(" + (text.type === 2
    ? text.expression // no need for () because already wrapped in _s()
    : transformSpecialNewlines(JSON.stringify(text.text))) + ")")
}

源码中使用genComment生成注释节点的代码字符串

主要源码为:

function genComment (comment) {
  return ("_e(" + (JSON.stringify(comment.text)) + ")")
}

总的来说:代码生成器就是字符串拼接,通过递归AST生成代码字符串,先生成根节点,然后生成子节点字符串,将其拼接再根节点的参数中,以此类推,最后完成。之后字符串会再with中返回给调用者。

学习真累,想躺平,但是没办法,学习如逆水行舟,不进则退,加油吧