關於指令(directive)
屬性繫結、事件繫結和v-modal
底層都是通過指令(directive
)實現的,那麼什麼是指令呢?我們一起看看Directive
的定義吧。
//檔案 ./src/directives/index.ts
export interface Directive<T = Element> {
(ctx: DirectiveContext<T>): (() => void) | void
}
指令(directive
)其實就是一個接受引數型別為DirectiveContext
並且返回cleanup
函式或啥都不返回的函式。那麼DirectiveContext
有是如何的呢?
//檔案 ./src/directives/index.ts
export interface DirectiveContext<T = Element> {
el: T
get: (exp?: string) => any // 獲取表示式字串運算後的結果
effect: typeof rawEffect // 用於新增副作用函式
exp: string // 表示式字串
arg?: string // v-bind:value或:value中的value, v-on:click或@click中的click
modifiers?: Record<string, true> // @click.prevent中的prevent
ctx: Context
}
深入v-bind
的工作原理
walk
方法在解析模板時會遍歷元素的特性集合el.attributes
,當屬性名稱name
匹配v-bind
或:
時,則呼叫processDirective(el, 'v-bind', value, ctx)
對屬性名稱進行處理並轉發到對應的指令函式並執行。
//檔案 ./src/walk.ts
// 為便於閱讀,我將與v-bind無關的程式碼都刪除了
const processDirective = (
el: Element,
raw, string, // 屬性名稱
exp: string, // 屬性值:表示式字串
ctx: Context
) => {
let dir: Directive
let arg: string | undefined
let modifiers: Record<string, true> | undefined // v-bind有且僅有一個modifier,那就是camel
if (raw[0] == ':') {
dir = bind
arg = raw.slice(1)
}
else {
const argIndex = raw.indexOf(':')
// 由於指令必須以`v-`開頭,因此dirName則是從第3個字元開始擷取
const dirName = argIndex > 0 ? raw.slice(2, argIndex) : raw.slice(2)
// 優先獲取內建指令,若查詢失敗則查詢當前上下文的指令
dir = builtInDirectives[dirName] || ctx.dirs[dirName]
arg = argIndex > 0 ? raw.slice(argIndex) : undefined
}
if (dir) {
// 由於ref不是用於設定元素的屬性,因此需要特殊處理
if (dir === bind && arg === 'ref') dir = ref
applyDirective(el, dir, exp, ctx, arg, modifiers)
}
}
當processDirective
根據屬性名稱匹配相應的指令和抽取入參後,就會呼叫applyDirective
來通過對應的指令執行操作。
//檔案 ./src/walk.ts
const applyDirective = (
el: Node,
dir: Directive<any>,
exp: string,
ctx: Context,
arg?: string
modifiers?: Record<string, true>
) => {
const get = (e = exp) => evaluate(ctx.scope, e, el)
// 指令執行後可能會返回cleanup函式用於執行資源釋放操作,或什麼都不返回
const cleanup = dir({
el,
get,
effect: ctx.effect,
ctx,
exp,
arg,
modifiers
})
if (cleanup) {
// 將cleanup函式新增到當前上下文,當上下文銷燬時會執行指令的清理工作
ctx.cleanups.push(cleanup)
}
}
現在我們終於走到指令bind
執行階段了
//檔案 ./src/directives/bind.ts
// 只能通過特性的方式賦值的屬性
const forceAttrRE = /^(spellcheck|draggable|form|list|type)$/
export const bind: Directive<Element & { _class?: string }> => ({
el,
get,
effect,
arg,
modifiers
}) => {
let prevValue: any
if (arg === 'class') {
el._class = el.className
}
effect(() => {
let value = get()
if (arg) {
// 用於處理v-bind:style="{color:'#fff'}" 的情況
if (modifiers?.camel) {
arg = camelize(arg)
}
setProp(el, arg, value, prevValue)
}
else {
// 用於處理v-bind="{style:{color:'#fff'}, fontSize: '10px'}" 的情況
for (const key in value) {
setProp(el, key, value[key], prevValue && prevValue[key])
}
// 刪除原檢視存在,而當前渲染的新檢視不存在的屬性
for (const key in prevValue) {
if (!value || !(key in value)) {
setProp(el, key, null)
}
}
}
prevValue = value
})
}
const setProp = (
el: Element & {_class?: string},
key: string,
value: any,
prevValue?: any
) => {
if (key === 'class') {
el.setAttribute(
'class',
normalizeClass(el._class ? [el._class, value] : value) || ''
)
}
else if (key === 'style') {
value = normalizeStyle(value)
const { style } = el as HTMLElement
if (!value) {
// 若`:style=""`則移除屬性style
el.removeAttribute('style')
}
else if (isString(value)) {
if (value !== prevValue) style.cssText = value
}
else {
// value為物件的場景
for (const key in value) {
setStyle(style, key, value[key])
}
// 刪除原檢視存在,而當前渲染的新檢視不存在的樣式屬性
if (prevValue && !isString(prevValue)) {
for (const key in prevValue) {
if (value[key] == null) {
setStyle(style, key, '')
}
}
}
}
}
else if (
!(el instanceof SVGElement) &&
key in el &&
!forceAttrRE.test(key)) {
// 設定DOM屬性(屬性型別可以是物件)
el[key] = value
// 留給`v-modal`使用的
if (key === 'value') {
el._value = value
}
} else {
// 設定DOM特性(特性值僅能為字串型別)
/* 由於`<input v-modal type="checkbox">`元素的屬性`value`僅能儲存字串,
* 通過`:true-value`和`:false-value`設定選中和未選中時對應的非字串型別的值。
*/
if (key === 'true-value') {
;(el as any)._trueValue = value
}
else if (key === 'false-value') {
;(el as any)._falseValue = value
}
else if (value != null) {
el.setAttribute(key, value)
}
else {
el.removeAttribute(key)
}
}
}
const importantRE = /\s*!important/
const setStyle = (
style: CSSStyleDeclaration,
name: string,
val: string | string[]
) => {
if (isArray(val)) {
val.forEach(v => setStyle(style, name, v))
}
else {
if (name.startsWith('--')) {
// 自定義屬性
style.setProperty(name, val)
}
else {
if (importantRE.test(val)) {
// 帶`!important`的屬性
style.setProperty(
hyphenate(name),
val.replace(importantRE, ''),
'important'
)
}
else {
// 普通屬性
style[name as any] = val
}
}
}
}
總結
通過本文我們以後不單可以使用v-bind:style
繫結單一屬性,還用通過v-bind
一次過繫結多個屬性,雖然好像不太建議這樣做>_<
後續我們會深入理解v-on
事件繫結的工作原理,敬請期待。