Vue VNode更新方式引发的BUG

近几日在调试一些“祖传代码”,遇到了vue更新使用VNode方式时带来的一些问题,于是乎费劲“九牛二虎”之力,终于找到了问题的根源,以后在此类代码设计中也算是增加了点经验,避免再次踩坑。项目中所使用的Vue版本:

Vue.js v2.6.12

问题背景

由于原本的业务代码经过不断“迭代”已经比较复杂,相关操作逻辑散落在“代码海洋”的各处,这里简单描述一下问题的场景。

组件在一个区域内可以自由拖动位置,有以下两种方法可以改变组件的位置

  • 鼠标移动组件
  • 通过快捷方式将组件快速移动到特定位置

使用简单的示意图

image.png

红色区块可以通过鼠标进行移动,也可以通过右边两个按钮可以分布直接定位到左上角、右上角。业务场景经过简化后,其实是比较简单和清晰的,但是真实情况中,由于各个业务逻辑耦合,导致各个代码功能耦合,还是比较难以直接定位问题关键原因的。

问题现象

通过鼠标移动红色区块后,再次通过右边两个快捷按钮进行定位,无法定位到左上角或者右上角,只能实现靠左以及靠右定位的效果。有兴趣的可以自己操作以下尝试一番,代码链接

问题原因

首先可以确认的是点击右边按钮之后,vuex中的属性值被改变成了期望值

这点可以借助与vue的调试工具来证明

image.png

这时我们可以再看一下代码中样式渲染逻辑

<div ref="container" class="root">
  <div ref="el" class="block" :style="style"></div>
</div>
复制代码

也就是说,在vuex的style属性值更新后,Vue并没有能正确渲染组件el的样式,那究竟是什么原因造成这一结果呢???

Vue响应式原理

经过一番定位后,基本上可以确认是Vue在重新渲染组件样式时出了问题,组件的样式并没有按照我们的设定值进行渲染,因为渲染出来的组件样式,与我们设置的style值并不一致, 此时通过chrome调试功能查看组件的样式,可以看到组件的样式为:

<div data-v-160ac6a1="" class="block" style="inset: 38px auto auto 0px;"></div>
复制代码

组件的top值被渲染成了38px,并不是我们所期望的0。

此时我们不得不深入Vue的响应式中进行查看,正如大家所知道的那样,组件的渲染其实是一个大的Watcher,在相应状态改变后,会调用这个Watcher对页面进行渲染,给各个组件设置正确的状态。

而在组件的渲染中,Vue为了减少渲染的工作量,尽量避免渲染不需要改变的节点,使用了VNode的概念,每个VNode与真实渲染的DOM一一对应,在渲染真实的DOM之前,会对更新前后的VNode状态进行比较,如果更改前后的VNode状态是一样的,Vue就会跳过这个VNode对应DOM的更新

VNode更新

于是,我们就是查看Vue更新Style的函数,由于代码较多,这里省略一些分支判断逻辑和注释,关注代码主干逻辑。

function updateStyle (oldVnode, vnode) {
  var data = vnode.data;
  var oldData = oldVnode.data;
  var cur, name;
  var el = vnode.elm;
  var oldStaticStyle = oldData.staticStyle;
  var oldStyleBinding = oldData.normalizedStyle || oldData.style || {};
  // ...
  var oldStyle = oldStaticStyle || oldStyleBinding;
  var newStyle = getStyle(vnode, true);
  for (name in oldStyle) {
    if (isUndef(newStyle[name])) {
      setProp(el, name, '');
    }
  }
  for (name in newStyle) {
    cur = newStyle[name];
    if (cur !== oldStyle[name]) {
      setProp(el, name, cur == null ? '' : cur);
    }
  }
}
复制代码

在setProp之前,会对更新前后的VNode上的style值进行比较,如果更新前后VNode的值相同就会跳过这个属性的更新。

通过chrome调试控制台查看newStyle和oldStyle值:

image.png

基本上可以看出来问题所在了,VNode中的top值仍然是0,并不是当前DOM真实的top值

问题根因

为什么VNode中的值和DOM中的真实值不一致呢,这时应该就要考虑出了通过Vue响应式原理更新DOM属性值,是不是还有其他地方通过其他方式更新了DOM属性值?

很明显,在组件拖动过程中,我们通过直接改变DOM的值改变了DOM的属性状态,但是并没有改变vuex中style的值,造成了VNode的状态和DOM中状态不一致的结果。

解决方法

在移动过程中通过改变vuex中style属性值来改变组件的样式,而不是通过直接改变DOM样式。

思考:收敛更改状态的入口,可以在日常业务的代码海洋中减少此类问题的出现频率。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享