Вышел новый редактор Наггетс (666), и друзья Наггетс поспешили его опробовать.
для Вью 3двусторонняя привязкаКому интересно, можете прочитатьДавайте поговорим о том, как работает двусторонняя привязка Vue 3.Эта статья. В этой статье брат А Бао возьмет простой пример в качестве отправной точки и расскажет всем шаг за шагом.$emitЧто произошло после этого. Прочитав эту статью, вы поймете принципы, лежащие в основе пользовательских событий в Vue 3.
<div id="app"></div>
<script>
const app = Vue.createApp({
template: '<welcome-button v-on:welcome="sayHi"></welcome-button>',
methods: {
sayHi() {
console.log('你好,我是阿宝哥!');
}
}
})
app.component('welcome-button', {
emits: ['welcome'],
template: `
<button v-on:click="$emit('welcome')">
欢迎
</button>
`
})
app.mount("#app")
</script>
В приведенном выше примере мы сначала передаемVue.createAppсоздание методаappобъект, затем используйтеcomponentСпособ регистрации глобальных компонентов -welcome-buttonкомпоненты. При определении этого компонента мы передаемemitsСвойства определяют пользовательские события для этого компонента. Конечно, пользователь нажимаетДобро пожаловатькнопку, он будет излучатьwelcomeсобытие, которое затем будет называтьсяsayHiметод, то консоль выведетЗдравствуйте, я брат Бао!.
Следуйте «Дороге полного совершенствования», чтобы прочитать 4 оригинальных бесплатных электронных книги (загружено более 30 000) и9 Учебные пособия по Vue 3 Advanced Series.
Хотя этот пример относительно прост, он также имеет следующие 2 проблемы:
-
$emitОткуда метод? - Каков поток обработки пользовательских событий?
Ниже мы сосредоточимся на этих проблемах для дальнейшего анализа механизма пользовательских событий, прежде всего, давайте проанализируем первую проблему.
1. Откуда взялся метод $emit?
С помощью инструментов разработчика Chrome мыsayHiДобавьте точку останова внутри метода и нажмитеДобро пожаловатькнопка, стек вызовов функций показан ниже:
В стеке вызовов в правой части рисунка выше мы нашлиcomponentEmits.tsв файлеemitметод. Но в шаблоне мы используем$emitметод, чтобы понять эту проблему, давайте посмотримonClickметод:
Как видно из рисунка выше, наша$emitметод из_ctxобъект, так что же это за объект? Опять же, используя точки останова, мы можем увидеть_ctxВнутренняя структура объекта:
Это понятно_ctxобъект являетсяProxyвозразите, если вы правыProxyТему пока не знаю, могу прочитатьПрокси, которого вы не знаетеЭта статья. при посещении_ctxобъект$emitсвойства, войдут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Кетчер будет продолжать звонитьPublicInstanceProxyHandlersобъектgetспособ получитьkeyсоответствующее значение. из-заPublicInstanceProxyHandlersВнутренний код относительно сложен, здесь мы анализируем только код, относящийся к примеру:
// 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
// 省略大部分内容
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указывает на параметрiобъектemitАтрибуты. Давайте посмотрим, как получить$emitимущество,targetЧто такое объект:
Это видно из приведенного выше рисункаtargetобъект имеет_свойство, значением которого является объект, содержащийvnode,typeа такжеparentи другие свойства. Итак, мы предполагаем_Значением свойства является экземпляр компонента. Чтобы подтвердить эту догадку, с помощью Chrome DevTools мы можем легко проанализировать, какие функции вызываются в процессе монтирования компонента:
На изображении выше мы видим, что на этапе монтирования компонента вызовcreateComponentInstanceфункция. Как следует из названия, эта функция используется для создания экземпляра компонента, и ее конкретная реализация выглядит следующим образом:
// 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__) { // 开发模式
instance.ctx = createRenderContext(instance)
} else { // 生产模式
instance.ctx = { _: instance }
}
instance.root = parent ? parent.root : instance
instance.emit = emit.bind(null, instance)
return instance
}
В приведенном выше коде, помимо нахожденияinstanceВ дополнение к объекту, также виделinstance.emit = emit.bind(null, instance)это утверждение. Затем мы нашли$emitОткуда метод. Разобравшись с первой проблемой, давайте проанализируем поток обработки пользовательских событий.
2. Каков поток обработки пользовательских событий?
Чтобы узнать, почему нажмитеДобро пожаловатькнопка отправкиwelcomeПосле события оно будет вызвано автоматическиsayHiпричина метода. мы должны проанализироватьemitЛогика внутренней обработки функции, которая определена в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
)
}
}
по фактуemitВнутри функции также будут задействованыv-model update:xxxОбработка событий, оv-modelБрат А Бао напишет отдельную статью, чтобы представить внутренние принципы инструкций. Здесь мы анализируем только логику обработки, относящуюся к текущему примеру.
существуетemitфункция, будем использоватьtoHandlerKeyфункция для преобразования имен событий в верблюжий регистрhandlerName:
// packages/shared/src/index.ts
export const toHandlerKey = cacheStringFunction(
(str: string) => (str ? `on${capitalize(str)}` : ``)
)
в полученииhandlerNameПосле этого изpropsполучить объектhandlerNameсоответствующийhandlerобъект. еслиhandlerобъект существует, он будет называтьсяcallWithAsyncErrorHandlingдля выполнения обработчика событий, соответствующего текущему пользовательскому событию.callWithAsyncErrorHandlingФункция определяется следующим образом:
// 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 < fn.length; i++) {
values.push(callWithAsyncErrorHandling(fn[i], instance, type, args))
}
return values
}
Как видно из кода выше, еслиfnЕсли параметр является функциональным объектом, вcallWithAsyncErrorHandlingФункция будет продолжать вызыватьcallWithErrorHandlingфункция для окончательного выполнения обработчика события:
// 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Внутри функции используйтеtry catchОператор для перехвата и обработки исключений. если звонишьfnПосле обработчика события возвращаетсяPromiseобъект, он пройдетPromiseна объектеcatchметод обработки исключений. Поняв вышеприведенное содержание, просмотрите стек вызовов функций, который вы видели раньше, я думаю, вы не будете незнакомы в это время.
Теперь мы нашли ответы на два упомянутых выше вопроса. Чтобы лучше понять релевантное содержание пользовательских событий, Brother Po будет использоватьVue 3 Template ExplorerЭтот онлайн-инструмент для анализа результатов компиляции шаблона на примере:
Шаблон компонента приложения
<welcome-button v-on:welcome="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"]))
}
}
шаблон компонента приветственной кнопки
<button v-on:click="$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"]))
}
}
Из вышеприведенных результатов мы видим, что поv-on:События привязки будут преобразованы вonсвойства в начале, такие какonWelcomeа такжеonClick. Зачем переходить в эту форму? Это потому, что вemitВнутри функция пройдетtoHandlerKeyа такжеcamelizeЭти две функции преобразуют имена событий:
// 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]
}
Чтобы понять правила преобразования, давайте сначала рассмотримcamelizeфункция:
// 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Функция функции, которая используется для преобразования имени события, названного kebab-case (именование, разделенное тире), в имя события camelCase (нотация CamelCase), например"test-event"имя события переданоcamelizeобработанная функция, будет преобразована в"testEvent". Преобразованный результат также пройдетtoHandlerKeyфункция для дальнейшей обработки,toHandlerKeyфункция определена в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"имя события переданоtoHandlerKeyобработанная функция, будет окончательно преобразована в"onTestEvent"форма. Чтобы иметь возможность более интуитивно понять юридическую форму слушателей событий, давайте посмотрим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)
})
Теперь, когда мы понимаем юридические формы прослушивателей событий, давайте посмотримcacheStringFunctionфункция:
// packages/shared/src/index.ts
const cacheStringFunction = <T 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Функция функции заключается в реализации функции кэширования.
Следуйте «Дороге полного совершенствования», чтобы прочитать 4 оригинальных бесплатных электронных книги (загружено более 30 000) и9 Учебные пособия по Vue 3 Advanced Series.
3. Брату А Бао есть что сказать
3.1 Как связать события в функциях рендеринга?
В предыдущем примере мы прошлиv-onИнструкция завершает привязку события, так как же привязать событие в функции рендеринга?
<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Функции создают функциональные компонентыComp, при созданииCompкомпонент, установивonFooСвойства реализуют операции привязки для пользовательских событий.
3.2 Как выполнить обработчик события только один раз?
установить в шаблоне
<welcome-button v-on:welcome.once="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Модификатор события для реализации функции выполнения обработчика события только один раз. Кроме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>
<!-- 只当在 event.target 是当前元素自身时触发处理函数 -->
<!-- 即事件不是从内部元素触发的 -->
<div @click.self="doThat">...</div>
установить в функции рендеринга
<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');
this.$emit('foo');
}
});
const onFoo = () => {
console.log("foo be called")
};
// 在事件名后添加Once,表示该事件处理器只执行一次
const Comp = () => h(Foo, { onFooOnce: onFoo })
const app = createApp(Comp);
app.mount("#app")
</script>
Причина, по которой вышеупомянутые два метода могут работать, заключается в том, что директивы в шаблонеv-on:welcome.once, который преобразуется вonWelcomeOnce, И вemitопределяется в функцииonceПравила обработки модификатора:
// packages/runtime-core/src/componentEmits.ts
export function emit(
instance: ComponentInternalInstance,
event: string,
...rawArgs: any[]
) {
const props = instance.vnode.props || EMPTY_OBJ
const onceHandler = props[handlerName + `Once`]
if (onceHandler) {
if (!instance.emitted) {
;(instance.emitted = {} as Record<string, boolean>)[handlerName] = true
} else if (instance.emitted[handlerName]) {
return
}
callWithAsyncErrorHandling(
onceHandler,
instance,
ErrorCodes.COMPONENT_EVENT_HANDLER,
args
)
}
}
3.3 Как добавить несколько обработчиков событий
установить в шаблоне
<div @click="foo(), bar()"/>
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("div", {
onClick: $event => (foo(), bar())
}, null, 8 /* PROPS */, ["onClick"]))
}
}
установить в функции рендеринга
<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 onBar = () => {
console.log("bar be called")
};
const Comp = () => h(Foo, { onFoo: [onFoo, onBar] })
const app = createApp(Comp);
app.mount("#app")
</script>
Причина, по которой вышеуказанные методы могут вступить в силу, заключается в том, что вышеупомянутыеcallWithAsyncErrorHandlingФункция содержит логику обработки нескольких обработчиков событий:
// packages/runtime-core/src/errorHandling.ts
export function callWithAsyncErrorHandling(
fn: Function | Function[],
instance: ComponentInternalInstance | null,
type: ErrorTypes,
args?: unknown[]
): any[] {
if (isFunction(fn)) {
// 省略部分代码
}
const values = []
for (let i = 0; i < fn.length; i++) {
values.push(callWithAsyncErrorHandling(fn[i], instance, type, args))
}
return values
}
3.4 Версия 3$emitс Вью 2$emitКакая разница?
во Вью 2$emitпутьVue.prototypeсвойства объектов, а на Vue 3$emitявляется свойством экземпляра компонента,instance.emit = emit.bind(null, instance).
// src/core/instance/events.js
export function eventsMixin (Vue: Class<Component>) {
const hookRE = /^hook:/
// 省略$on、$once和$off等方法的定义
// Vue实例是一个EventBus对象
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)
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
}
}
В этой статье Baoge в основном раскрывает секреты пользовательских событий в Vue 3. Чтобы позволить каждому получить более глубокое понимание соответствующих знаний о пользовательских событиях, брат Абао проанализировал с точки зрения исходного кода.$emitИсточник метода и поток обработки пользовательского события.
Статьи в расширенной серии Vue 3.0 все еще обновляются и были обновлены до девятой статьи.Друзья, которые хотят изучать Vue 3.0 вместе, могут добавить Abaoge WeChat - semlinker.