Vue的全局API【Vue源码学习】

2019-04-17

Vue框架提供了很多全局的API、例如Vue.extend()Vue.nextTick()Vue.component()Vue.filter()Vue.use()等等,通过这些API我们可以轻松的完成一些工作。这篇文章我们通过源码方式去理解他们的实现原理。

全局API初始化

全局API的初始化其实是在/src/core/index.js文件里,其中执行了initGlobalAPI(Vue),该函数定义在/src/core/global-api/index.js

initGlobalAPI(Vue)

// 省略中间代码...

// Vue.version也属于Vue的全局API。提供了 Vue 安装版本号
// 在这里定义成__VERSION__字符串,打包的时候通过rollup-plugin-replace插件进行替换
Vue.version = '__VERSION__'

我们打开文件找到initGlobalAPI函数,源码如下:

export function initGlobalAPI (Vue: GlobalAPI) {
  // config
  const configDef = {}
  configDef.get = () => config
  if (process.env.NODE_ENV !== 'production') {
    configDef.set = () => {
      warn(
        'Do not replace the Vue.config object, set individual fields instead.'
      )
    }
  }
  Object.defineProperty(Vue, 'config', configDef)

  // exposed util methods.
  // NOTE: these are not considered part of the public API - avoid relying on
  // them unless you are aware of the risk.
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }

  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick

  // 2.6 explicit observable API
  Vue.observable = <T>(obj: T): T => {
    observe(obj)
    return obj
  }

  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })

  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options._base = Vue

  extend(Vue.options.components, builtInComponents)

  initUse(Vue)
  initMixin(Vue)
  initExtend(Vue)
  initAssetRegisters(Vue)
}

我们可以发现函数体内,对Vue构造函数挂载了很多我们熟悉的全局API。注意到最后四行代码,都是通过init*函数来初始化其他API(use、mixin、extend、component、filter、directive),这四个函数文件也都定义同目录下。我们就先从initGlobalAPI函数开始分析各个全局API的实现。

Vue.config

initGlobalAPI函数开始有一段代码如下:

// 定义一个空对象,设置getter、setter,赋值给Vue.config
const configDef = {}
configDef.get = () => config
// 非生产环境、设置Vue.config会无效,并给出警告。
if (process.env.NODE_ENV !== 'production') {
  configDef.set = () => {
    warn(
      'Do not replace the Vue.config object, set individual fields instead.'
    )
  }
}
Object.defineProperty(Vue, 'config', configDef)

可以看出来,Vue.config其实就是引入的config,config是由/src/core/config.js导出,其中的配置项可用于调试、编译等各个地方。Vue阻止了用户修改config,并尝试给出警告。

Vue.util

接下来继续看下段代码:

// NOTE: these are not considered part of the public API - avoid relying on
// them unless you are aware of the risk.
Vue.util = {
  warn, // 警告工具函数  定义在/src/core/util/debug.js
  extend, // 将属性混合到目标对象  定义在/src/shared/util.js
  mergeOptions, // 合并选项 定义在/src/core/util/options.js
  defineReactive // 定义对象的响应式属性(getter、setter) 定义在/src/core/observer/index.js
}

Vue.util上挂载了4个工具函数,如上面注释所说,Vue.util不是公共的API,避免依赖于它,所以说Vue官方文档也没有说到这一点。其中mergeOptions在我的之前一篇文章有分析到,用于选型的合并。而defineReactive主要功能是定义对象的gettersetter,用于收集依赖和派发更新。

Vue.set

import { set, del } from '../observer/index'

Vue.set = set 
Vue.delete = delete 

Vue.set指向的是set函数,我们根据上面的引用路径,找到/src/core/observer/index文件,set函数源码如下:

/**
 * 在一个对象设置属性,添加一个新属性会派发更新
 */
export function set (target: Array<any> | Object, key: any, val: any): any {
  // 非生产环境下、如果没有传入target 或者 target是基本类型给出警告
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  // 如果target是数组,并且是一个有效的索引
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    // 修改target数组的长度,取key和原长度的最大值
    target.length = Math.max(target.length, key)
    // 取代数组索引上的值
    target.splice(key, 1, val)
    return val
  }
  // 如果不是数组便是对象,
  if (key in target && !(key in Object.prototype)) {
    // 如果target存在key属性,则修改对应的值
    target[key] = val
    return val
  }
  // 取到target.__ob__属性赋值给ob,有__ob__代表是响应式对象
  const ob = (target: any).__ob__
  // 如果target._isVue或者ob.vmCount有值,则说明target是根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
  }
  // 如果ob没有值,代表不是响应式对象,直接赋值target[key]为val返回
  if (!ob) {
    target[key] = val
    return val
  }
  // 通过defineReactive将target.key变成响应式,
  defineReactive(ob.value, key, val)
  // 派发更新
  ob.dep.notify()
  return val
}

大家应该都知道this.$set()this.$delete(),其实它们与Vue.set()Vue.delete是一样的,都是调用set或delete函数。主要的作用是为了解决Vue无法监听到的数据变化,从而主动的派发更新。当我们直接改变数组其中的元素、或者往对象上定义新属性,Vue都是无法监听的。这时就要用到set,所以set函数主要分为三个分支:

  • 如果target是数组,我们需要通过Array.prototype.splice()方法将对应索引的值修改or赋值,这时Vue会捕获到splice方法从而派发更新。注意这时要重新设置数组的长度,取到最大值。因为如果索引值超过长度,还没有改变数组长度,那splice()方法只会往数组末尾插入元素
    // 可以试一下面的代码,结果是arr数组会变成[1, 2, 3, 'ten']
    var arr = [1,2,3]
    arr.splice(10, 1, 'ten')
    console.log(arr)
    
  • 如果target不是数组便是对象(因为是原始类型或未定义,不会运行到这里)。此时如果target对象存在此属性,则直接修改此属性对应的值,然后返回。
  • 如果target.key本身不存在,并且target被处理过响应式(含有__ob__属性),那就要通过defineReactive函数将target.key设置setter、getter,变成响应式。并且通过ob.dep.notify()来触发一下更新。

Vue.delete

Vue.delete实际上也是执行了delete函数,该函数也是定义在/src/core/observer/index文件:

/**
 * 删除一个属性,必要时触发更新
 */
export function del (target: Array<any> | Object, key: any) {
  // 同set
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  // 数组并且有效索引,直接删除,
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1)
    return
  }
  const ob = (target: any).__ob__
  // 同set
  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
  }
  // 如果targe.key不存在,直接return
  if (!hasOwn(target, key)) {
    return
  }
  // 删除
  delete target[key]
  if (!ob) {
    return
  }
  // 如果target是响应式对象,触发更新
  ob.dep.notify()
}

看完set函数,那delete函数就很容易看懂😉,基本上与set一样。如果是数组,并且索引有效,直接通过splice方法进行删除即可。是对象的话,首先判断是否存在该属性,如果不存在那根本谈不上删除,直接return,通过delete操作符进行删除,如果存在__ob__属性那代表是响应式,需要触发下更新操作。

Vue.nextTick

Vue.nextTick()其实是调用了nextTick函数,nextTick函数其实是使用微任务(microtask)来实现异步执行函数。该函数定义在/src/core/util/next-tick.js。我们从文件开头看起:

// 是否使用了MicroTask(微任务)的标志位
export let isUsingMicroTask = false
// 回调函数存放的数组
const callbacks = []
// 正在执行回调函数的标志位
let pending = false
// 遍历callbacks,执行每一个回调函数
function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

这段代码如注释一样,很简单,除了标志位以外,flushCallbacks函数主要是依次执行callbacks中的函数。该函数会在事件循环MicroTask时期执行。关于宏任务和微任务,我之前也写过一篇博客,接下来我们继续往下看,我们去掉官方的大量提示。

let timerFunc
// 如果原生支持promise,采用Promise.resolve().then()调用flushCallbacks
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    // 处理ios一个bug,promise.then()不会执行,这里需要通过setTimeout来触发一下。
    if (isIOS) setTimeout(noop)
  }
  // 修改标志位,promise属于微任务
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // 当不支持原生Promise,常识使用MutationObserver
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  // MutationObserver 属于微任务
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // promise、MutationObserver都不支持,尝试使用原生setImmediate,
  // setImmediate属于宏任务,但它还是优于setTimeout(xxx,0)
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // 上述都不行,使用setTimeout。
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

上述代码去掉了大量官方注释,官方注释主要说的是,nextTick改来改去的原因,所出现的问题以及对应的issue。我们关注于现在就好,事件循环中,微任务要比宏任务优先执行,所以nextTicktimerFunc函数优先使用Promise,其次尝试使用MutationObserver。当前俩者都不支持时,就会采取宏任务。那之所以setImmediate优先于setTimeout,是因为setTimeout在将回调注册为宏任务之前要不停的做超时检测,而 setImmediate 则不需要,性能会更好。通过上述代码,timerFunc函数就被赋予值啦。所以接下来我们看一下具体的nextTick函数。

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // 将回调函数推入到callbacks数组
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  // 如果pending为false,那说明可以进入事件循环,执行timerFunc
  if (!pending) {
    pending = true
    timerFunc()
  }
  // 如果回调函数没有传值,并且支持Promise,将_resolve设置为promise的resolve
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

当我们通过this.$nextTick(() => {})使用时,nextTick函数会将回调函数包装一下,catch错误。然后将包装函数退出callbacks数组。然后判断pendingfalse的时候执行上面的timerFunc函数,注意:只有当微任务或宏任务执行时,也就是flushCallbacks执行时,才会将pending设置为true,代表在下一次事件循环中才会再执行flushCallbacks

Vue.observable

Vue.observable = <T>(obj: T): T => {
  observe(obj)
  return obj
}

Vue.observable是Vue2.6版本新增的,通过它可以让一个对象变成响应式,Vue内部会用它来处理data函数返回的对象。

Vue.observable其实是调用了observe函数,该函数就是Vue使数据变成响应式的入口,最终还是会执行defineReactive函数设置setter、getter,关于响应式我也会单独写篇文章,这里就不详细的说明啦。

Vue.options

// 定义成没有原型链的空对象
Vue.options = Object.create(null)
// component、directive、filter挂在到 Vue.options.*s上,并且暂为空对象
ASSET_TYPES.forEach(type => {
  Vue.options[type + 's'] = Object.create(null)
})
Vue.options._base = Vue

这里我们可以看到Vue.options定义始于这里,并且资源类型都会定义成空对象。但在其他地方,会对Vue.options进行扩充。这里举个例子,大家可以打开/src/plaforms/web/runtime/index.js文件。如下:

// 扩展平台相关的指令和组件,
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)

Vue.options上的东西都会在实例化或者组件初始化的时候,通过mergeOptions进行合并。

Vue.use

// /src/core/global-api/index.js
import { initUse } from './use'

initUse(Vue)

我们继续往下可以看到Vue.use()是通过initUse初始化的,源码如下:

export function initUse (Vue: GlobalAPI) {
  Vue.use = function (plugin: Function | Object) {
    // 获取到已经注册过的插件,如果已被安装,直接返回,不必重新安装
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }

    // 获取到其余参数转成数组
    const args = toArray(arguments, 1)
    // 将参数数组首位置插入自身,Vue
    args.unshift(this)
    // 调用插件的install方法,通过apply传入参数数组,
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
      // Vue.use()也可以直接传入函数
      plugin.apply(null, args)
    }
    installedPlugins.push(plugin)
    return this
  }
}

Vue.use()在开发中应该用的会比较少,因为只有当写一个插件的时候,才会用到此API进行安装。它的原理也很简单,就是调用对象的install方法或者直接调用函数。Vue.use()除了第一个参数外,其他参数都会传给插件所执行的函数。并且会把当前Vue当做第一个参数传入。

Vue.mixin

// /src/core/global-api/index.js
import { initMixin } from './mixin'

initMixin(Vue)

我们继续往下可以看到Vue.mixin()是通过initMixin初始化的,源码如下:

import { mergeOptions } from '../util/index'

export function initMixin (Vue: GlobalAPI) {
  // 调用mergeOptions函数与Vue.options合并,赋值给Vue.options
  Vue.mixin = function (mixin: Object) {
    this.options = mergeOptions(this.options, mixin)
    return this
  }
}

Vue.mixin很简单,就是调用mergeOptions函数进行选项合并,并且重新赋值给Vue.options,有关于mergeOptions的具体实现,在我的上一篇文章已经详细的分析过。

Vue.extend

import { initExtend } from './extend'
initExtend(Vue)

通过上面代码找到同目录下的extend.js,可以看到Vue.extend定义在这里,源码如下:

Vue.extend = function (extendOptions: Object): Function {
  extendOptions = extendOptions || {}
  // 定义父类Super是Vue
  const Super = this
  // 获取到父类自增的id
  const SuperId = Super.cid
  // extendOptions._Ctor用于缓存,尝试从缓存中获取,函数最后也会加入缓存
  const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
  if (cachedCtors[SuperId]) {
    return cachedCtors[SuperId]
  }
  // 取到extendOptions.name,校验组件名
  const name = extendOptions.name || Super.options.name
  if (process.env.NODE_ENV !== 'production' && name) {
    validateComponentName(name)
  }
  // 定义子组件造器
  const Sub = function VueComponent (options) {
    this._init(options)
  }
  // 通过原型链组合方式继承
  Sub.prototype = Object.create(Super.prototype)
  Sub.prototype.constructor = Sub
  Sub.cid = cid++
  // 通过mergeOptions合并选项
  Sub.options = mergeOptions(
    Super.options,
    extendOptions
  )
  Sub['super'] = Super

  // 对于props和计算属性,在extend时期执行初始化,
  // 这样可以避免每个创建的实例都调用object.defineproperty
  if (Sub.options.props) {
    initProps(Sub)
  }
  if (Sub.options.computed) {
    initComputed(Sub)
  }

  // 使子类也拥有Vue.extend, Vue.mixin, Vue.mixin方法
  Sub.extend = Super.extend
  Sub.mixin = Super.mixin
  Sub.use = Super.use

  // 访问子类的资源,直接代理到父类上
  ASSET_TYPES.forEach(function (type) {
    Sub[type] = Super[type]
  })
  // 允许递归调用自身
  if (name) {
    Sub.options.components[name] = Sub
  }

  // 保存父类选项、子类选项,以及合并后的选项
  Sub.superOptions = Super.options
  Sub.extendOptions = extendOptions
  Sub.sealedOptions = extend({}, Sub.options)

  // 缓存构造器,cachedCtors 是 extendOptions._Ctor的引用
  cachedCtors[SuperId] = Sub
  // 返回子类构造器
  return Sub
}

我们将中间代码部分省略,代码总体如下,我们就可以清楚的发现Vue.extend在做什么事情

Vue.extend = function (extendOptions: Object): Function {
  const Super = this
  // 定义子组件造器
  const Sub = function VueComponent (options) {
    this._init(options)
  }
  // 省略代码...
  Sub.prototype = Object.create(Super.prototype)
  Sub.prototype.constructor = Sub
  // 省略代码...
  return Sub
}

Vue.extend做的事情就是通过原型链继承的方式实现子组件构造器,当子组件patch时,就会执行构造器函数来实现挂载。


函数开头尝试通过父类cidextendOptions._Ctor获取缓存,其实在Vue.extend函数最后,会将本次的构造函数存在extendOptions._Ctor中,key值为父类cid。我们将俩端代码合并看的会清楚些,如下:

// extendOptions._Ctor用于缓存,尝试从缓存中获取,函数最后也会加入缓存
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) {
  return cachedCtors[SuperId]
}
// 缓存构造器,cachedCtors 是 extendOptions._Ctor的引用
cachedCtors[SuperId] = Sub

通过extend继承的子类构造器,需要拥有与父类一样的功能,所以除了原型链继承,还需要在子类上定义相同的方法,所以下面这段代码,主要是做这件事情

Sub.options = mergeOptions(
  Super.options,
  extendOptions
)
// 使子类也拥有Vue.extend, Vue.mixin, Vue.mixin方法
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use

// 访问子类的资源,直接代理到父类上
ASSET_TYPES.forEach(function (type) {
  Sub[type] = Super[type]
})

Vue.filter、Vue.directive、Vue.component

我们来看一下initGlobalAPI最后一行代码:

import { initAssetRegisters } from './assets'
initAssetRegisters(Vue)

打开同层目录下的assets.js文件,可以看到Vue是通过循环ASSET_TYPES来定义资源类型API的。通过在同一个函数里的if判断,来做不同的处理,我们逐行分析一下。

ASSET_TYPES.forEach(type => {
  Vue[type] = function (
    id: string,
    definition: Function | Object
  ): Function | Object | void {
    // 第二个参数没有传,相当于直接获取Vue.*s上的资源,有可能是undefined
    if (!definition) {
      return this.options[type + 's'][id]
    } else {
      // 非生产环境,如果是Vue.component,校验注册的组件名是否符合规定
      if (process.env.NODE_ENV !== 'production' && type === 'component') {
        validateComponentName(id)
      }
      // 如果是Vue.component 并且第二个参数是纯对象
      if (type === 'component' && isPlainObject(definition)) {
        // 优先采取定义上的名字。不存在采用id
        definition.name = definition.name || id
        // Vue.options._base就是 Vue. 调用Vue.extend()获取子类构造器赋予definition
        definition = this.options._base.extend(definition)
      }
      // 如果是指令,并且定义传入的是个函数,属于简略的写法,直接规范成对象,
      if (type === 'directive' && typeof definition === 'function') {
        definition = { bind: definition, update: definition }
      }
      // 处理好的definition 赋予Vue.options.*s上
      this.options[type + 's'][id] = definition
      return definition
    }
  }
})

如果我们在使用Vue.componentVue.filterVue.directive的时候,如果不传入第二个定义参数,那Vue会尝试去Vue.options.*s上去查找资源。如果传入了,Vue会首先看是否是注册组件,如果是组件的话,需要校验传入的名字是否合法,validateComponentName函数代码如下:

// 如果检验不通过,给出相应警告
export function validateComponentName (name: string) {
  // 符合HTML5规范,由普通字符和中横线(-)组成,并且必须以字母开头。
  if (!new RegExp(`^[a-zA-Z][\\-\\.0-9_${unicodeRegExp.source}]*$`).test(name)) {
    warn(
      'Invalid component name: "' + name + '". Component names ' +
      'should conform to valid custom element name in html5 specification.'
    )
  }
  // isBuiltInTag是检验名字不能与slot,component重名
  // isReservedTag是检验不能与html、svg内置标签重名
  if (isBuiltInTag(name) || config.isReservedTag(name)) {
    warn(
      'Do not use built-in or reserved HTML elements as component ' +
      'id: ' + name
    )
  }
}

接下来是俩个if语句,分别对componentdirective做额外的处理。当是Vue.component并且是纯对象时做处理,这里我们组件是通过import引入,经过vue-lodaer处理成对象。然后通过Vue.extend实现子类构造器。这里会优先采用组件内部的名字,如下,如果我们Loading组件内部name属性被指定是Loading,虽然使用时用MyLoading,但组件名还是loading

import Loading from 'common/Loading.vue'
// 使用时还是<MyLoading /> 但组件名是loading
// 可以通过vuejs-devtools查看
Vue.component('MyLoading', Loading) 

我们再来看一下directive的处理,当使用Vue.directive的时候,如果第二个参数不是对象,而是函数的话,属于Vue提供的便捷方式,代表{ bind: fnc, update: fnc },具体使用方法可以看下文档


我们可以发现,资源类型的API的注册,根本上就是将资源挂载到Vue.options.*s上,但其中对componentdirective做额外的处理,并没有对filter做特殊处理。Vue.options上的东西都会在实例化或者组件初始化的时候,通过mergeOptions进行合并。资源类型合并后会通过原型链合并。所以我们才可以在组件里直接使用全局过滤器、组件、指令,而不需要进行再次注册。