likes
comments
collection
share

(13)React 进阶——③ 虚拟 DOM(上):React 中的“虚拟 DOM” | React 基础理论实操

作者站长头像
站长
· 阅读数 27
转载请注明出处,未经同意,不可修改文章内容。

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

涉及面试题:
1. 什么是 Virtual DOM
2. Virtual DOM 如何工作?
3. Shadow DOMVirtual DOM 之间有什么区别?
4. 什么是 React Fiber,它的主要目标是什么?

编号:[react_13]

1 没有“虚拟 DOM”时的痛点

上篇文章,我们清楚地理解了:当组件的 state 或者 props 发生改变的时候, render 函数就会重新执行,页面也随即被“重新渲染”。

在 React 中,实现这种“重新渲染”的操作时,它的“性能”是非常高的,因为它引入了“虚拟 DOM”的概念!

紧接着上一篇的代码,打开 TodoList.js 文件:

import React, { Component, Fragment } from "react"; 

import TodoItem from "./TodoItem"; 

import "./style.css"; 

class TodoList extends Component {
  constructor(props) {
    super(props);
    this.state = { // 1️⃣当 state 发生改变时,
      inputValue: "",  
      list: []
    };
    
    this.handleInputChange = this.handleInputChange.bind(this);
    this.handleBtnClick = this.handleBtnClick.bind(this);
    this.handleItemDelete = this.handleItemDelete.bind(this);
    
  }

  render() { // 2️⃣render 函数下边的函数体会重新执行;
             /*
             ❓❓❓3️⃣但假设没有 React,我们要自己去实现这个功能,应该怎样思考呢?
             请认真阅读下边的各个方法! 
              */
    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( 
        <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;

假设没有 React,我想实现“数据”变化,页面跟着变化的功能,我们很惯性的会想到以下两种方法。

❌方法一:

  1. 定义一个 state 数据;

  2. 定义一个“JSX 模板”;

  3. 将“数据 + JSX 模板”结合,生成“真实的 DOM”来显示;

  4. 当数据 state 发生改变;

  5. “数据 + JSX 模板”相结合,生成“真实的 DOM”,并替换“原始的 DOM”

❗️缺陷——非常耗“性能”: 第一次生成了一个完整的 DOM 片段 --> 数据变化后,又生成了一个完整的 DOM 片段 --> 然后用第二次的 DOM 替换第一次的 DOM

❎方法二(对“方法一”改良一下):

  1. 定义一个 state 数据;

  2. 定义一个“JSX 模板”;

  3. 将“数据 + JSX 模板”结合,生成“真实的 DOM”来显示;

  4. 当数据 state 发生改变;

  5. 🔨“数据 + JSX 模板”相结合,生成“真实的 DOM”,但并不直接替换“原始的 DOM”;

  6. 将“新的 DOM(DocumentFragment)”和“原始的 DOM”作对比,找差异

  7. 找到某个“节点”(如 <input> 元素)发生了变化;

  8. 只用“新的 DOM”中的 <input> 元素,替换掉“原始的 DOM”中的 <input> 元素

❗️优势和缺陷: “方法二”相对于“方法一”,虽然步骤增加了,但“性能”上的确有一些提升:“局部替换”比“整体替换”消耗的性能更少。

但同时,其性能又损失了不少:新、旧 DOM 在“作对比,找差异”的过程中,性能又被“消耗”了。

这样“一增一减”,性能其实也没多大提升!

2 “虚拟 DOM”带来的新篇章

基于此痛点,React 提出了“虚拟 DOM”方案。

✔️方法三(虚拟 DOM):

  1. 定义一个 state 数据:
this.state = { 
  content: "hello, Oli."
};
  1. 定义一个“JSX 模板”:
render() { 
  return(
    <div id="abc">
      <span>{content}</span>
    </div>
  )
}
  1. “数据 + JSX 模板”结合,React 底层会调用 React.createElement() 接口,将 JSX 模板变成一个原始“虚拟 DOM”(定义:“虚拟 DOM”就是一个数组结构的 JS 对象,用它来描述“真实 DOM”)。

“虚拟 DOM”的格式为: (13)React 进阶——③ 虚拟 DOM(上):React 中的“虚拟 DOM” | React 基础理论实操

1️⃣——第一项表示“最外层元素的标签名”; 2️⃣——第二项表示“最外层标签的属性”; 3️⃣——第三项表示“children 子节点”,若“子节点”又是一个完整的 DOM,那么又需要以 [1️⃣, 2️⃣, 3️⃣] 的形式来写。

※本例中,“JSX 模板”转变为“JS 对象”的底层实现为(注意看 createElement 的用法)

render() { 
  /*
  🚀-①:我们将“JSX 模板”用 craeteElement 方法来改写,其接收 3 个参数:
  第一个参数为“最外层元素的标签名”;
  第二个参数为“最外层标签的属性”;
  第三个参数为“children 子节点”。
   */
  return React.createElement("div", {id: "abc"}, React.createElement("span", {}, "hello, Oli."))
  
  
  /*
  🚀-②:我们发现,即使没有“JSX 模板”,
  我们依然能通过 React.createElement() 来实现 JSX 的功能。
  但,会很繁杂!
  这也正是 JSX 存在的原因,因为它看着真的很简单!
   */
  
  /*
  return(
  	<div id="abc">
    	<span>{content}</span>
    </div>
  )
   */
}

本例生成的原始“虚拟 DOM” 为: ["div", {id: "abc"}, ["span", {}, "hello, Oli."]]

  1. 根据上边的“虚拟 DOM”生成“真实 DOM”:
<div id="abc">
  <span>hello, Oli.</span>
</div>
  1. 当数据 state 发生变化:
this.state = { 
  content: "hello, qdywxs."
};
  1. 随即“数据 + JSX 模板”结合,生成新的“虚拟 DOM”

["div", {id: "abc"}, ["span", {}, "hello, qdywxs."]]

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

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

🏆优势(性能大幅提升):

  • 用 JS 生成一个“JS 对象”,其性能消耗是非常小的。而“方法二”中,用 JS 去生成一个 DOM 元素,它的代价极高,其底层会调用 Web Application 这个级别的 API,性能损耗很大!

  • 第 7 步中,两个“虚拟 DOM”之间作对比(即两个“JS 对象”作对比),“Diff 算法”是不怎么消耗性能的。而“方法二”中,把两个“真实 DOM”作对比,其性能消耗极大。

3 “虚拟 DOM”之于“跨端应用”

有了“虚拟 DOM”,我们可以用 React(React Native)去写“原生的应用”了。

其原理为: 在上边的第 3 步中,“数据 + JSX 模板”生成了一个“原始虚拟 DOM”,这个“虚拟 DOM”是一个“JS 对象”。

JS 对象”不仅在“浏览器”里可以被识别,它在“原生应用——Android、iOS 机器上的代码”里也可以被很好地识别“原生应用”里是不存在 DOM 这个概念的,故“真实 DOM”在“原生应用”里不被识别,无法使用!)。

在“原生应用”里,有了生成的“虚拟 DOM”后,我们可以让它生成一些原生的组件。

这样的话,在复用“state”、“JSX 模板”等代码的优势下,让“虚拟 DOM”在不同“端”进行转化:

  • 网页端:转化为“真实 DOM”;
  • 原生应用:转化为原生的“组件”。

这样的话,在“原生应用”里,也可以把相应页面展示出来了。

祝好,qdywxs ♥ you!