资讯详情

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 v底层发动机18。

本文将使用约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);  

您还可以在编译的字符串中添加更多的元素,然后查看最终结果,我直接在这里给出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"));  

所以你可以看到它在工作,但现在它只渲染一次,不能与我们互动。 另外,请注意我们在这里使用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"));  

虽然添加了这么多代码,但我们只是重构了渲染逻辑。重构后的调用顺序是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 classNae="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"));

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

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

模拟requestIdleCallback

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

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

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

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

因为如果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"));

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

标签: 连接器主线

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台