react簡易實現(1) 組件的掛載

react常見的元素類型分爲3種:html

  • 文本類型
  • DOM類型
  • 自定義組件類型

不一樣的類型,確定有不一樣的渲染和更新邏輯前端

文本類型對應ReactTextComponent類,node

DOM類型對應ReactDomComponent類react

自定義組件類型對應ReactCompositeComponent,類web


我以前看到過一篇很是通俗易懂的文章,關於這三種react元素類型的源碼解析,代碼以下。瀏覽器

實現一個 hello React!的渲染

看以下代碼:bash

// js
React.render('hello React!',document.getElementById("root"))

// html
<div id="root"></div>

// 生成代碼
<div id="root">
    <span data-reactid="0">hello React!</span>
</div>複製代碼

針對上面代碼的具體實現dom

/**
 * component 類
 * 文本類型
 * @param {*} text 文本內容
 */
function ReactDOMTextComponent(text) {
  // 存下當前的字符串
  this._currentElement = "" + text;
  // 用來標識當前component
  this._rootNodeID = null;
}

/**
 * component 類 裝載方法,生成 dom 結構
 * @param {number} rootID 元素id
 * @return {string} 返回dom
 */
ReactDOMTextComponent.prototype.mountComponent = function(rootID) {
  this._rootNodeID = rootID;
  return (
    '<span data-reactid="' + rootID + '">' + this._currentElement + "</span>"
  );
};

/**
 * 根據元素類型實例化一個具體的component
 * @param {*} node ReactElement
 * @return {*} 返回一個具體的component實例
 */
function instantiateReactComponent(node) {
  //文本節點的狀況
  if (typeof node === "string" || typeof node === "number") {
    return new ReactDOMTextComponent(node);
  }
}

const React = {
 nextReactRootIndex: 0,

 /**
  * 接收一個React元素,和一個dom節點
  * @param {*} element React元素
  * @param {*} container 負責裝載的dom
  */
  render: function(element, container) {
    // 實例化組件
    var componentInstance = instantiateReactComponent(element);
    // 組件完成dom裝載
    var markup = componentInstance.mountComponent(React.nextReactRootIndex++);
    // 將裝載好的 dom 放入 container 中
    $(container).html(markup);
    $(document).trigger("mountReady");
  }
};複製代碼

這裏代碼分爲三個部分:函數

  • 1 React.render 做爲入口接受一個 React 元素和遊覽器中的 dom 負責調用渲染,nextReactRootIndex 爲每一個 component 的惟一標識
  • 2 引入 component 類的概念,ReactDOMTextComponent 是一個 component 類定義。ReactDOMTextComponent 針對於文本節點進行處理。而且在 ReactDOMTextComponent 的原型上實現了 mountComponent 方法,用於對組件的渲染,返回組件的 dom 結構。固然 component 還具備更新和刪除操做,這裏將在後續講解。
  • 3 instantiateReactComponent 用來根據 element 的類型(如今只有一種 string 類型),返回一個 component 的實例。其實就是個類工廠。

在這裏咱們把邏輯分爲幾個部分,渲染邏輯則由 component 內部定義,React.render 負責調度整個流程,在調用 instantiateReactComponent 生成一個對應 component 類型的實例對象,再調用對象的 mountComponent 返回 dom,最後再寫到 container 節點中優化

虛擬 dom

虛擬 dom 無疑是 React 的核心概念,在代碼中咱們會使用 React.createElement 來建立一個虛擬 dom 元素。

虛擬 dom 分爲兩種一種是遊覽器自帶的基本元素好比 div,還有一種是自定義元素(文本節點不算虛擬 dom)

虛擬節點的使用方式

// 綁定事件監聽方法
function sayHello(){
    alert('hello!')
}
var element = React.createElement('div',{id:'jason',onclick:hello},'click me')
React.render(element,document.getElementById("root"))

// 最終生成的html

<div data-reactid="0" id="jason">
    <span data-reactid="0.0">click me</span>
</div>複製代碼

咱們使用 React.createElement 來建立一個虛擬 dom 元素,如下是簡易實現

/**
 * ReactElement 就是虛擬節點的概念
 * @param {*} key 虛擬節點的惟一標識,後期能夠進行優化
 * @param {*} type 虛擬節點類型,type多是字符串('div', 'span'),也多是一個functionfunction時爲一個自定義組件
 * @param {*} props 虛擬節點的屬性
 */
function ReactElement(type, key, props) {
  this.type = type;
  this.key = key;
  this.props = props;
}

const React = {
  nextReactRootIndex: 0,
  /**
   * @param {*} type 元素的 component 類型
   * @param {*} config 元素配置
   * @param {*} children 元素的子元素
   */
  createElement: function(type, config, children) {
    var props = {};
    var propName;
    config = config || {};

    var key = config.key || null;

    for (propName in config) {
      if (config.hasOwnProperty(propName) && propName !== "key") {
        props[propName] = config[propName];
      }
    }

    var childrenLength = arguments.length - 2;
    if (childrenLength === 1) {
      props.children = Array.isArray(children) ? children : [children];
    } else if (childrenLength > 1) {
      var childArray = [];
      for (var i = 0; i < childrenLength; i++) {
        childArray[i] = arguments[i + 2];
      }
      props.children = childArray;
    }
    return new ReactElement(type, key, props);
  },

  /**
   * 自行添加上文中的render方法
   */
};複製代碼

createElement 方法對傳入的參數作了一些處理,最終會返回一個 ReactElement 虛擬元素實例,key 的定義能夠提升更新時的效率

有了虛擬元素實例,咱們須要改造一下 instantiateReactComponent 方法

/**
 * 根據元素類型實例化一個具體的component
 * @param {*} node ReactElement
 * @return {*} 返回一個具體的component實例
 */
function instantiateReactComponent(node) {
  //文本節點的狀況
  if (typeof node === "string" || typeof node === "number") {
    return new ReactDOMTextComponent(node);
  }
  //瀏覽器默認節點的狀況
  if (typeof node === "object" && typeof node.type === "string") {
    //注意這裏,使用了一種新的component
    return new ReactDOMComponent(node);
  }
}複製代碼

咱們增長了一個判斷,這樣當 render 的不是文本而是瀏覽器的基本元素時。咱們使用另一種 component 來處理它渲染時應該返回的內容。這裏就體現了工廠方法 instantiateReactComponent 的好處了,無論來了什麼類型的 node,均可以負責生產出一個負責渲染的 component 實例。這樣 render 徹底不須要作任何修改,只須要再作一種對應的 component 類型(這裏是 ReactDOMComponent)就好了。

ReactDOMComponent的具體實現

/**
 * component 類
 * react 基礎標籤類型,相似與html中的('div','span' 等)
 * @param {*} element 基礎元素
 */
function ReactDOMComponent(element) {
  // 存下當前的element對象引用
  this._currentElement = element;
  this._rootNodeID = null;
}

/**
 * component 類 裝載方法
 * @param {*} rootID 元素id
 * @param {string} 返回dom
 */
ReactDOMComponent.prototype.mountComponent = function(rootID) {
  this._rootNodeID = rootID;
  var props = this._currentElement.props;

  // 外層標籤
  var tagOpen = "<" + this._currentElement.type;
  var tagClose = "</" + this._currentElement.type + ">";

  // 加上reactid標識
  tagOpen += " data-reactid=" + this._rootNodeID;

  // 拼接標籤屬性
  for (var propKey in props) {
    // 屬性爲綁定事件
    if (/^on[A-Za-z]/.test(propKey)) {
      var eventType = propKey.replace("on", "");
      // 對當前節點添加事件代理
      $(document).delegate(
        '[data-reactid="' + this._rootNodeID + '"]',
        eventType + "." + this._rootNodeID,
        props[propKey]
      );
    }

    // 對於props 上的children和事件屬性不作處理
    if (
      props[propKey] &&
      propKey != "children" &&
      !/^on[A-Za-z]/.test(propKey)
    ) {
      tagOpen += " " + propKey + "=" + props[propKey];
    }
  }
  // 渲染子節點dom
  var content = "";
  var children = props.children || [];

  var childrenInstances = []; // 保存子節點component 實例
  var that = this;

  children.forEach((child, key) => {
    var childComponentInstance = instantiateReactComponent(child);
    // 爲子節點添加標記
    childComponentInstance._mountIndex = key;
    childrenInstances.push(childComponentInstance);
    var curRootId = that._rootNodeID + "." + key;

    // 獲得子節點的渲染內容
    var childMarkup = childComponentInstance.mountComponent(curRootId);

    // 拼接在一塊兒
    content += " " + childMarkup;
  });

  // 保存component 實例
  this._renderedChildren = childrenInstances;

  // 拼出整個html內容
  return tagOpen + ">" + content + tagClose;
};複製代碼

對於虛擬 dom 的渲染邏輯,本質上仍是個遞歸渲染的東西,reactElement 會遞歸渲染本身的子節點。能夠看到咱們經過 instantiateReactComponent 屏蔽了子節點的差別,只須要使用不一樣的 component 類,這樣都能保證經過 mountComponent 最終拿到渲染後的內容。

另外這邊的事件也要說下,能夠在傳遞 props 的時候傳入{onClick:function(){}}這樣的參數,這樣就會在當前元素上添加事件,代理到 document。因爲 React 自己全是在寫 js,因此監聽的函數的傳遞變得特別簡單。

這裏不少東西沒有考慮,這裏爲了保持簡單就再也不擴展了,另外 React 的事件處理其實很複雜,實現了一套標準的 w3c 事件。這裏偷懶直接使用 jQuery 的事件代理到 document 上了。

自定義元素的實現
隨着前端技術的發展瀏覽器的那些基本元素已經知足不了咱們的需求了,若是你對 web components 有必定的瞭解,就會知道人們一直在嘗試擴展一些本身的標記。

React 經過虛擬 dom 作到了相似的功能,還記得咱們上面 node.type 只是個簡單的字符串,若是是個類呢?若是這個類剛好還有本身的生命週期管理,那擴展性就很高了。

React 中使用自定義元素

var CompositeComponent = React.createClass({
  getInitialState: function() {
    return {
      count: 0
    };
  },
  componentWillMount: function() {
    console.log("聲明週期: " + "componentWillMount");
  },
  componentDidMount: function() {
    console.log("聲明週期: " + "componentDidMount");
  },
  onChange: function(e) {
    var count = ++this.state.count;
    this.setState({
      count: count
    });
  },
  render: function() {
    const count = this.state.count;
    var h3 = React.createElement(
      "h3",
      { onclick: this.onChange.bind(this), class: "h3" },
      `click me ${count}`
    );
    var children = [h3];

    return React.createElement("div", null, children);
  }
});

var CompositeElement = React.createElement(CompositeComponent);

var root = document.getElementById("container");

React.render(CompositeElement, root);複製代碼

React.createElement接受的再也不是字符串,而是一個 class。
React.createClass 生成一個自定義標記類,帶有基本的生命週期:

  • getInitialState 獲取最初的屬性值 this.state
  • componentWillmount 在組件準備渲染時調用
  • componentDidMount 在組件渲染完成後調用

React.createClass 的實現

/**
 * 全部自定義組件的超類
 * @function render全部自定義組件都有該方法
 */
function ReactClass() {}

ReactClass.prototype.render = function() {};

/**
 * 更新
 * @param {*} newState 新狀態
 */
ReactClass.prototype.setState = function(newState) {
  // 拿到ReactCompositeComponent的實例
  this._reactInternalInstance.receiveComponent(null, newState);
};

const React = {
  nextReactRootIndex: 0,

  /**
   * 建立 ReactClass
   * @param {*} spec 傳入的對象
   */
  createClass: function(spec) {
    var Constructor = function(props) {
      this.props = props;
      this.state = this.getInitialState ? this.getInitialState() : null;
    };

    Constructor.prototype = new ReactClass();
    Constructor.prototype.constructor = Constructor;

    Object.assign(Constructor.prototype, spec);
    return Constructor;
  },

  /**
   * 本身上文的createElement方法
   */

  /**
   * 本身上文的render方法
   */
};複製代碼

這裏 createClass 生成了一個繼承 ReactClass 的子類,在構造函數裏調用 this.getInitialState 得到最初的 state。

爲了演示方便,咱們這邊的 ReactClass 至關簡單,實際上原始的代碼處理了不少東西,好比類的 mixin 的組合繼承支持,好比 componentDidMount 等能夠定義屢次,須要合併調用等等,有興趣的去翻源碼吧,不是本文的主要目的,這裏就不詳細展開了。

看看咱們上面的兩種類型就知道,咱們是時候爲自定義元素也提供一個 component 類了,在那個類裏咱們會實例化 ReactClass,而且管理生命週期,還有父子組件依賴。

首先改造 instantiateReactComponent

/**
 * 根據元素類型實例化一個具體的component
 * @param {*} node ReactElement
 * @return {*} 返回一個具體的component實例
 */
function instantiateReactComponent(node) {
  // 文本節點的狀況
  if (typeof node === "string" || typeof node === "number") {
    return new ReactDOMTextComponent(node);
  }
  //瀏覽器默認節點的狀況
  if (typeof node === "object" && typeof node.type === "string") {
    // 注意這裏,使用了一種新的component
    return new ReactDOMComponent(node);
  }
  // 自定義的元素節點
  if (typeof node === "object" && typeof node.type === "function") {
    // 注意這裏,使用新的component,專門針對自定義元素
    return new ReactCompositeComponent(node);
  }
}複製代碼

這裏咱們添加了一個判斷,處理自定義類型的 component

ReactCompositeComponent 的具體實現以下

/**
 * component 類
 * 複合組件類型
 * @param {*} element 元素
 */
function ReactCompositeComponent(element) {
  // 存放元素element對象
  this._currentElement = element;
  // 存放惟一標識
  this._rootNodeID = null;
  // 存放對應的ReactClass的實例
  this._instance = null;
}

/**
 * component 類 裝載方法
 * @param {*} rootID 元素id
 * @param {string} 返回dom
 */
ReactCompositeComponent.prototype.mountComponent = function(rootID) {
  this._rootNodeID = rootID;

  // 當前元素屬性
  var publicProps = this._currentElement.props;
  // 對應的ReactClass
  var ReactClass = this._currentElement.type;

  var inst = new ReactClass(publicProps);
  this._instance = inst;

  // 保留對當前 component的引用
  inst._reactInternalInstance = this;

  if (inst.componentWillMount) {
    // 生命週期
    inst.componentWillMount();
    //這裏在原始的 reactjs 其實還有一層處理,就是  componentWillMount 調用 setstate,不會觸發 rerender 而是自動提早合併,這裏爲了保持簡單,就略去了
  }

  // 調用 ReactClass 實例的render 方法,返回一個element或者文本節點
  var renderedElement = this._instance.render();
  var renderedComponentInstance = instantiateReactComponent(renderedElement);
  this._renderedComponent = renderedComponentInstance; //存起來留做後用

  var renderedMarkup = renderedComponentInstance.mountComponent(
    this._rootNodeID
  );

  // dom 裝載到html 後調用生命週期
  $(document).on("mountReady", function() {
    inst.componentDidMount && inst.componentDidMount();
  });

  return renderedMarkup;
};複製代碼

自定義元素自己不負責具體的內容,他更多的是負責生命週期。具體的內容是由它的 render 方法返回的虛擬節點來負責渲染的。

本質上也是遞歸的去渲染內容的過程。同時由於這種遞歸的特性,父組件的 componentWillMount 必定在某個子組件的 componentWillMount 以前調用,而父組件的 componentDidMount 確定在子組件以後,由於監聽 mountReady 事件,確定是子組件先監聽的。

須要注意的是自定義元素並不會處理咱們 createElement 時傳入的子節點,它只會處理本身 render 返回的節點做爲本身的子節點。不過咱們在 render 時可使用 this.props.children 拿到那些傳入的子節點,能夠本身處理。其實有點相似 web components 裏面的 shadow dom 的做用。

相關文章
相關標籤/搜索