按部就班DIY一个react(四)

注意:下文中,反复提到"实例"一词,如无特别交代,它指的是第三篇章instance的这个概念react

在react中,“component”概念能够理解为一个身份的象征。假如咱们将root virtual DOM节点比做virtual DOM世界的周天子的话,那么“component”就是管辖着一块或大或小疆域的分封诸侯。只不过,这块疆域不是土地,而是“virtual DOM”。算法

在没有引入“component”这个概念以前,咱们面临这如下几个问题:数组

  • state是全局的。
  • 每更新一次界面,都须要手动地调用一次个render函数。
  • 对render函数的调用,会致使整树协调的发生,界面渲染性能不高。

所以,为了解决以上几个问题,咱们引入“component”这个概念。“component”经过自身的两个特征来解决以上问题:bash

  • local state。表明component所处的界面状态。
  • setState API。经过调用它来修改local state,从而使协调在整颗子树上发生。

经过调用setState来更新UI,就是咱们所要实现的子树协调。babel

“component”概念的落地工做,首先从实现base class开始。熟悉react的读者大概都知道,component的定义中,惟一不能缺乏的方法是render方法。因此这个父类的类图大概是这样的: 数据结构

好,下面咱们一块儿来实现这个父类大概的shape:架构

class Component {
    constructor(props){
        this.props = props || {}
        this.state = this.state || {}
    }
    
    setState(partialState){
        this.state = Object.assign({},this.state,partialState)
    }
    
    render(){
        console.error('You should implement your own render method!')
    }
}
复制代码

上面的代码其实就是作了两个事:app

  • 将用户调用setState时传入的局部state合并到已有的state当中去。
  • 提醒用户,render方法必须本身实现。

为了区分,react把DOM elment和text element分别称为“DOM Component”和“Text Component”,把咱们这里的“component”称之为“Composite Component”。在引入“component”这个概念后,咱们也沿用这些叫法。dom

目前为止,咱们还没让local state跟子树的协调联系起来。咱们调用setState,界面将不会发生任何变化。要想local state改变能跟协调算法联动起来,本质上就是要求咱们前后回答三个问题:函数

一. 如何对接jsx的编译?

目前咱们createElement的实现是这样的:

function createElement(type, props, ...childrens) {
  const newProps = Object.assign({}, props);
  const hasChildren = childrens.length > 0;
  const rawChildren = hasChildren ? [].concat(...childrens) : [];
  newProps.children = rawChildren
    .filter(child => !!child)
    .map(child => {
      return child instanceof Object ? child : createTextElement(child);
    });
  return {
    type,
    props: newProps
  };
}
复制代码

这一次,咱们不动createElement的实现。由于咱们能够经过改动转换jsx的babel插件的具体实现来知足咱们的需求。大概原理是,让babel插件将以大写字母开头的标签识别为Composite Component,而后原封不动地把用户自定义的组件类传给咱们。咱们经过在后续的实例化(面向对象意义上的实例化),来拿到咱们想要的数据。届时,咱们写的是这样的jsx:

class App extends Component {
    render(){.......}
}

render(<App />,rootDOM)
复制代码

转换jsx的babel插件将结合咱们实现的createElment函数编译为:

class App extends Component {
    render(){.......}
}

render(createElement(App,{}),rootDOM)
复制代码

对Composite Component调用createElement返回的virtual DOM的数据将会是这样的:

{
    type:App,// 从ES6的角度看,APP是一个“类”;从ES5的角度来看,它仍是一个函数
    props:{
        children:[]
    }
}
复制代码

如何改动jsx转换babel插件不在咱们的讨论范围内,故略过不表。 凡是认真阅读过第一篇章的读者可能就注意到了,Composite Component跟DOM Component和Text Component所对应的virtual DOM结构不一样的一点就是:type字段的值的类型是函数(提醒:ES6的类最后仍是会被编译为function),而不是string了。这一点很重要。Composite Component的实例化对接工做正是基于这一点。

二. 如何对接“实例”概念?

这个问题包含了两个问题。

第一是:Composite Component所对应的instance的数据结构是如何?

第二是:如何被实例化?

这两位问题对应两个任务:

task1: 肯定Composite Component所对应实例的数据结构。

显然,Composite Component跟DOM Component和Text Component都是属于“组件”概念范畴的,它也须要被挂载到具体的DOM节点上,也有对应的element,也应该有childInstance。只不过不一样于先前的DOM Component和Text Component,这些字段的取值基于Composite Component的特殊性,确定会有所不一样。在react的源码中,Composite Component所对应的instance确实有子实例的字段,只不过这个子实例的含义并不能跟咱们从jsx结构所看到的层级关系对应上。举个例子,DOM component里面,若是咱们看到

<div>
    我是文本节点
    <span>我是另一个节点</span>
</div>
复制代码

这种结构,咱们就能够说我是文本节点<span>我是另一个节点</span>所对应的实例是<div>组件的子实例。在Composite Component的概念里,状况就不同了。也就是说,若是咱们看到

<MyComponent>
    我是文本节点
    <span>我是另一个节点</span
<MyComponent>
复制代码

这种结构,咱们不能说我是文本节点<span>我是另一个节点</span>所对应的实例是<MyComponent>组件的子实例。实际上,<MyComponent>组件的子实例是另有其人。它就是组件的render函数所返回的react element所对应的实例,为何呢?缘由很简单,有二:

  • <MyComponent>只是一个身份的象征,象征着组件render方法所返回的react element。这比如日本的天皇是没有实权的,他只是一个国家的象征而已,掌握实权的是日本的首相。
  • <MyComponent>的子组件(我是文本节点<span>我是另一个节点</span>)最后都是被render方法经过this.props.children消费掉,成为它返回的react element的一部分。

因此,从实现的角度来讲,render方法返回的element所对应的实例才是<MyComponent>的子实例。

由于render方法只能返回一个element,因此Composite Component只有一个子实例,也就是说Composite Component所对应的的子实例的值并非由子实例组成的数组,只是单个实例而已。一样的,由于组件名只是一个象征而已,那么Composite Component对应实例的dom节点的值应该是由子实例所对应的DOM节点来充当。最后一点,若是咱们想把Composite Component的实例和它的真正实例(这里的真正实例就是指经过new操做符调用函数所返回的对象,react里面称之为publicInstance。为了区分,第三篇章所引入instance概念又称之为internalInstance)对应起来,那么咱们都须要在彼此的身上保存对方的引用。综上所述,Composite Component所对应的实例的数据结构以下:

const instance = {
    dom: DOMObject,
    element:reactElement,
    childInstance:childInstance,
    publicInstance:realInstance // 组件类经过new操做符运算所返回的真正意义上的实例
}
复制代码

task2: 用代码实现Composite Component的实例化。

既然上面已经弄清楚Composite Component所对应实例的数据结构(有什么字段,字段的值是什么),那么实现它的实例化也是顺水推舟的事了,咱们在原有的代码上添加上Composite Component所对应的条件分支:

function instantiate(element) {
  const { type, props } = element;
  // 根据type字段值的类型来判断是不是Composite Component
  const isDomElement = typeof type === "string";

  // 建立对应的DOM节点
  if(isDomElement){ // 实例化DOM Component 和 Text Component
    const isTextElement = type === "TEXT_ELEMENT";
    const domNode = isTextElement
      ? document.createTextNode("")
      : document.createElement(type);

    // 设置属性
    updateDomProperties(domNode, {}, props);

    // 对children element递归调用instantiate函数
    const children = props.children;
    let childInstances = [];
    if (children && children.length) {
      childInstances = children.map(childElement => instantiate(childElement));
      const childDoms = childInstances.map(childInstance => childInstance.dom);
      childDoms.forEach(childDom => domNode.appendChild(childDom));
    }

    const instance = {
      dom: domNode,
      element,
      childInstances
    };

    return instance;
  }else { // 实例化Composite Component
     const { type, props } = element;
     const instance = {};
     // component类的真正实例化
     const publicInstance = new type(props);
     
     // 将render方法返回的element的this指向publicInstance
     // 结合“this关键字的指向是由它执行的上下文所决定的”这句话来理解一下
     const childElement = publicInstance.render(); 
     
     // 对于Composite Component来讲,render方法返回element对应的instance的dom就是它对应实例的dom
     const childInstance = instantiate(childElement);
     const dom = childInstance.dom; 

     // 按照咱们在task1讨论出的数据结构,组装component element所对应的实例
     Object.assign(instance, { dom, element, childInstance, publicInstance });
     
     // 最后,把 Composite Component所对应实例的引用保存在publicInstance身上,打通二者之间的访问
     publicInstance.__internalInstance = instance;
     return instance;
  }

}
复制代码

三. 如何对接“协调”概念?

Composite Component在协调算法中,对应的“初始挂载”,“删除”和“替换”的实现跟DOM component和Text component的实现也是同样的,比较简单。二者之间不一样的是“更新”部分的实现逻辑。咱们先来看看reconcile函数的签名:

reconcile:(instance, element, domContainer) => instance
复制代码

在这系列接近尾声的时候,你们可能也观察出来了。reconcile函数的第一参数instance,第二参数element是reconcile函数语义上的标志。换句话说,协调,协调,协调的对象是谁跟谁呢?答曰:正是instance和elment。咱们要记住,不管什么时候何刻,传入reconcile函数的element参数都是表明着咱们渲染界面的最新意图。而instance从设计开始,它就被定义为用于保存当前这一帧界面的相关信息。简而言之,咱们能够简单地把instance理解为“旧的”,而element理解为“新的”。咱们须要实现的协调,本质上就是看看目前“旧的”有什么东西是能够复用的。回到“component”对接“协调”概念上来,大体步骤也是同样,不过细节有所不一样。概括起来能够分为三步走:

  1. 更新publicInstance的state。
  2. 更新publicInstance的props。
  3. 更新childInstance。

这里值得一提的是,第一步的完成不是在reconcile函数的内部来完成的,而是在咱们提供给开发者的component父类中去完成。因此,咱们得更新一下父类的实现:

class Component {
  constructor(props) {
    this.props = props;
    this.state = this.state || {};
  }

  setState(partialState) {
   // 1. 更新publicInstance的state
    this.state = Object.assign({}, this.state, partialState);
    const {
      dom,
      element
    } = this.__internalInstance
    const parentDom = dom.parentNode;
    reconcile(this.__internalInstance, element,parentDom);
  }
}
复制代码

第一步完成以后,咱们经过在setState内部调用reconcile函数进入第二和第三步:

function reconcile(instance, element, domContainer) {
  let newInstance = {};
  // 整树的初始挂载
  if (instance === null) {
  // .......
  } else if (element === null) { // 整树的删除
  // .......
  }else if(element.type !== instance.element.type){ // 整树的替换
    // .......
  } else { // 整树的更新
    // DOM component或者Text component
    if(typeof element.type === 'string'){
    // .......
    }else { // composite component
    
    // 2.更新publicInstance的props
    instance.publicInstance.props = element.props;
      
    // 3.更新childInstance
    const childElement = instance.publicInstance.render();
    const oldChildInstance = instance.childInstance;
    const childInstance = reconcile(oldChildInstance, childElement,domContainer);
    
    // 跟实例化过程同样, 更新后的childInstance就是Composite Component所对应instance的childInstance;
    // 更新后的childInstance的dom就是Composite Component所对应instance的dom。
    // element原封不动地挂载上去便可
    instance.dom = childInstance.dom;
    instance.childInstance = childInstance;
    instance.element = element;
    return instance;
    }
  } 
  return newInstance;
}
复制代码

至此,咱们已经完成了“component”概念和“协调”概念的对接工做。也就是说,如今若是咱们想要局部更新UI的话,只须要定义本身的component,而后调用setState API,这个局部UI所对应的子树的协调就会发生了。

《按部就班DIY一个react》系列到此结束。虽然,这是一个玩具版的react,可是经过这个DIY过程,我加深了对react思想,概念和基本原理的理解。固然,还有不少基本react feature没有实现,好比:ref,key,生命周期函数等等,更不用说改用Fiber架构以后所带来的新feature啦。最后,真心但愿这个系列能对你理解react世界带来些许帮助,至于完整的代码,我稍后再整理,放到codepen或者codesandbox供你们玩弄玩弄。

再见!

相关文章
相关标签/搜索