Vue源码(十二)常用API

764 阅读3分钟

Vue.use

文档

定义在src/core/global-api/use.js

Vue.use = function (plugin: Function | Object) {
  const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
  if (installedPlugins.indexOf(plugin) > -1) {
    return this
  }

  const args = toArray(arguments, 1)
  args.unshift(this)
  if (typeof plugin.install === 'function') {
    plugin.install.apply(plugin, args)
  } else if (typeof plugin === 'function') {
    plugin.apply(null, args)
  }
  installedPlugins.push(plugin)
  return this
}

Vue.use传入的第一个参数可以是对象也可以是函数。首先获取或创建_installedPlugins数组,并判断传入的参数是否在这个数组中,防止多次注册。接下来将第一个参数以外的其他参数转换成数组;并将Vue构造函数添加到数组开头。如果传入的第一个参数是对象并且有install方法,通过apply方法调用install,参数为数组元素,this指向这个传入的对象。如果第一个参数是一个函数,通过apply调用参数为数组元素,this指向null。最后返回Vue

也就是说如果Vue.use第一个参数是一个对象并且有install方法,install方法第一个参数是Vue,并且内部this指向这个对象

const installTest = {
  name: 'installTest',
  install(Vue, ...args){
    console.log(this.name, args)
  }
}
Vue.use(installTest, 1, 2, 3)

// 打印 installTest, [1, 2, 3]

如果Vue.use第一个参数是函数,这个函数的第一个参数也是Vue,并且this指向null

const fnTest = function (Vue, ...args){
  console.log(this, args)
}
Vue.use(fnTest, 1, 2, 3)
// 打印 null, [1, 2, 3]

Vue.mixin

定义在src/core/global-api/mixin.js

Vue.mixin = function (mixin: Object) {
  this.options = mergeOptions(this.options, mixin)
  return this
}

调用mergeOptions将传入的对象根据某些合并策略添加到Vue.options中,并返回Vue

export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (typeof child === 'function') {
    child = child.options
  }

  // 规范化 props
  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)

  if (!child._base) {
    if (child.extends) {
      parent = mergeOptions(parent, child.extends, vm)
    }
    if (child.mixins) {
      for (let i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm)
      }
    }
  }

  const options = {}
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

首先是规范化propsinjectdirectives,这里就看下props

function normalizeProps (options: Object, vm: ?Component) {
  const props = options.props
  if (!props) return
  const res = {}
  let i, val, name
  // props: ['name', 'nick-name']
  if (Array.isArray(props)) { // 数组
    i = props.length
    while (i--) {
      val = props[i]
      if (typeof val === 'string') {
        // nick-name -> nickName
        name = camelize(val)
        res[name] = { type: null }
      } else if (process.env.NODE_ENV !== 'production') {
        warn('props must be strings when using array syntax.')
      }
    }
  } else if (isPlainObject(props)) { //对象
    for (const key in props) {
      val = props[key]
      name = camelize(key)
      // val 可能是一个对象 也可能是一个构造函数(name: Boolean)
      res[name] = isPlainObject(val)
        ? val
        : { type: val }
    }
  } else if (process.env.NODE_ENV !== 'production') {
    warn(
      `Invalid value for option "props": expected an Array or an Object, ` +
      `but got ${toRawType(props)}.`,
      vm
    )
  }
  options.props = res
}
  • 如果props是数组,比如['nick-name'],经过转换变成{ nickName: { type: null } }
  • 如果props是一个对象,转换前后如下
// 转换前
{ 
  nick-name: Boolean,
  name: { type: String } 
}

// 转换后
{ 
  nickName: { type: Boolean },
  name: { type: String } 
}

回到mergeOptions,规范化完成后,如果child没有_base属性,将child.extendschild.mixins通过mergeOptions合并到parent

if (!child._base) {
  if (child.extends) {
    parent = mergeOptions(parent, child.extends, vm)
  }
  if (child.mixins) {
    for (let i = 0, l = child.mixins.length; i < l; i++) {
      parent = mergeOptions(parent, child.mixins[i], vm)
    }
  }
}

src/core/global-api/index.js中会往Vue.options上挂载_base属性,属性值为Vue Vue.options._base = Vue。在_init方法中如果是根实例会调用mergeOptions合并Vue.options到根实例的options中;组件实例在创建组件VNode时,调用Vue.extend创建组件实例的构造函数,在Vue.extend中也会调用mergeOptions合并Vue.options到构造函数的options中;也就是说经mergeOptions合并后的child都会带有_base;只有原始child对象才没有;所有这里只将原始childextendsmixins通过mergeOptions合并到parent

接下来开始合并逻辑

const options = {}
let key
for (key in parent) {
  mergeField(key)
}
for (key in child) {
  if (!hasOwn(parent, key)) {
    mergeField(key)
  }
}
function mergeField (key) {
  const strat = strats[key] || defaultStrat
  options[key] = strat(parent[key], child[key], vm, key)
}
return options

遍历parent中所有的keychild剩余的key,分别调用mergeField函数;strats是一个对象,存储的各种缓存策略

strats = {
  props: xxx,
  methods: xxx,
  computed: xxx,
  生命周期: xxx,
  ...
}

如果strats对象中没有相应的key,则调用默认策略defaultStrat

const defaultStrat = function (parentVal: any, childVal: any): any {
  return childVal === undefined
    ? parentVal
    : childVal
}

默认策略就是child优先,根据上述合并策略后,将合并后的属性值赋值给options并返回

介绍下几种合并策略

生命周期合并策略

const LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  ...
];
LIFECYCLE_HOOKS.forEach(hook => {
  strats[hook] = mergeHook
})

function mergeHook (
  parentVal: ?Array<Function>,
  childVal: ?Function | ?Array<Function>
): ?Array<Function> {
  const res = childVal
    ? parentVal ? parentVal.concat(childVal) : Array.isArray(childVal) ? childVal : [childVal]
    : parentVal
  return res ? dedupeHooks(res) : res
}

根据parentchild的有无,将生命周期合并成数组;通过dedupeHooks对数组去重后返回这个数组

data合并策略

strats.data = function (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  if (!vm) {
    if (childVal && typeof childVal !== 'function') {
      process.env.NODE_ENV !== 'production' && warn(
        'The "data" option should be a function ' +
        'that returns a per-instance value in component ' +
        'definitions.',
        vm
      )

      return parentVal
    }
    return mergeDataOrFn(parentVal, childVal)
  }
  return mergeDataOrFn(parentVal, childVal, vm)
}

不管有没有vm都会调用mergeDataOrFnVue.extendVue.mixin触发的mergeOptions函数没有vm属性,在开发环境下如果传入的data不是函数会报警告。子组件实例的构造函数通过Vue.extend创建,所以也不会传入vm属性

export function mergeDataOrFn (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  if (!vm) {
    if (!childVal) {
      return parentVal
    }
    if (!parentVal) {
      return childVal
    }
    return function mergedDataFn () {
      return mergeData(
        typeof childVal === 'function' ? childVal.call(this, this) : childVal,
        typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
      )
    }
  } else {
    return function mergedInstanceDataFn () {
      const instanceData = typeof childVal === 'function'
        ? childVal.call(vm, vm)
        : childVal
      const defaultData = typeof parentVal === 'function'
        ? parentVal.call(vm, vm)
        : parentVal
      if (instanceData) {
        return mergeData(instanceData, defaultData)
      } else {
        return defaultData
      }
    }
  }
}

如果没有传入vm,返回一个函数mergedDataFn,在创建实例过程中会调用这个函数,函数内部调用mergeData,并将childValparentVal的所有属性传入。

如果vm有值,返回mergedInstanceDataFn函数,在创建实例过程中会调用这个函数,首先获取childValparentVal的所有属性;如果childVal有值,调用mergeData,反之返回parentVal对象,其实有vm的情况只发生在根实例创建过程中,所以直接返回对象也没问题

function mergeData (to: Object, from: ?Object): Object {
  if (!from) return to
  let key, toVal, fromVal
  const keys = hasSymbol
    ? Reflect.ownKeys(from)
    : Object.keys(from)

  for (let i = 0; i < keys.length; i++) {
    key = keys[i]
    if (key === '__ob__') continue
    toVal = to[key]
    fromVal = from[key]
    if (!hasOwn(to, key)) {
      set(to, key, fromVal)
    } else if (
      toVal !== fromVal &&
      isPlainObject(toVal) &&
      isPlainObject(fromVal)
    ) {
      mergeData(toVal, fromVal)
    }
  }
  return to
}

获取from的属性名数组,遍历这个数组,如果当前key没在to中,调用Vue.prototype.$set将值添加到to中。如果属性值都是对象并且不相等,递归调用mergeData,最终返回to

Vue.$nextTick

代码定义在src/core/util/next-tick.js中,在定义nextTick方法之前,会初始化timerFunc变量

export let isUsingMicroTask = false

const callbacks = []
let pending = false
function flushCallbacks () {}

let timerFunc
// 如果当前环境支持 Promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // 如果当前环境支持 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)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // 如果当前环境支持 setImmediate
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // 上述都不支持
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

timerFunc始终是一个函数,只不过函数内容会根据当前环境支持API的情况设置不同值

如果支持Promise,创建一个Promise实例,并将isUsingMicroTask设置为true,代表使用微任务。在timerFunc中设置回调函数为flushCallbacksthen函数

如果不支持Promise,但是支持MutationObserver的话,创建一个回调函数为flushCallbacks的监听器和一个文本节点,监听这个文本节点;也会将isUsingMicroTask设置为true,代表微任务。timerFunc函数内部修改文本节点的属性,从而触发回调。

如果上面两个微任务都不支持,但是支持setImmediate的话,timerFunc函数内部调用setImmediate方法,回调函数为flushCallbacks,这是一个宏任务

如果上面三种方式都不支持,则timerFunc内部创建一个定时器,回调函数是flushCallbacks,延时时间为0

当调用Vue.$nextTick时,代码如下

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)
    }
  })
  if (!pending) {
    pending = true
    timerFunc()
  }
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

首先创建一个函数并将这个函数添加到callbacks中;如果pendingfalse,将pending设置为true,这样做的目的是在同一时刻,只有一个flushCallbacks函数等待执行。也就是说如果在同一时刻多次调用nextTick只会往callbacks中添加一个函数,而不会多次触发timerFunc

接下来调用timerFunc函数,timerFunc函数就是将flushCallbacks函数推入队列中(微任务优先)。如果没有传入cb,则返回一个PromisePromise内部将resolve赋值给_resolve ,也就是说调用_resolve()时,才会修改这个Promise的状态,而这个状态的修改也是发生在flushCallbacks中。

flushCallbacks函数如下

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

首先将pending置为false,表示当前队列没有flushCallbacks函数等待执行了。接下来就是遍历并执行callbacks数组中所有回调函数。

callbacks.push(() => {
  if (cb) {
    try {
      cb.call(ctx)
    } catch (e) {
      handleError(e, ctx, 'nextTick')
    }
  } else if (_resolve) {
    _resolve(ctx)
  }
})

首先判断有没有cb,如果有则调用cb,并且this指向ctx;如果没有cb,则调用_resolve,并将ctx传入。

综上,Vue.$nextTick就是根据当前环境支持的API设置回调的执行时机,微任务优先。

最后来一个小测试,考虑下打印结果

new Vue({
  el: '#app',
  template: `<div @click="change">
  {{title}}
  </div>`,
  data () {
    return {
      title: '我是标题'
    }
  },
  methods: {
    change () {
      this.$nextTick(() => {
        console.log(1, this.title)
      })
      this.title = 'test'
      this.$nextTick(() => {
        console.log(2, this.title)
      })
    }
  }
})

上述打印结果为

1, 'test'
2, 'test'

其他API原理对应文章中详细分析过,就直接贴下链接啦

Vue.prototype.$set

Vue 源码(二)响应式原理

Vue.extend

Vue 源码(一)如何创建VNode

Vue.component

Vue源码(八)异步组件原理