# Vue.js 源码 —— 实例方法

在上一节的 Vue 入口文件 (opens new window) 里,Vue 的封装分了三个步骤:

  1. 用 mixin 文件为 Vue 添加 Vue 2.x API 文档 (opens new window) 中定义的实例方法。
  2. 为 Vue 添加全局 API。
  3. 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 中分别定义了具体的原型方法👇。

image.png

# initMixin

在 initMixin 方法中给 Vue 添加了实例方法 _init,在 _init 方法中触发了两个生命周期钩子: beforeCreatecreated

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 方法

因此,我们可以得到这些结论:

  1. 因此我们想要调用 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
}

因此,我们可以得到这些结论:

  • 不能向 undefinednull基本类型 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

完全销毁一个实例。清理它与其它实例的连接,解绑它的全部指令及事件监听器。触发 beforeDestroydestroyed 的钩子。

  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.thenMutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

PromiseMutationObserver 属于微任务,但他们的兼容性有问题(Promise 需要环境支持,MutationObserver 需要在 IE 11+ 中),setImmediatesetTimeout 属于宏任务(但 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:如果文章中存在错误请大佬们指出!