Vue2 和 Vue3 处理 style 字符串值的差异

49 阅读3分钟

backgroundImage 引起的问题

<template>
  <div class="page" :style="'backgroundImage: url(' + url + ');padding: 50px'"></div>
</template>

<script>
export default {
  data() {
    return {
      url: 'https://v2.cn.vuejs.org/images/logo.svg'
    }
  }
}
</script>

以上这段代码在 Vue2 中表现如预期,背景图片正常渲染,但在 Vue3 中就出问题了,背景图片没显示,style 里并没有 background-image 属性。

Vue2 SFC Playground

Vue3 SFC Playground

改成如下形式 Vue2 和 Vue3 都能正常渲染:

<div class="page" :style="'background-image: url(' + url + ');padding: 50px'"></div>

当然官方文档没有推荐使用字符串语法,规范应该采用对象语法。

<div class="page" :style="{ backgroundImage: 'url(' + url + ')', padding: '50px' }"></div>

但为什么 <div class="page" :style="'backgroundImage: url(' + url + ');padding: 50px'"></div> 在 Vue2 能正常设置 backgroundImage,而 Vue3 就不行了呢?Vue2 和 Vue3 在处理 style 内联样式的字符串值有什么不同呢?下面就扒一扒源码一探究竟!

Vue2 为什么可以

先来看看 Vue2 是怎么处理的。核心代码如下:

src/platforms/web/util/style.ts

摘取重点代码,打印点日志看看。

解析 style 字符串值的逻辑:

export const parseStyleText = cached(function (cssText) {
  console.log(33333, cssText) // 打个日志看看
  const res = {}
  const listDelimiter = /;(?![^(]*\))/g
  const propertyDelimiter = /:(.+)/
  cssText.split(listDelimiter).forEach(function (item) {
    if (item) {
      const tmp = item.split(propertyDelimiter)
      tmp.length > 1 && (res[tmp[0].trim()] = tmp[1].trim())
    }
  })
  return res
})

// normalize possible array / string values into Object
export function normalizeStyleBinding(bindingStyle: any): Record<string, any> {
  if (Array.isArray(bindingStyle)) {
    return toObject(bindingStyle)
  }
  if (typeof bindingStyle === 'string') {
    return parseStyleText(bindingStyle)
  }
  return bindingStyle
}

src/platforms/web/runtime/modules/style.ts

设置 style 属性的逻辑:

const setProp = (el, name, val) => {
  console.log(22222, el, name, val) // 打个日志看看
  /* istanbul ignore if */
  if (cssVarRE.test(name)) {
    el.style.setProperty(name, val)
  } else if (importantRE.test(val)) {
    el.style.setProperty(
      hyphenate(name),
      val.replace(importantRE, ''),
      'important'
    )
  } else {
    const normalizedName = normalize(name)
    console.log(66666, normalizedName, val) // 打个日志看看
    if (Array.isArray(val)) {
      // Support values array created by autoprefixer, e.g.
      // {display: ["-webkit-box", "-ms-flexbox", "flex"]}
      // Set them one by one, and the browser will only set those it can recognize
      for (let i = 0, len = val.length; i < len; i++) {
        el.style[normalizedName!] = val[i]
      }
    } else {
      el.style[normalizedName!] = val
    }
  }
}

const vendorNames = ['Webkit', 'Moz', 'ms']

let emptyStyle
const normalize = cached(function (prop) {
  emptyStyle = emptyStyle || document.createElement('div').style
  prop = camelize(prop)
  if (prop !== 'filter' && prop in emptyStyle) {
    return prop
  }
  const capName = prop.charAt(0).toUpperCase() + prop.slice(1)
  for (let i = 0; i < vendorNames.length; i++) {
    const name = vendorNames[i] + capName
    if (name in emptyStyle) {
      return name
    }
  }
})
  • background-image

image.png

  • backgroundImage

image.png

从源码调试打印的日志可以看出:Vue2 会把 style 的字符串形式值解析拆分出单独的属性键值对,比如

  • :style="'background-image: url(' + url + ');padding: 50px''" 解析成 { background-image: 'url(https://v2.cn.vuejs.org/images/logo.svg)', padding: '50px' }

  • :style="'backgroundImage: url(' + url + ');padding: 50px''" 解析成 { backgroundImage: 'url(https://v2.cn.vuejs.org/images/logo.svg)', padding: '50px' }

normalize 方法是把短横线属性名转换成驼峰形式,所以两种形式最终都是执行:

el.style.backgroundImage = 'url(https://v2.cn.vuejs.org/images/logo.svg)'
el.style.padding = '50px'

这也就解释了为什么 Vue2 中 style="background-image: ..."style="backgroundImage: ..." 都支持。

Vue3 为什么不行

再来看看 Vue3 是怎么处理的。核心代码如下:

packages/runtime-dom/src/modules/style.ts

摘取重点代码,打印点日志看看。

export function patchStyle(el: Element, prev: Style, next: Style): void {
  console.log(123123, el, prev, next) // 打个日志看看
  const style = (el as HTMLElement).style
  const isCssString = isString(next)
  let hasControlledDisplay = false
  if (next && !isCssString) {
    if (prev) {
      if (!isString(prev)) {
        for (const key in prev) {
          if (next[key] == null) {
            setStyle(style, key, '')
          }
        }
      } else {
        for (const prevStyle of prev.split(';')) {
          const key = prevStyle.slice(0, prevStyle.indexOf(':')).trim()
          if (next[key] == null) {
            setStyle(style, key, '')
          }
        }
      }
    }
    for (const key in next) {
      if (key === 'display') {
        hasControlledDisplay = true
      }
      setStyle(style, key, next[key])
    }
  } else {
    if (isCssString) {
      if (prev !== next) {
        // #9821
        const cssVarText = (style as any)[CSS_VAR_TEXT]
        if (cssVarText) {
          ;(next as string) += ';' + cssVarText
        }
        style.cssText = next as string
        hasControlledDisplay = displayRE.test(next)
      }
    } else if (prev) {
      el.removeAttribute('style')
    }
  }
  // indicates the element also has `v-show`.
  if (vShowOriginalDisplay in el) {
    // make v-show respect the current v-bind style display when shown
    el[vShowOriginalDisplay] = hasControlledDisplay ? style.display : ''
    // if v-show is in hidden state, v-show has higher priority
    if ((el as VShowElement)[vShowHidden]) {
      style.display = 'none'
    }
  }
}
  • backgroundImage

image.png

  • background-image

image.png

最后执行的是如下分支逻辑:

    if (isCssString) {
      if (prev !== next) {
        // #9821
        const cssVarText = (style as any)[CSS_VAR_TEXT]
        if (cssVarText) {
          ;(next as string) += ';' + cssVarText
        }
        style.cssText = next as string
        hasControlledDisplay = displayRE.test(next)
      }
    }

可以看出,Vue3 的处理是直接将 style 的字符串值赋值给属性 cssText:style.cssText = next

el.style.cssText = 'background-image: url(https://v2.cn.vuejs.org/images/logo.svg);padding: 50px'

el.style.cssText = 'backgroundImage: url(https://v2.cn.vuejs.org/images/logo.svg);padding: 50px'

显然在style的cssText里使用backgroundImage是无效的,这种驼峰类型的style属性名称都是无效的。

image.png

CSSStyleDeclaration: cssText property - MDN

The cssText property of the CSSStyleDeclaration interface returns or sets the text of the element's inline style declaration only.

当然这也不能说是 Vue3 的bug,毕竟 style 的字符串值里使用 backgroundImage 本就不符合标准语法。

不过这也是 Vue2 项目升级 Vue3 潜在的一个坑。