Reconciler作用
目前React的应用场景不仅限于Web App,也有很多人用来完成2D画图库、3D图形库、命令行库等工具。无论处于哪种宿主环境中,React生态大致包括如下基本概念/功能:
host内置组件(div, span, img等)
函数式组件
类组件
props, state
effects, lifecycle
key, ref, context
React.lazy, error boundaries
concurrent mode, Suspense
除内置组件与宿主相关(如React Native中包括View, Text等特定组件)以外,上述逻辑功能在跨平台的环境中是通用的。 这些通用的部分就交给reconciler来实现,当有更新出现时,reconciler就会调用render。这篇文章不会介绍调度算法的内容,可以阅读理解React Fiber架构了解更多。
Sophie Alpert在会上还提到了react-reconciler使用时的两个模式:Mutation模式和Persistent模式。
- Mutation模式
我们对DOM的修改,即ReactDOM就遵循该模式。我们通过改变已有的BOM/DOM的属性来render,这也是DOM自身的工作原理。伪代码如下:
view = createView()
updateView(view, {color: 'red'}) // 更新已有视图
div = document.createElement('div');
div.style.color = 'red';
- Persistent模式
当API将系统当成Immutable Object时,我们会将整个视图替换成另一版本。因为React Native拥有对host的绝对控制,所以下一版本的RN采用此模式实现Concurrency。
view = createView();
cloneView(view, {color: 'red'}); // 克隆新视图
现在我们要用react-reconciler来替代ReactDOM,做一个简版的渲染器,以便更好的了解React的工作原理。实现诸如以下指令:
- 渲染一个新视图
- 动态显示dom
- 利用自定义props更新背景色
实现ReactDOMMini
首先,我们用create-react-app创建一个项目,安装react-reconciler依赖。然后,用我们要实现的ReactDOMMini替代ReactDOM。
// import ReactDOM from 'react-dom';
import ReactDOMMini from './ReactDOMMini';
...
ReactDOMMini.render(<App />, document.getElementById('root'));
渲染基础视图
在ReactDOMMini中,我们需要对宿主环境进行配置,然后更新视图。react-reconciler的API如下:
const Reconciler = require('react-reconciler');
const HostConfig = {
// You'll need to implement some methods here.
// See below for more information and examples.
};
const MyRenderer = Reconciler(HostConfig);
const RendererPublicAPI = {
render(element, container, callback) {
// Call MyRenderer.updateContainer() to schedule changes on the roots.
// See ReactDOM, React Native, or React ART for practical examples.
}
};
module.exports = RendererPublicAPI;
我们需要在hostConfig里对宿主环境进行配置,描述协调器工作时需要宿主环境发生怎样的变化。HostConfig相当于一个桥梁,用户通过参数声明的方式传给配置项,reconciler将其作为module引入。
在这里也可以看到DOM的HostConfig的实现。
我们先写一些基本的配置项:
const hostConfig = {
supportsMutation: true, // 开启mutating模式
createInstance: (
type,
props,
rootContainerInstance,
hostContext,
internalInstanceHandle
) => {},
createTextInstance: (
text,
rootContainerInstance,
hostContext,
internalInstanceHandle
) => {},
appendInitialChild: (parent, child) => {},
appendChildToContainer: (container, child) => {},
removeChildFromContainer: (container, child) => {},
getRootHostContext: () => {},
prepareForCommit: () => {},
resetAfterCommit: () => {},
getChildHostContext: () => {},
shouldSetTextContent: () => {},
finalizeInitialChildren: () => {}
}
const reconciler = reactReconciler(hostConfig);
export default {
render(reactElement, domEle, callback) {
if (!domEle._rootContainer) {
domEle._rootContainer = reconciler.createContainer(domEle, false, false);
}
reconciler.updateContainer(reactElement, domEle._rootContainer, null, callback);
}
};
render方法就做了两件事,创建rootContainer和根据reconciler更新container。它接收的第三个参数是可选回调,这里不需要。 我们写的这些配置项防止页面报错,也是告诉reconciler引入这些函数。我们使用mutation模式,以便目标(例如DOM)UI API支持UI树的更新(像appendChild, removeChild的操作等)。
目前页面上还是空白的,下面是见证奇迹的时刻。为createInstance、createTextInstance、appendInitialChild、appendChildToContainer增加渲染逻辑:
createInstance: (
type,
props,
rootContainerInstance,
hostContext,
internalInstanceHandle
) => document.createElement(type),
createTextInstance: (
text,
rootContainerInstance,
hostContext,
internalInstanceHandle
) => document.createTextNode(text),
appendInitialChild: (parent, child) => {
parent.appendChild(child);
},
appendChildToContainer: (container, child) => {
container.appendChild(child);
},
- createInstance用于根据目标创建UI元素实例
- createTextInstance用于创建文本节点,因为DOM Target是使用独立的文本节点创建文本的,所以我们需要它
- appendInitialChild用于初始化UI树的创建
- appendChildToContainer在React-Reconciler的Commit阶段被调用(这里可想象成git的分支commit操作)
可以看到页面上,文本节点已经被渲染出来了。但是我们的图片元素没有展示,a标签链接功能失效。基于上述描述,修改createInstance:
createInstance: (
type,
props,
rootContainer,
hostContext,
internalInstanceHandle
) => {
const el = document.createElement(type);
Object.keys(props).forEach(propName => {
if (propName !== 'children') {
el[propName] = props[propName];
}
else {
// 打印一下children是什么
console.log(propName, props[propName]);
}
});
return el;
}
- 当props不是children时,我们把props映射到元素上。包括alt、className、href、rel、src、target等,其中className会被react处理成class
- props是children时,我们打印出子树可以看到有host组件、文本等类型,由于文本已经被createTextInstance创建了节点,这里不用额外处理。
为实例元素增加attribute之后,可以看到背景及图片都显示正常,我们的小目标也实现了。
动态显示Logo
接下来的工作是,通过点击class为App的DOM,使react logo动态显示。首先写我们的业务:
// App.js
...
const [showLogo, setShowLogo] = useState(true);
return (
<div className="App" onClick={() => {
setShowLogo(show => !show);
}}>
<header className="App-header">
{showLogo && <img src={logo} className="App-logo" alt="logo" />}
...
点击却发现onClick没有反应,说明我们刚才只赋值了静态属性,没有添加事件监听。在createInstance添加以下逻辑:
if (propName === 'onClick') {
el.addEventListener('click', props[propName]);
}
else {
el[propName] = props[propName];
}
满心欢喜的点了一下页面以为everything under control了,结果。。。
!!!控制台报错了,查看以下API我们发现,需要配置dom remove及insert的module。
removeChildFromContainer: (container, child) => {
container.removeChild(child);
},
removeChild: (parent, child) => {
parent.removeChild(child);
},
insertInContainerBefore: (container, child, before) => {
container.insertBefore(child, before);
},
insertBefore: (parent, child, before) => {
parent.insertBefore(child, before);
},
四个模块都比较好理解,包含container的是commit阶段触发的行为。又实现了一个小目标。
使自定义props工作
最后一个任务是,用一个自定义属性控制p标签的背景色定时切换,我们还是先写简单的业务逻辑:
// App.js
...
const [color, setColor] = useState('red');
useEffect(() => {
const colors = ['red', 'green', 'blue'];
let i = 0;
const interval = setInterval(() => {
i++;
setColor(colors[i % 3]);
}, 1000);
return () => clearInterval(interval);
}, []);
...
<p bgColor={color}>
Edit <code>src/App.js</code> and save to reload.
</p>
...
要完成这个任务,我们需要两个新的module,prepareUpdate和commitUpdate:
prepareUpdate: (
instance,
type,
oldProps,
newProps,
rootContainerInstance,
currentHostContext
) => {
let payload;
if (oldProps.bgColor !== newProps.bgColor) {
payload = {newBgColor: newProps.bgColor};
}
return payload;
},
commitUpdate: (
instance,
updatePayload,
type,
oldProps,
newProps,
finishedWork
) => {
if (updatePayload.newBgColor) {
instance.style.backgroundColor = updatePayload.newBgColor;
}
}
- prepareUpdate用于diff元素的oldProps和newProps。在这里,当bgColor的props变化时,我们硬编码了一个新的props叫newBgColor
- commitUpdate用于随后更新实例props,这里当有newBgColor时,就将它赋予backgroundColor。
总结
到这里我们就依赖fiber-reconciler完成了一个mini版本的ReactDOM的render操作,仔细想想我们还是有很多功能没有实现,例如form controls、refs、svg、portals、事件冒泡等。要想用好fiber-reconciler的module,需要理解currentTree和workInProgressTree,render阶段和commit阶段,感兴趣的同学可以接下来自己深入研究。
附上ReactDOMMini不到100行的全部代码,去掉了没用到的参数:
/**
* @file ReactDOMMini.js
* @create date 2019-10-30 15:53:15
* @modify date 2019-10-31 15:15:29
*/
import reactReconciler from 'react-reconciler';
const hostConfig = {
/* 宿主环境配置 */
supportsMutation: true,
getRootHostContext: () => { },
prepareForCommit: () => { },
resetAfterCommit: () => { },
getChildHostContext: () => { },
shouldSetTextContent: () => { },
finalizeInitialChildren: () => { },
createInstance: (
type,
props
) => {
const el = document.createElement(type);
Object.keys(props).forEach(propName => {
if (propName !== 'children') {
if (propName === 'onClick') {
el.addEventListener('click', props[propName]);
}
else {
el[propName] = props[propName];
}
}
});
return el;
},
createTextInstance: text => document.createTextNode(text),
appendInitialChild: (parent, child) => {
parent.appendChild(child);
},
appendChildToContainer: (container, child) => {
container.appendChild(child);
},
removeChildFromContainer: (container, child) => {
container.removeChild(child);
},
removeChild: (parent, child) => {
parent.removeChild(child);
},
insertInContainerBefore: (container, child, before) => {
container.insertBefore(child, before);
},
insertBefore: (parent, child, before) => {
parent.insertBefore(child, before);
},
prepareUpdate: (
instance,
type,
oldProps,
newProps
) => {
let payload;
if (oldProps.bgColor !== newProps.bgColor) {
payload = {newBgColor: newProps.bgColor};
}
return payload;
},
commitUpdate: (
instance,
updatePayload,
) => {
if (updatePayload.newBgColor) {
instance.style.backgroundColor = updatePayload.newBgColor;
}
}
};
const reconciler = reactReconciler(hostConfig);
export default {
render(reactElement, domEle, callback) {
if (!domEle._rootContainer) {
domEle._rootContainer = reconciler.createContainer(domEle, false, false);
}
reconciler.updateContainer(reactElement, domEle._rootContainer, null, callback);
}
};