天天看點

解析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構造函數上。

繼續閱讀