petite-vue源码剖析-属性绑定`v-bind`的工作原理

vue

关于指令(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事件绑定的工作原理,敬请期待。

以上是 petite-vue源码剖析-属性绑定`v-bind`的工作原理 的全部内容, 来源链接: utcz.com/z/380835.html

回到顶部