摘要:所謂知其然還要知其所以然本文將分析的部分源碼包括組件初始渲染的過程和組件更新的過程在這之前假設(shè)讀者已經(jīng)對有一定了解知道區(qū)別了解生命周期事務(wù)批量更新大致概念等如何分析源碼代碼架構(gòu)預(yù)覽首先我們找到在上的地址把版本的源碼下來觀察它的整體架構(gòu)這
所謂知其然還要知其所以然. 本文將分析 React 15-stable的部分源碼, 包括組件初始渲染的過程和組件更新的過程.在這之前, 假設(shè)讀者已經(jīng):
對React有一定了解
知道React element、component、class區(qū)別
了解生命周期、事務(wù)、批量更新、virtual DOM大致概念等
如何分析 React 源碼代碼架構(gòu)預(yù)覽
首先, 我們找到React在Github上的地址, 把15-stable版本的源碼copy下來, 觀察它的整體架構(gòu), 這里首先閱讀關(guān)于源碼介紹的官方文檔, 再接著看.
我們 要分析的源碼在 src 目錄下:
// src 部分目錄 ├── ReactVersion.js # React版本號 ├── addons # 插件 ├── isomorphic # 同構(gòu)代碼,作為react-core, 提供頂級API ├── node_modules ├── package.json ├── renderers # 渲染器, 包括DOM,Native,art,test等 ├── shared # 子目錄之間需要共享的代碼,提到父級目錄shared ├── test # 測試代碼
分析方法
1、首先看一些網(wǎng)上分析的文章, 對重點部分的源碼有個印象, 知道一些關(guān)鍵詞意思, 避免在無關(guān)的代碼上迷惑、耗費時間;
2、準(zhǔn)備一個demo, 無任何功能代碼, 只安裝react,react-dom, Babel轉(zhuǎn)義包, 避免分析無關(guān)代碼;
3、打debugger; 利用Chrome devtool一步一步走, 打斷點, 看調(diào)用棧,看函數(shù)返回值, 看作用域變量值;
4、利用編輯器查找代碼、閱讀代碼等
正文我們知道, 對于一般的React 應(yīng)用, 瀏覽器會首先執(zhí)行代碼 ReactDOM.render來渲染頂層組件, 在這個過程中遞歸渲染嵌套的子組件, 最終所有組件被插入到DOM中. 我們來看看
調(diào)用ReactDOM.render 發(fā)生了什么 大致過程(只展示主要的函數(shù)調(diào)用):如果看不清這有矢量圖
讓我們來分析一下具體過程:首先, 對于你寫的jsx, Babel會把這種語法糖轉(zhuǎn)義成這樣:
// jsx ReactDOM.render(, document.getElementById("app") ) // 轉(zhuǎn)義后 ReactDOM.render( React.createElement(C, null), document.getElementById("app") );
沒錯, 就是調(diào)用React.createElement來創(chuàng)建元素. 元素是什么? 元素只是一個對象描述了DOM樹, 它像這樣:
{ $$typeof: Symbol(react.element) key: null props: {} // props有child屬性, 描述子組件, 同樣是元素 ref: null type: class C // type可以是類(自定義組件)、函數(shù)(wrapper)、string(DOM節(jié)點) _owner: null _store: {validated: false} _self: null _source: null __proto__: Object }
React.createElement源碼在ReactElement.js中, 邏輯比較簡單, 不做分析.
創(chuàng)建出來的元素被當(dāng)作參數(shù)和指定的 DOM container 一起傳進(jìn)ReactDOM.render. 接下來會調(diào)用一些內(nèi)部方法, 接著調(diào)用了 instantiateReactComponent, 這個函數(shù)根據(jù)element的類型實例化對應(yīng)的component. 當(dāng)element的類型為:
string時, 說明是文本, 實例化ReactDOMTextComponent;
ReactElement時, 說明是react元素, 進(jìn)一步判斷element.type的類型, 當(dāng)為
string時, 為DOM原生節(jié)點, 實例化ReactDOMComponent;
函數(shù)或類時, 為react 組件, 實例化ReactCompositeComponent
instantiateReactComponent函數(shù)在instantiateReactComponent.js :
/** * Given a ReactNode, create an instance that will actually be mounted. */ function instantiateReactComponent(node(這里node指element), shouldHaveDebugID) { ... // 如果element為空 if (node === null || node === false) { // 創(chuàng)建空component instance = ReactEmptyComponent.create(instantiateReactComponent); } else if (typeof node === "object") { // 如果是對象 ... // 這里是類型檢查 // 如果element.type是字符串 if (typeof element.type === "string") { //實例化 宿主組件, 也就是DOM節(jié)點 instance = ReactHostComponent.createInternalComponent(element); } else if (isInternalComponentType(element.type)) { // 保留給以后版本使用,此處暫時不會涉及到 } else { // 否則就實例化ReactCompositeComponent instance = new ReactCompositeComponentWrapper(element); } // 如果element是string或number } else if (typeof node === "string" || typeof node === "number") { // 實例化ReactDOMTextComponent instance = ReactHostComponent.createInstanceForText(node); } else { invariant(false, "Encountered invalid React node of type %s", typeof node); } ... return instance; }
在調(diào)用instantiateReactComponent拿到組件實例后, React 接著調(diào)用了batchingStrategy.batchedUpdates并將組件實例當(dāng)作參數(shù)執(zhí)行批量更新.
批量更新是一種優(yōu)化策略, 避免重復(fù)渲染, 在很多框架都存在這種機(jī)制. 其實現(xiàn)要點是要弄清楚何時存儲更新, 何時批量更新.
在React中, 批量更新受batchingStrategy控制,而這個策略除了server端都是ReactDefaultBatchingStrategy:
不信你看, 在ReactUpdates.js中 :
var ReactUpdatesInjection = { ... // 注入批量策略的函數(shù)聲明 injectBatchingStrategy: function(_batchingStrategy) { ... batchingStrategy = _batchingStrategy; }, };
在ReactDefaultInjection.js中注入ReactDefaultBatchingStrategy :
ReactInjection.Updates.injectBatchingStrategy(ReactDefaultBatchingStrategy); // 注入
那么React是如何實現(xiàn)批量更新的? 在ReactDefaultBatchingStrategy.js我們看到, 它的實現(xiàn)依靠了事務(wù).
在 Transaction.js中, React 介紹了事務(wù):
** wrappers (injected at creation time) * + + * | | * +-----------------|--------|--------------+ * | v | | * | +---------------+ | | * | +--| wrapper1 |---|----+ | * | | +---------------+ v | | * | | +-------------+ | | * | | +----| wrapper2 |--------+ | * | | | +-------------+ | | | * | | | | | | * | v v v v | wrapper * | +---+ +---+ +---------+ +---+ +---+ | invariants * perform(anyMethod) | | | | | | | | | | | | maintained * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|--------> * | | | | | | | | | | | | * | | | | | | | | | | | | * | | | | | | | | | | | | * | +---+ +---+ +---------+ +---+ +---+ | * | initialize close | * +-----------------------------------------+ *
React 把要調(diào)用的函數(shù)封裝一層wrapper, 這個wrapper一般是一個對象, 里面有initialize方法, 在調(diào)用函數(shù)前調(diào)用;有close方法, 在函數(shù)執(zhí)行后調(diào)用. 這樣封裝的目的是為了, 在要調(diào)用的函數(shù)執(zhí)行前后某些不變性約束條件(invariant)仍然成立.
這里的不變性約束條件(invariant), 我把它理解為 “真命題”, 因此前面那句話意思就是, 函數(shù)調(diào)用前后某些規(guī)則仍然成立. 比如, 在調(diào)和(reconciliation)前后保留UI組件一些狀態(tài).
React 中, 事務(wù)就像一個黑盒, 函數(shù)在這個黑盒里被執(zhí)行, 執(zhí)行前后某些規(guī)則仍然成立, 即使函數(shù)報錯. 事務(wù)提供了函數(shù)執(zhí)行的一個安全環(huán)境.
繼續(xù)看Transaction.js對事務(wù)的抽象實現(xiàn):
// 事務(wù)的抽象實現(xiàn), 作為基類 var TransactionImpl = { // 初始化/重置實例屬性, 給實例添加/重置幾個屬性, 實例化事務(wù)時會調(diào)用 reinitializeTransaction: function () { this.transactionWrappers = this.getTransactionWrappers(); if (this.wrapperInitData) { this.wrapperInitData.length = 0; } else { this.wrapperInitData = []; } this._isInTransaction = false; }, _isInTransaction: false, // 這個函數(shù)會交給具體的事務(wù)實例化時定義, 初始設(shè)為null getTransactionWrappers: null, // 判斷是否已經(jīng)在這個事務(wù)中, 保證當(dāng)前的Transaction正在perform的同時不會再次被perform isInTransaction: function () { return !!this._isInTransaction; }, // 頂級API, 事務(wù)的主要實現(xiàn), 用來在安全的窗口下執(zhí)行函數(shù) perform: function (method, scope, a, b, c, d, e, f) { var ret; var errorThrown; try { this._isInTransaction = true; errorThrown = true; this.initializeAll(0); // 調(diào)用所有wrapper的initialize方法 ret = method.call(scope, a, b, c, d, e, f); // 調(diào)用要執(zhí)行的函數(shù) errorThrown = false; } finally { // 調(diào)用所有wrapper的close方法, 利用errorThrown標(biāo)志位保證只捕獲函數(shù)執(zhí)行時的錯誤, 對initialize // 和close拋出的錯誤不做處理 try { if (errorThrown) { try { this.closeAll(0); } catch (err) {} } else { this.closeAll(0); } } finally { this._isInTransaction = false; } } return ret; }, // 調(diào)用所有wrapper的initialize方法的函數(shù)定義 initializeAll: function (startIndex) { var transactionWrappers = this.transactionWrappers; // 得到wrapper // 遍歷依次調(diào)用 for (var i = startIndex; i < transactionWrappers.length; i++) { var wrapper = transactionWrappers[i]; try { ... this.wrapperInitData[i] = wrapper.initialize ? wrapper.initialize.call(this):null; } finally { if (this.wrapperInitData[i] === OBSERVED_ERROR) { try { this.initializeAll(i + 1); } catch (err) {} } } } }, // 調(diào)用所有wrapper的close方法的函數(shù)定義 closeAll: function (startIndex) { ... var transactionWrappers = this.transactionWrappers; // 拿到wrapper // 遍歷依次調(diào)用 for (var i = startIndex; i < transactionWrappers.length; i++) { var wrapper = transactionWrappers[i]; var initData = this.wrapperInitData[i]; var errorThrown; try { ... if (initData !== OBSERVED_ERROR && wrapper.close) { wrapper.close.call(this, initData); } errorThrown = false; } finally { if (errorThrown) { ... try { this.closeAll(i + 1); } catch (e) {} } } } this.wrapperInitData.length = 0; } };
好的, 相信你已經(jīng)對事務(wù)如何實現(xiàn)有了大致了解, 但這只是React事務(wù)的抽象實現(xiàn), 還需要實例化事務(wù)并對其加強(qiáng)的配合, 才能發(fā)揮事務(wù)的真正作用.
剛講到, 在React中, 批量更新受batchingStrategy控制,而這個策略除了server端都是ReactDefaultBatchingStrategy, 而在ReactDefaultBatchingStrategy.js中, 批量更新的實現(xiàn)依靠了事務(wù):
ReactDefaultBatchingStrategy.js :
... var Transaction = require("Transaction");// 引入事務(wù) ... var RESET_BATCHED_UPDATES = { // 重置的 wrapper initialize: emptyFunction, close: function() { ReactDefaultBatchingStrategy.isBatchingUpdates = false; }, }; var FLUSH_BATCHED_UPDATES = { // 批處理的 wrapper initialize: emptyFunction, close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates), }; // 組合成 ReactDefaultBatchingStrategyTransaction 事務(wù)的wrapper var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES]; // 調(diào)用 reinitializeTransaction 初始化 function ReactDefaultBatchingStrategyTransaction() { this.reinitializeTransaction(); } // 參數(shù)中依賴了事務(wù) Object.assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction, { getTransactionWrappers: function() { return TRANSACTION_WRAPPERS; }, }); var transaction = new ReactDefaultBatchingStrategyTransaction(); // 實例化這類事務(wù) // 批處理策略 var ReactDefaultBatchingStrategy = { isBatchingUpdates: false, // 批量更新策略調(diào)用的就是這個方法 batchedUpdates: function(callback, a, b, c, d, e) { var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates; // 一旦調(diào)用批處理, 重置isBatchingUpdates標(biāo)志位 ReactDefaultBatchingStrategy.isBatchingUpdates = true; // 避免重復(fù)分配事務(wù) if (alreadyBatchingUpdates) { return callback(a, b, c, d, e); } else { return transaction.perform(callback, null, a, b, c, d, e); // 將callback放進(jìn)事務(wù)里執(zhí)行 } }, };
那么, 為什么批量更新的實現(xiàn)依靠了事務(wù)呢? 還記得實現(xiàn)批量更新的兩個要點嗎?
何時存儲更新
何時批處理
對于這兩個問題, React 在執(zhí)行事務(wù)時調(diào)用wrappers的initialize方法, 建立更新隊列, 然后執(zhí)行函數(shù) :
何時存儲更新—— 在執(zhí)行函數(shù)時遇到更新請求就存到這個隊列中
何時批處理—— 函數(shù)執(zhí)行后調(diào)用wrappers的close方法, 在close方法中調(diào)用批量處理函數(shù)
口說無憑, 得有證據(jù). 我們拿ReactDOM.render會調(diào)用的事務(wù)ReactReconcileTransaction來看看是不是這樣:
ReactReconcileTransaction.js 里有個wrapper, 它是這樣定義的(英文是官方注釋) :
var ON_DOM_READY_QUEUEING = { /** * Initializes the internal `onDOMReady` queue. */ initialize: function() { this.reactMountReady.reset(); }, /** * After DOM is flushed, invoke all registered `onDOMReady` callbacks. */ close: function() { this.reactMountReady.notifyAll(); }, };
我們再看ReactReconcileTransaction事務(wù)會執(zhí)行的函數(shù)mountComponent, 它在
ReactCompositeComponent.js :
/* * Initializes the component, renders markup, and registers event listeners. */ mountComponent: function( transaction, hostParent, hostContainerInfo, context, ) { ... if (inst.componentDidMount) { if (__DEV__) { transaction.getReactMountReady().enqueue(() => { // 將要調(diào)用的callback存起來 measureLifeCyclePerf( () => inst.componentDidMount(), this._debugID, "componentDidMount", ); }); } else { transaction.getReactMountReady().enqueue(inst.componentDidMount, inst); } } ... }
而上述wrapper定義的close方法調(diào)用的this.reactMountReady.notifyAll()在這
CallbackQueue.js :
/** * Invokes all enqueued callbacks and clears the queue. This is invoked after * the DOM representation of a component has been created or updated. */ notifyAll() { ... // 遍歷調(diào)用存儲的callback for (var i = 0; i < callbacks.length; i++) { callbacks[i].call(contexts[i], arg); } callbacks.length = 0; contexts.length = 0; } }你竟然讀到這了
好累(笑哭), 先寫到這吧. 我本來還想一篇文章就把組件初始渲染的過程和組件更新的過程講完, 現(xiàn)在看來要分開講了… React 細(xì)節(jié)太多了, 蘊含的信息量也很大…說博大精深一點不夸張...向React的作者們以及社區(qū)的人們致敬!
我覺得讀源碼是一件很費力但是非常值得的事情. 剛開始讀的時候一點頭緒也沒有, 不知道它是什么樣的過程, 不知道為什么要這么寫, 有時候還會因為斷點沒打好繞了很多彎路…也是硬著頭皮一遍一遍看, 結(jié)合網(wǎng)上的文章, 就這樣云里霧里的慢慢摸索, 不斷更正自己的認(rèn)知.后來看多了, 就經(jīng)常會有大徹大悟的感覺, 零碎的認(rèn)知開始連通起來, 逐漸摸清了來龍去脈.
現(xiàn)在覺得確實很值得, 自己學(xué)到了不少. 看源碼的過程就感覺是跟作者們交流討論一樣, 思想在碰撞! 強(qiáng)烈推薦前端的同學(xué)們閱讀React源碼, 大神們智慧的結(jié)晶!
未完待續(xù)...
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/107443.html
摘要:而且默認(rèn)帶有執(zhí)行的順序是,,即便是內(nèi)聯(lián)的,依然具有屬性。模塊腳本只會執(zhí)行一次必須符合同源策略模塊腳本在跨域的時候默認(rèn)是不帶的。通常被用作腳本被禁用的回退方案。最后標(biāo)簽真的令人感到興奮。 窺探 Script 標(biāo)簽 0x01 什么是 script 標(biāo)簽? script 標(biāo)簽允許你包含一些動態(tài)腳本或數(shù)據(jù)塊到文檔中,script 標(biāo)簽是非閉合的,你也可以將動態(tài)腳本或數(shù)據(jù)塊當(dāng)做 script 的...
摘要:而且默認(rèn)帶有執(zhí)行的順序是,,即便是內(nèi)聯(lián)的,依然具有屬性。模塊腳本只會執(zhí)行一次必須符合同源策略模塊腳本在跨域的時候默認(rèn)是不帶的。通常被用作腳本被禁用的回退方案。最后標(biāo)簽真的令人感到興奮。 窺探 Script 標(biāo)簽 0x01 什么是 script 標(biāo)簽? script 標(biāo)簽允許你包含一些動態(tài)腳本或數(shù)據(jù)塊到文檔中,script 標(biāo)簽是非閉合的,你也可以將動態(tài)腳本或數(shù)據(jù)塊當(dāng)做 script 的...
摘要:他指示了一個對象的屬性,返回的將用來獲得該屬性對應(yīng)的值在上面的分析中,我們知道,當(dāng)傳入的是一個函數(shù)時,還要經(jīng)過一個叫的內(nèi)置函數(shù)才能獲得最終的所以此處的必然是優(yōu)化回調(diào)的作用了。 開篇說明 對的,讓你所見,又開始造輪子了。哈哈,造輪子我們是認(rèn)真的~ 源碼閱讀是必須的,Underscore是因為剛剛學(xué)習(xí)整理了一波函數(shù)式編程,加上自己曾經(jīng)沒有太多閱讀源碼的經(jīng)驗,先拿Underscore練練手,...
摘要:主要兼容的微信的瀏覽器,因為要在朋友圈來營銷,總體來說,會偏設(shè)計以及動畫些。 有一天,我們組內(nèi)的一個小伙伴突然問我,你知道有一個叫重構(gòu)工程師的崗位?這是干什么的?重構(gòu)工程師 這個問題引發(fā)了我對前端領(lǐng)域發(fā)展的思考,所以我來梳理下前端領(lǐng)域的發(fā)展過程,順便小小的預(yù)測下2017年的趨勢。不想看回憶的,可以直接跳到后面看展望。 神說,要有光,就有了光 自1991年蒂姆·伯納斯-李公開提及HTML...
閱讀 2049·2021-11-15 11:39
閱讀 3232·2021-10-09 09:41
閱讀 1496·2019-08-30 14:20
閱讀 3266·2019-08-30 13:53
閱讀 3330·2019-08-29 16:32
閱讀 3383·2019-08-29 11:20
閱讀 3024·2019-08-26 13:53
閱讀 780·2019-08-26 12:18