Vue 事件原理(從源碼角度帶你分析)(4)

huihui_new 2022-05-14 12:04:28 阅读数:607

vue事件原理角度分析

本文已參與「新人創作禮」活動,一起開啟掘金創作之路。

之前我們已經介紹了event的編譯過程(點擊這裏跳轉),接下來我們分析在Vue初始化和更新的過程中event的內部是如何生成的。

event生成之自定義事件

Vueevent事件分為原生DOM事件與自定義事件,原生DOM事件的處理(點擊這裏跳轉),我們上一節已經分析過了。這一節我們來分析下自定義事件。

自定義事件是用在組件節點上的,組件節點上定義的事件可以分為兩類:一類是原生DOM事件( 在vue2.x版本在組件節點上使用原生DOM事件需要添加native修飾符),另一類就是自定義事件。

下面我們來分析自定義事件的流程:

創建組件vnode

創建組建vnode(虛擬節點)的時候會執行createComponent函數,其中有如下邏輯:

export function createComponent ( Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string ): VNode | Array<VNode> | void {
......
// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
// 自定義事件賦值給listeners
const listeners = data.on
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
// native事件賦值給data.on,這樣原生方法直接就上一節相同的邏輯了
data.on = data.nativeOn
......
// return a placeholder vnode
// 創建占比特符vnode
const name = Ctor.options.name || tag
// 生成虛擬節點的時候,將listeners當參數傳入
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
// 返回vnode
return vnode
}
複制代碼

創建組件vnode的過程中會將組件節點上的定義的自定義事件賦值給listeners變量,同時將組件節點上定義的原生事件賦值給data.on屬性,這樣,組件的原生事件就會執行如同上一節生成原生事件相同的邏輯。然後在創建組件vnode的時候,會將listeners(緩存了自定義事件)當做第七個參數(componentOptions)的屬性值。

vnode創建完成之後,在初始化組件的時候,會執行initInternalComponent函數:

組件初始化

initInternalComponent


export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
// 子組件構造器的options(配置項)
const opts = vm.$options = Object.create(vm.constructor.options)
// ....
// 我們之前創建的節點的第七個參數(componentOptions)
const vnodeComponentOptions = parentVnode.componentOptions
// 子組件構造器的_parentListeners屬性指向之前定義的listeners(組件自定義事件)
opts._parentListeners = vnodeComponentOptions.listeners
// ...
}
複制代碼

執行完這些配置項的生成之後,會初始化子組件事件


export function initEvents (vm: Component) {
vm._events = Object.create(null)
vm._hasHookEvent = false
// init parent attached events
const listeners = vm.$options._parentListeners
// 有listeners,執行updateComponentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
複制代碼

listeners非空,執行updateComponentListeners函數:


let target: any
export function updateComponentListeners ( vm: Component, listeners: Object, oldListeners: ?Object ) {
// target指向當前實例
target = vm
// 執行updateListeners
updateListeners(listeners, oldListeners || {}, add, remove, vm)
target = undefined
}
複制代碼

這個地方同樣執行updateListeners函數,與上一節原生DOM事件的生成相同,但與原生DOM事件的生成有幾處不同之處,如下addremove函數的定義。

function add (event, fn, once) {
if (once) {
// 如果有once屬性,執行$once方法
target.$once(event, fn)
} else {
否則執行$on方法
target.$on(event, fn)
}
}
function remove (event, fn) {
// remove方法是執行$off方法
target.$off(event, fn)
}
複制代碼

關於$once$on$off函數都定義在eventsMixin中:

export function eventsMixin (Vue: Class<Component>) {
const hookRE = /^hook:/
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 {
......
}
}
複制代碼

$on

Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
// 當前實例就是調用該方法的實例
const vm: Component = this
// 如果event是數組,遍曆數組,依次執行$on函數
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
this.$on(event[i], fn)
}
} else {
// 將當前實例的_events屬性初始化為空數組並push當前添加的函數
(vm._events[event] || (vm._events[event] = [])).push(fn)
// optimize hook:event cost by using a boolean flag marked at registration
// instead of a hash lookup
if (hookRE.test(event)) {
vm._hasHookEvent = true
}
}
return vm
}
複制代碼

$on的邏輯就是將當前的方法存入當前實例vm._events屬性中。

$once

Vue.prototype.$once = function (event: string, fn: Function): Component {
// 當前實例就是調用該方法的實例
const vm: Component = this
// 定義on函數
function on () {
// 執行$off銷毀當前事件
vm.$off(event, on)
// 執行函數fn
fn.apply(vm, arguments)
}
// on的fn屬性指向當前傳入的函數
on.fn = fn
// 將on函數存入vm._events中
vm.$on(event, on)
return vm
}
複制代碼

$once的邏輯就是對傳入的fn函數做了一層封裝,生成了一個內部函數onon.fn屬性指向傳入函數fn,將on函數存入實例的_events屬性對象中,這樣執行完一次這個函數後,該函數就被銷毀了。

$off

Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
// 當前實例就是調用該方法的實例
const vm: Component = this
// all
// 如果沒有傳參數,將vm._events置為空對象
if (!arguments.length) {
vm._events = Object.create(null)
return vm
}
// array of events
// event如果是數組,遍曆該數組,依次調用$off函數
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
this.$off(event[i], fn)
}
// 返回
return vm
}
// specific event
// 唯一的event
const cbs = vm._events[event]
// cbs未定義,直接返回
if (!cbs) {
return vm
}
// fn未定義(未傳入fn的情况下),vm._events[event]賦值為空,直接返回
if (!fn) {
vm._events[event] = null
return vm
}
// fn定義了
if (fn) {
// specific handler
let cb
let i = cbs.length
// 遍曆cbs對象
while (i--) {
cb = cbs[i]
// 如果查找到有屬性與fn相同
if (cb === fn || cb.fn === fn) {
// 移除該屬性,跳出循環
cbs.splice(i, 1)
break
}
}
}
return vm
}
複制代碼

$off的作用就是移除vm._events對象上定義的事件函數。

eventsMixin中還定義了一個函數$emit,在組件通訊的時候經常使用:

$emit

Vue.prototype.$emit = function (event: string): Component {
// 當前實例就是調用該方法的實例
const vm: Component = this
if (process.env.NODE_ENV !== 'production') {
const lowerCaseEvent = event.toLowerCase()
if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
tip(
`Event "${lowerCaseEvent}" is emitted in component ` +
`${formatComponentName(vm)} but the handler is registered for "${event}". ` +
`Note that HTML attributes are case-insensitive and you cannot use ` +
`v-on to listen to camelCase events when using in-DOM templates. ` +
`You should probably use "${hyphenate(event)}" instead of "${event}".`
)
}
}
// 拿到vm._events的event事件上的所有函數
let cbs = vm._events[event]
// 存在cbs
if (cbs) {
// cbs轉化
cbs = cbs.length > 1 ? toArray(cbs) : cbs
// 其他參數轉化成數組
const args = toArray(arguments, 1)
// 遍曆cbs,依次執行其中的函數
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
}
複制代碼

從源碼上可以看出,在我們平時開發過程中,其實看似通過$emit方法調用父組件上的函數,本質上是調用組件自身實例上定義的函數,而這個函數是在組件生成的過程中傳入到子組件的配置項中的。

還有一點值得提一下,組件自定義事件的事件調用,其實就是非常經典的事件中心的實現。而我們在Vue開發過程中常用的eventBus的實現,原理也是同上。

到此為止,關於Vueevent原理已經大致介紹完畢了,歡迎交流探討。

版权声明:本文为[huihui_new]所创,转载请带上原文链接,感谢。 https://gsmany.com/2022/134/202205141158167017.html