1. 程式人生 > >[Vue原始碼]一起來學Vue模板編譯原理(二)-AST生成Render字串

[Vue原始碼]一起來學Vue模板編譯原理(二)-AST生成Render字串

本文我們一起通過學習Vue模板編譯原理(二)-AST生成Render字串來分析Vue原始碼。預計接下來會圍繞Vue原始碼來整理一些文章,如下。

  • 一起來學Vue雙向繫結原理-資料劫持和釋出訂閱
  • 一起來學Vue模板編譯原理(一)-Template生成AST
  • 一起來學Vue模板編譯原理(二)-AST生成Render字串
  • 一起來學Vue虛擬DOM解析-Virtual Dom實現和Dom-diff演算法

這些文章統一放在我的git倉庫:https://github.com/yzsunlei/javascript-series-code-analyzing。覺得有用記得star收藏。

編譯過程

模板編譯是Vue中比較核心的一部分。關於 Vue 編譯原理這塊的整體邏輯主要分三個部分,也可以說是分三步,前後關係如下:

第一步:將模板字串轉換成element ASTs(解析器)

第二步:對 AST 進行靜態節點標記,主要用來做虛擬DOM的渲染優化(優化器)

第三步:使用element ASTs生成render函式程式碼字串(程式碼生成器)

對應的Vue原始碼如下,原始碼位置在src/compiler/index.js

export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  // 1.parse,模板字串 轉換成 抽象語法樹(AST)
  const ast = parse(template.trim(), options)
  // 2.optimize,對 AST 進行靜態節點標記
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  // 3.generate,抽象語法樹(AST) 生成 render函式程式碼字串
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

這篇文件主要講第三步使用element ASTs生成render函式程式碼字串,對應的原始碼實現我們通常稱之為程式碼生成器。

程式碼生成器執行過程

在分析程式碼生成器的原理前,我們先舉例看下程式碼生成器的具體作用。

<div>
  <p>{{name}}</p>
</div>

在上節"Template生成AST"中,我們已經說了通過解析器會把上面模板解析成抽象語法樹(AST),解析結果如下:

{
  tag: "div"
  type: 1,
  staticRoot: false,
  static: false,
  plain: true,
  parent: undefined,
  attrsList: [],
  attrsMap: {},
  children: [
    {
      tag: "p"
      type: 1,
      staticRoot: false,
      static: false,
      plain: true,
      parent: {tag: "div", ...},
      attrsList: [],
      attrsMap: {},
      children: [{
        type: 2,
        text: "{{name}}",
        static: false,
        expression: "_s(name)"
      }]
    }
  ]
}
  • 而在這一節我們會將AST轉換成可以直接執行的JavaScript字串,最終結果如下:
with(this) {
    return _c('div', [_c('p', [_v(_s(name))]), _v(" "), _m(0)])
}

現在看不懂不要緊。其實生成器的過程就是 generate 函式拿到解析好的 AST 物件,遞迴 AST 樹,為不同的 AST 節點建立不同的內部呼叫方法,然後組合成可執行的JavaScript字串,等待後面的呼叫。

內部呼叫方法

我們看上面示例生成的JavaScript字串,會發現裡面會有_v_c_s這樣的東西,這些其實就是Vue內部定義的一些呼叫方法。

其中 _c 函式定義在 src/core/instance/render.js 中。

  vm.$slots = resolveSlots(options._renderChildren, renderContext)
  vm.$scopedSlots = emptyObject
  // 定義的_c函式是用來建立元素的
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)

  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

而其他 _s_v 是定義在 src/core/instance/render-helpers/index.js 中:

export function installRenderHelpers (target: any) {
  target._o = markOnce
  target._n = toNumber
  target._s = toString
  target._l = renderList //生成列表VNode
  target._t = renderSlot //生成解析slot節點
  target._q = looseEqual
  target._i = looseIndexOf
  target._m = renderStatic //生成靜態元素
  target._f = resolveFilter
  target._k = checkKeyCodes
  target._b = bindObjectProps //繫結物件屬性
  target._v = createTextVNode //建立文字VNode
  target._e = createEmptyVNode //建立空節點VNode
  target._u = resolveScopedSlots
  target._g = bindObjectListeners
  target._d = bindDynamicKeys
  target._p = prependModifier
}

以上都是會在生成的JavaScript字串中用到的,像比較常用的 _c 執行 createElement 去建立 VNode_l 對應 renderList 渲染列表;_v 對應 createTextVNode 建立文字 VNode_e 對於 createEmptyVNode 建立空的 VNode

整體邏輯

程式碼生成器的入口函式是generate,具體定義如下,原始碼位置在src/compiler/codegen/index.js

export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {
  // 初始化一些options
  const state = new CodegenState(options)
  // 傳入ast和options進行生成
  const code = ast ? genElement(ast, state) : '_c("div")' //重點
  return {
    // 最外層包一個 with(this) 之後返回
    render: `with(this){return ${code}}`,
    // 這個陣列中的函式與 VDOM 中的 diff 演算法優化相關
    // 那些被標記為 staticRoot 節點的 VNode 就會單獨生成 staticRenderFns
    staticRenderFns: state.staticRenderFns
  }
}

生成器入口函式就比較簡單,先初始化一些配置options,然後傳入ast進行生成,沒有ast時直接生成一個空div,最後返回生成的JavaScript字串和靜態節點的渲染函式。這裡會把靜態節點區分出來,便於後面的Vnode diff,即Vue比較演算法更新DOM,現在先略過。

現在我們重點看看genElement函式,程式碼位置在src/compiler/codegen/index.js

export function genElement (el: ASTElement, state: CodegenState): string {
  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) {
    return genOnce(el, state)   // v-once處理生成函式
  } else if (el.for && !el.forProcessed) {
    return genFor(el, state)    // v-for處理生成函式
  } else if (el.if && !el.ifProcessed) {
    return genIf(el, state)     // v-if處理生成函式
  } else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
    return genChildren(el, state) || 'void 0' // 子節點處理生成函式
  } else if (el.tag === 'slot') {
    return genSlot(el, state)   // slot處理生成函式
  } else {
    // component or element
    let code
    if (el.component) {
      code = genComponent(el.component, el, state)  // component元件處理生成函式
    } else {
      let data
      if (!el.plain || (el.pre && state.maybeComponent(el))) {
        data = genData(el, state)  // attributes節點屬性處理生成函式
      }

      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
  }
}

你會發現genElement函式裡面有很多條件判斷。這是因為Vue裡面的指令寫法實在太多,像 v-ifv-forv-slot等,每種指令寫法都分離出一個函式來單獨處理,這樣程式碼閱讀起來也清晰明瞭。下面,我們重點來看看 genForgenData 的具體處理邏輯。

genFor

genFor 函式是用來處理 v-for 指令寫法的,原始碼位置在src/compiler/codegen/index.js

export function genFor (
  el: any,
  state: CodegenState,
  altGen?: Function,
  altHelper?: string
): string {
  const exp = el.for
  const alias = el.alias
  const iterator1 = el.iterator1 ? `,${el.iterator1}` : ''
  const iterator2 = el.iterator2 ? `,${el.iterator2}` : ''

  if (process.env.NODE_ENV !== 'production' &&
    state.maybeComponent(el) &&
    el.tag !== 'slot' &&
    el.tag !== 'template' &&
    !el.key
  ) {
    state.warn(
      `<${el.tag} v-for="${alias} in ${exp}">: component lists rendered with ` +
      `v-for should have explicit keys. ` +
      `See https://vuejs.org/guide/list.html#key for more info.`,
      el.rawAttrsMap['v-for'],
      true /* tip */
    )
  }

  el.forProcessed = true // avoid recursion
  return `${altHelper || '_l'}((${exp}),` +
    `function(${alias}${iterator1}${iterator2}){` +
      `return ${(altGen || genElement)(el, state)}` +
    '})'
}

genFor 的邏輯其實就是,首先 AST 元素節點中獲取了和 for 相關的一些屬性,然後返回了一個程式碼字串。

genData

genData 函式是用來處理節點屬性的,原始碼位置在src/compiler/codegen/index.js

export function genData (el: ASTElement, state: CodegenState): string {
  let data = '{'

  // directives first.
  // directives may mutate the el's other properties before they are generated.
  const dirs = genDirectives(el, state)
  if (dirs) data += dirs + ','

  // key
  if (el.key) {
    data += `key:${el.key},`
  }
  // ref
  if (el.ref) {
    data += `ref:${el.ref},`
  }
  if (el.refInFor) {
    data += `refInFor:true,`
  }
  // pre
  if (el.pre) {
    data += `pre:true,`
  }
  // record original tag name for components using "is" attribute
  if (el.component) {
    data += `tag:"${el.tag}",`
  }
  // module data generation functions
  for (let i = 0; i < state.dataGenFns.length; i++) {
    data += state.dataGenFns[i](el)
  }
  // attributes
  if (el.attrs) {
    data += `attrs:${genProps(el.attrs)},`
  }
  // DOM props
  if (el.props) {
    data += `domProps:${genProps(el.props)},`
  }
  // event handlers
  if (el.events) {
    data += `${genHandlers(el.events, false)},`
  }
  if (el.nativeEvents) {
    data += `${genHandlers(el.nativeEvents, true)},`
  }
  // slot target,only for non-scoped slots
  if (el.slotTarget && !el.slotScope) {
    data += `slot:${el.slotTarget},`
  }
  // scoped slots
  if (el.scopedSlots) {
    data += `${genScopedSlots(el, el.scopedSlots, state)},`
  }
  // component v-model
  if (el.model) {
    data += `model:{value:${
      el.model.value
    },callback:${
      el.model.callback
    },expression:${
      el.model.expression
    }},`
  }
  // inline-template
  if (el.inlineTemplate) {
    const inlineTemplate = genInlineTemplate(el, state)
    if (inlineTemplate) {
      data += `${inlineTemplate},`
    }
  }
  data = data.replace(/,$/, '') + '}'
  // v-bind dynamic argument wrap
  if (el.dynamicAttrs) {
    data = `_b(${data},"${el.tag}",${genProps(el.dynamicAttrs)})`
  }
  // v-bind data wrap
  if (el.wrapData) {
    data = el.wrapData(data)
  }
  // v-on data wrap
  if (el.wrapListeners) {
    data = el.wrapListeners(data)
  }
  return data
}

總結一下

程式碼生成是模板編譯的第三步,它完成了AST到Render的轉換,即將抽象語法樹AST轉換成可以直接執行的JavaScript字串。

其中genElement的程式碼比較多,因為需要分別處理的情況非常多,這裡只是對genFor和genData的程式碼邏輯進行了說明。

相關

  • https://ustbhuangyi.github.io/vue-analysis/compile/codegen.html#generate
  • https://github.com/liutao/vue2.0-source/blob/master/compile%E2%80%94%E2%80%94%E7%94%9F%E6%88%90render%E5%AD%97%E7%AC%A6%E4%B8%B2.md
  • https://github.com/lihongxun945/myblog/issues/29
  • https://segmentfault.com/a/1190000012922342