骑虫历险记 01 | 消失的scopeId

《骑鹅历险记》的主角是一个喜欢欺负小动物的小男孩,他因为戏弄小精灵而变成小人,然后意外的骑在一只志向远大的鹅身上,和路过的大雁们周游各地八个月,最后变成了一个讲礼貌的好孩子。小时候想变成小人跟着鹅到处旅行,长大以后成为了制造bug的程序员,随着它们去调用栈、去库和框架的源码里历险。另一种圆梦。

复现

观察下面这段vue代码:

<template>
  <div>
    <button @click="load">load</button>
    <list>
      <div class='item' v-for="item in list"></div>
    </list>
  </div>
</template>

<style lang="scss" scoped>
.item {
    //...
}
</style>
复制代码

这个组件实现的功能是:渲染一个列表,点击load按钮,list字段从[]切换为[{id: 1}]

代码中的List组件非常简单:

<div class="list">
  <slot></slot>
  <div class="placeholder">placeholder</div>
</div>
复制代码

基于以上的代码,出现的问题是:在点击load按钮、数据加载进来之后,.item的样式却并不会生效。

溯源

在了解这个问题出现的原因之前,需要了解一下scopeId的相关原理[1]:

<style> 标签有 scoped 属性时,它的 CSS 只作用于当前组件中的元素。


<template>
  <div class="example">hi</div>
</template>
<style scoped>
.example {
  color: red;
}
</style>
复制代码

转换结果:

<template>
  <div class="example" data-v-f3f3eg9>hi</div>
</template>
<style>
.example[data-v-f3f3eg9] {
  color: red;
}
</style>
复制代码

这里的data-v-f3f3eg9就是scopeId。其中,给样式增加scopeId的动作发生在vue-loader中,给html增加scopeId的动作在vue中执行:在解析模板、生成vnode的时候,会给当前组件里对应的元素添加对应的属性值:data-v-scopeId

对于给定的具体案例,在点击load按钮前,生成的实际html代码为:

<div>
    <button data-v-123>load</button>
    <div data-v-456 class="list">
      <div class="placeholder">placeholder</div>
    </div>
</div>
复制代码

在点击load按钮后,生成的真实html代码为:

<div>
    <button data-v-123>load</button>
    <div data-v-456 class="list">
      <div class="item">1</div>
      <div class="placeholder">placeholder</div>
    </div>
</div>
复制代码

可以看到,div.item这个元素的scopeId消失了,从而导致携带了scopeId的css代码无法对它生效。

前文中提到了,给html增加scopeId的动作在vue中执行:在解析模板、生成vnode的时候,会给当前组件里对应的元素添加对应的属性值。

vnode是真实DOM节点的代理,为了降低DOM操作带来的昂贵性能开销,会先用性能更好的JavaScript计算出真实DOM的最终改动,再将改动应用到真实的DOM上,减少修改更新真实DOM新的次数。这个计算的过程叫做dom diff。

当点击load按钮,vue的双向绑定特性触发dom diff算法,更新div.list节点进行相应的增删添加操作。

在这个具体的例子中:

旧的子节点列表只有一个vnode,也就是div.placeholder对应的vnode[vnode1]

新的子节点列表有两个vnode,也就是div.placeholderdiv.item[vnode1,vnode2]

[vnode1]变化为[vnode2,vnode1],在算法的执行流程中, 将会命中红线所示的流程,进入更新子节点的子流程:

IMG_08ED14A51F43-1.jpeg

在更新子节点的流程中,vue会判断vnode1和vnode2相同、可以复用,从而将vnode1中保存的真实dom给vnode2使用。即,把div.placeholder 更新为div.item,但是setScopeId只会在创建新的element(真实DOM)的时候执行。所以,div.itemscopeId在这里丢失了。

解决问题的方法就很简单了:只要不让两个vnode被判定相同从而进行element的复用,就可以生成新的element,从而创建出对应的scopeId。

判断vnode是否相同的代码sameVnode方法代码如下:

function sameVnode (a, b) {
  return (
    a.key === b.key && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}
复制代码

从这段代码来看,解决问题的方法似乎很简单,设置key值、让sameVnode的返回值为false即可。

在使用v-for的时候,不设置key值是一个写进了源码警告的操作,以上的代码似乎有生搬硬造之嫌。但是还有一种情况, 在设置了key值之后,scopeId也会消失:

观察以下代码:

<template>
  <div>
    <button @click="load">load</button>
    <list>
      <van-cell class='item' v-for="item in item.list" :key="item.id">
        {{ item.id }}
      </div>
    </list>
  </div>
</template>
复制代码

van-cell是有赞开源的组件库vant中的一个函数式组件。

如果创建的是类组件,vue会直接创建vnode,把data中的属性(包括key值)放到vnode中;如果创建的是函数式组件,vue会使用组件自己提供的渲染函数。

van-cell这个函数式组件提供的渲染函数并不接收key属性,也就是van-cell创建的vnode的key为undefined。这时候,sameVnode的返回值还是true,依然会进行element的复用。

发散

react也有自己的dom diff算法,在react中编写同样功能的代码,是否会出现同样的问题呢?观察以下代码:

const List = ({ list }: { list: number[] }) => {
  return (
    <div>
      {list.map((item) => {
        return <Cell item={item}></Cell>;
      })}
      <div>placeholder</div>
    </div>
  );
};

const Cell = ({ item }: { item: number }) => {
  return <div className={s.item}>{item}</div>;
};
复制代码

react判断是否可复用的标准是:key是否相同,type是否相同。在这个例子里,似乎也会因为div.itemdiv.placeholder没有key且type相同而错误将div.placeholer用于div.item的复用。

但是,react从jsx生成fiber node的时候,map操作会隐式生成一个fragment元素,因此,在点击load按钮之前和点击load按钮之后,div.list对应的fiber node的children均为[fragment, div.placeholder],区别在于点击load之后,fragment的children从[]变为了[div.item]

因此,即使没有设置key,react依然能准确的生成新的div.item对应的fiber node。

总结

vue把模版编译成vnode,然后作用于真实DOM。react把jsx代码编译成fiber node,然后作用于真实DOM。看上去似乎没多大区别,就像披萨不过是大饼上撒了肉和蔬菜。vnode和fiber在结构、设计上的不同,vue和react在整体流程上设计和架构的不同,在实际应用中的表现也会截然不同。

(注:只从实际开发中遇到的一个问题做一个发散式的探索,不对两个框架做具体评价,两者各有优势,应当根据具体情况具体选择)

参考文献

vue-loader文档

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