(14)React 进阶——④ 虚拟 DOM(下):“虚拟 DOM”中的 Diff 算法 | React 基础理论实操

转载请注明出处,未经同意,不可修改文章内容。

🔥🔥🔥"前端一万小时"两大明星专栏------"从零基础到轻松就业"、"前端面试刷题",已于本月大改版,合二为一,干货满满,欢迎点击公众号菜单栏各模块了解。

javascript 复制代码
涉及面试题:
1. 什么是"key"属性,在元素数组中使用它们有什么好处?
2. 索引作为键的影响是什么?
3. 安全地使用索引作为键的条件是什么?
4. 如何更新状态以及不更新状态?

编号:[react_14]

1 用 5 个问题了解"Diff 算法"

紧接上一篇文章:

  1. 当数据 state 发生变化:
javascript 复制代码
this.state = { 
  content: "hello, qdywxs."
};
  1. 随即"数据 + JSX 模板"结合,生成新的"虚拟 DOM"
    ["div", {id: "abc"}, ["span", {}, "hello, qdywxs."]]

  2. 比较 "原始虚拟 DOM"和"新的虚拟 DOM"的区别,找到区别是 <span> 中的内容;

  3. 直接操作 DOM,改变 <span> 中的内容。

❓1️⃣在第 7 步中,我们需要比较"原始虚拟 DOM"和"新的虚拟 DOM"的区别,怎么比较呢? 答:Diff 算法。

我们把这两个"JS 对象"找区别(比对)的方式,称作"Diff 算法"。

Diff 是 difference 的简写,其含义很直观------差异。

❓2️⃣"虚拟 DOM"什么时候会被"比较"?

答:当"数据"发生改变的时候,"虚拟 DOM"才会去做新的"比较"!

❓3️⃣什么时候算"数据"发生改变呢?

答:要么改变了 state,要么改变了 props(🔗详见《React 进阶------② state、props 与 render 函数的关系》)。就代码而言,即调用了 setState 时,数据就发生了变化

❓4️⃣我们之前的文章就说过, setState 是设计成"异步的",可为什么要这样设计呢?

答:之所以设计成"异步",是为了提高 React 底层的性能。

假设我们连续调用 3 次 setState ,且时间间隔非常小 。如果是"同步"的话,3 次数据变化,就需要做 3 次"虚拟 DOM"的比对,并更新 3 次页面。这样就比较浪费性能!

React 的做法是,把这连续 3 次的 setState 调用,合并成 1 次 。即,只做 1 次"虚拟 DOM"的比对,然后只去更新 1 次 DOM。这样就能省去了额外的 2 次"虚拟 DOM"比对所带来的性能耗费。

❓5️⃣"Diff 算法"是怎样对两个"虚拟 DOM"进行比较的呢?

答:采用"同层比较 "的方式------ 从最顶层开始,如果它们一致,React 就去比较第二层(以此类推 )。

  • 假如,顶层"比较"时,它们两者不一致,React 就不会再往下"比较"了,它会将"原始 DOM"进行全部替换

  • 假如,顶层是一致的,就比较第二层,若第二层不一致,原理同上。

这样的"比较"和"替换"方式,乍一看好像挺浪费性能的(也确实可能消耗了些性能)------一层不一样就将下边全部替换,很多 DOM 都没被复用。

但由于其"同层比较"的算法很简单,直接带来的好处就是:比较的速度很快。比较的速度快了,在"比较"这件事情上,性能一下就被提升了!

2 key 值的重要性

在 React 中,当我们做循环渲染的时候,我们需要给渲染出的每一项增加一个 key 值!这个 key 值是每一项唯一的"标识符"。

❓为什么一定需要一个 key 值呢,它与"虚拟 DOM"和"Diff 算法"又有什么关系呢?

答:"key 值"可以有效地提高 "虚拟 DOM"比较 的性能。

1️⃣假设我有一个数组,数组里边包含 5 个数据。当页面第一次循环渲染的时候,这 5 个数据会被映射成 5 个"虚拟 DOM"节点,并生成一个小的"原始虚拟 DOM"树:

2️⃣当数据发生变化时,会生成一个"新的虚拟 DOM 树":

3️⃣然后我们会作两个"虚拟 DOM"的比较(理想的是如下图一样作比较):

4️⃣❌但如果每个"虚拟 DOM"都没有彼此的 key 值,即没有自己的"名字"。那此刻"比较"时,节点与节点间的关系就很难被确立!

此时,就需要做两层循环的比较(用下边的每一个分别去和上边的所有节点作比较),这种"比较"会极其消耗性能!

5️⃣🏆相反,如果当我们做循环渲染的时候,我们给渲染出的每一项增加一个 key 值!那此时的"比较"就很轻松了:

如,第一次循环的时候,我给 5 个节点分别取了 a b c d e 5 个名字;第二次循环时,同样的节点我取了同样的名字(这点是提升"性能"的关键)

在作比较时,按节点的 key 值迅速做比较,最后发现只多出了一个 z ,然后只需将 z 增加到 DOM 树上就可以了,性能上是很优秀的。

3 为什么不建议用 index 做 key 值

紧接上文:

如,第一次循环的时候,我给 5 个节点分别取了 a b c d e 5 个名字;第二次循环时,同样的节点我取了同样的名字(这点是提升"性能"的关键) ,多出的节点,我取名为 z

由于提升"性能"的关键点是"同样的节点我取了同样的名字 "。如果用 index 作为 key 值,我们不能保证同样的节点拥有同样的"名字"。

打开 TodoList.js 文件:

jsx 复制代码
import React, { Component, Fragment } from "react"; 

import TodoItem from "./TodoItem"; 

import "./style.css"; 

class TodoList extends Component {
  constructor(props) {
    super(props);
    this.state = { 
      inputValue: "", 
      list: []
    };
    
    this.handleInputChange = this.handleInputChange.bind(this);
    this.handleBtnClick = this.handleBtnClick.bind(this);
    this.handleItemDelete = this.handleItemDelete.bind(this);
    
  }

  render() { 
    console.log("render") 
    
    return(
      <Fragment>
        <div>
          <label htmlFor="insertArea">请输入要进行的事项:</label>    
        
          <input 
            id="insertArea"
      
            className="input"
            value={this.state.inputValue}
            onChange={this.handleInputChange}
          />

          <button onClick={this.handleBtnClick}>
            提交
          </button>
        </div>

        <ul>
          {this.getTodoItem()}
        </ul>

      </Fragment>
    )
  }

  getTodoItem() {
    return this.state.list.map((item, index) => { 
      return( 
        
        // ❌1️⃣我们在这里用了 index 作为了 key 值;
        <TodoItem 
        	key={index}
        
        	content={item}
        	index={index} 
        	itemDelete={this.handleItemDelete}
        />  
      )  
    })
  }
  
  handleInputChange(e) {
    const value = e.target.value
    
    this.setState(() => ({
      inputValue: value
    }))
  }

  handleBtnClick() {
    this.setState((prevState) => ({
      list: [...prevState.list, prevState.inputValue],
      inputValue: ""        
    }))
  }

  handleItemDelete(index) { 
    this.setState((prevState) => {
      const list = [...prevState.list]
      list.splice(index, 1)
        
      return {list}
 
    })
  }
}

export default TodoList;

2️⃣打开页面,在 input 框输入一些内容:

此时:

lua 复制代码
Oli --> key: 0
qdywxs --> key: 1
Oli and qdywxs --> key: 2

3️⃣点击列表项内容 Oli

此时:

lua 复制代码
qdywxs --> key: 0
Oli and qdywxs --> key: 1

❗️这就直接造成一个问题:同一节点,前后的 key 值不相等,两个节点就无法建立起关系,此时 key 值就失去了它存在的意义!

❓应该用什么作为 key 值呢?

答:使用一个稳定的内容作为 key 值,才是最佳实践!

本例我们用每一项的 item 作为 key 值:

jsx 复制代码
  getTodoItem() {
    return this.state.list.map((item, index) => { 
      return( 
        
        // ✅4️⃣用 item 作为 key 值;
        <TodoItem 
        	key={item}
        
        	content={item}
        	index={index} 
        	itemDelete={this.handleItemDelete}
        />  
      )  
    })
  }

打开页面查看效果(不管怎么操作,key 值都是稳定 的):

当然,"同层比较"、"key 值比较"都是"Diff 算法"中的一部分,"Diff 算法"还有很多其他的知识点,但当下,我们可以先掌握好这两个,以后有精力可以去自行拓展。

祝好,qdywxs ♥ you!

相关推荐
也无晴也无风雨1 小时前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
Martin -Tang2 小时前
Vue 3 中,ref 和 reactive的区别
前端·javascript·vue.js
FakeOccupational3 小时前
nodejs 020: React语法规则 props和state
前端·javascript·react.js
小牛itbull3 小时前
ReactPress:构建高效、灵活、可扩展的开源发布平台
react.js·开源·reactpress
放逐者-保持本心,方可放逐3 小时前
react 组件应用
开发语言·前端·javascript·react.js·前端框架
曹天骄4 小时前
next中服务端组件共享接口数据
前端·javascript·react.js
阮少年、5 小时前
java后台生成模拟聊天截图并返回给前端
java·开发语言·前端
郝晨妤6 小时前
鸿蒙ArkTS和TS有什么区别?
前端·javascript·typescript·鸿蒙
AvatarGiser6 小时前
《ElementPlus 与 ElementUI 差异集合》Icon 图标 More 差异说明
前端·vue.js·elementui
喝旺仔la6 小时前
vue的样式知识点
前端·javascript·vue.js