实现函数式组件

大家好,我是万维读客的讲师曹欢欢。上节我们实现了react的fiber节点异步渲染,但是实际使用我们都是编写组件,本节我们学习如何渲染一个函数式组件。

函数式组件

函数式编程大家都比较熟悉,通过函数的组合而不是类的继承来完成功能。函数式组件是什么了呢?举个例子:

function App({title, children}){
  return (
    <div>
      <h3>{title}</h3>
      {children}
    </div>
  );
}

函数式组件和一般组件不同的地方,有以下几点:

  • 函数式组件没有render方法,没有DOM节点,stateNode=null
  • 组件的children来自于函数返回结果,而不是props.children

好了,了解了这些,我们可以修改我们的Treact,让他支持函数式组件的渲染。

测试用修改

创建新的treact04文件夹,创建jsx.test.jsx,增加测试用例如下:

import { describe, it, expect } from 'vitest';
import * as Treact from './Treact';

describe('async render Function Component', () => {
    it('render with act', async () => {
        function App({title, children}){
            return (
              <div>
                <h3>{title}</h3>
                {children}
              </div>
            );
          }
        const container = document.createElement('div');
        const root = Treact.createRoot(container);
        await Treact.act(() => {
            root.render(<App title="w3cdoc">
                <App title="hello"></App>
            </App>);
            expect(container.innerHTML).toBe('');
        });
        console.log('container.innerHTML', container.innerHTML);
        expect(container.innerHTML).toBe('xxx');
    })
})

然后查看测试用例执行会报错,错误如下:

 FAIL  treact04/jsx.test.jsx > async render Function Component > render with act
AssertionError: expected '<function app({ title, children }) {\…' to be '<div class="container"><span>hello</s…' // Object.is equality

- Expected
+ Received
Test Files  1 failed (1)
    Tests  1 failed (1)
  Start at  19:52:19
  Duration  28ms

可以看到解析的错误出现了function类型的标签,下面我们来完善Treact支持function类型。

支持function类型

这里我们主要修改处理fiber节点部分,这部分代码在performUnitOfWork函数中,之前代码:

performUnitOfWork(fiber) {
  ...
  if (!fiber.stateNode) {
      fiber.stateNode = fiber.type === 'HostText' ? document.createTextNode('') : document.createElement(fiber.type);
      Object.keys(fiber.props).filter(key => key != 'children').forEach(key => {
          fiber.stateNode[key] = fiber.props[key];
      })
  }
  ....

前面我们介绍了fiber节点数据结构的type属性,就是标签的类型。这个在函数组件的时候,type存放的就是这个function函数,比如上面的APP。所以可以用这个type来判断是不是函数式组件。修改如下:

performUnitOfWork(fiber) {
    const isFunctionComp = fiber.type instanceof Function;
    if (isFunctionComp) {
        fiber.props.children = [fiber.type(fiber.props)];
    } else {
        if (!fiber.stateNode) {
            fiber.stateNode = fiber.type === 'HostText' ? document.createTextNode('') : document.createElement(fiber.type);
            Object.keys(fiber.props).filter(key => key != 'children').forEach(key => {
                fiber.stateNode[key] = fiber.props[key];
            })
        }
        if (fiber.return) {
            fiber.return.stateNode.appendChild(fiber.stateNode);
        }
    }
  ...

然后查看测试用例控制台报错,错误是没有stateNode:

 ❯ TreactRoot.performUnitOfWork treact04/Treact.jsx:77:36
     75|         }
     76|         if (fiber.return) {
     77|             fiber.return.stateNode.appendChild(fiber.stateNode);
       |                                    ^
     78|         }

函数式组件没有stateNode,这里我们需要特殊处理下,找他的父级节点,修改treact代码如下:

if (fiber.return) {
    let tempParenNode = fiber.return;
    while (!tempParenNode.stateNode) {
        tempParenNode = tempParenNode.return;
    }
    tempParenNode.stateNode.appendChild(fiber.stateNode);
}

继续看测试用例报错日志如下:

TypeError: Cannot convert undefined or null to object
 ❯ TreactRoot.performUnitOfWork treact04/Treact.jsx:71:24
     69|             if (!fiber.stateNode) {
     70|                 fiber.stateNode = fiber.type === 'HostText' ? document.createTextNode('') : document.createElement(fiber.type);
     71|                 Object.keys(fiber.props).filter(key => key != 'children').forEach(key => {
       |                        ^
     72|                     fiber.stateNode[key] = fiber.props[key];
     73|                 })
 ❯ TreactRoot.workloop treact04/Treact.jsx:56:35
 ❯ Timeout._onTimeout treact04/Treact.jsx:7:9
 ❯ listOnTimeout node:internal/timers:569:17
 ❯ processTimers node:internal/timers:512:7

我们增加日志看下报错的数据是什么:

performUnitOfWork(fiber) {
  console.log('fiber props', fiber.props,fiber.props==null?fiber.return.props.children:'' );
...

打印结果如下:

 RERUN  treact04/Treact.jsx x30

stdout | treact04/jsx.test.jsx > async render Function Component > render with act
fiber props { children: [ { type: [Function: App], props: [Object] } ] } 
fiber props {
  title: 'w3cdoc',
  children: [ { type: [Function: App], props: [Object] } ]
} 
fiber props { children: [ { type: 'h3', props: [Object] }, [ [Object] ] ] } 
fiber props { children: [ { type: 'HostText', props: [Object] } ] } 
fiber props { nodeValue: 'w3cdoc', children: [] } 
fiber props undefined [
  { type: 'h3', props: { children: [Array] } },
  [ { type: [Function: App], props: [Object] } ]
]

 ❯ treact04/jsx.test.jsx (1) 5005ms
   ❯ async render Function Component (1) 5004ms
     × render with act 5003ms

可以看下fiber.return.props.children,这个数据是父节点的children数据,第二条是个数组(这个是因为我们在写APP组件的时候,直接用的props穿的参数children来渲染的),导致渲染这个sibling的时候报错,所以这个要在数据构造的时候处理下,把这个数组提取出来,可以用Array新增的api函数flat来处理。

export function createElement(type, props, ...children) {
    return {
        type,
        props: {
            ...props,
            children: children.flat().map(child => {
                if (typeof child != 'object') {
                    return {
                        type: 'HostText',
                        props: {
                            nodeValue: child,
                            children: []
                        }
                    }
                } else {
                    return child;
                }
            })
        }
    }
}

最后看下输出的数据,更新下测试用例的断言为需要渲染的html, 修改如下:

expect(container.innerHTML).toBe('<div><h3>w3cdoc</h3><div><h3>hello</h3></div></div>');

查看测试用例通过。

 ✓ treact04/jsx.test.jsx (1)
   ✓ async render Function Component (1)
     ✓ render with act

 Test Files  1 passed (1)
      Tests  1 passed (1)
   Start at  20:35:04
   Duration  49ms

课后问题

大家想一下为什么传递children直接渲染,子节点会出现数组的情况? 遇到这种问题该怎么调试?

参考

  1. 函数式组件:https://juejin.cn/post/7285673629526704164
  2. flat:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/flat


请遵守《互联网环境法规》文明发言,欢迎讨论问题
扫码反馈

扫一扫,反馈当前页面

咨询反馈
扫码关注
返回顶部