Skip to content

NuoHui/renderer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

原文链接:

React 帮助您以声明方式编写 UI。更多关于声明式与命令式的信息在这里React最初是为 Web 开发设计的,但到目前为止,它已扩展为 React NativeReact CanvasRedocxReact PDF 甚至 React Hardware

如果你想了解更多自定义渲染器,可以查阅该列表

我一直想了解这些渲染器是如何工作的,在这篇文章中,我将详细探讨 React 渲染器。 Ken Wheeler 在 React 阿姆斯特丹 2017 上的演讲之一激发了我的兴趣。

<iframe width="910" height="512" src="https://www.youtube.com/embed/oPofnLZZTwQ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

这篇文章的大部分内容是作者对 React 进行实验并阅读多个 React 渲染器代码库和博客文章的结果。

⚛️ React Core, Reconciler and Renderer

React仓库主要包括三部分:

React Core

主要作为上层应用,对外暴露顶级React API。比如

  • React.createElement()
  • React.createClass()
  • React.Component
  • React.Children
  • React.PropTypes

它不包括具体的diff算法或任何特定于平台的代码。

Renderer

React 最初是为 DOM(浏览器) 创建的,但后来经过演变,也支持 React Native 的原生平台使用。这将“渲染器renderers”的概念引入到 React 内部。渲染器管理 React Tree如何变成底层平台调用。

img

联想最近在看的remax源码,其内部就是通过Reconciler实现了一个运行时@remax/runtime,以及各个平台的View API。后面会详情介绍remax相关源码介绍。

Reconciler

React 最初只是服务于 DOM,但是这之后被改编成也能同时支持原生平台的 React Native。因此,在 React 内部机制中引入了“渲染器”这个概念。

渲染器用于管理一棵 React 树,使其根据底层平台进行不同的调用。

渲染器同样位于 packages/ 目录下:

  • React DOM Renderer 将 React 组件渲染成 DOM。它实现了全局 ReactDOMAPI,这在npm上作为 react-dom 包。这也可以作为单独浏览器版本使用,称为 react-dom.js,导出一个 ReactDOM 的全局对象.
  • React Native Renderer 将 React 组件渲染为 Native 视图。此渲染器在 React Native 内部使用。

即便 React DOMReact Native 渲染器的区别很大,但也需要共享一些逻辑。特别是协调算法需要尽可能相似,这样可以让声明式渲染,自定义组件,state,生命周期方法和 refs 等特性,保持跨平台工作一致。

为了解决这个问题,不同的渲染器彼此共享一些代码。我们称 React 的这一部分为 “reconciler”。当处理类似于 setState() 这样的更新时,reconciler 会调用树中组件上的 render(),然后决定是否进行挂载,更新或是卸载操作。

stack reconciler

“stack” reconciler 是 React 15 及更早的解决方案。虽然我们已经停止了对它的使用, 但是在这里有详细的文档。

Fiber reconciler

“fiber” reconciler 是一个新尝试,致力于解决 stack reconciler 中固有的问题,同时解决一些历史遗留问题。Fiber 从 React 16 开始变成了默认的 reconciler。

它的主要目标是:

  • 能够把可中断的任务切片处理。
  • 能够调整优先级,重置并复用任务。
  • 能够在父元素与子元素之间交错处理,以支持 React 中的布局。
  • 能够在 render() 中返回多个元素。
  • 更好地支持错误边界。

你可以在这里这里,深入了解 React Fiber 架构。虽然这已经在 React 16 中启用了,但是 async 特性还没有默认开启。

源代码在 packages/react-reconciler 目录下。

推荐阅读:

🕵🏻‍ Components, Instances, Elements and Fiber

假设我们有一个如下所示的 React 应用程序:

import React from 'react'
import ReactDom from 'react-dom'

class MyButton extends React.Component {
  state = { text: 'click me' }
  onBtnClick = () => {
    this.setState(() => ({ text: 'I was clicked' }))
  }
  render() {
    return <button onClick={this.onBtnClick}> {this.state.text} </button>
  }
}

const Content = props => <p>{props.text}</p>

const App = () => {
  return (
    <div>
      <p style="padding:20px">Hello World</p>
      <Content text="hello world" />
      <MyButton />
    </div>
  )
}

ReactDom.render(<App />, document.getElementById('root'))

Component

在上面的示例中:MyButtonContentApp 本质上是您定义的组件。组件可以定义为类(MyButton)或函数(Content App)。它基本上是对 UI 元素的外观和行为方式的声明。

站在渲染器的角度,有两种类型的React组件:

  • Host Components: 指的是运行于特定平台的组件,比如<div>,<View>
  • Composite Components: 复合组件指的是用户自定义的组件。比如<MyButton> or <Content>

Instances

对于声明为类的组件,实例是组件在内存中的实例化后的版本。或者你也可以理解实例指的就是你在编程中使用的this。它对于存储本地状态和响应生命周期事件非常有用。同一个组件可以有多个独立的实例。我们永远不会手动创建这些实例,它们将由 React 管理。此外,函数组件没有实例。

Elements

元素是描述组件实例或 DOM 节点及其所需属性的不可变纯对象。组件的渲染函数返回一个元素。在函数组件的情况下。输入是props,而输出是React元素。由于元素只是简单的 JS 对象,因此它们很容易遍历并且不需要解析。

我们可以简单看一个例子:

const Content = props => {
  return <p style={props.style}>{props.text}</p>
}

我们再看看我们是怎么调用的。

<Content style="background:blue;" text="hello world" />

我们通过console看看React是如何执行这个函数组件的,观察它的输出。

const props = { text: 'hello world', style: 'background:blue;' };
console.log(Content(props))
// This logs
{
  "type":"p",
  "props":{
    "style":"background:blue;",
    "children":"hello world"
  },
}

这是一个组件的React元素。它仅包含有关组件类型 p 及其props(样式、子项)的信息。换句话说,它是一个轻量级的 javascript 对象,只包含在屏幕上绘制元素所需的信息。

同理,我们看下面的App组件。

const App = () => {
  return (
    <div>
      <p style="padding:20px">Hello World</p>
      <Content text="hello world" />
      <MyButton />
    </div>
  )
}
console.log(App())
// This would log
{
   "type": "div",
   "props": {
      "children":[
         {
            "type":"p",
            "props":{
               "style":"padding:20px",
               "children":"Hello World"
            },
         },
         {
           "type": ƒ Content(props),
           "props": {"text":"hello world"},
         },
         {
           "type": ƒ MyButton()
         },
      ]
   },
}

如果您仔细观察,第二个和第三个孩子节点的类型不是字符串。它们是函数(实质上是组件)。现在,react reconciler 会在那些类型不是字符串的child节点上调用render。这将递归地发生,直到 react 可以将所有类型解析为字符串。因此,如果一个 react 元素类型是一个字符串,它就是一个 dom 元素,否则它就是一个组件。

这里推荐阅读:Dan Abramov here: https://reactjs.org/blog/2015/12/18/react-components-elements-and-instances.html。

fiber

这是在新的 React Fiber Reconciler 中引入的。fiber是一个 JavaScript 对象,其中包含有关组件及其输入和输出的信息。它与实例具有一对一的关系。它管理实例的工作。

fiber使用属性 stateNode 跟踪实例。它也有关于它与其他实例的关系的信息。在任何时候,一个组件实例最多有两个与之对应的fiber:the current (flushed fiber or rendered fiber) and the work-in-progress fiber。一个fiber节点看起来像这样

{
  child, stateNode, siblings, alternate, return, type, key
}

源码我们可以理解 React Fiber reconciler 使用 react 元素为组件实例生成 React Fiber。关于fiber推荐阅读You can find more details about the fiber here

现在我们已经完成前置知识的学习了,可以进入主题啦。

brace yourselves

👷🏻‍ Lets build a custom rendere

创建模板项目

npx create-react-app renderer
cd renderer

项目结构骨架

├── README.md
├── package.json
├── node_modules
├── public
├── src
│   ├── index.js #remove everything except index.js
│   └── renderer
│       └── index.js  #This is a new file
└── yarn.lock

入口文件

import React from 'react'
import ReactDOM from 'react-dom'

const Text = props => {
  return <p className={props.className}>{props.content}</p>
}

const App = () => {
  return (
    <div>
      <Text className="hello-class" content="Hello" />
      <span style={{ color: 'blue' }}>World</span>
    </div>
  )
}

ReactDOM.render(<App />, document.getElementById('root'))

启动

npm start

boilerplate demo

我们可以看到浏览器正确渲染出hello world

自定义渲染器第一次提交

src/index.js

import React from 'react'
// import ReactDOM from "react-dom";
import CustomRenderer from './renderer'

const Text = props => {
  return <p className={props.className}>{props.content}</p>
}

const App = () => {
  return (
    <div>
      <Text className="hello-class" content="Hello" />
      <span style={{ color: 'blue' }}>World</span>
    </div>
  )
}

// ReactDOM.render(<App />, document.getElementById("root"));
CustomRenderer.render(<App />, document.getElementById('root'))

src/renderer/index.js

const CustomRenderer = {
  render(element, renderDom, callback) {
    // element: This is the react element for App component
    // renderDom: This is the host root element to which the rendered app will be attached.
    // callback: if specified will be called after render is done.
    console.log('render called', element, renderDom, callback)
  },
}

module.exports = CustomRenderer

启动项目我们看到结果:

截屏2021-12-11 上午1.01.38

接下来看到报错后,我们可以参考其他渲染器相关的代码、文档。

  • 我们发现React 团队将实验版本的 react-reconciler 导出为 npm 包,可单独使用。
  • 每个平台的渲染器,无论是 domreact native 等,都必须有自己的hostConfig,以及 react-reconciler。渲染器需要在hostConfig中实现所有必要的平台特定功能。渲染器中的react-reconciler模块将通过提供的 hostConfig 调用平台特定的函数来执行dom更改或视图更新。

因此总结下来就是我们需要:自己实现一个react-reconciler示例。

renderer in short

安装react-reconciler

yarn add react-reconciler

修改src/renderer/index.js

const Reconciler = require('react-reconciler')

const HostConfig = {
  //TODO We will specify all required methods here
}
const reconcilerInstance = Reconciler(HostConfig)

const CustomRenderer = {
  render(element, renderDom, callback) {
    // element: This is the react element for App component
    // renderDom: This is the host root element to which the rendered app will be attached.
    // callback: if specified will be called after render is done.

    const isAsync = false // Disables async rendering
    const container = reconcilerInstance.createContainer(renderDom, isAsync) // Creates root fiber node.

    const parentComponent = null // Since there is no parent (since this is the root fiber). We set parentComponent to null.
    reconcilerInstance.updateContainer(
      element,
      container,
      parentComponent,
      callback
    ) // Start reconcilation and render the result
  },
}

module.exports = CustomRenderer

我们可以简单介绍我们上述所做了哪些事情:

  • 我们把hostConfig作为参数创建了一个reconciler实例。
  • 我们通过 reconcilerInstance.createContainer.方法创建了一个与renderDom对应的root fiber node(container)container将会被reconciler用来管理后续renderDom的更新。
  • 我们最后调用reconcilerInstance.updateContainer方法,开启reconcilation

可能你也发现我们设置isAsync为false,这个参数是用来控制fiber node的工作模式。设置为false标识禁用AsyncMode模式。

重新启动项目:

yarn start

我们会看到类似报错。

截屏2021-12-11 下午10.55.01

因为还没有在hostConfig中实现对应的很多方法。

HostConfig

我们需要在hostConfig中实现平台所有必要的功能。

const HostConfig = {
  //TODO We will specify all required methods here
}

参考source code of react-reconciler源码我们看到所有的方法列表。

export const getPublicInstance = $$$hostConfig.getPublicInstance;
export const getRootHostContext = $$$hostConfig.getRootHostContext;
export const getChildHostContext = $$$hostConfig.getChildHostContext;
export const prepareForCommit = $$$hostConfig.prepareForCommit;
export const resetAfterCommit = $$$hostConfig.resetAfterCommit;
export const createInstance = $$$hostConfig.createInstance;
export const appendInitialChild = $$$hostConfig.appendInitialChild;
export const finalizeInitialChildren = $$$hostConfig.finalizeInitialChildren;
export const prepareUpdate = $$$hostConfig.prepareUpdate;
export const shouldSetTextContent = $$$hostConfig.shouldSetTextContent;
export const shouldDeprioritizeSubtree =
  $$$hostConfig.shouldDeprioritizeSubtree;
export const createTextInstance = $$$hostConfig.createTextInstance;
export const scheduleDeferredCallback = $$$hostConfig.scheduleDeferredCallback;
export const cancelDeferredCallback = $$$hostConfig.cancelDeferredCallback;
export const scheduleTimeout = $$$hostConfig.setTimeout;
export const cancelTimeout = $$$hostConfig.clearTimeout;
export const noTimeout = $$$hostConfig.noTimeout;
export const now = $$$hostConfig.now;
export const isPrimaryRenderer = $$$hostConfig.isPrimaryRenderer;
export const supportsMutation = $$$hostConfig.supportsMutation;
export const supportsPersistence = $$$hostConfig.supportsPersistence;
export const supportsHydration = $$$hostConfig.supportsHydration;


// -------------------
//      Mutation
//     (optional)
// -------------------
export const appendChild = $$$hostConfig.appendChild;
export const appendChildToContainer = $$$hostConfig.appendChildToContainer;
export const commitTextUpdate = $$$hostConfig.commitTextUpdate;
export const commitMount = $$$hostConfig.commitMount;
export const commitUpdate = $$$hostConfig.commitUpdate;
export const insertBefore = $$$hostConfig.insertBefore;
export const insertInContainerBefore = $$$hostConfig.insertInContainerBefore;
export const removeChild = $$$hostConfig.removeChild;
export const removeChildFromContainer = $$$hostConfig.removeChildFromContainer;
export const resetTextContent = $$$hostConfig.resetTextContent;
export const hideInstance = $$$hostConfig.hideInstance;
export const hideTextInstance = $$$hostConfig.hideTextInstance;
export const unhideInstance = $$$hostConfig.unhideInstance;
export const unhideTextInstance = $$$hostConfig.unhideTextInstance;

// -------------------
//     Persistence
//     (optional)
// -------------------
export const cloneInstance = $$$hostConfig.cloneInstance;
export const createContainerChildSet = $$$hostConfig.createContainerChildSet;
export const appendChildToContainerChildSet =
  $$$hostConfig.appendChildToContainerChildSet;
export const finalizeContainerChildren =
  $$$hostConfig.finalizeContainerChildren;
export const replaceContainerChildren = $$$hostConfig.replaceContainerChildren;
export const cloneHiddenInstance = $$$hostConfig.cloneHiddenInstance;
export const cloneUnhiddenInstance = $$$hostConfig.cloneUnhiddenInstance;
export const createHiddenTextInstance = $$$hostConfig.createHiddenTextInstance;

// -------------------
//     Hydration
//     (optional)
// -------------------
export const canHydrateInstance = $$$hostConfig.canHydrateInstance;
export const canHydrateTextInstance = $$$hostConfig.canHydrateTextInstance;
export const getNextHydratableSibling = $$$hostConfig.getNextHydratableSibling;
export const getFirstHydratableChild = $$$hostConfig.getFirstHydratableChild;
export const hydrateInstance = $$$hostConfig.hydrateInstance;
export const hydrateTextInstance = $$$hostConfig.hydrateTextInstance;
export const didNotMatchHydratedContainerTextInstance =
  $$$hostConfig.didNotMatchHydratedContainerTextInstance;
export const didNotMatchHydratedTextInstance =
  $$$hostConfig.didNotMatchHydratedTextInstance;
export const didNotHydrateContainerInstance =
  $$$hostConfig.didNotHydrateContainerInstance;
export const didNotHydrateInstance = $$$hostConfig.didNotHydrateInstance;
export const didNotFindHydratableContainerInstance =
  $$$hostConfig.didNotFindHydratableContainerInstance;
export const didNotFindHydratableContainerTextInstance =
  $$$hostConfig.didNotFindHydratableContainerTextInstance;
export const didNotFindHydratableInstance =
  $$$hostConfig.didNotFindHydratableInstance;
export const didNotFindHydratableTextInstance =
  $$$hostConfig.didNotFindHydratableTextInstance;

看起来需要实现的API很多,不过实际上我们只需要提供部分必要的实现即可。

shocked meme

在初始化渲染阶段,Reconciler会从hostconfig调用不同的函数。

Initial render

首先我们打算渲染这样一个视图:

const Text = props => {
  return <p className={props.className}>{props.content}</p>
}

const App = () => {
  return (
    <div>
      <Text className="hello-class" content="Hello" />
      <span style="color:blue;">World</span>
    </div>
  )
}

因此我们渲染的视图树大致应该是这样的。

first render tree

现在我们看看之前的报错。

TypeError: getRootHostContext is not a function
const HostConfig = {
  getRootHostContext: function(...args) {
    console.log('getRootHostContext', ...args)
  },
}

我们发现还有报错,继续挨个处理,最后得到如下的hostConfig。

处理这个TypeError: appendAllChildren is not a function报错时候,查到对应一个issue

因为我们没有实现supportsMutation(它应该设置为true),因此一些内部函数没有被分配。

好了,终于没有报错了。

const hostConfig = {
  getRootHostContext: function(...args) {
    console.log('getRootHostContext', ...args);
  },
  getChildHostContext: function(...args) {
    console.log('getChildHostContext', ...args)
  },
  shouldSetTextContent: function(...args) {
    console.log('shouldSetTextContent', ...args)
  },
  prepareForCommit: function(...args) {
    console.log('prepareForCommit', ...args)
  },
  resetAfterCommit: function(...args) {
    console.log('resetAfterCommit', ...args)
  },
  createTextInstance: function(...args) {
    console.log('createTextInstance', ...args)
  },
  createInstance: function(...args) {
    console.log('createInstance', ...args)
  },
  supportsMutation: function(...args) {
    console.log('createInstance', ...args)
    return true;
  },
  appendInitialChild: function(...args) {
    console.log('appendInitialChild', ...args)
  },
  clearContainer: function(...args) {
    console.log('appendInitialChild', ...args)
  },
  finalizeInitialChildren: function(...args) {
    console.log('finalizeInitialChildren', ...args)
  },
  appendChildToContainer: function(...args) {
    console.log('appendChildToContainer', ...args)
  },
};

截屏2021-12-11 下午11.41.40

这里最重要的是什么?当然是观测初始渲染时候执行的顺序:

inital render flow

如果我们想要知道对应每个方法做了什么可以参考阅读the source code of react-dom to 源码。

now

现在now不是必须得了,会有默认兜底方法。

export {
  unstable_now as now,
  unstable_scheduleCallback as scheduleDeferredCallback,
  unstable_cancelCallback as cancelDeferredCallback,
} from 'scheduler';

协调器使用此函数来计算工作的当前时间。在 react-dom 的情况下,它使用 performace.now(如果可用)或降级到 Date.now。

getRootHostContext

我们看到该函数的签名。

function (rootContainerInstance: Container): HostContext {
  let context = {
    // This can contain any data that you want to pass down to immediate child
  }
  return context
}

参数:

  • rootContainerInstance就是我们在render时候指定的根dom节点,这里指的是<div id="root"></div>

返回值:

  • 返回的是一个context对象,这个context会传递给你的子child。

目的:

  • 该函数允许您与此 HostConfig 中的其他函数共享一些上下文。

因此我们修改下demo返回一个空对象。

getRootHostContext: function (rootContainerInstance: Container): HostContext {
  let rootContext = {
    from: 'from rootContext', // test code
  }
  return rootContext
}

截屏2021-12-12 上午12.11.36

getChildHostContext

函数签名如下:

function getChildHostContext(
  parentHostContext: HostContext,
  type: string,
  rootContainerInstance: Container,
): HostContext {
  let context = {
    // This can contain any data that you want to pass down to immediate child
  }
  return context
}

参数:

  • parentHostContext: 来自父级的context对象,比如roothost传递给child的rootContext。
  • type:这里主要指的是fiber类型。比如‘div’, ‘span’, ‘p’, ‘input’ etc.
  • rootContainerInstance:指的是你在调用render时候指定的根dom节点。这里指的是<div id="root"></div>

返回值:

  • 您希望传递给直接子级的上下文对象。

目的:

  • 这个函数提供了一种从父节点访问上下文的方法,也是一种将一些上下文传递给当前节点的直接子节点的方法。上下文基本上是一个包含一些信息的常规对象。

因此这里我们简单修改下我们的方法:

  getChildHostContext: function (parentHostContext, type, rootContainerInstance) {
    console.log('getChildHostContext', parentHostContext, type, rootContainerInstance);
    let context = {
      from: 'from getChildHostContext'
    }
    return context
  },

截屏2021-12-12 上午12.23.22

shouldSetTextContent

这个函数的签名如下:

function shouldSetTextContent(type: string, props: Props): boolean {
  return Boolean
}

参数:

  • type:这里主要指的是一些fiber type,比如‘div’, ‘span’, ‘p’, ‘input’ etc.
  • props: 包括传递给host react element的props属性。

返回值:

  • 返回一个布尔值。

目的:

  • 如果函数返回 true,则文本将在宿主元素内创建,并且不会单独创建新的文本元素。
  • 如果返回 true,则接下来调用的是当前元素的 createInstance方法, 并且遍历将在此节点处停止(不会遍历此元素的子元素)。
  • 如果返回 false,子元素将会继续调用getChildHostContextshouldSetTextContent。它会一直持续到 shouldSetTextContent 返回 true 或者递归到树的最后一个node节点(通常是文本节点)。当它到达最后一个叶子节点时,它将调用 createTextInstance方法。

在 react-dom 的情况下,实现如下

export function shouldSetTextContent(type: string, props: Props): boolean {
  return (
    type === 'textarea' ||
    type === 'option' ||
    type === 'noscript' ||
    typeof props.children === 'string' ||
    typeof props.children === 'number' ||
    (typeof props.dangerouslySetInnerHTML === 'object' &&
      props.dangerouslySetInnerHTML !== null &&
      props.dangerouslySetInnerHTML.__html != null)
  );
}

因此自定义渲染这我们更改下对应的实现:

  shouldSetTextContent: function(type, props) {
    console.log('shouldSetTextContent', type, props)
    return false;
  },

截屏2021-12-12 上午12.46.05

createTextInstance

我们看其函数签名

export function createTextInstance(
  text: string,
  rootContainerInstance: Container,
  hostContext: HostContext,
  internalInstanceHandle: Object,
): TextInstance {
  return textNode
}

参数:

  • text: 指的是需要被渲染使用的文本字符串
  • rootContainerInstance:渲染时候指定的根node节点,<div id="root"></div>.
  • hostContext:指的是包括当前text node的host node的context对象,比如<p>Hello</p>Hello就是text node,它的hostContext指的就是p
  • internalInstanceHandle:指的是text instance对应的fiber node。

返回值:

  • 这应该是一个实际的表示文本视图元素。在 dom(浏览器) 的情况下,它将是一个 textNode。

目的:

  • 这里我们指定渲染器应该如何处理文本内容。

对应我们修改我们的方法:

  createTextInstance: function(text, rootContainerInstance, hostContext, internalInstanceHandle) {
    console.log('createTextInstance', text, rootContainerInstance, hostContext, internalInstanceHandle)
    return document.createTextNode(text);
  },

截屏2021-12-12 上午11.00.12

createInstance

对应函数签名

export function createInstance(
  type: string,
  props: Props,
  rootContainerInstance: Container,
  hostContext: HostContext,
  internalInstanceHandle: Object,
): Instance {
  return domElement
}

参数:

  • type:主要指的是filber type,比如‘div’, ‘span’, ‘p’, ‘input’ etc.
  • props:指的是传递给host react element的props属性
  • rootContainerInstance:指的是渲染时候的root dom node。这里指的是<div id="root"></div>
  • hostContext:指的是当前node的父节点的context对象。它是其父节点getChildHostContext的返回值。
  • internalInstanceHandle:实例对应的fiber node。

返回值:

  • 节点Node实际对应的dom元素

目的:

  • 除了子文本节点之外,所有的Host node都会调用该方法创建实例。所以我们需要根据host type创建正确对应的视图元素。
  • 同时我们在这里处理传递给 host element 的props。比如设置onClickListeners或者样式。

对应我们的实现如下:

  createInstance: function (
    type,
    props,
    rootContainerInstance,
    hostContext,
    internalInstanceHandle
  ) {
    console.log(
      "createInstance",
      type,
      props,
      rootContainerInstance,
      hostContext,
      internalInstanceHandle
    );
    const element = document.createElement(type);
    element.className = props.className || "";
    element.style = props.style;
    // ....
    // ....
    // if (newProps.onClick) {
    //   element.addEventListener('click', newProps.onClick)
    // }
    return element;
  },

截屏2021-12-12 上午11.14.43

appendInitialChild

函数签名:

export function appendInitialChild(
  parentInstance: Instance,
  child: Instance | TextInstance,
): void {
  parentInstance.appendChild(child);
}

参数:

  • parentInstance:遍历中的当前节点
  • child:当前节点的子 dom 节点。

目的:

  • 在这里,我们将在初始渲染阶段将子 dom 节点附加到父节点。将为当前节点的每个子节点调用此方法。

对应修改如下:

  appendInitialChild: function (parentInstance, child) {
    console.log("appendInitialChild", parentInstance, child);
    parentInstance.appendChild(child)
  },

截屏2021-12-12 上午11.19.27

finalizeInitialChildren

函数签名:

export function finalizeInitialChildren(
  domElement: Instance,
  type: string,
  props: Props,
  rootContainerInstance: Container,
  hostContext: HostContext,
): boolean {
  setInitialProperties(domElement, type, props, rootContainerInstance);
  return shouldAutoFocusHostComponent(type, props);
}

参数:

  • domElement:domElement是appendInitialChild 之后的 dom 元素。
  • type:fiber type,‘div’, ‘span’, ‘p’, ‘input’ etc.
  • props:传递给 host react element 的 props。
  • rootContainerInstance:根节点、这里指的是<div id="root"></div>
  • hostContext:它是其父节点getChildHostContext的返回值。

返回值:

  • 布尔:一个布尔值,决定是否需要调用此元素的 commitMount。

目的:

  • 在 react native 渲染器的情况下,这个函数除了返回 false 什么都不做。
  • 在 react-dom 的情况下,这会添加默认的 dom 属性,例如事件侦听器等。为了实现某些input元素的autofocus(autofocus只能在渲染完成后发生),react-dom 发送返回类型为 true

因此我们对应修改为

  finalizeInitialChildren: function (
    domElement,
    type,
    props,
    rootContainerInstance,
    hostContext
  ) {
    console.log(
      "finalizeInitialChildren",
      domElement,
      type,
      props,
      rootContainerInstance,
      hostContext
    );
    return props.autofocus; //simply return true for experimenting
  },

截屏2021-12-12 上午11.34.29

现在所有的子实例都创建完成。Reconciler 会将递归向上移动到该节点的父节点。请记住,当前节点的父节点还没有被实例化。因此,Reconciler将向上调用父级上的 createInstance → appendInitialChild → finalizeInitialChildren。这个循环会一直发生,直到我们到达递归树的顶部。当没有更多元素时,将调用 prepareForCommit。

prepareForCommit

函数签名:

function prepareForCommit(containerInfo: Container): void

参数:

  • containerInfo:根node节点,这里指的是<div id="root"></div>

目的:

  • 当我们完成制作了所有视图的内存渲染树时,将调用此函数(请记住,我们尚未将其添加到实际的根 dom 节点)。在这里,我们可以在添加内存渲染树之前在 containerInfo 上做任何需要做的准备。例如:在 react-dom 的情况下,它会跟踪所有当前聚焦的元素、暂时禁用的事件等。

截屏2021-12-12 上午11.45.59

在 prepareForCommit 之后,reconciler 会将内存树提交到 rootHost,然后浏览器将触发重绘。

resetAfterCommit

函数签名:

export function resetAfterCommit(containerInfo: Container): void {
  ReactInputSelection.restoreSelection(selectionInformation);
  selectionInformation = null;
  ReactBrowserEventEmitter.setEnabled(eventsEnabled);
  eventsEnabled = null;
}

参数:

  • rootContainerInstance: 渲染挂载的根节点,<div id="root"></div>

目的:

  • 在将内存树附加到根 dom 元素后,将执行此函数。在这里,我们可以执行任何需要完成的 post attach 操作。例如:react-dom 重新启用在 prepareForCommit 中暂时禁用的事件并重新聚焦元素等。

因此我们修改如下:

  resetAfterCommit: function (containerInfo) {
    console.log("resetAfterCommit", containerInfo);
  },

现在在此为止,我们希望我们的文档被渲染,但事实并没有。问题是我们没有通过代码告诉如何将我们的内存树附加到root div。答案是 appendChildToContainer方法。

appendChildToContainer

函数签名:

export function appendChildToContainer(
  container: DOMContainer,
  child: Instance | TextInstance,
): void {}

参数:

  • container:root node 或者 其他容器元素。
  • child:子 dom 节点树或内存树。

目的:

  • 在这里,我们将我们的内存树挂载到root div。但是这个函数只有在我们设置了 supportsMutation:true 时才有效。

我们对应修改如下:

  appendChildToContainer: function (container, child) {
    console.log("appendChildToContainer", container, child);
    container.appendChild(child);
  },

截屏2021-12-12 上午11.56.33

截屏2021-12-12 上午11.56.49

😄,我们实现了一个迷你版本的React。

hell_yeahhell yeah

让我们在完成之前继续实现commitMount。

commitMount

函数签名:

export function commitMount(
  domElement: Instance,
  type: string,
  newProps: Props,
  internalInstanceHandle: Object,
): void {}

参数:

  • domElement:再次渲染的dom element
  • type:fiber type,‘div’, ‘span’, ‘p’, ‘input’ etc
  • newProps:传递给host react element 的 props
  • internalInstanceHandle:该element对应的fiber node。

目的:

  • 对于将 finalizeInitialChildren 的返回值设置为 true 的每个元素,都会调用此函数。在所有步骤完成后(即在 resetAfterCommit 之后)调用此方法,这意味着整个树已附加到 dom。该方法在react-dom主要用于实现自动对焦。这个方法只存在于 react-dom 中,不存在于 react-native 中。

对应我们的修改

  commitMount: (domElement, type, newProps, fiberNode) => {
    domElement.focus();
    console.log("commitMount", domElement, type, newProps, fiberNode);
 },

到目前为止,hostConfig如下:

const hostConfig = {
  getRootHostContext: function (nextRootInstance) {
    console.log("getRootHostContext", nextRootInstance);
    let rootContext = {
      from: "from rootContext",
    };
    return rootContext;
  },
  getChildHostContext: function (
    parentHostContext,
    type,
    rootContainerInstance
  ) {
    console.log(
      "getChildHostContext",
      parentHostContext,
      type,
      rootContainerInstance
    );
    let context = {
      from: "from getChildHostContext",
    };
    return context;
  },
  shouldSetTextContent: function (type, props) {
    console.log("shouldSetTextContent", type, props);
    return false;
  },
  prepareForCommit: function (containerInfo) {
    console.log("prepareForCommit", containerInfo);
  },
  resetAfterCommit: function (containerInfo) {
    console.log("resetAfterCommit", containerInfo);
  },
  commitMount: (domElement, type, newProps, fiberNode) => {
    domElement.focus();
    console.log("commitMount", domElement, type, newProps, fiberNode);
 },
  createTextInstance: function (
    text,
    rootContainerInstance,
    hostContext,
    internalInstanceHandle
  ) {
    console.log(
      "createTextInstance",
      text,
      rootContainerInstance,
      hostContext,
      internalInstanceHandle
    );
    return document.createTextNode(text);
  },
  createInstance: function (
    type,
    props,
    rootContainerInstance,
    hostContext,
    internalInstanceHandle
  ) {
    console.log(
      "createInstance",
      type,
      props,
      rootContainerInstance,
      hostContext,
      internalInstanceHandle
    );
    const element = document.createElement(type);
    element.className = props.className || "";
    element.style = props.style;
    // ....
    // ....
    // if (newProps.onClick) {
    //   element.addEventListener('click', newProps.onClick)
    // }
    return element;
  },
  supportsMutation: function (...args) {
    console.log("createInstance", ...args);
    return true;
  },
  appendInitialChild: function (parentInstance, child) {
    console.log("appendInitialChild", parentInstance, child);
    parentInstance.appendChild(child);
  },
  clearContainer: function (...args) {
    console.log("clearContainer", ...args);
  },
  finalizeInitialChildren: function (
    domElement,
    type,
    props,
    rootContainerInstance,
    hostContext
  ) {
    console.log(
      "finalizeInitialChildren",
      domElement,
      type,
      props,
      rootContainerInstance,
      hostContext
    );
    return props.autofocus; //simply return true for experimenting
  },
  appendChildToContainer: function (container, child) {
    console.log("appendChildToContainer", container, child);
    container.appendChild(child);
  },
};

现在我们已经完成微小版本的渲染器。

like a boss

现在我们已经完成静态的渲染,现在让我们向我们的应用程序添加状态,并使用一个按钮来改变点击状态,看看会发生什么。

updated

我们修改src/index.js

import React from 'react'
// import ReactDOM from "react-dom";
import CustomRenderer from './renderer'

const Text = props => {
  return <p className={props.className}>{props.content}</p>
}

class App extends React.Component {
  state = {
    text: Date.now(),
  }
  onButtonClick = () => {
    this.setState(() => ({ text: Date.now() }))
  }
  render() {
    return (
      <div>
        <Text className="hello-class" content={this.state.text} />
        <span style="color:blue;" autofocus>
          World
        </span>
        <button onClick={this.onButtonClick}>Get current time</button>
      </div>
    )
  }
}

// ReactDOM.render(<App />, document.getElementById("root"));
CustomRenderer.render(<App />, document.getElementById('root'))

看下界面如下:

added a new button

当您尝试单击按钮时,我们看到没有任何反应。发生这种情况是因为我们的 onClick 侦听器没有被调用。

发生这种情况的原因是我们的渲染器不知道如何处理按钮上的 onClick 属性。让我们将该功能添加到我们的 hostConfig。

  createInstance: function (
    type,
    props,
    rootContainerInstance,
    hostContext,
    internalInstanceHandle
  ) {
    console.log(
      "createInstance",
      type,
      props,
      rootContainerInstance,
      hostContext,
      internalInstanceHandle
    );
    const element = document.createElement(type);
    element.className = props.className || "";
    element.style = props.style;
    // ....
    // ....
    if (props.onClick) {
      element.addEventListener('click', props.onClick)
    }
    return element;
  },

但是当我们点击时候,发现报错了。

截屏2021-12-12 下午1.04.40

我们继续维护我们的hostConfig。

prepareUpdate

函数签名:

export function prepareUpdate(
  domElement: Instance,
  type: string,
  oldProps: Props,
  newProps: Props,
  rootContainerInstance: Container,
  hostContext: HostContext,
): null | Array<mixed> {
  return {
    /* update payload */
  }
}

参数:

  • domElement: 当前node的dom实例
  • type:fiber type,‘div’, ‘span’, ‘p’, ‘input’ etc.
  • oldProps:更新之前的props
  • newProps:新的props
  • rootContainerInstance: root根节点, <div id="root"></div>
  • hostContext:这是父节点的 getChildHostContext 的返回值。

返回值:

  • 这个函数应该返回一个payload object。 Payload 是一个 Javascript 对象,它可以包含有关此宿主元素上需要更改的内容的信息。

  • 如果此函数不返回任何内容,则协调器会根据其算法决定是否应在此节点上执行更新。

  • 这个想法是我们不会在这个函数中执行任何 dom 更改。 Dom 更改应该只在渲染器的提交阶段完成。一旦完成prepareUpdate 的树遍历,就会在rootContainerInstance 上调用prepareForCommit 方法,然后在我们从prepareUpdate 返回updatePayload 的每个节点上调用commitUpdate。

commitUpdate

函数签名:

export function commitUpdate(
  domElement: Instance,
  updatePayload: Array<mixed>,
  type: string,
  oldProps: Props,
  newProps: Props,
  internalInstanceHandle: Object,
): void {
  return {
    /* update payload */
  }
}

如果需要,我们应该在这里完成我们所有的 dom 操作工作。

commitTextUpdate

函数签名

export function commitTextUpdate(
  textInstance: TextInstance,
  oldText: string,
  newText: string,
): void {
  textInstance.nodeValue = newText;
}

在这里我们对 textNode 执行实际的 dom 更新。

commitTextUpdate: function(textInstance, oldText, newText) {
    textInstance.nodeValue = newText;
}

现在,让我们运行我们的应用程序,看看会发生什么。单击“获取当前时间”按钮👊🏽。我们的文本字段现在应该更新为状态中的最新值。 🥳🥳🥳

截屏2021-12-12 下午1.23.51

执行更新的顺序

如果您在 React Dom 源代码中看到 hostConfig 中所有函数的列表,您应该会看到许多尚未涵盖但似乎与更新功能有些相关的函数。在尝试了很多demo之后,这是作者想出的执行顺序。

update flow

update_flow_draw_io

这涵盖了首次渲染和后续更新期间渲染器的所有基本方法。

Methods used for edge cases (during Commit Phase)

现在如果你看一下hostConfig,你会发现还有一些方法没有涉及到。如果您更多地使用渲染器,您会发现其中一些方法将在某些边缘情况下被触发。

appendChild

函数签名:

export function appendChild(
  parentInstance: Instance,
  child: Instance | TextInstance,
): void {
  parentInstance.appendChild(child);
}

目的:

  • 每当需要在最后将新元素插入到父元素中时,都会调用此函数。例如:
<div>
  <p>test</p>
  {this.state.test === "yolo" && <button>Hello</button>}
</div>

所以这里当 state.test 变成 yolo 时。此函数将在提交阶段使用 parentInstance = div 和 child = button 调用。

因为我们添加如下:

appendChild: function(parentInstance, child) {
    parentInstance.appendChild(child);
}

insertBefore

函数签名:

export function insertBefore(
  parentInstance: Instance,
  child: Instance | TextInstance,
  beforeChild: Instance | TextInstance,
): void {
  parentInstance.insertBefore(child, beforeChild);
}

每当需要在父元素内的子元素之前插入新元素时,都会调用此函数。例如:

<div>
  <p>test</p>
  {this.state.test === "yolo" && <button>Hello</button>}
  <p>test2</p>
</div>

所以这里当 state.test 变成 yolo 时。在提交阶段,将使用 parentInstance = div, beforeChild = p(test2) , child = button 调用此函数。

因此我们添加如下:

insertBefore: (parentInstance, child, beforeChild) => {
  parentInstance.insertBefore(child, beforeChild)
}

removeChild

函数签名:

export function removeChild(
  parentInstance: Instance,
  child: Instance | TextInstance,
): void {
  parentInstance.removeChild(child);
}

每当需要从父元素中删除元素时,都会调用此函数。例如:

<div>{this.state.test === "yolo" && <button>Hello</button>}</div>

所以当 state.test 变成 yolo 以外的东西时。此函数将在提交阶段使用 parentInstance = div 和 child = button 调用。

因此我们添加如下:

removeChild: function(parentInstance, child) {
 parentInstance.removeChild(child);
}

insertInContainerBefore

函数签名:

export function insertInContainerBefore(
  container: Container,
  child: Instance | TextInstance,
  beforeChild: Instance | TextInstance,
): void {
  if (container.nodeType === COMMENT_NODE) {
    (container.parentNode: any).insertBefore(child, beforeChild);
  } else {
    container.insertBefore(child, beforeChild);
  }
}

每当元素需要插入到最顶层组件(根组件)本身之前,就会调用此函数。例如:

const App = () => (
  <>
    {this.state.test === 'yolo' && <button>Hello</button>}
    <div> World</div>
  </>
)

所以这里当 state.test 变成 yolo 时。在提交阶段,将使用 container = root#div 和 child = div(World) 和 beforeChild = button 调用此函数。

因此我们添加如下:

insertInContainerBefore: function(container, child, beforeChild) {
  container.insertBefore(child, beforeChild);
}

removeChildFromContainer

函数签名:

export function removeChildFromContainer(
  container: Container,
  child: Instance | TextInstance,
): void {
  if (container.nodeType === COMMENT_NODE) {
    (container.parentNode: any).removeChild(child);
  } else {
    container.removeChild(child);
  }
}

每当元素出现在顶层节点并且需要删除时,就会调用此函数。例如:

const App = () => (
  <>
    {this.state.test === 'yolo' && <button>Hello</button>}
    <div> World</div>
  </>
)

所以这里当 state.test 变成 NOT yolo 时。该函数将在提交阶段使用 container = root#div 和 child=button 调用。

因此我们添加如下:

removeChildFromContainer: function(container, child) {
  container.removeChild(child);
}

resetTextContent

函数签名:

export function resetTextContent(domElement: Instance): void {
  setTextContent(domElement, '');
}

它在 react-dom 中用于重置 dom 元素的文本内容。

我们这里暂时不做任何处理。

resetTextContent: function(domElement) {

}

Extra Methods

这些包含我能够找出的 hostConfig 中的其余方法。如果阅读此博客的人能帮助我在下面的评论部分中弄清楚 hostConfig 中的其余方法的作用,我将不胜感激。然后我会在这里添加它们。

shouldDeprioritizeSubtree

函数签名:

export function shouldDeprioritizeSubtree(type: string, props: Props): boolean {
  return !!props.hidden;
}

此函数用于降低某些子树的渲染优先级。主要用于子树隐藏或屏幕外的情况。在 react-dom 代码库中,此函数如上所述。

因此我们修改如下:

shouldDeprioritizeSubtree: function(type, nextProps) {
  return !!nextProps.hidden
}

Final hostConfig

我们最终的配置如下:

const hostConfig = {
  getRootHostContext: function (nextRootInstance) {
    console.log("getRootHostContext", nextRootInstance);
    let rootContext = {
      from: "from rootContext",
    };
    return rootContext;
  },
  getChildHostContext: function (
    parentHostContext,
    type,
    rootContainerInstance
  ) {
    console.log(
      "getChildHostContext",
      parentHostContext,
      type,
      rootContainerInstance
    );
    let context = {
      from: "from getChildHostContext",
    };
    return context;
  },
  shouldSetTextContent: function (type, props) {
    console.log("shouldSetTextContent", type, props);
    return false;
  },
  prepareForCommit: function (containerInfo) {
    console.log("prepareForCommit", containerInfo);
  },
  resetAfterCommit: function (containerInfo) {
    console.log("resetAfterCommit", containerInfo);
  },
  commitMount: (domElement, type, newProps, fiberNode) => {
    domElement.focus();
    console.log("commitMount", domElement, type, newProps, fiberNode);
  },
  createTextInstance: function (
    text,
    rootContainerInstance,
    hostContext,
    internalInstanceHandle
  ) {
    console.log(
      "createTextInstance",
      text,
      rootContainerInstance,
      hostContext,
      internalInstanceHandle
    );
    return document.createTextNode(text);
  },
  createInstance: function (
    type,
    props,
    rootContainerInstance,
    hostContext,
    internalInstanceHandle
  ) {
    console.log(
      "createInstance",
      type,
      props,
      rootContainerInstance,
      hostContext,
      internalInstanceHandle
    );
    const element = document.createElement(type);
    element.className = props.className || "";
    element.style = props.style;
    // ....
    // ....
    if (props.onClick) {
      element.addEventListener("click", props.onClick);
    }
    return element;
  },
  supportsMutation: function (...args) {
    console.log("createInstance", ...args);
    return true;
  },
  appendInitialChild: function (parentInstance, child) {
    console.log("appendInitialChild", parentInstance, child);
    parentInstance.appendChild(child);
  },
  clearContainer: function (...args) {
    console.log("clearContainer", ...args);
  },
  appendChild: function (parentInstance, child) {
    parentInstance.appendChild(child);
    console.log("appendChild", parentInstance, child);
  },
  insertBefore: (parentInstance, child, beforeChild) => {
    parentInstance.insertBefore(child, beforeChild);
    console.log("insertBefore", parentInstance, child, beforeChild);
  },
  insertInContainerBefore: function(container, child, beforeChild) {
    container.insertBefore(child, beforeChild);
    console.log("insertInContainerBefore", container, child, beforeChild);
  },
  removeChildFromContainer: function(container, child) {
    container.removeChild(child);
    console.log("removeChildFromContainer", container, child);
  },
  resetTextContent: function(domElement) {
    console.log("resetTextContent", domElement);
  },
  shouldDeprioritizeSubtree: function(type, nextProps) {
    console.log("shouldDeprioritizeSubtree", type, nextProps);
    return !!nextProps.hidden
  },
  removeChild: function (parentInstance, child) {
    parentInstance.removeChild(child);
    console.log("removeChild", parentInstance, child);
  },
  finalizeInitialChildren: function (
    domElement,
    type,
    props,
    rootContainerInstance,
    hostContext
  ) {
    console.log(
      "finalizeInitialChildren",
      domElement,
      type,
      props,
      rootContainerInstance,
      hostContext
    );
    return props.autofocus; //simply return true for experimenting
  },
  appendChildToContainer: function (container, child) {
    console.log("appendChildToContainer", container, child);
    container.appendChild(child);
  },
  prepareUpdate: function (
    domElement,
    type,
    oldProps,
    newProps,
    rootContainerInstance,
    hostContext
  ) {
    console.log(
      "prepareUpdate",
      domElement,
      type,
      oldProps,
      newProps,
      rootContainerInstance,
      hostContext
    );
    return;
  },
  commitUpdate: function (
    instance,
    updatePayload,
    type,
    oldProps,
    newProps,
    finishedWork
  ) {
    console.log(
      "commitUpdate",
      instance,
      updatePayload,
      type,
      oldProps,
      newProps,
      finishedWork
    );
    return; //return nothing.
  },
  commitTextUpdate: function (textInstance, oldText, newText) {
    textInstance.nodeValue = newText;
    console.log("commitTextUpdate", textInstance, oldText, newText);
  },
};

现在有很多的方法,我仍然不确定它们的用途。我在下面制作了一个状态跟踪器,用于跟踪我迄今为止所知道的信息。当我找到更多细节时,我会继续更新它。

------
LEGEND
------
 - Means I figured what these methods do.
🔔 - Have some idea but not completely sure. Need help with these.
 - No freakin idea what these do. Need help with these.


$$$hostConfig.getPublicInstance; - 
$$$hostConfig.getRootHostContext; - 
$$$hostConfig.getChildHostContext; - 
$$$hostConfig.prepareForCommit; - 
$$$hostConfig.resetAfterCommit; - 
$$$hostConfig.createInstance; - 
$$$hostConfig.appendInitialChild; - 
$$$hostConfig.finalizeInitialChildren; - 
$$$hostConfig.prepareUpdate; - 
$$$hostConfig.shouldSetTextContent; - 
$$$hostConfig.shouldDeprioritizeSubtree; - 
$$$hostConfig.createTextInstance; - 
$$$hostConfig.scheduleDeferredCallback; - 
$$$hostConfig.cancelDeferredCallback; - 
$$$hostConfig.setTimeout; - 🔔 React Suspense stuff: Provide an implementation of setTimeout here to help in pause execution
$$$hostConfig.clearTimeout; - 🔔 React Suspense stuff: Provide an implementation of clearTimeout
$$$hostConfig.noTimeout; - 🔔 React Suspense stuff: Usually set it to -1. But can be any ID that setTimeout doesnt provide. So that it can be used to check if timeout handler is present or not
$$$hostConfig.now; - 
$$$hostConfig.isPrimaryRenderer; - 🔔 Set this to true. This is primarily used in codebase to manage context if there are more than one renderers I think. This is the hunch I got after reading the codebase.
$$$hostConfig.supportsMutation; - 
$$$hostConfig.supportsPersistence; - 🔔❌ set this to false. Current react-dom doesnt support it yet aswell.
$$$hostConfig.supportsHydration; - 🔔❌ set this to false. Enable if you can support hydration. More on hydration here: https://reactjs.org/docs/react-dom.html#hydrate
-------------------
     Mutation
    (optional)
-------------------
$$$hostConfig.appendChild; - 
$$$hostConfig.appendChildToContainer; - 
$$$hostConfig.commitTextUpdate; - 
$$$hostConfig.commitMount; - 
$$$hostConfig.commitUpdate; - 
$$$hostConfig.insertBefore; - 
$$$hostConfig.insertInContainerBefore; - 
$$$hostConfig.removeChild; - 
$$$hostConfig.removeChildFromContainer; - 
$$$hostConfig.resetTextContent; - 🔔
$$$hostConfig.cloneInstance; - 🔔❌ This will be used for persistence
$$$hostConfig.createContainerChildSet; - 🔔❌ This will be used for persistence
$$$hostConfig.appendChildToContainerChildSet; - 🔔❌ This will be used for persistence
$$$hostConfig.finalizeContainerChildren; - 🔔❌ This will be used for persistence
$$$hostConfig.replaceContainerChildren; - 🔔❌ This will be used for persistence
-------------------
    Hydration
    (optional)
-------------------
$$$hostConfig.canHydrateInstance; - 🔔❌ This will be used for hydration
$$$hostConfig.canHydrateTextInstance; - 🔔❌ This will be used for hydration
$$$hostConfig.getNextHydratableSibling; - 🔔❌ This will be used for hydration
$$$hostConfig.getFirstHydratableChild; - 🔔❌ This will be used for hydration
$$$hostConfig.hydrateInstance; - 🔔❌ This will be used for hydration
$$$hostConfig.hydrateTextInstance; - 🔔❌ This will be used for hydration
$$$hostConfig.didNotMatchHydratedContainerTextInstance; - 🔔❌ This will be used for hydration
$$$hostConfig.didNotMatchHydratedTextInstance; - 🔔❌ This will be used for hydration
$$$hostConfig.didNotHydrateContainerInstance; - 🔔❌ This will be used for hydration
$$$hostConfig.didNotHydrateInstance; - 🔔❌ This will be used for hydration
$$$hostConfig.didNotFindHydratableContainerInstance; - 🔔❌ This will be used for hydration
$$$hostConfig.didNotFindHydratableContainerTextInstance; - 🔔❌ This will be used for hydration
$$$hostConfig.didNotFindHydratableInstance; - 🔔❌ This will be used for hydration
$$$hostConfig.didNotFindHydratableTextInstance; - 🔔❌ This will be used for hydration

参考资料

Releases

No releases published

Packages

No packages published