【React原始碼解讀】- 元件的實現

薄荷前端發表於2018-10-31

前言

react使用也有一段時間了,大家對這個框架褒獎有加,但是它究竟好在哪裡呢? 讓我們結合它的原始碼,探究一二!(當前原始碼為react16,讀者要對react有一定的瞭解)

15397566862932

回到最初

根據react官網上的例子,快速構建react專案

npx create-react-app my-app

cd my-app

npm start
複製程式碼

開啟專案並跑起來以後,暫不關心專案結構及語法糖,看到App.js裡,這是一個基本的react元件 我們console一下,看看有什麼結果。

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

class App extends Component {

  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <p>
            Edit <code>src/App.js</code> and save to reload.
          </p>
        </header>
      </div>
    );
  }
}

export default App;

console.log(<App/>)


複製程式碼

15397572879758

可以看到,<App/>元件其實是一個JS物件,並不是一個真實的dom。

ES6 引入了一種新的原始資料型別Symbol,表示獨一無二的值。有興趣的同學可以去阮一峰老師的ES6入門詳細瞭解一下

上面有我們很熟悉的props,ref,key,我們稍微修改一下console,看看有什麼變化。

console.log(<App key={1} abc={2}><div>你好,這裡是App元件</div></App>)
複製程式碼

15397577334580

可以看到,props,key都發生了變化,值就是我們賦予的值,props中巢狀了children屬性。可是為什麼我們嵌入的是div,實際上卻是一個物件呢?

開啟原始碼

/node_modules/react

15397580720896

首先開啟index.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');
}
複製程式碼

可以知道目前用上的是./cjs/react.development.js,直接開啟檔案。 根據最初的程式碼,我們元件<App/>用到了React.Component。找到React暴露的介面:

15397617558881

接著找到Component: Component方法,

function Component(props, context, updater) {
  this.props = props;
  this.context = context;
  // If a component has string refs, we will assign a different object later.
  this.refs = emptyObject;
  // We initialize the default updater but the real one gets injected by the
  // renderer.
  this.updater = updater || ReactNoopUpdateQueue;
}

Component.prototype.isReactComponent = {};

Component.prototype.setState = function (partialState, callback) {
  !(typeof partialState === 'object' || typeof partialState === 'function' || partialState == null) ? invariant(false, 'setState(...): takes an object of state variables to update or a function which returns an object of state variables.') : void 0;
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

Component.prototype.forceUpdate = function (callback) {
  this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');
};

複製程式碼

上面就是一些簡單的建構函式,也可以看到,我們常用的setState是定義在原型上的2個方法。

至此,一個<App/>元件已經有一個大概的雛形:

15397595217487

到此為止了嗎?這看了等於沒看啊,究竟元件是怎麼變成div的?render嗎? 可是全域性搜尋,也沒有一個function是render啊。

原來,我們的jsx語法會被babel編譯的。

15397600724075

這下清楚了,還用到了React.createElement

createElement: createElementWithValidation,
複製程式碼

通過createElementWithValidation,

function createElementWithValidation(type, props, children) {
······

  var element = createElement.apply(this, arguments);


  return element;
}
複製程式碼

可以看到,return了一個element,這個element又是繼承自createElement,接著往下找:

function createElement(type, config, children) {
  var propName = void 0;

  // Reserved names are extracted
  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 = {
    // 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
  };

······
  return element;
};
複製程式碼

誒,這裡好像返回的就是element物件,再看我們最初的<App/>的結構,是不是很像

15397606651880
驗證一下我們的探索究竟對不對,再每一個方法上我們都打上console,(注意,將App裡的子元素全部刪空,利於我們觀察)

15397611759810

React.createElement 、 createElementWithValidation 、 createElement 、 ReactElement,通過這些方法,我們用class宣告的React元件在變成真實dom之前都是ReactElement型別的js物件

createElementWithValidation:

  • 首先校驗type是否是合法的

15397657382603

  • 校驗了props是否符合設定的proptypes

15397667118968

  • 校驗了子節點的key,確保每個陣列中的元素都有唯一的key

15397667422295

createElement

  • type是你要建立的元素的型別,可以是html的div或者span,也可以是其他的react元件,注意大小寫
  • config中包含了props、key、ref、self、source等

15397667913454

  • 向props加入children,如果是一個就放一個物件,如果是多個就放入一個陣列。

15397668352993

  • 那如果type.defaultProps有預設的props時,並且對應的props裡面的值是undefined,把預設值賦值到props中

15397668766904

  • 也會對key和ref進行校驗

15397669476655

ReactElement

ReactElement就比較簡單了,建立一個element物件,引數裡的type、key、ref、props、等放進去,然後return了。最後呼叫Object.freeze使物件不可再改變。

元件的掛載

我們上面只是簡單的探究了<App/>的結構和原理,那它究竟是怎麼變成真實dom的呢

15397616989193

ReactDOM.render(<App />, document.getElementById('root'));
複製程式碼

我們接著用babel編譯一下:

15397619877496

原來ReactDOM.render呼叫的是render方法,一樣,找暴露出來的介面。


var ReactDOM = {
······
  render: function (element, container, callback) {
    return legacyRenderSubtreeIntoContainer(null, element, container, false, callback);
  },
······
};

複製程式碼

它返回的是一個legacyRenderSubtreeIntoContainer方法,這次我們直接打上console.log

15397629379495

這是列印出來的結果,

15397633591876

legacyRenderSubtreeIntoContainer 這個方法除主要做了兩件事:

  • 清除dom容器元素的子元素
while (rootSibling = container.lastChild) {
      {
        if (!warned && rootSibling.nodeType === ELEMENT_NODE && rootSibling.hasAttribute(ROOT_ATTRIBUTE_NAME)) {
          warned = true;
        }
      }
      container.removeChild(rootSibling);
    }
複製程式碼
  • 建立ReactRoot物件

15397648731115

原始碼暫時只讀到了這裡,關於React16.1~3的新功能,以及新的生命週期的使用和原理、Fiber究竟是什麼,我們將在後續文章接著介紹。

廣而告之

本文釋出於薄荷前端週刊,歡迎Watch & Star ★,轉載請註明出處。

歡迎討論,點個贊再走吧 。◕‿◕。 ~

相關文章