前言
上文讲述了创建VNode的函数createElement,这里主要讲述模板编译中涉及的创建VNode流程。
在render函数中,使用with(this)把作用域扩展到Vue实例的属性上,下面的代码可以直接使用其中的属性:
ts- export function generate(/* ... */): CodegenResult {
- // ...
- return {
- render: `with(this){return ${code}}`,
- staticRenderFns: state.staticRenderFns
- }
- }
模板编译过程中,会生成很多 _xxx 函数,这些函数大都在Vue实例上面:
ts- 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
- }
这些函数调用render的时候自然可以直接使用。
创建节点
举个栗子:
html- <div><span>test</span></div>
可以生成以下代码片段
ts- _c('div',[_c('span',[_v("test")])])
_c生成标签节点,其实是一个不需要标准化的createElement:
ts- vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
_v,生成一个文本VNode:
ts- export function createTextVNode(val: string | number) {
- return new VNode(undefined, undefined, undefined, String(val))
- }
绑定属性
举个栗子:
html- <div :data="test" @click="testHandler"><span>{{test}}</span></div>
有
ts- _c(
- 'div',
- {attrs:{"data":test},on:{"click":testHandler}},
- [_c('span',[_v(_s(test))])]
- )
test和testHandler是指在组件中的状态和回调函数。_s(test)把状态test转化为文本(其实就是toString):
ts- target._s = toString
再来看看有状态的标签:
html- <input :value="test">
此时有:
ts- _c('input',{domProps:{"value":test}})
domProps就是记录标签的状态的
v-on、v-bind
再来看v-on和v-bind绑定对象的情况:
html- <div
- v-on="listenObj"
- v-bind="attrObj"
- :data="test"
- @click="testHandler"
- >
- <span>{{test}}</span>
- </div>
有:
ts- _c(
- 'div',
- _g(
- _b(
- {attrs:{"data":test},on{"click":testHandler}},
- 'div',
- attrObj,
- false
- )
- ,listenObj
- ),
- [_c('span',[_v(_s(test))])]
- )
_b就是bindObjectProps:
ts- export function bindObjectProps(
- data: any,
- tag: string,
- value: any,
- asProp: boolean,
- isSync?: boolean
- ): VNodeData {
- if (value) {
- if (!isObject(value)) {
- } else {
- if (isArray(value)) {
- value = toObject(value)
- }
- let hash
- for (const key in value) {
- if (key === 'class' || key === 'style' || isReservedAttribute(key)) {
- hash = data
- } else {
- const type = data.attrs && data.attrs.type
- hash =
- asProp || config.mustUseProp(tag, type, key)
- ? data.domProps || (data.domProps = {})
- : data.attrs || (data.attrs = {})
- }
- const camelizedKey = camelize(key)
- const hyphenatedKey = hyphenate(key)
- if (!(camelizedKey in hash) && !(hyphenatedKey in hash)) {
- hash[key] = value[key]
- // 处理.sync
- if (isSync) {
- const on = data.on || (data.on = {})
- on[`update:${key}`] = function ($event) {
- value[key] = $event
- }
- }
- }
- }
- }
- }
- return data
- }
这个函数作用主要是把对象中的属性合并到前面的data中,Vue 中的属性(例如ref、key)和class、style被直接写到data上,表示标签状态的属性(例如input标签的value)记录在data.domProps,其他属性写到data.attrs中。如果有.sync修饰符,则会在data.on添加update:key的监听回调函数用于双向绑定。
接下来看绑定监听回调函数的_g,也就是bindObjectListeners:
ts- export function bindObjectListeners(data: any, value: any): VNodeData {
- if (value) {
- if (!isPlainObject(value)) {
- } else {
- const on = (data.on = data.on ? extend({}, data.on) : {})
- for (const key in value) {
- const existing = on[key]
- const ours = value[key]
- on[key] = existing ? [].concat(existing, ours) : ours
- }
- }
- }
- return data
- }
往data.on上面添加回调函数就完了。
v-for
html- <div>
- <div v-for="(item, index) in testArr">test</div>
- </div>
可以生成:
ts- _c(
- 'div',
- _l((testArr),function(item,index){return _c('div',[_v("test")])}),
- 0
- )
_l,就是renderList:
ts- export function renderList(
- val: any,
- render: (val: any, keyOrIndex: string | number, index?: number) => VNode
- ): Array<VNode> | null {
- let ret: Array<VNode> | null = null,
- i,
- l,
- keys,
- key
- if (isArray(val) || typeof val === 'string') {
- // 遍历字符串
- ret = new Array(val.length)
- for (i = 0, l = val.length; i < l; i++) {
- ret[i] = render(val[i], i)
- }
- } else if (typeof val === 'number') {
- // 从 0 开始递增
- ret = new Array(val)
- for (i = 0; i < val; i++) {
- ret[i] = render(i + 1, i)
- }
- } else if (isObject(val)) {
- if (hasSymbol && val[Symbol.iterator]) {
- // 遍历数组、集合等可遍历对象
- ret = []
- const iterator: Iterator<any> = val[Symbol.iterator]()
- let result = iterator.next()
- while (!result.done) {
- ret.push(render(result.value, ret.length))
- result = iterator.next()
- }
- } else {
- // 普通对象
- keys = Object.keys(val)
- ret = new Array(keys.length)
- for (i = 0, l = keys.length; i < l; i++) {
- key = keys[i]
- ret[i] = render(val[key], key, i)
- }
- }
- }
- if (!isDef(ret)) {
- ret = []
- }
- ;(ret as any)._isVList = true
- return ret
- }
这个函数实现了模板中对字符串、数字、可迭代对象、普通对象的遍历,第二个入参的函数用于生成v-for的每一项。
v-if
html- <div>
- <div v-if="testIf">testIf</div>
- <div v-else-if="testElif">testElif</div>
- <div v-else="testElse">testElse</div>
- </div>
有
ts- _c(
- 'div',
- [(testIf)
- ? _c('div',[_v("testIf")])
- : (testElif)
- ? _c('div',[_v("testElif")])
- :_c('div',[_v("testElse")])
- ]
- )
生成了一个? :三元表达式来表示分支条件
静态节点
这里有一段包含静态节点的模板:
html- <div :data="test">
- <div>
- <div>test1</div>
- <div>test2</div>
- </div>
- </div>
在generate后可以得到这样子的代码片段:
ts- _c('div',{attrs:{"data":test}},[_m(0)])
上面模板中这段是静态的:
html- <div>
- <div>test1</div>
- <div>test2</div>
- </div>
被记录在this.$options.staticRenderFns中,类似于这样子:
ts- [
- function () {
- with (this) {
- return _c(
- 'div',
- [_c('div',[_v("test1")]),_v(" "),_c('div',[_v("test2")])]
- )
- }
- }
- ]
_m,即renderStatic,把this.$options.staticRenderFns的内容渲染并缓存起来:
ts- 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,
- this._c,
- this // for render fns generated for functional component templates
- )
- // 给静态节点都标记上 isStatic, key
- markStatic(tree, `__static__${index}`, false)
- return tree
- }
cached中记录了VNode的渲染结果,缓存起来节省性能。
总结
本文介绍了模板编译中涉及的创建VNode流程,包括普通的创建标签、数据绑定,以及生成v-bind、v-on、v-if、v-for相关VNode和缓存静态节点的过程。
