实例方法与全局API

本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2021-05-23

在Vuejs中有这样一段代码:

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

其中定义了Vue构造函数,然后分别调用了 initMixinstateMixineventsMixinlifecycleMixinrenderMixin 5个函数,并将Vue构造函数传给这5个函数,这些函数的作用就是向Vue的原型中挂载方法或属性或进行一些初始化操作。

事件相关的实例方法

与事件相关的实例方法有四个,vm.$onvm.$oncevm.$offvm.$emit,这四个方法是在 eventsMixin 中挂载到 Vue 构造函数的 prototype 属性中的,其代码如下:

export function eventsMixin(Vue) {
  Vue.prototype.$on = function (event, fn) {}
  // ...
  // 对应的 $once $off $emit
}

vm.$on

监听当前实例上的自定义事件,事件可以由vm.$emit触发。

// 使用示例 vm.$on(event, callback)
// 参数 
// { string | Array<string> } event
// { Function } callback   
vm.$on('test', function(msg) {
  console.log(msg);
})

vm.$emit('test', 'hi');
// => "hi"

内部实现:

Vue.prototype.$on = function (event, fn) {
  const vm = this;

  if (Array.isArray(event)) {
    for (let i = 0, l = event.length; i < l; i++) {
      this.$on(event[i], fn)
    }
  } else {
    ((vm._events[event]) || (vm._events[event] = [])).push(fn)
  }
}

其中 vm._events 是一个用来对象用来存储事件,在执行 new Vue() 会执行 this._init 方法进行一系列操作,其中就会在Vue实例上创建一个 _events 属性用来存储事件。

vm._events = Object.create(null);

vm.$off

用于移除自定义事件监听器

// 使用示例 vm.$off(event, callback)
// 参数 
// { string | Array<string> } event
// { Function } callback   

Vue.prototype.$off = function(event, fn) {
  const vm = this;
  // 未传任何参数,则清空整个事件列表
  if (!arguments.length) {
    vm._events = Object.create(null)
    return vm
  }

  if (Array.isArray(event)) {
    for (let i = 0, l = event.length; i < l; i++) {
      this.$off(event[i], fn)
    }
    return vm
  }

  const cbs = vm._events[event]
  if (!cbs) {
    return vm
  }

  // 只传事件名,只移除该事件的所有监听器
  if (arguments.length === 1) {
    vm._events[event] = []
    return vm
  }

  if (fn) {
    const cbs = vm._events[event]
    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.$once

用于监听一个自定义事件,但是只触发一次,在第一次触发之后移除监听器。

// 使用示例 vm.$once(event, callback)
// 参数 
// { string | Array<string> } event
// { Function } callback   

Vue.prototype.$once = function (event, fn) {
  const vm = this;
  function on () {
    vm.$off(event, on)
    // 需要注意的是 这里的 arguments 是事件被触发时,调用on的参数列表,而不是 $once的参数列表
    fn.apply(vm, arguments)
  }

  // 需要注意的是 提供给$on注册的fn是我们自定义的拦截器on函数 
  // 已不是用户调用 $once时提供的fn,如果用户调用 $off(event, fn) 时就移除不了
  // 所以这里要把原来用户提供的fn挂载到on上,在$off的时候也就需要判断 cb.fn === fn
  on.fn = fn
  vm.$on(event, on)
  return vm
}

vm.$emit

用于触发当前实例上的事件,附加参数都会传递给监听器回调。

// 使用示例 vm.$emit(event, [...args])
// 参数 
// { string } event
// { any } args   

Vue.prototype.$emit = function (event) {
  const vm = this
  let cbs = vm._events[event]
  if (cbs) {
    const args = toArray(arguments, 1)
    for (let i = 0, l = cbs.length; i < l; i++) {
      try {
        cbs[i].apply(vm, args)
      } catch {
        handleError(e, vm, `event handler for "${event}"`)
      }
    }
  }
  return vm
}

生命周期相关的实例方法

与生命周期相关的实例方法有4个,分别是 vm.$mountvm.$forceUpdatevm.$nextTickvm.$destroy

其中 vm.$forceUpdatevm.destroy 是在 lifecycleMixin 中挂载到Vue.prototype上的,vm.$nextTick 是在 renderMixin 中挂载到Vue.prototype上的。而 vm.$mount 是在跨平台的代码中挂载到Vue.prototype上的。

vm.$forceUpdate

用于迫使Vuejs实例重新渲染。

Vue.prototype.$forceUpdate = function () {
  const vm = this;
  if (vm._watcher) {
    vm._watcher.update()
  }
}

vm._watcher 就是 Vuejs 实例的Watcher,所以只需要主动执行实例Watcher的update方法,就可以让实例重新渲染。

vm.$destroy

vm.$destroy 的作用是完全销毁一个实例,它会清理该实例与其他实例的连接,并解绑全部指令及监听器,同时会触发 beforeDestroydestroyed 的钩子函数。

Vue.prototype.$destroy = function () {
  const vm = this
  if (vm._isBeingDestroyed) { // 正在被销毁,不需要重复执行
    return
  }

  callHook(vm, 'beforeDestroy')
  vm._isBeingDestroyed = true

  // 删除自己与父级之间的连接
  const parent = vm.$parent
  if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) { // 存在父组件且父组件没有被销毁且自身不是抽象组件
    remove(parent.$children, vm)
  }
  
  // 从watcher监听的所有状态的依赖列表中移除watcher
  if (vm._watcher) {
    vm._watcher.teardown()
  }

  let i = vm._watchers.length

  while (i--) {
    vm._watchers[i].teardown()
  }

  vm._isDestroyed = true
  // 在vnode树上触发destroy钩子函数解绑指令
  vm.__patch__(vm._vnode, null)
  // 触发destroyed钩子函数
  callHook(vm, 'destroyed')
  // 移除当前实例的所有事件监听
  vm.$off()
}

function remove(arr, item) {
  if (arr.length) {
    const index = arr.indexOf(item)
    if (index > 1) {
      return arr.splice(index, 1)
    }
  }
}

vm.$nextTick

$nextTick接受一个回调函数作为参数,它的作用是将回调延迟到下次DOM更新周期之后执行,与全局的Vue.nextTick一样,不同的是回调的this自动绑定到调用它的实例上,如果没有提供回调且在支持Promise环境中,则返回一个Promise。

在Vuejs中,当状态发生变化时,watcher 会得到通知,然后触发虚拟DOM的渲染流程,而Watcher触发渲染的这个操作并不是同步的,而是异步的。Vuejs中有一个队列,每当需要渲染时会将Watcher推送到这个队列中,在下一次事件循环中再让Watcher触发渲染的流程。

为什么Vuejs使用异步更新队列?

当状态发生变化,变化侦测的通知只发送到组件级别,组件内用到所有的状态,当组件中多个状态发生变化的时候会通知的会是同一个Watcher(组件级别的Watcher),如果在一个事件循环中有多个数据发生了变化,那么组件Watcher就会收到多份通知,从而进行多次渲染,而事实上,并不需要渲染多次,只需要等所有的状态都修改完毕后,一次性将整个组件的DOM渲染到最新即可。

因此会将收到的Watcher缓存起来,并且在添加到队列之前检查其中是否已经存在相同的Watcher,只有不存在时,才将Watcher实例添加到队列中,然后在下一次事件循环中,Vuejs会让队列中的Watcher触发渲染流程并清空队列,这样就可以保证即便在同一事件循环中有两个状态发生改变,Watcher最后也只执行一次渲染流程。

因此当我们想要获取更新之后的DOM时,就需要使用到$nextTick,需要注意顺序问题,因为不论是更新DOM的回调还是调用vm.$nextTick来获取更新后的DOM,都是向微任务队列中添加任务,所以哪个任务先添加到队列中,就先执行哪个任务。

new Vue({
  // ...
  template: `
    <div>
      <p id='msg'>{{ msg }}</p>
      <p id='text'>{{ text }}</p>
    </div>
  `,
  data() {
    return {
      msg: '',
      text: '',
    }
  },
  methods: {
    
    handler() {
      this.$nextTick(() => { // 回调被添加到队列中 1
        // DOM未更新
        console.log(document.querySelector('#msg').innerText) // ''
      })
      this.msg = 'hello' // 改变状态 watcher添加到队列中 2
      this.$nextTick(() => { // 回调被添加到队列中 3
        // DOM更新了 
        console.log(document.querySelector('#msg').innerText) // hello
      })

      // 三个微任务的执行顺序就是 1、2、3
      // 所以应该是先改变值,再使用nextTick注册回调
      // 如果顺序反了,就会先执行nextTick回调,再执行watcher异步渲染DOM 
      // 就不是期望的DOM更新时了 

      // 但是可以使用setTimeout将获取更新ODM后的处理逻辑添加到下一个宏任务队列中
      setTimeout(() => {
        // DOM更新了 
        console.log(document.querySelector('#text').innerText) // 你好
      }, 0);
      this.text = '你好'

    }
  },
})

vm.$nextTickVue.nextTick 是相同的,所以 nextTick 的具体实现并不是在Vue原型上的 $nextTick方法中,而是抽象成了 nextTick 方法供两个方法共用。

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

Vue.prototype.$nextTick = function(fn) {
  return nextTick(fn, this)
}

// 需要注意的是,在一轮事件循环中,vm.$nextTick 只会向任务队列添加一个任务 
// 多次使用vm.$nextTick只会将回调添加到回调列表中缓存起来
// 当任务触发时 依次执行列表中的所有回调并清空列表

在Vuejs2.4版本之前,nextTick 方法在任何地方都使用微任务,但是微任务优先级太高,在某些场景可能会出现问题。

所以在Vuejs2.5版本中,采用了宏任务和微任务结合使用的方式,可强制采用宏任务。

可是,这样依然存在一些问题,比如状态在重绘之前被改变时会产生一些微妙的问题,在事件处理程序中使用宏任务也会导致一些奇怪的问题,所以在之后的版本中,依旧优先采用微任务的方式,只有在微任务不支持的情况下,才使用宏任务。

import { noop } from 'shared/util' // noop就是一个空函数
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'

export let isUsingMicroTask = false

const callbacks = []
let pending = false

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0) // 复制
  callbacks.length = 0 // 清空
  for (let i = 0; i < copies.length; i++) {
    copies[i]() // 调用
  }
}

let timerFunc

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]'
)) {
  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)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

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的作用就是在下一次事件循环时执行flushCallbacks清空掉事件队列(循环执行)
  // 初始pending为false,添加第一个任务后,pending为true
  // 然后调用timerFunc准备在下次循环执行flushCallbacks
  // 如果在本次循环中,多次调用 nextTick,此时 pending还为true只会进行队列的push
  // 只有 flushCallbacks在下次循环前被执行完,队列被清空后,pending才置为false
  // 等待下一个循环的第一个任务被添加时,又置为 true ...
  if (!pending) {
    pending = true
    timerFunc()
  }

  // 如果没有传值回调,且环境支持Promise则返回一个Promise
  // example: 
  // this.$nextTick().then(() => console.log('DOM更新了'))
  if (!cb && typeof Promise !== 'undefined') { 
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

vm.$mount

如果在实例化Vuejs时设置了el选项,会自动把Vuejs实例挂载到DOM元素上。如果在实例化的时候没有设置el选项,则它处于”未挂载“状态,没有关联的DOM,这时候使用vm.$mount手动挂载一个未挂载的实例。

如果没有提供参数,模板将会被渲染为文档之外的元素,并且使用原生DOM的API把它插入文档中,这个方法返回实例自身,因此可以链式调用其他实例方法。

参考 官方文档生命周期图示 可以知道,template 选项的优先级最高,其次是 el 选项。

// 用法 vm.$mount([elementOrSelector]) 接受一个DOM元素或者一个元素选择器作为参数(挂载对象) 
var MyComponent = Vue.extend({
  template: '<div>Hello</div>'
})
new MyComponent().$mount('#app') // 挂载到 #app上

// 或者 不提供参数,返回一个类fragment,手动插入
var component = new MyComponent().$mount()
document.getElementById('app').appendChild(component.$el)

之前提到过,完整版和只包含运行时版本之间的差异在于是否有编译器,而是否有编译器的差异主要在于 vm.$mount 方法的表现形式。

在完整的构建版本中,vm.$mount 会首先检查 templateel 选项所提供的模板是否已经转换成渲染函数 render,如果没有,则立即进入编译过程,将模板编译成渲染函数,完成之后再进入挂载与渲染的流程中。

只包含运行时的版本中,vm.$mount 主要就是负责“挂载”,没有编译步骤,它会默认实例上已经存在渲染函数,如果不存在,则会设置一个执行时会返回一个空节点的VNode渲染函数,以保证执行时不会因为函数不存在而报错。

同时,开发环境下Vuejs会触发警告,提示我们当前使用的是只包含运行时版本,会让我们提供渲染函数,或者去使用完整的构建版本。

完整的构建版本的vm.$mount的实现代码如下:

// 函数劫持
// 使用mount存储原始$mount
// 然后在原始只支持挂载的功能上新增一些其他功能,这个新方法再调用mount

const mount = Vue.prototype.$mount // 只包含运行时的版本的mount
Vue.prototype.$mount = function(el) {
  el = el && query(el)

  // 是否存在渲染函数
  const options = this.$options
  if (!options.render) { // 1、是否存在render函数
    // 获取模板
    let template = options.template
    if (template) { // 2、template选项是否存在
      if (typeof template === 'string') { // 2.1、template 是不是字符串
        if (template.charAt(0) === '#') {  // 2.1.1、template选项是一个id选择器
          template = idToTemplate(template)
        }
        // 2.1.2 如果 template 存在,且是字符串,且不是以#开头,直接走第4步流程

      } else if (template.nodeType) { // 2.2、template选项是一个DOM节点
        template = template.innerHTML
      } else { // 2.3 都不是,抛出警告
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) { // 3、是否传递了有效的el参数
      template = getOuterHTML(el)
    }
  }

  // 4、将模板编译成渲染函数并赋值给 options.render
  if (template) {
    const { render } = compileToFunctions(template, { /* ... */ }, this)
    options.render = render
  }
  return mount.call(this, el)
}

function idToTemplate() {
  const el = query(id)
  return el && el.innerHTML
}

function query(el) {
  if (typeof el === 'string') {
    const selected = document.querySelector(el)
    if (!selected) {
      return document.createElement('div')
    }
    return selected
  } else {
    return el
  }
}

function getOuterHTML(el) {
  if (el.outerHTML) {
    return el.outerHTML
  } else {
    const container = document.createElement('div')
    container.appendChild(el.cloneNode(true))
    return container.innerHTML
  }
}

将模板编译成代码字符串并将其转换成渲染函数的过程是在compileToFunctions函数中实现的,该函数的内部实现如下:

function compileToFunctions(template, options, vm) {
  options = extend({}, options) // 混合,让options称为可选参数

  // 检查缓存 是否存在编译后的模板
  const key = options.delimiters ? String(options.delimiters) + template : template
  if (cache[key]) {
    return cache[key]
  }

  // 编译
  const compiled = compile(template, options) // 前面提到的将模板编译成代码字符串

  // 将代码字符串转换为函数
  const res = {}
  res.render = createFunction(compiled.render)
  return (cache[key] = res)
}

function createFunction(code) {
  return new Function(code)
}

2、只包含运行时版本的的vm.$mount

只负责挂载,包含了 vm.$mount 方法的核心功能。

Vue.prototype.$mount = function (el) {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el)
}

function mountComponent(vm, el) {
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      // 发出警告
    }
  }

  // 触发hook
  callHook(vm, 'beforeMount')

  // 挂载
  vm._watcher = new Watcher(vm, () => {
    vm._update(vm._render())
  }, noop)

  // 触发hook
  callHook(vm, 'mounted')
  return vm
}

_update调用虚拟DOM中的patch方法来执行节点的对比与渲染操作,_render的作用是执行渲染函数得到一份最新的vnode节点树。

所以 vm._update(vm._render()) 的作用就是将最新的 vnode 节点树和上一次用到的旧vnode进行对比并更新DOM节点,简单的说就是执行了渲染操作。

全局API的实现原理

Vue.extend

使用基础Vue构造器创建一个子类,其参数是一个包含组件选项的对象。

// 使用示例
var Profile = Vue.extend({ // data选项必须是函数
  template: '<p>{{ firstName }} {{ lastName }} aka {{ alias }}</p>',
  data: function() {
    return {
      firstName: 'jack',
      lastName: 'ma',
      alias: 'xcjs'
    }
  }
})

new Profile().$mount('#mount-ele')

全局API与实例方法是不同的,实例API都是挂载在 Vue.prototype,而全局API都是挂载在 Vue 构造函数上。

Vue.cid = 0
let cid = 1 // 用于缓存的key值 由于相同的选项应该返回相同的子类,所以需要对于返回进行缓存 

// 主要做的事情就是将全局的一些选项(Super.xxx)复制到子类上
Vue.extend = function (extendOptions) {
  extendOptions = extendOptions || {}
  const Super = this
  const SuperId = Super.cid
  const cacheCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
  if (cacheCtors[SuperId]) {
    return cacheCtors[SuperId]
  }

  const name = extendOptions.name || Super.options.name
  if (process.env.NODE_ENV !== 'production') {
    if (!/^[a-zA-Z][\w-]*$/.test(name)) {
      warn('Invalid component name ... ')
    }
  }

  const Sub = function VueComponent(options) {
    this._init(options)
  }
  // 继承
  Sub.prototype = Object.create(Super.prototype)
  Sub.prototype.constructor = Sub
  Sub.cid = cid++ // Sub.cid 还是 1

  // 将父类的options选项继承到子类 并将父类保存到子类的Super属性
  Sub.options = mergeOptions(Super.options, extendOptions)
  Sub['Super'] = Super

  // 对于 Props 和 Computer属性,在扩展时在Vue实例上定义代理getter
  // 使得访问vm.name代理为 Sub.prototype._props.name  
  if (Sub.options.props) {
    initProps(Sub)
  }

  if (Sub.options.computed) {
    initComputed(Sub)
  }

  // 复制父类中存在的方法
  Sub.extend = Super.extend
  Sub.mixin = Super.mixin
  Sub.use = Super.use

  // ASSETS_TYPES = ['component', 'directive', 'filter']
  ASSETS_TYPES.forEach(function(type) {
    Sub[type] = Super[type]
  })

  if (name) {
    Sub.options.component[name] = Sub
  }

  // 新增属性
  Sub.superOptions = Super.options
  Sub.extendOptions = extendOptions
  Sub.sealedOptions = extend({}, Sub.options)

  cacheCtors[SuperId] = Sub // 使用父类的id作为缓存的key
  return Sub
}

function initProps (Comp) {
  const props = Comp.options.props
  for (const key in props) {
    proxy(Comp.prototype, `_props`, key)
  }
}

function initComputed (Comp) {
  const computed = Comp.options.computed
  for (const key in computed) {
    defineComputed(Comp.prototype, key, computed[key])
  }
}

Vue.nextTick

在下一次DOM更新结束之后执行延迟回调,修改数据之后立即使用这个方法获取更新后的DOM

与之前提到的 vm.$nextTick一样都是使用 nextTick 函数

// Vue.nextTick([callback, context])
import { nextTick } from '../util/index'

Vue.nextTick = nextTick

Vue.set

设置对象的属性,如果对象是响应式的,确保属性创建后也是响应式的,同时触发视图更新,这个方法主要用于避开Vue不能检测属性被添加的限制。与 vm.$set 实现原理一致。

Vue.delete

删除对象的属性,如果对象是响应式的,确保删除能够触发视图更新,这个方法主要用于避开Vue不能检测属性被删除的限制。与 vm.$delete 实现原理一致。

Vue.directive / Vue.filter / Vue.component

注册或获取全局指令,注意是注册和获取,而不是让指令生效,注册和获取需要做的事是将指令保存在某个位置,而让指令生效是从某个位置拿出来并执行它。

// Vue.directive(id, [definition])
// { String } id 
//  { Function | Object } definition

Vue.options = Object.create(null)
Vue.options['directives'] = Object.create(null) // 保存指令的位置

Vue.directive = function(id, definition) {
  if (!definition) { // 获取
    return this.options['directives'][id]
  } else { // 注册
    if (typeof definition === 'function') {
      definition = { bind: definition, update: definition }
    }
    this.options['directives'][id] = definition
    return definition
  }
}

同理,Vue.filter 用来注册或获取全局过滤器,Vue.component 用来注册或获取全局组件,所以可以封装到一起。

export function initAssetRegisters (Vue: GlobalAPI) {
  // ASSETS_TYPES = ['component', 'directive', 'filter']
  ASSET_TYPES.forEach(type => {
    Vue[type] = function (id, definition) {
      if (!definition) {
        return this.options[type + 's'][id]
      } else {
        // 无效name处理
        if (process.env.NODE_ENV !== 'production' && type === 'component') {
          validateComponentName(id)
        }
        if (type === 'component' && isPlainObject(definition)) { // isPlainObject 判断是不是对象 [object Object]
          definition.name = definition.name || id // 如果注册组件时没有设置组件名,则自动使用给定的id设置组件的名称
          definition = this.options._base.extend(definition) // Component其实是一个构造函数,所以需要将传入的 definition 处理成构造器,将其变成Vue的子类
        }
        if (type === 'directive' && typeof definition === 'function') {
          definition = { bind: definition, update: definition }
        }
        this.options[type + 's'][id] = definition
        return definition
      }
    }
  })
}

Vue.use

安装插件,如果插件是一个对象,必须提供 install 方法。如果插件是一个对象,它会被当做 install 方法,调用 install 方法会将 Vue 作为参数传入,插件只能被安装一次,多次调用没有意义。

Vue.use = function (plugin) {
  const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
  if (installedPlugins.indexOf(plugin) > -1) { // 只能安装一次
    return this
  }
  
  const args = toArray(arguments, 1) // 除plugin外的其他参数
  args.unshift(this) // 将this即Vue补充到参数列表中
  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.mixin

全局注册一个混入(mixin),影响注册之后的每个Vue.js实例(组件也是实例),插件作者可以使用混入向组件注入自定义行为,如监听生命周期钩子。

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

export function initMixin(Vue) {
  Vue.mixin = function(mixin) {
    // this.options 就是 Vue.options 之后创建的每个实例都会用到该属性
    this.options = mergeOptions(this.options, this)
    return this
  }
}

Vue.compile

编译模板字符串并返回包含渲染函数的对象,只在完整版中才有效。

var res = Vue.compile(`<div><span>{{ msg }}</span></div>`)

new Vue({
  data: {
    msg: 'hello'
  },
  render: res.render
})

其实现方式就是之前提到过的 compileToFunctions:

Vue.compile = compileToFunctions;

Vue.version

提供字符串式的Vuejs安装版本号。

// 使用场景

var version = Number(Vue.version.split('.')[0])

if (version === 2) {
  // Vuejs v2.x.x
} else if (version === 1) {
  // Vuejs v1.x.x
} else {
  // ...
}

具体的实现步骤是:Vuejs在构建文件的配置中定义了 __VERSION__ 常量,使用 rollup-plugin-replace 插件在构建的过程中将代码的常量 __VERSION__ 替换成 pkg.version


Vue.version = '__VERSION__'

// 构建之后
Vue.version = '2.5.2'