在破坏React元素创建过程的同时,从内部来看


这篇文章是Bitkey公司2023年圣诞节日历的第13篇文章。

 

首先

你好!我是川又,在Bitkey株式会社担任SET。这是我第一次在Qiita上写作!在Bitkey,我们在服务开发中使用React。

我也是,目前在使用React,但是如果有没有碰过或者没有看过React代码库的人,请务必借此机会一起进行实际操作来了解其内部实现。


首先,将React的代码库克隆到本地。

git clone https://github.com/facebook/react.git
yarn install
git checkout main  #最新バージョンのReactに向けます

为了确认内部处理,请编写测试。
我打算在react-dom下进行编写。

# root配下にて
touch packages/react-dom/src/__tests__/ReactHoge-test.js
我正在参考现有的测试进行实施!

样本测试实施

'use strict';

let React;
let ReactDOM;

describe('reactdom', () => {
  beforeEach(() => {
    React = require('react');
    ReactDOM = require('react-dom');
  });
  it('JSXをHTMLに変換する', () => {
    // Arrange
    function App() {
      return (
        <div>
          <div>
            <div>
              <span>hello</span>
            </div>
          </div>
        </div>
      );
    }

    // Act
    const container = document.createElement('div');
    console.log(container.innerHTML)
    ReactDOM.render(<App />, container);

    // Assert
     expect(container.innerHTML).toBe(
      '<div><div><div><span>hello</span></div></div></div>',
    );
  });
});

进行测试

# root配下にて
yarn test packages/react-dom/src/__tests__/ReactHoge-test.js

结果

  console.log
    <div><div><div><span>hello</span></div></div></div>

      at Object.<anonymous> (packages/react-dom/src/__tests__/ReactHoge-test.js:26:13)

 PASS  packages/react-dom/src/__tests__/ReactHoge-test.js
  reactdom
    ✓ JSXがHTMLに代わる (394 ms)

没有问题地将定义好的组件添加到DOM中。
显然,DOM的div嵌套很多,HTML不易于阅读。为了改善外观,让我们改变React的行为,将

去掉。

破坏React元素创建处理

在实现React时,使用JSX是基本的,但是需要注意的是,JSX只是JavaScript语法的扩展,会被转换为其他形式来进行处理。

为了知道在代码库中元素被创建的位置,需要深入理解JSX。

查看公式文件后发现有相关文章。

 

image.png

在React中,似乎是通过createElement创建React元素的。

在代码库中进行调查后,我们发现以下内容在packages/react/src/React.js中定义。

const createElement: any = __DEV__
  ? createElementWithValidation
  : createElementProd;

在实施过程中,我发现有两种情况,一种是运行验证,另一种是不运行验证。从性能方面考虑,在生产环境中不需要运行各种验证。因此,让我们确认一下不带验证的createElement。

在React的代码库中,可以经常看到__DEV__标志。当我进行grep操作时,发现了整整2254条匹配项? 。控制台日志的有无以及行为变化都由此标志决定。

让我们来确认一下定义的先验条件。

// packages/react/src/ReactElement.js
export function createElement(type, config, children) {
  let propName;

  // Reserved names are extracted
  const props = {};
    ...

createElement函数可以接受三个参数,分别是type、config和children。

在测试中定义的组件会被转译成以下的Javascript。

React.createElement('div', null,
  React.createElement('div', null,
    React.createElement('div', null,
      React.createElement('span', null, 'hello')
    )
  )
);

在Chinese,我们甚至可以传递React组件并不限于HTML标签的类型。

config是一个对象(props的信息)或者为null。由于此次没有传递任何内容,所以会变成null。

children可以传递给React的节点的全部内容。本例中是由createElement输出的React元素。

可以将createElement作为编写JSX的替代方法导入并在您的工作项目中使用!请参考以下详细信息。

和DOM一样,调用也发生了嵌套?
我将更改createElement的行为,不再渲染div。
根据type的定义,似乎确定了要插入到DOM中的元素。
如果传递了type=’div’,我们可以返回另一个元素。

// packages/react/src/ReactElement.js
export function createElement(type, config, children) {
  if (type === 'div') {
    type = 'hoge';
  } // if文を追加
  
  let propName;
  ...

我将再次进行测试。

我现在开始发出以下警告

    Warning: The tag <hoge> is unrecognized in this browser. If you meant to render a React component, start its name with an uppercase letter.
        at hoge
        at hoge
        at hoge
        at App

在HTML中并不存在这个标签,所以产生警告是理所当然的。
不过有趣的是,

  ● reactdom › JSXがHTMLに代わる

    expect(received).toBe(expected) // Object.is equality

    Expected: "<div><div><div><span>hello</span></div></div></div>"
    Received: "<hoge><hoge><hoge><span>hello</span></hoge></hoge></hoge>"
被标签替代了?
我们成功去除了div标签,但是需要确保它能正常运行。

无需任何内容即可正常呈现

让我们添加一场测试吧。

目标就只剩下hello了,对吧。

it('divを無くせる', () => {
    // Arrange
    function App() {
      return (
        <div>
          <div>
            <div>
              <span>hello</span>
            </div>
          </div>
        </div>
      );
    }
    // Act
    const container = document.createElement('div');
    ReactDOM.render(<App />, container);
    console.log(container.innerHTML)

    //Assert
    expect(container.innerHTML).toBe('<span>hello</span>');
  });

可以通过将常见的片段替换来实现不使用div进行正常渲染。

如果按照以下方式定义,由于不存在与”hohe”相同的”fragment”标签,因此可能会受到批评。

// packages/react/src/ReactElement.js
export function createElement(type, config, children) {
if (type === 'div') {
    type = 'fragment';
  }
  
  let propName;
  ...

// Warning: The tag <fragment> is unrecognized in this browser

通常在JSX中,我们使用 <> </> 的形式来编写Fragment。但是如果要在内部进行替换,应该传递转译后的类型。让我们在代码库内引用Fragment。

让我们以Babel提供的编译器演示来查看JSX的React片段会以哪种形式编译。

image.png

经过编译,简单的 JSX 已被转换。由于 Fragment 被调用到 createElement 中,我们需要查看导入的 react/jsx-runtime 的内容。

包/packages/react/jsx-runtime.js

export {Fragment, jsx, jsxs} from './src/jsx/ReactJSX';

我试图跟着 Fragment 的定义跳转,最终到达了 ReactSymbols.js。
看起来,所有 React 元素的类型都在这里定义?。

通过在Symbol中进行定义,React元素的标签在任何环境下都会返回唯一的值,且保持不可变性。

让我们将相应的类型定义定义在createElement函数中。

import {REACT_FRAGMENT_TYPE} from 'shared/ReactSymbols';

export function createElement(type, config, children) {
  if (type === 'div') {
    type = REACT_FRAGMENT_TYPE;
  }
  
  let propName;
  ...

进行测试

 console.log
    <span>hello</span>

      at Object.<anonymous> (packages/react-dom/src/__tests__/ReactHoge-test.js:45:13)

 PASS  packages/react-dom/src/__tests__/ReactHoge-test.js
  reactdom
    ✓ divを無くせる (134 ms)

渲染时成功地删除了div的所有内容?

結局

這次我們使用實作式的方式來看React元素的建立過程。
createElement只是React UI架構中的一個階段而已。還有後續的處理,像是調和(React Fiber)、掛載DOM、狀態和生命周期等話題。我們可以在相應的函數中加入console log,或改變行為,同時玩著React的內部實現,一起學習吧!


2023 年的第 14 天,株式会社 BitKey 的 Advent Calendar 将由 @takumi_sakao 负责!敬请期待✨

广告
将在 10 秒后关闭
bannerAds