1
头图

About directives

Property binding, event binding and v-modal the bottom layer are all implemented through instructions ( directive ), so what are instructions? Let's take a look at the definition of Directive .

 //文件 ./src/directives/index.ts

export interface Directive<T = Element> {
  (ctx: DirectiveContext<T>): (() => void) | void
}

The instruction ( directive ) is actually a parameter that accepts a parameter type of DirectiveContext and returns cleanup
A function or a function that returns nothing. So DirectiveContext how about there?

 //文件 ./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
}

In-depth v-bind how it works

walk characteristic method when parsing the template will traverse the collection of elements el.attributes , when the attribute name name match v-bind or : , call processDirective(el, 'v-bind', value, ctx) to process the attribute name and forward it to the corresponding instruction function and execute it.

 //文件 ./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)
  }
}

When processDirective matches the corresponding instruction according to the attribute name and extracts the input parameters, it will call applyDirective to execute the operation through the corresponding instruction.

 //文件 ./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)
  }
}

Now we finally reach the execution stage of the instruction 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
      }
    }
  }
}

Summarize

Through this article, we can not only use v-bind:style to bind a single attribute, but also use v-bind to bind multiple attributes at one time, although it is not recommended to do so>_<

In the future, we will deeply understand the working principle of v-on event binding, so stay tuned.

"Anatomy of Petite-Vue Source Code" booklet

"Petite-Vue Source Code Analysis" combines examples to interpret the source code line by line from online rendering, responsive system and sandbox model, and also makes a detailed analysis of the SMI optimization dependency cleaning algorithm using the JS engine in the responsive system. It is definitely an excellent stepping stone before getting started with Vue3 source code. If you like it, remember to forward it and appreciate it!


肥仔John
2.8k 声望1.8k 粉丝

《Petite-Vue源码剖析》作者