深蓝互联专注深圳小程序开发、微网站、O2O系统、APP开发和深圳网站建设欢迎咨询邮箱:wisepu@szdbi.com   电话:13530005652   联系我们   |   网站地图   



自己写个React渲染器: 以 Remax 为例(用React写小程序)

来源:     发布:     点击:

关于React的一些基本概念

创建一个 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 的类型,它有两种类型:

  • Host Component: 宿主组件,这是由渲染的平台提供的‘内置’组件,例如ReactDOM 平台下面的 DOM 节点,如 divspan... 这些组件类型为字符串
  • Composite Component: 复合组件,这是一种用户自定义的组件封装单位。通常包含自定义的逻辑、状态以及输出 Element 树。复合类型可以为类或函数
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)

  • 协调阶段(Reconciliation Phase) 这个阶段 React 会找出需要更新的节点。这个阶段是可以被打断的,比如有优先级更高的事件要处理时。
  • 提交阶段(Commit Phase) 将上一个阶段计算出来的需要处理的副作用(Effects)一次性执行了。这个阶段必须同步执行,不能被打断

 

如果按照render为界,可以将生命周期函数按照两个阶段进行划分:

  • 协调阶段
    • constructor
    • componentWillMount 废弃
    • componentWillReceiveProps 废弃
    • static getDerivedStateFromProps
    • shouldComponentUpdate
    • componentWillUpdate 废弃
    • render
    • getSnapshotBeforeUpdate()
  • 提交阶段
    • componentDidMount
    • componentDidUpdate
    • componentWillUnmount

 

没理解?那么下文读起来对你可能比较吃力,建议阅读一些关于React基本原理的相关文章。

 

就目前而言,React 大部分核心的工作已经在 Reconciler 中完成,好在 React 的架构和模块划分还比较清晰,React官方也暴露了一些库,这极大简化了我们开发 Renderer 的难度。开始吧!

 

自定义React渲染器

React官方暴露了一些库供开发者来扩展自定义渲染器:

  • react-reconciler - 这就是 React 的协调器, React 的核心所在。我们主要通过它来开发渲染器。
  • scheduler - 合作调度器的一些 API 。本文不会用到
需要注意的是,这些包还是实验性的,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 渲染器适配

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;
  // 表示一个空的计时器,见                

相关文章

无相关信息


深圳市龙华新区龙观西路2号宝龙大厦A903(与布龙路交汇处)

  • 服务热线:0755-23110995
  • 24小时咨询:13530005652
  • 邮箱:wisepu@szdbi.com