加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 百科 > 正文

React学习之相关堆栈调解器的实现(三十七)

发布时间:2020-12-15 07:26:08 所属栏目:百科 来源:网络整理
导读:这一部分讲述的是堆栈调解器的实现 React 的 API 可以被分为三部分, 核心 , 渲染器 , 调解器 ,如果你对代码库可能有点不了解的话,可以看我的博客 其中堆栈调解器是 React 产品中最重要的部分,被 React DOM 和 React Native 渲染器共同使用,它的代码地

这一部分讲述的是堆栈调解器的实现

ReactAPI可以被分为三部分,核心渲染器调解器,如果你对代码库可能有点不了解的话,可以看我的博客

其中堆栈调解器是React产品中最重要的部分,被React DOMReact Native渲染器共同使用,它的代码地址src/renderers/shared/stack/reconciler

1.从零开始构建React的历史

Paul O'Shannessy给予React开发一个非常大的灵感,它对最终的代码库的文档和解说可以得到更好的理解,所以有时间可以去看看。

2.概要

调解器自身并不是一个公共API,而渲染器则作为一个接口,通过用户写的React组件来被React DOMReact Native两个渲染器有效的更新。

3.绑定就是递归

下面是一个绑定组件的实例

ReactDOM.render(<App />,rootEl);

<App/>是一个记录了该如何去渲染的React对象元素,React DOM<App />作为对象传递给调解器,你可以认为是如下这样的对象

console.log(<App />);
// { type: App,props: {} }
  1. 如果App是一个组件类或者函数类,调解器就会检查他们。

  2. 如果App是一个函数,调解器就会调用App(props)去渲染元素。

如果App是一个类,调解器就会通过new App(props)去实例化一个App对象,然后调用componentWillMount()生命周期方法,接着是调用render()方法去渲染元素。

无论是哪一种方法,调解器都能够知道如何去渲染App元素。

这个过程是递归的,App可能渲染出<Greeting/>Greeting可能会渲染出<Button />,接着不断处理,调解器通过知道一个组件如何渲染的来往深度处理用户自定义的组件。

你可以认为这个过程是如下的伪代码:

function isClass(type) {
  // React.Component子类都会有这些标记
  return (
    Boolean(type.prototype) &&
    Boolean(type.prototype.isReactComponent)
  );
}

// 这个函数会处理React元素
// 返回一个代表需要被绑定的树的DOM或者Native节点
function mount(element) {
  var type = element.type;
  var props = element.props;

  // 我们决定要被渲染的元素
  // 它可能是一个函数运行的结果
  // 也可能是一个类实例调用render运行后的结果
  var renderedElement;
  if (isClass(type)) {
    // 类组件
    var publicInstance = new type(props);
    // 设置props
    publicInstance.props = props;
    // 如果有必要调用生命周期方法
    if (publicInstance.componentWillMount) {
      publicInstance.componentWillMount();
    }
    // 调用render得到要被渲染的元素
    renderedElement = publicInstance.render();
  } else {
    // 函数式组件
    renderedElement = type(props);
  }

  // 由于组件可能会返回另外一个组件
  // 所以这个过程是一个递归的过程
  return mount(renderedElement);

  // 注意:这个实现也是不完整的,因为递归依旧没有被停止
  // 它只能处理自定义组合组件,比如<App />或<Button />
  // 而不能处理host组件,比如<div />或<p />
}

var rootEl = document.getElementById('root');
var node = mount(<App />);
rootEl.appendChild(node);

注意

上述的真是一段伪代码,和真实的实现相差太远,直接一看都知道问题,递归无法截止,所以上述只是一个简单的思路,代码需要完善。

我们先总结一下上述代码的几点关键点:

  1. React元素表现为一个对象的话,可以用组件的typeprops来表示,这就是React.createElement的任务了

  2. 自定义的组件(比如App)可以是类或者是函数,他们都会返回需要渲染的元素

  3. 绑定是一个创建DOM或者Native节点树的递归过程

4.绑定Host元素

如果我们不在视图中显示什么的话,那么上述的那些代码都是没有用的,显示出结果是我们的目的。

除了用户自定义的组合组件,React元素还可能是Host组件,比如Buttonrender方法中可能会返回<div/>

如果元素的type是一个字符串,我们就需要按照host元素来处理:

console.log(<div />);
// { type: 'div',props: {} }

当调解器检测到是一个host元素后,它会让渲染器去关心如何绑定它,例如,React DOM渲染器可能就创建一个DOM节点,而其它的渲染器又会创建其它,而这些都不是调解器关心的,这里大家可以留个心思,真是的渲染过程永远都是渲染器的事,跟调解器半毛钱的关系都没有。

如果一个host元素有孩子,调解器就会用同样的方法递归去绑定他们(记住是绑定而不是渲染),孩子是hosthost处理,孩子是组合就按照组合的方式处理。

DOM节点被孩子组件创建出来加入父亲DOM节点中,不断递归的处理,最后完整的DOM结构就组装而成了。

注意

调解器自己并不会依赖DOM,绑定的最终结果取决于渲染器,如果是DOM节点就是React DOM渲染器处理的,如果是字符串则是React DOM Server渲染器处理的,如果是一个代表着Native视图的数字则是React Native渲染器处理的,这些都不是调解器需要关心的。

如果你扩展之前讲的代码处理host元素,那么伪代码就变为了如下形式:

function isClass(type) {
  // React.Component子类都会有这些标记
  return (
    Boolean(type.prototype) &&
    Boolean(type.prototype.isReactComponent)
  );
}

// 这个函数只是用来处理组合组件的
// 比如 <App />和<Button />,不能处理<div />.
function mountComposite(element) {
  var type = element.type;
  var props = element.props;

  var renderedElement;
  if (isClass(type)) {
    // 类组件实例化
    var publicInstance = new type(props);
    // 设置props
    publicInstance.props = props;
    //调用生命周期函数
    if (publicInstance.componentWillMount) {
      publicInstance.componentWillMount();
    }
    renderedElement = publicInstance.render();
  } else if (typeof type === 'function') {
    // 函数式组件
    renderedElement = type(props);
  }

  // 当我们遇到的元素是个Host组件而不是一个组合组件时
  // 这个递归过程就会停止
  return mount(renderedElement);
}
// 这个函数只是用来处理host组件的
// 比如 <div />和<p />,不能处理<App />.

function mountHost(element) {
  var type = element.type;
  var props = element.props;
  var children = props.children || [];
  if (!Array.isArray(children)) {
    children = [children];
  }
  children = children.filter(Boolean);

  // 这一部分代码不应该存在调解器中
  // 因为不同的渲染器初始化节点方式是可能不同的
  // 比如说,React Native会创建IOS或者Android视图
  var node = document.createElement(type);
  Object.keys(props).forEach(propName => {
    if (propName !== 'children') {
      node.setAttribute(propName,props[propName]);
    }
  });

  // 绑定子级
  children.forEach(childElement => {
    // 孩子可能是host或者组合组件
    // 然后就是递归处理他们
    var childNode = mount(childElement);

    // 这部分代码也是特殊的
    // 形式方法的不同取决于不同的渲染器
    node.appendChild(childNode);
  });

  // 返回DOM节点作为绑定的结果
  // 递归过程从此处返回
  return node;
}

function mount(element) {
  var type = element.type;
  if (typeof type === 'function') {
    // 用户自定义组件
    return mountComposite(element);
  } else if (typeof type === 'string') {
    // host组件
    return mountHost(element);
  }
}

var rootEl = document.getElementById('root');
var node = mount(<App />); rootEl.appendChild(node);

这一部分代码里真正的调解器相差依旧是非常遥远的,它连更新功能都没有实现。

5.内部实例

React的一个非常关键的特点就是你可以重复渲染任何东西,这个重复渲染的过程并不会重新创建DOM或者是重设置state,这是非常有意思的,而是不渲染,有趣。

ReactDOM.render(<App />,rootEl);
// 下面的代码并不会有什么性能损失,因为下面的代码相当于没有执行,有趣
ReactDOM.render(<App />,rootEl);

然而,我们之前的代码真是一个最简单的构造初始绑定树的方式了,因为我们在前面的代码没有进行更新阶段的数据储备,所以根本无法实行更新的操作,比如说publicInstances,或者哪个组件对应着哪个DOM节点,这些数据在初始化绑定后统统不知道,这就非常尴尬了。

这个堆栈调解器代码库就通过一个在类中的mount()函数方法来解决它,当然这个方法有一定缺陷,我们会尝试重写调解器,不过,它是怎么工作的呢?

我们不再使用mountHostmountComposite函数,我们用两个类来取代他们:DOMComponentCompositeComponent,因为对象就可以存储数据,函数一般都是纯函数不影响数据储备。

这两个类都有一个接受element为参数的构造函数和一个mount方法去返回被绑定的节点,而全局的mount()函数被如下的代码替换掉了:

function instantiateComponent(element) {
  var type = element.type;
  if (typeof type === 'function') {
    // 用户自定义组件
    return new CompositeComponent(element);
  } else if (typeof type === 'string') {
    // host组件
    return new DOMComponent(element);
  }  
}

首先我们先编写CompositeComponent类的实现:

class CompositeComponent {
  constructor(element) {
    this.currentElement = element;
    this.renderedComponent = null;
    this.publicInstance = null;
  }

  getPublicInstance() {
    // For composite components,expose the class instance.针对组合组件暴露出它的类实例
    return this.publicInstance;
  }

  mount() {
    var element = this.currentElement;
    var type = element.type;
    var props = element.props;

    var publicInstance;
    var renderedElement;
    if (isClass(type)) {
      // 组件类
      publicInstance = new type(props);
      // 设置props
      publicInstance.props = props;
      // 调用生命周期函数
      if (publicInstance.componentWillMount) {
        publicInstance.componentWillMount();
      }
      renderedElement = publicInstance.render();
    } else if (typeof type === 'function') {
      // 函数式组件没有实例
      publicInstance = null;
      renderedElement = type(props);
    }

    // 保存实例
    this.publicInstance = publicInstance;

    // 根据元素实例化孩子内部实例
    // 这个实例可能是DOMComponent
    // 也可能是CompositeComponent
    var renderedComponent = instantiateComponent(renderedElement);
    this.renderedComponent = renderedComponent;

    // 将绑定的数据输出
    return renderedComponent.mount();
  }
}

这个和前面的mountComposite的实现没有多少不同,但是我们保存了一些对我们更新数据有用的信息,比如this.currentElementthis.renderedComponentthis.publicInstance

有一点要注意,我们的CompositeComponent实例和用户自己实例化element.type是不相同的,CompositeComponenet是调解器内部的实现无法被外界使用,或者是没有暴露出来,你并不知道它,而用户自己通过new element.type()是不一样的,你可以直接对他进行处理,换一句话说的是我们在类中实现的getPublicInstance()函数就是让我们得到一个公共操作的实例,但是更加底层内部的实例呢,我们很明显不能操作,也就只能操作当前这一层当做公共接口暴露出来的实例了。

为了避免出现混乱,我将CompositeComponentDOMComponent称为内部实例,他们的存在可以长久的保存着数据,而只有渲染器和调解器能够直接处理他们。

与此相反,我们将用户自定义类的实例称为公共实例(外部实例),这个公共实例你可以直接操作。

mountHost函数被重构成了DOMComponent中的mount函数。

class DOMComponent {
  constructor(element) {
    this.currentElement = element;
    this.renderedChildren = [];
    this.node = null;
  }

  getPublicInstance() {
    return this.node;
  }

  mount() {
    var element = this.currentElement;
    var type = element.type;
    var props = element.props;
    var children = props.children || [];
    if (!Array.isArray(children)) {
      children = [children];
    }

    // 创建并保存节点
    var node = document.createElement(type);
    this.node = node;

    // 设置节点属性
    Object.keys(props).forEach(propName => {
      if (propName !== 'children') {
        node.setAttribute(propName,props[propName]);
      }
    });

    // 创建和保存被包含的孩子
    // 他们可能是DOMComponent或者是CompositeCompoennt
    // 这取决与他们的type是字符串还是function
    var renderedChildren = children.map(instantiateComponent);
    this.renderedChildren = renderedChildren;

    // 收集要绑定的节点
    var childNodes = renderedChildren.map(child => child.mount());
    childNodes.forEach(childNode => node.appendChild(childNode));

    // 将DOM节点作为绑定的结果返回
    return node;
  }
}

这个和之前的代码主要的不同就是我们重构了moutHost()并且保存了当前的noderenderedChildren来关联内部的DOM组件实例,以后我们就可以无需声明就可以使用他们。

总的来说,每一个内部实例,不管是组合也好,host也好,现在都会有指向他们的孩子内部实例的变量保存,从而构成一个内部实例链,为了更加形象的来说明他,如果<App>是一个函数式组件<Button>是一个类组件,而Button又渲染出了<div>那么最后的内部实例链就如同下面所示。

[object CompositeComponent] {
  currentElement: <App />,publicInstance: null,renderedComponent: [object CompositeComponent] {
    currentElement: <Button />,publicInstance: [object Button],
    renderedComponent: [object DOMComponent] {
      currentElement: <div />,node: [object HTMLDivElement],
      renderedChildren: []
    }
  }
}

DOM中你讲只会看到<div>,然而内部实例树既包括组合内部实例又包括host内部实例。

组合内部实例需要保存如下一些东西:

  1. 当前的元素

  2. 如果元素的type是一个类,那么要保存公共实例

  3. 当前的元素不是DOMComponent就是CompositeComponent,我们需要保存他们渲染的内部实例。

host内部实例需要保存的:

  1. 当前的元素

  2. DOM节点

  3. 所有的孩子内部实例,他们可能是DOMComponent也可能是CompositeComponent

你可以想象一个内部实例树怎么去构建一个复杂的应用呢,React DevTools可以给你一个非常直观的结果,它可以用灰色高亮host实例,用紫色来高亮组合实例

然而如果要完成最终的重构,我们还要设计一个函数去进行真实的绑定操作像是ReactDOM.render(),它还会返回一个公共实例,前面的mount得到的只是要进行绑定的节点,而没有进行真实的绑定。

function mountTree(element,containerNode) {
  // 创建顶部内部实例
  var rootComponent = instantiateComponent(element);

  // 将顶部组件加入最终DOM容器中实现真正的绑定
  var node = rootComponent.mount();
  containerNode.appendChild(node);

  // 返回公共实例
  var publicInstance = rootComponent.getPublicInstance();
  return publicInstance;
}

var rootEl = document.getElementById('root');
mountTree(<App />,rootEl);

6.卸载

现在我们已经有了保存着孩子和DOM节点的内部实例,接下来我们就可以实现卸载,对于Composite Component,卸载会递归的调用生命周期函数。

class CompositeComponent {

  // ...

  unmount() {
    // 调用生命周期函数
    var publicInstance = this.publicInstance;
    if (publicInstance) {
      if (publicInstance.componentWillUnmount) {
        publicInstance.componentWillUnmount();
      }
    }

    // 卸载渲染
    var renderedComponent = this.renderedComponent;
    renderedComponent.unmount();
  }
}

对于DOMComponent,需要告诉每一个孩子都要卸载

class DOMComponent {

  // ...

  unmount() {
    // 卸载所有的孩子
    var renderedChildren = this.renderedChildren;
    renderedChildren.forEach(child => child.unmount());
  }
}

实际上,卸载DOM组件也需要移除事件监听器,清除缓冲,不过这些细节我先暂时跳过。

我们现在再增加一个全新的全局函数unmountTree(containerNode),他的功能和ReactDOM.unmountComponentAtNode()类似。与mountTree功能相反

function unmountTree(containerNode) {
  // 从DOM节点中读取一个内部实例
  // 这一个_internalInstance内部实例我们会在mountTree给它增加
  var node = containerNode.firstChild;
  var rootComponent = node._internalInstance;

  // 卸载树并清理容器
  rootComponent.unmount();
  containerNode.innerHTML = '';
}

为了让上述的代码可以正常的工作了,我们需要为DOM节点读取一个root内部实例,我们修改mountTree()增加一个_internalInstance属性为rootDOM节点,我们要告诉mountTree应该摧毁已经存在的树,这样才可以多次调用:

function mountTree(element,containerNode) {
  // 摧毁已经存在的树
  if (containerNode.firstChild) {
    unmountTree(containerNode);
  }

  // 创建一个顶级的内部实例
  var rootComponent = instantiateComponent(element);

  // 将顶级内部实例的渲染结果加入DOM中
  var node = rootComponent.mount();
  containerNode.appendChild(node);

  // 保存内部实例
  node._internalInstance = rootComponent;

  // 返回一个公共实例
  var publicInstance = rootComponent.getPublicInstance();
  return publicInstance;
}

7.更新

在上面,我们实现了卸载,然而如果每一次prop改变都卸载旧的树,构造性的树,React就没有存在的必要了,而调解器的作用就是为了重复使用已经存在实例,从而达到性能的提升。

var rootEl = document.getElementById('root');

mountTree(<App />,rootEl);
// 下面的语句相当于没有执行
mountTree(<App />,rootEl);

我们将扩展我们的内部实例实现一个方法,DOMComponentCompositeComponent都需要实现一个新的函数叫做receive(nextElement)

class CompositeComponent {
  // ...

  receive(nextElement) {
    // ...
  }
}

class DOMComponent {
  // ...

  receive(nextElement) {
    // ...
  }
}

这个函数的工作就是让组件和它的孩子可以及时的了解到nextElement的信息,进行更新。

这一部分在前面被描述为“虚拟DOM diff”,通过我们沿着内部实例树递归往下走,让每一个内部实例都可以接受到更新。

8.更新组合组件

当一个组合组件接受到一个新的元素的时候,我们会运行componeentWillUpdate()生命周期函数,然后会通过新的porps去重渲染组件,得到一个新的渲染元素。

class CompositeComponent {

  // ...

  receive(nextElement) {
    var prevProps = this.currentElement.props;
    var publicInstance = this.publicInstance;
    var prevRenderedComponent = this.renderedComponent;
    var prevRenderedElement = prevRenderedComponent.currentElement;

    // 更新自己的元素
    this.currentElement = nextElement;
    var type = nextElement.type;
    var nextProps = nextElement.props;

    // 得出新render内容
    var nextRenderedElement;
    if (isClass(type)) {

      if (publicInstance.componentWillUpdate) {
        publicInstance.componentWillUpdate(nextProps);
      }
      // 更新props
      publicInstance.props = nextProps;
      // 重新渲染
      nextRenderedElement = publicInstance.render();
    } else if (typeof type === 'function') {
      nextRenderedElement = type(nextProps);
    }

    // ...

得到了nextRenderedElement我们就可以查看渲染的元素的type,跟我之前将更新的时候的判断是一样的,如果type没有改变,那么就向下递归,而不改变当前组件。

比如说,如果render第一次返回了<Button color="red"/>,第二次返回<Button color="blue">,我们就可以告诉相应的内部实例receive新元素。

// ...

    // 如果渲染的元素type没有变化
    // 则重使用已经存在的实例,不去新创建实例
    if (prevRenderedElement.type === nextRenderedElement.type) {
      prevRenderedComponent.receive(nextRenderedElement);
      return;
    }

    // ...

但是,如果新渲染的元素和之前的元素type不一样的话,那么我们就无法进行更新操作了,因为<button>是无法成为<input>、所以,我们不得不卸载摧毁已经存在的内部实例然后装载相应的新的渲染元素:

// ...

    // 得到旧的节点
    var prevNode = prevRenderedComponent.getHostNode();

    // 卸载旧的孩子装载新的孩子
    prevRenderedComponent.unmount();
    var nextRenderedComponent = instantiateComponent(nextRenderedElement);
    var nextNode = nextRenderedComponent.mount();

    // 替换引用
    this.renderedComponent = nextRenderedComponent;

    // 注意:这部分代码理论上应该放在CompositeComponent外面而不是里面
    prevNode.parentNode.replaceChild(nextNode,prevNode);
  }
}

综上所述,一个组合组件接收到一个新的元素,他会直接更新内部实例,或者是卸载旧的实例,装载新的实例。

这里还有另外一种情况,当一个元素的key发生变化时组件就是被重装载而不是接受一个新的元素,当然我们这里先不讨论key造成的改变,因为它的会比较复杂。

值得注意的是,我们需要为内部实例增加一个叫做getHostNode()的函数,以至于在更新阶段找到指定的节点然后更新它,下面是它在两个类中的简单实现:

class CompositeComponent {
  // ...

  getHostNode() {
    // 递归处理
    return this.renderedComponent.getHostNode();
  }
}

class DOMComponent {
  // ...

  getHostNode() {
    return this.node;
  }  
}

9.更新Host组件

Host组件的实现就像是DOMComponent,和CompositeComponent更新截然不同,当他们接收到一个元素时,他们需要更新底层的DOM节点,就ReactDOM而言,他们会更新DOM属性:

class DOMComponent {
  // ...

  receive(nextElement) {
    var node = this.node;
    var prevElement = this.currentElement;
    var prevProps = prevElement.props;
    var nextProps = nextElement.props;    
    this.currentElement = nextElement;

    // 移除旧的属性
    Object.keys(prevProps).forEach(propName => {
      if (propName !== 'children' && !nextProps.hasOwnProperty(propName)) {
        node.removeAttribute(propName);
      }
    });
    // 设置新的属性
    Object.keys(nextProps).forEach(propName => {
      if (propName !== 'children') {
        node.setAttribute(propName,nextProps[propName]);
      }
    });

    // ...

然后,host组件就开始更新他们的孩子,不像组合组件那样,他们只会包含最多一个孩子。

在下面这个例子中,我是用一个数组的内部实例,然后遍历它,或者是进行更新还是替换内部实例取决于新的元素和旧的元素的type是否相同。真正的调解器还会使用元素的key去处理,当然我们暂时不会涉及这个处理逻辑。

我们可以收集DOM节点需要操作的孩子然后批量的处理他们来节省时间:

// ...

    // 在这里React元素是一个数组
    var prevChildren = prevProps.children || [];
    if (!Array.isArray(prevChildren)) {
      prevChildren = [prevChildren];
    }
    var nextChildren = nextProps.children || [];
    if (!Array.isArray(nextChildren)) {
      nextChildren = [nextChildren];
    }
    // 在这里内部实例也是一个数组
    var prevRenderedChildren = this.renderedChildren;
    var nextRenderedChildren = [];

    // 我们迭代孩子增加操作到数组中
    var operationQueue = [];

    // Note: 这一份代码是极度简化了的
    // 他没有处理渲染以及key的问题
    // 他只是用来说明整个处理流程而不是细节

    for (var i = 0; i < nextChildren.length; i++) {
      // 尽可能得到该孩子的内部实例
      var prevChild = prevRenderedChildren[i];

      // 如果下标表示的没有内部实例
      // 那么会增加一个孩子到数组结尾
      // 并创建一个新的内部实例,挂载它,以及使用它。
      if (!prevChild) {
        var nextChild = instantiateComponent(nextChildren[i]);
        var node = nextChild.mount();

        // 记录我们需要增加的节点
        operationQueue.push({type: 'ADD',node});
        nextRenderedChildren.push(nextChild);
        continue;
      }

      // 如果元素的类型匹配,那么我们只更新实例.
      // 比如,<Button size="small" /> 会被更新为
      // <Button size="large" /> 但是不能是一个 <App />.
      var canUpdate = prevChildren[i].type === nextChildren[i].type;

      // 如果我们没有更新已经存在的实例,那么我们就不得不卸载然后
      // 再挂载新的节点去代替旧的
      if (!canUpdate) {
        var prevNode = prevChild.node;
        prevChild.unmount();

        var nextChild = instantiateComponent(nextChildren[i]);
        var nextNode = nextChild.mount();

        // 记录我们需要交换的节点
        operationQueue.push({type: 'REPLACE',prevNode,nextNode});
        nextRenderedChildren.push(nextChild);
        continue;
      }

      // 如果我们更新了已经存在的内部实例
      // 那就直接接收下一个新的元素,用它来更新自己
      prevChild.receive(nextChildren[i]);
      nextRenderedChildren.push(prevChild);
    }

    // 最后卸载不存在的孩子
    for (var j = nextChildren.length; j < prevChildren.length; j++) {
     var prevChild = prevRenderedChildren[j];
     var node = prevChild.node;
     prevChild.unmount();

     // 记录我们需要移除的节点
     operationQueue.push({type: 'REMOVE',node});
    }

    // 将孩子的列表更新到最新的版本
    this.renderedChildren = nextRenderedChildren;

    // ...

在最后一步我们执行了DOM操作,同样的,真正的调解器代码会更加复杂,因为他还要处理移除情况。

// ...

    // Process the operation queue.
    while (operationQueue.length > 0) {
      var operation = operationQueue.shift();
      switch (operation.type) {
      case 'ADD':
        this.node.appendChild(operation.node);
        break;
      case 'REPLACE':
        this.node.replaceChild(operation.nextNode,operation.prevNode);
        break;
      case 'REMOVE':
        this.node.removeChild(operation.node);
        break;
      }
    }
  }
}

10.顶级更新

现在CompositeComponentDOMComponent都实现了receive(nextElement)方法,我们需要改变全局函数mountTree()增加在元素type相同的时候的处理。

function mountTree(element,containerNode) {
  // 检查已经存在的树
  if (containerNode.firstChild) {
    var prevNode = containerNode.firstChild;
    var prevRootComponent = prevNode._internalInstance;
    var prevElement = prevRootComponent.currentElement;

    // 如果可以,重利用已经存在的root组件
    if (prevElement.type === element.type) {
      prevRootComponent.receive(element);
      return;
    }

    // 否则,卸载已经存在的树
    unmountTree(containerNode);
  }

  // ...

}

如今,两次调用mountTree(),他能充分的利用已经存在的资源

var rootEl = document.getElementById('root');

mountTree(<App />,rootEl);

mountTree(<App />,rootEl);

11.我们遗漏了什么

我的这篇博客写的内容和真实的代码相差甚远,这里我提几个没有涉及到的但又非常重要的几点。

  1. 组件是可以返回null的,调解器可以处理空的数组和渲染出空的元素

  2. 调解器也能从元素中读取key属性,使用它们来建立内部实例对应的数组中的元素,这样可以提高性能

  3. 除了是组合和host内部实例外,还可以是text或者空的组件,他们表示text节点和我们render返回null

  4. 渲染器使用注入方式将host内部类传递给调解器,比如,React DOM渲染器会告诉调解器使用ReactDOMComponent作为host的内部实例的实现。

  5. 更新的列表孩子的逻辑被提取到了一个叫做ReactMultiChildmixin中,这个东西可以被在React DOMReact Native渲染器中的host内部实例所使用。

  6. 调解器也实现了对setState()的支持,多个更新事件可以批量处理

  7. 调解器还对组合组件和host组件的ref进行了处理

  8. DOM准备好之后,生命周期钩子componentDidMount()componentDidUpdate()也会调用,他们被加入到了一个“回调队列”中,去顺序的批量的处理他们

  9. React将当前信息更新到内部实例中去的过程叫做“事务”,事务方式处理对于等待生命周期钩子的队列的跟踪非常有用。事务能保证React在更新成功后可以清除所有东西。比如,事务类提供ReactDOM更新出现问题时可以恢复到更新之前的状态。

12.代码比较

  1. ReactMount的代码有点像mountTreeunmountTree用于组件的绑定和卸载,在React Native中是这个ReactNativeMount

  2. ReactDOMComponentDOMComponent是类似的,它就是React DOM渲染器host组件类的实现,ReactNativeBaseComponent则是React Native的。

  3. ReactCompositeComponentCompositeComponent是类似的,它用来处理用户自定义组件和他们的状态

  4. instantiateReactComponent则构造一个元素实例,和instantiateComponent相似

  5. ReactReconciler则是mountComponent,receiveComponent,和unmountComponent的集合,它在底层实现了内部实例,但是也包括所有内部实例共享的代码。

  6. ReactChildReconciler则是根据孩子元素的key去绑定孩子,更新孩子和卸载孩子。

  7. ReactMultiChild作为一个独立渲染器来处理孩子的插入和删除工作。

  8. 因为历史遗留问题,在React代码中,mount(),receive(),和unmount()名字改为了 mountComponent(),receiveComponent(),和 unmountComponent(),但是他们依旧可以接受元素

  9. 内部实例由于是私有的,一般都以下划线命名开头。

12.将来的发展方向

在前一章中我提到过一个调解器叫做纤维调解器,它就是用来取代堆栈调解器的,因为堆栈调解器具有一定的局限性,同步问题,或者是无法分离代码,耦合度比较大,等等等,个人想法而已。

下一篇将讲什么暂时不知道,嘿

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读