我已经老了。。。。面对现在的观众不知该如何表达
。既然这样的话
那......
直接上代码吧:
<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
这个值是个常量,理论上来说他的页面背景应该一直呈现黄色
有人问,为啥你要设置成黄色?
额,这不是重点,可能因为我们是黄种人
。
然而现实情况却在黄色和没有颜色之间徘徊,这是为什么?
抱着好奇的态度我首先怀疑的是我的我对于vue
的style
动态的值的绑定是不是理解的不透彻
我怀着忐忑的心情,找到了vue文档,在文档中我只需要确认两点:
在他的官方文档中我们可以发现
<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
的问题
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
源码中是刻意不解决这个问题吗?他是一个使用场景的取舍吗?可有告知?
作者:老骥farmer