React@15.6.2源碼解析---從 ReactDOM.render 到頁面渲染(1)ReactMount

之前介紹了React16.8版本的React公用API徒探,本著學(xué)習(xí)最新版的React的想法,但是敗在了Fiber的陣下喂窟,還有回過頭來寫搞明白React15的源碼测暗,畢竟從15到16是一次重大的更新。本文中React源碼版本為 15.6.2 磨澡,望各位看官找準(zhǔn)版本號碗啄,不同的版本還是有著細微的區(qū)別的

值得一提的是,在閱讀源碼時稳摄,在Chrome中打斷點是一個很好的操作稚字,可以了解到函數(shù)的調(diào)用棧,變量的值厦酬,一步一步的調(diào)試還可以了解整個執(zhí)行的流程胆描,一邊調(diào)試一邊記錄著流程一邊在加以理解一邊感慨這神乎其技的封裝。

博客會同步到github上弃锐,這樣也算是有了開源的項目袄友。歡迎各位看官指教!

準(zhǔn)備步驟

首先需要安裝 React@15.6.2霹菊, ReactDOM@15.6.2 ,其次搭建webpack打包支竹,因為必不可少的需要console.log啥的旋廷,另外需要babel的配置,babel6 babel7倒是無所謂礼搁,關(guān)鍵是可以解析我們的jsx語法饶碘。

示例代碼

import React from 'react';
import ReactDom from 'react-dom';

class App extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            name: 'Hello World'
        }
    }
    componentWillMount() {
        console.log('component will mount');
        this.setState({
            name: 'Hello CHina'
        })
    }
    componentDidMount() {
        console.log('component did mount');
        this.setState({
            name: 'Hello CHange'
        })
    }
    componentWillReceiveProps(nextProps) {
        console.log('component will receive props');
    }
    componentWillUpdate(nextProps, nextState) {
        console.log('component will updates');
    }
    componentDidUpdate(prevProps, prevState){
        console.log('component Did Update');
    };
    render() {
        console.log('render');
        return (
            <div>
                { this.state.name }
            </div>
        )
    }
};

ReactDom.render(
    <App>
        <div>Hello World</div>
    </App>,
    document.getElementById('root')
);

本片博客就是基于該代碼進行調(diào)試的,將這段代碼使用babel轉(zhuǎn)碼之后的結(jié)果為

// 主要代碼段
var App =
  /*#__PURE__*/
  function (_React$Component) {
    _inherits(App, _React$Component);

    function App(props) {
      var _this;

      _classCallCheck(this, App);

      _this = _possibleConstructorReturn(this, _getPrototypeOf(App).call(this, props));
      _this.state = {
        name: 'Hello World'
      };
      return _this;
    }

    _createClass(App, [{
      key: "componentWillMount",
      value: function componentWillMount() {
        console.log('component will mount');
        this.setState({
          name: 'Hello CHina'
        });
      }
    }, {
      key: "componentDidMount",
      value: function componentDidMount() {
        console.log('component did mount');
        this.setState({
          name: 'Hello CHange'
        });
      }
    }, {
      key: "componentWillReceiveProps",
      value: function componentWillReceiveProps(nextProps) {
        console.log('component will receive props');
      }
    }, {
      key: "componentWillUpdate",
      value: function componentWillUpdate(nextProps, nextState) {
        console.log('component will updates');
      }
    }, {
      key: "componentDidUpdate",
      value: function componentDidUpdate(prevProps, prevState) {
        console.log('component Did Update');
      }
    }, {
      key: "render",
      value: function render() {
        console.log('render');
        return _react["default"].createElement("div", null, this.state.name);
      }
    }]);

    return App;
  }(_react["default"].Component);

;

_reactDom["default"].render(_react["default"].createElement(App, null, _react["default"].createElement("div", null, "Hello World")), document.getElementById('root'));

一個立即執(zhí)行函數(shù)碍岔,返回一個名為App的構(gòu)造函數(shù)弥锄,內(nèi)部的componentWillMount render等方法,等會通過Object.defineProperty方法添加到App的原型鏈中闲孤。之后使用React.createElementApp轉(zhuǎn)換為ReactElement對象傳入到ReactDOM.render

看源碼需要扎實的Js基礎(chǔ)豪治,原型鏈洞拨、閉包、this指向负拟、模塊化烦衣、Object.defineProperty等常用的方法都是必須提前掌握的。

ReactDOM.render

在引入ReactDOM.js文件的時候掩浙,從上往下仔細看會發(fā)現(xiàn)有這么一行代碼是在引入的時候被執(zhí)行了ReactDefaultInjection.inject();花吟,這個ReactDefaultInjection調(diào)用了其內(nèi)部的一個inject方法,主要目的是進行一次全局的依賴注入厨姚,本博主一開始光注意著研究ReactDOM.render了衅澈,漏了這一句,導(dǎo)致后面有的東西很迷谬墙,所以在這提個醒今布,在引入一個文件時,文件內(nèi)部有的函數(shù)是沒有被導(dǎo)出的反而是在引入文件時直接執(zhí)行的芭梯。這個inject具體的代碼后面用到時會進行詳細的介紹险耀。

下面就是ReactDOM文件的代碼了

/* 各種文件的引入 */

// 執(zhí)行依賴注入
ReactDefaultInjection.inject();

// ReactDOM對象
var ReactDOM = {
  findDOMNode: findDOMNode,
  render: ReactMount.render,
  unmountComponentAtNode: ReactMount.unmountComponentAtNode,
  version: ReactVersion,

  /* eslint-disable camelcase */
  unstable_batchedUpdates: ReactUpdates.batchedUpdates,
  unstable_renderSubtreeIntoContainer: renderSubtreeIntoContainer
  /* eslint-enable camelcase */
};

/* 雜七雜八的東西 */

那么實質(zhì)上ReactDOM.render方法就是ReactMount.render方法,ReactMount文件可以說是render的入口了玖喘,是一個極其重要的文件甩牺。當(dāng)然ReactDOM兩萬多行代碼,重要的文件一大堆累奈。贬派。。澎媒。

ReactMount

還是一樣的搞乏,從上往下看仔細看,不要去找關(guān)鍵詞ReactMount戒努,一旦找關(guān)鍵詞會錯過很多細節(jié)请敦。一旦錯過了那么導(dǎo)致的結(jié)局就是臥槽,這個東西什么時候被賦值了储玫,臥槽侍筛,這個屬性哪里來的尷尬局面。所以再一次強調(diào)撒穷,打斷點的好處匣椰。Chrome斷點,??

那么你會發(fā)現(xiàn)端礼,有這么一個構(gòu)造函數(shù)

/**
 * Temporary (?) hack so that we can store all top-level pending updates on
 * composites instead of having to worry about different types of components
 * here.
 */
var topLevelRootCounter = 1;
var TopLevelWrapper = function () {
  this.rootID = topLevelRootCounter++;
};
TopLevelWrapper.prototype.isReactComponent = {};
if (process.env.NODE_ENV !== 'production') {
  TopLevelWrapper.displayName = 'TopLevelWrapper';
}
TopLevelWrapper.prototype.render = function () {
  return this.props.child;
};
TopLevelWrapper.isReactTopLevelWrapper = true;

這個TopLevelWrapper就是整個組件的最頂層禽笑,我們調(diào)用ReactDOM.render時入录,傳遞的參數(shù)被這個構(gòu)造函數(shù)給包裹起來。

// ReactMount.js

 /**
   *
   * @param {ReactElement} nextElement Component element to render.
   * @param {DOMElement} container DOM element to render into.
   * @param {?function} callback function triggered on completion
   * @return {ReactComponent} Component instance rendered in `container`.
   */
render: function (nextElement, container, callback) {
    return ReactMount._renderSubtreeIntoContainer(null, nextElement, container, callback);
  },

參數(shù)說明

nextElement: 這是React.createElement(App, null, React.createElement("div", null, "Hello World")))的結(jié)果佳镜,babel在解析jsx時僚稿,會調(diào)用React.createElement將我們寫的組件變成一個 ReactElement

container: ReactDOM.render 的第二個參數(shù),所需要掛載的節(jié)點邀杏,document.getElementById('root')

callback: 可選的回調(diào)函數(shù)贫奠,第三個參數(shù)

內(nèi)部就一句話,關(guān)鍵代碼還是ReactMount._renderSubtreeIntoContainer函數(shù)

ReactMount._renderSubtreeIntoContainer

// ReactMount.js

_renderSubtreeIntoContainer: function (parentComponent, nextElement, container, callback) {
    // 校驗 callback
    ReactUpdateQueue.validateCallback(callback, 'ReactDOM.render');
    !React.isValidElement(nextElement) ? /**/
    nextElement != null && nextElement.props !== undefined ? /**/

    /**/

    var nextWrappedElement = React.createElement(TopLevelWrapper, {
      child: nextElement
    });
    var nextContext;
    if (parentComponent) {
      var parentInst = ReactInstanceMap.get(parentComponent);
      nextContext = parentInst._processChildContext(parentInst._context);
    } else {
      nextContext = emptyObject;
    }

    var prevComponent = getTopLevelWrapperInContainer(container);
    if (prevComponent) {
      var prevWrappedElement = prevComponent._currentElement;
      var prevElement = prevWrappedElement.props.child;
      if (shouldUpdateReactComponent(prevElement, nextElement)) {
        var publicInst = prevComponent._renderedComponent.getPublicInstance();
        var updatedCallback = callback && function () {
          callback.call(publicInst);
        };
        ReactMount._updateRootComponent(prevComponent, nextWrappedElement, nextContext, container, updatedCallback);
        return publicInst;
      } else {
        ReactMount.unmountComponentAtNode(container);
      }
    }
    var reactRootElement = getReactRootElementInContainer(container);
    var containerHasReactMarkup = reactRootElement && !!internalGetID(reactRootElement); // false
    // 如果DOM元素包含一個由React呈現(xiàn)但不是根元素R的直接子元素望蜡,則為True唤崭。
    var containerHasNonRootReactChild = hasNonRootReactChild(container); // false

    if (process.env.NODE_ENV !== 'production') {
      /**/
    }

    var shouldReuseMarkup = containerHasReactMarkup && !prevComponent && !containerHasNonRootReactChild; // false
    var component = ReactMount._renderNewRootComponent(nextWrappedElement, container, shouldReuseMarkup, nextContext)._renderedComponent.getPublicInstance();
    if (callback) {
      callback.call(component);
    }
    return component;
  }

參數(shù)


流程:
首先檢查callback nextElement是否是合法的,判斷一下類型啥的脖律,然后會使用React.createElement創(chuàng)建一個typeTopLevelWrapperReactElement

var nextWrappedElement = React.createElement(TopLevelWrapper, {
    child: nextElement
});

我們傳入的nextElement會變成nextWrapperElement的一個props;

之后對parentComponent是否存在進行判斷并對nextContext賦值谢肾,當(dāng)前為空賦值為一個空對象emptyObject

調(diào)用getTopLevelWrapperInContainer(container)方法,這個方法主要是檢查容器內(nèi)部是否已經(jīng)存在一個有ReactDOM直接渲染的節(jié)點小泉,當(dāng)前是無芦疏,我們的容器內(nèi)部是空的

再往下執(zhí)行var reactRootElement = getReactRootElementInContainer(container);

getReactRootElementInContainer

/**
 * @param {DOMElement|DOMDocument} container DOM element that may contain
 * a React component
 * @return {?*} DOM element that may have the reactRoot ID, or null.
 */
function getReactRootElementInContainer(container) {
  if (!container) {
    return null;
  }
  // DOC_NODE_TYPE = 9
  if (container.nodeType === DOC_NODE_TYPE) {
    return container.documentElement;
  } else {
    return container.firstChild;
  }
}

對container.nodeType做判斷,nodeType是html節(jié)點的一個屬性微姊,nodeType = 9 的話表明當(dāng)前container是document節(jié)點酸茴,不是話返回內(nèi)部的第一個子節(jié)點

接下來執(zhí)行var containerHasReactMarkup = reactRootElement && !!internalGetID(reactRootElement);

這個標(biāo)記變量containerHasReactMarkup 用來判斷當(dāng)前container是否具有React標(biāo)記,當(dāng)前值為 false

下一個var containerHasNonRootReactChild = hasNonRootReactChild(container);如果DOM元素包含一個由React呈現(xiàn)但不是根元素R的直接子元素兢交,則為True薪捍。當(dāng)前為 false

下面根據(jù)以上的幾個變量得出一個標(biāo)記變量shouldReuseMarkup

var shouldReuseMarkup = containerHasReactMarkup && !prevComponent && !containerHasNonRootReactChild; // false

下面就是該函數(shù)的核心了

 var component = ReactMount._renderNewRootComponent(nextWrappedElement, container, shouldReuseMarkup, nextContext)._renderedComponent.getPublicInstance();
    if (callback) {
      callback.call(component);
    }
    return component;

執(zhí)行ReactMount._renderNewRootComponent()._renderedComponent.getPublicInstance()函數(shù)并將返回值返回出來,如果傳入了callback的話配喳,在return之前在調(diào)用一下callback酪穿。

那么先看ReactMount._renderNewRootComponent()方法

ReactMount._renderNewRootComponent

傳入的參數(shù)為

nextWrappedElement: nextWrappedElement // 對 TopLevelWrapper調(diào)用React.createElement的結(jié)果

container: document.getElementById('root')

shouldReuseMarkup: false

nextContext: {}
 _renderNewRootComponent: function (nextElement, container, shouldReuseMarkup, context) {
    process.env.NODE_ENV !== 'production' /**/

    !isValidContainer(container) /**/
    ReactBrowserEventEmitter.ensureScrollValueMonitoring();
    var componentInstance = instantiateReactComponent(nextElement, false);
    // 初始render是同步的,但是在render期間發(fā)生的任何更新晴裹,在componentWillMount或componentDidMount中被济,都將根據(jù)當(dāng)前的批處理策略進行批處理。
    ReactUpdates.batchedUpdates(batchedMountComponentIntoNode, componentInstance, container, shouldReuseMarkup, context);
    var wrapperID = componentInstance._instance.rootID;
    instancesByReactRootID[wrapperID] = componentInstance;

    return componentInstance;
  },

流程如下:

對 container 進行驗證

調(diào)用ReactBrowserEventEmitter.ensureScrollValueMonitoring() 確保監(jiān)聽瀏覽器滾動涧团,在React15中渲染時應(yīng)該是不會管頁面中高性能事件的只磷,所以在React16中引入的fiber架構(gòu)。

調(diào)用instantiateReactComponent方法實例化一個ReactComponent泌绣,這個方法也是ReactDOM的一個重點喳瓣,在下篇會說

調(diào)用ReactUpdates.batchedUpdates();開始執(zhí)行批量更新,這當(dāng)中會用到一開始注入的ReactDefaultBatchingStrategy

外部存儲一下當(dāng)前實例instancesByReactRootID[wrapperID] = componentInstance赞别,對象instancesByReactRootID外部閉包的一個Object,key值為實例的rootID,value值為當(dāng)前實例化出來的實例

最后return出這個實例配乓。

當(dāng)前流程圖

本篇留坑

  1. instantiateReactComponent方法

  2. ReactUpdates 文件

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末仿滔,一起剝皮案震驚了整個濱河市惠毁,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌崎页,老刑警劉巖鞠绰,帶你破解...
    沈念sama閱讀 221,820評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異飒焦,居然都是意外死亡蜈膨,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評論 3 399
  • 文/潘曉璐 我一進店門牺荠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來翁巍,“玉大人,你說我怎么就攤上這事休雌≡詈” “怎么了?”我有些...
    開封第一講書人閱讀 168,324評論 0 360
  • 文/不壞的土叔 我叫張陵杈曲,是天一觀的道長驰凛。 經(jīng)常有香客問我,道長担扑,這世上最難降的妖魔是什么恰响? 我笑而不...
    開封第一講書人閱讀 59,714評論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮涌献,結(jié)果婚禮上胚宦,老公的妹妹穿的比我還像新娘。我一直安慰自己洁奈,他們只是感情好间唉,可當(dāng)我...
    茶點故事閱讀 68,724評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著利术,像睡著了一般呈野。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上印叁,一...
    開封第一講書人閱讀 52,328評論 1 310
  • 那天被冒,我揣著相機與錄音,去河邊找鬼轮蜕。 笑死昨悼,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的跃洛。 我是一名探鬼主播率触,決...
    沈念sama閱讀 40,897評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼汇竭!你這毒婦竟也來了葱蝗?” 一聲冷哼從身側(cè)響起穴张,我...
    開封第一講書人閱讀 39,804評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎两曼,沒想到半個月后皂甘,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,345評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡悼凑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,431評論 3 340
  • 正文 我和宋清朗相戀三年偿枕,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片户辫。...
    茶點故事閱讀 40,561評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡渐夸,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出寸莫,到底是詐尸還是另有隱情捺萌,我是刑警寧澤,帶...
    沈念sama閱讀 36,238評論 5 350
  • 正文 年R本政府宣布膘茎,位于F島的核電站桃纯,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏披坏。R本人自食惡果不足惜态坦,卻給世界環(huán)境...
    茶點故事閱讀 41,928評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望棒拂。 院中可真熱鬧伞梯,春花似錦、人聲如沸帚屉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽攻旦。三九已至喻旷,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間牢屋,已是汗流浹背且预。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留烙无,地道東北人锋谐。 一個月前我還...
    沈念sama閱讀 48,983評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像截酷,于是被迫代替她去往敵國和親涮拗。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,573評論 2 359

推薦閱讀更多精彩內(nèi)容

  • 40、React 什么是React多搀?React 是一個用于構(gòu)建用戶界面的框架(采用的是MVC模式):集中處理VIE...
    萌妹撒閱讀 1,022評論 0 1
  • 原教程內(nèi)容詳見精益 React 學(xué)習(xí)指南歧蕉,這只是我在學(xué)習(xí)過程中的一些閱讀筆記,個人覺得該教程講解深入淺出康铭,比目前大...
    leonaxiong閱讀 2,843評論 1 18
  • 在介紹React渲染機制之間先來說一說下面幾個概念,對于新入手React的學(xué)員來說赌髓,經(jīng)常會被搞蒙圈从藤。 Rea...
    殊一ONLY閱讀 5,662評論 0 4
  • 原文鏈接地址:https://github.com/Nealyang 轉(zhuǎn)載請注明出處 前言 戰(zhàn)戰(zhàn)兢兢寫下開篇......
    Nealyang閱讀 2,430評論 0 3
  • 泉州涂嶺鄉(xiāng)的白井村是我的老家,有一年多都沒有回去了锁蠕,于是我在暑假的時候踏上了“還鄉(xiāng)之路”夷野。 為什么...
    流星小飛豬J閱讀 439評論 0 2