<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【Vue.js】875- Vue 3.0 進(jìn)階之自定義事件探秘

          共 11836字,需瀏覽 24分鐘

           ·

          2021-02-21 09:13

          這是 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、typeparent 等屬性。因此我們猜測 _ 屬性的值是組件實(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 組件模板

          "$emit('welcome')">歡迎</button>

          const?_Vue?=?Vue
          return?function?render(_ctx,?_cache,?$props,?$setup,?$data,?$options)?{
          ??with?(_ctx)?{
          ????const?{?createVNode:?_createVNode,?openBlock:?_openBlock,
          ??????createBlock:?_createBlock?}?=?_Vue

          ????return?(_openBlock(),?_createBlock("button",?{
          ??????onClick:?$event?=>?($emit('welcome'))
          ????},?"歡迎",?8?/
          *?PROPS?*/,?["onClick"]))
          ??}
          }

          觀察以上結(jié)果,我們可知通過 v-on: 綁定的事件,都會轉(zhuǎn)換為以 on 開頭的屬性,比如 onWelcomeonClick。為什么要轉(zhuǎn)換成這種形式呢?這是因?yàn)樵?emit 函數(shù)內(nèi)部會通過 toHandlerKeycamelize 這兩個函數(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>
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                    <th id="afajh"><progress id="afajh"></progress></th>
                    BB在线视频网站 | 成人毛片视频在线观看 | 欧美日韩亚洲视频 | A片x395| 天天干,夜夜操 |