21天造React (三)

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

Reconciliation

更新实现是React的核心,React v15之前使用stack reconciliation算法进行更新,v16则使用了fiber reconciliation

本章和下章主要讲解stack reconciliation的实现。

因为update操作涉及到旧组件实例的unmount,所以先介绍unmount的实现。

unmount的实现类似于mount,先卸载子组件后卸载父组件。前面我们在internal instance上已经存储了组件的信息,只需后序遍历组件树进行卸载即可,同时执行componentWillUnmount的生命周期。

function unmountComponentAtNode(mountNode){
  var node = mountNode.firstChild;
  var rootComponent = node._internalInstance; // 读取 internal instance
  rootComponent.unmount();
  mountNode.innerHTML = '';
}

unmountComponentAtNode需要读取_internalInstance且对_internalInstance进行unmount操作,这需要我们在render里对_internalInstance进行存储和Composite Component及Dom Component对unmount操作进行支持。实现如下:

function render(element, mountNode){
  if(mountNode.firstChild){ // 若组件已经mount,则卸载以前的组件
    unmountComponentAtNode(mountNode);
  }
  var rootComponent = instantiateComponent(element); // top-level internal instance
  var node = rootComponent.mount(); // top-level node
  mountNode.appendChild(node);
  node._internalInstance = rootComponent; // 存储internal instance供 unmount和update使用
  var publicInstance = rootComponent.getPublicInstance(); // top-level public instance
  return publicInstance;
}
class DomComponent {
  unmount(){
    this.renderedChildren.forEach(child => child.unmount()); //递归卸载children组件
  }
}
class CompositeComponent {
  unmount(){
    hooks(this.publicInstance, 'componentWillUnmount'); //执行生命周期
    this.renderedComponent.unmount(); // 递归执行renderedComponent
  }
}

Update

React的更新基于两个关键点

  • 不同的组件类型认为会生成完全不同的组件树,React不会尝试去比对他们,而是直接卸载上一个组件树,加载新的组件树。
  • 列表的对比是基于key值的,不同的key值表示不同的组件树,React会通过替换、删除、增加、更新的方式将一个列表更新成新的列表。

为了支持更新操作我们扩充internal instance,支持一个新的方法receive(nextElement)以支持更新操作。

class CompositeComponent{
   getHostNode() {
    return this.renderedComponent.getHostNode();
   }
  receive(nextElement){
    const prevProps = this.currentElement.props;
    const publicInstance = this.publicInstance;
    const prevRenderedComponent = this.renderedComponent;
    const prevRenderedElement = prevRenderedComponent.currentElement;

    this.currentElement = nextElement;
    const type = nextElement.type;
    const nextProps = nextElement.props;

    let nextRenderedElement;
    if(isClass(type)) {
      hooks(publicInstance, 'componentWillUpdate', nextProps);
      publicInstance.props = nextProps;
      nextRenderedElement = publicInstance.render();
    } else if(typeof type === 'function') {
      nextRenderedElement = type(nextProps);
    }
    if(prevRenderedElement.type === nextRenderedElement.type) {
      prevRenderedComponent.receive(nextRenderedElement);
      hooks(publicInstance,'componentDidUpdate',prevProps);
      return
    }
    const prevNode = prevRenderedComponent.getHostNode();
    prevRenderedComponent.unmount();
    const nextRenderedComponent = instantiateComponent(nextRenderedElement);
    const nextNode = nextRenderedComponent.mount();
    this.renderedComponent = nextRenderedComponent;
    prevNode.parentNode.replaceChild(nextNode, prevNode);
  }
}
function findDOMNode(instance){
  return instance.getHostNode();
}

CompositeComponent 的receive实现如上所示,主要逻辑是

  • 判断组件类型是否相同,相同则执行更新操作(由于CompositeComponent并不负责创建DOM节点,所以与mount类似,将其delegate to renderedComponent执行,依次递归直到DOMComposite才负责执行真正的DOM更新。
  • 如果组件类型不同则卸载旧组件,加载新组件,同时替换旧组件渲染的DOM节点为新组件的DOM节点,因此我们需要获得旧组件的DOM节点,同理delegate到DOM Composite,ReactDOM.findDOMNode(instance)的执行逻辑即如此
  • 执行componentWillUpdate和componentDidUpdate生命周期,Function Component因为不存在public instance也就无法执行所有的生命周期。

我们发现CompositeComponent做的更新工作实际很少,实际的DOM更新都delegate给DomComponent了

class DomComponent{  
  getHostNode() {
    return this.node;
  }
  updateDomProperties(prevProps, nextProps) {
    const node = this.node;
    // 删除旧的attribute
    Object.keys(prevProps).forEach(propName => {
      if(propName !== 'children' && !nextProps.hasOwnProperty(propName)) {
        node.removeAttribute(propName);
      }
    })
    // 更新新的attribute
    Object.keys(nextProps).forEach(propName => {
      if(propName !== 'children') {
        node.setAttribute(propName, nextProps[propName])
      }
    })
  }
  updateChildren(prevProps, nextProps) {
    // TODO 粗暴实现
    const children = nextProps.children;
    const node = this.node;
    // 卸载所有旧的组件
    this.renderedChildren.forEach(child => child.unmount())
    this.node.innerHTML = '';
    // 加载新的组件
    const renderedChildren = children.map(instantiateComponent);
    this.renderedChildren = renderedChildren;
    const childNodes = renderedChildren.map(child => child.mount());
    for(let child of childNodes) {
      node.appendChild(child);
    }
  }
  receive(nextElement) {
    const node = this.node;
    const preveElement = this.currentElement;
    const prevProps = preveElement.props;
    const nextProps = nextElement.props;
    this.currentElement = nextElement;

    this.updateDomProperties(prevProps, nextProps);
    this.updateChildren(prevProps, nextProps);
  }
}

DomComponent的更新主要包含两部分attribute的更新和children的更新,attribute的更新比较简单,删除不存在的attribute和替换新的attribute,而children的更新则比较复杂,最为粗暴的实现是卸载所有旧的children组件,加载新的children组件。这样使得所有的children组件没有办法复用之前的DOM结构,浪费性能。

较为复杂的实现是依次比对新旧两个list,类型相同的则进行更新,类型不同则进行替换。

列表比对的结果有四种,对应四种操作:

  • 旧列表不存在,新列表存在,对应新增(ADD)操作。
  • 旧列表存在,新列表不存在,对应删除(DELETE)操作。
  • 旧列表存在,新列表存在,但组件类型不同,对应替换(REPLACE)操作。
  • 旧列表存在,新列表存在,且组件类型相同,对应更新(UPDATE),更新原组件即可。
updateChildren(prevProps, nextProps) {
    const prevChildren = prevProps.children;
    const nextChildren = nextProps.children;
    const prevRenderedChildren = this.renderedChildren;
    const nextRenderedChildren =  [];
    const operationQueue = [];
    for(let i=0;i< nextChildren.length;i++){
      const prevChild = prevRenderedChildren[i];
      // insert
      if(!prevChild){
        const nextChild = instantiateComponent(nextChildren[i]);
        const node = nextChild.mount();
        operationQueue.push({
          type: 'ADD',
          node
        })
        nextRenderedChildren.push(nextChild);
        continue;
      }
      const canUpdate = prevChildren[i].type === nextChildren[i].type;
      // replace
      if(!canUpdate){
        const prevNode = prevChild.getHostNode();
        prevChild.unmount();
        const nextChild = instantiateComponent(nextChildren[i]);
        const nextNode = nextChild.mount();
        console.log('prevNode:', prevNode);
        console.log('nextNode:', nextNode);
        operationQueue.push({
          type: 'REPLACE',
          prevNode,
          nextNode
        });
        nextRenderedChildren.push(nextChild);
        continue;
      }
      prevChild.receive(nextChildren[i]);
      nextRenderedChildren.push(prevChild);
    }
    // delete
    for(let j=nextChildren.length;j<prevChildren.length;j++){
      const prevChild = prevRenderedChildren[j];
      const node = prevChild.node;
      prevChild.unmount();
      operationQueue.push({
        type: 'REMOVE', node
      })
    }
    this.renderedChildren = nextRenderedChildren;

    // batch update DOM
    while(operationQueue.length > 0){
      const 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;
      }
    }
  }

至此我们已经实现了组件的更新操作,但其仍然存在很大的缺陷。

其对于列表的追加和尾部删除,效率很高如[<A/>,<B/>,<C/>] => [<A/>,<B/>] 或者[<A/>,<B/>] => [<A/>,<B/>,<C/>]只需增加或删除尾部的<C/>即可,但对于头部的插入或者删除效率很差,需要替换所有的组件,所有的DOM都无法复用。

为此需要一种更高效的算法来尽可能的复用原有的组件,React通过key唯一的标记组件来实现原有组件尽可能的复用。

至此所有代码如下

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 createElement(type, props, ...args) {
  props = Object.assign({}, props);
  let children = [].concat(...args);
  props.children = children;
  return { type, props };
}


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

class DomComponent {
  constructor(element) {
    this.currentElement = element;
    this.renderedChildren = [];
    this.node = null;
  }
  getPublicInstance() {
    return this.node;
  }
  getHostNode() {
    return this.node;
  }
  unmount() {
    this.renderedChildren.forEach(child => child.unmount());
  }
  updateDomProperties(prevProps, nextProps) {
    const node = this.node;
    // 删除旧的attribute
    Object.keys(prevProps).forEach(propName => {
      if(propName !== 'children' && !nextProps.hasOwnProperty(propName)) {
        node.removeAttribute(propName);
      }
    })
    // 更新新的attribute
    Object.keys(nextProps).forEach(propName => {
      if(propName !== 'children') {
        node.setAttribute(propName, nextProps[propName])
      }
    })
  }
  updateChildren(prevProps, nextProps) {
    const prevChildren = prevProps.children;
    const nextChildren = nextProps.children;
    const prevRenderedChildren = this.renderedChildren;
    const nextRenderedChildren =  [];
    const operationQueue = [];
    for(let i=0;i< nextChildren.length;i++){
      const prevChild = prevRenderedChildren[i];
      // insert
      if(!prevChild){
        const nextChild = instantiateComponent(nextChildren[i]);
        const node = nextChild.mount();
        operationQueue.push({
          type: 'ADD',
          node
        })
        nextRenderedChildren.push(nextChild);
        continue;
      }
      const canUpdate = prevChildren[i].type === nextChildren[i].type;
      // replace
      if(!canUpdate){
        const prevNode = prevChild.getHostNode();
        prevChild.unmount();
        const nextChild = instantiateComponent(nextChildren[i]);
        const nextNode = nextChild.mount();
        console.log('prevNode:', prevNode);
        console.log('nextNode:', nextNode);
        operationQueue.push({
          type: 'REPLACE',
          prevNode,
          nextNode
        });
        nextRenderedChildren.push(nextChild);
        continue;
      }
      prevChild.receive(nextChildren[i]);
      nextRenderedChildren.push(prevChild);
    }
    // delete
    for(let j=nextChildren.length;j<prevChildren.length;j++){
      const prevChild = prevRenderedChildren[j];
      const node = prevChild.node;
      prevChild.unmount();
      operationQueue.push({
        type: 'REMOVE', node
      })
    }
    this.renderedChildren = nextRenderedChildren;

    // batch update DOM
    while(operationQueue.length > 0){
      const 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;
      }
    }
  }
  receive(nextElement) {
    const node = this.node;
    const preveElement = this.currentElement;
    const prevProps = preveElement.props;
    const nextProps = nextElement.props;
    this.currentElement = nextElement;

    this.updateDomProperties(prevProps, nextProps);
    this.updateChildren(prevProps, nextProps);
  }
  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;
  }
  getHostNode() {
    return this.node;
  }
  receive(element){
    this.currentElement = element;
    this.node.textContent = element;
  }
  unmount() {
    this.node = null;
  }
  mount() {
    const node = document.createTextNode(this.currentElement);
    this.node = node;
    return node;
  }
}

class CompositeComponent {
  constructor(element) {
    this.currentElement = element;
    this.publicInstance = null;
    this.renderedComponent = null;
  }
  getPublicInstance() {
    return this.publicInstance;
  }
  getHostNode() {
    return this.renderedComponent.getHostNode();
  }
  receive(nextElement) {
    const prevProps = this.currentElement.props;
    const publicInstance = this.publicInstance;
    const prevRenderedComponent = this.renderedComponent;
    const prevRenderedElement = prevRenderedComponent.currentElement;

    this.currentElement = nextElement;
    const type = nextElement.type;
    const nextProps = nextElement.props;

    let nextRenderedElement;
    if(isClass(type)) {
      hooks(publicInstance, 'componentWillUpdate', nextProps);
      publicInstance.props = nextProps;
      nextRenderedElement = publicInstance.render();
    } else if(typeof type === 'function') {
      nextRenderedElement = type(nextProps);
    }
    if(prevRenderedElement.type === nextRenderedElement.type) {
      prevRenderedComponent.receive(nextRenderedElement);
      hooks(publicInstance,'componentDidUpdate',prevProps);
      return
    }
    const prevNode = prevRenderedComponent.getHostNode();
    prevRenderedComponent.unmount();
    const nextRenderedComponent = instantiateComponent(nextRenderedElement);
    const nextNode = nextRenderedComponent.mount();
    this.renderedComponent = nextRenderedComponent;
    prevNode.parentNode.replaceChild(nextNode, prevNode);
    
  }
  unmount() {
    hooks(this.publicInstance, 'componentWillUnmount');
    this.renderedComponent.unmount();
  }
  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);
  if(typeof element.type === 'string') return new DomComponent(element);
  if(typeof element.type === 'function') return new CompositeComponent(element);
  throw new Error('wrong element type');
}
function mount(element) {
  const rootComponent = instantiateComponent(element);
  return rootComponent.mount();
}
function findDOMNode(instance){
  return instance.getHostNode();
}
function render(element, mountNode) {
  if(mountNode.firstChild) {
    const prevNode = mountNode.firstChild;
    const prevComponent = prevNode._internalInstance;
    const prevElement = prevComponent.currentElement;
    if(prevElement.type === element.type) {
      prevComponent.receive(element);
      return;
    }
    unmountComponentAtNode(mountNode);
  }
  var rootComponent = instantiateComponent(element); // top-level internal instance
  var node = rootComponent.mount(); // top-level node
  mountNode.appendChild(node);
  node._internalInstance = rootComponent;
  var publicInstance = rootComponent.getPublicInstance(); // top-level public instance
  return publicInstance;
}
function unmountComponentAtNode(mountNode) {
  var node = mountNode.firstChild;
  var rootComponent = node._internalInstance; // 读取 internal instance

  rootComponent.unmount();
  mountNode.innerHTML = '';
}

const React = {
  render,
  unmountComponentAtNode
}

// test example
class Link extends Component {
  componentWillMount() {
    console.log('Link will Mount');
  }
  componentWillUnmount() {
    console.log('Link will Unmount');
  }
  componentWillUpdate() {
    console.log('Link will update')
  }
  componentDidUpdate() {
    console.log('Link Did update')
  }
  render() {
    const { children } = this.props
    return (
      <a href="http://www.baidu.com">{children}</a>
    )
  }
}
function Button(props) {
  return (
    <button class="btn">{props.text}</button>
  )
}
class App extends Component {
  componentWillMount() {
    console.log('App will Mount');
  }
  componentWillUnmount() {
    console.log('App will Unmount');
  }
  componentWillUpdate() {
    console.log('App will update')
  }
  componentDidUpdate() {
    console.log('App Did update')
  }
  render() {
    const { content } = this.props;
    if(content === 'toutiao'){
      return (
        <div class="container">
          <Link> { content}</Link>
          <Link> { content}</Link>
        </div>
      )
    }
    return (
      <div class="container">
        <Button text="this is a button" />
        <Link>{content}</Link>
      </div>
    )
  }
}
const mountNode = document.querySelector('#root');
React.render(<App content="baidu"/>, mountNode);
setTimeout(() => {
  React.render(<App content="toutiao"/>, mountNode);
}, 1000)