【Vue.js】875- Vue 3.0 進(jìn)階之自定義事件探秘
這是 Vue 3.0 進(jìn)階系列 的第二篇文章,該系列的第一篇文章是 Vue 3.0 進(jìn)階之指令探秘。本文阿寶哥將以一個簡單的示例為切入點(diǎn),帶大家一起一步步揭開自定義事件背后的秘密。
<div?id="app">div>
<script>
???const?app?=?Vue.createApp({
?????template:?' ',
?????methods:?{
???????sayHi()?{
?????????console.log('你好,我是阿寶哥!');
???????}
?????}
????})
???app.component('welcome-button',?{
?????emits:?['welcome'],
?????template:?`
???????
??????`
????})
????app.mount("#app")
script>
在以上示例中,我們先通過 Vue.createApp 方法創(chuàng)建 app 對象,之后利用該對象上的 component 方法注冊全局組件 —— ?welcome-button 組件。在定義該組件時,我們通過 emits 屬性定義了該組件上的自定義事件。當(dāng)然用戶點(diǎn)擊 歡迎 按鈕時,就會發(fā)出 welcome 事件,之后就會調(diào)用 sayHi 方法,接著控制臺就會輸出 你好,我是阿寶哥! 。
雖然該示例比較簡單,但也存在以下 2 個問題:
$emit方法來自哪里?自定義事件的處理流程是什么?
下面我們將圍繞這些問題來進(jìn)一步分析自定義事件背后的機(jī)制,首先我們先來分析第一個問題。
一、$emit 方法來自哪里?
使用 Chrome 開發(fā)者工具,我們在 sayHi 方法內(nèi)部加個斷點(diǎn),然后點(diǎn)擊 歡迎 按鈕,此時函數(shù)調(diào)用棧如下圖所示:

在上圖右側(cè)的調(diào)用棧,我們發(fā)現(xiàn)了一個存在于 componentEmits.ts 文件中的 emit 方法。但在模板中,我們使用的是 $emit 方法,為了搞清楚這個問題,我們來看一下 onClick 方法:

由上圖可知,我們的 $emit 方法來自 _ctx 對象,那么該對象是什么對象呢?同樣,利用斷點(diǎn)我們可以看到 _ctx 對象的內(nèi)部結(jié)構(gòu):

很明顯 _ctx 對象是一個 Proxy 對象,如果你對 Proxy 對象還不了解,可以閱讀 你不知道的 Proxy 這篇文章。當(dāng)訪問 _ctx 對象的 $emit 屬性時,將會進(jìn)入 get 捕獲器,所以接下來我們來分析 get 捕獲器:

通過 [[FunctionLocation]] 屬性,我們找到了 get 捕獲器的定義,具體如下所示:
//?packages/runtime-core/src/componentPublicInstance.ts
export?const?RuntimeCompiledPublicInstanceProxyHandlers?=?extend(
??{},
??PublicInstanceProxyHandlers,
??{
????get(target:?ComponentRenderContext,?key:?string)?{
??????//?fast?path?for?unscopables?when?using?`with`?block
??????if?((key?as?any)?===?Symbol.unscopables)?{
????????return
??????}
??????return?PublicInstanceProxyHandlers.get!(target,?key,?target)
????},
????has(_:?ComponentRenderContext,?key:?string)?{
??????const?has?=?key[0]?!==?'_'?&&?!isGloballyWhitelisted(key)
??????//?省略部分代碼
??????return?has
????}
??}
)
觀察以上代碼可知,在 get 捕獲器內(nèi)部會繼續(xù)調(diào)用 PublicInstanceProxyHandlers 對象的 get 方法來獲取 key 對應(yīng)的值。由于 PublicInstanceProxyHandlers 內(nèi)部的代碼相對比較復(fù)雜,這里我們只分析與示例相關(guān)的代碼:
//?packages/runtime-core/src/componentPublicInstance.ts
export?const?PublicInstanceProxyHandlers:?ProxyHandler<any>?=?{
??get({?_:?instance?}:?ComponentRenderContext,?key:?string)?{
????const?{?ctx,?setupState,?data,?props,?accessCache,?type,?appContext?}?=?instance
???
????//?省略大部分內(nèi)容
????const?publicGetter?=?publicPropertiesMap[key]
????//?public?$xxx?properties
????if?(publicGetter)?{
??????if?(key?===?'$attrs')?{
????????track(instance,?TrackOpTypes.GET,?key)
????????__DEV__?&&?markAttrsAccessed()
??????}
??????return?publicGetter(instance)
????},
????//?省略set和has捕獲器
}
在上面代碼中,我們看到了 publicPropertiesMap 對象,該對象被定義在 componentPublicInstance.ts 文件中:
//?packages/runtime-core/src/componentPublicInstance.ts
const?publicPropertiesMap:?PublicPropertiesMap?=?extend(Object.create(null),?{
??$:?i?=>?i,
??$el:?i?=>?i.vnode.el,
??$data:?i?=>?i.data,
??$props:?i?=>?(__DEV__???shallowReadonly(i.props)?:?i.props),
??$attrs:?i?=>?(__DEV__???shallowReadonly(i.attrs)?:?i.attrs),
??$slots:?i?=>?(__DEV__???shallowReadonly(i.slots)?:?i.slots),
??$refs:?i?=>?(__DEV__???shallowReadonly(i.refs)?:?i.refs),
??$parent:?i?=>?getPublicInstance(i.parent),
??$root:?i?=>?getPublicInstance(i.root),
??$emit:?i?=>?i.emit,
??$options:?i?=>?(__FEATURE_OPTIONS_API__???resolveMergedOptions(i)?:?i.type),
??$forceUpdate:?i?=>?()?=>?queueJob(i.update),
??$nextTick:?i?=>?nextTick.bind(i.proxy!),
??$watch:?i?=>?(__FEATURE_OPTIONS_API__???instanceWatch.bind(i)?:?NOOP)
}?as?PublicPropertiesMap)
在 publicPropertiesMap 對象中,我們找到了 $emit 屬性,該屬性的值為 $emit: i => i.emit,即 $emit 指向的是參數(shù) i 對象的 emit 屬性。下面我們來看一下,當(dāng)獲取 $emit 屬性時,target 對象是什么:

由上圖可知 target 對象有一個 _ 屬性,該屬性的值是一個對象,且該對象含有 vnode、type 和 parent 等屬性。因此我們猜測 _ 屬性的值是組件實(shí)例。為了證實(shí)這個猜測,利用 Chrome 開發(fā)者工具,我們就可以輕易地分析出組件掛載過程中調(diào)用了哪些函數(shù):

在上圖中,我們看到了在組件掛載階段,調(diào)用了 createComponentInstance 函數(shù)。顧名思義,該函數(shù)用于創(chuàng)建組件實(shí)例,其具體實(shí)現(xiàn)如下所示:
//?packages/runtime-core/src/component.ts
export?function?createComponentInstance(
??vnode:?VNode,
??parent:?ComponentInternalInstance?|?null,
??suspense:?SuspenseBoundary?|?null
)?{
??const?type?=?vnode.type?as?ConcreteComponent
??const?appContext?=
????(parent???parent.appContext?:?vnode.appContext)?||?emptyAppContext
??const?instance:?ComponentInternalInstance?=?{
????uid:?uid++,
????vnode,
????type,
????parent,
????appContext,
????//?省略大部分屬性
????emit:?null?as?any,?
????emitted:?null,
??}
??if?(__DEV__)?{?//?開發(fā)模式
????instance.ctx?=?createRenderContext(instance)
??}?else?{?//?生產(chǎn)模式
????instance.ctx?=?{?_:?instance?}
??}
??instance.root?=?parent???parent.root?:?instance
??instance.emit?=?emit.bind(null,?instance)
??return?instance
}
在以上代碼中,我們除了發(fā)現(xiàn) instance 對象之外,還看到了 instance.emit = emit.bind(null, instance) 這個語句。這時我們就找到了 $emit 方法來自哪里的答案。弄清楚第一個問題之后,接下來我們來分析自定義事件的處理流程。
二、自定義事件的處理流程是什么?
要搞清楚,為什么點(diǎn)擊 歡迎 按鈕派發(fā) welcome 事件之后,就會自動調(diào)用 sayHi 方法的原因。我們就必須分析 emit 函數(shù)的內(nèi)部處理邏輯,該函數(shù)被定義在 runtime-core/src/componentEmits.t 文件中:
//?packages/runtime-core/src/componentEmits.ts
export?function?emit(
??instance:?ComponentInternalInstance,
??event:?string,
??...rawArgs:?any[]
)?{
??const?props?=?instance.vnode.props?||?EMPTY_OBJ
?//?省略大部分代碼
??let?args?=?rawArgs
??//?convert?handler?name?to?camelCase.?See?issue?#2249
??let?handlerName?=?toHandlerKey(camelize(event))
??let?handler?=?props[handlerName]
??if?(handler)?{
????callWithAsyncErrorHandling(
??????handler,
??????instance,
??????ErrorCodes.COMPONENT_EVENT_HANDLER,
??????args
????)
??}
}
其實(shí)在 emit 函數(shù)內(nèi)部還會涉及 v-model update:xxx 事件的處理,關(guān)于 v-model 指令的內(nèi)部原理,阿寶哥會寫單獨(dú)的文章來介紹。這里我們只分析與當(dāng)前示例相關(guān)的處理邏輯。
在 emit 函數(shù)中,會使用 toHandlerKey 函數(shù)把事件名轉(zhuǎn)換為駝峰式的 handlerName:
//?packages/shared/src/index.ts
export?const?toHandlerKey?=?cacheStringFunction(
??(str:?string)?=>?(str???`on${capitalize(str)}`?:?``)
)
在獲取 handlerName 之后,就會從 props 對象上獲取該 handlerName 對應(yīng)的 handler 對象。如果該 handler 對象存在,則會調(diào)用 callWithAsyncErrorHandling 函數(shù),來執(zhí)行當(dāng)前自定義事件對應(yīng)的事件處理函數(shù)。callWithAsyncErrorHandling 函數(shù)的定義如下:
//?packages/runtime-core/src/errorHandling.ts
export?function?callWithAsyncErrorHandling(
??fn:?Function?|?Function[],
??instance:?ComponentInternalInstance?|?null,
??type:?ErrorTypes,
??args?:?unknown[]
):?any[]?{
??if?(isFunction(fn))?{
????const?res?=?callWithErrorHandling(fn,?instance,?type,?args)
????if?(res?&&?isPromise(res))?{
??????res.catch(err?=>?{
????????handleError(err,?instance,?type)
??????})
????}
????return?res
??}
??//?處理多個事件處理器
??const?values?=?[]
??for?(let?i?=?0;?i?????values.push(callWithAsyncErrorHandling(fn[i],?instance,?type,?args))
??}
??return?values
}
通過以上代碼可知,如果 fn 參數(shù)是函數(shù)對象的話,在 callWithAsyncErrorHandling 函數(shù)內(nèi)部還會繼續(xù)調(diào)用 callWithErrorHandling 函數(shù)來最終執(zhí)行事件處理函數(shù):
//?packages/runtime-core/src/errorHandling.ts
export?function?callWithErrorHandling(
??fn:?Function,
??instance:?ComponentInternalInstance?|?null,
??type:?ErrorTypes,
??args?:?unknown[]
)?{
??let?res
??try?{
????res?=?args???fn(...args)?:?fn()
??}?catch?(err)?{
????handleError(err,?instance,?type)
??}
??return?res
}
在 callWithErrorHandling 函數(shù)內(nèi)部,使用 try catch 語句來捕獲異常并進(jìn)行異常處理。如果調(diào)用 fn 事件處理函數(shù)之后,返回的是一個 Promise 對象的話,則會通過 Promise 對象上的 catch 方法來處理異常。了解完上面的內(nèi)容,再回顧一下前面見過的函數(shù)調(diào)用棧,相信此時你就不會再陌生了。

現(xiàn)在前面提到的 2 個問題,我們都已經(jīng)找到答案了。為了能更好地掌握自定義事件的相關(guān)內(nèi)容,阿寶哥將使用 Vue 3 Template Explorer 這個在線工具,來分析一下示例中模板編譯的結(jié)果:
App 組件模板
"sayHi"></welcome-button>
const?_Vue?=?Vue
return?function?render(_ctx,?_cache,?$props,?$setup,?$data,?$options)?{
??with?(_ctx)?{
????const?{?resolveComponent:?_resolveComponent,?createVNode:?_createVNode,?
??????openBlock:?_openBlock,?createBlock:?_createBlock?}?=?_Vue
????const?_component_welcome_button?=?_resolveComponent("welcome-button")
????return?(_openBlock(),?_createBlock(_component_welcome_button,
?????{?onWelcome:?sayHi?},?null,?8?/*?PROPS?*/,?["onWelcome"]))
??}
}
welcome-button 組件模板
觀察以上結(jié)果,我們可知通過 v-on: 綁定的事件,都會轉(zhuǎn)換為以 on 開頭的屬性,比如 onWelcome 和 onClick。為什么要轉(zhuǎn)換成這種形式呢?這是因?yàn)樵?emit 函數(shù)內(nèi)部會通過 toHandlerKey 和 camelize 這兩個函數(shù)對事件名進(jìn)行轉(zhuǎn)換:
//?packages/runtime-core/src/componentEmits.ts
export?function?emit(
??instance:?ComponentInternalInstance,
??event:?string,
??...rawArgs:?any[]
)?{
?//?省略大部分代碼
??//?convert?handler?name?to?camelCase.?See?issue?#2249
??let?handlerName?=?toHandlerKey(camelize(event))
??let?handler?=?props[handlerName]
}
為了搞清楚轉(zhuǎn)換規(guī)則,我們先來看一下 camelize 函數(shù):
//?packages/shared/src/index.ts
const?camelizeRE?=?/-(\w)/g
export?const?camelize?=?cacheStringFunction(
??(str:?string):?string?=>?{
????return?str.replace(camelizeRE,?(_,?c)?=>?(c???c.toUpperCase()?:?''))
??}
)
觀察以上代碼,我們可以知道 camelize 函數(shù)的作用,用于把 kebab-case (短橫線分隔命名) 命名的事件名轉(zhuǎn)換為 camelCase (駝峰命名法) 的事件名,比如 "test-event" 事件名經(jīng)過 camelize 函數(shù)處理后,將被轉(zhuǎn)換為 "testEvent"。該轉(zhuǎn)換后的結(jié)果,還會通過 toHandlerKey 函數(shù)進(jìn)行進(jìn)一步處理,toHandlerKey 函數(shù)被定義在 shared/src/index.ts 文件中:
//?packages/shared/src/index.ts
export?const?toHandlerKey?=?cacheStringFunction(
??(str:?string)?=>?(str???`on${capitalize(str)}`?:?``)
)
export?const?capitalize?=?cacheStringFunction(
??(str:?string)?=>?str.charAt(0).toUpperCase()?+?str.slice(1)
)
對于前面使用的 "testEvent" 事件名經(jīng)過 toHandlerKey 函數(shù)處理后,將被最終轉(zhuǎn)換為 "onTestEvent" 的形式。為了能夠更直觀地了解事件監(jiān)聽器的合法形式,我們來看一下 runtime-core 模塊中的測試用例:
//?packages/runtime-core/__tests__/componentEmits.spec.ts
test('isEmitListener',?()?=>?{
??const?options?=?{
????click:?null,
????'test-event':?null,
????fooBar:?null,
????FooBaz:?null
??}
??expect(isEmitListener(options,?'onClick')).toBe(true)
??expect(isEmitListener(options,?'onclick')).toBe(false)
??expect(isEmitListener(options,?'onBlick')).toBe(false)
??//?.once?listeners
??expect(isEmitListener(options,?'onClickOnce')).toBe(true)
??expect(isEmitListener(options,?'onclickOnce')).toBe(false)
??//?kebab-case?option
??expect(isEmitListener(options,?'onTestEvent')).toBe(true)
??//?camelCase?option
??expect(isEmitListener(options,?'onFooBar')).toBe(true)
??//?PascalCase?option
??expect(isEmitListener(options,?'onFooBaz')).toBe(true)
})
了解完事件監(jiān)聽器的合法形式之后,我們再來看一下 cacheStringFunction 函數(shù):
//?packages/shared/src/index.ts
const?cacheStringFunction?=?extends?(str:?string)?=>?string>(fn:?T):?T?=>?{
??const?cache:?Record<string,?string>?=?Object.create(null)
??return?((str:?string)?=>?{
????const?hit?=?cache[str]
????return?hit?||?(cache[str]?=?fn(str))
??})?as?any
}
以上代碼也比較簡單,cacheStringFunction 函數(shù)的作用是為了實(shí)現(xiàn)緩存功能。
三、阿寶哥有話說
3.1 如何在渲染函數(shù)中綁定事件?
在前面的示例中,我們通過 v-on 指令完成事件綁定,那么在渲染函數(shù)中如何綁定事件呢?
<div?id="app">div>
<script>
??const?{?createApp,?defineComponent,?h?}?=?Vue
??
??const?Foo?=?defineComponent({
????emits:?["foo"],?
????render()?{?return?h("h3",?"Vue?3?自定義事件")},
????created()?{
??????this.$emit('foo');
????}
??});
??const?onFoo?=?()?=>?{
????console.log("foo?be?called")
??};
??const?Comp?=?()?=>?h(Foo,?{?onFoo?})
??const?app?=?createApp(Comp);
??app.mount("#app")
script>
在以上示例中,我們通過 defineComponent 全局 API 定義了 Foo 組件,然后通過 h 函數(shù)創(chuàng)建了函數(shù)式組件 Comp,在創(chuàng)建 Comp 組件時,通過設(shè)置 onFoo 屬性實(shí)現(xiàn)了自定義事件的綁定操作。
3.2 如何只執(zhí)行一次事件處理器?
在模板中設(shè)置
"sayHi"></welcome-button>
const?_Vue?=?Vue
return?function?render(_ctx,?_cache,?$props,?$setup,?$data,?$options)?{
??with?(_ctx)?{
????const?{?resolveComponent:?_resolveComponent,?createVNode:?_createVNode,?
??????openBlock:?_openBlock,?createBlock:?_createBlock?}?=?_Vue
????const?_component_welcome_button?=?_resolveComponent("welcome-button")
????return?(_openBlock(),?_createBlock(_component_welcome_button,?
??????{?onWelcomeOnce:?sayHi?},?null,?8?/*?PROPS?*/,?["onWelcomeOnce"]))
??}
}
在以上代碼中,我們使用了 once 事件修飾符,來實(shí)現(xiàn)只執(zhí)行一次事件處理器的功能。除了 once 修飾符之外,還有其他的修飾符,比如:
<a?@click.stop="doThis">a>
<form?@submit.prevent="onSubmit">form>
<a?@click.stop.prevent="doThat">a>
<form?@submit.prevent>form>
<div?@click.capture="doThis">...div>
<div?@click.self="doThat">...div>
在渲染函數(shù)中設(shè)置
"app"></div>
BB在线视频网站
|
成人毛片视频在线观看
|
欧美日韩亚洲视频
|
A片x395|
天天干,夜夜操
|
