Vue编译之 optimize和 code generate源码解析

486 阅读8分钟

前奏

笔者的整理出vue编译思想篇后,又整理了编译的parse部分

今天准备的详细得讲解下vue在parse解析生成astElement后,编译过程中另外两个重要的部分optimizecode generate

正文

一、optimize

让我们先看下vue中关于这部分的注释:

/**
 * 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.
 */

大致意思就是不需要重新创建新的nodes,跳过patch的过程。

再次重申下vue的工作流程。

    1. parse 解析template,生成astElement tree。
    1. optimize 遍历 astElement, 给astElement 打上必要的staticRoot标签。
    1. code generate,将astElement转换成函数的形式,但是是以string的形式存在,等待执行。
    1. render函数,执行第三步得到的string,生成vnode 节点,也就是我们经常碎碎念念的虚拟dom了。
    1. patch,对比新旧虚拟dom,生成真实的dom。

笔者花了漫长的时间写完了第一个过程的解析部分,今天带来的源码解析是关于2和3部分的。

而optimize的优化的部分就是在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) // 模块二
}

1)我们先讲下功能函数isStaticKey:

首先我们得知道options.staticKeys其实是一个string:

"staticClass,staticStyle"

genStaticKeysCached的源码是:

const genStaticKeysCached = cached(genStaticKeys);

export function cached<F: Function> (fn: F): F {
  const cache = Object.create(null)
  return (function cachedFn (str: string) {
    const hit = cache[str]
    return hit || (cache[str] = fn(str))
  }: any)
}

function genStaticKeys (keys: string): Function {
  return makeMap(
    'type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap' +
    (keys ? ',' + keys : '')
  )
}

  • 1.genStaticKeysCached 就是 cached的 return 也就是 cachedFn, 所以
genStaticKeysCached(options.staticKeys || '') 

就是

cachedFn(options.staticKeys || ''),
  • 2.因为一开始cache并不存在,所以会执行
cache[options.staticKeys] = fn(options.staticKeys);

而fn函数是传递进来的函数 genStaticKeys: 也就是执行的其实是:

cache[options.staticKeys] = genStaticKeys(options.staticKeys);

在替换options.staticKeys 到 "staticClass,staticStyle",得到:

cache["staticClass,staticStyle"]= genStaticKeys("staticClass,staticStyle")

所以: 最终结果是 :

markUp('type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap,staticClass,staticStyle')

这个函数是干嘛的? 源码如下:

export function makeMap (
  str: string,
  expectsLowerCase?: boolean
): (key: string) => true | void {
  const map = Object.create(null)
  const list: Array<string> = str.split(',')
  for (let i = 0; i < list.length; i++) {
    map[list[i]] = true
  }
  return expectsLowerCase
    ? val => map[val.toLowerCase()]
    : val => map[val]
}

很容易发现,其实:

字符串'type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap,staticClass,staticStyle',会变成对象:

const map = {
    type:true,
    tag:true,
    attrsList:true,
    attrsMap:true,
    plain:true,
    parent:true,
    ...,
    staticClass:true,
    staticStyle:true
}

最后返回的函数其实是:

isStaticKey = (val)=> map[val];

恍然大悟是不是,就是判断到底是不是静态key,所谓的静态key就是上诉的这些属性。 这个函数下面会使用到的。

2)isPlatformReservedTag功能函数详解:

这个函数很简单,其实就是判断是否为平台的tag,web平台也就是普通的标签和svg标签。

3)markStatic函数。

这个函数其实是辅助函数,就是标志ast树的每个节点是否为静态的节点,具体的判断方式,需要拿源码的进行分析。

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
    }
    for (let i = 0, l = node.children.length; i < l; i++) { // 模块二
      const child = node.children[i]
      markStatic(child)
      if (!child.static) { //
        node.static = false
      }
    }
    if (node.ifConditions) { // 模块三
      for (let i = 1, l = node.ifConditions.length; i < l; i++) {
        const block = node.ifConditions[i].block
        markStatic(block)
        if (!block.static) {
          node.static = false
        }
      }
    }
  }
}

先从宏观的角度来思考符合static的标准。

    1. node.type为2的时候一定不是静态节点,因为这个标志代表的是表达式。具体细节请参考笔者的parse篇
    1. node.type 为3的时候一定是静态节点,因为这是文本。
    1. node.type为1的时候这么考量的呢?? 很复杂

需要符合下面两个要求:

  • 1.符合一系列的要求,这块需要查看isStatic的源码部分。
  • 2.子节点必须都是静态节点,或者if的许多块级也不能存在动态节点。

两者缺一不可,我们一一来分析下:

先来看第一点,isStatic源码如下:

function isStatic (node: ASTNode): boolean {
  if (node.type === 2) { // expression
    return false
  }
  if (node.type === 3) { // text
    return true
  }
  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)
  ))
}

第一点中所谓的一些列的要求,其实就是这个函数的return部分。

  1. 当是node.pre,的时候就是静态节点。

  2. 或同时满足以下所有条件。

    • 1). 没有动态bind如: v-focus :focus @focus .focus
    • 2)没有if,没有for。
    • 3)非built-in 标签,也就是不是 slot或着component标签。
    • 4)必须是平台标签。
    • 5)不是template for 中的一个节点的直属孩子。
    • 6)所有属性都是静态属性,来自于前面分析的isStaticKey函数的判断。

再看第二点

也就是说当上面第一点当所有要求都已经达到了,依然只是基本条件。

还有要求就是所有的孩子节点都要满足上述当条件,才能算得上是静态节点

4)markStaticRoots函数详解

function markStaticRoots (node: ASTNode, isInFor: boolean) {
  if (node.type === 1) {
    if (node.static || node.once) {
      node.staticInFor = isInFor // 添加 staticInFor的标志。后面的codegen会用到
    }

    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 (let i = 0, l = node.children.length; i < l; i++) {
        markStaticRoots(node.children[i], isInFor || !!node.for)
      }
    }
    if (node.ifConditions) {
      for (let i = 1, l = node.ifConditions.length; i < l; i++) {
        markStaticRoots(node.ifConditions[i].block, isInFor)
      }
    }
  }
}

markStaticRoots函数主要给所有符合要求的节点打上两个标志:

staticInForstaticRoot,而这个标志才是真正能用于下一个大阶段code generate。

staticInFor非常简单,只要节点的祖先节点处于for当中,且自身有static或once属性,就会被标记。

再看看staticRoot,它的条件被分为两个部分,

node.static && node.children.length // 条件一
 && !( // 条件二
      node.children.length === 1 &&
      node.children[0].type === 3
)

条件一的意思是满足static且有孩子节点。

条件二的意思是。把条件一当中的只有一个孩子,且是动态节点的情况去掉。 举个例子:

<div>somexxx</div>

这种情况下div是不能算是staticRoot。

相信会有人心中会有疑问,为啥?

这点我们后面会在code generate中给出答案。

总结

1.父节点是static,孩子一定是static,,子节点未必是staticRoot,父节点一定是staticRoot。

2.真正起到作用的是staticInFor和staticRoot属性。


二、code generate。

再次强调,generate生成的是字符串,短函数类的字符串,为后面的render的执行做准备。本质是对象形式转换成短函数形式。

解析前的准备

本质上,我们会从一个astElement Tree

变成以函数为单位的树节点:

_c(
    'div',
    {
        key:'xxx',
        ref:'xx',
        pre:'xxx',
        domPro:xxx,
        ....
    },
    [ // chidren
        _v(_s('ding')),
        _c('p',{model:'isshow',}, [ ...xxx ])
    ]
)

那么一共有哪些短函数呢?

关于短函数,其实是在renderMixin中定义的,让我们看看。

export function installRenderHelpers (target: any) {
  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
}

我们需要明白 执行玩短函数得到短是另一种树,vnode树。 所以这些短函数要么是创建一些vnode节点,要么是为vnode节点创建一些属性。

所以我们短函数格式主要是

function ('tagName',{ id:'xxx' }, [ ... ]) {}

第一个参数是tag名称,第二个是属性,第三个参数则是子函数,也就是子节点。

接下来让我们看看主函数把。

1)主流程分析。

主流程是generate函数开始的。

export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {
  const state = new CodegenState(options) // 全程记录当前编译状态的参数
  const code = ast ? genElement(ast, state) : '_c("div")' // 生成我们需要的短函数字符串
  return { // 输出。
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}

先看下我们return的内容吧

return { // 输出。
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
 }

可以看到一共两个属性renderstaticRenderFns

简单先说下,optimize部分标记的staticRoot和staticInFor不会放在code当中,而是放在staticRenderFns。 具体的玩法,后面会有详细的讲解。

函数的话,主要只有两个过程,上面的代码我已经将两个模块分别给出了注释。

我们先来看看第一个块的具体情况:

const state = new CodegenState(options) // 全程记录当前编译状态的参数

其实上面的注释并不准确,state还有很多功能函数,CodegenState本身是个class,具体如下:

export class CodegenState {
  options: CompilerOptions;
  warn: Function;
  transforms: Array<TransformFunction>;
  dataGenFns: Array<DataGenFunction>;
  directives: { [key: string]: DirectiveFunction };
  maybeComponent: (el: ASTElement) => boolean;
  onceId: number;
  staticRenderFns: Array<string>;
  pre: boolean;

  constructor (options: CompilerOptions) {
    this.options = options // 平台传递的options,主要是一些工具函数
    this.warn = options.warn || baseWarn
    this.transforms = pluckModuleFunction(options.modules, 'transformCode') // 笔者没弄懂的地方
    this.dataGenFns = pluckModuleFunction(options.modules, 'genData') // 处理style 和 class的部分
    this.directives = extend(extend({}, baseDirectives), options.directives) // 重点部分,指令处理
    const isReservedTag = options.isReservedTag || no // 判断是否为平台 的标签
    this.maybeComponent = (el: ASTElement) => !!el.component || !isReservedTag(el.tag) // 初次判断是否为组件的函数
    this.onceId = 0 // 静态节点的记录,后面会有详细解说
    this.staticRenderFns = [] // 所有静态生成函数
    this.pre = false // 是否在pre当中。。
  }
}

需要说明的是directive其实只是生成短函数,真是短处理都是在render函数和patch部分完成的。如果对vue源码的周期还不熟悉的同学,请翻阅笔者的parse思想篇。

再看看第二模块的:

const code = ast ? genElement(ast, state) : '_c("div")'

可以看到,ast的兜底是短函数_c("div"),否则进入核心函数genElement。

genElement是codegen流程最重要的功能函数,处理短函数的绝大部分情况。

看下源码:

export function genElement (el: ASTElement, state: CodegenState): string {
  if (el.parent) {
    el.pre = el.pre || el.parent.pre
  }

  if (el.staticRoot && !el.staticProcessed) { // 这里使用了 staicRoot optimize 里面做的标记
    return genStatic(el, state)
  } else if (el.once && !el.onceProcessed) {
    return genOnce(el, state)
  } else if (el.for && !el.forProcessed) {
    return genFor(el, state);
  } else if (el.if && !el.ifProcessed) {
    return genIf(el, state)
  } else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
    let a =  genChildren(el, state) || 'void 0'
    return a
  } else if (el.tag === 'slot') {
    return genSlot(el, state)
  } else {
    // component or element
    let code
    if (el.component) {
      code = genComponent(el.component, el, state)
    } else {
      let data
      if (!el.plain || (el.pre && state.maybeComponent(el))) {
        data = genData(el, state) // 处理  标签所有属性
      }

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

ifelse的分支一共分为8种情况,让我们来一一分析下。

(1)genStatic

先来看看判断的条件

el.staticRoot && !el.staticProcessed

恍然大悟有没有,原来optimize部分的staticRoot是在这个地方处理的,之前我们有说过,staticRoot的节点,在patch的过程中,只做第一次渲染,后面的对比是完全忽略的。

我们需要考虑两个问题

  • 1.静态节点,是怎么完成一次渲染的?
  • 2.静态节点,是在哪里判断不用再次渲染的?

先看看静态节点怎么渲染的,先观看genStatic代码:

// hoist static sub-trees out
function genStatic (el: ASTElement, state: CodegenState): string {
  el.staticProcessed = true
  // Some elements (templates) need to behave differently inside of a v-pre
  // node.  All pre nodes are static roots, so we can use this as a location to
  // wrap a state change and reset it upon exiting the pre node.
  const originalPreState = state.pre
  if (el.pre) {
    state.pre = el.pre
  }
  state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`) //保存静态渲染的函数
  state.pre = originalPreState
  return `_m(${ // 生成短函数
    state.staticRenderFns.length - 1
  }${
    el.staticInFor ? ',true' : ''
  })`
}

state.staticRenderFns以数组的形式保存静态渲染函数,短函数真正执行的守候,根据传递的index,去数组拿需要渲染的静态函数。

看下_m短函数的原函数是renderStatic:

export function renderStatic (
  index: number,
  isInFor: boolean
): VNode | Array<VNode> {
  const cached = this._staticTrees || (this._staticTrees = [])
  let tree = cached[index]
  // if has already-rendered static tree and not inside v-for,
  // we can reuse the same tree.
  if (tree && !isInFor) {
    return tree
  }
  // otherwise, render a fresh tree.
  tree = cached[index] = this.$options.staticRenderFns[index].call(
    this._renderProxy,
    null,
    this // for render fns generated for functional component templates
  ) // 直接生成vnode
  markStatic(tree, `__static__${index}`, false) // 打上标记
  return tree
}

renderStatic的主要做了两件事,那就是生成vnode和打上static标记。所谓的标记就是:

function markStaticNode (node, key, isOnce) {
  node.isStatic = true
  node.key = key
  node.isOnce = isOnce
}

当然这些参数在patch环节会发挥相应的作用,这里不做赘述,笔者后续的源码分析模块会有patch的源码解析。

2)

// v-once  本质就是static
function genOnce (el: ASTElement, state: CodegenState): string {
  el.onceProcessed = true
  if (el.if && !el.ifProcessed) {
    return genIf(el, state)
  } else if (el.staticInFor) {
    let key = ''
    let parent = el.parent
    while (parent) { // 先找到key
      if (parent.for) {
        key = parent.key
        break
      }
      parent = parent.parent
    }
    if (!key) {
      process.env.NODE_ENV !== 'production' && state.warn(
        `v-once can only be used inside v-for that is keyed. `,
        el.rawAttrsMap['v-once']
      )
      return genElement(el, state)
    }
    return `_o(${genElement(el, state)},${state.onceId++},${key})`
  } else {
    return genStatic(el, state)
  }
}

genOnce情况主要分为3种:

  • 1.v-if标签
  • 2.staticInFor (再次遇到optimize阶段打的标记,这里不做太多赘述)
  • 3.genStatic。

所以genOnce的基础就是genStatic,但是当持有v-if的标签的时候,就必须走genIf,这是因为if的形式是将节点放在v-if的标签的ifConditions数组当中的,根据具体的条件进行渲染目标模块,并不具备一次渲染的情况

至于staticInFor,情况则比较复杂了。

我们必须知道Vue的数组 后续补充。。