基於React版本16.2.0的原始碼解析(一):元件實現(小白也可讀)

amandakelake發表於2019-03-04
我在學習過程中喜歡做記錄,分享的是我在前端之路上的一些積累和思考,也希望能跟大家一起交流與進步。 這是我的github部落格,歡迎一起學習,歡迎star


本次分析的原始碼採用的是16.2.0的版本  目前網上現有的react原始碼分析文章基於的都是版本16以前的原始碼,入口和核心構造器不一樣了,如下圖所示


基於React版本16.2.0的原始碼解析(一):元件實現(小白也可讀)

本想借鑑前人的原始碼分析成果,奈何完全對不上號,只好自己慢慢摸索

水平有限,如果有錯誤和疏忽的地方,還請指正。


最快捷開始分析原始碼的辦法

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版本16.2.0的原始碼解析(一):元件實現(小白也可讀)

核心介面

分析原始碼先找對外的暴露介面,當然就是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一種

新版本定義了三種不同型別的元件基類ComponentPureComponent ,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看一下是什麼

基於React版本16.2.0的原始碼解析(一):元件實現(小白也可讀)

npm start複製程式碼

啟動專案看看 

基於React版本16.2.0的原始碼解析(一):元件實現(小白也可讀)

其實就是個物件,有很多屬性,注意到props裡面, 沒有內容 


現在給元件A裡面加一點內容

componentDidMount() {
    console.log('元件A',<A><span>加點內容看看</span></A>)
  }
複製程式碼

基於React版本16.2.0的原始碼解析(一):元件實現(小白也可讀)


可以看到,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,返回了一個物件,再看這個物件,是不是跟上面列印出來的物件格式很像?再看一眼基於React版本16.2.0的原始碼解析(一):元件實現(小白也可讀)

這就是元件的本源


元件三種基類

前面說了,版本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');複製程式碼

再去看PureComponentAsyncComponent,定義的時候居然跟Component 是一樣的基於React版本16.2.0的原始碼解析(一):元件實現(小白也可讀)

基於React版本16.2.0的原始碼解析(一):元件實現(小白也可讀)

區別

區別呢?

這裡就需要用到原型鏈方面的知識了 

雖然原型和繼承在日常專案和工作中用的不多

但,那是因為我們平時很大部分在程式導向程式設計,特別是業務程式碼,但想要進階,就要去讀別人的原始碼,去自己封裝元件,這時它們就派上用場了,這就是為什麼它們很重要的原因。


核心的方法,和屬性,以及這三種元件直接的關係都是通過原型鏈的知識聯絡起來的,關鍵程式碼如下,我畫了個簡圖,希望能對看文章的各位有所幫助,如果有畫錯的,希望能指正我

先上核心程式碼,一些細枝末節的程式碼暫時忽略

setStateforceUpdate這兩個方法掛載在Component(元件構造器)的原型上

Component.prototype.setState = function (partialState, callback) {
  ...
};

Component.prototype.forceUpdate = function (callback) {
  ...
};
複製程式碼


接下來定義一個ComponentDummy,其實也是一個構造器,按照名字來理解就是“假元件”?,它是當做輔助用的

ComponentDummy的原型指向Component的原型,這樣它也能訪問原型上面的共有方法和屬性了,比如setStateforceUpdate

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;
};
複製程式碼


所以到目前為止,可以得出一個原型圖 

基於React版本16.2.0的原始碼解析(一):元件實現(小白也可讀)


但是,有個問題來了,render方法掛載在AsyncComponent的原型上,那通過Component構造器構造出來的例項豈不是讀不到render方法,那為什麼日常元件是這樣寫的?基於React版本16.2.0的原始碼解析(一):元件實現(小白也可讀)


其實還有兩句程式碼,上面做了個小劇透的_assign

_assign(pureComponentPrototype, Component.prototype);
複製程式碼

_assign(asyncComponentPrototype, Component.prototype);
複製程式碼

每句話上面還特意有個註釋,Avoid an extra prototype jump for these methods.,避免這些方法額外的原型跳轉,先不管它,先看_assign做了什麼

把Component的原型跟AsyncComponent的原型合併, 

那麼到這裡,答案就呼之欲出了,如此一來,AsyncComponent上面的render方法,不就相當於掛載到Component上面了嗎?

以此類推,三種基類構造器最後都是基於同一個原型,共享所以方法,包括rendersetStateforceUpdate等等,最後的原型圖應該就變成了這樣基於React版本16.2.0的原始碼解析(一):元件實現(小白也可讀)


到這裡,有個問題要思考的是:

既然最後三個基類共用同一個原型,那為什麼要分開來寫? 中間還通過一個假元件構造器ComponentDummy來輔助構建兩個例項  

原始碼還沒讀完,這個地方我目前還沒弄明白,應該是後面三個基類又分別掛載了不一樣的方法,希望有大佬能提前回答一下。


後話

感謝您耐心看到這裡,希望有所收穫!

如果不是很忙的話,麻煩點個star⭐【Github部落格傳送門】,舉手之勞,卻是對作者莫大的鼓勵。

我在學習過程中喜歡做記錄,分享的是自己在前端之路上的一些積累和思考,希望能跟大家一起交流與進步,更多文章請看【amandakelake的Github部落格】


相關文章