骑虫历险记 01 | 消失的scopeId

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

复现

观察下面这段vue代码:css

<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}]html

代码中的List组件很是简单:vue

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

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

溯源

在了解这个问题出现的缘由以前,须要了解一下scopeId的相关原理[1]:node

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

<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代码为:markdown

<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文档

相关文章
相关标签/搜索