上一章节讲的是$mount
的挂载,其中mountComponent
方法的逻辑也是非常清晰的,它会完成整个渲染工作,接下来我们要重点分析其中的细节,也就是最核心的 2 个方法:vm._render
和 vm._update
。
这一章节让我们来了解vm._render
的实现。
Vue 的 _render
方法是实例的一个私有方法,它用来把实例渲染成一个虚拟 Node。
/src/core/instance/render.js
export function renderMixin(Vue: Class<Component>) {
// install runtime convenience helpers
installRenderHelpers(Vue.prototype)
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
}
/*_render渲染函数,返回一个VNode节点*/
Vue.prototype._render = function (): VNode {
const vm: Component = this
// 从vm.$options中结构出啦的render函数,就是我们上一节讲到的(通过template编译成的或者手写的render函数)部分。
const { render, _parentVnode } = vm.$options
if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
)
}
// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
vm.$vnode = _parentVnode
// render self
// 定义了vnode,传入两个参数,vm.renderProxy和vm.$createElement
let vnode
try {
// There's no need to maintain a stack because all render fns are called
// separately from one another. Nested component's render fns are called
// when parent component is patched.
currentRenderingInstance = vm
// vm._renderProxy在开发环境就是Proxy对象,在生产环境就是this,返回的是一个Vnode节点。这个部分是最重要的
// render,call()传递的两个参数,一个renderProxy,一个vm.$createElement.
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' && 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
}
} finally {
currentRenderingInstance = null
}
// if the returned array contains only a single node, allow it
if (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0]
}
/*判断VNode节点是否创建成功,未成功则创建一个空节点*/
// return empty vnode in case the render function errored out
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 = _parentVnode
return vnode
}
}
- 我们需要着重看
$options
的 render 函数。 - 定义了 vnode,调用
render.call
方法传入两个参数,vm.renderProxy
和vm.$createElement
我们先来看看vm.$createElement
。我们之前在new Vue
的时候,Vue.prototype._init
方法执行时,会执行
initRender(vm)
。然后便运行到了下面的代码。
/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
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
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.
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._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
vm.$createElement
和vm._c
的函数定义是差不多的,唯一区别是最后一个参数不同。
两个方法使用是不一样的,vm._c
是给模板编译生成的 render 来使用的。
而vm.$createElement
方法通常是给我们手动写 render 使用的。
例如像这样:
render(createElement){
return createElement('div',{
attrs:{
id:'app'
}
},this.message)
}
接下来我们看看vm.renderProxy
。这个也是在new Vue
的时候,Vue.prototype._init
方法执行时,会执行
initProxy(vm)
,生产环境就直接执行vm._renderProxy = vm
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
我们来看看initProxy
到底操作了什么
/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,BigInt,' +
'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 warnReservedPrefix = (target, key) => {
warn(
`Property "${key}" must be accessed with "$data.${key}" because ` +
'properties starting with "$" or "_" are not proxied in the Vue instance to ' +
'prevent conflicts with Vue internals. ' +
'See: https://vuejs.org/v2/api/#data',
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) === '_' &&
!(key in target.$data))
if (!has && !isAllowed) {
if (key in target.$data) warnReservedPrefix(target, key)
else warnNonPresent(target, key)
}
return has || !isAllowed
},
}
const getHandler = {
get(target, key) {
if (typeof key === 'string' && !(key in target)) {
if (key in target.$data) warnReservedPrefix(target, key)
else 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
}
}
}
hasProxy
只是判断当前浏览器支不支持 Proxy。如果支持Proxy
的情况下就会执行new Proxy(vm,handlers)
。
我们接下来分析下它的第二个参数,handlers.
const hasHandler = {
has(target, key) {
const has = key in target
const isAllowed =
allowedGlobals(key) ||
(typeof key === 'string' &&
key.charAt(0) === '_' &&
!(key in target.$data))
if (!has && !isAllowed) {
if (key in target.$data) warnReservedPrefix(target, key)
else warnNonPresent(target, key)
}
return has || !isAllowed
},
}
const getHandler = {
get(target, key) {
if (typeof key === 'string' && !(key in target)) {
if (key in target.$data) warnReservedPrefix(target, key)
else warnNonPresent(target, key)
}
return target[key]
},
}
const handlers =
options.render && options.render._withStripped ? getHandler : hasHandler
getHandler
和hasHandler
所做的事情几乎差不多,都是在渲染阶段对不合法的数据做判断和处理。initProxy
是在渲染阶段对不合法的数据作出判断和处理。在我们日常经常遇到:未在data
定义的参数,在<template>
中却使用了。
下一章:【源码分析】 Virtual Dom
本章:【源码剖析】render 的实现
上一章: 【源码剖析】$mount 挂载