React 原始碼學習(一):HTML 元素渲染

Zongzi發表於2019-04-01

閱讀原始碼成了今年的學習目標之一,在選擇 Vue 和 React 之間,我想先閱讀 React 。 在考慮到讀哪個版本的時候,我想先接觸到原始碼早期的思想可能會更輕鬆一些,最終我選擇閱讀 0.3-stable 。 那麼接下來,我將從幾個方面來解讀這個版本的原始碼。

  1. React 原始碼學習(一):HTML 元素渲染
  2. React 原始碼學習(二):HTML 子元素渲染
  3. React 原始碼學習(三):CSS 樣式及 DOM 屬性
  4. React 原始碼學習(四):事務機制
  5. React 原始碼學習(五):事件機制
  6. React 原始碼學習(六):元件渲染
  7. React 原始碼學習(七):生命週期
  8. React 原始碼學習(八):元件更新

React.DOM.*

直接步入正題,在官方的例子中可以看到 render 函式會返回類似這樣一段程式碼:

// #examples
return React.DOM.h1(null, 'Zong is learning the source code of React.')
複製程式碼

使用 type="text/jsx" 的形式編寫:

/** @jsx React.DOM */
// #examples
return <h1>Zong is learning the source code of React.</h1>
複製程式碼

JSXTransformer.js 會將 type="text/jsx" 的形式轉換成 React.DOM.h1 的函式形式。

結合 React.renderComponent<h1> 標籤最終渲染在指定的元素下,這段程式碼最終渲染至 DOM 下可能如下:

<h1 id=".reactRoot[0]">Zong is learning the source code of React.</h1>
複製程式碼

如何實現 HTML 元素的渲染

那麼, React 是如何實現 HTML 元素的渲染呢?

工廠函式 objMapKeyVal.js

objMapKeyVal 是個工廠函式,他最終會返回一個“鍵”與 obj 對應的物件“值”則是 func 的執行結果。

// utils/objMapKeyVal.js
function objMapKeyVal(obj, func, context) {
  if (!obj) {
    return null;
  }
  var i = 0;
  var ret = {};
  for (var key in obj) {
    if (obj.hasOwnProperty(key)) {
      ret[key] = func.call(context, key, obj[key], i++);
    }
  }
  return ret;
}
複製程式碼

建立 React.DOM.* 方法

// core/ReactDOM.js
/**
 * 用於建立 DOM 元件的類,其原型連線 ReactNativeComponent
 */
function createDOMComponentClass(tag, omitClose) {
  var Constructor = function(initialProps, children) {
    this.construct(initialProps, children);
  };

  Constructor.prototype = new ReactNativeComponent(tag, omitClose);
  Constructor.prototype.constructor = Constructor;

  return function(props, children) {
    return new Constructor(props, children);
  };
}

var ReactDOM = objMapKeyVal({
  // ...
  // Danger: this gets monkeypatched! See ReactDOMForm for more info.
  form: false,
  img: true,
  // ...
}, createDOMComponentClass);
複製程式碼

ReactDOM 物件中的“鍵”並非包含目前所有的 HTML 元素,若你需要加入新的 HTML 元素,在此物件中新增即可。但,若你需要同時支援 type="text/jsx" 的編寫形式,則同時需要在 JSXTransformer.js 進行新增,以實現轉換。

在經過 objMapKeyVal 工廠函式的執行後, ReactDOM 得到的值已經並非是之前的布林值,鍵值以 tag, omitClose 的形式作為引數供 ReactNativeComponent 進行例項化了。返回的則是接受 props, children 引數的函式,用於例項化這個 Constructor

所以例子中是以 props = null, children = 'Zong is learning the source code of React.' 的形式來接受的引數,並例項化 Constructor

原型 React 原生元件

當然,這裡同樣需要提到,ReactNativeComponent 例項化時操作了什麼:

// core/ReactNativeComponent.js
function ReactNativeComponent(tag, omitClose) {
  this._tagOpen = '<' + tag + ' ';
  this._tagClose = omitClose ? '' : '</' + tag + '>';
  this.tagName = tag.toUpperCase();
}
複製程式碼

比如,鍵值對 form: false 返回的物件為:

ReactNativeComponent {
  "_tagOpen": "<form ",
  "_tagClose": "</form>",
  "tagName": "FORM",
}
複製程式碼

工具函式 - 混合

就這麼點東西嗎?當然不是,你需要注意到 ReactNativeComponent.js 末端的幾行程式碼:

// utils/mixInto.js
/**
 * Simply copies properties to the prototype.
 */
var mixInto = function(constructor, methodBag) {
  var methodName;
  for (methodName in methodBag) {
    if (!methodBag.hasOwnProperty(methodName)) {
      continue;
    }
    constructor.prototype[methodName] = methodBag[methodName];
  }
};
複製程式碼

這裡依次將 3 個物件混合到 ReactNativeComponent.prototype

// core/ReactNativeComponent.js
mixInto(ReactNativeComponent, ReactComponent.Mixin);
mixInto(ReactNativeComponent, ReactNativeComponent.Mixin);
mixInto(ReactNativeComponent, ReactMultiChild.Mixin);
複製程式碼

mixInto 方法的功能其實就是 Object.assign 的功能,上方的程式碼也可以這樣寫: Object.assign(ReactNativeComponent.prototype, ReactComponent.Mixin)

那麼,接下來我們來看看 this.construct(initialProps, children) 這裡到底做了什麼,逆向尋找發現這個方法在 ReactComponent.Mixin 中。

// core/ReactComponent.js
var ReactComponent = {
  Mixin: {
    construct: function(initialProps, children) {
      this.props = initialProps || {};
      if (typeof children !== 'undefined') {
        this.props.children = children;
      }
      // Record the component responsible for creating this component.
      this.props[OWNER] = ReactCurrentOwner.current;
      // All components start unmounted.
      this._lifeCycleState = ComponentLifeCycle.UNMOUNTED;
    },
  }
}
複製程式碼

將 React 元件例項掛載至 DOM

到此為止,我們改如何將這個例項渲染到 DOM 上呢?我們來看到這段程式碼:

// #examples
React.renderComponent(
  React.DOM.h1(null, 'Zong is learning the source code of React.'),
  document.getElementById('container')
);
複製程式碼

註冊 React 例項

React.renderComponent 方法將例項渲染到 DOM 上的,我們來看看這個方法究竟做了些什麼,此次先忽略其他邏輯(元件更新/事件註冊):

// core/ReactMount.js
// 用於統計公共掛載的數量
var globalMountPointCounter = 0;

/** Mapping from reactRoot DOM ID to React component instance. */
// React 元件例項基於 ReactRootID 的對映
var instanceByReactRootID = {};

/** Mapping from reactRoot DOM ID to `container` nodes. */
// container 基於 ReactRootID 的對映
var containersByReactRootID = {};

/**
 * @param {DOMElement} container DOM element that may contain a React component.
 * @return {?string} A "reactRoot" ID, if a React component is rendered.
 */
function getReactRootID(container) {
  return container.firstChild && container.firstChild.id;
}

var ReactMount = {
  renderComponent: function(nextComponent, container) {
    // 上面邏輯包含元件更新及事件註冊
    // 獲得/生成 reactRootID
    var reactRootID = ReactMount.registerContainer(container);
    // 對映 React 元件
    instanceByReactRootID[reactRootID] = nextComponent;
    // 呼叫元件自身方法
    nextComponent.mountComponentIntoNode(reactRootID, container);
    return nextComponent;
  },
  registerContainer: function(container) {
    // 獲得 reactRootID
    var reactRootID = getReactRootID(container);
    if (reactRootID) {
      // 若存在的情況下確認 ID 是否為 "reactRoot" ID,否則返回 null
      // If one exists, make sure it is a valid "reactRoot" ID.
      reactRootID = ReactInstanceHandles.getReactRootIDFromNodeID(reactRootID);
    }
    if (!reactRootID) {
      // No valid "reactRoot" ID found, create one.
      // 若 ID 不存在,則返回新的 ID
      reactRootID = ReactInstanceHandles.getReactRootID(
        globalMountPointCounter++
      );
    }
    // 對映 container
    containersByReactRootID[reactRootID] = container;
    return reactRootID;
  },
}
複製程式碼
// core/ReactInstanceHandles.js
var ReactInstanceHandles = {
  getReactRootID: function(mountPointCount) {
    return '.reactRoot[' + mountPointCount + ']';
  },
  getReactRootIDFromNodeID: function(id) {
    var regexResult = /\.reactRoot\[[^\]]+\]/.exec(id);
    return regexResult && regexResult[0];
  },
}
複製程式碼

將元件掛載至 DOM 節點方法

// core/ReactComponent.js
var ReactComponent = {
  Mixin: {
    mountComponent: function(rootID, transaction) {
      // 元件生命週期和 ref 相關暫不做解讀

      this._rootNodeID = rootID;
    },
    mountComponentIntoNode: function(rootID, container) {
      // 這裡牽扯到 React 事務,後續再做解讀
      var transaction = ReactComponent.ReactReconcileTransaction.getPooled();
      // 本次討論,你只需簡單理解為:
      // this._mountComponentIntoNode.call(this, rootID, container, transaction)
      transaction.perform(
        this._mountComponentIntoNode,
        this,
        rootID,
        container,
        transaction
      );
      ReactComponent.ReactReconcileTransaction.release(transaction);
    },
    _mountComponentIntoNode: function(rootID, container, transaction) {
      // 這裡包含這一些時間計算,不做解讀
      var renderStart = Date.now();
      // markup 即為返回的 HTML 標記
      // this.mountComponent 則為 ReactNativeComponent.Mixin.mountComponent
      var markup = this.mountComponent(rootID, transaction);
      ReactMount.totalInstantiationTime += (Date.now() - renderStart);

      var injectionStart = Date.now();
      // Asynchronously inject markup by ensuring that the container is not in
      // the document when settings its `innerHTML`.
      // 以下程式碼是用來判斷 markup 需要被如何插入值 DOM 節點。
      var parent = container.parentNode;
      if (parent) {
        var next = container.nextSibling;
        parent.removeChild(container);
        container.innerHTML = markup;
        if (next) {
          parent.insertBefore(container, next);
        } else {
          parent.appendChild(container);
        }
      } else {
        container.innerHTML = markup;
      }
      ReactMount.totalInjectionTime += (Date.now() - injectionStart);
    },
  }
}
複製程式碼

生成 Markup 標記

// core/ReactNativeComponent.js
// For quickly matching children type, to test if can be treated as content.
var CONTENT_TYPES = {'string': true, 'number': true};

ReactNativeComponent.Mixin = {
  mountComponent: function(rootID, transaction) {
    ReactComponent.Mixin.mountComponent.call(this, rootID, transaction);
    // 引數校驗不做解讀
    assertValidProps(this.props);
    // 返回的就是 HTML 標記
    return (
      this._createOpenTagMarkup() +
      this._createContentMarkup(transaction) +
      this._tagClose
    );
  },
  _createOpenTagMarkup: function() {
    var ret = this._tagOpen;
    // 暫時不解讀(事件註冊/ CSS 樣式/ DOM 屬性)

    return ret + ' id="' + this._rootNodeID + '">';
  },
  _createContentMarkup: function(transaction) {
    // 這裡忽略 dangerouslySetInnerHTML 的情況
    var contentToUse = this.props.content != null ? this.props.content :
      CONTENT_TYPES[typeof this.props.children] ? this.props.children : null;
    var childrenToUse = contentToUse != null ? null : this.props.children;
    if (contentToUse != null) {
      // content == null 並且 children 為 string / number 的情況
      // 直接返回 Zong is learning the source code of React.
      return escapeTextForBrowser(contentToUse);
    } else if (childrenToUse != null) {
      // 多個 children 的情況
      return this.mountMultiChild(
        flattenChildren(childrenToUse),
        transaction
      );
    }
    return '';
  },
}
複製程式碼
// utils/escapeTextForBrowser.js
var ESCAPE_LOOKUP = {
  "&": "&amp;",
  ">": "&gt;",
  "<": "&lt;",
  "\"": "&quot;",
  "'": "&#x27;",
  "/": "&#x2f;"
};

function escaper(match) {
  return ESCAPE_LOOKUP[match];
}

var escapeTextForBrowser = function (text) {
  var type = typeof text;
  var invalid = type === 'object';
  if (text === '' || invalid) {
    return '';
  } else {
    if (type === 'string') {
      return text.replace(/[&><"'\/]/g, escaper);
    } else {
      return (''+text).replace(/[&><"'\/]/g, escaper);
    }
  }
};
複製程式碼

那麼到此為止,實現 HTML 元素渲染功能。

相關文章