首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >petite-vue源码剖析-属性绑定`v-bind`的工作原理

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

作者头像
^_^肥仔John
发布2022-05-09 16:04:34
4140
发布2022-05-09 16:04:34
举报

关于指令(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://cloud.tencent.com/developer/article/1997338 肥仔John

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-03-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 关于指令(directive)
  • 深入v-bind的工作原理
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档