美文网首页
如何理解vue中的v-bind?

如何理解vue中的v-bind?

作者: 趁你还年轻233 | 来源:发表于2020-03-24 12:41 被阅读0次
    v-bind.jpg

    如果你写过vue,对v-bind这个指令一定不陌生。
    下面我将从源码层面去带大家剖析一下v-bind背后的原理。

    会从以下几个方面去探索:

    • v-bind关键源码分析
      • v-bind化的属性统一存储在哪里:attrsMap与attrsList
      • 绑定属性获取函数 getBindingAttr 和 属性操作函数 getAndRemoveAttr
    • v-bind如何处理不同的绑定属性
      • v-bind:key源码分析
      • v-bind:title源码分析
      • v-bind:class源码分析
      • v-bind:style源码分析
      • v-bind:text-content.prop源码分析
      • v-bind的修饰符.camel .sync源码分析

    v-bind关键源码分析

    v-bind化的属性统一存储在哪里:attrsMap与attrsList

    <p v-bind:title="vBindTitle"></p>
    

    假设为p标签v-bind化了title属性,我们来分析title属性在vue中是如何被处理的。

    vue在拿到这个html标签之后,处理title属性,会做以下几步:

    • 解析HTML,解析出属性集合attrs,在start回调中返回
    • 在start回调中创建ASTElement,createASTElement(... ,attrs, ...)
    • 创建后ASTElement会生成attrsList和attrsMap

    至于创建之后是如何处理v-bind:title这种普通的属性值的,可以在下文的v-bind:src源码分析中一探究竟。

    解析HTML,解析出属性集合attrs,在start回调中返回
      function handleStartTag (match) {
        ...
        const l = match.attrs.length
        const attrs = new Array(l)
        for (let i = 0; i < l; i++) {
          const args = match.attrs[i]
          ...
          attrs[i] = {
            name: args[1],
            value: decodeAttr(value, shouldDecodeNewlines)
          }
        }
       ...
        if (options.start) {
          // 在这里上传到start函数
          options.start(tagName, attrs, unary, match.start, match.end)
        }
      }
    

    在start回调中创建ASTElement,createASTElement(... ,attrs, ...)

    // 解析HMTL
    parseHTML(template, {
        ...
        start(tag, attrs, unary, start, end) {
            let element: ASTElement = createASTElement(tag, attrs, currentParent) // 注意此处的attrs
        }
    })
    

    创建后ASTElement会生成attrsList和attrsMap

    // 创建AST元素
    export function createASTElement (
      tag: string,
      attrs: Array<ASTAttr>, // 属性对象数组
      parent: ASTElement | void // 父元素也是ASTElement
    ): ASTElement { // 返回的也是ASTElement
      return {
        type: 1,
        tag,
        attrsList: attrs,
        attrsMap: makeAttrsMap(attrs),
        rawAttrsMap: {},
        parent,
        children: []
      }
    }
    

    attrs的数据类型定义

    // 声明一个ASTAttr 属性抽象语法树对象 数据类型
    declare type ASTAttr = {
      name: string; // 属性名
      value: any; // 属性值
      dynamic?: boolean; // 是否是动态属性
      start?: number;
      end?: number
    };
    

    绑定属性获取函数 getBindingAttr 和 属性操作函数 getAndRemoveAttr

    getBindingAttr及其子函数getAndRemoveAttr在处理特定场景下的v-bind十分有用,也就是”v-bind如何处理不同的绑定属性“章节很有用。
    这里将其列举出来供下文v-bind:key源码分析;v-bind:src源码分析;v-bind:class源码分析;v-bind:style源码分析;v-bind:dataset.prop源码分析源码分析参照。

    export function getBindingAttr (
      el: ASTElement,
      name: string,
      getStatic?: boolean
    ): ?string {
      const dynamicValue =
        getAndRemoveAttr(el, ':' + name) ||
        getAndRemoveAttr(el, 'v-bind:' + name)
      if (dynamicValue != null) {
        return parseFilters(dynamicValue)
      } else if (getStatic !== false) {
        const staticValue = getAndRemoveAttr(el, name)
        if (staticValue != null) {
          return JSON.stringify(staticValue)
        }
      }
    }
    
    // note: this only removes the attr from the Array (attrsList) so that it
    // doesn't get processed by processAttrs.
    // By default it does NOT remove it from the map (attrsMap) because the map is
    // needed during codegen.
    export function getAndRemoveAttr (
      el: ASTElement,
      name: string,
      removeFromMap?: boolean
    ): ?string {
      let val
      if ((val = el.attrsMap[name]) != null) {
        const list = el.attrsList
        for (let i = 0, l = list.length; i < l; i++) {
          if (list[i].name === name) {
            list.splice(i, 1) // 从attrsList删除一个属性,不会从attrsMap删除
            break
          }
        }
      }
      if (removeFromMap) {
        delete el.attrsMap[name]
      }
      return val
    }
    

    如何获取v-bind的值

    以下面代码为例从源码分析vue是如何获取v-bind的值。

    会从记下几个场景去分析:

    • 常见的key属性
    • 绑定一个普通html attribute:title
    • 绑定class和style
    • 绑定一个html DOM property:textContent
    vBind:{
        key: +new Date(),
        title: "This is a HTML attribute v-bind",
        class: "{ borderRadius: isBorderRadius }"
        style: "{ minHeight: 100 + 'px' , maxHeight}"
        text-content: "hello vue v-bind"
    }
    
    <div
       v-bind:key="vBind.key"
       v-bind:title="vBind.title"
       v-bind:class="vBind.class"
       v-bind:style="vBind.style"
       v-bind:text-content.prop="vBind.textContent"
     />
    </div>
    

    v-bind:key源码分析

    function processKey (el) {
      const exp = getBindingAttr(el, 'key')
       if(exp){
          ...
          el.key = exp;
       }
    }
    

    processKey函数中用到了getBindingAttr函数,由于我们用的是v-bind,没有用:,所以const dynamicValue = getAndRemoveAttr(el, 'v-bind:'+'key');,getAndRemoveAttr(el, 'v-bind:key')函数到attrsMap中判断是否存在'v-bind:key',取这个属性的值赋为val并从从attrsList删除,但是不会从attrsMap删除,最后将'v-bind:key'的值,也就是val作为dynamicValue,之后再返回解析过滤后的结果,最后将结果set为processKey中将元素的key property。然后存储在segments中,至于segments是什么,在上面的源码中可以看到。

    v-bind:title源码分析

    title是一种“非vue特殊的”也就是普通的HTML attribute。

    function processAttrs(el){
         const list = el.attrsList;
         ...
         if (bindRE.test(name)) { // v-bind
            name = name.replace(bindRE, '')
            value = parseFilters(value)
            ...
            addAttr(el, name, value, list[i], ...)
          }
    }
    export const bindRE = /^:|^\.|^v-bind:/
    export function addAttr (el: ASTElement, name: string, value: any, range?: Range, dynamic?: boolean) {
      const attrs = dynamic
        ? (el.dynamicAttrs || (el.dynamicAttrs = []))
        : (el.attrs || (el.attrs = []))
      attrs.push(rangeSetItem({ name, value, dynamic }, range))
      el.plain = false
    }
    

    通过阅读源码我们看出:对于原生的属性,比如title这样的属性,vue会首先解析出name和value,然后再进行一系列的是否有modifiers的判断(modifier的部分在下文中会详细讲解),最终向更新ASTElement的attrs,从而attrsList和attrsMap也同步更新。

    v-bind:class源码分析

    css的class在前端开发的展现层面,是非常重要的一层。
    因此vue在对于class属性也做了很多特殊的处理。

    function transformNode (el: ASTElement, options: CompilerOptions) {
      const warn = options.warn || baseWarn
      const staticClass = getAndRemoveAttr(el, 'class')
      if (staticClass) {
        el.staticClass = JSON.stringify(staticClass)
      }
      const classBinding = getBindingAttr(el, 'class', false /* getStatic */)
      if (classBinding) {
        el.classBinding = classBinding
      }
    }
    

    在transfromNode函数中,会通过getAndRemoveAttr得到静态class,也就是class="foo";在getBindingAttr得到绑定的class,也就是v-bind:class="vBind.class"v-bind:class="{ borderRadius: isBorderRadius }",将ASTElement的classBinding赋值为我们绑定的属性供后续使用。

    v-bind:style源码分析

    style是直接操作样式的优先级仅次于important,比class更加直观的操作样式的一个HTML attribute。
    vue对这个属性也做了特殊的处理。

    function transformNode (el: ASTElement, options: CompilerOptions) {
      const warn = options.warn || baseWarn
      const staticStyle = getAndRemoveAttr(el, 'style')
      if (staticStyle) {
        el.staticStyle = JSON.stringify(parseStyleText(staticStyle))
      }
      const styleBinding = getBindingAttr(el, 'style', false /* getStatic */)
      if (styleBinding) {
        el.styleBinding = styleBinding
      }
    }
    

    在transfromNode函数中,会通过getAndRemoveAttr得到静态style,也就是style="{fontSize: '12px'}";在getBindingAttr得到绑定的style,也就是v-bind:style="vBind.style"v-bind:class={ minHeight: 100 + 'px' , maxHeight}",其中maxHeight是一个变量,将ASTElement的styleBinding赋值为我们绑定的属性供后续使用。

    v-bind:text-content.prop源码分析

    textContent是DOM对象的原生属性,所以可以通过prop进行标识。
    如果我们想对某个DOM prop直接通过vue进行set,可以在DOM节点上做修改。

    下面我们来看源码。

    function processAttrs (el) {
      const list = el.attrsList
      ...
      if (bindRE.test(name)) { // v-bind
          if (modifiers) {
              if (modifiers.prop && !isDynamic) {
                name = camelize(name)
                if (name === 'innerHtml') name = 'innerHTML'
              }
           }
           if (modifiers && modifiers.prop) {
              addProp(el, name, value, list[i], isDynamic)
            }
       }
    }
    export function addProp (el: ASTElement, name: string, value: string, range?: Range, dynamic?: boolean) {
      (el.props || (el.props = [])).push(rangeSetItem({ name, value, dynamic }, range))
      el.plain = false
    }
    props?: Array<ASTAttr>;
    

    通过上面的源码我们可以看出,v-bind:text-content.prop中的text-content首先被驼峰化为textContent(这是因为DOM property都是驼峰的格式),vue还对innerHtml错误写法做了兼容也是有心,之后再通过prop标识符,将textContent属性增加到ASTElement的props中,而这里的props本质上也是一个ASTAttr。

    有一个很值得思考的问题:为什么要这么做?与HTML attribute有何异同?

    • 没有HTML attribute可以直接修改DOM的文本内容,所以需要单独去标识
    • 比通过js去手动更新DOM的文本节点更加快捷,省去了查询dom然后替换文本内容的步骤
    • 在标签上即可看到我们对哪个属性进行了v-bind,非常直观
    • 其实v-bind:title可以理解为v-bind:title.attr,v-bind:text-content.prop只不过vue默许不加修饰符的就是HTML attribute罢了

    v-bind的修饰符.camel .sync源码分析

    .camel仅仅是驼峰化,很简单。
    但是.sync就不是这么简单了,它会扩展成一个更新父组件绑定值的v-on侦听器。

    其实刚开始看到这个.sync修饰符我是一脸懵逼的,但是仔细阅读一下组件的.sync再结合实际工作,就会发现它的强大了。

    <Parent
      v-bind:foo="parent.foo"
      v-on:updateFoo="parent.foo = $event"
    ></Parent>
    

    在vue中,父组件向子组件传递的props是无法被子组件直接通过this.props.foo = newFoo去修改的。
    除非我们在组件this.$emit("updateFoo", newFoo),然后在父组件使用v-on做事件监听updateFoo事件。若是想要可读性更好,可以在$emit的name上改为update:foo,然后v-on:update:foo。

    有没有一种更加简洁的写法呢???
    那就是我们这里的.sync操作符。
    可以简写为:

    <Parent v-bind:foo.sync="parent.foo"></Parent>
    

    然后在子组件通过this.$emit("update:foo", newFoo);去触发,注意这里的事件名必须是update:xxx的格式,因为在vue的源码中,使用.sync修饰符的属性,会自定生成一个v-on:update:xxx的监听。

    下面我们来看源码:

    if (modifiers.camel && !isDynamic) {
      name = camelize(name)
    }
    if (modifiers.sync) {
      syncGen = genAssignmentCode(value, `$event`)
      if (!isDynamic) {
        addHandler(el,`update:${camelize(name)}`,syncGen,null,false,warn,list[i]) 
       // Hyphenate是连字符化函数,其中camelize是驼峰化函数
        if (hyphenate(name) !== camelize(name)) {
          addHandler(el,`update:${hyphenate(name)}`,syncGen,null,false,warn,list[i])
        }
      } else {
        // handler w/ dynamic event name
        addHandler(el,`"update:"+(${name})`,syncGen,null,false,warn,list[i],true)
      }
    }
    

    通过阅读源码我们可以看到:
    对于v-bind:foo.sync的属性,vue会判断属性是否为动态属性。
    若不是动态属性,首先为其增加驼峰化后的监听,然后再为其增加一个连字符的监听,例如v-bind:foo-bar.sync,首先v-on:update:fooBar,然后v-on:update:foo-bar。v-on监听是通过addHandler加上的。
    若是动态属性,就不驼峰化也不连字符化了,通过addHandler(el,update:${name}, ...),老老实实监听那个动态属性的事件。

    一句话概括.sync:
    .sync是一个语法糖,简化v-bind和v-on为v-bind.sync和this.$emit('update:xxx')。为我们提供了一种子组件快捷更新父组件数据的方式。

    参考资料:
    https://cn.vuejs.org/v2/api/#v-bind
    https://github.com/vuejs/vue/tree/dev/src
    https://cn.vuejs.org/v2/guide/components-custom-events.html#sync-%E4%BF%AE%E9%A5%B0%E7%AC%A6

    相关文章

      网友评论

          本文标题:如何理解vue中的v-bind?

          本文链接:https://www.haomeiwen.com/subject/uvroyhtx.html