vue模版编译源码(二)

113 阅读2分钟

上一个blog讲了vue模版编译的parse部分,这篇主要讲一下优化和生成的源码

var createCompiler = createCompilerCreator(function baseCompile (
    template,
    options
  ) {
    var ast = parse(template.trim(), options);
    if (options.optimize !== false) {
      optimize(ast, options);
    }
    var code = generate(ast, options);
    return {
      ast: ast,
      render: code.render,
      staticRenderFns: code.staticRenderFns
    }
  });

optimize

/**
   * Goal of the optimizer: walk the generated template AST tree
   * and detect sub-trees that are purely static, i.e. parts of
   * the DOM that never needs to change.
   *
   * Once we detect these sub-trees, we can:
   *
   * 1. Hoist them into constants, so that we no longer need to
   *    create fresh nodes for them on each re-render;
   * 2. Completely skip them in the patching process.
*/

optimize的目标:walk一遍parse生成的ast树, 检测出来所有静态(dom中永远都不需要改变)的子树,一旦检测出来:

  1. 将它们变成常量, 这样re-render的时候不需要再去为他们生成新的node
  2. 在patch阶段跳过它们
export function optimize (root: ?ASTElement, options: CompilerOptions) {
  if (!root) return
  isStaticKey = genStaticKeysCached(options.staticKeys || '')
  isPlatformReservedTag = options.isReservedTag || no
  // first pass: mark all non-static nodes.
  markStatic(root)
  // second pass: mark static roots.
  markStaticRoots(root, false)
}

optimize 方法主要有markStatic和markStaticRoots 两个方法,分别看下两个方法做了什么

// markStatic 将递归循环整个树,根据每个node的情况标记static为当前node是否是静态node
function markStatic (node: ASTNode) {
  node.static = isStatic(node)
  if (node.type === 1) {
    // do not make component slot content static. this avoids
    // 1. components not able to mutate slot nodes
    // 2. static slot content fails for hot-reloading
    // if (
    //   !isPlatformReservedTag(node.tag) &&
    //   node.tag !== 'slot' &&
    //   node.attrsMap['inline-template'] == null
    // ) {
    //   return
    // }
    // 递归循环node.children
    for (let i = 0, l = node.children.length; i < l; i++) {
      const child = node.children[i]
      markStatic(child)
      // 如果子node的不是静态node,那么父node的static重制为false
      if (!child.static) {
        node.static = false
      }
    }
    // 如果有ifConditions数组,则循环ifConditions数组中的所有情况的node
    if (node.ifConditions) {
      for (let i = 1, l = node.ifConditions.length; i < l; i++) {
        const block = node.ifConditions[i].block
        markStatic(block)
        // 如果node的ifConditions中有一个情况的node不是静态的,那么node肯定也不是静态的
        if (!block.static) {
          node.static = false
        }
      }
    }
  }
}

具体看下isStatic是怎么判断是不是静态node的

function isStatic (node: ASTNode): boolean {
    // 如果是个表达式,那么肯定不是静态的
  if (node.type === 2) { // expression
    return false
  }
  // 如果只是个文本,那么是静态的
  if (node.type === 3) { // text
    return true
  }
  // 如果是pre, 也就是指定不需要表达式,则是静态的
  // 如果同时满足以下条件,则是静态的
  // 没有指令
  // 没有if 
  // 没有for
  // tag是不是内部tag slot,component
  // 是html原生的tag标签
  // 父node不是for的template
  // 是不是node所有的key都是静态的key
  return !!(node.pre || (
    !node.hasBindings && // no dynamic bindings
    !node.if && !node.for && // not v-if or v-for or v-else
    !isBuiltInTag(node.tag) && // not a built-in
    isPlatformReservedTag(node.tag) && // not a component
    !isDirectChildOfTemplateFor(node) &&
    Object.keys(node).every(isStaticKey)
  ))
}

然后markStaticRoots将会标记最终的。 markStaticRoots同样是递归整颗树标记staticRoot, staticRoot必须要满足

  1. 当前node是静态的
  2. 必须要有children
  3. children不能只有一个且是文本

否则的话,对它的优化成本将大于优化后带来的收益。

function markStaticRoots (node, isInFor) {
    if (node.type === 1) {
      if (node.static || node.once) {
        node.staticInFor = isInFor;
      }
      // For a node to qualify as a static root, it should have children that
      // are not just static text. Otherwise the cost of hoisting out will
      // outweigh the benefits and it's better off to just always render it fresh.
      // 主要是下面这段
      if (node.static && node.children.length && !(
        node.children.length === 1 &&
        node.children[0].type === 3
      )) {
        node.staticRoot = true;
        return
      } else {
        node.staticRoot = false;
      }
      // 递归子树
      if (node.children) {
        for (var i = 0, l = node.children.length; i < l; i++) {
          markStaticRoots(node.children[i], isInFor || !!node.for);
        }
      }
      // 循环所有ifConditions
      if (node.ifConditions) {
        for (var i$1 = 1, l$1 = node.ifConditions.length; i$1 < l$1; i$1++) {
          markStaticRoots(node.ifConditions[i$1].block, isInFor);
        }
      }
    }
  }

generate

function generate (
    ast,
    options
  ) {
    var state = new CodegenState(options);
    var code = ast ? genElement(ast, state) : '_c("div")';
    return {
      render: ("with(this){return " + code + "}"),
      staticRenderFns: state.staticRenderFns
    }
  }

简单来说generate会生成一个render函数,执行render函数就可以得到vnode虚拟节点,用作之后进行patch和更新的。

先观察一下这个render函数, 可以看到使用了with(this) 这个就相当于

with(this){
    console.log(a);
}
// 上面就相当于
console.log(this.a);

with 有个很大的问题就是性能,具体原因可以百度或者参考js高级程序设计60页。 但是vue为什么用这个了, 直接看下小右自己的回答吧。 如何看待Vue.js 2.0 的模板编译使用了with(this)的语法?

然后我们继续看genElement里面是怎么生成的:

function genElement (el, state) {
    if (el.parent) {
      el.pre = el.pre || el.parent.pre;
    }

    if (el.staticRoot && !el.staticProcessed) {
        // 静态根节点
      return genStatic(el, state)
    } else if (el.once && !el.onceProcessed) {
        // once
      return genOnce(el, state)
    } else if (el.for && !el.forProcessed) {
        // for
      return genFor(el, state)
    } else if (el.if && !el.ifProcessed) {
        // if
      return genIf(el, state)
    } else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
        // template
      return genChildren(el, state) || 'void 0'
    } else if (el.tag === 'slot') {
        // slot
      return genSlot(el, state)
    } else {
      // component or element
      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
    }
  }

可以看到,针对于不同的节点分别有不同的生成逻辑,具体生成的逻辑有兴趣可以自己看。 总结一下,其实还是递归遍历了所有的ast然后,分别根据各个node生成一个生成vnode的方法,然后把所有的拼接起来,感觉有点说不明白,大家可以看下这个地址。 template-explorer 这个可以在线生成最终的render函数, 更直观。

比如

<div id="app">{{ msg }}</div>
function render() {
  with(this) {
    return _c('div', {
      attrs: {
        "id": "app"
      }
    }, [_v(_s(msg))])
  }
}

再比如

<div :id="app"><div v-for="item in items">{{ item }}</div></div>
function render() {
  with(this) {
    return _c('div', {
      attrs: {
        "id": app
      }
    }, _l((items), function (item) {
      return _c('div', [_v(_s(item))])
    }), 0)
  }
}

具体里面的_l啊, _v之类的,可以对应到具体代码中看, 只要作用就是生成对应vnode

function installRenderHelpers (target) {
    target._o = markOnce;
    target._n = toNumber;
    target._s = toString;
    target._l = renderList;
    target._t = renderSlot;
    target._q = looseEqual;
    target._i = looseIndexOf;
    target._m = renderStatic;
    target._f = resolveFilter;
    target._k = checkKeyCodes;
    target._b = bindObjectProps;
    target._v = createTextVNode;
    target._e = createEmptyVNode;
    target._u = resolveScopedSlots;
    target._g = bindObjectListeners;
    target._d = bindDynamicKeys;
    target._p = prependModifier;
  }

至此,从模版生成render函数的流程就走完了,总结一下。

  1. parse ==> 主要就是将模版所有的信息转变成ast树,每个dom的参数变量等都会挂在树的节点上
  2. optimize ==> 优化阶段,主要是将静态根节点, 标记出来的根节点会直接跳过patch更新阶段,jieyueshijian
  3. generate ==> 生成阶段,循环遍历已经优化好的ast树,拼接成最终需要执行获取vnode的方法字符串