• 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事件绑定的工作原理,敬请期待。
    尊重原创,转载请注明来自:https://www.cnblogs.com/fsjohnhuang/p/15981570.html 肥仔John

  • 相关阅读:
    PyCharm不能使用Tab键进行整体向左缩进解决方法
    Python代码规范(PEP8)问题及解决
    Python学习开始
    Spring Annotation(@Autowire、@Qualifier)
    Spring自动装配
    servlet验证码
    Spring集合装配
    帐号明文传输漏洞
    java单元测试
    项目building workspace很慢,或者直接内存溢出的问题解决办法。
  • 原文地址:https://www.cnblogs.com/fsjohnhuang/p/15981570.html
Copyright © 2020-2023  润新知