skip to content
OnionTalk

Deep In React (三)Internal Instance

在上一篇文章中我们谈到了React的递归式渲染这个名词,那么一个React element经过了怎样的变化最后映射成了对应的DOM结构,Reacte element又是怎样在DOM树中挂载/卸载的?这篇文章会带你一探究竟。

从一次挂载说起

如果你写过 React 代码,那么ReactDOM.render(<App />, container) 可能是你最熟悉的一段代码了。<App />是一个 React Element,container 是一个 node 节点。这个方法是把 React element 映射到真正的 DOM 结构的一个触发器,当你调用这个方法之后,会把 React Element 渲染成 virtual DOM Tree。

首先有必要提一下 Element 和 virtual DOM Tree 这两个概念。

React Element 可能会存在 type 为 Component Element 的节点。而 Virtual DOM Tree 指代完全映射为真实 DOM 结构的树,所有节点的 type 都是 string 类型。

{
// React Element Tree
 type: App,
 props: {
   children:[{
     type: 'div',
     ...
   }, {
     type: Button,
     ...
   }]
 }
}

// 最终渲染成的virtual DOM Tree
{
// Virtual DOM Tree
 type: 'div',
 props: {
   children:[{
     type: 'div',
     ...
   }, {
     type: 'button',
     ...
   }]
 }
}
}

渲染是怎么发生的?

React 在接受<App />这个 Element 的时候,其实是不知道这个 Element 的深度是多少,每个节点对应的 DOM 元素是什么的,<App />这个 Element 下,可能只是一个简单的<div />,也可能是许多复杂组件的组合。因此,React 需要自顶向下进行递归的渲染,最终得到一个对应到真实 DOM 结构的 Virtual DOM 树。

// 伪代码 并不会工作
function mount(element) {
  let renderedElement = element.render();
  return mount(renderedElement);
}

在上文中,我们谈到过 React 中 React 中有两种常见的 component,一种是 class,一种是 function。二者在 React Element 中有着不同的表现。不同之处在于,class 在 type 上对应的是这个构造的构造函数,而 function 对应的是组件的渲染函数。

考虑这两种不同的 component,渲染逻辑会有以下改变。

function mount(element) {
  let type = element.type;
  let props = element.props;
  let renderedElement;
  if (isClass(element)) {
    let publicInstance = new type(props);
    // componentWillMount 生命周期函数
    if (publicInstance.componentWillMount) {
      publicInstance.componentWillMount();
    }
    renderedElement = publicInstance.render(props);
  } else {
    renderedElement = type(props);
  }
  return mount(renderedElement);
}

mount 这个递归函数大致的描述了 React 整个的渲染流程。但是作为一个递归,上面这个函数缺少了一个完备的递归最重要的一个印子,即结束条件。而在 React 中,递归的结束条件就是将 React Element Tree 渲染成对应的 Native View tree。

渲染 Host Component

前一篇文章中也提到过,Host Component 对应的就是不同 Platform 的基本元素。比如 ReactDOM 对应的就是 DOM Element,ReactNative 对应的就是对应的 native view。React Element Tree Parse 最终会把所有的 element parse 成对应的 Host Component tree。

以下以 ReactDOM 为例。 我们想要挂载一个 Host Component 时,实际上是生成了一个对应平台的基本元素。

// basic example
function mountHost(element) {
  let type = element.type;
  let props = element.props;
  // type 是一个string
  let node = document.createElement(type);
  Object.keys(props).forEach((propName) => node.setAttribute(propName, props[propName]));
  return node;
}

上面是一个最简单的例子,在上面的例子中,你的 Host Component 不会有任何的 children。加上 children 之后,实现会发生一些改变。

// basic example
function mountHost(element) {
  let type = element.type;
  let props = element.props;
  // type 是一个string
  let children = props.children || [];
  let node = document.createElement(type);
  Object.keys(props).forEach((propName) => {
    // children 不是 attribute
    if (propName !== 'children') {
      node.setAttribute(propName, props[propName]);
    }
  });

  // render children

  children.forEach((childElement) => {
    let childNode = mountHost(childElement);
    node.appendChild(childNode);
  });

  return node;
}

上面这种写法能正确渲染这种元素

<div>
  <header />
  <main />
  <footer />
</div>

但是如果 children 中有非 Host Component,上面这种写法就无法工作了。

<div>
  <Sidebar />
  <Container />
  <Footer />
</div>

渲染 Composite Component

为了完成 Children 中的 Composite Component 渲染,我们需要了解一下 Composite Component 的渲染。

Composite Component 其实就是我们之前提到过的 Component Element(以下统称 Composite Component)。对于这种 Element 的渲染,其实可以参考我们最初创建的 mount 函数。二者唯一的区别就是,我们会递归调用经过组合后的 mount 函数。

function mountComposite(element) {
  let type = element.type;
  let props = element.props;
  let renderedElement;
  if (isClass(element)) {
    let publicInstance = new type(props);
    // componentWillMount 生命周期函数
    if (publicInstance.componentWillMount) {
      publicInstance.componentWillMount();
    }
    renderedElement = publicInstance.render(props);
  } else {
    renderedElement = type(props);
  }
  // 组合版mount函数
  return mount(renderedElement);
}

// 组合mountHost和mountComposite
function mount(element) {
  if (typeof element.type === 'string') {
    mountHost(element);
  } else if (element.type === 'function') {
    mountComposite(element);
  }
}

有了这个 mount 函数之后,我们便能很好的处理我们上面的 children 的挂载了。

function mountHost(element) {
  let type = element.type;
  let props = element.props;
  // type 是一个string
  let children = props.children || [];
  let node = document.createElement(type);
  Object.keys(props).forEach((propName) => {
    // children 不是 attribute
    if (propName !== 'children') {
      node.setAttribute(propName, props[propName]);
    }
  });

  // render children
  children.forEach((childElement) => {
    let childNode = mount(childElement);
    node.appendChild(childNode);
  });

  return node;
}

至此,我们的 Element Tree 已经完全渲染成对应的 DOM tree 了。真正的挂载到 DOM 上就是简单的调用 JavaScript。

function mountTree(element, container) {
  let node = mount(element);
  let rootNode = container.firstChild;
  container.rootElement.appendChild(node);
}

var container = document.querySelector('#container');
mountTree(<App />, container);

以上其实就是ReactDOM.render函数调用后发生的所有事情的简化版(现实中的 React 要比这复杂得多)。

如何高效更新

有了这个 mount 函数,我们已经有了一种机制去很好的更新我们的 DOM 了。最暴力的方法就是每次元素有更新的时候我们去重复一下上面的操作,刷新我们的组件。但是这无疑是一种低效的行为,我们更想要的是尽可能的 reuse 已经存在的 DOM,更新只需要更新的节点。

那么,当某个节点的 props 发生更新时,我们怎么才能知道是这个节点需要更新呢?看上去我们需要保存一些必要的信息,来帮助我们维护 React Element Tree 和真实 DOM 之间的映射关系。

而 class 就是一种很好的能维持这些数据的抽象。

Host Component(以 DOM 为例) 和 Composite Component

藉由上面的 mountHost 和 mountComposite 两个函数,我们可以简单的抽象出 Host Component 和 Composite Component 这两个 class。

//我们将这两个component的实例化交由一个工厂去完成
function instantiateComponent(element) {
  const type = element.type;
  if (typeof type === 'function') {
    return new CompositeComponent(element);
  } else if (typeof type === 'string') {
    return new HostComponent(element);
  }
}

class CompositeComponent {
  constructor(element) {
    // 保存必要的信息以供后面使用
    this.currentElement = element;
    this.renderedComponent = null;
    this.publicInstance = null;
  }

  getPublicInstance() {
    return this.publicInstance;
  }

  mount() {
    const type = this.currentElement.type;
    const props = this.currentElement.props;
    let renderedElement;
    let publicInstance;
    if (isClass(type)) {
      publicInstance = new type(props);
      // componentWillMount 生命周期函数
      if (publicInstance.componentWillMount) {
        publicInstance.componentWillMount();
      }
      renderedElement = publicInstance.render(props);
    } else if (typeof type === 'function') {
      publicInstance = null;
      renderedElement = type(props);
    }
    this.publicInstance = publicInstance;
    this.renderedComponent = instantiateComponent(renderedElement);
    return this.renderedComponent.mount();
  }
}

//HostComponent
class HostComponent {
  constructor(element) {
    // 保存必要的信息以供后面使用
    this.currentElement = element;
    this.renderedChildren = [];
    this.node = null;
  }

  getPublicInstance() {
    return this.node;
  }

  mount() {
    let type = this.currentElement.type;
    let props = this.currentElement.props;
    let children = props.children || [];
    let node = document.createElement(type);
    this.node = node;
    Object.keys(props).forEach((propName) => {
      // children 不是 attribute
      if (propName !== 'children') {
        node.setAttribute(propName, props[propName]);
      }
    });

    // render children
    this.renderedChildren = children.map(instantiateComponent);
    const childrenNodes = this.renderedChildren.map((childComponent) => childComponent.mount());
    childrenNodes.forEach((childNode) => node.appendChild(childNode));
    return node;
  }
}

在这里,我们抽象了 HostComponent 和 Composite Component 这两个对象,而这两个对象就是 React 术语中提及到的 Internal Instance。

Internal Instance 的意思是,

  1. 这两种抽象由 React 内部维护,使用 React 框架的人并不需要关心。
  2. Internal Instance 也不像我们之前谈到过的组件(比如一个 Button 组件),无法由使用者自行创造。
  3. Internal Instance 其中包含的信息也仅供 React 内部消费。

如果你想了解更加直观的看到 Internal Instance,可以使用 React Dev Tools

{{

}}

紫色部分就是CompositeComponent,灰色部分就是HostComponent

使用 Internal Instance — 卸载(unmount)组件

上面我们谈到了在 Internal Instance 中维护了三个数据,分别是

在 CompositeComponent 中,

  1. currentElement 当前 CompositeComponent 所对应的 React Element
  2. publicInstance 当前 CompositeComponent 绑定的 React Element 实例
  3. renderedComponent 当前 CompositeComponent 所对应的渲染过的 Internal instance

在 HostComponent 中,

  1. currentElement 同上
  2. node 和当前 HostComponent 绑定的 DOM node
  3. renderedChildren 当前 HostComponent 所对应渲染过的 Internal Instance 组

那么,这些保存的信息到底有什么作用呢?让我们来看一个卸载的例子。

Internal Instance unmount

对于CompositeComponent,在 unmount 时我们还需要进行生命周期函数的调用。

class CompositeComponent {
  // ...

  unmount() {
    if (this.publicInstance) {
      if (this.publicInstance.componentWillUnMount) {
        this.publicInstance.componentWillUnMount();
      }
    }
    this.renderedComponent.unmount();
  }
}

对于HostElement,在 unmount 时做的事就要简单许多

class HostComponent {
  // ...

  unmount() {
    this.renderedChildren.forEach((renderedChild) => rederedChild.unmount());
  }
}

unmount tree

对于已经挂载到 DOM 节点上的 React Element,卸载的操作其实就是递归调用所有componentWillUnMount生命周期函数,然后让节点的 innerHTML 置空。

function unmountTree(containerNode) {
  const rootNode = containerNode.firstChild;
  // 不会工作,因为我们还没有在container node上存储过这个值
  const internalInstance = rootNode.internalInstance;
  internalInstance.unmount();
  rootNode.innerHTML = '';
}

更新 mountTree 函数

在上面的代码中我们发现我们在mountTree的时候需要存储rootNodeinternalInstance,以供卸载时使用。

function mountTree(element, containerNode) {
  const rootComponent = instantiateComponent(element);
  const node = rootComponent.mount();
  containerNode.appendChild(node);

  containerNode.internalInstance = rootComponent;

  // 模拟ReactDOM.render() 一样的返回
  const publicInstance = rootComponent.getPublicInstance();
  return publicInstance;
}

What’s Next

上面组件树卸载的例子对于 Internal Instance 的使用只是非常基本的使用,对于 Internal Instance 中维护的数据,更巧妙的使用是在组件树发生更新时,只更新对应的组件。关于这些细节,我会在下一篇博客和大家分享。

参考资料

https://reactjs.org/docs/implementation-notes.html