关键词:vm.$createElement
、vm._renderProxy
、VNode
逻辑:vm._render
最终是通过执行createElement
方法并返回的是vnode
。
src/core/instance/lifecycle.js
文件运行的vm._render()
定义在src/core/instance/render.js
文件中。
(1)定义了原型上_render
的私有方法,返回的是一个VNode
。从$options
拿到render
函数(render
函数可以用户自己写也可以通过编译生成),利用render.call()
方法传入vm._renderProxy
和vm.$createElement
。call
的第一个参数是当前上下文,vm._renderProxy
在生产环境( production )下就是vm
,在开发环境( development )中可能是proxy
对象。vm.$createElement
在initRender
函数中定义。
initRender
函数在src/core/instance/init.js
中执行initRender(vm)
。initRender
中定义了两个函数:vm._c
和vm.$createElement
。这两个函数最终都调用了createElement
方法,区别是最后一个参数不一样。因为vm._c
是被编译生成的render
函数所使用的方法,vm.$createElement
实际上是给我们手写render
函数提供了一个创建 VNode 的方法。
// 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)
}
}
// src/core/instance/render.js
Vue.prototype._render = function (): VNode {
const vm: Component = this
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
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
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]
}
// 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
}
(2)手写render
函数(使用$createElement
)
这跟直接在 html 上写是不一样的。他没有一个把从插值({{message}}
)变换的过程。之前的写法是在 html 里面定义了一个插值,会先把插值渲染出来,然后在new Vue
之后执行$mount
的时候再把插值替换成真实的数据。
而这是通过纯render
函数,不用在页面上显示插值,而是通过render
函数执行完毕之后把message
替换上去,体验会更好。因为手写render
函数就不会执行把template
转换成render
函数这一步了,挂载的元素(id="app1"
)实际上会替换掉定义的(id="app"
)。这就是为什么不能在body
上做这个事情,因为会把body
替换。
<div id="app">div>
export default {
data() {
return {
message: 'Hello Vue!',
};
},
// render第一个参数是 vm.$createElement ,所以createElement是函数创建 VNode , VNode标签 是个 div
render(createElement){
return createElement('div',{
attrs:{
id:'app1'
}
},this.message)
},
};
(1)vm._renderProxy
也是发生在src/core/instance/init.js
中,如果当前是生产环境,就vm._renderProxy = vm
,开发阶段则initProxy(vm)
,initProxy()
的定义在src/core/instance/proxy.js
文件中。
判断hasProxy
(判断当前浏览器支不支持proxy
,proxy
实际上是 ES6 提供的 API(阮一峰老师) ,实际作用就是对对象访问做一个劫持)。因为chrome支持proxy
,所以会执行vm._renderProxy = new Proxy(vm, handlers)
,handlers
在目前条件下指向hasHandler
。hasHandler
是个判断,如果我们的元素不在target
下,has
为false
。isAllowed
是全局的属性和方法。在两个情况都不满足的条件下,执行warnNonPresent
方法。warnNonPresent
就是报警告。该警告是报使用了一个未在 data、method 定义的一个变量。
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
}
}
}
export { initProxy }
render
方法实际上就是生成一个vnode
,出错的话handleError
会给用户一个接口去处理一些错误,再做一系列降级。再对vnode
进行判断是不是VNode
,如果同时是个Array
,说明模板会有多个根节点,会返回多个vnode
。vnode
实际上是Virtual DOM
的概念。
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// a flag to avoid this being observed
vm._isVue = true
// merge options
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.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
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)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}