天天看点

解析vue2.x源码之vue实例方法与全局API

基本概念介绍:

vue实例方法:vm.$set、vm.$del、vm.$nextTick等,挂在Vue.prototype上的方法。

全局API: Vue.directive、Vue.filter、Vue.component等,挂在Vue构造函数上的方法。

本章从源码角度分析,Vue.js是如何实现这些功能的

一、Vue实例方法的实现:

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) {
  // 限制Vue构造函数只能用 new Vue(options) 方式调用
  // 此时 this 是 Vue构造函数的实例,(this instanceof Vue) 为true
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  // 进行Vue实例的初始化,由于内容过多,这块会另起一章详细描述。
  this._init(options)
}
// 定义了_init方法,供Vue构造函数调用
initMixin(Vue)

// 以下四个方法,通过在Vue.prototype上添加方法,实现了Vue的实例方法
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue
           

stateMixin:

stateMixin函数实现了与数据相关的实例方法:vm.$set、vm.$del、vm.$watch

具体代码如下:

export function stateMixin (Vue: Class<Component>) {
  // 将set、del方法赋值给 Vue.prototype
  Vue.prototype.$set = set
  Vue.prototype.$delete = del

  // 利用watcher构造函数实现$watch
  Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      try {
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }
}
           

其中所用到的方法在之前的博客里已经详细写过,这里不再赘述

set、del方法不清楚的同学请看: 解析vue2.x源码之API $set、$del

watcher构造函数不清楚的请看: 解析vue2.x源码之Object与Array的变化侦测

eventsMixin:

eventsMixin函数实现了与事件相关的实例方法:vm.$on、vm.$once、vm.$off、vm.$emit

具体代码如下:

export function eventsMixin (Vue: Class<Component>) {
  // 注册事件,event可以是字符串或数组,字符串可直接注册,数组需循环取注册
  // 如 vm.$on('click', cb) 或 vm.$on(['click','mouseover'], cb)
  Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
    const vm: Component = this
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        vm.$on(event[i], fn)
      }
    } else {
      // 将注册事件存入 vm._events
      (vm._events[event] || (vm._events[event] = [])).push(fn)
    }
    return vm
  }

  // $once 注册的事件只会响应一次,响应后会取消监听该事件
  Vue.prototype.$once = function (event: string, fn: Function): Component {
    const vm: Component = this
    // 这里的操作称为函数劫持
    // 用回调函数On替代原本的回调函数fn,这样当事件触发时一样会调用fn,并且取消事件监听
    function on () {
      vm.$off(event, on)
      fn.apply(vm, arguments)
    }
    //on.fn这个属性是为了方便在外部$off取消监听
    on.fn = fn

    // 实际监听的是封装函数on
    vm.$on(event, on)
    return vm
  }

  // 取消事件监听
  Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
    const vm: Component = this
    // 如果没传参数,默认将所有监听清空
    if (!arguments.length) {
      vm._events = Object.create(null)
      return vm
    }
    // 如果event是一个数组,循环调用$off取消监听
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        vm.$off(event[i], fn)
      }
      return vm
    }
    // 如果event是字符串,取出event对应的事件数组。
    const cbs = vm._events[event]
    // 事件数组为空则函数结束
    if (!cbs) {
      return vm
    }
    // 事件数组不为空,fn为空,则默认情况该event对应的事件数组
    if (!fn) {
      vm._events[event] = null
      return vm
    }
    // 事件数组不为空,fn也不为空,遍历事件数组找出与fn相同的一项,移出数组,实现取消监听。
    let cb
    let i = cbs.length
    while (i--) {
      cb = cbs[i]
      // cb.fn === fn 这个条件是为了移除 $once注册的事件
      if (cb === fn || cb.fn === fn) {
        cbs.splice(i, 1)
        break
      }
    }
    return vm
  }

  // $emit负责从_events中找出事件对应的回调数组并执行
  Vue.prototype.$emit = function (event: string): Component {
    const vm: Component = this
    // 取出event对应的回调数组
    let cbs = vm._events[event]
    if (cbs) {
      // toArray去除event,保留剩下的参数
      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
  }
}
           

lifecycleMixin:

lifecycleMixin函数实现了与生命周期相关的实例方法:vm.$forceUpdate、vm.$destroy

具体代码如下:

export function lifecycleMixin (Vue: Class<Component>) {

  Vue.prototype.$forceUpdate = function () {
    const vm: Component = this
    // 调用组件watcher的update方法,强制组件重新渲染
    if (vm._watcher) {
      vm._watcher.update()
    }
  }

  Vue.prototype.$destroy = function () {
    const vm: Component = this
    // 如果已经在销毁过程中,则return,以免多次调用
    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)
    }
    // vm._watcher 是当前组件渲染函数的watcher
    // vm._watchers 是用户自定义的watcher
    // 详情可见watcher构造函数代码

    // 释放组件中的状态所收集的依赖
    if (vm._watcher) {
      vm._watcher.teardown()
    }
    // 释放用户所定义的watcher中状态收集的依赖
    let i = vm._watchers.length
    while (i--) {
      vm._watchers[i].teardown()
    }

    // 标识已销毁
    vm._isDestroyed = true
    // 调用__patch__,发现新vnode为null
    // 说明组件已销毁,这时触发vnode树的destory钩子函数
    vm.__patch__(vm._vnode, null)
    // 调用组件destroyed钩子中方法
    callHook(vm, 'destroyed')
    // 调用实例方法的$off方法,且不传任何参数,这时会将所有监听都取消,_event清空
    vm.$off()
  }
}
           

renderMixin :

renderMixin 函数实现了与渲染相关的实例方法:vm.$nextTick 

具体代码如下:

export function renderMixin (Vue: Class<Component>) {
  Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this)
  }
}
           

要了解nextTick首先需要了解事件循环,js中的任务分为同步任务与异步任务,异步任务又分为宏任务与微任务。

每次事件循环中,先优先执行同步任务,同步任务执行完后会到微任务队列中取出所有微任务执行,微任务全部执行完毕后再去宏任务队列取出一个宏任务执行,执行完再按这个顺序重复循环,这个循环就叫事件循环。

nextTick的作用是将调用nextTick的回调函数存入一个数组,在异步任务中遍历执行。

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'

export let isUsingMicroTask = false

// 用来存在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 timerFunc
// Promise 与 MutationObserver 属于微任务
// setImmediate 与 setTimeout 属于宏任务
// 优先使用微任务,如果运行环境不支持,则依次降级为宏任务
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数组
  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()
  }

  // 这里使为了支持 this.$nextTick().then(cb)这种调用方式。
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}
           

二、全局API的实现:

Vue.extend:

Vue.cid = 0
  let cid = 1

  /**
   * 传入options,返回一个Vue构造函数
   * 如:
   * let test = Vue.extend({
   *  template: '<p>{{word}}</p>',
   *  data: function() {
   *    return {
   *      word: 'Hello, world!'
   *    }
   *  }
   * })
   * new test().$mount('#app')
   */
  Vue.extend = function (extendOptions: Object): Function {
    extendOptions = extendOptions || {}
    // this 指向vue的构造函数
    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
    if (process.env.NODE_ENV !== 'production' && name) {
      validateComponentName(name)
    }
    // Sub为要返回的构造函数,函数内调用 _init进行Vue实例的初始化
    const Sub = function VueComponent (options) {
      this._init(options)
    }
    // 继承Vue构造函数
    Sub.prototype = Object.create(Super.prototype)
    // 修复constructor指针
    Sub.prototype.constructor = Sub
    Sub.cid = cid++
    // 合并父子options
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    Sub['super'] = Super

    // 代理,使访问vm.key 时, 返回vm._props.key
    if (Sub.options.props) {
      initProps(Sub)
    }
    // 代理,使访问vm.key 时, 返回用户自定义的computed的getter,下一章会详细讲解
    if (Sub.options.computed) {
      initComputed(Sub)
    }

    // 将Vue构造函数上的API赋给子构造函数
    // 分别有 extend、mixin、use、directive、filter、component
    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[SuperId] = Sub
    return Sub
  }
           

Vue.set、Vue.del、Vue.nextick:

这三个API都是复用之前定义过的函数,上文Vue的实例方法中已经讲过。

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

Vue.directive、Vue.filter、Vue.component:

这三个方法实现方式相似,所以源码中是放在一个数组中循环实现的。

以Vue.directive为例,

// Vue.directive(id, [definition])
// 传入id 与 definition,代表注册指令
Vue.directive('my-directive', {
    bind: function() {
        ...
    }
})

// 传入id,不传definition,代表取出指令
let myDirective = Vue.directive('my-directive', function() {
    bind: function() {
        ...
    }
})
           

实现源码:

/**
   * ASSET_TYPES = ['component','directive','filter']
   */
  ASSET_TYPES.forEach(function (type) {
    Vue[type] = function (
      id,
      definition
    ) {
      // 不传definition,代表取出
      if (!definition) {
        return this.options[type + 's'][id]
      } else {
        // 传入id 与definition,代表注册
        if (type === 'component') {
          // 注册component需要校验component名称
          validateComponentName(id);
        }
        if (type === 'component' && isPlainObject(definition)) {
          // 注册component,如果definition是object
          // 取出组件名,用Vue.extend,返回构造函数
          definition.name = definition.name || id;
          definition = Vue.extend(definition);
        }
        // 注册指令时,如果传入的是方法,则转为一个对象
        // 默认在bind与update钩子上调用这个函数
        if (type === 'directive' && typeof definition === 'function') {
          definition = { bind: definition, update: definition };
        }
        // 注册,存入options
        this.options[type + 's'][id] = definition;
        return definition
      }
    };
  });
           

Vue.use:

Vue.use = function (plugin: Function | Object) {
    // 取出已注册的插件数组
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    // 如果该插件已存在则return,避免重复注册
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }

    // 去掉第一个参数,即plugin本身
    const args = toArray(arguments, 1)
    // 将Vue构造函数插到第一个参数,供插件方法使用
    args.unshift(this)
    // 如果plugin有install方法,则调用install方法
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
      // 如果plugin没有install,且自身是一个函数,则将plugin视为install调用
      plugin.apply(null, args)
    }
    // 讲插件加入插件数组
    installedPlugins.push(plugin)
    return this
  }
           

Vue.mixin:

Vue.mixin用于全局混入,Vue.mixin方法注册后,会影响之后创建的每个Vue实例,因为该方法会更改Vue.options

Vue.mixin({
    created: function () {
        console.log(this.$options.word)
    }
})

new Vue({
    word: 'Hello, world!'
})
//Hello, world!
           

其实现原理就是将用户传入的options与Vue.options合并在一起

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

总结:

本篇介绍了Vue的实例方法与全局API,区别在于:实例方法定义在Vue.proyotype,全局API定义在Vue构造函数上。

继续阅读