我在學習過程中喜歡做記錄,分享的是我在前端之路上的一些積累和思考,也希望能跟大家一起交流與進步。 這是我的github部落格,歡迎一起學習,歡迎star
本次分析的原始碼採用的是16.2.0
的版本 目前網上現有的react原始碼分析文章基於的都是版本16以前的原始碼,入口和核心構造器不一樣了,如下圖所示
本想借鑑前人的原始碼分析成果,奈何完全對不上號,只好自己慢慢摸索
水平有限,如果有錯誤和疏忽的地方,還請指正。
最快捷開始分析原始碼的辦法
mkdir analyze-react@16.2.0
cd analyze-react@16.2.0
npm init -y
npm i react --save
複製程式碼
然後開啟專案,進入node_nodules => react
先看入口檔案ndex.js
'use strict';
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react.production.min.js');
} else {
module.exports = require('./cjs/react.development.js');
}
複製程式碼
看開發環境下的版本即可,壓縮版本是打包到生產環境用的
開啟圖中檔案
核心介面
分析原始碼先找對外的暴露介面,當然就是react
了,直接拉到最下面
var React = {
Children: {
map: mapChildren,
forEach: forEachChildren,
count: countChildren,
toArray: toArray,
only: onlyChild
},
Component: Component,
PureComponent: PureComponent,
unstable_AsyncComponent: AsyncComponent,
Fragment: REACT_FRAGMENT_TYPE,
createElement: createElementWithValidation,
cloneElement: cloneElementWithValidation,
createFactory: createFactoryWithValidation,
isValidElement: isValidElement,
version: ReactVersion,
};
複製程式碼
ReactChildren
ReactChildren
提供了處理 this.props.children
的工具集,跟舊版本的一樣
Children: {
map: mapChildren,
forEach: forEachChildren,
count: countChildren,
toArray: toArray,
only: onlyChild
},
複製程式碼
元件
舊版本只有ReactComponent一種
新版本定義了三種不同型別的元件基類Component
,PureComponent
,unstable_AsyncComponent
Component: Component,
PureComponent: PureComponent,
unstable_AsyncComponent: AsyncComponent,
複製程式碼
等下再具體看都是什麼
生成元件
createElement: createElementWithValidation,
cloneElement: cloneElementWithValidation,
createFactory: createFactoryWithValidation,
複製程式碼
判斷元件:isValidElement
校驗是否是合法元素,只需要校驗型別,重點是判斷.$$typeof
屬性
function isValidElement(object) {
return typeof object === 'object' && object !== null && object.$$typeof === REACT_ELEMENT_TYPE;
}
複製程式碼
_assign
其實是object-assign
,但文中有關鍵地方用到它,下文會講
var _assign = require('object-assign');
複製程式碼
React元件的本質
元件本質是物件
不急著看程式碼,先通過例子看看元件是什麼樣子的
用creact-react-app
生成一個最簡單的react專案
在App.js
檔案加點東西,然後列印元件A看一下是什麼
npm start複製程式碼
啟動專案看看
其實就是個物件,有很多屬性,注意到props
裡面, 沒有內容
現在給元件A裡面加一點內容
componentDidMount() {
console.log('元件A',<A><span>加點內容看看</span></A>)
}
複製程式碼
可以看到,props.children
裡面開始巢狀內容了
以我們聰明的程式設計師的邏輯思維能力來推理一下,其實不斷的頁面巢狀,就是不斷的給這個物件巢狀props
而已
不信再看一下
componentDidMount() {
console.log('元件A',<A><span>加點內容看看<a>不信再加多一點</a></span></A>)
}
複製程式碼
虛擬DOM概念
所以到目前為止,我們知道了react的元件只是物件,而我們都知道真正的頁面是由一個一個的DOM節點組成的,在比較原生的jQuery年代,通過JS來操縱DOM元素,而且都是真實的DOM元素,而且我們都知道複雜或頻繁的DOM操作通常是效能瓶頸產生的原因, 所以React引入了虛擬DOM(Virtual DOM)的概念
總的說起來,無論多複雜的操作,都只是先進行虛擬DOM的JS計算,把這個元件物件計算好了以後,再一次性的通過Diff演算法進行渲染或者更新,而不是每次都要直接操作真實的DOM。
在即時編譯的時代,呼叫DOM的開銷是很大的。而Virtual DOM的執行完全都在Javascript 引擎中,完全不會有這個開銷。
元件的本源
知道了什麼是虛擬DOM以及元件的本質後,我們還是來看一下程式碼吧
先從生成元件開始切入,因為要生成元件就肯定會去找元件是什麼
createElement: createElementWithValidation
複製程式碼
摘取一些核心概念出來看就好
function createElementWithValidation(type, props, children) {
var element = createElement.apply(this, arguments);
return element;
}
複製程式碼
可以看到,返回了一個element
,這個元素又是由createElement
方法生成的,順著往下找
function createElement(type, config, children) {
var props = {};
var key = null;
var ref = null;
var self = null;
var source = null;
return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);
}
複製程式碼
返回的是ReactElement
方法,感覺已經很近了
var ReactElement = function (type, key, ref, self, source, owner, props) {
var element = {
$$typeof: REACT_ELEMENT_TYPE,
type: type,
key: key,
ref: ref,
props: props,
_owner: owner
};
return element;
};
複製程式碼
bingo,返回了一個物件,再看這個物件,是不是跟上面列印出來的物件格式很像?再看一眼
這就是元件的本源
元件三種基類
前面說了,版本16.2.0中,封裝了三種元件基類:分別是元件、純元件、非同步元件
Component: Component,
PureComponent: PureComponent,
unstable_AsyncComponent: AsyncComponent,
複製程式碼
一個個去看一下區別在哪裡,先看Component
function Component(props, context, updater) {
this.props = props;
this.context = context;
this.refs = emptyObject;
this.updater = updater || ReactNoopUpdateQueue;
}
複製程式碼
很簡單,一個建構函式,通過它構造的例項物件有四個私有屬性,refs
則是個emptyObject
,看名字就知道是空物件
這個emptyObject
也是引入的外掛
var emptyObject = require('fbjs/lib/emptyObject');複製程式碼
再去看PureComponent
,AsyncComponent
,定義的時候居然跟Component
是一樣的
區別
區別呢?
這裡就需要用到原型鏈方面的知識了
雖然原型和繼承在日常專案和工作中用的不多
但,那是因為我們平時很大部分在程式導向程式設計,特別是業務程式碼,但想要進階,就要去讀別人的原始碼,去自己封裝元件,這時它們就派上用場了,這就是為什麼它們很重要的原因。
核心的方法,和屬性,以及這三種元件直接的關係都是通過原型鏈的知識聯絡起來的,關鍵程式碼如下,我畫了個簡圖,希望能對看文章的各位有所幫助,如果有畫錯的,希望能指正我
先上核心程式碼,一些細枝末節的程式碼暫時忽略
setState
和forceUpdate
這兩個方法掛載在Component
(元件構造器)的原型上
Component.prototype.setState = function (partialState, callback) {
...
};
Component.prototype.forceUpdate = function (callback) {
...
};
複製程式碼
接下來定義一個ComponentDummy
,其實也是一個構造器,按照名字來理解就是“假元件”?,它是當做輔助用的
讓ComponentDummy
的原型指向Component
的原型,這樣它也能訪問原型上面的共有方法和屬性了,比如setState
和forceUpdate
function ComponentDummy() {}
ComponentDummy.prototype = Component.prototype;
複製程式碼
下面這句話,假元件構造器ComponentDummy
例項化出來一個物件pureComponentPrototype
,然後把這個物件的constructor屬性又指向了PureComponent
,因此PureComponent
也成為了一個構造器,也就是上面的第二種元件基類
var pureComponentPrototype = PureComponent.prototype = new ComponentDummy();
pureComponentPrototype.constructor = PureComponent;
複製程式碼
AsyncComponent
基類也是一樣
var asyncComponentPrototype = AsyncComponent.prototype = new ComponentDummy();
asyncComponentPrototype.constructor = AsyncComponent;
複製程式碼
但是AsyncComponent
的原型多了一個方法render
,看到了嗎,媽媽呀,這就是render的出處
asyncComponentPrototype.render = function () {
return this.props.children;
};
複製程式碼
所以到目前為止,可以得出一個原型圖
但是,有個問題來了,render
方法掛載在AsyncComponent
的原型上,那通過Component
構造器構造出來的例項豈不是讀不到render
方法,那為什麼日常元件是這樣寫的?
其實還有兩句程式碼,上面做了個小劇透的_assign
_assign(pureComponentPrototype, Component.prototype);
複製程式碼
_assign(asyncComponentPrototype, Component.prototype);
複製程式碼
每句話上面還特意有個註釋,Avoid an extra prototype jump for these methods.
,避免這些方法額外的原型跳轉,先不管它,先看_assign
做了什麼
把Component的原型跟AsyncComponent的原型合併,
那麼到這裡,答案就呼之欲出了,如此一來,AsyncComponent
上面的render
方法,不就相當於掛載到Component
上面了嗎?
以此類推,三種基類構造器最後都是基於同一個原型,共享所以方法,包括render
、setState
、forceUpdate
等等,最後的原型圖應該就變成了這樣
到這裡,有個問題要思考的是:
既然最後三個基類共用同一個原型,那為什麼要分開來寫?
中間還通過一個假元件構造器ComponentDummy
來輔助構建兩個例項
原始碼還沒讀完,這個地方我目前還沒弄明白,應該是後面三個基類又分別掛載了不一樣的方法,希望有大佬能提前回答一下。
後話
感謝您耐心看到這裡,希望有所收穫!
如果不是很忙的話,麻煩點個star⭐【Github部落格傳送門】,舉手之勞,卻是對作者莫大的鼓勵。
我在學習過程中喜歡做記錄,分享的是自己在前端之路上的一些積累和思考,希望能跟大家一起交流與進步,更多文章請看【amandakelake的Github部落格】