• React 18发布,仅用400行代码就能实现一个Mini-React


    导语:400行代码在React 18中实现可中断的异步更新的最小模型!

    原文链接:https://betterprogramming.pub/react-18-has-been-released-implement-mini-react-in-400-lines-of-code-837559761758
    声明:本文为 CSDN 翻译,转载请注明来源。

    以下为译文:

    React v18已经发布,它给我们带来了许多特性,但最重要的特性是可中断的异步更新,许多新的上层API都是通过它创建的。可以说,它是React v18的底层引擎。

    本文将使用大约400行代码带你实现一个可以异步更新和可中断更新的Mini-React,如下图所示:
    在这里插入图片描述
    我使用了React官方网站提供的tic-tac-toe教程示例(https://reactjs.org/tutorial/tutorial.html#what-are-we-building),可以看到它非常有效。此外,它目前支持函数组件和类组件,可以满足开发者80%的需求!我也把它放在GitHub上(https://github.com/islizeqiang/mini-react),你也可以在本地复制它,并按照我的文章一步一步地调试。

    这是我在阅读了大量React的源代码后创建的,在整体逻辑和函数命名上基本上和React一样,如果你对React的内部原理感兴趣,这篇文章就是为你准备的!

    JSX和createEelement

    我相信你对 React 中的 JSX 并不陌生。 我们使用 JSX 来描述 DOM,它们最终会被 babel 转换成 React 提供的 API。 例如下面的代码:
    在这里插入图片描述
    你也可以自己在StackBlitz上试试(在终端输入node transform-JSX.js):

    // run `node transform-JSX.js` in the terminal
    
    const babel = require('@babel/core');
    const optionsObject = {
      presets: ['@babel/preset-env'],
      plugins: [['@babel/plugin-transform-react-jsx']],
    };
    
    const { code } = babel.transformSync(
      'const element = <div id="test"><h1>Hello</h1></div>',
      optionsObject
    );
    
    console.log(code);
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    你还可以在编译好的字符串中加入更多的元素,再看看最终的结果,我在这里直接给出React.createElement提供的选项。
    1.type:表示当前节点的类型,如上图中的div。
    2.config:表示当前元素节点上的属性,如上图中的{id: “test”}。
    3.children:子元素,可以是多个、简单的文本,也可以由React.createElement创建的子节点。
    然后根据这个要求实现你自己的React.createElement,就像下面的代码一样,我们定义一个自定义的数据结构。

    渲染

    然后我们可以根据上面创建的数据结构实现一个简化版的渲染函数,将JSX渲染到真实的DOM上。

    下面的代码演示将使用CodeSandbox,拖动左栏查看代码,点击上方的菜单按钮查看目录结构。你也可以直接编辑,查看显示的结果。

    import React from "./mini-react";
    
    const App = (
      <div id="test">
        <h1>Hello</h1>
      </div>
    );
    
    // eslint-disable-next-line react/no-deprecated
    React.render(App, document.getElementById("root"));
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    所以你可以看到它工作,但现在它只渲染一次,不能与我们互动。
    另外,请注意我们在这里使用react-scripts@3.4.4来帮助编译JSX,API在以后的版本中已经发生了变化,但是React.createElement在结束时仍然被调用。我提供的GitHub库使用了Vite而不是react-scripts。

    接下来,是React的核心纤程架构和并发模式,这是从React 17开始提出的,主要是为了解决一旦完整的元素树被递归,就无法终止的问题,这可能导致主线程长时间被阻塞,那些高优先级的任务(比如那些用户输入或动画等)无法及时处理。

    所以在React源代码中,工作被分解成小单元。一旦浏览器处于空闲状态,它将处理这些小的工作单元,然后将结果映射到实际的DOM,直到所有结果都被处理完。

    requestIdleCallback是一个实验性API,它在浏览器空闲时执行回调。接下来,我们将使用这个API来简单地实现这个功能。我将在最后给出React目前使用的调度程序包的模拟实现。

    在开始编写下一个代码之前,我想再次介绍一下工作单元之间的连接。
    在这里插入图片描述
    就像上面的图片一样,我们将像链表一样创建每个纤程节点之间的连接,它们是:

    1.child:父节点指向第一个子元素的指针。

    2.return/parent:所有子元素都有一个指向父元素的指针。

    3.sibling:从第一个子元素指向下一个同级元素。

    所以现在你可以愉快地编写代码:

    import React from "./mini-react";
    
    const App = (
      <div id="test">
        <h1>Hello</h1>
      </div>
    );
    
    // eslint-disable-next-line react/no-deprecated
    React.render(App, document.getElementById("root"));
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    尽管添加了这么多代码,我们只是重构了渲染逻辑。重构后的调用顺序为workLoop →performUnitOfWork→reconcileChildren。下面让我来总结一下各个功能的作用:

    1.workLoop:通过连续调用requestIdleCallback来获得空闲时间。如果当前空闲且有单元任务要执行,则执行每个单元任务。

    2.performUnitOfWork:执行的特定单元任务。这是链表思想的体现。即一次只处理一个纤程节点,并返回下一个要处理的节点。

    3.reconcileChildren:协调当前纤程节点,它实际上是虚拟DOM的比较,并记录要进行的更改。你可以看到,我们直接在每个纤程节点上修改和保存,因为现在它只是对JavaScript对象的修改,而不涉及真正的DOM。

    4.最后一步是commitRoot。如果当前需要更新(根据wipRoot),并且没有下一个单元任务要处理(根据!nextUnitOfWork),这意味着需要将虚拟更改映射到实际的DOM。commitRoot负责根据纤程节点的变化修改真实的DOM。

    到目前为止,我们已经实现了纤程架构,是时候见证它的威力了。
    我们想给组件添加状态,让我们实现一个useState。

    import React from "./mini-react";
    import "./styles.css";
    
    function calculateWinner(squares) {
      const lines = [
        [0, 1, 2],
        [3, 4, 5],
        [6, 7, 8],
        [0, 3, 6],
        [1, 4, 7],
        [2, 5, 8],
        [0, 4, 8],
        [2, 4, 6]
      ];
      for (let i = 0; i < lines.length; i += 1) {
        const [a, b, c] = lines[i];
        if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
          return squares[a];
        }
      }
      return null;
    }
    
    class Square extends React.Component {
      render() {
        return (
          <button onClick={this.props.onClick} className="square">
            {this.props.value}
          </button>
        );
      }
    }
    
    class Board extends React.Component {
      renderSquare(i) {
        return (
          <Square
            value={this.props.squares[i]}
            onClick={() => {
              this.props.onClick(i);
            }}
          />
        );
      }
    
      render() {
        return (
          <div>
            <div className="board-row">
              {this.renderSquare(0)}
              {this.renderSquare(1)}
              {this.renderSquare(2)}
            </div>
            <div className="board-row">
              {this.renderSquare(3)}
              {this.renderSquare(4)}
              {this.renderSquare(5)}
            </div>
            <div className="board-row">
              {this.renderSquare(6)}
              {this.renderSquare(7)}
              {this.renderSquare(8)}
            </div>
          </div>
        );
      }
    }
    
    class App extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          history: [
            {
              squares: Array(9).fill(null)
            }
          ],
          stepNumber: 0,
          xIsNext: true
        };
      }
    
      handleClick(i) {
        const history = this.state.history.slice(0, this.state.stepNumber + 1);
        const current = history[history.length - 1];
        const squares = current.squares.slice();
        if (calculateWinner(squares) || squares[i]) {
          return;
        }
    
        squares[i] = this.state.xIsNext ? "X" : "O";
        this.setState({
          history: history.concat([
            {
              squares
            }
          ]),
          stepNumber: history.length,
          xIsNext: !this.state.xIsNext
        });
      }
    
      jumpTo(step) {
        this.setState({
          stepNumber: step,
          xIsNext: step % 2 === 0
        });
      }
    
      render() {
        const { history } = this.state;
        const current = history[this.state.stepNumber];
        const winner = calculateWinner(current.squares);
    
        const moves = history.map((step, move) => {
          const desc = move ? `Go to move #${move}` : "Go to game start";
          return (
            <li key={move}>
              <button onClick={() => this.jumpTo(move)}>{desc}</button>
            </li>
          );
        });
    
        let status;
        if (winner) {
          status = `Winner: ${winner}`;
        } else {
          status = `Next player: ${this.state.xIsNext ? "X" : "O"}`;
        }
    
        return (
          <div className="game">
            <div className="game-board">
              <Board
                squares={current.squares}
                onClick={(i) => {
                  this.handleClick(i);
                }}
              />
            </div>
            <div className="game-info">
              <div>{status}</div>
              <ol>{moves}</ol>
            </div>
          </div>
        );
      }
    }
    
    // eslint-disable-next-line react/no-deprecated
    React.render(<App />, document.getElementById("root"));
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152

    useState巧妙地将hook的状态保留在纤程节点上,并通过队列修改状态。从这里,我们也可以知道为什么React-hooks要求每次调用的顺序不能改变。

    除此以外,我们还实现了一个Component ,这里只是简单地转换为一个渲染的方法,并添加了一点它的独特身份。

    模拟requestIdleCallback

    现在我们几乎实现了所有的功能,让我解释一下React目前采用的调度器包,它实际上是一个比requestIdleCallback更复杂的调度逻辑,包括更新任务的优先级等等。

    上面是我实现模拟requestIdleCallback的参考调度程序,它结合了requestAnimationFrame和MessageChannel。这里使用MessageChannel的目的是使用宏任务来处理每一轮的单元任务。

    那么为什么要使用宏任务呢?
    为了放弃主线程,浏览器可以在空闲期间更新DOM或接收事件。因为浏览器更新DOM是一个独立的任务,而JavaScript在这个时候不会被执行,因为主线程一次只能运行一个功能,要么执行JS,要么处理DOM计算样式,要么接收输入事件,等等。

    为什么不使用微任务呢?
    因为微任务包含在每一轮宏任务中,所以在所有微任务执行完毕之前,也就是当前宏任务未完成时,主线程不能放弃。

    为什么不使用setTimeout呢?
    因为如果setTimeout被嵌套调用超过5次,该函数将被视为阻塞,浏览器将把最小时间设置为4ms,所以它不够精确。

    最终版本
    下面是最终的版本,你可以看到,在去掉注释后,不到400行代码就实现了React的核心思想。

    import React from "./mini-react";
    import "./styles.css";
    
    function calculateWinner(squares) {
      const lines = [
        [0, 1, 2],
        [3, 4, 5],
        [6, 7, 8],
        [0, 3, 6],
        [1, 4, 7],
        [2, 5, 8],
        [0, 4, 8],
        [2, 4, 6]
      ];
      for (let i = 0; i < lines.length; i += 1) {
        const [a, b, c] = lines[i];
        if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
          return squares[a];
        }
      }
      return null;
    }
    
    class Square extends React.Component {
      render() {
        return (
          <button onClick={this.props.onClick} className="square">
            {this.props.value}
          </button>
        );
      }
    }
    
    class Board extends React.Component {
      renderSquare(i) {
        return (
          <Square
            value={this.props.squares[i]}
            onClick={() => {
              this.props.onClick(i);
            }}
          />
        );
      }
    
      render() {
        return (
          <div>
            <div className="board-row">
              {this.renderSquare(0)}
              {this.renderSquare(1)}
              {this.renderSquare(2)}
            </div>
            <div className="board-row">
              {this.renderSquare(3)}
              {this.renderSquare(4)}
              {this.renderSquare(5)}
            </div>
            <div className="board-row">
              {this.renderSquare(6)}
              {this.renderSquare(7)}
              {this.renderSquare(8)}
            </div>
          </div>
        );
      }
    }
    
    class App extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          history: [
            {
              squares: Array(9).fill(null)
            }
          ],
          stepNumber: 0,
          xIsNext: true
        };
      }
    
      handleClick(i) {
        const history = this.state.history.slice(0, this.state.stepNumber + 1);
        const current = history[history.length - 1];
        const squares = current.squares.slice();
        if (calculateWinner(squares) || squares[i]) {
          return;
        }
    
        squares[i] = this.state.xIsNext ? "X" : "O";
        this.setState({
          history: history.concat([
            {
              squares
            }
          ]),
          stepNumber: history.length,
          xIsNext: !this.state.xIsNext
        });
      }
    
      jumpTo(step) {
        this.setState({
          stepNumber: step,
          xIsNext: step % 2 === 0
        });
      }
    
      render() {
        const { history } = this.state;
        const current = history[this.state.stepNumber];
        const winner = calculateWinner(current.squares);
    
        const moves = history.map((step, move) => {
          const desc = move ? `Go to move #${move}` : "Go to game start";
          return (
            <li key={move}>
              <button onClick={() => this.jumpTo(move)}>{desc}</button>
            </li>
          );
        });
    
        let status;
        if (winner) {
          status = `Winner: ${winner}`;
        } else {
          status = `Next player: ${this.state.xIsNext ? "X" : "O"}`;
        }
    
        return (
          <div className="game">
            <div className="game-board">
              <Board
                squares={current.squares}
                onClick={(i) => {
                  this.handleClick(i);
                }}
              />
            </div>
            <div className="game-info">
              <div>{status}</div>
              <ol>{moves}</ol>
            </div>
          </div>
        );
      }
    }
    
    // eslint-disable-next-line react/no-deprecated
    React.render(<App />, document.getElementById("root"));
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152

    我还在GitHub中添加了一个TypeScript版本的Mini-React(https://github.com/islizeqiang/mini-react/blob/master/src/mini-react.ts),如果你有兴趣,可以去看看。

  • 相关阅读:
    uni-app详解
    Java面试八股之myBatis与myBatis plus的对比
    Promise.all API 的出错处理
    Qt自己画个Y轴,如何和qcustomplot建立的X轴原点对齐?
    未来社区的人车房隐私数据权属确认方法
    APP商品详情源数据接口(淘宝/京东/拼多多/苏宁/抖音等平台详情数据分析接口)代码对接教程
    开源的滚滚浪潮:近年来开源多样化的几大亮点
    无源无线测温系统设计及在高压开关柜中的应用
    基础架构之Redis
    Pyside6 Qlabel显示图片、文本
  • 原文地址:https://blog.csdn.net/csdnnews/article/details/124446471