Diy React

babel

在runtime时, babel转译器将对每一个节点都执行在编译注释(Pragma)中声明的函数。 例如:

before babel: (the code you write)

/** @jsx h */
let foo = <div id="foo">Hello!</div>;
复制代码

after babel: (the code you run)

var foo = h('div', {id:"foo"}, 'Hello!');  
复制代码

也可以在浏览器里打印看一下结果:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
  <script type="text/jsx" data-presets="es2016,react">
    /** @jsx h */
    let foo = <div id="foo">Hello!</div>;
    
    const h = (type, attributes, ...args) => {
      let children = args.length ? [].concat(...args) : null;
      return { nodeName, attributes, children };
    }

    console.log(foo);
    // foo: {
    //   attributes: {id: "foo"}
    //   children: ["Hello!"]
    //   nodeName: "div"
    // }
  </script>
</body>
</html>
复制代码

createElement

将jsx中的element转换成 plain object

function createElement(type, config, ...args) {
  const props = Object.assign({}, config);
  const hasChildren = args.length > 0;
  const rawChildren = hasChildren ? [].concat(...args) : [];
  props.children = rawChildren
    .filter(c => c != null && c !== false)
    .map(c => c instanceof Object ? c : createTextElement(c));
  return { type, props };
}

function createTextElement(value) {
  return createElement(TEXT_ELEMENT, { nodeValue: value });
}
复制代码

这里对text node特殊处理了一下,使其不那么特殊, 如此后面处理时省略了许多判断

instance

每个element(jsx转换而来的)对应一个instance,instance包含了dom, element, childInstances。 如果是custom component 则还有一个publicInstance,是组件的实例.

function instantiate(element) {
  const { type, props } = element;
  const isDomElement = typeof type === "string";

  // 这里对custom component与built-in component分别做处理
  if (isDomElement) {
    const isTextElement = type === TEXT_ELEMENT;
    const dom = isTextElement
      ? document.createTextNode("")
      : document.createElement(type);

    // updateDomProperties 作用是更新dom上的属性,比如class、id、或者删除新增事件监听函数等
    updateDomProperties(dom, [], props);

    // 处理子组件
    const childElements = props.children || [];
    const childInstances = childElements.map(instantiate);
    const childDoms = childInstances.map(childInstance => childInstance.dom);

    // 将子组件的实例插入到dom上
    childDoms.forEach(childDom => dom.appendChild(childDom));

    const instance = { dom, element, childInstances };
    return instance;
  } else {
    const instance = {};
    // 或者custom-component 实例
    const publicInstance = createPublicInstance(element, instance);

    // 调用render方法,获得子组件,再获取child instance.
    // 注意这里是childInstance而不是childInstances,因为custom component只能一个子组件,不能是数组
    const childElement = publicInstance.render();
    const childInstance = instantiate(childElement);
    const dom = childInstance.dom;

    Object.assign(instance, { dom, element, childInstance, publicInstance });
    return instance;
  }
}
复制代码

createPublicInstance

function createPublicInstance(element, internalInstance) {
  const { type, props } = element;
  const publicInstance = new type(props);

  publicInstance.__internalInstance = internalInstance;
  return publicInstance;
}
复制代码

这里的internalInstance其实就是 instance,之所以需要将其绑定到组件实例上是因为 在自定义组件中update需要用到,这里可以先看一下Component的setState实现

setState(partialState) {
  this.state = Object.assign({}, this.state, partialState);
  updateInstance(this.__internalInstance);
}

function updateInstance(internalInstance) {
  const parentDom = internalInstance.dom.parentNode;
  const element = internalInstance.element;
  reconcile(parentDom, internalInstance, element);
}
复制代码

reconciliation 对组件进行 diff

下面instance是当前的状态,而element是新的状态,将这两者进行diff,就可以得出如何更新dom节点

function reconcile(parentDom, instance, element) {
  if (instance == null) {
    // 不存在instance则创建一个
    const newInstance = instantiate(element);
    parentDom.appendChild(newInstance.dom);
    return newInstance;
  } else if (element == null) {
    // 不存在element说明该element已经被删除
    parentDom.removeChild(instance.dom);
    return null;
  } else if (instance.element.type !== element.type) {
    // 如果新旧element类型不同则替换
    const newInstance = instantiate(element);
    parentDom.replaceChild(newInstance.dom, instance.dom);
    return newInstance;
  } else if (typeof element.type === "string") {
    // build-in component 更新属性
    updateDomProperties(instance.dom, instance.element.props, element.props);
    instance.childInstances = reconcileChildren(instance, element);
    instance.element = element;
    return instance;
  } else {
    // 更新custom component的属性
    instance.publicInstance.props = element.props;
    const childElement = instance.publicInstance.render();
    const oldChildInstance = instance.childInstance;
    const childInstance = reconcile(
      parentDom,
      oldChildInstance,
      childElement
    );
    instance.dom = childInstance.dom;
    instance.childInstance = childInstance;
    instance.element = element;
    return instance;
  }
}

function reconcileChildren(instance, element) {
  const dom = instance.dom;
  const childInstances = instance.childInstances;
  const nextChildElements = element.props.children || [];
  const newChildInstances = [];
  const count = Math.max(childInstances.length, nextChildElements.length);
  for (let i = 0; i < count; i++) {
    const childInstance = childInstances[i];
    const childElement = nextChildElements[i];
    const newChildInstance = reconcile(dom, childInstance, childElement);
    newChildInstances.push(newChildInstance);
  }
  // 过滤掉已经被删除的element
  return newChildInstances.filter(instance => instance != null);
}
复制代码

Component

由于之前已经讲过了setState,其他部分就很简单了

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

  setState(partialState) {
    this.state = Object.assign({}, this.state, partialState);
    updateInstance(this.__internalInstance);
  }
}
复制代码

render

function render(element, container) {
  reconcile(container, null, element);
}
复制代码

一个完整的例子

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <div id="root"></div>
  <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
  <script type="text/jsx" data-presets="es2016,react">
    /** @jsx Didact.createElement */
    
    const Didact = importFromBelow();
    class App extends Didact.Component {

      constructor(props) {
        super(props);
        this.state = { count: 0 };
      }

      add = () => {
        this.setState({
          count: this.state.count + 1,
        })
      }

      render() {
        return (
          <div>
            <h1>count: {this.state.count}</h1>
            <Button onClick={this.add} />
          </div>
        );
      }
    }
    
    class Button extends Didact.Component {
      render() {
        const { onClick } = this.props;

        return (
          <button onClick={onClick}>add</button>
        );
      }
    }
    
    Didact.render(<App />, document.getElementById("root"));

    function importFromBelow() {
      let rootInstance = null;
      const TEXT_ELEMENT = "TEXT_ELEMENT";
    
      function createElement(type, config, ...args) {
        const props = Object.assign({}, config);
        const hasChildren = args.length > 0;
        const rawChildren = hasChildren ? [].concat(...args) : [];
        props.children = rawChildren
          .filter(c => c != null && c !== false)
          .map(c => c instanceof Object ? c : createTextElement(c));
        return { type, props };
      }
    
      function createTextElement(value) {
        return createElement(TEXT_ELEMENT, { nodeValue: value });
      }
    
      function render(element, container) {
        const prevInstance = rootInstance;
        const nextInstance = reconcile(container, prevInstance, element);
        rootInstance = nextInstance;
      }
    
      function reconcile(parentDom, instance, element) {
        if (instance == null) {
          const newInstance = instantiate(element);
          parentDom.appendChild(newInstance.dom);
          return newInstance;
        } else if (element == null) {
          parentDom.removeChild(instance.dom);
          return null;
        } else if (instance.element.type !== element.type) {
          const newInstance = instantiate(element);
          parentDom.replaceChild(newInstance.dom, instance.dom);
          return newInstance;
        } else if (typeof element.type === "string") {
          updateDomProperties(instance.dom, instance.element.props, element.props);
          instance.childInstances = reconcileChildren(instance, element);
          instance.element = element;
          return instance;
        } else {
          instance.publicInstance.props = element.props;
          const childElement = instance.publicInstance.render();
          const oldChildInstance = instance.childInstance;
          const childInstance = reconcile(
            parentDom,
            oldChildInstance,
            childElement
          );
          instance.dom = childInstance.dom;
          instance.childInstance = childInstance;
          instance.element = element;
          return instance;
        }
      }
    
      function reconcileChildren(instance, element) {
        const dom = instance.dom;
        const childInstances = instance.childInstances;
        const nextChildElements = element.props.children || [];
        const newChildInstances = [];
        const count = Math.max(childInstances.length, nextChildElements.length);
        for (let i = 0; i < count; i++) {
          const childInstance = childInstances[i];
          const childElement = nextChildElements[i];
          const newChildInstance = reconcile(dom, childInstance, childElement);
          newChildInstances.push(newChildInstance);
        }
        return newChildInstances.filter(instance => instance != null);
      }
    
      function instantiate(element) {
        const { type, props } = element;
        const isDomElement = typeof type === "string";
    
        if (isDomElement) {
          const isTextElement = type === TEXT_ELEMENT;
          const dom = isTextElement
            ? document.createTextNode("")
            : document.createElement(type);
    
          updateDomProperties(dom, [], props);
    
          const childElements = props.children || [];
          const childInstances = childElements.map(instantiate);
          const childDoms = childInstances.map(childInstance => childInstance.dom);
          childDoms.forEach(childDom => dom.appendChild(childDom));

          const instance = { dom, element, childInstances };
          return instance;
        } else {
          const instance = {};
          const publicInstance = createPublicInstance(element, instance);
          const childElement = publicInstance.render();
          const childInstance = instantiate(childElement);
          const dom = childInstance.dom;
    
          Object.assign(instance, { dom, element, childInstance, publicInstance });
          return instance;
        }
      }
    
      function updateDomProperties(dom, prevProps, nextProps) {
        const isEvent = name => name.startsWith("on");
        const isAttribute = name => !isEvent(name) && name != "children";
    
        Object.keys(prevProps).filter(isEvent).forEach(name => {
          const eventType = name.toLowerCase().substring(2);
          dom.removeEventListener(eventType, prevProps[name]);
        });
    
        Object.keys(prevProps).filter(isAttribute).forEach(name => {
          dom[name] = null;
        });
    
        Object.keys(nextProps).filter(isAttribute).forEach(name => {
          dom[name] = nextProps[name];
        });
    
        Object.keys(nextProps).filter(isEvent).forEach(name => {
          const eventType = name.toLowerCase().substring(2);
          dom.addEventListener(eventType, nextProps[name]);
        });
      }
      function createPublicInstance(element, internalInstance) {
        const { type, props } = element;
        const publicInstance = new type(props);
        publicInstance.__internalInstance = internalInstance;
        return publicInstance;
      }
    
      class Component {
        constructor(props) {
          this.props = props;
          this.state = this.state || {};
        }
    
        setState(partialState) {
          this.state = Object.assign({}, this.state, partialState);
          updateInstance(this.__internalInstance);
        }
      }
    
      function updateInstance(internalInstance) {
        const parentDom = internalInstance.dom.parentNode;
        const element = internalInstance.element;
        reconcile(parentDom, internalInstance, element);
      }
    
      return {
        createElement,
        render,
        Component
      };
    }
	</script>
</body>
</html>
复制代码

总结

本文是对文章Didact: a DIY guide to build your own React 的总结。旨在帮助在阅读react源码之前先整体把握react的结构组成,帮助更好的阅读源码。由于react16大量更新了代码,引入了Fiber:Incremental reconciliation,下篇看一下fiber的简单的实现。是同一位作者所写。

参考文章

转载于:https://juejin.im/post/5bcc2f5e6fb9a05d382817b0

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值