为AST生成render函数
以上我们了解到HTML模板经过解析处理,最终会变成一个AST节点树。随后编译器执行generate函数,为AST生成render函数的代码体。
function generate ( ast, options ) { var state = new CodegenState(options); // 根据AST节点生成代码 var code = ast ? genElement(ast, state) : '_c("div")'; return { render: ("with(this){return " + code + "}"), staticRenderFns: state.staticRenderFns } }
render函数的主要作用是创建虚拟节点vnode。而创建一个vnode,需要用到三个参数:元素标签,数据对象和子元素列表。genElement作为核心的代码生成方法,会按照顺序去生成这三部分的代码。
/** * AST元素的代码生成函数 * @param {ASTElement} el AST对象 * @param {CodegenState} state 代码生成状态 */ function genElement (el: ASTElement, state: CodegenState): string { // ... 省略其他代码 let data // 1. 首先生成节点本身的data代码,例如<div ref="myref" id="app"></div> 生成的data数据代码为"{ref:"myref",attrs:{"id":"app"}}" if (!el.plain || (el.pre && state.maybeComponent(el))) { data = genData(el, state) } // 2. 其次生成子元素创建代码 const children = el.inlineTemplate ? null : genChildren(el, state, true) // 3. 拼装成一个元素节点创建方法的字符串 形式如下:_c(tag,data,children) code = `_c('${el.tag}'${ data ? `,${data}` : '' // data }${ children ? `,${children}` : '' // children })` // ... 省略其他代码
data数据对象代码
仍以第一个HTML模板进行举例:
<div id="app">{{msg}}</div>
经解析后的AST对象如下:
{ attrsList: [{name: "id", value: "app", start: 5, end: 13}], attrsMap: {id: "app"}, children: [{type: 2, expression: "_s(msg)", tokens: [{@binding: "msg"}], text: "{{msg}}"}], end: 14, parent: undefined, plain: false, rawAttrsMap: {id: {name: "id", value: "app", start: 5, end: 13}}, start: 0, tag: "div", type: 1, static: false, staticRoot: false }
下面一段代码是data数据对象代码的生成逻辑:
function genData (el: ASTElement, state: CodegenState): string { let data = '{' // 先为指令生成代码,因为指令可能会修改元素的其他属性 const dirs = genDirectives(el, state) if (dirs) data += dirs + ',' // key if (el.key) { data += `key:${el.key},` } // ref 给定ref='myref',则生成 'ref: "myref"' if (el.ref) { data += `ref:${el.ref},` } // refInFor 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}",` } // 主要是class和style代码的生成 staticClass,staticStyle,classBinding,styleBinding for (let i = 0; i < state.dataGenFns.length; i++) { data += state.dataGenFns[i](el) } // attributes元素attribute属性的生成,如给定el.attrs=[{name: 'id', value:'app', dynamic: undefined, start:0,end:5}],则返回'attrs:{"id":"app"}' 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(/,$/, '') + '}' 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 }
上述代码中可以看到,vue针对不同的props、attrs、events、directives等分别生成各自的代码,在本例中的AST节点对象只存在attrs属性,因此其处理过程只会执行下面这一个语句:
if (el.attrs) { data += `attrs:${genProps(el.attrs)},` }
genProps函数如下:
function genProps (props: Array<ASTAttr>): string { let staticProps = `` let dynamicProps = `` for (let i = 0; i < props.length; i++) { const prop = props[i] const value = __WEEX__ ? generateValue(prop.value) : transformSpecialNewlines(prop.value) if (prop.dynamic) { dynamicProps += `${prop.name},${value},` } else { staticProps += `"${prop.name}":${value},` } } staticProps = `{${staticProps.slice(0, -1)}}` if (dynamicProps) { return `_d(${staticProps},[${dynamicProps.slice(0, -1)}])` } else { return staticProps } }
可以看到genProps
会将 el.attrs
,也就是[{name: "id", value: "app", start: 5, end: 13}]
处理成如下字符串:
"{attrs:{"id":"app"}}"
生成children代码
子元素的创建代码生成函数如下:
function genChildren ( el: ASTElement, state: CodegenState, checkSkip?: boolean, altGenElement?: Function, altGenNode?: Function ): string | void { const children = el.children // 无子元素不处理 if (children.length) { const el: any = children[0] // 如果子元素使用了v-for指令 if (children.length === 1 && el.for && el.tag !== 'template' && el.tag !== 'slot' ) { const normalizationType = checkSkip ? state.maybeComponent(el) ? `,1` : `,0` : `` return `${(altGenElement || genElement)(el, state)}${normalizationType}` } const normalizationType = checkSkip ? getNormalizationType(children, state.maybeComponent) : 0 // 遍历children数组,为每个子元素生成代码 const gen = altGenNode || genNode return `[${children.map(c => gen(c, state)).join(',')}]${ normalizationType ? `,${normalizationType}` : '' }` } } /** * 生成创建node节点的代码,对各种node类型进行了封装 * @param {*} node * @param {*} state */ function genNode (node: ASTNode, state: CodegenState): string { // 元素节点 if (node.type === 1) { return genElement(node, state) // 注释 } else if (node.type === 3 && node.isComment) { return genComment(node) // 文本节点 } else { return genText(node) } } /** * 生成创建文本节点字符串,返回内容形如:"_v("see me")" * @param {*} text 文本类型的AST节点 */ function genText (text: ASTText | ASTExpression): string { return `_v(${text.type === 2 ? text.expression // no need for () because already wrapped in _s() : transformSpecialNewlines(JSON.stringify(text.text)) })` }
对于本例而言,div的子元素是一个文本节点,执行genText会生成如下代码:
"[_v(_s(message))]"
render函数的完整代码
最后在genElement函数中,将各部分代码拼接到一起,组成一段完整的代码:
'with(this){return _c('div',{attrs:{"id":"app"}},[_v(_s(message))])}'
以上是render函数基本的创建步骤,接下来我们再看下针对于v-if、v-model和v-for(v-on在v-model的处理中也有涉及,不单独举例了)几个常见的指令,生成的代码有什么不同。
v-if的处理
再来看一下genElement函数中关于v-if的处理逻辑:
function genElement (el: ASTElement, state: CodegenState): string { //... 省略其他代码 // 节点存在v-if指令执行genIf if (el.if && !el.ifProcessed) { return genIf(el, state) } }
可以发现如果模板中有HTML标签使用了v-if,编译器会调用genIf,其主要代码如下:
function genIf ( el: any, state: CodegenState, altGen?: Function, altEmpty?: string ): string { el.ifProcessed = true return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty) } function genIfConditions ( conditions: ASTIfConditions, state: CodegenState, altGen?: Function, altEmpty?: string ): string { if (!conditions.length) { return altEmpty || '_e()' } // 将ifConditions数组中的条件语句转成三元运算表达式 const condition = conditions.shift() // 生成三元表达式形如a?1:2 if (condition.exp) { return `(${condition.exp})?${ genTernaryExp(condition.block) }:${ genIfConditions(conditions, state, altGen, altEmpty) }` } else { return `${genTernaryExp(condition.block)}` } // v-if 使用了 v-once (a)?_m(0):_m(1) function genTernaryExp (el) { return altGen ? altGen(el, state) : el.once ? genOnce(el, state) : genElement(el, state) } }
执行完genIf,会创建一个三元运算表达式。如果以如下模板为例:
div id="app"><p v-if="seen">you can see me</p><p v-else>you can not see me</p></div> 复制代码
那么使用v-if和v-else的p标签所生成的对应代码如下:
"(seen)?_c('p',[_v("you can see me")]):_c('p',[_v("you can not see me")])"
从生成的代码中,我们就能理解v-show和v-if的区别了。当使用v-if来控制dom元素的隐藏和显示的时候,每次都需要移除和重新创建的。
v-model的处理
如果我们在如下模板使用了v-model:
<div id="app"><input v-model="message" placeholder="edit me"></div>
input标签所对应的AST节点核心信息如下:
{ attrs: [{name: "placeholder", value: ""edit me"", dynamic: undefined, start: 39, end: 60}] attrsList: [{name: "v-model", value: "message", start: 21, end: 38}, {name: "placeholder", value: "edit me", start: 39, end: 60}], attrsMap: {v-model: "message",placeholder: "edit me"}, children: [], directives:[{name: "model", rawName: "v-model", value: "message", arg: null, isDynamicArg: false,modifiers: undefined,start:21,end:38}], events:{input: {value: "if($event.target.composing)return;message=$event.target.value", dynamic: undefined}}, props:[{name: "value", value: "(message)", dynamic: undefined}], parent: {type: 1, tag: "div", attrsList: Array(1), attrsMap: {…}, rawAttrsMap: {…}, …}, rawAttrsMap: {placeholder: {name: "placeholder", value: "edit me", start: 39, end: 60},v-model: {name: "v-model", value: "message", start: 21, end: 38}}, hasBindings: true, tag: "input", type: 1 }
input没有子元素,因此只需要生成data数据对象。生成的代码如下:
"{directives:[{name:"model",rawName:"v-model",value:(message),expression:"message"}],attrs:{"placeholder":"edit me"},domProps:{"value":(message)},on:{"input":function($event){if($event.target.composing)return;message=$event.target.value}}}"
从代码中可以看到,data数据对象中,新增了在domProps和on部分代码,其中指定了value和input事件处理函数。因此也就理解了v-model在本质上是把v-bind与v-on:input封装之后的语法糖。
经拼接后,完整的input生成的代码如下:
"_c('input',{directives:[{name:"model",rawName:"v-model",value:(message),expression:"message"}],attrs:{"placeholder":"edit me"},domProps:{"value":(message)},on:{"input":function($event){if($event.target.composing)return;message=$event.target.value}}})"
v-for的处理
如果如下模板中使用了v-for:
<div id="app"><p v-for="(item, index) in items" :key="index">{{item}}</p></div>
p标签对应的AST核心信息如下:
{ alias: "item", for: "items", iterator1: "index", forProcessed: true, key: "index", attrsList: [], attrsMap: {v-for: "(item, index) in items", :key: "index"}, children: [{type: 2, expression: "_s(item)", tokens: Array(1), text: "{{item}}", start: 61, …}], parent: {type: 1, tag: "div", attrsList: Array(1), attrsMap: {…}, rawAttrsMap: {…}, …}, rawAttrsMap: {:key: {name: ":key", value: "index", start: 48, end: 60},v-for: {name: "v-for", value: "(item, index) in items", start: 17, end: 47}}, tag: "p", type: 1 }
调用genFor函数:
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}` : '' // 非生产环境下,如果未指定:key,控制台会提示用户 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 // 拼接完整代码 return `${altHelper || '_l'}((${exp}),` + `function(${alias}${iterator1}${iterator2}){` + `return ${(altGen || genElement)(el, state)}` + '})' }
可以生成如下代码:
"_l((items),function(item,index){return _c('p',{key:index},[_v(_s(item))])}),0"
下面的是render函数中所用到的帮助方法的说明:
- Vue.prototype._s 转换为字符类型
- Vue.prototype._l 渲染列表
- Vue.prototype._v 创建文本类型的vnode
- Vue.prototype._c 创建vnode
总结
以上就是vue模板编译的总体过程:通过核心正则表达式,逐个将HTML标签解析成AST节点,最后根据AST节点,生成render函数的函数体。render函数负责生成vnode。