开篇
在生成渲染函数字符串的时候用到了几个函数,比如_c _v _s _m _f _l _u 下面就分别从源码入手分析函数的运行和作用
_c
函数在src/core/instance/render.js下面定义,如图
可以看到内部是调用了createElement函数,那么继续分析这个函数
createElement继续调用了_createElement函数,接下来接着分析(由于 _createElement 做了很多的兼容处理,这里只保留核心代码)
function _createElement(context, tag, data, children, normalizationType) {
let vnode; // 新建一个vnode节点
if (typeof tag === "string") {
//如果标签是字符串
if (config.isReservedTag(tag)) {
//如果是平台保留标签
vnode = new VNode(
config.parsePlatformTagName(tag),
data,
children,
undefined,
undefined,
context
);
} else if (如果是组件) {
vnode = createComponent(Ctor, data, context, children, tag);
} else {
//不符合上面的情况
vnode = new VNode(tag, data, children, undefined, undefined, context);
}
} else {
//如果不是字符串那就是组件了
vnode = createComponent(tag, data, context, children);
}
return vnode;
}
function createComponent(Ctor,data,context,children,tag) (
const baseCtor = context.$options._base // 实际上是Vue的构造函数
//如果Ctor不是函数就通过Vue.extend去创建构造函数
if(isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
/*
这里是处理异步组件,什么是异步组件呢?举个例子
Vue.component('async-example',function(resolve,reject){
setTimeout(function(){
resolve({
template : `<div>i am async component</div>`
})
},1000)
})
<async-example></async-example>
*/
let asyncFactory
if (isUndef(Ctor.cid)) { //判断是否初始化了Vue 一个组件对应一个Vue实例
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
if (Ctor === undefined) {
return createAsyncPlaceholder( //创建一个占位符
asyncFactory,
data,
context,
children,
tag
)
}
}
data = data || {}
/*
解析配置项,判断父类的选项和基类的选项是否不一致,如果有不一致就合并
*/
resolveConstructorOptions(Ctor)
//处理v-model的情况
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}
//提取props,得到res[key] = val集合
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
/*
如果是函数式组件 那么就先生成一个渲染上下文,然后调用配置项里面的render方法,绑定生成的上下文,最后生成一个vnode并返回出去
*/
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children)
}
//获取事件监听器对象data.on 也就是在组件上设置的v-on指令
const listeners = data.on
//修饰符
data.on = data.nativeOn
//如果是抽象组件就保留slot
if (isTrue(Ctor.options.abstract)) {
const slot = data.slot
data = {}
if (slot) {
data.slot = slot
}
}
/*
在组件的data上面设置四个对象,分别是init,insert,prepatch,destroy属性
这四个属性分别负责组件的创建,插入,更新,销毁,会在组件的patch阶段调用
*/
installComponentHooks(data)
//创建一个新的vnode并且返回出去
const name = Ctor.options.name || tag
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
}
_v
函数在src/core/instance/render-helpers/index.js下面定义,如图
可以看到最后也是返回了个文本节点的vnode
_s
如上图 也是在src/core/instance/render-helpers/index.js下面定义的,那么看看toString方法是什么样的
可以看到toString方法主要是将值转换为字符串格式
_m
如上图 也是在src/core/instance/render-helpers/index.js下面定义的,那么看看renderStatic方法是什么样的
export function renderStatic (
index: number,
isInFor: boolean
): VNode | Array<VNode> {
//获取缓存,如果没有就设置
const cached = this._staticTrees || (this._staticTrees = [])
let tree = cached[index]
if (tree && !isInFor) {
return tree
}
//调用staticRenderFns数组里面的方法生成vnode 并保存到cached里面
tree = cached[index] = this.$options.staticRenderFns[index].call(
this._renderProxy,
null,
this // for render fns generated for functional component templates
)
markStatic(tree, `__static__${index}`, false)
return tree
}
在生成渲染函数字符串的时候,就已经将静态节点保存到staticRenderFns里面了,所以_m就是调用里面的方法生成vnode
_f
如上图 也是在src/core/instance/render-helpers/index.js下面定义的,那么看看resolveFilter方法是什么样的
export function resolveFilter (id: string): Function {
return resolveAsset(this.$options, 'filters', id, true) || identity
}
export function resolveAsset (
options: Object,
type: string,
id: string,
warnMissing?: boolean
): any {
const assets = options[type]
if (hasOwn(assets, id)) return assets[id]
const camelizedId = camelize(id)
if (hasOwn(assets, camelizedId)) return assets[camelizedId]
const PascalCaseId = capitalize(camelizedId)
if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId]
const res = assets[id] || assets[camelizedId] || assets[PascalCaseId]
return res
}
可以看到也是调用了用户自己定义的filter方法,将值返回出去
_l
如上图 也是在src/core/instance/render-helpers/index.js下面定义的,那么看看renderList方法是什么样的
/*
renderList里面接受两个参数,第一个是循环生成vnode的次数,第二个是render方法,这里放一个v-for的generate函数
_l((3), function (item, index) {
return _c('span', {
key: index
}, [_v(_s(item))])
}), 0)
不断调用render方法,将生成的vnode节点放到ret里面,然后返回ret
*/
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') {
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') {
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: any)._isVList = true
return ret
}
_u
如上图 也是在src/core/instance/render-helpers/index.js下面定义的,那么看看resolveScopedSlots方法是什么样的
/*
格式化generate生成的slot值,如下
_u([{
key: "header",
fn: function(user) {
return [_v(_s(user))]
}
}])
最终会生成
{
$stable: true,
header: function(user) {
return [_v(_s(user))]
}
}
*/
export function resolveScopedSlots (
fns: ScopedSlotsData, // see flow/vnode
res?: Object,
// the following are added in 2.6
hasDynamicKeys?: boolean,
contentHashKey?: number
): { [key: string]: Function, $stable: boolean } {
res = res || { $stable: !hasDynamicKeys }
for (let i = 0; i < fns.length; i++) {
const slot = fns[i]
if (Array.isArray(slot)) {
resolveScopedSlots(slot, res, hasDynamicKeys)
} else if (slot) {
// marker for reverse proxying v-slot without scope on this.$slots
if (slot.proxy) {
slot.fn.proxy = true
}
res[slot.key] = slot.fn
}
}
if (contentHashKey) {
(res: any).$key = contentHashKey
}
return res
}
总结
template是如何转换为vnode并渲染到页面上的呢?
1.先进行Vue的初始化,最后执行vm.$mount(vm.$options.el)方法
2.vm.$mount方法先获取template字符串,调用compileToFunctions方法,获取render函数(渲染函数字符串)和staticRenderFns(静态节点渲染函数数组),然后放到vm.$options上。最后调用mount方法,绑定当前this环境。
3.compileToFunctions先调用compile方法,将template字符串和options传进去,得到一个对象,里面包括ast和render和staticRenderFns属性,然后调用对象的staticRenderFns,创建静态节点vnode放进一个数组里,最后返回该对象和静态节点数组
4.compile调用baseCompile方法,将template字符串和finalOptions(包含警告函数,用于捕捉错误信息)传进去,然后得到ast,render,staticRenderFns属性,返回出去
5.baseCompile做了三件事情,第一件事情是将template字符串转换为ast对象,第二件事情是对ast对象标记是否是静态节点和静态根节点(需要满足三个条件,节点本身是静态节点 && 而且有子节点 && 子节点不全是文本节点),第三件事情是将ast对象转换为render渲染函数
6.mount方法先获取el对象,也就是root(根节点),之后调用mountComponent方法,将实例传进去
7.mountComponent方法先执行beforeMount方法(生命周期),之后定义一个updateComponent方法,如下
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
之后实例化一个watcher,将当前实例,updateComponent,noop,before(beforeUpdate 生命周期)传进去,之后会执行一遍updateComponent方法
8.vm.render方法会生成一次vnode,将值传给vm.update方法
9.vm._update方法会调用patch方法,如果旧的vnode不存在,那么就根据vnode重新创造节点,插入到dom中。如果旧的vnode存在,那么就通过diff算法,对比两者之间的差距,更新页面。