其他章節請看:
例項方法(或 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:
- initMixin(Vue) -
_init()
- stateMixin(Vue) -
$data
、$props
、$set()
、$delete()
、$watch()
- eventsMixin(Vue) -
$on()
、$once()
、$off()
、$emit()
- lifecycleMixin(Vue) -
_update()
、$forceUpdate()
、$destroy()
- 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 新增全域性功能。外掛的功能範圍沒有嚴格的限制——一般有下面幾種:
- 新增全域性方法或者 property。如:vue-custom-element
- 新增全域性資源:指令/過濾器/過渡等。如 vue-touch
- 通過全域性混入來新增一些元件選項。如 vue-router
- 新增 Vue 例項方法,通過把它們新增到 Vue.prototype 上實現。
- 一個庫,提供自己的 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 =
:
- platforms\web\runtime\index.js
- platforms\weex\runtime\index.js
- 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})中增加編譯器的功能。
其他章節請看: