# Vue.js 源码 —— 实例方法
在上一节的 Vue 入口文件 (opens new window) 里,Vue 的封装分了三个步骤:
- 用 mixin 文件为 Vue 添加 Vue 2.x API 文档 (opens new window) 中定义的实例方法。
- 为 Vue 添加全局 API。
- patch 和 $mount 方法。
今天我们来分析第一步: Vue 是如何定义这些实例方法。
实例方法中有四个部分:实例 property、实例方法/数据、实例方法/生命周期、实例方法/事件。关于这些实例方法,可以看到在 src/core/instance/index.js
文件中引入对应的 mixin 函数在 Vue 方法的原型链上添加实例方法。
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
在这些 mixin 中分别定义了具体的原型方法👇。
# initMixin
在 initMixin 方法中给 Vue 添加了实例方法 _init
,在 _init
方法中触发了两个生命周期钩子: beforeCreate
和 created
。
在 beforeCreate
之前,初始化了 $parent、 $root、 $children、 $refs 和 $on、 $off、 $once。
在 beforeCreate
结束后,才初始化了 props、methods、data、computed、watch 方法。
/* @flow */
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
// 一些优化内部组件实例化工作: merge options
// 初始化 Proxy 设置
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
initLifecycle(vm) // 初始化 $parent、$root、$children、$refs
initEvents(vm) // 添加 $on $off $once 等 listener
initRender(vm) // createElement,响应式 $attrs 和 $listeners
callHook(vm, 'beforeCreate') // 创建 beforeCreate 生命周期钩子
initInjections(vm) // resolve injections before data/props
initState(vm) // 初始化 props、methods、data、computed、watch
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created') //// 创建 created 生命周期钩子
// 挂载
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
// 其他的一些关于 merge options 方法
因此,我们可以得到这些结论:
- 因此我们想要调用 data 里的属性,最早的生命周期是
created
。
# stateMixin
在 stateMixin 中,在 Vue 的原型上定义了两个实例 properties: $data
$props
,以及三个实例方法/数据: $set
$delete
$watch
。
stateMixin 的结构和顺序如下:
export function stateMixin (Vue: Class<Component>) {
// 省略 dataDef 和 propsDef 具体定义过程
Object.defineProperty(Vue.prototype, '$data', dataDef)
Object.defineProperty(Vue.prototype, '$props', propsDef)
Vue.prototype.$set = set
Vue.prototype.$delete = del
Vue.prototype.$watch // 省略 $watch 具体方法
}
# $data 、 $props
先来看一下 $data
、 $props
的定义的源码 👇,其实非常简单,它直接返回了 _data 对象和 _props 对象。
const dataDef = {}
dataDef.get = function () { return this._data }
const propsDef = {}
propsDef.get = function () { return this._props }
// 非生产环境下,对 $data 和 $props 进行修改赋值操作会报错:
if (process.env.NODE_ENV !== 'production') {
dataDef.set = function () {
// 抛出错误:Avoid replacing instance root $data. Use nested data properties instead.
}
propsDef.set = function () {
// 抛出错误:$props is readonly.
}
}
Object.defineProperty(Vue.prototype, '$data', dataDef)
Object.defineProperty(Vue.prototype, '$props', propsDef)
- 那么 _data 和 _props 是从哪一步中得到的呢?🤔️
其实在上一步的 initMixin 中,beforeCreate 生命周期过后,我们就调用了 initState(vm) 初始化了 props、methods、data、computed、watch。
export function initState (vm: Component) {
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
// 初始化 methods computed watch,省略。。
}
initState 方法调用了的 initProps 和 initData 方法。他们逻辑很相似,都是遍历 propsData 或 data 的每个 key 为其添加响应式的属性,同时将 key 代理到 _props
对象和 _data
对象中。
因此我们在调用 initProps 和 initData 方法之后,就获得了 _props
对象和 _data
对象。所以在第二步的 stateMixin
中我们就能直接返回它了。
# $set
$set
方法直接使用了 src/core/observer/index 下的 set
方法。从文件路径就可以看出它与响应式有关。
Vue.prototype.$set = set
set
方法接受三个参数:目标数组/对象、键名、键值。目的是为了向数组或对象中添加新的属性,并触发响应式。
export function set (target: Array<any> | Object, key: any, val: any): any {
// 非生产环境下,不能向 undefined、null、原始类型 String Number 等设置属性,否则抛出错误警告
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
// 如果目标是数组并且传入的 key 是合法的 index 值就用 splice 修改数组
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
// 如果目标对象中已存在的 key 并且不是 Object 原型链上的值就直接修改对象属性
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
// target 有 __ob__ 属性(意味着 target 继承自 Observer 类)
const ob = (target: any).__ob__
// _isVue 是一个避免被观察到的 flag,表明它是一个 Vue 实例,在 initMixin 中定义过
// vmCount 默认值为 0,如果是作为根组件,那么会自增
// 抛出错误:避免在运行时向 Vue 实例或他的根 $data 添加响应式属性,在 data 选项中提前声明它
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
// 如果 target 没有 __ob__ 属性,表示不是响应式对象,虽然新的属性会被设置,但是不会做响应式处理
if (!ob) {
target[key] = val
return val
}
// 对于新增的属性添加响应式,通知 target 的订阅者列表触发更新
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
因此,我们可以得到这些结论:
- 不能向
undefined
、null
、基本类型 String Number 等
设置属性,否则抛出错误警告; - 修改数组/对象上
已存在
的属性则直接修改值;如果添加不存在
的属性,那么会为属性添加响应式并触发更新; - 如果修改的目标未被定义过的数组/对象(未被观察过),那么虽然新的属性会被设置,但是不会做响应式处理。
# $delete
$delete
方法直接使用了 src/core/observer/index 下的 del
方法。从文件路径就可以看出删除操作也与响应式有关。
Vue.prototype.$delete = del
del
方法接受两个参数:目标数组/对象、键名。目的是为了向数组或对象中删除属性,并可能触发响应式。
export function del (target: Array<any> | Object, key: any) {
// 删除未定义的目标或者是基本数据类型,抛出错误:不能删除 undefind\null\原始值的属性
// isUndef() 判断是否是 undefined 或 null,isPrimitive() 判断是否是 string number symbol boolean
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
// 删除目标是数组并且是合法的 index,那么用 splice 操作删除
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1)
return
}
// 删除目标是 Vue 实例或是根组件,抛出错误:避免删除 Vue 实例或根组件的 $data
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid deleting properties on a Vue instance or its root $data ' +
'- just set it to null.'
)
return
}
// 删除的属性不属于目标,不会报错,直接返回
if (!hasOwn(target, key)) {
return
}
// 删除属性,如果目标数组/对象未被观察过就直接返回,否则触发响应式更新
delete target[key]
if (!ob) {
return
}
ob.dep.notify()
}
因此,我们可以得到这些结论:
- 不能删除
undefined
null
string
number
symbol
boolean
的属性值,否则抛出错误 - 不能删除 Vue 实例或是根组件的 $data,否则抛出错误
- 删除的属性不属于目标,不会有报错也不会有响应
- 删除属性,如果目标数组/对象未被定义过就直接返回,否则才触发响应式更新
# $watch
$watch
接受三个参数:表达式/方法、回调函数、immediate / deep 选项。
Vue.prototype.$watch = function (
// 观察 Vue 实例上的一个表达式或者一个函数计算结果的变化,表达式只接受简单的键路径。
// 对于更复杂的表达式,用一个函数取代。
expOrFn: string | Function,
cb: any, // 回调函数的参数为新值和旧值
options?: Object // immediate / deep
): Function {
const vm: Component = this
// isPlainObject(cb) 做严格对象的类型检查
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
// 监听对象内部值的变化,可以在选项参数中指定 deep: true 注意,监听数组的变化不需要这么做
// 在选项参数中指定 immediate: true 将立即以表达式的当前值触发回调
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
// 开启 immediate,立即执行回调函数,invokeWithErrorHandling 对调用方法错误时进行处理
if (options.immediate) {
const info = `callback for immediate watcher "${watcher.expression}"`
pushTarget()
invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
popTarget()
}
// watcher.teardown() 原理是将 watcher 从所有监听列表中移除,并移除它的订阅者列表
return function unwatchFn () {
watcher.teardown()
}
}
因此,我们可以得到这些结论:
vm.$watch
返回一个取消观察函数unwatchFn()
方法,可以用来停止监听。使用方法:
var unwatch = vm.$watch('a', cb)
unwatch() // 取消观察时调用它
- 不仅仅可以监听表达式,还可以监听方法:
// 键路径
vm.$watch('a.b.c', function (newVal, oldVal) {
// 做点什么
})
// 函数
vm.$watch(
function () {
// 表达式 `this.a + this.b` 每次得出一个不同的结果时,处理函数都会被调用。
// 这就像监听一个未被定义的计算属性
return this.a + this.b
},
function (newVal, oldVal) {
// 做点什么
}
)
# eventsMixin
eventsMixin
定义了 $on
$once
$off
$emit
四个事件,结构很简单:
Vue.prototype.$on
Vue.prototype.$once
Vue.prototype.$off
Vue.prototype.$emit
# $on
vm.$on
监听当前实例上的自定义事件,事件可以由 vm.$emit 触发。
vm.$on
接受两个参数:event 以及它的回调函数 fn。
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
const vm: Component = this
// 当 event 是数组时,遍历每一项执行 else 中的逻辑。当 event 是 Key 时,则放入 _events[event] 中,等待 $emit 的触发
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn)
}
} else {
(vm._events[event] || (vm._events[event] = [])).push(fn)
// 优化钩子:在注册时使用 _hasHookEvent 布尔标记而不是哈希查找来优化事件开销。
// optimize hook:event cost by using a boolean flag marked at registration instead of a hash lookup
if (hookRE.test(event)) {
// _hasHookEvent = true 是生命周期回调钩子 callHook 函数触发 $emit('hook:' + hook) 的必要条件
// 在 lifecycleMixin 中用到了 _hasHookEvent
vm._hasHookEvent = true
}
}
return vm
}
回调函数 fn
会接收所有传入事件触发函数的额外参数。
vm.$on('hi', function (msg) {
console.log(msg)
})
vm.$emit('hi', 'hello')
// => "hello"
Vue.prototype.$on 做了什么?
当 event 是数组时,遍历每一项然后执行 event 是 Key 时的逻辑。当 event 是 Key 时,则放入 _events[event] 中,等待 vm.$emit 的触发后执行回调函数 fn。
# $once
vm.$once
监听一个自定义事件,但是只触发一次。一旦触发之后,监听器就会被移除。
vm.$once
接受两个参数,event 以及它的回调函数 fn。
Vue.prototype.$once = function (event: string, fn: Function): Component {
const vm: Component = this
function on () {
vm.$off(event, on) // 先移除监听
fn.apply(vm, arguments) // 后将参数传给回调函数并执行
}
on.fn = fn
// 仍然执行的是 $on,只是在 $on 的回调函数执行中先移除监听,再执行方法,从而达到执行一次的效果
vm.$on(event, on)
return vm
}
vm.$once
仍然执行的是 $on
,只是在 $on
的回调函数执行中先移除监听,再执行方法,从而达到执行一次的效果。
# $off
vm.$off
移除自定义事件监听器。vm.$off
接受两个参数:event 以及它的回调函数 fn。
- 如果没有提供参数,则移除所有的事件监听器;
- 如果只提供了事件,则移除该事件所有的监听器;
- 如果同时提供了事件与回调,则只移除这个回调的监听器。
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
const vm: Component = this
// event 和 fn 都是可选的,如果没有提供参数,则移除所有的事件监听器;
if (!arguments.length) {
vm._events = Object.create(null)
return vm
}
// events 是数组时,遍历每一项并执行后面的逻辑
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$off(event[i], fn)
}
return vm
}
const cbs = vm._events[event]
// 该事件的回调列表为空,$off 不做任何处理
if (!cbs) {
return vm
}
// 如果没有传回调函数,只提供了事件,则移除该事件所有的监听器(回调函数);
if (!fn) {
vm._events[event] = null
return vm
}
// 如果传了回调函数,就在该事件的回调函数列表中用 splice 方法移除这个回调的监听器。
let cb
let i = cbs.length
while (i--) {
cb = cbs[i]
if (cb === fn || cb.fn === fn) {
cbs.splice(i, 1)
break
}
}
return vm
}
vm.$off
的两个参数都是可选的,里面的逻辑做了很多边界条件的处理,比如不传参数时则重置监听器列表、传来的 event
是否没有回调列表、event
是否存在 fn
等。
# $emit
vm.$emit
触发当前实例上的事件。附加参数都会传给监听器回调。
Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
if (process.env.NODE_ENV !== 'production') {
const lowerCaseEvent = event.toLowerCase()
// 传入的事件名是驼峰并且 vm._events 已经注册了小写的事件名时,提示:
// HTML 不区分大小写,v-on 不能监听驼峰形式的事件 Key,你应该使用中划线替代驼峰
if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
tip(
`Event "${lowerCaseEvent}" is emitted in component ` +
`${formatComponentName(vm)} but the handler is registered for "${event}". ` +
`Note that HTML attributes are case-insensitive and you cannot use ` +
`v-on to listen to camelCase events when using in-DOM templates. ` +
`You should probably use "${hyphenate(event)}" instead of "${event}".`
)
}
}
let cbs = vm._events[event]
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs
// 将类数组的对象转化为真数组,执行当前实例上事件的所有回调函数
const args = toArray(arguments, 1)
const info = `event handler for "${event}"`
for (let i = 0, l = cbs.length; i < l; i++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info)
}
}
return vm
}
使用 v-on 监听事件时要用中划线 event-name,不要使用驼峰 eventName。
v-on:send-msg="handleSendMsg" // ✅
v-on:sendMsg="handleSendMsg" // ❌
# lifecycleMixin
lifecycleMixin
方法是在 Vue.prototype 上添加了 _update,$forceUpdate,$destroy 方法。结构如下:
export function lifecycleMixin (Vue: Class<Component>) {
Vue.prototype._update
Vue.prototype.$forceUpdate
Vue.prototype.$destroy
}
# _update
_update 是 Vue 实例的私有方法,作用是利用 patch 方法将 vnode 转化成真实 dom。
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
// setActiveInstance 接受一个实例,将当前激活实例设置为传来的实例,返回一个将激活实例恢复成前实例的方法。
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// 首次渲染时执行的逻辑。
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// 更新时执行的逻辑。
vm.$el = vm.__patch__(prevVnode, vnode)
}
// 将激活实例恢复成前实例。
restoreActiveInstance()
// 更新 __vue__ 引用。
// 清除前 $el 的 __vue__ 引用,$el 的 __vue__ 置为当前实例。
// PS:在 destroyed 阶段后也会将 $el 的 __vue__ 置为 null。
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
// if parent is an HOC, update its $el as well
// $vnode 保存的是父节点,$parent 是父节点实例,_vnode 保存的是旧节点。
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
// 在父组件的 updated 钩子里,子组件的 updated 钩子也会被调度器调用
}
# $forceUpdate
迫使 Vue 实例重新渲染。注意它仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。
Vue.prototype.$forceUpdate = function () {
const vm: Component = this
if (vm._watcher) {
vm._watcher.update() // 调用 core/instance/observer/watcher.js 中的 update() 方法
}
}
# $destory
完全销毁一个实例。清理它与其它实例的连接,解绑它的全部指令及事件监听器。触发 beforeDestroy
和 destroyed
的钩子。
Vue.prototype.$destroy = function () {
const vm: Component = this
// 如果正在被销毁,就不再继续执行销毁操作,直接返回
if (vm._isBeingDestroyed) {
return
}
// 调用 beforeDestroy 生命周期钩子后打开正在被销毁标志
callHook(vm, 'beforeDestroy')
vm._isBeingDestroyed = true
// 父实例存在、且没有正在被销毁、也不是抽象组件就移除自身组件实例
const parent = vm.$parent
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
remove(parent.$children, vm)
}
// teardown watchers
// 从所有订阅者列表中删除自身,意味着不会再被观察
if (vm._watcher) {
vm._watcher.teardown()
}
let i = vm._watchers.length
while (i--) {
vm._watchers[i].teardown()
}
// remove reference from data ob
// frozen object may not have observer.
if (vm._data.__ob__) {
vm._data.__ob__.vmCount--
}
// __patch__ 方法传入旧节点和新节点 null 用于销毁节点,之后调用 destroy 钩子
vm._isDestroyed = true
vm.__patch__(vm._vnode, null)
callHook(vm, 'destroyed')
// vm.$off 移除自定义事件监听器。如果没有提供参数,则移除实例上所有的事件监听器
vm.$off()
// 移除 __vue__ 引用
if (vm.$el) {
vm.$el.__vue__ = null
}
// 释放循环引用
if (vm.$vnode) {
vm.$vnode.parent = null
}
}
$destory
做了什么?
- 判断了如果是正在被销毁的组件执行了 $destory,将不会执行销毁操作。
- 触发了
beforeDestroy
生命周期。 - 从父组件的 $children 中删除自身实例。
- 从订阅者列表 _watchers 中移除自身实例。
- 用 __ patch __ 方法销毁节点。
- 触发了
destroyed
生命周期。 - 使用 vm.$off 方法移除实例上所有的事件监听器。
# renderMixin
renderMixin 给 Vue.prototype 添加了许多 runtime helpers 方法、$nextTick、_render 方法。
export function renderMixin (Vue: Class<Component>) {
installRenderHelpers(Vue.prototype)
Vue.prototype.$nextTick
Vue.prototype._render
}
installRenderHelpers 为 Vue.prototype 添加了许多方法:
export function installRenderHelpers (target: any) {
target._o = markOnce // 使用唯一 key 将节点标记为静态
target._n = toNumber // 字符串转成数字,失败的话返回原值
target._s = toString // 将任何类型的值转成字符串
target._l = renderList // 渲染 v-for 列表,返回一个 VNode 组成的数组
target._t = renderSlot // 渲染 <slot>,返回一个 VNode 组成的数组
target._q = looseEqual // 判断任意类型是否松散相等
target._i = looseIndexOf // 获取数组中与参数相等的值的下标
target._m = renderStatic // 优先获取缓存渲染树,其次返回一个新的渲染树
target._f = resolveFilter // 处理过滤器,返回 this.$options['filters'][id]
target._k = checkKeyCodes // 校验 eventKeyCode 方法
target._b = bindObjectProps // 合并 v-bind="object" 到 VNode 的 data 中
target._v = createTextVNode // 创建字符串节点
target._e = createEmptyVNode // 创建空节点,可以传入 text
target._u = resolveScopedSlots // 处理 scoped slots
target._g = bindObjectListeners
target._d = bindDynamicKeys // 绑定动态 key,<div :[key]="value">
target._p = prependModifier //动态添加修饰器运行标记到事件名称上
}
# $nextTick
$nextTick
将回调延迟到下次 DOM 更新循环之后执行。$nextTick
涉及到了事件循环机制,源码在 src/core/util/next-tick.js
中。
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
}
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
// timerFunc 优先使用 Promise、MutationObserver、setImmediate,最后使用 setTimeout 执行 callbacks 数组中的回调函数。
if (!pending) {
pending = true
timerFunc()
}
// 这是当 nextTick 不传 cb 参数的时候,提供一个 Promise 化的调用,比如:nextTick().then(() => {})
// 当 _resolve 函数执行,就会跳到 then 的逻辑中。
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
Vue 在内部对异步队列尝试使用原生的 Promise.then
、MutationObserver
和 setImmediate
,如果执行环境不支持,则会采用 setTimeout(fn, 0)
代替。
Promise
和 MutationObserver
属于微任务,但他们的兼容性有问题(Promise 需要环境支持,MutationObserver 需要在 IE 11+ 中),setImmediate
和 setTimeout
属于宏任务(但 setImmediate 只有在最新版本的 IE 和Node.js 0.10+ 实现了该方法)。IE:你看我干嘛?关于 Vue 的降级策略,推荐看这篇文章 # 全面解析Vue.nextTick实现原理 (opens new window)。
# _render
_render 是 Vue 实例的私有方法,它返回一个 Vnode。它调用了外部传入的 render 和 renderError 方法。
Vue 选项中的 render
函数若存在,则 Vue 构造函数不会从 template
选项或通过 el
选项指定的挂载元素中提取出的 HTML 模板编译渲染函数。而是接收一个 createElement
方法作为第一个参数用来创建 VNode
。
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
// 如果父组件存在,表示该实例是子组件。
if (_parentVnode) {
// 作用域插槽 `vm.$scopedSlots`,用渲染函数向子组件中传递作用域插槽,使插槽内容能够访问子组件中才有的数据。
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
)
}
// 设置父节点 vnode。
vm.$vnode = _parentVnode
// 开始渲染节点,如果 render 函数出错或是 renderError 函数也出错,兜底方案是返回前一个 vnode。
let vnode
try {
currentRenderingInstance = vm
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
handleError(e, vm, `render`)
// renderError 函数只在开发环境下工作。
if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
try {
// Vue 选项中的 `renderError` 函数若存在,当 render 函数遭遇错误时,提供另外一种渲染输出。
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
}
// render 函数返回数组且只有一个节点时,让 vnode 等于这个值。
if (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0]
}
// render 函数出错的情况下,返回空 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 = _parentVnode
return vnode
}
# 总结
实例方法中定义了很多边界条件的判断,并抛出错误,通过这些错误处理可以很大程度上避免在开发时写出 bug,阅读源码同时可以和 API 文档 (opens new window) 一起阅读,能够更具体的理解方法中参数的作用,也有助于掌握实例方法的使用。
因为最近在看 Vue.js
的源码,所有学习过程中的笔记和总结都会记录在我的 GitHub (opens new window) 中,感谢关注~
PS:如果文章中存在错误请大佬们指出!