petite-vue源码剖析-属性绑定`v-bind`的工作原理
关于指令(directive)
【petite-vue源码剖析-属性绑定`v-bind`的工作原理】属性绑定、事件绑定和v-modal
底层都是通过指令(directive
)实现的,那么什么是指令呢?我们一起看看Directive
的定义吧。
//文件 ./src/directives/index.tsexport interface Directive {
(ctx: DirectiveContext): (() => void) | void
}
指令(
directive
)其实就是一个接受参数类型为DirectiveContext
并且返回cleanup函数或啥都不返回的函数。那么
DirectiveContext
有是如何的呢?//文件 ./src/directives/index.tsexport interface DirectiveContext {
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 // @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 | undefined // v-bind有且仅有一个modifier,那就是camelif (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.tsconst applyDirective = (
el: Node,
dir: Directive,
exp: string,
ctx: Context,
arg?: string
modifiers?: Record
) => {
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 => ({
el,
get,
effect,
arg,
modifiers
}) => {
let prevValue: any
if (arg === 'class') {
el._class = el.className
}effect(() => {
let value = https://www.it610.com/article/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 = https://www.it610.com/article/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 = https://www.it610.com/article/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 = https://www.it610.com/article/value
}
} else {
// 设置DOM特性(特性值仅能为字符串类型)/* 由于``元素的属性`value`仅能存储字符串,
* 通过`:true-value`和`:false-value`设置选中和未选中时对应的非字符串类型的值。
*/
if (key === 'true-value') {
;
(el as any)._trueValue = https://www.it610.com/article/value
}
else if (key ==='false-value') {
;
(el as any)._falseValue = https://www.it610.com/article/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
事件绑定的工作原理,敬请期待。推荐阅读
- JAVA|JAVA ArrayList集合底层源码分析
- Vue|Vue 源码解读(11)—— render helper
- 用Assert(断言)封装异常,让代码更优雅(附项目源码)
- Java8中AbstractExecutorService与FutureTask源码详解
- Mybatis-Spring源码分析图解
- 号称国内著名的免费精品源码分享社区莎莎源码论坛疑似倒闭了
- 分享源码学习技术|亲测-分享最新微信付费进群收费进群系统源码-附带搭建教
- JDK|阅读 JDK 源码(HashMap 扩容总结及图解)
- 【源码】vuex@3.6.3
- C#调用Oracle存储过程方法介绍(附源码)