自己写个React渲染器: 以 Remax 为例(用React写小程序)
2019-11-19 13:54:13
创建一个 React 自定义渲染器,你需要对React渲染的基本原理有一定的了解。所以在深入阅读本文之前,先要确保你能够理解以下几个基本概念:
1. Element
我们可以通过 JSX
或者 React.createElement
来创建 Element,用来描述我们要创建的视图节点。比如:
<button class='button button-blue'>
<b>
OK!
</b>
</button>
JSX
会被转义译为:
React.createElement(
"button",
{ class: 'button button-blue' },
React.createElement("b", null, "OK!")
)
React.createElement
最终构建出类似这样的对象:
{
type: 'button',
props: {
className: 'button button-blue',
children: {
type: 'b',
props: {
children: 'OK!'
}
}
}
}
也就是说 Element 就是一个普通的对象,描述用户创建的节点类型、props 以及 children。这些 Elements 组合成树,描述用户视图
2. Component
可以认为是 Element 的类型,它有两种类型:
ReactDOM
平台下面的 DOM
节点,如 div
、span
... 这些组件类型为字符串
const DeleteAccount = () => (
<div>
<p>Are you sure?</p>
<DangerButton>Yep</DangerButton>
<Button color='blue'>Cancel</Button>
</div>
);
3. Instance
当 React 开始渲染一个 Element 时,会根据组件类型为它创建一个‘实例’,例如类组件,会调用new
操作符实例化。这个实例会一直引用,直到 Element 从 Element Tree 中被移除。
首次渲染
: React 会实例化一个 MyButton
实例,调用挂载相关的生命周期方法,并执行 render
方法,递归渲染下级
render(<MyButton>foo</MyButton>, container)
更新
: 因为组件类型没有变化,React 不会再实例化,这个属于‘节点更新’,React 会执行更新相关的生命周期方法,如shouldComponentUpdate
。如果需要更新则再次执行render
方法
render(<MyButton>bar</MyButton>, container)
卸载
: 组件类型不一样了, 原有的 MyButton 被替换. MyButton 的实例将要被销毁,React 会执行卸载相关的生命周期方法,如componentWillUnmount
render(<button>bar</button>, container)
4. Reconciler & Renderer
Reconciler
和 Renderer
的关系可以通过下图缕清楚.
Reconciler 的职责是维护 VirtualDOM 树,内部实现了 Diff/Fiber 算法,决定什么时候更新、以及要更新什么
而 Renderer 负责具体平台的渲染工作,它会提供宿主组件、处理事件等等。例如ReactDOM就是一个渲染器,负责DOM节点的渲染和DOM事件处理。
5. Fiber 的两个阶段 React 使用了 Fiber 架构之后,更新过程被分为两个阶段(Phase)
如果按照render
为界,可以将生命周期函数按照两个阶段进行划分:
constructor
componentWillMount
废弃componentWillReceiveProps
废弃static getDerivedStateFromProps
shouldComponentUpdate
componentWillUpdate
废弃render
getSnapshotBeforeUpdate()
componentDidMount
componentDidUpdate
componentWillUnmount
没理解?那么下文读起来对你可能比较吃力,建议阅读一些关于React基本原理的相关文章。
就目前而言,React 大部分核心的工作已经在 Reconciler 中完成,好在 React 的架构和模块划分还比较清晰,React官方也暴露了一些库,这极大简化了我们开发 Renderer 的难度。开始吧!
React官方暴露了一些库供开发者来扩展自定义渲染器:
需要注意的是,这些包还是实验性的,API可能不太稳定。另外,没有详细的文档,你需要查看源代码或者其他渲染器实现;本文以及扩展阅读中的文章也是很好的学习资料。
创建一个自定义渲染器只需两步:
第一步: 实现宿主配置,这是react-reconciler
要求宿主提供的一些适配器方法和配置项。这些配置项定义了如何创建节点实例、构建节点树、提交和更新等操作。下文会详细介绍这些配置项
const Reconciler = require('react-reconciler');
const HostConfig = {
// ... 实现适配器方法和配置项
};
第二步:实现渲染函数,类似于ReactDOM.render()
方法
// 创建Reconciler实例, 并将HostConfig传递给Reconciler
const MyRenderer = Reconciler(HostConfig);
/**
* 假设和ReactDOM一样,接收三个参数
* render(<MyComponent />, container, () => console.log('rendered'))
*/
export function render(element, container, callback) {
// 创建根容器
if (!container._rootContainer) {
container._rootContainer = ReactReconcilerInst.createContainer(container, false);
}
// 更新根容器
return ReactReconcilerInst.updateContainer(element, container._rootContainer, null, callback);
}
容器既是 React 组件树挂载的目标
(例如 ReactDOM 我们通常会挂载到 #root
元素,#root
就是一个容器)、也是组件树的 根Fiber节点(FiberRoot)
。根节点是整个组件树的入口,它将会被 Reconciler 用来保存一些信息,以及管理所有节点的更新和渲染。
关于 Fiber 架构的一些细节可以看这些文章:
HostConfig
支持非常多的参数,完整列表可以看这里. 下面是一些自定义渲染器必须提供的参数:
interface HostConfig {
/**
* 用于分享一些上下文信息
*/
// 获取根容器的上下文信息, 只在根节点调用一次
getRootHostContext(rootContainerInstance: Container): HostContext;
// 获取子节点的上下文信息, 每遍历一个节点都会调用一次
getChildHostContext(parentHostContext: HostContext, type: Type, rootContainerInstance: Container): HostContext;
/**
* 节点实例的创建
*/
// 普通节点实例创建,例如DOM的Element类型
createInstance(type: Type, props: Props, rootContainerInstance: Container, hostContext: HostContext, internalInstanceHandle: OpaqueHandle,): Instance;
// 文本节点的创建,例如DOM的Text类型
createTextInstance(text: string, rootContainerInstance: Container, hostContext: HostContext, internalInstanceHandle: OpaqueHandle): TextInstance;
// 决定是否要处理子节点/子文本节点. 如果不想创建则返回true. 例如ReactDOM中使用dangerouslySetInnerHTML, 这时候子节点会被忽略
shouldSetTextContent(type: Type, props: Props): boolean;
/**
* 节点树构建
*/
// 如果节点在*未挂载*状态下,会调用这个来添加子节点
appendInitialChild(parentInstance: Instance, child: Instance | TextInstance): void;
// **下面都是副作用(Effect),在’提交‘阶段被执行**
// 添加子节点
appendChild?(parentInstance: Instance, child: Instance | TextInstance): void;
// 添加子节点到容器节点(根节点)
appendChildToContainer?(container: Container, child: Instance | TextInstance): void;
// 插入子节点
insertBefore?(parentInstance: Instance, child: Instance | TextInstance, beforeChild: Instance | TextInstance): void;
// 插入子节点到容器节点(根节点)
insertInContainerBefore?(container: Container, child: Instance | TextInstance, beforeChild: Instance | TextInstance,): void;
// 删除子节点
removeChild?(parentInstance: Instance, child: Instance | TextInstance): void;
// 从容器节点(根节点)中移除子节点
removeChildFromContainer?(container: Container, child: Instance | TextInstance): void;
/**
* 节点挂载
*/
// 在完成所有子节点初始化时(所有子节点都appendInitialChild完毕)时被调用, 如果返回true,则commitMount将会被触发
// ReactDOM通过这个属性和commitMount配置实现表单元素的autofocus功能
finalizeInitialChildren(parentInstance: Instance, type: Type, props: Props, rootContainerInstance: Container, hostContext: HostContext): boolean;
// 和finalizeInitialChildren配合使用,commitRoot会在’提交‘完成后(resetAfterCommit)执行, 也就是说组件树渲染完毕后执行
commitMount?(instance: Instance, type: Type, newProps: Props, internalInstanceHandle: OpaqueHandle): void;
/**
* 节点更新
*/
// 准备节点更新. 如果返回空则表示不更新,这时候commitUpdate则不会被调用
prepareUpdate(instance: Instance, type: Type, oldProps: Props, newProps: Props, rootContainerInstance: Container, hostContext: HostContext,): null | UpdatePayload;
// **下面都是副作用(Effect),在’提交‘阶段被执行**
// 文本节点提交
commitTextUpdate?(textInstance: TextInstance, oldText: string, newText: string): void;
// 普通节点提交
commitUpdate?(instance: Instance, updatePayload: UpdatePayload, type: Type, oldProps: Props, newProps: Props, internalInstanceHandle: OpaqueHandle): void;
// 重置普通节点文本内容, 这个需要和shouldSetTextContent(返回true时)配合使用,
resetTextContent?(instance: Instance): void;
/**
* 提交
*/
// 开始’提交‘之前被调用,比如这里可以保存一些状态,在’提交‘完成后恢复状态。比如ReactDOM会保存当前元素的焦点状态,在提交后恢复
// 执行完prepareForCommit,就会开始执行Effects(节点更新)
prepareForCommit(containerInfo: Container): void;
// 和prepareForCommit对应,在提交完成后被执行
resetAfterCommit(containerInfo: Container): void;
/**
* 调度
*/
// 这个函数将被Reconciler用来计算当前时间, 比如计算任务剩余时间
// ReactDOM中会优先使用Performance.now, 普通场景用Date.now即可
now(): number;
// 自定义计时器
setTimeout(handler: (...args: any[]) => void, timeout: number): TimeoutHandle | NoTimeout;
// 取消计时器
clearTimeout(handle: TimeoutHandle | NoTimeout): void;
// 表示一个空的计时器,见
扫描二维码分享到微信