前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >因为一个写法,我翻烂了 vue 源码,这是 vue的问题吧,我要不要提 pr!

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

作者头像
zz_jesse
发布2023-09-20 20:18:55
1930
发布2023-09-20 20:18:55
举报
文章被收录于专栏:前端技术江湖

问题背景

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

那......

直接上代码吧:

代码语言:javascript
复制

<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的驼峰写法规则

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

代码语言:javascript
复制
<div :style="[baseStyles, overridingStyles]"></div>

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

源码如下:

代码语言:javascript
复制
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

代码语言:javascript
复制
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 函数 代码如下:

代码语言:javascript
复制
export const patchProp: DOMRendererOptions['patchProp'] = (
  el,
  key,
  prevValue,
  nextValue,
  isSVG = false,
  prevChildren,
  parentComponent,
  parentSuspense,
  unmountChildren
) => {
    
    patchStyle(el, prevValue, nextValue)

}

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

代码语言:javascript
复制
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源码中也给了我们对应的函数

代码语言:javascript
复制
//将连字符转化为转驼峰 '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函数中,处理即可,代码如下:

代码语言:javascript
复制
   if (normalized) {
        for (const key in normalized) {
          res[hyphenate(key)] = normalized[hyphenate(key)]
        }
      }

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

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

作者:老骥farmer

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-09-12 22:33,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端技术江湖 微信公众号,前往查看

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 问题背景
  • 问题探究过程
    • 探究vue文档
      • style绑定数据的规则&style的驼峰写法规则
  • 查看模板解析
    • 在vue-devtools 中可以直接查看编译结果
      • 在浏览器中查看
        • 排查createElementVNode
          • 样式更新
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档