Vue版本:2.5.17-beta.0
在《Vue源码笔记 — 数据驱动--Vue实例挂载》文章中的结尾记录过:updateComponent
函数主要做了两件事,第一个就是通过 vm._render()
生成 vnode
对象,第二个就是通过 vm.update(vm._render(), hydrating)
挂载到最终dom上。
这次记录 vm._render()
的大体实现过程。首先 _render
的原型方法在 src/core/instance/render.js
文件中,源码如下:
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
// reset _rendered flag on slots for duplicate slot check
if (process.env.NODE_ENV !== 'production') {
for (const key in vm.$slots) {
// $flow-disable-line
vm.$slots[key]._rendered = false
}
}
if (_parentVnode) {
vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject
}
// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
// 占位符vnode
vm.$vnode = _parentVnode
// render self
let vnode
try {
// vm._renderProxy 在生产环境下为 vm; 开发环境下的设置 在 _init中
// render.call是 vm.$createElement返回值 为一个vnode对象
// 生成渲染vnode
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
handleError(e, vm, `render`)
// return error render result,
// or previous vnode to prevent render error causing blank component
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
if (vm.$options.renderError) {
try {
vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
} catch (e) {
handleError(e, vm, `renderError`)
vnode = vm._vnode
}
} else {
vnode = vm._vnode
}
} else {
vnode = vm._vnode
}
}
// return empty vnode in case the render function errored out
// 如果有多个根节点(vnode) 报警告
if (!(vnode instanceof VNode)) {
if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
warn(
'Multiple root nodes returned from render function. Render function ' +
'should return a single root node.',
vm
)
}
vnode = createEmptyVNode()
}
// set parent
// 渲染vnode 的parent指向 占位符vnode
vnode.parent = _parentVnode
return vnode
}
上述源码中,大体逻辑是先获取 render
和 _parentVnode
(_parentVnode
其实是当前子节点的父级vnode数据,在普通节点渲染中不会有,只有组件渲染时会有,以后记录组件相关文章会提到。),然后调用 render
函数并传入 vm._renderProxy
和 vm.$createElement
,最后生成当前vnode数据并返回。
其中 vm._renderProxy
是在调用 _init
时就被初始化了,_init
部分源码如下:
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
// 子组件实例合并较快 因为不需要调用mergeOptions
// vm 为Sub构造器实例
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor), // 返回 Vue.options
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm) // 初始化生命周期
initEvents(vm) // 初始化事件中心
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm) // 对props、methods、data做proxy处理; 对data做响应式处理; 建立user Watcher和computed Watcher
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
上述源码中可看到,在合并配置完成后就定义了 vm._renderProxy
,并且在生产环境下设置 vm._renderProxy = vm
其实就是当前实例对象,但是在开发环境下调用 initProxy(vm)
设置 vm._renderProxy
,initProxy(vm)
方法在 src/core/instance/proxy.js
文件中,源码如下:
let initProxy
if (process.env.NODE_ENV !== 'production') {
const allowedGlobals = makeMap(
'Infinity,undefined,NaN,isFinite,isNaN,' +
'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' +
'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' +
'require' // for Webpack/Browserify
)
const warnNonPresent = (target, key) => {
warn(
`Property or method "${key}" is not defined on the instance but ` +
'referenced during render. Make sure that this property is reactive, ' +
'either in the data option, or for class-based components, by ' +
'initializing the property. ' +
'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.',
target
)
}
const hasProxy =
typeof Proxy !== 'undefined' && isNative(Proxy)
if (hasProxy) {
const isBuiltInModifier = makeMap('stop,prevent,self,ctrl,shift,alt,meta,exact')
config.keyCodes = new Proxy(config.keyCodes, {
set (target, key, value) {
if (isBuiltInModifier(key)) {
warn(`Avoid overwriting built-in modifier in config.keyCodes: .${key}`)
return false
} else {
target[key] = value
return true
}
}
})
}
const hasHandler = {
has (target, key) {
const has = key in target
const isAllowed = allowedGlobals(key) || (typeof key === 'string' && key.charAt(0) === '_')
if (!has && !isAllowed) {
warnNonPresent(target, key)
}
return has || !isAllowed
}
}
const getHandler = {
get (target, key) {
if (typeof key === 'string' && !(key in target)) {
warnNonPresent(target, key)
}
return target[key]
}
}
initProxy = function initProxy (vm) {
if (hasProxy) {
// determine which proxy handler to use
const options = vm.$options
const handlers = options.render && options.render._withStripped
? getHandler
: hasHandler
vm._renderProxy = new Proxy(vm, handlers)
} else {
vm._renderProxy = vm
}
}
}
在上述源码中最下面的代码里,设置了 initProxy
为一个函数,并当浏览器支持 Proxy
时调用 new Proxy(vm, handlers)
,在当前的逻辑中 handlers
为 hasHandler
可以看到,其实当使用 this.xxx
获取内容时,会先劫持判断是否符 hasHandler
中 has
和 isAllowed
的条件,不符合就报 warnNonPresent
中的警告内容:
warn(
`Property or method "${key}" is not defined on the instance but ` +
'referenced during render. Make sure that this property is reactive, ' +
'either in the data option, or for class-based components, by ' +
'initializing the property. ' +
'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.',
target
)
vm._renderProxy
已分析完毕,而 vm.$createElement
就是生成vnode数据的重要方法了,它是在 _init
时调用 initRender(vm)
时创建的,initRender
在 src/core/instance/render.js
文件中,源码如下:
export function initRender (vm: Component) {
vm._vnode = null // the root of the child tree
vm._staticTrees = null // v-once cached trees
const options = vm.$options
// 获取占位符vnode
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
// bind the createElement fn to this instance
// so that we get proper render context inside it.
// args order: tag, data, children, normalizationType, alwaysNormalize
// internal version is used by render functions compiled from templates
// 编译生成render函数 提供的方法
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// normalization is always applied for the public version, used in
// user-written render functions.
// 手写render函数 提供的方法
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
// $attrs & $listeners are exposed for easier HOC creation.
// they need to be reactive so that HOCs using them are always updated
const parentData = parentVnode && parentVnode.data
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
!isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
}, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
!isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
}, true)
} else {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}
}
上述源码中可看到,vm.$createElement
和 vm._c
都是调用 createElement
方法,只是最后一个参数不同,因为一个是通过编译生成 render
函数时调用的方法(vm._c
),一个是我们手写 render
函数时调用的方法(vm.$createElement
),而 createElement
方法以后会去记录。
至此,render
已记录完毕。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。