coverPiccoverPic

render 函数如何运行2:模板编译(v2)

前言

上文讲述了创建VNode的函数createElement,这里主要讲述模板编译中涉及的创建VNode流程。

render函数中,使用with(this)把作用域扩展到Vue实例的属性上,下面的代码可以直接使用其中的属性:

ts
  1. export function generate(/* ... */): CodegenResult {
  2. // ...
  3. return {
  4. render: `with(this){return ${code}}`,
  5. staticRenderFns: state.staticRenderFns
  6. }
  7. }

模板编译过程中,会生成很多 _xxx 函数,这些函数大都在Vue实例上面:

ts
  1. export function installRenderHelpers(target: any) {
  2. target._o = markOnce
  3. target._n = toNumber
  4. target._s = toString
  5. target._l = renderList
  6. target._t = renderSlot
  7. target._q = looseEqual
  8. target._i = looseIndexOf
  9. target._m = renderStatic
  10. target._f = resolveFilter
  11. target._k = checkKeyCodes
  12. target._b = bindObjectProps
  13. target._v = createTextVNode
  14. target._e = createEmptyVNode
  15. target._u = resolveScopedSlots
  16. target._g = bindObjectListeners
  17. target._d = bindDynamicKeys
  18. target._p = prependModifier
  19. }

这些函数调用render的时候自然可以直接使用。

创建节点

举个栗子:

html
  1. <div><span>test</span></div>

可以生成以下代码片段

ts
  1. _c('div',[_c('span',[_v("test")])])

_c生成标签节点,其实是一个不需要标准化的createElement

ts
  1. vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)

_v,生成一个文本VNode

ts
  1. export function createTextVNode(val: string | number) {
  2. return new VNode(undefined, undefined, undefined, String(val))
  3. }

绑定属性

举个栗子:

html
  1. <div :data="test" @click="testHandler"><span>{{test}}</span></div>

ts
  1. _c(
  2. 'div',
  3. {attrs:{"data":test},on:{"click":testHandler}},
  4. [_c('span',[_v(_s(test))])]
  5. )

testtestHandler是指在组件中的状态和回调函数。_s(test)把状态test转化为文本(其实就是toString):

ts
  1. target._s = toString

再来看看有状态的标签:

html
  1. <input :value="test">

此时有:

ts
  1. _c('input',{domProps:{"value":test}})

domProps就是记录标签的状态的

v-on、v-bind

再来看v-onv-bind绑定对象的情况:

html
  1. <div
  2. v-on="listenObj"
  3. v-bind="attrObj"
  4. :data="test"
  5. @click="testHandler"
  6. >
  7. <span>{{test}}</span>
  8. </div>

有:

ts
  1. _c(
  2. 'div',
  3. _g(
  4. _b(
  5. {attrs:{"data":test},on{"click":testHandler}},
  6. 'div',
  7. attrObj,
  8. false
  9. )
  10. ,listenObj
  11. ),
  12. [_c('span',[_v(_s(test))])]
  13. )

_b就是bindObjectProps

ts
  1. export function bindObjectProps(
  2. data: any,
  3. tag: string,
  4. value: any,
  5. asProp: boolean,
  6. isSync?: boolean
  7. ): VNodeData {
  8. if (value) {
  9. if (!isObject(value)) {
  10. } else {
  11. if (isArray(value)) {
  12. value = toObject(value)
  13. }
  14. let hash
  15. for (const key in value) {
  16. if (key === 'class' || key === 'style' || isReservedAttribute(key)) {
  17. hash = data
  18. } else {
  19. const type = data.attrs && data.attrs.type
  20. hash =
  21. asProp || config.mustUseProp(tag, type, key)
  22. ? data.domProps || (data.domProps = {})
  23. : data.attrs || (data.attrs = {})
  24. }
  25. const camelizedKey = camelize(key)
  26. const hyphenatedKey = hyphenate(key)
  27. if (!(camelizedKey in hash) && !(hyphenatedKey in hash)) {
  28. hash[key] = value[key]
  29. // 处理.sync
  30. if (isSync) {
  31. const on = data.on || (data.on = {})
  32. on[`update:${key}`] = function ($event) {
  33. value[key] = $event
  34. }
  35. }
  36. }
  37. }
  38. }
  39. }
  40. return data
  41. }

这个函数作用主要是把对象中的属性合并到前面的data中,Vue 中的属性(例如refkey)和classstyle被直接写到data上,表示标签状态的属性(例如input标签的value)记录在data.domProps,其他属性写到data.attrs中。如果有.sync修饰符,则会在data.on添加update:key的监听回调函数用于双向绑定。

接下来看绑定监听回调函数的_g,也就是bindObjectListeners

ts
  1. export function bindObjectListeners(data: any, value: any): VNodeData {
  2. if (value) {
  3. if (!isPlainObject(value)) {
  4. } else {
  5. const on = (data.on = data.on ? extend({}, data.on) : {})
  6. for (const key in value) {
  7. const existing = on[key]
  8. const ours = value[key]
  9. on[key] = existing ? [].concat(existing, ours) : ours
  10. }
  11. }
  12. }
  13. return data
  14. }

data.on上面添加回调函数就完了。

v-for

html
  1. <div>
  2. <div v-for="(item, index) in testArr">test</div>
  3. </div>

可以生成:

ts
  1. _c(
  2. 'div',
  3. _l((testArr),function(item,index){return _c('div',[_v("test")])}),
  4. 0
  5. )

_l,就是renderList

ts
  1. export function renderList(
  2. val: any,
  3. render: (val: any, keyOrIndex: string | number, index?: number) => VNode
  4. ): Array<VNode> | null {
  5. let ret: Array<VNode> | null = null,
  6. i,
  7. l,
  8. keys,
  9. key
  10. if (isArray(val) || typeof val === 'string') {
  11. // 遍历字符串
  12. ret = new Array(val.length)
  13. for (i = 0, l = val.length; i < l; i++) {
  14. ret[i] = render(val[i], i)
  15. }
  16. } else if (typeof val === 'number') {
  17. // 从 0 开始递增
  18. ret = new Array(val)
  19. for (i = 0; i < val; i++) {
  20. ret[i] = render(i + 1, i)
  21. }
  22. } else if (isObject(val)) {
  23. if (hasSymbol && val[Symbol.iterator]) {
  24. // 遍历数组、集合等可遍历对象
  25. ret = []
  26. const iterator: Iterator<any> = val[Symbol.iterator]()
  27. let result = iterator.next()
  28. while (!result.done) {
  29. ret.push(render(result.value, ret.length))
  30. result = iterator.next()
  31. }
  32. } else {
  33. // 普通对象
  34. keys = Object.keys(val)
  35. ret = new Array(keys.length)
  36. for (i = 0, l = keys.length; i < l; i++) {
  37. key = keys[i]
  38. ret[i] = render(val[key], key, i)
  39. }
  40. }
  41. }
  42. if (!isDef(ret)) {
  43. ret = []
  44. }
  45. ;(ret as any)._isVList = true
  46. return ret
  47. }

这个函数实现了模板中对字符串、数字、可迭代对象、普通对象的遍历,第二个入参的函数用于生成v-for的每一项。

v-if

html
  1. <div>
  2. <div v-if="testIf">testIf</div>
  3. <div v-else-if="testElif">testElif</div>
  4. <div v-else="testElse">testElse</div>
  5. </div>

ts
  1. _c(
  2. 'div',
  3. [(testIf)
  4. ? _c('div',[_v("testIf")])
  5. : (testElif)
  6. ? _c('div',[_v("testElif")])
  7. :_c('div',[_v("testElse")])
  8. ]
  9. )

生成了一个? :三元表达式来表示分支条件

静态节点

这里有一段包含静态节点的模板:

html
  1. <div :data="test">
  2. <div>
  3. <div>test1</div>
  4. <div>test2</div>
  5. </div>
  6. </div>

generate后可以得到这样子的代码片段:

ts
  1. _c('div',{attrs:{"data":test}},[_m(0)])

上面模板中这段是静态的:

html
  1. <div>
  2. <div>test1</div>
  3. <div>test2</div>
  4. </div>

被记录在this.$options.staticRenderFns中,类似于这样子:

ts
  1. [
  2. function () {
  3. with (this) {
  4. return _c(
  5. 'div',
  6. [_c('div',[_v("test1")]),_v(" "),_c('div',[_v("test2")])]
  7. )
  8. }
  9. }
  10. ]

_m,即renderStatic,把this.$options.staticRenderFns的内容渲染并缓存起来:

ts
  1. export function renderStatic(
  2. index: number,
  3. isInFor: boolean
  4. ): VNode | Array<VNode> {
  5. const cached = this._staticTrees || (this._staticTrees = [])
  6. let tree = cached[index]
  7. // if has already-rendered static tree and not inside v-for,
  8. // we can reuse the same tree.
  9. if (tree && !isInFor) {
  10. return tree
  11. }
  12. // otherwise, render a fresh tree.
  13. tree = cached[index] = this.$options.staticRenderFns[index].call(
  14. this._renderProxy,
  15. this._c,
  16. this // for render fns generated for functional component templates
  17. )
  18. // 给静态节点都标记上 isStatic, key
  19. markStatic(tree, `__static__${index}`, false)
  20. return tree
  21. }

cached中记录了VNode的渲染结果,缓存起来节省性能。

总结

本文介绍了模板编译中涉及的创建VNode流程,包括普通的创建标签、数据绑定,以及生成v-bindv-onv-ifv-for相关VNode和缓存静态节点的过程。

0 条评论未登录用户
Ctrl or + Enter 评论
© 2023-2025 LittleRangiferTarandus, All rights reserved.
🌸 Run