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构造函数,分别调用了initMixin、stateMixin、eventsMixin、lifecycleMixin和renderMixin五个函数。这五个函数的作用是向Vue的原型挂载方法。

以initMixin为例

export function initMixin (Vue) {
  Vue.prototype._init = function (options) {
    // 做些什么
  }
}

当函数initMixin被调用时,会向Vue构造函数的prototype属性添加 _init方法。执行new Vue()时,会调用 _init方法,该方法实现了一系列初始化操作,包括整个生命周期的流程以及响应式系统流程的启动等。

# 数据相关的实例方法

与数据相关的实例方法有:vm.$watch、vm.$set和vm.$delete

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

export function stateMixin (Vue) {
  Vue.prototype.$set = set
  Vue.prototype.$delete = del
  Vue.prototype.$watch = function (expOrFn, cb, options) {}
}

# 事件相关的实例方法

与事件相关的实例方法有:vm.$on、vm.$once、vm.$off和vm.$emit

# vm.$on

vm.$on(event, callback)

参数:

{string | Array} event {Function} callback

用法:

监听当前实例上的自定义事件,事件可以由vm.$emit触发。回调函数会接收所有传入事件所触发的函数的额外参数。

示例:

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 {
//在执行new Vue()时,Vue会执行this._init方法进行一系列初始化操作,其中就会在Vue.js的实例上创建一个 _events属性,用来存储事件
    (vm._events[event] || (vm._events[event] = [])).push(fn)
  }
  return vm
}

# vm.$off

vm.$off([event, callback])

参数:

  • {string | Array} event
  • {Function} callback

用法:移除自定义事件监听器。

  • 如果没有提供参数,则移除所有的事件监听器。
  • 如果只提供了事件,则移除该事件所有的监听器。
  • 如果同时提供了事件与回调,则只移除这个回调的监听器。
Vue.prototype.$off = function (event, fn) {
  const vm = this
  // 处理没有提供参数的情况
  if (!arguments.length) {
    vm._events = Object.create(null)
    return vm
  }



  // 处理event为数组的情况
  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] = null
    return vm
  }



  //处理提供了event和回调的情况 只移除与fn相同的监听器
  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} event
  • {Function} callback

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

实现的思路:vm.$once中调用vm.$on来实现监听自定义事件的功能,当自定义事件触发后会执行拦截器,将监听器从事件列表中移除

Vue.prototype.$once = function (event, fn) {
  const vm = this
  function on () {
    vm.$off(event, on)
    fn.apply(vm, arguments)
  }
  on.fn = fn
  vm.$on(event, on)
  return vm
}

# vm.$emit

vm.$emit( event, [...args] )

参数:

  • {string} event
  • [...args]

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

Vue.prototype.$emit = function (event) {
  const vm = this
  // 取出事件监听器回调函数列表
  let cbs = vm._events[event]
  if (cbs) {
    // toArray的作用是将类似于数组的数据转换成真正的数组
    // [].slice.call(arguments, 1)
    const args = toArray(arguments, 1)
    for (let i = 0, l = cbs.length; i < l; i++) {
      try {
        cbs[i].apply(vm, args)
      } catch (e) {
        handleError(e, vm, `event handler for "${event}"`)
      }
    }
  }
  return vm
}

# 生命周期相关的实例方法

相关的方法有vm.$mount、vm.$forceUpdate、vm.$nextTick和vm.$destroy。

export function lifecycleMixin (Vue) {
  Vue.prototype.$forceUpdate = function () {
    // 做点什么
  }

  Vue.prototype.$destroy = function () {
    // 做点什么
  }
}

export function renderMixin (Vue) {
  Vue.prototype.$nextTick = function (fn) {
    // 做点什么
  }
}

vm.$mount方法则是在跨平台的代码中挂载到Vue构造函数的prototype属性上的

# vm.$forceUpdate

vm.$forceUpdate的原理:手动执行实例watcher的update方法,使Vue.js实例重新渲染

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

vm.$forceUpdate仅仅影响实例本身以及插入插槽内容的子组件,而不是所有子组件。

# vm.$destroy

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

Vue.prototype.$destroy = function () {
  const vm = this
  // 防止vm.$destroy被反复执行
  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)
  }


  // 从watcher监听的所有状态的依赖列表中移除watcher
  if (vm._watcher) {
    vm._watcher.teardown()
  }
  // Vue.js提供了vm.$watch方法,它允许用户监听某个状态。因此,还需要销毁用户使用vm.$watch所创建的watcher实例
  let i = vm._watchers.length
  while (i--) {
    // 每当创建watcher实例时,都将watcher实例添加到vm._watchers中,当用户使用vm.$watch时,会在vm._watchers中添加一个watcher实例,通过vm._watchers就可以得到所有watcher实例,只需要遍历vm._watchers并依次执行每一项watcher实例的teardown方法,就可以将watcher实例从它所监听的状态的依赖列表中移除
    vm._watchers[i].teardown()
  }
  // Vue.js实例添加 _isDestroyed属性来表示Vue.js实例已经被销毁
  vm._isDestroyed = true
  // 在vnode树上触发destroy钩子函数解绑指令
  vm.__patch__(vm._vnode, null)
  // 触发destroyed钩子函数
  callHook(vm, 'destroyed')
  // 移除所有的事件监听器
  vm.$off()
}

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

# vm.$nextTick

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

当更新了状态(数据)后,需要对新DOM做一些操作,但是这时我们其实获取不到更新后的DOM,因为还没有重新渲染。这个时候我们需要使用nextTick方法

  1. 为什么vue.js使用异步更新策略

Vue.js 2.0开始使用虚拟DOM进行渲染,变化侦测的通知只发送到组件,组件内用到的所有状态的变化都会通知到同一个watcher,然后虚拟DOM会对整个组件进行“比对(diff)”并更改DOM。也就是说,如果在同一轮事件循环中有两个数据发生了变化,那么组件的watcher会收到两份通知,从而进行两次渲染。事实上,并不需要渲染两次,虚拟DOM会对整个组件进行渲染,所以只需要等所有状态都修改完毕后,一次性将整个组件的DOM渲染到最新即可。

要解决这个问题,Vue.js的实现方式是将收到通知的watcher实例添加到队列中缓存起来,并且在添加到队列之前检查其中是否已经存在相同的watcher,只有不存在时,才将watcher实例添加到队列中。然后在下一次事件循环(event loop)中,Vue.js会让队列中的watcher触发渲染流程并清空队列。这样就可以保证即便在同一事件循环中有两个状态发生改变,watcher最后也只执行一次渲染流程。

  1. 什么是事件循环

JS是一门单线程脚本语言,JS在执行过程中只有一个主线程处理任务,当执行到异步任务时会将这个任务放到事件队列中,被放到事件队列中的任务不会立即执行回到,而是等当前执行栈中所有同步任务执行完毕后,去检查微任务队列中是否有事件存在,如果存在就依次执行微任务队列中事件对应的回调,执行完后去宏任队列中取出一个任务加入到当前执行栈中,当执行栈的任务执行完后,再去微任务队列检查是否有事件,如此循环这个循环就叫事件循环。

异步任务分两种类型:

  • 宏任务
    • setTimeout
    • setInterval
    • setImmediate
    • MessageChannel
    • requestAnimationFrame
    • I/O
    • UI交互事件
    • ...
  • 微任务
    • Promise.then
    • MutationObserver
    • Object.observe
    • process.nextTick
    • ...
  1. 什么是执行栈

当我们执行一个方法时,JavaScript会生成一个与这个方法对应的执行环境(context),又叫执行上下文。这个执行环境中有这个方法的私有作用域、上层作用域的指向、方法的参数、私有作用域中定义的变量以及this对象。这个执行环境会被添加到一个栈中,这个栈就是执行栈。

如果在这个方法的代码中执行到了一行函数调用语句,那么JavaScript会生成这个函数的执行环境并将其添加到执行栈中,然后进入这个执行环境继续执行其中的代码。执行完毕并返回结果后,JavaScript会退出执行环境并把这个执行环境从栈中销毁,回到上一个方法的执行环境。这个过程反复进行,直到执行栈中的代码全部执行完毕。这个执行环境的栈就是执行栈。

注意:修改数据一定要放在nextTick的前面,否则会得不到最新的DOM

new Vue({
  // ……
  methods: {
    // ……
    example: function () {
      // 如果是先使用vm.$nextTick注册回调,然后修改数据,则在微任务队列中先执行使用vm.$nextTick注册的回调,然后执行更新DOM的回调。所以在回调中得不到最新的DOM,因为此时DOM还没有更新
      // 先使用nextTick注册回调
      this.$nextTick(function () {
        // DOM没有更新
      })
      // 然后修改数据
      this.message = 'changed'
    }
  }
})

Vue原型上的 $nextTick方法只是调用了nextTick方法

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

// Vue原型上的 $nextTick方法调用了nextTick方法
Vue.prototype.$nextTick = function (fn) {
  return nextTick(fn, this)
}

nextTick方法的实现

// 用来存储vm.$nextTick参数中提供的回调。
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 microTimerFunc
const p = Promise.resolve()
// 使用Promise.then将flushCallbacks添加到微任务队列中。
microTimerFunc = () => {
  p.then(flushCallbacks)
}

export function nextTick (cb, ctx) {
  callbacks.push(() => {
    if (cb) {
      cb.call(ctx)
    }
  })
  if (!pending) {
    pending = true
    microTimerFunc()
  }
}

// 测试一下
nextTick(function () {
  console.log(this.name) // f
}, {name: 'f'})

# vm.$mount

vm.$mount( [elementOrSelector] )

参数:{Element | string} [elementOrSelector]

返回值:vm,即实例自身

用法: 如果Vue.js实例在实例化时没有收到el选项,则它处于“未挂载”状态,没有关联的DOM元素。我们可以使用vm.$mount手动挂载一个未挂载的实例。如果没有提供elementOrSelector参数,模板将被渲染为文档之外的元素,并且必须使用原生DOM的API把它插入文档中。这个方法返回实例自身,因而可以链式调用其他实例方法。

例子:

var MyComponent = Vue.extend({
  template: '<div>Hello!</div>'
})

// 创建并挂载到#app(会替换#app)
new MyComponent().$mount('#app')

// 创建并挂载到#app(会替换#app)
new MyComponent({ el: '#app' })

// 或者,在文档之外渲染并且随后挂载
var component = new MyComponent().$mount()
document.getElementById('app').appendChild(component.$el)

在完整的构建版本中vm.$mount会检查模板是否已经转换成渲染函数。如果没有则进入编译过程,将模板编译成渲染函数,再进入挂载与渲染的流程。

运行时版本的vm.$mount默认实例上已存在渲染函数,如果不存在就设置一个。这个渲染函数在执行时会返回一个空节点的VNode,保证正常执行。

  1. 完整版vm.$mount的实现原理
// 函数劫持,可以在原始功能上扩展一些其他功能
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (el) {
  // 获取el
  el = el && query(el)

  // 可以获取到一些用户设置的参数
  const options = this.$options

  // 是否存在渲染函数,不存在则将模板编译成渲染函数
  if (!options.render) {
    let template = options.template
    // 
    if (template) {
      if (typeof template === 'string') {
        // 如果template是字符串且以#开头,则看作为选择器
        if (template.charAt(0) === '#') {
          // 获取DOM元素后将它的innerHTML作为模板
          template = idToTemplate(template)
        }
      } else if (template.nodeType) {
        // 如果是DOM元素则使用DOM元素的innerHTML作为模板
        template = template.innerHTML
      } else {
        // 如果template不是字符串也不是DOM元素,会触发警告
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      // 如果用户没有提供template配置项,则从用户提供的el选项中获取模板
      template = getOuterHTML(el)
    }

    // 编译相关逻辑
    if (template) {
      // 通过执行compileToFunctions函数将模板编译成渲染函数
      const { render } = compileToFunctions(
        template,
        {...},
        this
      )
      // 将渲染函数设置到this.$options上
      options.render = render
    }
  }

  return mount.call(this, el)
}

function query (el) {
  if (typeof el === 'string') {
    // 如果el是字符串(有可能是选择器)则通过querySelector获取元素
    const selected = document.querySelector(el)
    
    // 如果获取不到则创建一个空div标签
    if (!selected) {
      return document.createElement('div')
    }
    return selected
  } else {
    // 如果el不是字符串则认为是元素类型
    return el
  }
}

// 返回DOM元素的HTML字符串
function getOuterHTML (el) {
  if (el.outerHTML) {
    return el.outerHTML
  } else {
    const container = document.createElement('div')
    container.appendChild(el.cloneNode(true))
    return container.innerHTML
  }
}

// 获取DOM元素后将它的innerHTML作为模板
function idToTemplate (id) {
  const el = query(id)
  return el && el.innerHTML
}

function compileToFunctions (template, options, vm) {
  // 让options成为可选参数
  options = extend({}, 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)
}
  1. 只包含运行时版本的vm.$mount的实现原理
Vue.prototype.$mount = function (el) {
  el = el && inBrowser ? query(el) : undefined
  // 将ID转换为DOM元素后,使用mountComponent函数将Vue.js实例挂载到DOM元素上
  return mountComponent(this, el)
}

export function mountComponent (vm, el) {
  // 是否存在渲染函数
  if (!vm.$options.render) {
    // 不存在,则设置一个默认的渲染函数createEmptyVNode,该渲染函数执行后,会返回一个注释类型的VNode节点,并在开发环境中给出警告
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      // 在开发环境发出警告
    }
  }
  // 触发生命周期钩子
  callHook(vm, 'beforeMount')

  // 执行挂载操作,挂载是持续性的,挂载之后,每当状态发生变化时,都会进行渲染操作
  vm._watcher = new Watcher(vm, () => {
    // _update调用虚拟DOM中的patch方法来执行节点的比对与渲染操作
    // _render的作用是执行渲染函数,得到一份最新的VNode节点树
    // 这行代码的作用是先调用渲染函数得到一份最新的VNode节点树,然后通过 _update方法对最新的VNode和上一次渲染用到的旧VNode进行对比并更新DOM节点
    vm._update(vm._render())
  }, noop)

  // 触发mounted钩子函数
  callHook(vm, 'mounted')
  return vm
}

watcher观察数据的过程:

状态通过Observer转换成响应式之后,每当触发getter时,会从全局的某个属性中获取watcher实例并将它添加到数据的依赖列表中。watcher在读取数据之前,会先将自己设置到全局的某个属性中。而数据被读取会触发getter,所以会将watcher收集到依赖列表中。收集好依赖后,当数据发生变化时,会向依赖列表中的watcher发送通知。

由于Watcher的第二个参数支持函数,所以当watcher执行函数时,函数中所读取的数据都将会触发getter去全局找到watcher并将其收集到函数的依赖列表中。也就是说,函数中读取的所有数据都将被watcher观察。这些数据中的任何一个发生变化时,watcher都将得到通知。

# 全局API的实现原理

Vue.extend( options )

参数:{Object} options

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

例子:

<div id="mount-point"></div>
// 创建构造器
var Profile = Vue.extend({
  template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
  // data在Vue.extend()必须是函数
  data: function () {
    return {
      firstName: 'Walter',
      lastName: 'White',
      alias: 'Heisenberg'
    }
  }
})
// 创建Profile实例,并挂载到一个元素上
new Profile().$mount('#mount-point')

代码:

let cid = 1

Vue.extend = function (extendOptions) {
  extendOptions = extendOptions || {}
  const Super = this
  const SuperId = Super.cid
  const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})

  // 缓存,如果已经存在,直接返回缓存中的结果
  if (cachedCtors[SuperId]) {
    return cachedCtors[SuperId]
  }

  const name = extendOptions.name || Super.options.name

  // 如果发现name选项不合规发出警告
  if (process.env.NODE_ENV !== 'production') {
    if (!/^[a-zA-Z][\w-]*$/.test(name)) {
      warn(
        'Invalid component name: "' + name + '". Component names ' +
        'can only contain alphanumeric characters and the hyphen, ' +
        'and must start with a letter.'
      )
    }
  }


  const Sub = function VueComponent (options) {
    this._init(options)
  }

  // 父类的原型继承到子类中,继承Vue的能力
  Sub.prototype = Object.create(Super.prototype)
  Sub.prototype.constructor = Sub

  // 为子类添加cid,它表示每个类的唯一标识
  Sub.cid = cid++

  // 合并了父类选项与子类选项
  // mergeOptions方法会将两个选项合并为一个新对象
  Sub.options = mergeOptions(
    Super.options,
    extendOptions
  )
  // 将父类保存到子类的super属性中
  Sub['super'] = Super

  // 如果选项中存在props属性,则初始化它,初始化props的作用是将key代理到 _props中。例如,vm.name实际上可以访问到的是Sub.prototype._props.name
  if (Sub.options.props) {
    initProps(Sub)
  }

  // 初始化computed
  if (Sub.options.computed) {
    initComputed(Sub)
  }

  // 将父类中存在的属性依次复制到子类中
  Sub.extend = Super.extend
  Sub.mixin = Super.mixin
  Sub.use = Super.use

  // ASSET_TYPES = ['component', 'directive', 'filter']
  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)

  // 使用父类的id作为缓存的key,将子类缓存在cachedCtors中
  cachedCtors[SuperId] = Sub
  return Sub
}

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

function proxy (target, sourceKey, key) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

// 将computed对象遍历一遍,并将里面的每一项都定义一遍
function initComputed (Comp) {
  const computed = Comp.options.computed
  for (const key in computed) {
    // computed对象中的每一项进行定义
    defineComputed(Comp.prototype, key, computed[key])
  }
}
Vue.nextTick( [callback, context] )

参数:

  • {Function} [callback]
  • {Object} [context]

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

示例:

// 修改数据
vm.msg = 'Hello'
// DOM还没有更新
Vue.nextTick(function () {
// DOM更新了
})

// 作为一个Promise使用(这是Vue.js 2.1.0版本新增的)
Vue.nextTick()
.then(function () {
  // DOM更新了
})

实现原理和前面的vm.$nextTick一样

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

Vue.nextTick = nextTick
Vue.set( target, key, value )

参数:

  • {Object | Array} target
  • {string | number} key
  • {any} value

返回值:设置的值

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

Vue.set与前面介绍的vm.$set的实现原理相同

import { set } from '../observer/index'
Vue.set = set
Vue.delete( target, key )

参数:

  • {Object | Array} target
  • {string | number} key/index

用法:删除对象的属性。如果对象是响应式的,确保删除能触发更新视图。这个方法主要用于避开Vue.js不能检测到属性被删除的限制。

Vue.delete与vm.$delete的实现原理相同

import { del } from '../observer/index'
Vue.delete = del

# Vue.directive

除了内置指令外,Vue还允许注册自定义指令

Vue.directive( id, [definition] )

参数:

  • {string} id
  • {Function | Object} [definition]

用法:注册或获取全局指令。

示例:

// 注册
Vue.directive('my-directive', {
  bind: function () {},
  inserted: function () {},
  update: function () {},
  componentUpdated: function () {},
  unbind: function () {}
})

// 注册(指令函数)
Vue.directive('my-directive', function () {
  // 这里将会被bind和update调用
})

// getter方法,返回已注册的指令
var myDirective = Vue.directive('my-directive')

代码:

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

Vue.directive = function (id, definition) {
  // 如果definition参数不存在,则使用id从this.options ['directives'] 中读出指令并将它返回
  if (!definition) {
    return this.options['directives'][id]
  } else {
    // 如果definition参数存在,则说明是注册操作,那么进而判断definition参数的类型是否是函数
    if (typeof definition === 'function') {
      // 如果是函数监听bind和update两个事件
      definition = { bind: definition, update: definition }
    }
    // 如果definition不是函数,则说明它是用户自定义的指令对象,此时不需要做任何操作,直接将用户提供的指令对象保存在this.options['directives'] 上
    this.options['directives'][id] = definition
    return definition
  }
}

# Vue.filter

Vue.filter( id, [definition] )

参数:

  • {string} id
  • {Function | Object} [definition]

用法:注册或获取全局过滤器。

// 注册
Vue.filter('my-filter', function (value) {
  // 返回处理后的值
})

// getter方法,返回已注册的过滤器
var myFilter = Vue.filter('my-filter')

过滤器可以用在两个地方:双花括号插值和v-bind表达式,由“管道”符号指示。

<!-- 在双花括号中 -->
{{ message | capitalize }}

<!-- 在v-bind中 -->
<div v-bind:id="rawId | formatId"></div>

代码

Vue.options['filters'] = Object.create(null)

Vue.filter = function (id, definition) {
  if (!definition) {
    return this.options['filters'][id]
  } else {
    this.options['filters'][id] = definition
    return definition
  }
}

# Vue.component

Vue.component( id, [definition] )

参数:

  • {string} id
  • {Function | Object} [definition] 用法:注册或获取全局组件。注册组件时,还会自动使用给定的id设置组件的名称。
// 注册组件,传入一个扩展过的构造器
Vue.component('my-component', Vue.extend({ /* ... */ }))

// 注册组件,传入一个选项对象(自动调用Vue.extend)
Vue.component('my-component', { /* ... */ })

// 获取注册的组件(始终返回构造器)
var MyComponent = Vue.component('my-component')

代码:

// 新增components属性用于存放组件
Vue.options['components'] = Object.create(null)
// 
Vue.component = function (id, definition) {
  if (!definition) {
    // 如果definition不存在,则使用id从this.options['components'] 中读出组件并将它返回
    return this.options['components'][id]
  } else {
    // definition参数支持两种参数,分别是选项对象和构造器。组件其实是一个构造函数,是使用Vue.extend生成的子类,需要将参数definition统一处理成构造器
    if (isPlainObject(definition)) {
      // 如果选项对象中没有设置组件名,则自动使用给定的id设置组件的名称
      definition.name = definition.name || id
      // 调用Vue.extend方法将它变成Vue的子类
      definition = Vue.extend(definition)
    }
    this.options['components'][id] = definition
    return definition
  }
}

Vue源码中Vue.directive、Vue.filter和Vue.component是放在一起实现的

Vue.options = Object.create(null)
// ASSET_TYPES = ['component', 'directive', 'filter']
ASSET_TYPES.forEach(type => {
  Vue.options[type + 's'] = Object.create(null)
})
ASSET_TYPES.forEach(type => {
  Vue[type] = function (id, definition) {
    if (!definition) {
      return this.options[type + 's'][id]
    } else {
      if (type === 'component' && isPlainObject(definition)) {
        definition.name = definition.name || id
        definition = Vue.extend(definition)
      }
      if (type === 'directive' && typeof definition === 'function') {
        definition = { bind: definition, update: definition }
      }
      this.options[type + 's'][id] = definition
      return definition
    }
  }
})

# Vue.use

Vue.use( plugin )

参数:{Object | Function} plugin

用法:安装Vue.js插件。如果插件是一个对象,必须提供install方法。如果插件是一个函数,它会被作为install方法。调用install方法时,会将Vue作为参数传入。install方法被同一个插件多次调用时,插件也只会被安装一次。

Vue.use = function (plugin) {
  const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))

  // 判断插件是不是已经被注册过
  if (installedPlugins.indexOf(plugin) > -1) {
    return this
  }

  // 其他参数
  const args = toArray(arguments, 1)

  // 将Vue添加到args列表的最前面,保证install方法被执行时第一个参数是Vue,其余参数是注册插件时传入的参数。
  args.unshift(this)
  
  if (typeof plugin.install === 'function') {
    plugin.install.apply(plugin, args)
  } else if (typeof plugin === 'function') {
    plugin.apply(null, args)
  }

  // 插件添加到installedPlugins中,保证相同的插件不会反复被注册
  installedPlugins.push(plugin)
  return this
}

# Vue.mixin

Vue.mixin( mixin )

参数:{Object} mixin

用法:全局注册一个混入(mixin),影响注册之后创建的每个Vue.js实例。插件作者可以使用混入向组件注入自定义行为(例如:监听生命周期钩子)。不推荐在应用代码中使用。

// 为自定义的选项myOption注入一个处理器
Vue.mixin({
  created: function () {
    var myOption = this.$options.myOption
    if (myOption) {
      console.log(myOption)
    }
  }
})

new Vue({
  myOption: 'hello!'
})
// => "hello!"

实现原理

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

export function initMixin (Vue) {
  Vue.mixin = function (mixin) {
    // mergeOptions方法会将用户传入的mixin与this.options合并成一个新对象,然后将这个生成的新对象覆盖this.options属性,这里的this.options其实就是Vue.options
    this.options = mergeOptions(this.options, mixin)
    return this
  }
}

mixin方法修改了Vue.options属性,之后创建的每个实例都会用到该属性,会影响创建的每个实例。但也正是因为有影响,所以mixin在某些场景下才堪称神器。

# Vue.compile

Vue.compile( template )

参数:{string} template

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

示例:

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

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

代码:

// Vue.compile方法只需要调用编译器就可以实现功能
Vue.compile = compileToFunctions

# Vue.version

作用:提供字符串形式的Vue.js安装版本号。这对社区的插件和组件来说非常有用,你可以根据不同的版本号采取不同的策略。

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

if (version === 2) {
  // Vue.js v2.x.x
} else if (version === 1) {
  // Vue.js v1.x.x
} else {
  // 不支持的Vue.js版本
}

Vue.js在构建文件的配置中定义了 VERSION 常量,使用rollup-plugin-replace插件在构建的过程中将代码中的常量 VERSION 替换成package.json文件中的版本号