Модификаторы событий
Что мы будем делать в этот раз
Поскольку мы реализовали директиву v-on в прошлый раз, давайте теперь реализуем модификаторы событий.
Vue.js имеет модификаторы, которые соответствуют preventDefault и stopPropagation.
https://vuejs.org/guide/essentials/event-handling.html
В этот раз давайте реализуем следующий интерфейс разработчика.
import { createApp, defineComponent, ref } from 'chibivue'
const App = defineComponent({
setup() {
const inputText = ref('')
const buffer = ref('')
const handleInput = (e: Event) => {
const target = e.target as HTMLInputElement
buffer.value = target.value
}
const submit = () => {
inputText.value = buffer.value
buffer.value = ''
}
return { inputText, buffer, handleInput, submit }
},
template: `<div>
<form @submit.prevent="submit">
<label>
Input Data
<input :value="buffer" @input="handleInput" />
</label>
<button>submit</button>
</form>
<p>inputText: {{ inputText }}</p>
</div>`,
})
const app = createApp(App)
app.mount('#app')
В частности, обратите внимание на следующую часть.
<form @submit.prevent="submit"></form>
Здесь есть описание @submit.prevent
. Это означает, что при вызове обработчика события submit выполняется preventDefault
.
Если не включить .prevent
, страница будет перезагружена при отправке формы.
Реализация AST и Parser
Поскольку мы добавляем новый синтаксис в шаблон, необходимы изменения в Parser и AST.
Сначала давайте посмотрим на AST. Это очень просто: просто добавляем свойство под названием modifiers
(массив строк) в DirectiveNode
.
export interface DirectiveNode extends Node {
type: NodeTypes.DIRECTIVE
name: string
exp: ExpressionNode | undefined
arg: ExpressionNode | undefined
modifiers: string[] // Добавляем это
}
Давайте реализуем Parser соответственно.
На самом деле, это очень легко, потому что оно уже включено в регулярное выражение, заимствованное из исходного кода.
function parseAttribute(
context: ParserContext,
nameSet: Set<string>,
): AttributeNode | DirectiveNode {
// .
// .
// .
const modifiers = match[3] ? match[3].slice(1).split('.') : [] // Извлекаем модификаторы из результата match
return {
type: NodeTypes.DIRECTIVE,
name: dirName,
exp: value && {
type: NodeTypes.SIMPLE_EXPRESSION,
content: value.content,
isStatic: false,
loc: value.loc,
},
loc,
arg,
modifiers, // Включаем в возвращаемое значение
}
}
Да. С этим реализация AST и Parser завершена.
compiler-dom/transform
Давайте немного рассмотрим текущую архитектуру компилятора.
Текущая конфигурация выглядит следующим образом.
Когда вы снова понимаете роли compiler-core и compiler-dom,
compiler-core предоставляет функциональность компилятора, которая не зависит от DOM, такую как генерация и преобразование AST.
До сих пор мы реализовывали директиву v-on в compiler-core, но это просто преобразование нотации @click="handle"
в объект { onClick: handle }
,
Она не выполняет никакой обработки, которая зависит от DOM.
Теперь давайте посмотрим на то, что мы хотим реализовать в этот раз.
В этот раз мы хотим сгенерировать код, который фактически выполняет e.preventDefault()
или e.stopPropagation()
.
Эти функции сильно зависят от DOM.
Поэтому мы также реализуем трансформеры на стороне compiler-dom. Мы будем реализовывать трансформеры, связанные с DOM, здесь.
В compiler-core нам нужно рассмотреть взаимодействие между transform в compiler-core и transform, реализованным в compiler-dom.
Взаимодействие заключается в том, как реализовать transform, реализованный в compiler-dom, при выполнении transform в compiler-core.
Итак, сначала давайте изменим интерфейс DirectiveTransform
, реализованный в compiler-core.
export type DirectiveTransform = (
dir: DirectiveNode,
node: ElementNode,
context: TransformContext,
augmentor?: (ret: DirectiveTransformResult) => DirectiveTransformResult, // Добавлено
) => DirectiveTransformResult
Я добавил augmentor
.
Ну, это просто функция обратного вызова. Позволяя получать обратные вызовы как часть интерфейса DirectiveTransform
, мы делаем функцию transform расширяемой.
В compiler-dom мы реализуем трансформер, который оборачивает трансформеры, реализованные в compiler-core.
// Пример реализации
// Реализация на стороне compiler-dom
import { transformOn as baseTransformOn } from 'compiler-core'
export const transformOn: DirectiveTransform = (dir, node, context) => {
return baseTransformOn(dir, node, context, () => {
/** Реализуем собственную реализацию compiler-dom здесь */
return {
/** */
}
})
}
И если вы передадите этот transformOn
, реализованный на стороне compiler-dom, как опцию компилятору, это будет нормально.
Вот диаграмма отношений.
Вместо передачи всех трансформеров из compiler-dom, реализация по умолчанию реализована в compiler-core, и конфигурация позволяет добавлять дополнительные трансформеры.
С этим compiler-core может выполнять трансформеры без зависимости от DOM, а compiler-dom может реализовывать обработку, которая зависит от DOM, при выполнении трансформеров в compiler-core.
Реализация трансформера
Теперь давайте реализуем трансформер на стороне compiler-dom.
Как мы должны его преобразовать? Пока что, поскольку есть различные типы модификаторов, даже если мы просто говорим "модификатор", давайте классифицируем их, чтобы мы могли учитывать будущие возможности.
В этот раз мы реализуем "модификатор события". Давайте начнем с извлечения его как eventModifiers
.
const isEventModifier = makeMap(
// управление распространением событий
`stop,prevent,self`,
)
const resolveModifiers = (modifiers: string[]) => {
const eventModifiers = []
for (let i = 0; i < modifiers.length; i++) {
const modifier = modifiers[i]
if (isEventModifier(modifier)) {
eventModifiers.push(modifier)
}
}
return { eventModifiers }
}
Теперь, когда мы извлекли eventModifiers
, как мы должны его использовать? В итоге мы реализуем вспомогательную функцию под названием withModifiers
на стороне runtime-dom и преобразуем ее в выражение, которое вызывает эту функцию.
// runtime-dom/runtimeHelpers.ts
export const V_ON_WITH_MODIFIERS = Symbol()
export const transformOn: DirectiveTransform = (dir, node, context) => {
return baseTransform(dir, node, context, baseResult => {
const { modifiers } = dir
if (!modifiers.length) return baseResult
let { key, value: handlerExp } = baseResult.props[0]
const { eventModifiers } = resolveModifiers(modifiers)
if (eventModifiers.length) {
handlerExp = createCallExpression(context.helper(V_ON_WITH_MODIFIERS), [
handlerExp,
JSON.stringify(eventModifiers),
])
}
return {
props: [createObjectProperty(key, handlerExp)],
}
})
}
С этим реализация трансформера почти завершена.
Теперь давайте реализуем withModifiers
на стороне compiler-dom.
Реализация withModifiers
Давайте продолжим реализацию в runtime-dom/directives/vOn.ts.
Реализация очень проста.
Реализуем функцию-guard для модификаторов событий и реализуем ее так, чтобы она выполнялась столько раз, сколько модификаторов получено в массиве.
const modifierGuards: Record<string, (e: Event) => void | boolean> = {
stop: e => e.stopPropagation(),
prevent: e => e.preventDefault(),
self: e => e.target !== e.currentTarget,
}
export const withModifiers = (fn: Function, modifiers: string[]) => {
return (event: Event, ...args: unknown[]) => {
for (let i = 0; i < modifiers.length; i++) {
const guard = modifierGuards[modifiers[i]]
if (guard && guard(event)) return
}
return fn(event, ...args)
}
}
На этом реализация завершена.
Давайте проверим работу! Если содержимое ввода отражается на экране без перезагрузки страницы при нажатии кнопки, все в порядке!
Исходный код до этого момента: GitHub
Другие модификаторы
Теперь, когда мы зашли так далеко, давайте реализуем другие модификаторы.
Основной подход к реализации тот же.
Давайте классифицируем модификаторы следующим образом:
const keyModifiers = []
const nonKeyModifiers = []
const eventOptionModifiers = []
Затем сгенерируем необходимые карты и классифицируем их с помощью resolveModifiers
.
Два момента, на которые нужно обратить внимание:
- Разница между именем модификатора и фактическим именем DOM API
- Реализация новой вспомогательной функции для выполнения с определенными событиями клавиш (withKeys)
Пожалуйста, попробуйте реализовать, читая фактический код! Если вы дошли до этого момента, вы должны быть в состоянии это сделать.
Исходный код до этого момента: GitHub