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

关于指令(directive)

属性绑定、事件绑定和v-modal底层都是通过指令(directive)实现的,那么什么是指令呢?我们一起看看Directive的定义吧。

代码语言:javascript
AI代码解释
复制
//文件 ./src/directives/index.ts export interface Directive<T = Element> { (ctx: DirectiveContext<T>): (() => void) | void }

指令(directive)其实就是一个接受参数类型为DirectiveContext并且返回cleanup

函数或啥都不返回的函数。那么DirectiveContext有是如何的呢?

代码语言:javascript
AI代码解释
复制
//文件 ./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)对属性名称进行处理并转发到对应的指令函数并执行。

代码语言:javascript
AI代码解释
复制
//文件 ./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来通过对应的指令执行操作。

代码语言:javascript
AI代码解释
复制
//文件 ./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执行阶段了

代码语言:javascript
AI代码解释
复制
//文件 ./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

上一篇 petite-vue-源码剖析-v-for重新渲染工作原理
下一篇 petite-vue源码剖析-v-if和v-for的工作原理