早上好!繼續學習Vue原始碼~這次我們來學習 event 事件。
原始碼簡析
其實看了前兩篇的同學已經知道原始碼怎麼找了,這裡再提一下。
先找到Vue核心原始碼index方法 src/core/instance/index.js
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)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
index方法中定義了一個Vue的建構函式執行 _init
方法初始化,然後執行了多個 xxxMixin
方法,這些方法是為Vue 的建構函式定義各類屬性的。比如我們今天關注的事件,Vue的幾個事件方法都是在 eventsMixin
中定義的。
export function eventsMixin (Vue: Class<Component>) {
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
……
}
Vue.prototype.$once = function (event: string, fn: Function): Component {
……
}
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
……
}
Vue.prototype.$emit = function (event: string): Component {
……
}
}
另外要注意的是,initMixin
方法中定義了Vue的初始化方法 _init
,該方法中對Vue各類屬性進行了初始化。
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
initProxy(vm)
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, `beforeCreate`)
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, `created`)
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
所以,在本篇部落格中只需要關注 initEvents
和 eventsMixin
方法即可
initEvents
初始化過程很簡單,清空資料,並初始化連線父級的事件。
// src/core/instance/events.js
export function initEvents (vm: Component) {
vm._events = Object.create(null)
vm._hasHookEvent = false
// init parent attached events
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
我深入看了下 updateComponentListeners
方法,最終走到了 src/core/vdom/helpers/update-listeners.js
的 updateListeners
方法中,因為並沒有傳 oldOn
引數,所以我簡化了下程式碼,簡化程式碼如下:
// src/core/vdom/helpers/update-listeners.js
export function updateListeners (
on: Object,
oldOn: Object,
add: Function,
remove: Function,
vm: Component
) {
let name, def, cur, old, event
for (name in on) {
def = cur = on[name]
old = oldOn[name]
event = normalizeEvent(name)
if (isUndef(old)) {
if (isUndef(cur.fns)) {
cur = on[name] = createFnInvoker(cur)
}
add(event.name, cur, event.once, event.capture, event.passive, event.params)
}
}
}
其中這個add方法如下:
// src/core/instance/events.js
// target 臨時引用vm,用完後即變為undefined
var target;
function add (event, fn, once) {
if (once) {
target.$once(event, fn);
} else {
target.$on(event, fn);
}
}
整理下來就是將父級的事件定義到當前vm中。
$on
監聽當前例項上的自定義事件。事件可以由vm.$emit觸發。回撥函式會接收所有傳入事件觸發函式的額外引數。
程式碼如下
// src/core/instance/events.js
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++) {
this.$on(event[i], fn)
}
} else {
(vm._events[event] || (vm._events[event] = [])).push(fn)
// 通過使用標記為註冊而不是雜湊查詢的布林標記來優化鉤子 hook: 事件成本。
if (hookRE.test(event)) {
vm._hasHookEvent = true
}
}
return vm
}
如果 event 是陣列則遍歷執行 $on 方法(2.2.0+ 中支援);
否則 向 vm._events[event] 中傳遞迴調函式 fn,這裡既然 vm._events[event] 是一個陣列,那麼我猜想一個 event 可以執行多個回撥函式咯?
如果是 event 字串中有 hook:
,修改 vm._hasHookEvent
的狀態。如果 _hasHookEvent
為 true,那麼在觸發各類生命週期鉤子的時候會觸發如 hook:created
事件,這只是一種優化方式,與我們主題關係不大,具體請看程式碼~
// src/core/instance/lifecycle.js
export function callHook (vm: Component, hook: string) {
const handlers = vm.$options[hook]
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
try {
handlers[i].call(vm)
} catch (e) {
handleError(e, vm, `${hook} hook`)
}
}
}
if (vm._hasHookEvent) {
vm.$emit(`hook:` + hook)
}
}
$once
監聽一個自定義事件,但是隻觸發一次,在第一次觸發之後移除監聽器。
程式碼
// src/core/instance/events.js
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
vm.$on(event, on)
return vm
}
這個就簡單了,定義一個 $on 事件監聽,回撥函式中使用 $off 方法取消事件監聽,並執行回撥函式。
$off
移除自定義事件監聽器。
- 如果沒有提供引數,則移除所有的事件監聽器;
- 如果只提供了事件,則移除該事件所有的監聽器;
- 如果同時提供了事件與回撥,則只移除這個回撥的監聽器。
程式碼如下,分析見註釋。
// src/core/instance/events.js
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
}
// 關閉陣列中的事件監聽器
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
this.$off(event[i], fn)
}
return vm
}
// 具體某個事件監聽
const cbs = vm._events[event]
// 沒有這個監聽事件,直接返回vm
if (!cbs) {
return vm
}
// 沒有 fn,將事件監聽器變為null,返回vm
if (!fn) {
vm._events[event] = null
return vm
}
// 有回撥函式
if (fn) {
// specific handler
let cb
let i = cbs.length
while (i--) {
// cbs = vm._events[event] 是一個陣列
cb = cbs[i]
if (cb === fn || cb.fn === fn) {
// 移除 fn 這個事件監聽器
cbs.splice(i, 1)
break
}
}
}
return vm
}
$emit
觸發當前例項上的事件。附加引數都會傳給監聽器回撥。
程式碼
// src/core/instance/events.js
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)
for (let i = 0, l = cbs.length; i < l; i++) {
try {
cbs[i].apply(vm, args)
} catch (e) {
handleError(e, vm, `event handler for "${event}"`)
}
}
}
return vm
}
程式碼分析:首先獲取 vm._events[event] ,之前我們說過這玩意是個陣列;如果有這個事件監聽器,從第二個引數開始獲取作為觸發方法的傳參 args,遍歷事件監聽器陣列傳參執行回撥函式。
最後
就這麼多啦~其實事件還是很簡單的。明後天研究研究渲染這個難點!我們後天見!
Vue.js學習系列
鑑於前端知識碎片化嚴重,我希望能夠系統化的整理出一套關於Vue的學習系列部落格。
Vue.js學習系列專案地址
本文原始碼已收入到GitHub中,以供參考,當然能留下一個star更好啦^-^。
https://github.com/violetjack/VueStudyDemos
關於作者
VioletJack,高效學習前端工程師,喜歡研究提高效率的方法,也專注於Vue前端相關知識的學習、整理。
歡迎關注、點贊、評論留言~我將持續產出Vue相關優質內容。
新浪微博: http://weibo.com/u/2640909603
掘金:https://gold.xitu.io/user/571…
CSDN: http://blog.csdn.net/violetja…
簡書: http://www.jianshu.com/users/…
Github: https://github.com/violetjack