vue 快速入門 系列 —— 實例方法(或 property)和靜態方法

其他章節請看:

vue 快速入門 系列

實例方法(或 property)和靜態方法

在 Vue(自身) 項目結構 一文中,我們研究了 vue 項目自身構建過程,也知曉了 import Vue from 'core/index' 就是引入 vue 的核心程式碼,該文件的前兩行對應著 vue 的實例方法和靜態方法(或稱全局方法),本篇就和大家一起來看一下這兩部分。

// vuev2.5.20/src/core/index.js

// 返回 Vue 構造函數,並準備好實例方法
import Vue from './instance/index'
// 靜態方法(或稱全局方法)
import { initGlobalAPI } from './global-api/index'
...

Tip:vuev2.5.20 只是 vue 克隆到本地的項目名,下面將默認是這個項目。

實例方法(instance/index.js)

instance/index.js 的內容並不複雜,全部程式碼不到 30 行:

// src/core/instance/index.js 全部程式碼:

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)
}

// 初始化相關。每次調用 new Vue() 就會被執行,裡面包含很多初始化操作
initMixin(Vue)
// 狀態相關。數據相關更確切
stateMixin(Vue)
// 事件相關
eventsMixin(Vue)
// 生命周期相關
lifecycleMixin(Vue)
// 渲染相關
renderMixin(Vue)

export default Vue

首先定義 Vue 構造函數,接著調用initMixin()、stateMixin()等 5 個方法,給 Vue 的原型添加方法(或 property)。以下是 initMixin()、stateMixin() 的程式碼片段:

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    ...
  }
}
export function stateMixin (Vue: Class<Component>) {
  ...
  Object.defineProperty(Vue.prototype, '$data', dataDef)
  Object.defineProperty(Vue.prototype, '$props', propsDef)

  Vue.prototype.$set = set
  Vue.prototype.$delete = del

  Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    ...
  }
}

剩餘的三個方法也類似,也是給 Vue 的 prototype 增加方法。

以下是每個方法中在 Vue 的原型上定義的方法或 property:

  1. initMixin(Vue) - _init()
  2. stateMixin(Vue) - $data、$props、$set()、$delete()、$watch()
  3. eventsMixin(Vue) - $on()、$once()、$off()、$emit()
  4. lifecycleMixin(Vue) - _update()、$forceUpdate()、$destroy()
  5. renderMixin(Vue) - $nextTick()、_render()

接著,我們將逐一分析其中的實現。

initMixin(初始化相關)

此方法僅僅給 Vue 定義了一個原型方法,即 _init()。每次調用 new Vue() 就會被執行。

function Vue (options) {
  ...
  this._init(options)
}

如果你看過 jQuery 的源碼,裡面也有一個類似的方法,是 init。

var jQuery = function( selector, context ) {
	// 返回jQuery對象
	return new jQuery.fn.init( selector, context, rootjQuery );
}

jQuery.fn = jQuery.prototype = {
	constructor: jQuery,
	init: function( selector, context, rootjQuery ) {
    // 很多邏輯的處理
    ...
  }
  ...
}

每次執行 jQuery(jqOptions) 都會調用 new jQuery.fn.init(),裡面有非常多的處理,所以我們可以給 jqOptions 傳遞多種不同類型的參數。

同樣的,new Vue() 也支援多種參數,所以這個 _init() 裡面也會做很多事情:

// 核心程式碼
Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // 合併參數
    if (options && options._isComponent) {
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    // 初始化生命周期、初始化事件...
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    // 觸發生命鉤子:beforeCreate 
    callHook(vm, 'beforeCreate')
    // resolve injections before data/props
    // 我們可以使用的順序:inject -> data/props -> provide
    initInjections(vm) 
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    if (vm.$options.el) {
      // 掛載
      vm.$mount(vm.$options.el)
    }
  }

_init() 方法依次會合併參數、接著對生命周期、事件等做初始化,其中也會依次觸發生命周期鉤子 beforeCreate 和 created,最後根據條件進行掛載。

stateMixin(數據相關)

stateMixin(Vue) 中 給 Vue 的原型定義的方法(或 property)有:$data、$props、$set()、$delete()、$watch()。

vm.$data、vm.$props

vm.$data ,返回 Vue 實例觀察的數據對象。

vm.$props,返回當前組件接收到的 props 對象。

vm.$set()、vm.$delete() 和 vm.$watch()

$set、$delete 和 $watch 都是與數據相關。出現的背景和原理分析請看 偵測數據的變化 - [vue api 原理]

eventsMixin(事件相關)

eventsMixin(Vue) 中定義的的原型方法有:$on、$once、$off、$emit。分別是註冊事件,註冊只觸發一次的事件、解除事件、觸發自定義事件。

vm.$on()

這四個方法,$on() 應該是核心,只有先註冊事件,才能解綁事件,或觸發事件。

用法如下:

vm.$on('test', function (msg) {
  console.log(msg)
})
vm.$emit('test', 'hi')
// => "hi"

以下是 $on() 的源碼,核心功能就是「收集事件和對應的回調」:

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 {
    // 將回調存入 event 對應的數組中
    (vm._events[event] || (vm._events[event] = [])).push(fn)
    ...
  }
  return vm
}
vm.$once()

$once(),註冊只觸發一次的事件。

如果你看過 jQuery 源碼(事件系統),也能猜到 $once 應該是基於 $on 來實現:

Vue.prototype.$once = function (event: string, fn: Function): Component {
    const vm: Component = this
    // 回調的代理:先註銷事件,在觸發
    function on () {
      vm.$off(event, on)
      fn.apply(vm, arguments)
    }
    on.fn = fn
    // 在 $on 的基礎上實現。給回調設置一個代理
    vm.$on(event, on)
    return vm
  }
vm.$off()

$off(),解除事件綁定。

邏輯比較簡單:支援沒有傳參的情況、傳參是數組、解綁指定事件、解綁指定事件的回調。請看源碼:

Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
  const vm: Component = this
  // 如果沒有提供參數,則移除所有的事件監聽器
  if (!arguments.length) {
    // Object.create(null) 創建一個原型為null的空對象,以此方式來清空事件
    vm._events = Object.create(null)
    return vm
  }
  // 數組,則依次解除事件綁定
  if (Array.isArray(event)) {
    for (let i = 0, l = event.length; i < l; i++) {
      vm.$off(event[i], fn)
    }
    return vm
  }
  // specific event
  // 解綁指定的事件
  const cbs = vm._events[event]
  // 沒有回調,直接返回
  if (!cbs) {
    return vm
  }
  // 未指定要解綁的事件的回調,則直接清空該事件的所有回調
  if (!fn) {
    vm._events[event] = null
    return vm
  }
  // 解綁事件指定的回調
  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.$emit()

$emit(),觸發當前實例上的事件。

取得回調數組,依次觸發回調。請看源碼:

Vue.prototype.$emit = function (event: string): Component {
  const vm: Component = this
  ...
  // 取得回調數組
  let cbs = vm._events[event]
  if (cbs) {
    cbs = cbs.length > 1 ? toArray(cbs) : cbs
    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(Vue) 中給 Vue 定義的原型方法有:_update()、$forceUpdate()、$destroy()。

vm.$forceUpdate()

迫使 Vue 實例重新渲染。注意它僅僅影響實例本身和插入插槽內容的子組件,而不是所有子組件。

程式碼出奇的少:

// $forceUpdate() 的全部程式碼

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

關鍵應該就是 vm._watcher.update()

通過全局搜索,發現 vm._watcher 賦值是在 watcher.js 的構造函數中(行{1}):

// src/core/observer/watcher.js
export default class Watcher {
  vm: Component;
  ...

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this // {1}
    }

於是我們知道 vm 的 _watcher 是一個 Watcher。而根據 偵測數據的變化 - [基本實現] 中對 Watcher 的研究,我們寫過這樣一段程式碼:

class Watcher{
    ...
    // 數據改變時收到通知,然後再通知到外界
    update(newVal, oldVal){
        this.callback(newVal, oldVal)
    }
}

調用 update(),會通知到外界。這裡的外界或許就是 vm,然後 vm 會做一些事情,其中或許就包含重新渲染。

vm.$destroy()

$destroy(),完全銷毀一個實例。清理它與其它實例的連接,解綁它的全部指令及事件監聽器。請看源碼:

Vue.prototype.$destroy = function () {
  const vm: Component = this
  // 防止重複調用
  if (vm._isBeingDestroyed) {
    return
  }
  // 觸發鉤子:beforeDestroy
  callHook(vm, 'beforeDestroy')
  vm._isBeingDestroyed = true
  // remove self from parent
  // 從父元素中刪除自己
  const parent = vm.$parent
  // 有父元素 & 父元素沒有開始被刪除 & 不是抽象的?
  if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
    remove(parent.$children, vm)
  }
  // teardown watchers
  if (vm._watcher) {
    // teardown,取消觀察。也就是從所有依賴(Dep)中把自己刪除
    vm._watcher.teardown()
  }
  // 解綁用戶通過 vm.$watch 添加的監聽。
  let i = vm._watchers.length
  while (i--) {
    vm._watchers[i].teardown()
  }
  // remove reference from data ob
  // frozen object may not have observer.
  if (vm._data.__ob__) {
    vm._data.__ob__.vmCount--
  }
  // call the last hook...
  // vm 已被銷毀
  vm._isDestroyed = true
  // invoke destroy hooks on current rendered tree
  // 在當前渲染的樹上調用銷毀鉤子
  vm.__patch__(vm._vnode, null)
  // 調用鉤子:destroyed
  callHook(vm, 'destroyed')
  // 解綁所有事件
  vm.$off()
  // remove __vue__ reference
  if (vm.$el) {
    vm.$el.__vue__ = null
  }
  // release circular reference (#6759)
  // 釋放循環引用 (#6759)
  if (vm.$vnode) {
    vm.$vnode.parent = null
  }
}

Tip:vm._watchers 是在 Watcher 的構造函數中添加元素的,而在 vm.$watch 方法中就有 new Watcher(),所以推測 vm._watchers 中的內容來自 vm.$watch。

export default class Watcher {
  vm: Component;
  ...

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    ...
  }
}
Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
    ...
    const watcher = new Watcher(vm, expOrFn, cb, options)
    ...
  }

renderMixin(渲染相關)

renderMixin(Vue) 中給 Vue 定義的原型方法有:$nextTick()、_render()。

vm.$nextTick()

$nextTick(),將回調延遲到下次 DOM 更新循環之後執行

例如:

new Vue({
  // ...
  methods: {
    // ...
    example: function () {
      // 修改數據
      this.message = 'changed'
      // DOM 還沒有更新
      this.$nextTick(function () {
        // DOM 現在更新了
        // `this` 綁定到當前實例
        this.doSomethingElse()
      })
    }
  }
})

根據用法我們可以猜測 $nextTick() 會將我們我們的回調先保存起來,然後再合適的時間再觸發。請看源碼:

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

程式碼應該在 nextTick() 中:

// next-tick.js
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)
    }
  })
  //感覺這裡有秘密 {1}
  if (!pending) {
    pending = true
    timerFunc()  
  }
  ...
}

nextTick() 中,首先將我們的回調保存起來,將回調方法封裝到一個匿名函數,然後再存入 callbacks 數組中。

什麼時候觸發回調?感覺行{1}處有秘密。事實證明我猜對了,請看源碼:

// next-tick.js 中相關程式碼如下:

// 存放回調函數
const callbacks = []
let pending = false

// 依次執行回調函數
function flushCallbacks () {
  pending = false
  // 備份數組 callbacks。
  // 防止遍歷回調時,對 callbacks 修改
  const copies = callbacks.slice(0)
  // 清空數組 callbacks
  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)
    // ios 兼容處理 Promise
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  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
  // window.setImmediate,該特性是非標準的,請盡量不要在生產環境中使用它!
  // ie支援, 作用類似於 setTimeout(fn, 0)
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // 回退到 setTimeout。
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

timerFunc 是一個函數,會根據條件選擇不同的非同步方法封裝。這裡有4種非同步方法,Promise.then 和 MutationObserver 是微任務,後兩種(setImmediate 和 setTimeout)是宏任務。

已第一種為例:

timerFunc = () => {
  p.then(flushCallbacks)
  // ios 兼容處理 Promise
  if (isIOS) setTimeout(noop)
}

當我們執行 timerFunc() 時,就會立刻執行 p.then(flushCallbacks),flushCallbacks() 方法會依次執行回調函數,但 flushCallbacks() 方法只會在合適的時間被觸發(即事件循環中)。

最後我們總結下:執行 this.$nextTick(fn) ,將進入 nextTick(),首先將我們的回調函數用匿名函數封裝起來,在將這個匿名函數扔在 callbacks 中,因為沒有代辦事項(let pending = false),於是執行 timerFunc()。

Tip: 有關事件循環、微任務和宏任務的介紹可以看這裡

靜態方法(global-api/index.js)

global-api/index.js 文件程式碼總共約 70 行,除去通過 import 導入的 20 行,剩餘的都在 initGlobalAPI() 方法中:

export function initGlobalAPI (Vue: GlobalAPI) {
  // config
  const configDef = {}
  configDef.get = () => config
  ...
  Object.defineProperty(Vue, 'config', configDef)

  // 暴露的 util 方法。注意:這些不被視為公共 API 的一部分 - 避免依賴
  Vue.util = {
    ...
  }

  // vm.$set 是 Vue.set 的別名。
  // delete 和 nextTick 類似
  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick

  // 2.6 explicit observable API
  // 讓一個對象可響應
  Vue.observable = <T>(obj: T): T => {
    observe(obj)
    return obj
  }

  Vue.options = Object.create(null)
  // ASSET_TYPES = [ 'component', 'directive', 'filter' ]
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })

  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options._base = Vue

  extend(Vue.options.components, builtInComponents)

  // 定義了一個方法:Vue.use
  initUse(Vue)
  // 定義了一個方法:Vue.mixin()
  initMixin(Vue)
  // 初始化繼承。定義了一個方法:Vue.extned()
  initExtend(Vue)
  // 初始化資產註冊。定義了三個方法:Vue.component()、Vue.directive()和Vue.filter()
  initAssetRegisters(Vue)
}

接著我們就源碼的順序依次分析。

Vue.set()、Vue.delete()和Vue.nextTick()

  • Vue.set() 是 vm.$set() 的別名

  • Vue.delete() 是 vm.$delete 的別名

  • Vue.nextTick() 是 vm.$nextTick() 的別名

Vue.observable()

程式碼很少:

Vue.observable = <T>(obj: T): T => {
    observe(obj)
    return obj
  }

直接看官網介紹:

用法:讓一個對象可響應。Vue 內部會用它來處理 data 函數返回的對象。

返回的對象可以直接用於渲染函數和計算屬性內,並且會在發生變更時觸發相應的更新。也可以作為最小化的跨組件狀態存儲器,用於簡單的場景:

const state = Vue.observable({ count: 0 })

const Demo = {
  render(h) {
    return h('button', {
      on: { click: () => { state.count++ }}
    }, `count is: ${state.count}`)
  }
}

initUse

// src/core/global-api/use.js

export function initUse (Vue: GlobalAPI) {
  Vue.use = function (plugin: Function | Object) {
    // 按照了的插件
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    // 如果已經安裝了此插件,就直接返回 Vue
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }

    // additional parameters
    // 附加參數
    const args = toArray(arguments, 1)
    // this 插入第一位
    args.unshift(this)
    // 插件的 install 屬性 如果是函數,則執行 install
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args)
    // 如果 plugin 本身是函數,就執行 plugin
    } else if (typeof plugin === 'function') {
      plugin.apply(null, args)
    }
    // 將該插件放入已安裝的插件列表中
    installedPlugins.push(plugin)
    return this
  }
}
Vue.use()

initUse(Vue) 中只定義了 Vue.use() 這個全局方法。根據前面的源碼分析,此方法的作用應該是安裝插件。我們查閱 api 確認一下。

用法:安裝 Vue.js 插件。如果插件是一個對象,必須提供 install 方法。如果插件是一個函數,它會被作為 install 方法。install 方法調用時,會將 Vue 作為參數傳入。

Tip:到底什麼是插件?官網:插件通常用來為 Vue 添加全局功能。插件的功能範圍沒有嚴格的限制——一般有下面幾種:

  1. 添加全局方法或者 property。如:vue-custom-element
  2. 添加全局資源:指令/過濾器/過渡等。如 vue-touch
  3. 通過全局混入來添加一些組件選項。如 vue-router
  4. 添加 Vue 實例方法,通過把它們添加到 Vue.prototype 上實現。
  5. 一個庫,提供自己的 API,同時提供上面提到的一個或多個功能。如 vue-router

這樣說來,插件的用處挺大的。

Tip:與 jQuery 有些類似,jQuery 也提供了方法(如 jQuery.extend = jQuery.fn.extend = function() {})來擴展核心功能。

initMixin

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

initMixin(Vue) 中只定義了一個全局方法 Vue.mixin。

mixin 有點類似 Object.assign 的味道。我們看一下官網的介紹:

用法:全局註冊一個混入,影響註冊之後所有創建的每個 Vue 實例。不推薦在應用程式碼中使用。

我們在看一下它的用法(來自官網):

// 為自定義的選項 'myOption' 注入一個處理器。
Vue.mixin({
  created: function () {
    var myOption = this.$options.myOption
    if (myOption) {
      console.log(myOption)
    }
  }
})

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

根據用法和源碼,我們能推測出,Vue.mixin 就是將我們的參數合併到 Vue(this.options) 中,後續創建 Vue 實例時這些參數就會有體現。

initExtend

Vue.extend()

initExtend(Vue) 中只定義了 Vue.extend() 這個全局方法。

extend 這個方法名,又讓我想起 Object.assign、jQuery.extend。讓我們先看一下 api 如何介紹:

用法:使用基礎 Vue 構造器,創建一個「子類」。參數是一個包含組件選項的對象。

好像猜錯了。根據用法介紹,Vue.extend 的作用更像 es6 中 Class 的 extends。直接分析其源碼再次驗證我們的猜測:

Vue.extend = function (extendOptions: Object): Function {
  extendOptions = extendOptions || {}
  // Super 指向 Vue
  const Super = this
  const SuperId = Super.cid
  // 快取相關 {1}
  const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
  if (cachedCtors[SuperId]) {
    return cachedCtors[SuperId]
  }

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

  // 定義一個子類 Sub
  const Sub = function VueComponent (options) {
    this._init(options)
  }
  // 以 Vue 的原型作為新建對象的原型,並將新建對象設置為 Sub 的原型
  // 用 new Sub() 創建的對象也能訪問到 Vue.prototype 中的方法或屬性
  Sub.prototype = Object.create(Super.prototype)
  Sub.prototype.constructor = Sub
  Sub.cid = cid++
  // 合併參數
  Sub.options = mergeOptions(
    Super.options,
    extendOptions
  )
  // 新增 super 屬性,指向 Vue
  Sub['super'] = Super

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

  // 允許進一步的擴展/混合/插件使用
  Sub.extend = Super.extend
  Sub.mixin = Super.mixin
  Sub.use = Super.use

  ...

  // 在擴展時保留對超級選項的引用。
  Sub.superOptions = Super.options
  Sub.extendOptions = extendOptions
  Sub.sealedOptions = extend({}, Sub.options)

  // 快取構造函數
  cachedCtors[SuperId] = Sub
  // 返回子類 Sub
  return Sub
}

執行 Vue.extend() 將返回一個子類 Sub,該子類繼承了父級(Vue)。為了提高性能,裡面增加了快取機制(行{1})。

initAssetRegisters

initAssetRegisters(Vue) 中定義了三個全局方法。

export function initAssetRegisters (Vue: GlobalAPI) {
  /**
   * Create asset registration methods.
   * 創建資產註冊方法。
   */
  ASSET_TYPES.forEach(type => {
    Vue[type] = function (
      id: string,
      definition: Function | Object
    ): Function | Object | void {
      ...
    }
  })
}

ASSET_TYPES 是一個數組:

export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]

Tip:如果你看過 jQuery,你也會發現有類似的寫法,也就是當多個方法的邏輯相似,就可以寫在一起,顯得精簡。

Vue.component()、Vue.directive()和Vue.filter()

直接看源碼有點難懂(注釋可稍後再看):

ASSET_TYPES.forEach(type => {
  Vue[type] = function (
    id: string,
    definition: Function | Object
  ): Function | Object | void {
    // 沒有傳第二個參數,表示獲取。比如獲取指令
    if (!definition) {
      return this.options[type + 's'][id]
    } else {
      // 組件的特別處理
      if (type === 'component' && isPlainObject(definition)) {
        definition.name = definition.name || id
        definition = this.options._base.extend(definition)
      }
      // 指令的特殊處理
      if (type === 'directive' && typeof definition === 'function') {
        definition = { bind: definition, update: definition }
      }
      // 與獲取對應,這裡應該就是註冊。例如註冊指令、組件或過濾器
      this.options[type + 's'][id] = definition
      return definition
    }
  }
})

我們翻閱一下對應 api:

  • Vue.filter( id, [definition] ) - 註冊或獲取全局過濾器。
// 註冊
Vue.filter('my-filter', function (value) {
  // 返回處理後的值
})

// getter,返回已註冊的過濾器
var myFilter = Vue.filter('my-filter')
  • Vue.directive() - 註冊或獲取全局指令。

  • Vue.component() - 註冊或獲取全局組件

核心就是註冊或獲取。感覺作用比較簡單,有點類似註冊事件,取得事件的回調。現在我們在來看一下源碼和相應的注釋,會發現容易理解多了。

Tip:組件、指令、過濾器當然不是回調這麼簡單,所以我們可以猜測:這裡只是負責註冊和獲取,至於如何生效,卻不在此處。

分布它處的方法

vm.$mount()

如果 Vue 實例在實例化時沒有收到 el 選項,則它處於「未掛載」狀態,沒有關聯的 DOM 元素。可以使用 vm.$mount() 手動地掛載一個未掛載的實例。

項目中搜索 $mount ,發現有三個文件對其有定義 Vue.prototype.$mount =:

  1. platforms\web\runtime\index.js
  2. platforms\weex\runtime\index.js
  3. platforms\web\entry-runtime-with-compiler.js

前兩個文件是根據不同的平台(web或weex)對 $mount 方法的不同的定義。

而最後一個文件有些意思,請看源碼:

// entry-runtime-with-compiler.js

// 函數劫持。保存原始 $mount
const mount = Vue.prototype.$mount    // {1}
// 對原始 $mount 進行封裝。主要做的工作是將模板編譯成渲染函數
Vue.prototype.$mount = function (     // {2}
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)
  ...
  const options = this.$options
  // resolve template/el and convert to render function
  // 解析模板/el並轉換為渲染函數
  if (!options.render) {
    ...
  }
  return mount.call(this, el, hydrating)
}

在 Vue(自身) 項目結構 一文中,我們知道 entry-runtime-with-compiler.js 生成的文件是完整版本(dist/vue.js),完整版本是包含編譯器的,而我們在 模板 一文中也知曉了編譯器的主要作用,將模板編譯成渲染函數。

所以我們能推測出這個 $mount(行{2}),就是在原始 $mount(行{1})中增加編譯器的功能。

其他章節請看:

vue 快速入門 系列

Tags: