進擊React源碼之磨刀試煉1

第二篇導航:《進擊React源碼之磨刀試煉2》:包含Component、Refs、ReactChildren相關解析javascript

前言

前端的腳步飛快,可是咱們實際開發中真正用的到有技術含量的東西真的不是不少,不少時候咱們不得不糾結於樣式的實現,浪費了大量的時間,也看過不少文章和視頻,大佬們一致的結論仍是:後端更「編程」,越接近數據的人才會越「值錢」。前端也想作那個「值錢」的人,因此咱們就得接觸更有技術含量的東西,鍛鍊真正的編程(而不是排版)能力,並努力擁抱後端。閱讀和理解一些開源項目源碼即是一種很好的學習和鍛鍊方式,只不過提及來簡單,作起來並不容易,畢竟神仙們的代碼,咱們不必定看得懂。html

然而幸運的咱們可能並不用徹底靠本身摸索去讀代碼,慕課網講師jocky和掘金活躍做者yck都在提供帶領咱們閱讀源碼的課程/博客,咱們能夠抓住機會,利用這些優質資源,進行學習和筆記。我也好久沒寫筆記了,在學習過程當中,個人體驗仍是作筆記會學的更牢固。本系列博客也是基於兩位大神的分享與本身的理解展開的。前端

兩位大神資料地址java

準備工做

clone一份yck大神帶註釋的源碼(版本16.8.6)或者去imooc購買課程能夠看jocky大神課程對應的源碼(固然感受沒時間看視頻或者經費緊張的小夥伴能夠先研究博客,本身感受這樣下來學到東西會更多),固然本身也得有一份官方的源碼,這裏我clone了16.9.0的。react

在我下載的時候速度只有幾k,最高不過15k,之前都仍是好的,若是遇到相似問題,請各位百度搜索github下載速度慢相關問題尋找解決方案(hosts中添加內容)。git

  • clone 官方源碼:git clone https://github.com/facebook/react.git
  • clone yck大神源碼:git clone https://github.com/KieSun/react-interpretation.git

React源碼目錄

進入react項目以後,再進入packages目錄,這裏存放的代碼就是咱們須要學習的內容,它的結構大體以下。github

react/packages目錄結構npm

├── create-subscription
├── eslint-plugin-react-hooks
├── jest-mock-scheduler
├── jest-react
├── legacy-events `事件系統`
├── react `react核心包`
├── react-art
├── react-cache
├── react-debug-tools
├── react-devtools
├── react-devtools-core
├── react-devtools-inline
├── react-dom `dom相關`
├── react-events
├── react-is
├── react-native-renderer
├── react-noop-renderer
├── react-reconciler `react-dom與react-native等共用`
├── react-refresh
├── react-stream
├── react-test-renderer
├── scheduler `調度:異步渲染等`
├── shared `共享`
└── use-subscription
複製代碼

咱們能夠看到react將不一樣的功能封裝爲不一樣的包,有些包能夠爲其餘包所公用,一些重要的且功能獨立的功能還能夠發佈爲單獨的npm包,咱們能夠經過研究其模塊化拆分、組合等技巧,萬一哪天咱們本身也作了個很火的開源項目呢,代碼得漂亮一點。仔細觀察16.9.0目錄結構跟16.8.6是有些不同的,事件系統也由events變成了legacy-events,具體的變化咱們後面仔細研究(推薦學習的時候順便再擼一遍官方文檔(英文))。本文寫做時React英文文檔16.9.0,中文文檔16.8.6編程

JSX與React.createElement

對於學習React源碼的同窗一定多少了解和使用過JSX,這裏就不對它進行描述了,有須要的能夠去React官網—JSX簡介進行閱讀了解。後端

官方說JSX 能夠生成 React 「元素」,那麼它是如何生成React元素的?咱們能夠經過babel在線編譯(點擊進入)進行了解

babel在線編譯界面

例1

在左邊能夠輸入JSX表達式,右邊會幫助咱們編譯爲React代碼。

// 輸入
<div id="hello" class="hello" key="hello">Hello React</div>
複製代碼

輸入上述JSX代碼,babel會幫助咱們編譯爲下方代碼

// 輸出
"use strict";

React.createElement("div", {
  id: "hello",
  class: "hello",
  key: "hello"
}, "Hello React");
複製代碼

經過編譯後的代碼咱們能夠知道,爲何在寫React組件時,明明沒有用到React卻須要引入,不然就會報錯,這是由於JSX的本質仍是React.createElement(),它只是這個API的語法糖,不引入React就沒法使用這個API。

僅僅是看編譯後的代碼,咱們也能看出目前編譯出的React.createElement()傳入了三個參數:

  1. 第一個是div元素的名稱。
  2. 第二個是元素的屬性,其爲一個對象,元素的全部屬性都被編譯到這個對象中了。
  3. 第三個是這個元素中所包含的內容,當前是一個字符串。

想一想若是是咱們本身,咱們會用怎樣的數據結構來表示一個HTML元素,其實也不過就是元素名,屬性和內容了,因此以上三個參數也是很好理解的。

例2: 寫一個稍微複雜點的JSX表達式試試

// 輸入
<ul id="hello" class="hello" key="hello">
	<li>Hello React</li>
	<li>Hello JSX</li>  
</ul>
複製代碼
// 輸出
"use strict";

React.createElement("ul", {
  id: "hello",
  class: "hello",
  key: "hello"
}, React.createElement("li", null, "Hello React"), React.createElement("li", null, "Hello JSX"));
複製代碼

從編譯結果咱們能夠看到,React.createElement()傳入的參數總數變了,變成了四個,前兩個參數還是元素名稱和屬性,後兩個都是React.createElement(),這兩個函數的參數又跟咱們第一個例子同樣了。

例3:接下來寫個React函數組件看看什麼效果

// 輸入
function Wrapper({children}) {
	return <div class="wrapper">{children}</div>
}
<Wrapper>
  <ul id="hello" class="hello" key="hello">
	<li>Hello React</li>
	<li>Hello JSX</li>  
  </ul>
</Wrapper>
複製代碼
// 輸出
"use strict";

function Wrapper(_ref) {
  var children = _ref.children;
  return React.createElement("div", {
    class: "wrapper"
  }, children);
}

React.createElement(Wrapper, null, React.createElement("ul", {
  id: "hello",
  class: "hello",
  key: "hello"
}, React.createElement("li", null, "Hello React"), React.createElement("li", null, "Hello JSX")));
複製代碼

這裏寫了一個簡單的Wrapper組件,在編譯後的結果相信你們能夠看得懂了,只不過細心的同窗能夠看到第一個參數再也不是一個字符串,而是Wrapper變量,若是咱們在使用Wrapper時候將其首字母改成小寫wrapper,此時編譯結果爲

"use strict";

function Wrapper(_ref) {
  var children = _ref.children;
  return React.createElement("div", {
    class: "wrapper"
  }, children);
}

React.createElement("wrapper", null, React.createElement("ul", {
  id: "hello",
  class: "hello",
  key: "hello"
}, React.createElement("li", null, "Hello React"), React.createElement("li", null, "Hello JSX")));
複製代碼

最外層第一個參數變成了字符串,這裏要說明的是爲何寫React時要求咱們將組件首字母大寫,若是是小寫,會編譯爲字符串,匹配原生的HTML標籤,因此會出錯,首字母大寫纔是組件。

源碼初探——createElement

經過babel的在線運行平臺,咱們看清楚JSX轉換到JavaScript的祕密。接下來能夠試試查看源碼: 打開packages/react/src/React.js(基於16.9.0), 20-26行引入了ReactElement.js中的內容

import {
  createElement,
  createFactory,
  cloneElement,
  isValidElement,
  jsx,
} from './ReactElement';
複製代碼

其中就包含咱們剛剛所看到的createElement,咱們能夠打開packages/react/src/createElement.js,定位到createElement導出的地方,看看這個API如何實現。

代碼上方有段註釋Create and return a new ReactElement of the given type.代表這個API做用是根據所給出的type類型建立並返回了一個新的ReactElement類型的元素。並提供了API說明文檔有興趣的同窗能夠先查看一下該API的描述和使用方式再看實現。

代碼片斷1:函數定義

export function createElement(type, config, children) {
  let propName;

  // Reserved names are extracted
  const props = {};

  let key = null;
  let ref = null;
  let self = null;
  let source = null;
  ...
}
複製代碼

先來看函數和內部變量的定義,函數的三個參數在咱們使用babel進行編譯的時候就猜到了是什麼意思:

  • type:函數會根據type的不一樣區建立不一樣種類的ReactElement(react元素),最簡單的理解就是咱們第三個例子,當使用首字母小寫的時候第一個參數爲字符串、首字母大寫的時候第一個參數是一個變量,react會根據不一樣的類型給咱們建立不一樣的react元素。
  • config:這個參數用來描述元素的屬性,根據編譯結果咱們也知道它是一個對象類型,元素的每一個屬性能夠對應裏面一個key-value對,例如id,className,key,ref等都在config中進行描述。
  • children:表明元素的內容或者子元素,可是有奇怪的是咱們以前看到的函數有可能會有第4、第5、第六個參數啊,這裏怎麼用一個children來表示?不妨想一想,若是用一個children來表示從第三個元素開始後面的元素,代碼中你會怎樣作?我想你們都猜到了,能夠用arguments參數來進行截取。若是是你來實現,你會有其餘方法嘛?好比第三個參數使用rest參數(...children),在函數中咱們就能夠不用截取。不過這裏爲何沒有使用這種方式還須要你們本身去探索和思考一下。

而後就是一些變量的定義了,能夠看到的是儘管這是JavaScript代碼,能夠隨時定義變量,做者仍將變量的定義提早,這也是值得咱們學習和實踐的。看源碼的過程當中但願你們儘可能注意做者給出的註釋。Reserved names are extracted代表這裏會將保留名提取出來,所以咱們能夠猜到在props中只會存在一些正常的屬性,特殊的內容會被過濾掉,後面一定也有相關邏輯。

代碼片斷2:元素屬性過濾

if (config != null) {
    if (hasValidRef(config)) {
      ref = config.ref;
    }
    if (hasValidKey(config)) {
      key = '' + config.key;
    }

    self = config.__self === undefined ? null : config.__self;
    source = config.__source === undefined ? null : config.__source;
    // Remaining properties are added to a new props object
    for (propName in config) {
      if (
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        props[propName] = config[propName];
      }
    }
  }
複製代碼

流程圖

這裏畫了一張不標準的流程圖(大體描述代碼),若是有強迫症的同窗能夠動手畫一畫作的標準一點。從流程圖咱們能夠比較清楚的看到這段代碼的做用,首先從config中過濾出refkey屬性,這兩個屬性應該是保留屬性中的內容,而後獲取了config__self__source屬性,以後對config的自有屬性進行了遍歷,過濾了保留屬性中的內容,將其餘屬性存放在props對象中。 一句話將就是獲取普通屬性放到props中並提取了保留屬性。上面提到的__self__source長什麼樣子,咱們能夠看看16-21行的定義中看到。

const RESERVED_PROPS = {
  key: true,
  ref: true,
  __self: true,
  __source: true,
};
複製代碼

代碼片斷3:提取children

// Children can be more than one argument, and those are transferred onto
  // the newly allocated props object.
  const childrenLength = arguments.length - 2;
  if (childrenLength === 1) {
    props.children = children;
  } else if (childrenLength > 1) {
    const childArray = Array(childrenLength);
    for (let i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2];
    }
    if (__DEV__) {
      if (Object.freeze) {
        Object.freeze(childArray);
      }
    }
    props.children = childArray;
  }
複製代碼

流程圖

代碼片斷3能夠看出首先經過arguments對象長度計算children的長度,若children只有一個元素則props對象的children屬性就指向這個元素,若是children元素個數大於1,則建立childArray對象存儲他們,再讓props對象的children元素指向這個數組。 注意事項: 從上述源碼中咱們能夠看出來props.children多是一個數組,也可能不是,因此在使用children過程當中,咱們必須注意children是否是數組

其中有段代碼是凍結childArray對象,對於Object.freeze(),平時使用並很少,若是小夥伴不知道它的做用,看看下面取自MDN的一段描述: Object.freeze() 方法能夠凍結一個對象。一個被凍結的對象不再能被修改;凍結了一個對象則不能向這個對象添加新的屬性,不能刪除已有屬性,不能修改該對象已有屬性的可枚舉性、可配置性、可寫性,以及不能修改已有屬性的值。此外,凍結一個對象後該對象的原型也不能被修改。

代碼片斷4:取默認屬性

// Resolve default props
  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }
複製代碼

流程圖

這段代碼比較容易看懂,若是type中存在默認屬性,則遍歷取出來,若是props對象中沒有該屬性則進行存入。而這個擁有defaultPropstype是什麼,咱們以前看到過,type能夠爲字符串:表示一個html標籤;也能夠爲變量:表示一個組件,那一個組件中就可能存在一些屬性了。

代碼片斷5:結束

if (__DEV__) {
    if (key || ref) {
      const displayName =
        typeof type === 'function'
          ? type.displayName || type.name || 'Unknown'
          : type;
      if (key) {
        defineKeyPropWarningGetter(props, displayName);
      }
      if (ref) {
        defineRefPropWarningGetter(props, displayName);
      }
    }
  }
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );
複製代碼

在這個函數中還剩下最後一段代碼(原本不想說這個了,可是強迫症逼的仍是說一下吧),在開發環境下(__DEV__),若是keyref任何一個屬性存在,則會判斷type的類型是否爲function,若是是的話,則取出typedisplayNamename,若是這兩個屬性都沒有則取Unknown,若是不是function,就取type(此時指的是這個html標籤名稱),取出來的做用就是在key或者ref爆出警告時候,展現出來的名稱就是這個displayname。最後根據一系列屬性返回一個ReactElement;

既然說到了ReactElement,咱們就先來看看這個API。

ReactElement: 探索React元素

const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    // This tag allows us to uniquely identify this as a React Element
    $$typeof: REACT_ELEMENT_TYPE,

    // Built-in properties that belong on the element
    type: type,
    key: key,
    ref: ref,
    props: props,

    // Record the component responsible for creating this element.
    _owner: owner,
  };

  if (__DEV__) {
    // The validation flag is currently mutative. We put it on
    // an external backing store so that we can freeze the whole object.
    // This can be replaced with a WeakMap once they are implemented in
    // commonly used development environments.
    element._store = {};

    // To make comparing ReactElements easier for testing purposes, we make
    // the validation flag non-enumerable (where possible, which should
    // include every environment we run tests in), so the test framework
    // ignores it.
    Object.defineProperty(element._store, 'validated', {
      configurable: false,
      enumerable: false,
      writable: true,
      value: false,
    });
    // self and source are DEV only properties.
    Object.defineProperty(element, '_self', {
      configurable: false,
      enumerable: false,
      writable: false,
      value: self,
    });
    // Two elements created in two different places should be considered
    // equal for testing purposes and therefore we hide it from enumeration.
    Object.defineProperty(element, '_source', {
      configurable: false,
      enumerable: false,
      writable: false,
      value: source,
    });
    if (Object.freeze) {
      Object.freeze(element.props);
      Object.freeze(element);
    }
  }

  return element;
};
複製代碼

這個函數的註釋很是多,開頭的一些沒有貼上來,但願你們能夠仔細看看這個函數相關注釋。根據開頭註釋的描述,這個函數是個建立React元素的工廠方法,它不在支持類的模式,所以不能使用new操做符來新建元素,因此instanceof也沒法對它的類型進行檢查。那若是想要查看是否爲React元素怎麼辦?這裏提供了Symbol.for('react.element')的方式來對`$typeof`字段進行檢查來肯定是否爲React元素。

忽略if語句,其實這段代碼就是建立了一個element並將其返回,這個element元素中包含了一些屬性,其中typekeyrefprops咱們已經很瞭解了,那麼$$typeof:REACT_ELEMENT_TYPE是個什麼東西?根據上一段文字,咱們能夠知道它是用來判斷一個元素是否爲React元素的東西,咱們能夠順着找到它的定義所在packages/shared/ReactSymbols.js:

export const REACT_ELEMENT_TYPE = hasSymbol
  ? Symbol.for('react.element')
  : 0xeac7;
複製代碼

對於這段代碼的邏輯相信你們都看的懂,可是對於Symbol.for,你們平時用到應該不是不少,有些同窗可能會對他感到陌生,下面引用MDN中一段描述來講明一下它的做用:

Symbol.for(key) 方法會根據給定的鍵 key,來從運行時的 symbol 註冊表中找到對應的 symbol,若是找到了,則返回它,不然,新建一個與該鍵關聯的 symbol,並放入全局 symbol 註冊表中。

還有一個_owner咱們不太熟悉,可是根據註釋咱們就能夠很清楚的知道它是負責記錄建立此元素的組件的。經過它咱們能夠知道這個元素是哪一個組件建立的。

咱們一會說元素,一會說組件,可能有小夥伴會感受很暈,什麼是元素,什麼又是組件啊?yck大神在他的文章中很清楚的告訴了咱們:若是用JSX來寫<App/>,那麼<App/>就是ReactElement(React元素),而App表明React Component(React組件)

我爲何把if語句也貼了出來,其實但願你們能夠看看裏面的註釋(寫的很是明確),不懂的話能夠有道翻譯,瞭解一下它作了什麼。這裏就不深刻它了。

本章小結

  1. 首先咱們經過Babel在線運行工具查看了JSX轉換的祕密,從而瞭解到爲何寫JSX代碼時候咱們沒用到React卻要引入它,並了初步瞭解了React.createElement()的使用方式,以及它參數不一樣時會出現什麼結果。
  2. 而後咱們深刻了解了React.createElement(type, config, children)API的實現,總的來講,它乾的事情並非不少,過濾了一些保留的屬性,將普通屬性放到props中,經過對children的處理,將其放到props.children中,在使用時,咱們必定要注意children是否爲數組,最後返回一個React元素。
  3. 最後咱們看了ReactElement()的實現,它其實就是根據咱們上面第二條中處理的一些參數來建立一個React元素,React元素的檢驗方式要經過他的$$typeof屬性來檢驗,它還有個_owner屬性表示建立它的組件。
  4. 最後還留了個讓你們本身查看的內容,但願你們能夠配合源碼、文檔、有道詞典來仔細研究源碼。

說在最後 本篇內容其實也不算短了,原本想再寫一點,可是這一篇寫的時間有點長,也有點寫不下去了,若是再長點,讀者還沒讀完也就放棄了,仍是放到後面文章裏面在寫吧。

再說說已經有大神在寫React源碼博客了,而且還有相關視頻課程,我一個菜鳥寫的有人看嘛?其實我就是這種想法,爲何不去看大神的要看一個菜鳥的。想了想,其實看源碼、博客,每一個人的理解和最終的表達方式都不多是同樣的,以前也很久沒寫文章了,由於總感受我要寫的別人都已經寫過了,並且也寫的很好,想着我再牛逼一點再寫點高質量的文章。

不過如今感受本身錯了,大神的想法畢竟是大神的,大神的表達也是大神的,不必定每一個人看理解的程度都能達到大神們的境界,每一個人看文章也好,源碼也罷,理解也多是不一樣的,若是一個菜鳥能將本身的想法講清楚,其實很大程度上也能夠幫助到跟本身同一個level或者更低level的人更清楚地學習這些知識。並且閱讀、理解是輸入,寫做是輸出,在輸出中會想清楚更多的東西。

在這裏也很是鼓勵你們將本身的學習分享出去,咱們一塊兒勇敢一點、大不了寫的太爛結果就是沒人看嘛,頂多再嘲諷兩句,但本身學到的東西會不斷推着咱們向前。

本系列博客GitHub地址:github.com/kingshuaish… 因爲時間緣由,更新不穩定,但必定會持續,但願有志同道合的小夥伴們相互督促,一塊兒學習,明天的咱們必定會感謝今天努力的本身。

相關文章
相關標籤/搜索