因为一个写法,我翻烂了vue源码,这是vue的问题吧,我要不要提pr!

53,534 阅读4分钟

问题背景

我已经老了。。。。 面对现在的观众不知该如何表达。既然这样的话

那......

直接上代码吧:


<template>
  <div>
    <div class="test" :style="[is ? {backgroundColor:'red'} : '',bg]">这是测试页面</div>
  </div>
</template>
<script setup>
import { ref } from 'vue'
const is=ref(true)
const bg = ref({'background-color': 'yellow'})
setInterval(() => {
       is.value=!is.value
}, 5000);
</script>
<style>
.test{
  height: 500px;
  width: 500px;
  text-align: center;
  line-height: 500px;
  font-size: 40px;
}
</style>

事情就发在昨天,在我们单位的办公大厅里,有一个产品向我走来。他主动介绍自己,他对我说,“老骥: 你这个页面有问题,很大很大的问题,现在我是特地来告诉你,对我来说,还得辛苦你给我解决问题”

我很慌乱.....

因为此时我的正在吃早饭,嘴里还有个茶叶蛋

我慌忙的咽了下去,提醒焦急的产品:

我知道你很急,但.....

请你不要着急!!

我得一点一点的排查问题。

具体业务问题就不交代了,复现代码请见开头

具体现象如下,请细品

Kapture 2023-06-14 at 17.10.50.gif

首先我设置了一个定时器,定时器中通过一个变量控制者绑定的style 在以上代码中,虽然定时器在不停的执行,

但是,由于bg这个值是个常量,理论上来说他的页面背景应该一直呈现黄色

有人问,为啥你要设置成黄色?

额,这不是重点,可能因为我们是黄种人

然而现实情况却在黄色和没有颜色之间徘徊,这是为什么?

问题探究过程

抱着好奇的态度我首先怀疑的是我的我对于vuestyle动态的值的绑定是不是理解的不透彻

探究vue文档

我怀着忐忑的心情,找到了vue文档,在文档中我只需要确认两点:

  • 1、style绑定数据的规则
  • 2、style的驼峰写法规则

style绑定数据的规则&style的驼峰写法规则

在他的官方文档中我们可以发现

<div :style="[baseStyles, overridingStyles]"></div>

他的绑定朴实无华,并且根据我翻看源码得出结论,数组后方的变量覆盖前方的变量

源码如下:

export function normalizeStyle(
  value: unknown
): NormalizedStyle | string | undefined {
  // 判断样式数组的情况
  if (isArray(value)) {
    // 最后格式化之后的样式对象
    const res: NormalizedStyle = {}
    // 对当前数组进行遍历,此处就可以预示着,在初始值后方的数组内容会覆盖前方的
    for (let i = 0; i < value.length; i++) {
      // 拿到数组中的每一项
      const item = value[i]
      // 如果是个字符串,那就表示这个样式需要解析
      const normalized = isString(item)
        ? parseStringStyle(item)
        // 否则防止是个多维数组,递归调用最终将所有的都放在res中
        : (normalizeStyle(item) as NormalizedStyle)
      if (normalized) {
        for (const key in normalized) {
          res[key] = normalized[key]
        }
      }
    }
    return res
    // 其他情况暂且不看
  } else if (isString(value)) {
    return value
  } else if (isObject(value)) {
    return value
  }
}

同样是通过上述源码中内容可以发现,他并没有对于类似background-color以及 backgroundColor做统一的格式处理,这个所谓的normalizeStyle其实就是将绑定的值,做一个集成处理,方便在后续绑定的时候做统一的处理循环绑定。

此时我们先排除了代码的写法错误,接下来我的排查方向其实应该就是vue源码中的蛛丝马迹

于是我首先将问题定位在了源码中的的模板解析错误

查看模板解析

我们知道vue的模板的的编译结果是可以在浏览器中查看的,具体查看方式有两种

vue-devtools 中可以直接查看编译结果

image.png

从源码中我们可以看到他先调用上方的normalizeStyle方法对绑定样式做处理,在调用createElementVNode 去创建vnode

当然如果你嫌弃vue提供的不清不楚,不头不尾,别急。。。。

我们在浏览器的控制台中也能看到端倪

在浏览器中查看

image.png

如上图所示,在开发环境下,我们利用 sourcemap,可以完美的查看到整个代码结构,以及编译后的源码,包括他的引用链条,并且他还可以打断点!

从上述代码中我们可以清楚的发现,这个常亮的值确实被编译成功了

那既然这样的话,我就开始怀疑是createElementVNode 的问题

排查createElementVNode

function createBaseVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,// vnode类型
  props: (Data & VNodeProps) | null = null,// 属性
  children: unknown = null,// 子节点
  patchFlag = 0, // 补丁标记
  dynamicProps: string[] | null = null,
  shapeFlag = type === Fragment ? 0 : ShapeFlags.ELEMENT,
  isBlockNode = false,
  needFullChildrenNormalization = false
) {
  // 创建vnode
  const vnode = {
    __v_isVNode: true,
    __v_skip: true,
    type,
    props,// 中间包含style内容
    key: props && normalizeKey(props),
    ref: props && normalizeRef(props),
    scopeId: currentScopeId,
    slotScopeIds: null,
    children,
    component: null,
    suspense: null,
    ssContent: null,
    ssFallback: null,
    dirs: null,
    transition: null,
    el: null,
    anchor: null,
    target: null,
    targetAnchor: null,
    staticCount: 0,
    shapeFlag,
    patchFlag,
    dynamicProps,
    dynamicChildren: null,
    appContext: null,
    ctx: currentRenderingInstance
  } as VNode
  if (needFullChildrenNormalization) {
    normalizeChildren(vnode, children)
    if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
      ; (type as typeof SuspenseImpl).normalize(vnode)
    }
  } else if (children) {
    // compiled element vnode - if children is passed, only possible types are
    // string or Array.
    vnode.shapeFlag |= isString(children)
      ? ShapeFlags.TEXT_CHILDREN
      : ShapeFlags.ARRAY_CHILDREN
  }


  if (__DEV__ && vnode.key !== vnode.key) {
    warn(`VNode created with invalid key (NaN). VNode type:`, vnode.type)
  }


  if (
    isBlockTreeEnabled > 0 &&
    !isBlockNode &&
    currentBlock &&
    (vnode.patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) &&
    vnode.patchFlag !== PatchFlags.HYDRATE_EVENTS
  ) {
    currentBlock.push(vnode)
  }

  if (__COMPAT__) {
    convertLegacyVModelProps(vnode)
    defineLegacyVNodeProperties(vnode)
  }

  return vnode
}

export { createBaseVNode as createElementVNode }

从以上代码中我们发现,其实createElementVNode主要做的事情只有一个,就是创建vnode 并且vnode中是包含样式信息的

效果图如下:

image.png

从上图中我们可以发现,他确实包含两个属性,那就表示,这个vnode中应该是包含所有的style信息,并没有缺失,那么就只能是样式更新的问题了

样式更新

说起样式更新,我们还得老规矩,从丘处机路过牛家村开始

在样式的更新操作中,避免不了patch 函数,以及diff过程,这个过程的主流程咱就不过多赘述了,讲的人已经够多了,俺嘴皮子磨破,也就那么两句,没啥新意,俺就主要讲讲diff过程中的跟样式有关的内容,在diff的过程中,有很多的类型改变响应的处理函数,而我们的props的处理对应的就是patchProp 函数 代码如下:

export const patchProp: DOMRendererOptions['patchProp'] = (
  el,
  key,
  prevValue,
  nextValue,
  isSVG = false,
  prevChildren,
  parentComponent,
  parentSuspense,
  unmountChildren
) => {
    
    patchStyle(el, prevValue, nextValue)

}

而在patchProp 函数中还有patchStyle函数,用用来专门处理内联样式,代码如下:

export function patchStyle(el: Element, prev: Style, next: Style) {
  // 拿到style样式
  const style = (el as HTMLElement).style
  const isCssString = isString(next)
  //如果不是字符窜
  if (next && !isCssString) {
    // 遍历对象 设置style
    for (const key in next) {
      setStyle(style, key, next[key])
    }
    // 老的style删除
    if (prev && !isString(prev)) {
      for (const key in prev) {
        // 优化手段,如果新的节点没有,那么就表示需要删除,如果按照正常思维,
        //应该是先给老的全删了新的全加上
        if (next[key] == null) {
          setStyle(style, key, '')
        }
      }
    }
  } else {
    // 字符串的情况我们暂且不论
    const currentDisplay = style.display
    if (isCssString) {
      if (prev !== next) {
        style.cssText = next as string
      }
    } else if (prev) {
      el.removeAttribute('style')
    }
    if ('_vod' in el) {
      style.display = currentDisplay
    }
  }
}

看到这,我相信大家已经一目了然了

image.png

image.png

根本原因就是在vue内部没有样式写法做标准化统一, 经过测试,vue2也会有这个问题,

所以,我就怀疑这是不是尤大是故意为之,他不允许你这么书写

其实据我粗浅的理解,解决方式非常简单,我们只需要将代码标准化为驼峰写法,或者连字符写法即可,并且vue3源码中也给了我们对应的函数

//将连字符转化为转驼峰 'on-click' => 'onClick'
const camelizeRE = /-(\w)/g

const camelize = cacheStringFunction(str => {
  return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''))
})


//将小驼峰转化为连字符字符串 'onClick' => 'on-click'

onst hyphenateRE = /\B([A-Z])/g const hyphenate = cacheStringFunction((str: string) => str.replace(hyphenateRE, '-$1').toLowerCase() )

而我们只需要在normalizeStyle函数中,处理即可,代码如下:

   if (normalized) {
        for (const key in normalized) {
          res[hyphenate(key)] = normalized[hyphenate(key)]
        }
      }

好了,问题排查完毕,然后需要发出灵魂一问?

vue源码中是刻意不解决这个问题吗? 他是一个使用场景的取舍吗? 可有大佬告知?