xiaosong
5/9/2018 - 6:42 AM

21天 react

21天 react

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)