Diy React

opt_bbt發表於2018-10-21

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的簡單的實現。是同一位作者所寫。

參考文章

相關文章