React 源碼分析

寫在前面

React 開發一年多,最近仔細研究了 React 源碼,在這裏總結一下原理。React 源碼比較複雜不適合初學者去學習。因此本文經過實現一套簡易版的 React,使得理解原理更加容易(本文基於 React v15)。包括:html

  • React 的幾種組件以及首次渲染實現
  • React 更新機制的實現以及 React diff 算法

React 的代碼仍是很是複雜的,雖然這裏是一個簡化版本。可是仍是須要有不錯的面向對象思惟的。React 的核心主要有一下幾點。前端

  • 虛擬 dom 對象(Virtual DOM)
  • 虛擬 dom 差別化算法(diff algorithm)
  • 單向數據流
  • 組件聲明週期
  • 事件處理

本文代碼倉庫node

  • 直接在遊覽器中打開 main.html 中查看效果
  • 更改代碼請先執行執行npm i安裝依賴(使用了部分 es6 代碼)
  • 修改代碼後請執行npm run dev從新編譯代碼

實現一個 hello React!的渲染

看以下代碼:react

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

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

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

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

/**
 * 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");
  }
};
複製代碼

這裏代碼分爲三個部分:es6

  • 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 節點中github

虛擬 dom

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

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

虛擬節點的使用方式npm

// 綁定事件監聽方法
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 的做用。

初始化渲染的大體流程以下:

實現一個簡單的更新機制

通常在 React 中咱們須要更新時都是調用的 setState 方法。因此本文的更新就基於 setState 實現。看下面的調用方式:

/**
 * ReactCompositeComponent組件
 */
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("root");

React.render(CompositeElement, root);

// 生成html
<div id="root">
  <div data-reactid="0">
    <h3 data-reactid="0.0" class="h3">
      <span data-reactid="0.0.0">click me 0</span>
    </h3>
  </div>
</div>

// 點擊click me 計數會遞增
複製代碼

點擊文字就會調用 setState 走更新流程,咱們回顧一下 ReactClass,看一下 setState 的實現

/**
 * 更新
 * @param {*} newState 新狀態
 */
ReactClass.prototype.setState = function(newState) {
  // 拿到ReactCompositeComponent的實例
  // 在裝載的時候保存
  // 代碼:this._reactInternalInstance = this
  this._reactInternalInstance.receiveComponent(null, newState);
};
複製代碼

能夠看到 setState 主要調用了對應的 component 的 receiveComponent 來實現更新。全部的掛載,更新都應該交給對應的 component 來管理。因此就像全部的 component 都實現了 mountComponent 來處理第一次渲染,全部的 component 類都應該實現 receiveComponent 用來處理本身的更新。

文本節點的 receiveComponent

文本節點的更新比較簡單,拿到新的文本進行比較,不一樣則直接替換整個節點

/**
 * component 類 更新
 * @param {*} newText
 */
ReactDOMTextComponent.prototype.receiveComponent = function(nextText) {
  var nextStringText = "" + nextText;
  // 跟之前保存的字符串比較
  if (nextStringText !== this._currentElement) {
    this._currentElement = nextStringText;
    // 替換整個節點
    $('[data-reactid="' + this._rootNodeID + '"]').html(this._currentElement);
  }
};
複製代碼

自定義元素的 receiveComponent

先來看自定義元素的 receiveComponent 的實現

/**
 * component 類 更新
 * @param {*} nextElement
 * @param {*} newState
 */
ReactCompositeComponent.prototype.receiveComponent = function(
  nextElement,
  newState
) {
  // 若是接受了新的element,則直接使用最新的element
  this._currentElement = nextElement || this._currentElement;

  var inst = this._instance;
  // 合併state
  var nextState = Object.assign(inst.state, newState);
  var nextProps = this._currentElement.props;

  // 更新state
  inst.state = nextState;

  // 生命週期方法
  if (
    inst.shouldComponentUpdate &&
    inst.shouldComponentUpdate(nextProps, nextState) === false
  ) {
    // 若是實例的 shouldComponentUpdate 返回 false,則不須要繼續往下執行更新
    return;
  }

  // 生命週期方法
  if (inst.componentWillUpdate) inst.componentWillUpdate(nextProps, nextState);

  // 獲取老的element
  var prevComponentInstance = this._renderedComponent;
  var prevRenderedElement = prevComponentInstance._currentElement;

  // 經過從新render 獲取新的element
  var nextRenderedElement = this._instance.render();

  // 比較新舊元素
  if (_shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) {
    // 兩種元素爲相同,須要更新,執行字節點更新
    prevComponentInstance.receiveComponent(nextRenderedElement);
    // 生命週期方法
    inst.componentDidUpdate && inst.componentDidUpdate();
  } else {
    // 兩種元素的類型不一樣,直接從新裝載dom
    var thisID = this._rootNodeID;

    this._renderedComponent = this._instantiateReactComponent(
      nextRenderedElement
    );

    var nextMarkup = _renderedComponent.mountComponent(thisID);
    // 替換整個節點
    $('[data-reactid="' + this._rootNodeID + '"]').replaceWith(nextMarkup);
  }
};

/**
 * 經過比較兩個元素,判斷是否須要更新
 * @param {*} preElement  舊的元素
 * @param {*} nextElement 新的元素
 * @return {boolean}
 */
function _shouldUpdateReactComponent(prevElement, nextElement) {
  if (prevElement != null && nextElement != null) {
    var prevType = typeof prevElement;
    var nextType = typeof nextElement;
    if (prevType === "string" || prevType === "number") {
      // 文本節點比較是否爲相同類型節點
      return nextType === "string" || nextType === "number";
    } else {
      // 經過type 和 key 判斷是否爲同類型節點和同一個節點
      return (
        nextType === "object" &&
        prevElement.type === nextElement.type &&
        prevElement.key === nextElement.key
      );
    }
  }
  return false;
}
複製代碼

上述代碼的大體流程是:

  • 合併 state
  • 更新 state
  • 而後看業務代碼中是否實現生命週期方法 shouldComponentUpdate 有則調用,若是返回值爲 false 則中止往下執行
  • 而後是生命週期方法 componentWillUpdate
  • 而後經過拿到新 state 的 instance 調用 render 方法拿到新的 element 和之舊的 element 進行比較
  • 若是要更新就繼續調用對應的 component 類對應的 receiveComponent 就好啦,其實就是直接當甩手掌櫃,事情直接丟給手下去辦了。固然還有種狀況是,兩次生成的 element 差異太大,就不是一個類型的,那好辦直接從新生成一份新的代碼從新渲染一次就 o 了

_shouldUpdateReactComponent 是一個全局方法,這個是一種 React 的優化機制。用來決定是直接所有替換,仍是使用很細微的改動。當兩次 render 出來的子節點 key 不一樣,直接所有從新渲染一遍,替換就行了。不然,咱們就得來個遞歸的更新,保證最小化的更新機制,這樣能夠不會有太大的閃爍。

在這裏本質上仍是遞歸調用 receiveComponent 的過程。

基本元素的 receiveComponent

基礎元素的更新包括兩方面

  • 屬性的更新,包括對特殊屬性好比事件的處理
  • 子節點的更新

子節點的更新比較複雜,是提高效率的關鍵,因此須要處理如下問題:

  • diff - 拿新的子節點樹跟之前老的子節點樹對比,找出他們之間的差異。
  • patch - 全部差異找出後,再一次性的去更新。

下面是基礎元素更新的基本結構

/**
 * component 類 更新
 * @param {*} nextElement
 */
ReactDOMComponent.prototype.receiveComponent = function(nextElement) {
  var lastProps = this._currentElement.props;
  var nextProps = nextElement.props;
  this._currentElement = nextElement;
  // 處理當前節點的屬性
  this._updateDOMProperties(lastProps, nextProps);
  // 處理當前節點的子節點變更
  this._updateDOMChildren(nextElement.props.children);
};
複製代碼

先看看,更新屬性怎麼變動:

/**
 * 更新屬性
 * @param {*} lastProps
 * @param {*} nextProps
 */
ReactDOMComponent.prototype._updateDOMProperties = function(
  lastProps,
  nextProps
) {
  // 當老屬性不在新屬性的集合裏時,須要刪除屬性
  var propKey;
  for (propKey in lastProps) {
    if (
      nextProps.hasOwnProperty(propKey) ||
      !lastProps.hasOwnProperty(propKey)
    ) {
      // 新屬性中有,且再也不老屬性的原型中
      continue;
    }
    if (/^on[A-Za-z]/.test(propKey)) {
      var eventType = propKey.replace("on", "");
      // 特殊事件,須要去掉事件監聽
      $(document).undelegate(
        '[data-reactid="' + this._rootNodeID + '"]',
        eventType,
        lastProps[propKey]
      );
      continue;
    }

    // 刪除不須要的屬性
    $('[data-reactid="' + this._rootNodeID + '"]').removeAttr(propKey);
  }

  // 對於新的事件,須要寫到dom上
  for (propKey in nextProps) {
    if (/^on[A-Za-z]/.test(propKey)) {
      var eventType = propKey.replace("on", "");
      // 刪除老的事件綁定
      lastProps[propKey] &&
        $(document).undelegate(
          '[data-reactid="' + this._rootNodeID + '"]',
          eventType,
          lastProps[propKey]
        );
      // 針對當前的節點添加事件代理,以_rootNodeID爲命名空間
      $(document).delegate(
        '[data-reactid="' + this._rootNodeID + '"]',
        eventType + "." + this._rootNodeID,
        nextProps[propKey]
      );
      continue;
    }

    if (propKey == "children") continue;

    // 添加新的屬性,重寫同名屬性
    $('[data-reactid="' + this._rootNodeID + '"]').prop(
      propKey,
      nextProps[propKey]
    );
  }
};
複製代碼

屬性的變動並非特別複雜,主要就是找到之前老的不用的屬性直接去掉,新的屬性賦值,而且注意其中特殊的事件屬性作出特殊處理就好了。

子節點更新,也是最複雜的部分:

// 全局的更新深度標識
var updateDepth = 0;
// 全局的更新隊列,全部的差別都存在這裏
var diffQueue = [];

ReactDOMComponent.prototype._updateDOMChildren = function(
  nextChildrenElements
) {
  updateDepth++;
  // _diff用來遞歸找出差異,組裝差別對象,添加到更新隊列diffQueue。
  this._diff(diffQueue, nextChildrenElements);
  updateDepth--;
  if (updateDepth == 0) {
    // 在須要的時候調用patch,執行具體的dom操做
    this._patch(diffQueue);
    diffQueue = [];
  }
};
複製代碼

就像咱們以前說的同樣,更新子節點包含兩個部分,一個是遞歸的分析差別,把差別添加到隊列中。而後在合適的時機調用_patch 把差別應用到 dom 上。那麼什麼是合適的時機,updateDepth 又是幹嗎的?這裏須要注意的是,_diff 內部也會遞歸調用子節點的 receiveComponent 因而當某個子節點也是瀏覽器普通節點,就也會走_updateDOMChildren 這一步。因此這裏使用了 updateDepth 來記錄遞歸的過程,只有等遞歸回來 updateDepth 爲 0 時,表明整個差別已經分析完畢,能夠開始使用 patch 來處理差別隊列了。

diff 實現

// 差別更新的幾種類型
var UPDATE_TYPES = {
  MOVE_EXISTING: 1,
  REMOVE_NODE: 2,
  INSERT_MARKUP: 3
};

/**
 * 生成子節點 elements 的 component 集合
 * @param {object} prevChildren 前一個 component 集合
 * @param {Array} nextChildrenElements 新傳入的子節點element數組
 * @return {object} 返回一個映射
 */
function generateComponentChildren(prevChildren, nextChildrenElements) {
  var nextChildren = {};
  nextChildrenElements = nextChildrenElements || [];
  $.each(nextChildrenElements, function(index, element) {
    var name = element.key ? element.key : index;
    var prevChild = prevChildren && prevChildren[name];
    var prevElement = prevChild && prevChild._currentElement;
    var nextElement = element;

    // 調用_shouldUpdateReactComponent判斷是不是更新
    if (_shouldUpdateReactComponent(prevElement, nextElement)) {
      // 更新的話直接遞歸調用子節點的receiveComponent就行了
      prevChild.receiveComponent(nextElement);
      // 而後繼續使用老的component
      nextChildren[name] = prevChild;
    } else {
      // 對於沒有老的,那就從新新增一個,從新生成一個component
      var nextChildInstance = instantiateReactComponent(nextElement, null);
      // 使用新的component
      nextChildren[name] = nextChildInstance;
    }
  });

  return nextChildren;
}

/**
 * 將數組轉換爲映射
 * @param {Array} componentChildren
 * @return {object} 返回一個映射
 */
function flattenChildren(componentChildren) {
  var child;
  var name;
  var childrenMap = {};
  for (var i = 0; i < componentChildren.length; i++) {
    child = componentChildren[i];
    name =
      child && child._currentelement && child._currentelement.key
        ? child._currentelement.key
        : i.toString(36);
    childrenMap[name] = child;
  }
  return childrenMap;
}

/**
 * _diff用來遞歸找出差異,組裝差別對象,添加到更新隊列diffQueue。
 * @param {*} diffQueue
 * @param {*} nextChildrenElements
 */
ReactDOMComponent.prototype._diff = function(diffQueue, nextChildrenElements) {
  var self = this;
  // 拿到以前的子節點的 component類型對象的集合,這個是在剛開始渲染時賦值的,記不得的能夠翻上面
  // _renderedChildren 原本是數組,咱們搞成map
  var prevChildren = flattenChildren(self._renderedChildren);
  // 生成新的子節點的component對象集合,這裏注意,會複用老的component對象
  var nextChildren = generateComponentChildren(
    prevChildren,
    nextChildrenElements
  );
  // 從新賦值_renderedChildren,使用最新的。
  self._renderedChildren = [];
  $.each(nextChildren, function(key, instance) {
    self._renderedChildren.push(instance);
  });

  /**注意新增代碼**/
  var lastIndex = 0; // 表明訪問的最後一次的老的集合的位置

  var nextIndex = 0; // 表明到達的新的節點的index
  // 經過對比兩個集合的差別,組裝差別節點添加到隊列中
  for (name in nextChildren) {
    if (!nextChildren.hasOwnProperty(name)) {
      continue;
    }
    var prevChild = prevChildren && prevChildren[name];
    var nextChild = nextChildren[name];
    // 相同的話,說明是使用的同一個component,因此咱們須要作移動的操做
    if (prevChild === nextChild) {
      // 添加差別對象,類型:MOVE_EXISTING
      /**注意新增代碼**/
      prevChild._mountIndex < lastIndex &&
        diffQueue.push({
          parentId: self._rootNodeID,
          parentNode: $("[data-reactid=" + self._rootNodeID + "]"),
          type: UPDATE_TYPES.MOVE_EXISTING,
          fromIndex: prevChild._mountIndex,
          toIndex: nextIndex
        });
      /**注意新增代碼**/
      lastIndex = Math.max(prevChild._mountIndex, lastIndex);
    } else {
      // 若是不相同,說明是新增長的節點
      // 可是若是老的還存在,就是element不一樣,可是component同樣。咱們須要把它對應的老的element刪除。
      if (prevChild) {
        // 添加差別對象,類型:REMOVE_NODE
        diffQueue.push({
          parentId: self._rootNodeID,
          parentNode: $("[data-reactid=" + self._rootNodeID + "]"),
          type: UPDATE_TYPES.REMOVE_NODE,
          fromIndex: prevChild._mountIndex,
          toIndex: null
        });

        // 若是之前已經渲染過了,記得先去掉之前全部的事件監聽,經過命名空間所有清空
        if (prevChild._rootNodeID) {
          $(document).undelegate("." + prevChild._rootNodeID);
        }

        /**注意新增代碼**/
        lastIndex = Math.max(prevChild._mountIndex, lastIndex);
      }
      // 新增長的節點,也組裝差別對象放到隊列裏
      // 添加差別對象,類型:INSERT_MARKUP
      diffQueue.push({
        parentId: self._rootNodeID,
        parentNode: $("[data-reactid=" + self._rootNodeID + "]"),
        type: UPDATE_TYPES.INSERT_MARKUP,
        fromIndex: null,
        toIndex: nextIndex,
        markup: nextChild.mountComponent(self._rootNodeID + "." + name) //新增的節點,多一個此屬性,表示新節點的dom內容
      });
    }
    // 更新mount的index
    nextChild._mountIndex = nextIndex;
    nextIndex++;
  }

  // 對於老的節點裏有,新的節點裏沒有的那些,也全都刪除掉
  for (name in prevChildren) {
    if (
      prevChildren.hasOwnProperty(name) &&
      !(nextChildren && nextChildren.hasOwnProperty(name))
    ) {
      // 添加差別對象,類型:REMOVE_NODE
      diffQueue.push({
        parentId: self._rootNodeID,
        parentNode: $("[data-reactid=" + self._rootNodeID + "]"),
        type: UPDATE_TYPES.REMOVE_NODE,
        fromIndex: prevChildren[name]._mountIndex,
        toIndex: null
      });
      // 若是之前已經渲染過了,記得先去掉之前全部的事件監聽
      if (prevChildren[name]._rootNodeID) {
        $(document).undelegate("." + prevChildren[name]._rootNodeID);
      }
    }
  }
};
複製代碼

注意 flattenChildren 咱們這裏把數組集合轉成了對象 map,以 element 的 key 做爲標識,固然對於 text 文本或者沒有傳入 key 的 element,直接用 index 做爲標識。經過這些標識,咱們能夠從類型的角度來判斷兩個 component 是不是同樣的。

generateComponentChildren 會盡可能的複用之前的 component,也就是那些坑,當發現能夠複用 component(也就是 key 一致)時,就還用之前的,只須要調用他對應的更新方法 receiveComponent 就好了,這樣就會遞歸的去獲取子節點的差別對象而後放到隊列了。若是發現不能複用那就是新的節點,咱們就須要 instantiateReactComponent 從新生成一個新的 component。

lastIndex,這個表明最後一次訪問的老集合節點的最大的位置。 而咱們加了個判斷,只有_mountIndex 小於這個 lastIndex 的纔會須要加入差別隊列。有了這個判斷上面的例子 2 就不須要 move。而程序也能夠好好的運行,實際上大部分都是 2 這種狀況。

這是一種順序優化,lastIndex 一直在更新,表明了當前訪問的最右的老的集合的元素。 咱們假設上一個元素是 A,添加後更新了 lastIndex。 若是咱們這時候來個新元素 B,比 lastIndex 還大說明當前元素在老的集合裏面就比上一個 A 靠後。因此這個元素就算不加入差別隊列,也不會影響到其餘人,不會影響到後面的 path 插入節點。由於咱們從 patch 裏面知道,新的集合都是按順序從頭開始插入元素的,只有當新元素比 lastIndex 小時才須要變動。其實只要仔細推敲下上面那個例子,就能夠理解這種優化手段了。 查看React diff 策略

_patch 的實現

/**
 *
 * @param {*} parentNode
 * @param {*} childNode
 * @param {*} index
 */ function insertChildAt(parentNode, childNode, index) {
  var beforeChild = parentNode.children().get(index);
  beforeChild
    ? childNode.insertBefore(beforeChild)
    : childNode.appendTo(parentNode);
}

/**
 *
 * @param {*} diffQueue
 */
ReactDOMComponent.prototype._patch = function(diffQueue) {
  var update;
  var initialChildren = {};
  var deleteChildren = [];
  for (var i = 0; i < updates.length; i++) {
    update = updates[i];
    if (
      update.type === UPDATE_TYPES.MOVE_EXISTING ||
      update.type === UPDATE_TYPES.REMOVE_NODE
    ) {
      var updatedIndex = update.fromIndex;
      var updatedChild = $(update.parentNode.children().get(updatedIndex));
      var parentID = update.parentID;

      // 全部須要更新的節點都保存下來,方便後面使用
      initialChildren[parentID] = initialChildren[parentID] || [];
      // 使用parentID做爲簡易命名空間
      initialChildren[parentID][updatedIndex] = updatedChild;

      // 全部須要修改的節點先刪除,對於move的,後面再從新插入到正確的位置便可
      deleteChildren.push(updatedChild);
    }
  }

  // 刪除全部須要先刪除的
  $.each(deleteChildren, function(index, child) {
    $(child).remove();
  });

  // 再遍歷一次,此次處理新增的節點,還有修改的節點這裏也要從新插入
  for (var k = 0; k < updates.length; k++) {
    update = updates[k];
    switch (update.type) {
      case UPDATE_TYPES.INSERT_MARKUP:
        insertChildAt(update.parentNode, $(update.markup), update.toIndex);
        break;
      case UPDATE_TYPES.MOVE_EXISTING:
        insertChildAt(
          update.parentNode,
          initialChildren[update.parentID][update.fromIndex],
          update.toIndex
        );
        break;
      case UPDATE_TYPES.REMOVE_NODE:
        // 什麼都不須要作,由於上面已經幫忙刪除掉了
        break;
    }
  }
};
複製代碼

_patch 主要就是挨個遍歷差別隊列,遍歷兩次,第一次刪除掉全部須要變更的節點,而後第二次插入新的節點還有修改的節點。這裏爲何能夠直接挨個的插入呢?緣由就是咱們在 diff 階段添加差別節點到差別隊列時,自己就是有序的,也就是說對於新增節點(包括 move 和 insert 的)在隊列裏的順序就是最終 dom 的順序,因此咱們才能夠挨個的直接根據 index 去塞入節點。

這樣整個的更新機制就完成了。咱們再來簡單回顧下 React 的差別算法:

首先是全部的 component 都實現了 receiveComponent 來負責本身的更新,而瀏覽器默認元素的更新最爲複雜,也就是常常說的 diff algorithm。

react 有一個全局_shouldUpdateReactComponent 用來根據 element 的 key 來判斷是更新仍是從新渲染,這是第一個差別判斷。好比自定義元素裏,就使用這個判斷,經過這種標識判斷,會變得特別高效。

每一個類型的元素都要處理好本身的更新:

  • 自定義元素的更新,主要是更新 render 出的節點,作甩手掌櫃交給 render 出的節點的對應 component 去管理更新。

  • text 節點的更新很簡單,直接更新文案。

  • 瀏覽器基本元素的更新,分爲兩塊:

    • 先是更新屬性,對比出先後屬性的不一樣,局部更新。而且處理特殊屬性,好比事件綁定。
    • 而後是子節點的更新,子節點更新主要是找出差別對象,找差別對象的時候也會使用上面的_shouldUpdateReactComponent 來判斷,若是是能夠直接更新的就會遞歸調用子節點的更新,這樣也會遞歸查找差別對象,這裏還會使用 lastIndex 這種作一種優化,使一些節點保留位置,以後根據差別對象操做 dom 元素(位置變更,刪除,

end

這只是個玩具,但實現了 React 最核心的功能,虛擬節點,差別算法,單向數據更新都在這裏了。還有不少 React 優秀的東西沒有實現,好比對象生成時內存的線程池管理,批量更新機制,事件的優化,服務端的渲染,immutable data 等等。這些東西受限於篇幅就不具體展開了。

React 做爲一種解決方案,虛擬節點的想法比較新奇,不過我的仍是不能接受這種彆扭的寫法。使用 React,就要使用他那一整套的開發方式,而他核心的功能其實只是一個差別算法,而這種其實已經有相關的庫實現了。

相關資料:

相關文章
相關標籤/搜索