21天造React (二)

119 阅读6分钟
原文链接: zhuanlan.zhihu.com

前面React已经实现了Function Component,这节主要实现Class Component。

Class Component比Function Component要复杂的多。Function Component仅仅需要实现render功能,而class Component功能要多得多。主要功能如下:

  • 支持生命周期,包括(componentWillMount,componentDidMount,componentWillReceiveProps, componentWillUpdate,componentDidUpdate,componentWillUnmount)。
  • 支持状态更新setState
  • 支持reconciler(Virtual Dom diff)

为支持Class Component需要扩充我们的ReactElement的类型定义,扩充定义如下

type ReactElement = ReactComponentElement | ReactDOMElement;
type ReactComponentElement<TProps> = {
  type : ReactClassComponent<TProps>|ReactFuncComponent<TProps>,
  props : TProps
};
type ReactFuncComponent<TProps> = TProps => ReactElement
type ReactClassComponent<TProps> = (TProps) => ReactComponent<TProps>;

type ReactComponent<TProps> = {
  props : TProps,
  render : () => ReactElement
};

为此,mountComposite也需要对Class Component支持,因此需要我们区分Function Component和class Component。

Function or Class

如何判断一个对象是Function还是Class并非轻而易举。typeof (function(){}) 和typeof (class{})均返回'function',Function和Class一个区别是Class必须使用new 调用,否则会抛异常。因此可以使用如下方法判断。

isClass(obj){ 
  function isClass(obj){
  if(typeof obj !== 'function') return false;
  try {
    obj()
    return false;
  }catch(err){
    return true;
  }
}
console.log(isClass(function(){}));// false
console.log(isClass(class{})); // true


上面的方法存在几种缺陷

  • obj本身执行就会抛出异常,这是需要我们精确判断抛出异常是因为class的无new调用导致的,很不幸规范没规定了抛出的具体异常信息,因此难以判断。
  • 更严重的是,这里执行了obj,加入obj本身带有副作用,这将影响程序的执行逻辑。

另一种方法是Class的定义含有class关键字,可以通过Function.prototype.toString进行判断。

function isClass(v) {
  return typeof v === 'function' && /^\s*class\s+/.test(v.toString());
}

这里存在的问题是babel等转译工具会将class转译为函数,并不能保证转译后的函数能通过该判断。

幸运的是我们并不需要判断一个对象是不是Class,因为React的组件都继承自Component 这个Class,因此只需要判断对象是否为Component的子类即可。通过如下判断即可

class Component {
}
Component.prototype.isReactComponent = true;

function isClass(type){
  return (
    Boolean(type.prototype) &&
    Boolean(type.prototype.isReactComponent)
  );
}

接下来就可以实现mountComposite了

function hooks(obj,name,...args){
  obj[name] && obj[name].apply(obj,args);
}
function mountComposite(element){
  const { type, props } = element;
  const children = props.children;
  if(isClass(type)){
    var instance = new type(props);
    instance.props = props;
    hooks(instance, 'componentWillMount')
    element = instance.render(element);
  }else {
    element = element.type(element.props);
  }
  
  return mount(element);  // delegate to mount
}
class Link extends Component {
  render(){
    const { children } = this.props
    return (
      <a href="http://www.baidu.com">{children}</a>
    )
  }
}


var element = (
  <div class="container">
    <Button text="this is a button"/>
    <Button text="this is another button"/>
    <Link>baidu</Link>
  </div>
)
var node = mount(element);
document.body.appendChild(node);

至此已经可以实现Class Component的渲染功能了。

Element && Component && Instance区别

至此我们实现的组件均没有状态,只能接受props进行渲染,这样每次更新都会造成所有的组件重新渲染,重新生成新的Dom节点和element对象,效率低下。因此我们希望尽可能复用原有的DOM节点,只对其属性进行更新,这样尽可能的减小Dom操作。因此需要存储上次生成的Dom节点和element对象,为此引入了内部实例(internal Instance)来存储这些状态。

至此我们引入了三个概念,ElementComponent、和Internal Instance/Component Instance。其区别如下:

  • Element:element是用来描述Dom节点或Instance的对象,element分为两种描述Dom节点的如{type: 'button', props: { id: 'btn'}}和描述Instance的如{type: Button, props: { text: 'this is a button'}},其仅仅为一个基本对象,不含有任何额外的方法。
  • Component:Component是对Element组合的抽象,其用来描述组件树,其输入是props,输出是组件树,输出的组件树既可以包含描述Dom的Element,也可以包含描述instance的Element,通过Component可以将不同的Element组合为Element tree,Component也分为两种Function Component和 Class Component
  • Component Instance/Internal Instance:Component Instance是Component的实例,且只有Class Component才有实例,instance用于存储局部状态(this.state等)和响应生命周期,而Internal Instance 则是React实现的细节,用于存储Component Instance以用于更新和卸载操作。

函数多态 or 对象多态

之前的实现时mount根据传入的element类型决定调用不同的mount操作。这实际上是利用了函数的多态,对于无状态的操作这很方便,但当引入状态后,函数多态变得难以处理,为此使用面向对象的方式重构代码。

mount(element){
   if(typeof element === 'string') return mountText();
   if(typeof element.type === 'string') return mountDom();
   if(typeof element.type === 'function') return mountComposite();
}

可重构为如下形式的代码

class Mountable {
  mount(){
    throw new Error('must implement mount method');
  }
}
class TextComponent extends Mountable{
  mount(){

  }
}
class DomComponent extends Mountable {
  mount(){

  }
}
class CompositeComponent extends Mountable {
  mount(){
  }
}
function instantiateComponent(element) {
  if(typeof element === 'string') return new TextComponent();
  if(typeof element.type === 'string') return new DomComponent();
  if(typeof element.type === 'function') return CompositeComponent();
}
function mount(element){
  const rootComponent = instantiateComponent(element);
  return rootComponent.mount();
}

重构后的就可以在各类型的instance上存储数据了。

重构后的代码如下

class Component {
  render(){
    throw new Error('must implement render method');
  }
}
Component.prototype.isReactComponent = true;

function isClass(type) {
  return (
    Boolean(type.prototype) &&
    Boolean(type.prototype.isReactComponent)
  );
}

function hooks(obj,name,...args){
  obj[name] && obj[name].apply(obj,args);
}

class DomComponent {
  constructor(element){
    this.currentElement = element;
    this.renderedChildren = [];
    this.node = null;
  }
  getPublicInstance(){
    return this.node;
  }
  mount(){
    const { type, props } = this.currentElement;
    let children  = props.children;
    children = children.filter(Boolean);

    const node = document.createElement(type);
    Object.keys(props).forEach(propName => {
      if(propName !== 'children'){
        node.setAttribute(propName, props[propName])
      }
    })
    const renderedChildren = children.map(instantiateComponent);
    this.renderedChildren = renderedChildren;
    const childNodes = renderedChildren.map(child => child.mount());
    for(let child of childNodes){
      node.appendChild(child);
    }
    this.node = node;
    return node;
  }
}

class TextComponent {
  constructor(element){
    this.currentElement = element;
    this.node = null;
  }
  getPublicInstance(){
    return this.node;
  }
  mount(){
    const node = document.createTextNode(this.currentElement);
    this.node = node;
    return node;
  }
}

class CompositeComponent {
  constructor(element){
    this.currentElement = element;
    this.publicInstance = null; // public instance
    this.renderedComponent = null;
  }
  getPublicInstance(){
    return this.publicInstance;
  }
  mount(){
    const { type, props } = this.currentElement;
    const children = props.children;
    let instance, renderedElement;
    // delegate to mount
    if(isClass(type)){
      instance  = new type(props); 
      instance.props = props;
      hooks(instance, 'componentWillMount');
      renderedElement = instance.render();
      this.publicInstance = instance;
    }else {
      renderedElement = type(props);
    }
    const renderedComponent = instantiateComponent(renderedElement);
    this.renderedComponent = renderedComponent;
    return renderedComponent.mount();
  }
}
function instantiateComponent(element) {
  if(typeof element === 'string') return new TextComponent(element); // internal instance
  if(typeof element.type === 'string') return new DomComponent(element);
  if(typeof element.type === 'function') return new CompositeComponent(element);
}
function mount(element){
  const rootComponent = instantiateComponent(element);
  return rootComponent.mount();
}

function Button(props){
  return (
    <button class="btn">{props.text}</button>
  )
}
function createElement(type,props,...args){
  props = Object.assign({},props);
  let children = [].concat(...args);
  props.children = children;
  return { type, props };
}
class Link extends Component {
  componentWillMount(){
    console.log('Link will Mount');
  }
  render(){
    const { children } = this.props
    return (
      <a href="http://www.baidu.com">{children}</a>
    )
  }
}

const element = (
  <div class="container">
    <Button text="this is a button"/>
    <Button text="this is another button"/>
    <Link>baidu</Link>
  </div>
)
const node = mount(element);
document.body.appendChild(node);

此时textComponent,DomComponent和CompositeComponent的mount逻辑全部从mount函数转移到内部的mount方法内了。

这里的CompositeComponent和DOMComponent区别于Link这种Component,CompositeComponent和DOMComponent是React内部实现的细节,不提供向外的API,同理CompositeComponent和Link的instance也不同,为了进行区分,把CompositeComponent/DOMComponent的instance称为内部实例(internal instance)而把Link这类的实例称为外部实例(public instance),外部无法访问内部实例,但能通过getPublicInstace访问外部实例,也即组件实例,这里可以看出DOMComponent和CompositeComponent的外部实例返回类型不一致,DOMComponent返回的是node节点而CompositeComponent返回的则是组件的实例,且对于Function Component其返回值为null。其和React 的ref回调返回的实例情况一致。

至此我们可以实现React的render功能了,其接受一个element和mountNode节点,将element描述的dom树渲染到mountNode中。

function render(element, mountNode){
  var rootComponent = instantiateComponent(element); // top-level internal instance
  var node = rootComponent.mount(); // top-level node
  mountNode.appendChild(node);
  var publicInstance = rootComponent.getPublicInstance(); // top-level public instance
  console.log('internal instance:', rootComponent);
  console.log('public instance:', publicInstance);
  return publicInstance;
}
render(app, document.querySelector('#root'));

执行后的结果是

从internal instance信息可以看出,

  • public instance就是internal instance的node属性值。
  • DOMComposite的node为public Instance节点,Function Component对应的publicInstance为null,Class Component对应的publicInstance是Link的实例,其完整的存储了上次渲染产生的结果,这样就可以为后续的更新操作提供便捷。

这里实现的render函数与React 的render有重要差别,React的render只有首次render时才会进行mount操作,后续render进行update操作,因此我们的render也要实现update功能。