这是我参与2022首次更文挑战的第39天,活动详情查看:2022首次更文挑战」
一、前情回顾 & 背景
这是更文活动的最后一天了,但是却不是这个专栏的最后一篇,从心里感谢本次更文活动,让我有勇气坚持下来。把一件巨大无比的工作拆成细碎的一小点,每天完成就一点点,最后就是一个大成就,这不是鸡汤,是心得。为了避免烂尾,我会坚持着把这个专栏写完,欢迎大家监督!加油吧摸鱼专家~
上一篇小作文介绍了常见的渲染函数的帮助函数的功能:
_c:创建元素的科里化方法_l:处理列表渲染_s:转成字符串_t:渲染插槽slot标签_i:looseIndexOf,判断某个值在数组中的位置,判断字面量_m:渲染静态树,缓存、静态标记
此外还讨论两种不同的初始化的场景:
vm._c的初始化是在执行Vue的初始化逻辑的方法_init方法中完成的创建,是vm实例的私有方法,再次期间还初始了vm.$createElement也就是大家熟知的h;- 其余的帮助函数都是在
renderMixin中,挂载到Vue.prototype对象的公有方法;
上一篇因为篇幅所限制,没有展开讲 _c,所以本篇小作文的重点就是讲透 _c 方法;
二、vm._c 方法
和其他的渲染函数帮助函数如 _l/_s... 等不同,vm._c 是 Vue 实例的私有方法,这个方法的初始化是伴随着 Vue 的实例初始化完成的,其顺序为:
new Vue()
-> this._init()
-> initRender
-> vm._c = (a, b, c, d) => createElement(vm , a, b, c, d, false) // vm._c 代码
2.1 vm._c 调用示例
- 以下问渲染
<div class="static-div">静态节点</div>元素对应的渲染函数
_c( 'div',{ staticClass: "static-div" }, [_v("静态节点")] )
方法位置:src/core/instance/render.js -> function initRender -> vm._c
方法参数:(你会发现大佬也会用 abcd 做参数😂😂)
a,tag,标签名b,data,是渲染标签所需要用到数据,例如上面的staticClassc,children,子节点数组d,normalizationType,节点规范化类型
方法作用:vm._c 作用是个科里化的函数,其目的在于自动绑定 vm 实例;
2.2 createElement 方法
方法位置:src/core/vdom/create-element.js -> function createElement
方法参数:
context:Vue实例tag:标签名data:渲染标签对应的数据,如上面的staticClassnormalizationType:节点的规范化类型alwaysNormalize: 是否一直normalize
方法作用:它是 _createElement 的包装函数,生成组件或者普通元素的 VNode;
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
// context 就是 vm
// 执行 _createElement 方法创建组件 VNode
return _createElement(context, tag, data, children, normalizationType)
}
2.3 _createElement
方法位置:src/core/vdom/create-element.js -> function _createElement
方法参数:
context:Vue实例tag:标签名data:渲染标签对应的数据,如上面的staticClassnormalizationType:节点的规范化类型alwaysNormaliza: 是否一直normalize
方法作用:根据 data、和 tag 处理不同场景下创建 VNode,具体工作如下:
- 如果
data是个响应式的数据,返回空节点 - 处理动态组件,将
tag参数改成is属性绑定的真实标签名; - 如果此时
tag为假值,返回空节点; - 检查组件上的
key属性只能为数字或者字符串; - 处理只有一个子节点的情况,将其当做插槽处理并情况子节点列表;
- 开始处理普通的元素、组件的
VNode生成逻辑;
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
if (isDef(data) && isDef((data: any).__ob__)) {
// 属性不能是一个响应式对象
process.env.NODE_ENV !== 'production' && warn(
`Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
'Always create fresh vnode data objects in each render!',
context
)
// 如果属性是一个响应式对象,则返回一个空节点的 VNode
return createEmptyVNode()
}
// object syntax in v-bind
if (isDef(data) && isDef(data.is)) {
// 这里是处理动态组件 component,data.is 就是 <component is="someComp" /> 的 is 属性
tag = data.is
}
if (!tag) {
// 动态组件的 is 属性是一个假值时 tag 为 false,则返回一个空节点 VNode
return createEmptyVNode()
}
// 检测唯一键 key,只能是数字或者字符串
if (process.env.NODE_ENV !== 'production' &&
isDef(data) && isDef(data.key) && !isPrimitive(data.key)
) {
if (!__WEEX__ || !('@binding' in data.key)) {
warn(
'Avoid using non-primitive value as key, ' +
'use string/number value instead.',
context
)
}
}
// 子节点数组中只有一个函数时,将他当做默认插槽处理并清空子节点列表
if (Array.isArray(children) &&
typeof children[0] === 'function'
) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
// 将子元素进行标准化处理
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
// 从这里开始才是重点,前面都不需要关注,以下是需要创建 VNode 的过程
let vnode, ns
if (typeof tag === 'string') {
// 标签是字符串时,该标签有三种可能:
// 1. 平台保留标签
// 2. 自定义组件
// 3. 不知名标签
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// tag 原生 HTML 标签
// platform built-in elements
if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn) && data.tag !== 'component') {
// v-on 的 native 只在组件上生效
// ... 抛出警告
}
// 实例化一个 VNode
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// resolveAssets 是个值得一看的方法
// 普通组件,例如我们的例子中的 <some-com /> 的渲染就会走这里
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// 未知标签,也生成 VNode,
// 运行时检查会检查,因为父元素进行规范化子节点时有可能获取一个命名空间
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// tag 不是字符串,可能是一个组件的配置对象或构造函数
vnode = createComponent(tag, data, context, children)
}
// 返回组件的 VNode
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) applyNS(vnode, ns)
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
}
}
2.4 resolveAssets
方法位置:src/core/util/options.js -> function resolveAssets
方法参数:
options,是Vue实例的$options;type,assets_type类型,指的是options上的components/directives/filters中的一个,Vue中导出了一个数组常量,名为ASSET_TYPES = ['component', 'directive', 'filter']这个东西在讲初始化的时候有提到过;id,要获取的资源名字,比如上面的例子中就是从vm.$options['components'][id = "some-com"],id就是组件名;
方法作用:该方法用于从 vm.$options 中的指定资源类型 type 中对应 id 的资源。说人话就是从 vm.$options 即选项获取注册的组件、指令、过滤器等资源;
export function resolveAsset (
options: Object,
type: string,
id: string,
warnMissing?: boolean
): any {
/* istanbul ignore if */
if (typeof id !== 'string') {
return
}
// 这个 options 就是合并过选项后的 vm.$options 从这个上面拿 vm.components[key] ,
// 对于组件来说,要么就是全局组件存在 Vue.components 中,
// 要么就是自父组件的 options 中注册的子组件,即 { components: { someComp: Obj }}
const assets = options[type]
// 首先查找本地注册的
if (hasOwn(assets, id)) return assets[id]
const camelizedId = camelize(id) // some-comp 变成 someComp 这种驼峰命名的
if (hasOwn(assets, camelizedId)) return assets[camelizedId] // 这里就命中了我们的组件 some-com
const PascalCaseId = capitalize(camelizedId)
if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId]
// fallback to prototype chain
const res = assets[id] || assets[camelizedId] || assets[PascalCaseId]
if (process.env.NODE_ENV !== 'production' && warnMissing && !res) {
warn(
'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
options
)
}
return res
}
下面是在渲染组件 some-com 是调用 resolveAssets 得到的结果:
这个东西就是我们注册的子组件:
const sub = {
template: `
<div style="color: red;background: #5cb85c;display: inline-block">
<slot name="namedSlot"></slot>
{{ someKey + foo }}</div>`,
props: {
someKey: {
type: Object,
default: () => 'hhhhhhh'
}
},
inject: ['foo']
}
debugger
new Vue({
el: '#app',
data: {},
components: {
someCom: sub // 子组件选项对象
}
})
当然,要从选项对象变成组件,还需要下面的 createComponent 方法
2.5 createComponent
createComponent 是处理自定义组件渲染的方法,该方法合并基类 Vue 的选项: Vue.options 和 组件选项 options, 然后基于合并后的选项以 Vue 为基类扩展出用于创建子组件的子类;
然后处理 v-model 指令,提取 propsData、installComponentHooks,最后得到组件的 VNode 并返回,这个方法的篇幅也很长,在下一篇深入讨论;
三、总结
本篇小作文的主题有以下几方面:
-
Vue中最复杂的渲染函数的运行时帮助函数vm._c,我们强调了这个方法与其他的不同,其他的帮助函数如_l/_t/_s...都是挂在Vue.prototype对象上的公有方法,而vm._c是vm实例私有的方法; -
此外还解释了
vm._c科里化createElement的原因——绑定vm实例; -
接着讨论了
_createElement、resolveAssets方法细节,处理自定义组件VNode的方法createElement将会再下一篇继续深入讨论;