「这是我参与2022首次更文挑战的第38天,活动详情查看:2022首次更文挑战」。
一、前情回顾 & 背景
上一篇小作文我们给大家展示了根实例的最终得到的 render 函数主体
:是一个 _c()
的一个调用,他的第一个参数是标签名,第二个是行内属性,第三个是代表子元素的数组;
从这个 render 函数的主体
可以看出,他是一个递归调用的过程,最终完成顶层 div#app
的渲染工作;
获得 render 函数主体
之后,又经过 createFunction
得到最终的 render 函数
,接着就是一些列的返回结果并出栈,回到了 Vue.prototype.$mount
方法,将获得的 render、renderStaticFn
赋值到 this.$options
上,以备 Vue.prototype._render
调用进行挂载;
经历过上面的步骤我们已经获取到 render 函数
(渲染函数
)代码,其中包含了很多的以 _
开头的方法,这些方法我们在生成渲染函数的时候有提到,称他们为渲染函数的运行时帮助函数,包括 _l/_t/_s/_i/_c...
;
编译时将模板编译成 ast
,再由 ast
生成渲染函数,而渲染函数就是有多个运行时帮助函数组织起来的调用;所以可以这么说,Vue
是编译时+运行时辅助
的框架;本篇小作文的笔墨放在介绍这些帮助函数的功能和注册时机;
二、renderMixin
renderMixin
是在 Vue
的注册过程中被调用的一个方法,其作用是在 Vue
实例上添加运行时的渲染函数的辅助函数,即将 _t/_l/_i/_s
等属性添加到 Vue.prototype
上;
2.1 renderMixin
方法位置:src/core/instance/render.js -> renderMixin
方法参数:Vue
, Vue 构造函数
方法作用:
- 执行
installRenderHelper
方法,注册渲染函数的运行时帮助方法; - 向
Vue
的原型对象添加$nextTick
、_render
方法,其中_render
方法会在后面的创建渲染watcher
时被调用,我们前面得到的render
函数将会在Vue.prototype._render
中调用获取VNode
;
export function renderMixin (Vue: Class<Component>) {
// 在 Vue 实例上挂载一些运行时渲染函数的帮助函数
installRenderHelpers(Vue.prototype)
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
}
// 通过执行 render 函数生成 VNode,并加入处理异常逻辑
Vue.prototype._render = function (): VNode {
}
}
2.2 installRenderHelpers
方法位置:src/core/instance/render-helpers/index.js -> export function installRenderHelpers
方法参数:target
,Vue.prototype
对象;
方法作用:向 Vue
的原型扩展运行时帮助函数,这些函数有各自不同的能力,最终编译所得的渲染函数能力依靠这些帮助函数实现 Vue
的能力,比如插槽、条件渲染、列表渲染...
export function installRenderHelpers (target: any) {
// v-once 帮助函数,为 VNode 加上静态标记
target._o = markOnce
// 将值转换为数字
target._n = toNumber
// 将值转成字符串,
// 普通值 String(val) ,对象 JSON.stringify(val)
target._s = toString
// 列表渲染 v-for 指令的帮助函数
target._l = renderList
// 插槽的帮助函数,<slot /> 标签的渲染
target._t = renderSlot
// 判断两个值是否相等
target._q = looseEqual
// indexOf 方法
target._i = looseIndexOf
// 运行时负责生成 VNode 静态树的帮助函数:
// 1. 执行 staticRenderFns 数组中对应下标的渲染函数,生成静态 VNode 树并缓存
// 2. 为静态 VNode 树加静态标识
target._m = renderStatic
target._f = resolveFilter
target._k = checkKeyCodes
target._b = bindObjectProps
// 创建文本节点 VNode
target._v = createTextVNode
// 创建空节点 VNode
target._e = createEmptyVNode
target._u = resolveScopedSlots
target._g = bindObjectListeners
target._d = bindDynamicKeys
target._p = prependModifier
}
在前面的 render 函数
中,我们见过其中的很多方法:
_v
:createTextVNode
_i
:looseIndexOf
_m
:renderStatic
_t
:renderSlot
_s
:toString
_l
:renderList
接下来我们就会一一讨论这些方法,不知道你发现没发现,这里面居然没有 render 函数
中出场频率最高的 _c
方法,这个 _c
方法的注册是在另一个时机了,后面我们再讨论他;
三、_c & createTextVNode
方法位置:src/core/vdom/vnode.js -> createTextVNode
方法参数:
val
,字符串的字面量
方法作用:字符串创建 VNode
实例;
export function createTextVNode (val: string | number) {
return new VNode(undefined, undefined, undefined, String(val))
}
四、_i & looseIndexOf
方法位置:src/shared/util.js -> functions looseIndexOf
方法参数:
arr
,数组val
,某个值
方法作用:判断 val
在数组 arr
中首次出现的位置索引,相当于 Array.prototype.indexOf
,如果没有出现过,返回 -1
;
但是其比较原则是不相同的,如果是对象,则比较的是字面量,具体比较规则见 looseEqual
方法;
export function looseIndexOf (arr: Array<mixed>, val: mixed): number {
for (let i = 0; i < arr.length; i++) {
if (looseEqual(arr[i], val)) return i
}
return -1
}
五、_m & renderStatic
方法位置:src/core/instance/render-helpers/render-static.js -> function renderStatic
方法参数:
index
,静态渲染函数在staticRenderFns
数组的索引;isInFor
,是否被v-for
包裹;
方法作用:
- 调用
vm.$options.staticRenderFns
指定索引的render
函数,得到静态根节点对应的渲染树并将树缓存到cached
对象; - 判断缓存,如果命中
cached
中的缓存,则不再调用静态渲染函数,直接从缓存中返回; - 给前面生成的静态树添加静态标识:
isStatic: true
export function renderStatic (
index: number,
isInFor: boolean
): VNode | Array<VNode> {
// 缓存对象,静态节点二次渲染时从缓存中走缓存
const cached = this._staticTrees || (this._staticTrees = [])
let tree = cached[index]
// 如果当前静态树已经被渲染过一次,即命中缓存,且没有被 v-for 节点包裹,则直接返回缓存的 tree
// if has already-rendered static tree and not inside v-for,
// we can reuse the same tree.
if (tree && !isInFor) {
return tree
}
// 执行 staticRenderFns 数组中指定索引对应的 render 函数,
// 生成该静态树的 VNode,并缓存
tree = cached[index] = this.$options.staticRenderFns[index].call(
this._renderProxy,
null,
this // for render fns generated for functional component templates
)
// 为静态树的 VNode 加标记,即添加 { isStatic: true, key: `__static__${index}`, isOnce: false }
markStatic(tree, `__static__${index}`, false)
return tree
}
六、_t & renderSlot
方法位置:src/core/instance/render-helpers/render-slot.js -> function renderSlot
方法参数:
name
:slotName
,插槽名fallbackRender
: 兜底渲染函数,当不传入插槽时展示兜底内容;props
:slot
标签的props
bindObject
:slot-scope
绑定的对象
方法作用:处理普通 slot
和 slot-scope
的渲染工作;
在我们的例子中只有简答的不带作用域的插槽:
// <slot name="namedSlot"></slot>
"_t(\"namedSlot\")
export function renderSlot (
name: string,
fallbackRender: ?((() => Array<VNode>) | Array<VNode>),
props: ?Object,
bindObject: ?Object
): ?Array<VNode> {
// 优先从 scopedSlots 取值,scopedSlots 是作用域插槽
const scopedSlotFn = this.$scopedSlots[name]
let nodes
if (scopedSlotFn) {
// scoped slot
props = props || {}
if (bindObject) {
if (process.env.NODE_ENV !== 'production' && !isObject(bindObject)) {
warn('slot v-bind without argument expects an Object', this)
}
props = extend(extend({}, bindObject), props)
}
nodes =
scopedSlotFn(props) ||
(typeof fallbackRender === 'function' ? fallbackRender() : fallbackRender)
} else {
// 我们的例子的是这里
nodes =
this.$slots[name] ||
(typeof fallbackRender === 'function' ? fallbackRender() : fallbackRender)
}
const target = props && props.slot
if (target) {
// 组件有 slot 属性,说明部分内容需要被分发到插槽
// 创建 slot 是 targe 的 template 元素,nodes 是作为子元素
return this.$createElement('template', { slot: target }, nodes)
} else {
// 没有插槽内容,显示兜底内容
return nodes
}
}
七、_s & toString
方法位置:src/shared/util.js -> function toString
方法参数:val
,需要变成字符串的值
方法作用:将给定的值变成字符串形式:
- 基本类型
String(val)
- 复杂类型
JSON.stringify(val, null, 2)
export function toString (val: any): string {
return val == null
? ''
: Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
? JSON.stringify(val, null, 2)
: String(val)
}
八、_l & renderList
方法位置:src/core/instance/render-helpers/render-list.js -> function renderList
方法参数:
val
,可迭代对象;render
,渲染列表中的每个元素所需的渲染函数;
方法作用:遍历给定可迭代对象,这其中处理了数字、字符串的场景,为每一项调用渲染函数生成一个节点并放入结果列表。最后将得到的 VNode
结果列表返回,完成列表渲染;
我们的例子中有一个列表渲染:
// 处理 <span v-for="item in someArr" :key="index">{{item}}</span>
_l(
(someArr), // renderList 的 val 参数
function (item) { // renderList 的 render 参数
return _c(
'span',
{ key:index },
[
_v(_s(item))
]
)
}
),
以下是 renderList
代码:
export function renderList (
val: any,
render: (
val: any,
keyOrIndex: string | number,
index?: number
) => VNode
): ?Array<VNode> {
let ret: ?Array<VNode>, i, l, keys, key
if (Array.isArray(val) || typeof val === 'string') {
// 抹平 val 是数组或者字符串
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') {
// val 是数字,遍历 0 - (val - 1) 的所有数字
ret = new Array(val)
for (i = 0; i < val; i++) {
ret[i] = render(i + 1, i)
}
} else if (isObject(val)) {
// val 为一个对象
if (hasSymbol && val[Symbol.iterator]) {
// val 部署了迭代器接口
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 {
// val 是普通的未部署迭代器接口的对象
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: any)._isVList = true
// 返回 VNode 数组
return ret
}
九、initRender 和 _c
前面说了很多的渲染函数的运行时帮助函数,但是其中没有 _c
,这是因为 _c
的初始位置和之前的有所不同。
9.1 initRender 调用
initRender
是 Vue.prototype._init
方法中的一个初始化步骤,这个已经很久远了。。。
方法调用:
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// ...
// 解析组件中的插槽信息,得到 vm.$slot,
// 得到 vm.$createElement、vm._c 方法
initRender(vm)
// ....
if (vm.$options.el) {
// 调用 $mount 方法,进入到挂载阶段
vm.$mount(vm.$options.el)
}
}
}
9.2 initRender 方法
方法位置:src/core/instance/render.js -> export function initRender
方法参数:vm
,Vue
实例
方法作用:
- 解析组件中的插槽信息;
- 创建
vm._c
方法,它是createElement
的一个科里化方法,这样一来可以方便的为渲染函数绑定vm
实例; - 创建
vm.$createElement
方法,这个方法就是我们写render
选项时的h
方法;
export function initRender (vm: Component) {
vm._vnode = null
vm._staticTrees = null
const options = vm.$options
const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
const renderContext = parentVnode && parentVnode.context
// 解析插槽信息
vm.$slots = resolveSlots(options._renderChildren, renderContext)
vm.$scopedSlots = emptyObject
// 内部使用
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// 用户创建 render 时所需要的 h
// render(h) { return h(div, attrs, children)}
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
const parentData = parentVnode && parentVnode.data
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, '$attrs', parentData && parentData.attrs || em
} else {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}
}
十、总结
本篇小作文介绍了常见的渲染函数的帮助函数的功能:
_c
:创建元素的科里化方法_l
:处理列表渲染_s
:转成字符串_t
:渲染插槽slot
标签_i
:looseIndexOf
,判断某个值在数组中的位置,判断字面量_m
:渲染静态树,缓存、静态标记
还说了一下两种初始化的时机:
vm._c
的初始化是在执行Vue
的初始化逻辑的方法_init
方法中完成的创建,是vm
实例的私有方法,再次期间还初始了vm.$createElement
也就是大家熟知的h
;- 其余的帮助函数都是在
renderMixin
中,挂载到Vue.prototype
对象的公有方法;
因为篇幅的原因,也由于 vm._c
和前面的几个不太一样,所以很多的的细节并没有展开讲。vm._c
将作为重中之重单独开篇讨论!