《骑鹅历险记》的主角是一个喜欢欺负小动物的小男孩,他由于戏弄小精灵而变成小人,而后意外的骑在一只志向远大的鹅身上,和路过的大雁们周游各地八个月,最后变成了一个讲礼貌的好孩子。小时候想变成小人跟着鹅处处旅行,长大之后成为了制造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.placeholder
和div.item
:[vnode1,vnode2]
从[vnode1]
变化为[vnode2,vnode1]
,在算法的执行流程中, 将会命中红线所示的流程,进入更新子节点的子流程:
在更新子节点的流程中,vue会判断vnode1和vnode2相同、能够复用,从而将vnode1中保存的真实dom给vnode2使用。即,把div.placeholder
更新为div.item
,可是setScopeId
只会在建立新的element(真实DOM)的时候执行。因此,div.item
的scopeId
在这里丢失了。
解决问题的方法就很简单了:只要不让两个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.item
和div.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在总体流程上设计和架构的不一样,在实际应用中的表现也会大相径庭。
(注:只从实际开发中遇到的一个问题作一个发散式的探索,不对两个框架作具体评价,二者各有优点,应当根据具体状况具体选择)
参考文献