基本概念介紹:
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構造函數上。