关于指令(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事件绑定的工作原理,敬请期待。
尊重原创,转载请注明来自:https://cloud.tencent.com/developer/article/1997338 肥仔John