实现函数式组件
On this page
大家好,我是万维读客的讲师曹欢欢。上节我们实现了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直接渲染,子节点会出现数组的情况? 遇到这种问题该怎么调试?
参考
- 函数式组件:https://juejin.cn/post/7285673629526704164
- flat:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/flat