乐趣区

关于javascript:petitevue源码剖析属性绑定vbind的工作原理

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

退出移动版