本文咱們一塊兒經過學習Vue模板編譯原理(二)-AST生成Render字符串來分析Vue源碼。預計接下來會圍繞Vue源碼來整理一些文章,以下。javascript
這些文章統一放在個人git倉庫:https://github.com/yzsunlei/javascript-series-code-analyzing。以爲有用記得star收藏。html
模板編譯是Vue中比較核心的一部分。關於 Vue 編譯原理這塊的總體邏輯主要分三個部分,也能夠說是分三步,先後關係以下:vue
第一步:將模板字符串轉換成element ASTs(解析器)java
第二步:對 AST 進行靜態節點標記,主要用來作虛擬DOM的渲染優化(優化器)node
第三步:使用element ASTs生成render函數代碼字符串(代碼生成器)git
對應的Vue源碼以下,源碼位置在src/compiler/index.js
github
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函數代碼字符串,對應的源碼實現咱們一般稱之爲代碼生成器。算法
在分析代碼生成器的原理前,咱們先舉例看下代碼生成器的具體做用。express
<div> <p>{{name}}</p> </div>
在上節"Template生成AST"中,咱們已經說了經過解析器會把上面模板解析成抽象語法樹(AST),解析結果以下:segmentfault
{ 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)" }] } ] }
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-if
、 v-for
、 v-slot
等,每種指令寫法都分離出一個函數來單獨處理,這樣代碼閱讀起來也清晰明瞭。下面,咱們重點來看看 genFor
和 genData
的具體處理邏輯。
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 函數是用來處理節點屬性的,源碼位置在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的代碼邏輯進行了說明。