前端戰五渣學React——JSX & React.createElement() & React.ReactElement()源碼

最近《一拳超人》動畫更新第二季了,感受打鬥場面沒有第一季那麼燒錢了,可是劇情還挺好看的,就找了漫畫來看。琦玉老師真的厲害!!!打誰都一拳,就喜歡看老師一拳把那些上來就吹牛逼的反派打的稀爛,專治各類不服!!!javascript

招聘AD

阿里巴巴集團核心前端崗位css

薪資25到50html

一年通常至少16個月工資前端

有意者微信聯繫:Dell-JSvue

正文

三大民工框架

說到如今的前端,各類招聘JD上都會寫java

「對主流框架(React/Vue/Angular)有了解,至少深刻了解一種」react

或者是webpack

「精通MV*框架(React/Vue/Angular),至少熟練使用一種,有大型項目經驗」git

從中咱們能夠看出如今前端在工做中使用的框架幾乎造成了三足鼎立之勢,形如當初的「三大民工漫畫」——《海賊王》、《火影忍者》以及《死神》,而Angular又相似《死神》同樣,國內人氣低迷(我只是從招聘信息來看的。。。angular佈道者勿噴)。而React憑藉本身的靈活性和vue憑藉簡單好上手的優點,勢均力敵。這回就來主要講一講React的一大核心概念——JSX,以及對應的React.createElement()這個方法的源碼閱讀。github

本文閱讀須要具有如下知識儲備:

  1. JavaScript基本語法,用js開發過項目最好
  2. 最好使用過react,沒用過的😅可能。。。

JSX

(瞭解的能夠直接跳到下一節看React.createElement()的源碼)

話很少說,讓咱們來實現一個功能:

建立一個div標籤,class名爲「title」,內容爲「你好 前端戰五渣」

看下面的代碼⬇️

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <!-- 引入react核心代碼 -->
  <script src="https://cdn.bootcss.com/react/16.8.6/umd/react.development.js"></script>
  <!-- 引入reactDom核心代碼 -->
  <script src="https://cdn.bootcss.com/react-dom/16.8.6/umd/react-dom.development.js"></script>
  <!-- 引入babel核心代碼 -->
  <script src="https://cdn.bootcss.com/babel-standalone/6.26.0/babel.min.js"></script>
  <title>JSX & React.createElement()</title>
</head>
<body>
  <!-- 使用javascript原生插入節點的根節點 -->
  <div id="rootByJs"></div>
  <!-- 使用React.createElement()方法插入節點的根節點 -->
  <div id="rootByReactCreateElement"></div>
  <!-- 使用JSX方法插入節點的根節點 -->
  <div id="rootByJsx"></div>
  <script> // 原生方法插入 let htmlNode = document.createElement('div'); htmlNode.innerHTML = '你好 前端戰五渣'; htmlNode.className = 'title'; document.getElementById('rootByJs').appendChild(htmlNode); </script>
  <script> // 使用React.createElement()方法插入 ReactDOM.render( React.createElement('div', {className: "title"}, '你好 前端戰五渣'), document.getElementById('rootByReactCreateElement') ); </script>
  <script type="text/babel"> // 使用JSX方法插入 ReactDOM.render( <div className="title">你好 前端戰五渣</div>, document.getElementById('rootByJsx') ); </script>
</body>
</html>
複製代碼

上面實現這個功能,用了三種方法,一種是js原生方法,一種是用react提供的createElement方法,還有最後一種使用JSX來實現的。

什麼是JSX

其實jsx就是react這個框架提出的一種語法擴展,在react建議使用jsx,由於jsx能夠清晰明瞭的描述DOM結構。可能到這裏咱們可能有人會說,這跟模板語言有什麼區別呢?template也能夠實現啊,可是JSX具備JavaScript的所有功能(官網這麼說的🤦‍♀️)

一句話總結:JSX語法就是JavaScript和html能夠混着寫,靈活的一筆

JSX的優勢呢?

  1. 能夠在js中寫更加語義化且簡單易懂的標籤
  2. 更加簡潔
  3. 結合原生js的語法

(也有人說jsx寫起來很亂,仁者見仁智者見智吧)

JSX和React.createElement()的關係

那咱們知道了JSX是什麼,但是這跟咱們這回要說的React.createElement()方法有什麼關係呢?先來回顧一個面試會問的問題「你能說說vue和react有什麼區別嗎」,有一個區別就是在使用webpack打包的過程當中,vue是用vue-loader來處理.vue後綴的文件,而react在打包的時候,是經過babel來轉換的,由於react的組件說白了仍是.js或者.jsx,是擴展的js語法,因此是經過babel轉換成瀏覽器識別的es5或者其餘版本的js

那咱們來看看jsx的語法經過babel轉換會變成什麼樣⬇️

咱們能夠看到經過babel轉換之後,咱們的JSX語法中的標籤會被轉換成一個React.createElement()並傳入對應的參數

ReactDOM.render(
  <div className="title">hello gedesiwen</div>,
  document.getElementById('rootByJsx')
);
複製代碼

變~

ReactDOM.render(
  React.createElement('div', {className: 'title'}, 'hello gedesiwen'),
  document.getElementById('rootByJsx')
);
複製代碼

這咱們看見了jsx變成了React.createElement()

多個子節點

上面的代碼中,咱們只是有一個子節點,就是文本節點「你好 前端戰五渣」,那若是咱們有不少個呢

咱們在React組件中代碼是這樣的⬇️

import DragonBall from './dragonBall';

let htmlNode = (
  <Fragment> <DragonBall name="孫悟空"/> <div className="hello" key={1}>hello</div> <div className="world" key={2}>world</div> </Fragment> ) ReactDOM.render( htmlNode, document.getElementById('rootByJsx') ); 複製代碼

咱們的節點中包括DragonBall組件,還有Fragment,而且還有兩個div

Fragment是幹什麼的呢???這就是JSX語法的一個規則,咱們只能有一個根節點,若是咱們有兩個並列的div,可是直接寫並列的兩個div會報錯,咱們就只能在外面套一層div,可是咱們不想建立不用的標籤,這時候咱們就能使用Fragment,他不會被渲染出來

React 中的一個常見模式是一個組件返回多個元素。Fragments 容許你將子列表分組,而無需向 DOM 添加額外節點。 ————react文檔

那上面這段咱們經過babel會轉換成這樣⬇️

var htmlNode = React.createElement(
    Fragment,
    null,
    React.createElement(_dragonBall.default, {name: "saiyajin"}),
    React.createElement("div", {className: "hello", key: 1}, "hello"),
    React.createElement("div", {className: "world", key: 2}, "world")
);
ReactDOM.render(htmlNode, document.getElementById('rootByJsx'));
複製代碼

這就是咱們轉換完的js,那咱們的React.createElement()方法到底作了什麼呢

React.createElement()源碼

首先咱們須要從github上把react的源碼,v16.8.6拉下來

而後咱們找到在文件/packages/react/src/ReactElement.js這個文件中就有咱們須要的React.createElement()方法

(代碼中左右判斷__DEV__的代碼,不作考慮)

先上完整的方法代碼,伴有註釋

/** * React的建立元素方法 * @param type 標籤名字符串(如’div‘或'span'),也能夠是React組件類型,或是React fragment類型 * @param config 包含元素各個屬性鍵值對的對象 * @param children 包含元素的子節點或者子元素 */
function createElement(type, config, children) {
  let propName; // 聲明一個變量,儲存後面循環須要用到的元素屬性
  const props = {}; // 儲存元素屬性的鍵值對集合
  let key = null; // 儲存元素的key值
  let ref = null; // 儲存元素的ref屬性
  let self = null;  // 下面文章介紹
  let source = null; // 下面文章介紹

  if (config != null) { // 判斷config是否爲空,看看是否是沒有屬性
    // hasValidRef()這個方法就是判斷config有沒有ref屬性,有的話就賦值給以前定義好的ref變量
    if (hasValidRef(config)) {
      ref = config.ref;
    }
    // hasValidKey()這個方法就是判斷config有沒有key屬性,有的話就賦值給以前定義好的key變量
    if (hasValidKey(config)) {
      key = '' + config.key; // key值看來還給轉成了字符串😳
    }
    // __self和__source下面文章作介紹,實際也沒搞明白是幹嗎的
    self = config.__self === undefined ? null : config.__self;
    source = config.__source === undefined ? null : config.__source;
    // 如今就是要把config裏面的屬性都一個一個挪到props這個以前聲明好的對象裏面
    for (propName in config) {
      if (
        // 判斷某個config的屬性是否是原型上的
        hasOwnProperty.call(config, propName) &&  // 這行判斷是否是原型鏈上的屬性
        !RESERVED_PROPS.hasOwnProperty(propName) // 不能是原型鏈上的屬性,也不能是key,ref,__self以及__source
      ) {
        props[propName] = config[propName]; // 乾坤大挪移,把config上的屬性一個一個轉到props裏面
      }
    }
  }
  // 處理除了type和config屬性剩下的其餘參數
  const childrenLength = arguments.length - 2; // 拋去type和config,剩下的參數個數
  if (childrenLength === 1) { // 若是拋去type和config,就只剩下一個參數,就直接把這個參數的值賦給props.children
    props.children = children; // 一個參數的狀況通常是隻有一個文本節點
  } else if (childrenLength > 1) { // 若是不是一個呢??
    const childArray = Array(childrenLength); // 聲明一個有剩下參數個數的數組
    for (let i = 0; i < childrenLength; i++) { // 而後遍歷,把每一個參數賦值到上面聲明的數組裏
      childArray[i] = arguments[i + 2];
    }
    props.children = childArray; // 最後把這個數組賦值給props.children
  } // 因此props.children要不是一個字符串,要不就是一個數組

  // 若是有type而且type有defaultProps屬性就執行下面這段
  // 那defaultProps屬性是啥呢??
  // 若是傳進來的是一個組件,而不是div或者span這種標籤,可能就會有props,從父組件傳進來的值若是沒有的默認值
  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) { // 遍歷,而後也放到props裏面
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }
  // 因此props裏面存的是config的屬性值,而後還有children的屬性,存的是字符串或者數組,還有一部分defaultProps的屬性
  // 而後返回一個調用ReactElement執行方法,並傳入剛纔處理過的參數
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );
}
複製代碼

React.createElement()方法的代碼加註釋就是上面這個,小夥伴們應該都能看懂了吧,只是其中其中還有__self__source以及type.defaultProps沒有講清楚,那咱們下面會講到,咱們能夠先來看看這個最後返回的ReactElement()方法

ReactElement()源碼

這個方法很簡單,就是添加一個判斷爲react元素類型的值,而後返回,

/** * @param {*} type * @param {*} props * @param {*} key * @param {string|object} ref * @param {*} owner * @param {*} self A *temporary* helper to detect places where `this` is * different from the `owner` when React.createElement is called, so that we * can warn. We want to get rid of owner and replace string `ref`s with arrow * functions, and as long as `this` and owner are the same, there will be no * change in behavior. * * 這雖說了用於判斷this指向的,可是。。。。。方法裏面也沒有用到,不知道是幹嗎的😳😳😳😳 * * @param {*} source An annotation object (added by a transpiler or otherwise) * indicating filename, line number, and/or other information. * * 這個參數同樣。。。。也沒有用到啊。。。那我傳進來是幹嗎的,什麼註釋對象。。😳😳😳搞不懂 * * @internal */
const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    $$typeof: REACT_ELEMENT_TYPE, // 聲明一下是react的元素類型
    type: type,
    key: key,
    ref: ref,
    props: props,
    _owner: owner,
  };
  return element;
};
複製代碼

__self和__source

剛看到React.createElement()方法裏面就用到了__self__source兩個屬性,當時還去查了一下react的文檔

文檔中也沒有說是幹嗎用的,而後查了一下issues

發現是這哥們提交的commit,好😳😳😳他說_self是用來判斷this和owner是否是同一個指向巴拉巴拉的,他還說__source是什麼註釋對象,我也沒看懂是幹嗎的。。。。而後繼續往下看,看到React.createElement()方法返回ReactElement()方法,而且把這些都傳進去了。。。。

ReactElement源碼中居然沒有用這兩個參數

大哥你開心就好🤡🤡🤡

看到這篇文章的大佬有知道是幹嗎的能夠告訴我。。。。我反正如今是懵逼的😶😶😶

type.defaultProps

這個是什麼呢,咱們來看一段代碼吧

import React, { Component } from 'react';
import ReactDom from 'react-dom';

class DragonBall extends Component {
  render() {
    return (
      <div> {this.props.name} </div>
    )
  }
}

ReactDom.render(<DragonBall />, document.getElementById('root')) 複製代碼

若是我這個DragonBall組件須要展現從props傳過來,若是咱們沒傳呢,就會是undefined,就什麼都不顯示,若是咱們想設置默認值呢,能夠這麼寫⬇️

import React, { Component } from 'react';
import ReactDom from 'react-dom';

class DragonBall extends Component {
  render() {
    return (
      <div> {this.props.name || '戈德斯文'} </div>
    )
  }
}

ReactDom.render(<DragonBall />, document.getElementById('root')) 複製代碼

就是像上面這樣寫,這樣咱們就進行了一次判斷,若是props.name若是沒有的話,就顯示後面的「戈德斯文」,那還有沒有什麼別的辦法呢??

想也知道啊,確定就是咱們說的這個defaultProps,這個東西怎麼用呢⬇️

import React, { Component } from 'react';
import ReactDom from 'react-dom';

class DragonBall extends Component {
  render() {
    return (
      <div> {this.props.name} </div>
    )
  }
}

DragonBall.defaultProps = {
  name: '戈德斯文'
}

ReactDom.render(<DragonBall />, document.getElementById('root')) 複製代碼

咱們只須要這樣設置就能夠,若是咱們頁面中不少地方須要用到props傳進來的值,就不須要每一個用到props值的地方都進行一次判斷了

因此,在React.createElement()源碼中

if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) { // 遍歷,而後也放到props裏面
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }
複製代碼

這段代碼就是把默認的props從新賦值。

回到開始

通過React.createElement()方法處理,而且通過ReactElement()方法洗禮,咱們最開始的

let htmlNode = React.createElement(
  Fragment, 
  null, 
  React.createElement(_dragonBall.default, null), 
  React.createElement("div", null, "hello"), 
  React.createElement("div", null, "world")
);
ReactDOM.render(htmlNode, document.getElementById('rootByJsx'));
複製代碼

最後究竟是變成什麼樣的呢?

{
	"key": null,
	"ref": null,
	"props": {
		"children": [{
			"key": null,
			"ref": null,
			"props": {
				"name": "saiyajin"
			},
			"_owner": null,
			"_store": {}
		}, {
			"type": "div",
			"key": "1",
			"ref": null,
			"props": {
				"className": "hello",
				"children": "hello"
			},
			"_owner": null,
			"_store": {}
		}, {
			"type": "div",
			"key": "2",
			"ref": null,
			"props": {
				"className": "world",
				"children": "world"
			},
			"_owner": null,
			"_store": {}
		}]
	},
	"_owner": null,
	"_store": {}
}
複製代碼

而後再通過ReactDom.render()方法渲染到頁面上

ps:端午節快樂~~回家過節嘍


我是前端戰五渣,一個前端界的小學生。

參考

  1. 《剖析 React 源碼:先熱個身》
  2. React:issues adding __self and __source special props
  3. 《Understanding React Default Props》
相關文章
相關標籤/搜索