事件修飾符
這次要做的事情
由於我們上次實作了 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 和解析器的實作
由於我們要向模板加入新語法,需要對解析器和 AST 進行更改。
首先,讓我們看看 AST。這很簡單,只需向 DirectiveNode 加入一個名為 modifiers(字串陣列)的屬性。
export interface DirectiveNode extends Node {
type: NodeTypes.DIRECTIVE
name: string
exp: ExpressionNode | undefined
arg: ExpressionNode | undefined
modifiers: string[] // 加入這個
}讓我們相應地實作解析器。
實際上,這很容易,因為它已經包含在從原始碼借用的正規運算式中。
function parseAttribute(
context: ParserContext,
nameSet: Set<string>,
): AttributeNode | DirectiveNode {
// .
// .
// .
const modifiers = match[3] ? match[3].slice(1).split('.') : [] // 從匹配結果中提取修飾符
return {
type: NodeTypes.DIRECTIVE,
name: dirName,
exp: value && {
type: NodeTypes.SIMPLE_EXPRESSION,
content: value.content,
isStatic: false,
loc: value.loc,
},
loc,
arg,
modifiers, // 包含在回傳中
}
}是的。透過這樣,AST 和解析器的實作就完成了。
compiler-dom/transform
讓我們稍微回顧一下目前的編譯器架構。
目前的設定如下。
當你再次理解 compiler-core 和 compiler-dom 的角色時, compiler-core 提供不相依於 DOM 的編譯器功能,如產生和轉換 AST。
到目前為止,我們在 compiler-core 中實作了 v-on 指令,但這只是將符號 @click="handle" 轉換為物件 { onClick: handle }, 它不執行任何相依於 DOM 的處理。
現在,讓我們看看這次我們想要實作的內容。 這次,我們想要產生實際執行 e.preventDefault() 或 e.stopPropagation() 的程式碼。 這些嚴重相依於 DOM。
因此,我們也將在 compiler-dom 端實作轉換器。我們將在這裡實作與 DOM 相關的轉換器。
在 compiler-core 中,我們需要考慮 compiler-core 中的 transform 和在 compiler-dom 中實作的 transform 之間的交互。 交互是如何在執行 compiler-core 中的 transform 的同時實作在 compiler-dom 中實作的 transform。
所以首先,讓我們修改在 compiler-core 中實作的 DirectiveTransform 介面。
export type DirectiveTransform = (
dir: DirectiveNode,
node: ElementNode,
context: TransformContext,
augmentor?: (ret: DirectiveTransformResult) => DirectiveTransformResult, // 加入
) => DirectiveTransformResult我加入了 augmentor。 嗯,這只是一個回呼函式。透過允許接收回呼作為 DirectiveTransform 介面的一部分,我們使轉換函式可擴充。
在 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 {
/** */
}
})
}如果你將在 compiler-dom 端實作的這個 transformOn 作為選項傳遞給編譯器,就可以了。 這是關係的圖表。 不是從 compiler-dom 傳遞所有轉換器,而是在 compiler-core 中實作預設實作,設定允許加入額外的轉換器。
透過這樣,compiler-core 可以執行不相依於 DOM 的轉換器,compiler-dom 可以在執行 compiler-core 中的轉換器的同時實作相依於 DOM 的處理。
轉換器的實作
現在,讓我們在 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,我們應該如何使用它?總之,我們將在 runtime-dom 端實作一個名為 withModifiers 的輔助函式,並將其轉換為呼叫該函式的運算式。
// 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)],
}
})
}透過這樣,轉換器的實作幾乎完成了。
現在讓我們在 compiler-dom 端實作 withModifiers。
withModifiers 的實作
讓我們在 runtime-dom/directives/vOn.ts 中繼續實作。
實作非常簡單。
為事件修飾符實作一個保護函式,並實作它,使其執行與陣列中接收的修飾符數量一樣多的次數。
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
