关于Vue/React中Diffing算法以及key的作用
目录
1. DIffing算法
1.1 React中的Diffing算法
1.2 Vue中的Diffing算法
1.3 两者对比
2. React/Vue中key的作用(key的内部原理)
3. 正确解决方案
最近在学习React和复习Vue相关知识的时候,又重新学习到了两者在循环遍历时key的相关知识内容,正好记录复习一下。
1. DIffing算法
首先React和Vue作为两个前端目前比较热门的框架,两个框架都实现的是将虚拟DOM转换为真实DOM并渲染到页面上,并且有着相当高的效率,一提到效率我们就得先讲解一下两者框架的虚拟DOM中的Diffing算法。
1.1 React中的Diffing算法
React 采用的是一种叫做 虚拟 DOM (Virtual DOM) 的技术,在进行 UI 更新时,React 会先创建一棵虚拟 DOM 树,然后在更新时通过 diffing 算法比较旧的虚拟 DOM 和新的虚拟 DOM。React 的 diffing 算法遵循一些优化原则来高效地进行比较。
核心原则
1. 单向数据流:
React 通过单向数据流来简化 diffing 操作,即所有组件的状态和属性变化都会导致重新渲染。这使得 React 可以将整个应用的变化分解成小的组件更新,方便进行优化。
2. 元素类型不同:
如果新旧两个虚拟 DOM 节点的类型不同,React 会直接销毁旧节点并创建一个新节点。例如,如果你从 <div> 改为 <span>,React 就会认为它们是两个完全不同的元素。
3. 相同类型的元素比较子节点:
如果新旧节点类型相同,React 会比较它们的子节点。React 通过 深度优先遍历 来更新树的不同部分,递归地比较子节点,进行最小更新。
4. 节点复用与排序优化:
React 假设相同类型的元素之间不会改变顺序。因此,对于相同类型的兄弟节点,React 会尽量复用它们。如果节点的顺序变化较大,React 会做出必要的调整来减少重渲染。
5. Keys:
React 使用 key 来标识节点在列表中的位置,key 主要用于优化同级元素的更新。当有 key 时,React 会利用它们来决定哪些节点可以复用,从而避免不必要的删除和插入操作。
Diffing 过程简述:
• React 首先通过 浅比较 组件的 props 和 state,决定是否需要更新。
• 如果更新必要,React 会根据节点类型判断是否是完全不同的节点(例如标签不同)还是只是部分变化(例如文本内容变化)。
• 对于相同类型的节点,React 会比较子节点,并递归处理每一层的变化,采用 O(n) 的时间复杂度进行节点更新。
1.2 Vue中的Diffing算法
Vue 的 diffing 算法也基于虚拟 DOM,但与 React 略有不同,Vue 的 diffing 更加注重性能优化,尤其是在节点更新的过程中。Vue 的实现依赖于其响应式系统和模板编译器,因此它的 diffing 算法可能会和 React 在一些细节上有所区别。
核心原则
1. 虚拟 DOM 与响应式系统:
Vue 通过数据驱动视图的更新,采用响应式系统,当数据变化时,Vue 会自动触发组件的更新。Vue 的 diffing 算法基于它的 虚拟 DOM 实现,Vue 会在每次更新时构建一个新的虚拟 DOM 树,并与旧的虚拟 DOM 树进行对比。
2. 浅比较:
Vue 采用的是 浅比较,与 React 类似。当虚拟 DOM 中的节点发生变化时,Vue 会只对节点的属性、事件等进行比较。如果节点类型不同,Vue 会重新创建节点。
3. “就地更新”与“节点复用”:
Vue 使用了一种 就地更新 的策略,尽量避免无意义的 DOM 操作。如果某些节点的变化不会影响 DOM,Vue 会选择不更新这些节点,只更新发生变化的部分。Vue 会尽量保留现有节点,避免不必要的重渲染。
4. 动态节点与静态节点分离:
Vue 会对节点进行分类,将 静态节点 和 动态节点 分离。静态节点不会被重新渲染,而动态节点会根据数据的变化进行更新。Vue 会通过 树的比较 来优化更新过程,从而减少不必要的操作。
5. Keys:
Vue 也支持使用 key 来优化 diffing 算法。当同一层级的节点发生变化时,Vue 会通过 key 来识别并更新节点的位置,以便提高渲染效率。
Diffing 过程简述:
• Vue 会通过构建新的虚拟 DOM 树并与旧的虚拟 DOM 树进行比较。
• 在比较过程中,Vue 会对比节点的 类型、属性、事件等,如果类型不同,Vue 会销毁旧节点并创建新节点。
• 对于相同类型的节点,Vue 会利用它的 优化策略(如静态节点的保留、动态节点的更新)来决定哪些节点需要更新。
• Vue 会依赖 key 来帮助优化同级节点的更新。
1.3 两者对比
特性 | React | Vue |
---|---|---|
虚拟 DOM | 是的,React 使用虚拟 DOM,进行最小化的 DOM 更新 | 是的,Vue 使用虚拟 DOM,并通过响应式系统触发更新 |
浅比较 | 是的,React 通过浅比较来决定是否更新节点 | 是的,Vue 也通过浅比较来判断节点的变化 |
节点类型变化 | 类型不同的节点会完全重新渲染 | 同 React,类型不同的节点会完全重新渲染 |
子节点比较 | 对于相同类型的节点,会比较子节点并递归更新 | 同样,Vue 会递归比较子节点,尤其注重静态与动态节点的区分 |
key 的使用 | 使用 key 来优化列表更新,帮助避免不必要的 DOM 操作 | 使用 key 来优化同层节点的更新 |
更新优化 | React 假设节点顺序不会频繁变化,利用双指针算法减少更新 | Vue 更加注重节点复用和静态节点的分离,优化树的 |
2. React/Vue中key的作用(key的内部原理)
当数据发生变化时,React会根据新的数据生成新的虚拟DOM,随后React进行diff算法比较,主要规则如下:
a. 旧虚拟DOM中找到了与新虚拟DOM相同的key:
(1) 若虚拟DOM中内容没变,直接使用之前的真实DOM
(2) 若虚拟DOM中内容变了,则生成新的真实DOM,随后替换掉旧的真实DOM
b. 旧虚拟DOM中未找到与新虚拟DOM相同的key:
根据数据创建新的真实DOM,随后渲染到页面
但是如果我们简单的使用index作为key的话,那么请看以下代码:
class Person extends React.Component {
state = {
persons: [
{id:1, name: 'stephen', age:18},
{id:2, name: 'Jack', age:21}
]
}
render() {
return (
<div>
<h2>Show Informations</h2>
<button onClick={this.add}>Add James</button>
<ul>
{
this.state.persons.map((person, index) => {
return <li key={index}>{person.name} -- {person.age}</li>
})
}
</ul>
</div>
)
}
add = () => {
const {persons} = this.state
const p = {id: persons.length+1, name: 'James', age: 20}
{/* 注意此时的添加顺序是逆序的 */}
this.setState({persons: [p, ...persons]})
}
}
ReactDOM.render(<Person/>, document.getElementById('test'))
并且请注意我此时添加的顺序是逆序的,那么React在生成虚拟DOM进行diffing算法对比时就会出现问题:
当进行Diffing对比时,按照规则对比key时,发现对应不上,也就是说React又重新帮我们重新创建出三个虚拟DOM,其实我们只添加了一条数据但是却要创建多个虚拟DOM,如果我们本身的数据量很大,那么带来了一定的资源浪费,这是我们需要避免的。
但是其实有人也发现了:那如果我不逆序添加不就没有问题了吗,对于上述例子来说这种说法是正确的。但是问题又来了,这仅仅是数据的展示,如果每一个<li>标签下又存在一个<input>标签,我们需要为每一个<li>标签添加对应的<input>值,如果此时我们使用index作为key,后续生成新的DOM时会发现原先对应的数据顺序全部混乱,这在我们开发时是十分重大错误。产生这个原因在于我上述所讲到的React 在diffing算法时的原理,按照每一个标签进行渲染。
3. 正确解决方案
我们在开发过程中使用key尽量避免用index以免产生错误或者效率问题。我们应该选用数据的唯一标识,例如后端服务器传回来的data中,可能会含有Primary Key的字段,那么我们可以使用该字段作为key来避免一些问题的出现,以下是合理代码的改造:
class Person extends React.Component {
state = {
persons: [
{id:1, name: 'stephen', age:18},
{id:2, name: 'jack', age:21}
]
}
render() {
return (
<div>
<h2>Show Informations</h2>
<button onClick={this.add}>Add James</button>
<ul>
{
this.state.persons.map((person) => {
return <li key={person.id}>{person.name} -- {person.age}</li>
})
}
</ul>
</div>
)
}
add = () => {
const {persons} = this.state
const p = {id: persons.length+1, name: 'James', age: 20}
this.setState({persons: [p, ...persons]})
}
}
ReactDOM.render(<Person/>, document.getElementById('test'))