標籤:

React首次渲染

原文:深入理解React源碼 - 首次渲染 I

界面更新本質上就是數據的變化。通過把所有會動的東西收斂到狀態(state),React提供了一個非常直觀的前端框架。我也比較喜歡review基於React代碼,因為我一般都是從數據結構開始看,這樣可以在鑽到細節代碼之前建立對整個邏輯的初步理解。我也經常會好奇React的實現方式,然後就有了這篇文章。

我一直認為項目的可控,離不開對底層庫實現的理解。不管是魔改,貢獻代碼,還是日常升級都可以更穩了。

這篇會通過渲染一個簡單的組件來打通React的一條關鍵路徑。(組合組件,界面更新等其他主題會在後續文章中討論)

本文用到的文件:

isomorphic/React.js: ReactElement.createElement()的入口

isomorphic/classic/element/ReactElement.js:ReactElement.createElement()的具體實現

renderers/dom/ReactDOM.js: ReactDOM.render()的入口

renderers/dom/client/ReactMount.js: ReactDom.render()的具體實現

renderers/shared/stack/reconciler/instantiateReactComponent.js: 基於元素類型創建組件 (ReactComponents)

renderers/shared/stack/reconciler/ReactCompositeComponent.js: 頂級元素的ReactComponents 包裝

調用棧里用到的標籤

- 函數調用

= 別名

~ 間接調用

由於React對組件進行了扁平化處理,文件的位置不太容易從import語句中看到,所以我會用@標籤在代碼塊中標註其對應的文件路徑。

從JSX到React.createElement()

JSX是在編譯的時候由Babel轉譯成React.createElement()調用的。舉例來說,create-react-app 自帶的App.js:

import React, { Component } from 『react』;nimport logo from 『./logo.svg』;nimport 『./App.css』;nclass App extends Component {n render() {n return (n <div className=」App」>n <header className=」App-header」>n <img src={logo} className=」App-logo」 alt=」logo」 />n <h1 className=」App-title」>Welcome to React</h1>n </header>n <p className=」App-intro」>n To get started, edit <code>src/App.js</code> and save to reload.n </p>n </div>n );n }n}nexport default App;n

會被轉譯成:

import React, { Component } from 『react』;nimport logo from 『./logo.svg』;nimport 『./App.css』;nclass App extends Component {n render() {n return React.createElement(n 『div』,n { className: 『App』 },n React.createElement(n 『header』,n { className: 『App-header』 },n React.createElement(『img』, { src: logo, className: 『App-logo』, alt: 『logo』 }),n React.createElement(n 『h1』,n { className: 『App-title』 },n 『Welcome to React』n )n ),n React.createElement(n 『p』,n { className: 『App-intro』 },n 『To get started, edit 『,n React.createElement(n 『code』,n null,n 『src/App.js』n ),n 『 and save to reload.』n )n );n }n}nexport default App;n

然後這個函數返回的ReactElement 會在應用層的"index.js"渲染:

ReactDOM.render(n <App />,n document.getElementById(『root』)n);n

(這個過程應該都知道了)

上面的這個組件樹對於入門來說有點複雜了,所以最好先從簡單一點的??開始來撬開React的實現。

…nReactDOM.render(n <h1 stylex={{「color」:」blue」}}>hello world</h1>,n document.getElementById(『root』)n);n…n

轉譯後:

…nReactDOM.render(React.createElement(n 『h1』,n { style: { 「color」: 「blue」 } },n 『hello world』n), document.getElementById(『root』));n…n

React.createElement()?- 創建一個 ReactElement

第一步其實沒做啥。僅僅是實例化一個ReactElement,再用傳入的參數初始化它。這一步的目標結構是:

這一步的調用棧:

React.createElementn|=ReactElement.createElement(type, config, children)n |-ReactElement(type,…, props)n

1. React.createElement(type, config, children) 僅僅是 ReactElement.createElement()的一個別名;

…nvar createElement = ReactElement.createElement;n…nvar React = {n…n createElement: createElement,n…n};nmodule.exports = React;nnReact@isomorphic/React.jsn

2. ReactElement.createElement(type, config, children) 做了三件事: 1) 把 config里的數據一項一項拷入props, 2) 拷貝 childrenprops.children, 3) 拷貝 type.defaultPropsprops;

…n // 1)n if (config != null) {n …extracting not interesting properties from config…n // Remaining properties are added to a new props objectn for (propName in config) {n if (n hasOwnProperty.call(config, propName) &&n !RESERVED_PROPS.hasOwnProperty(propName)n ) {n props[propName] = config[propName];n }n }n }n // 2)n // Children can be more than one argument, and those are transferred onton // the newly allocated props object.n var childrenLength = arguments.length — 2;n if (childrenLength === 1) {n props.children = children; // scr: one child is stored as objectn } else if (childrenLength > 1) {n var childArray = Array(childrenLength);n for (var i = 0; i < childrenLength; i++) {n childArray[i] = arguments[i + 2]; // scr: multiple children are stored as arrayn }n props.children = childArray;n }n // 3)n // Resolve default propsn if (type && type.defaultProps) {n var defaultProps = type.defaultProps;n for (propName in defaultProps) {n if (props[propName] === undefined) {n props[propName] = defaultProps[propName];n }n }n }n return ReactElement(n type,n key,n ref,n self,n source,n ReactCurrentOwner.current,n props,n );n…nnReactElement.createElement@isomorphic/classic/element/ReactElement.jsn

3. 然後 ReactElement(type,…, props) 會把 typeprops 原樣透傳給 ReactElement 的構造函數,並返回新構造的實例.

…nvar ReactElement = function(type, key, ref, self, source, owner, props) {n // This tag allow us to uniquely identify this as a React Elementn $$typeof: REACT_ELEMENT_TYPE,n// Built-in properties that belong on the elementn type: // scr: --------------> 『h1』n key: // scr: --------------> not of interest for nown ref: // scr: --------------> not of interest for nown props: {n children: // scr: --------------> 『hello world』n …other props: // scr: --------------> style: { 「color」: 「blue」 }n },n// Record the component responsible for creating this element.n _owner: // scr: --------------> nulln};n…nnReactElement@isomorphic/classic/element/ReactElement.jsn

這個新構建的ReactElement一會會在ReactMount.instantiateReactComponent() 函數中用到。因為下一步也會構建一個ReactElement我們先把這一步生成的對象命名為ReactElement[1]

ReactDom.render() - ?開始渲染

_renderSubtreeIntoContainer()? - 給ReactElement[1] 加上TopLevelWrapper

下一步的目標是把ReactElement[1]包裝到另外一個ReactElement,(我們叫它[2]吧),然後把ReactElement.type賦值為TopLevelWrapper。這個TopLevelWrapper的名字很能說明問題了-(傳入render()函數的)頂級元素的包裝:

這裡的TopLevelWrapper定義很重要,所以我在這裡打上三個星號***,方便你以後會到這篇文章時搜索。

…nvar TopLevelWrapper = function() {n this.rootID = topLevelRootCounter++;n};nTopLevelWrapper.prototype.isReactComponent = {};nTopLevelWrapper.prototype.render = function() { n// scr: this function will be used to strip the wrapper later in the // rendering processn return this.props.child;n};nTopLevelWrapper.isReactTopLevelWrapper = true;n…nnTopLevelWrapper@renderers/dom/client/ReactMount.js n

廢話一句,傳入ReactElement.type的是一個類型(TopLevelWrapper)。這個類型會在接下來的渲染過程中被實例化。而render()函數則是用於提取包含在this.props.childReactElement[1]

這一步的調用棧:

ReactDOM.rendern|=ReactMount.render(nextElement, container, callback)n|=ReactMount._renderSubtreeIntoContainer(n parentComponent, // scr: --------------> nulln nextElement, // scr: --------------> ReactElement[1]n container,// scr: --------------> document.getElementById(『root』)n callback』 // scr: --------------> undefinedn)n

對於首次渲染,ReactMount._renderSubtreeIntoContainer()其實比它看起來簡單很多,因為大部分的分支都被跳過了。這個階段函數中唯一有效的代碼是:

…n var nextWrappedElement = React.createElement(TopLevelWrapper, {n child: nextElement,n });n…nn_renderSubtreeIntoContainer@renderers/dom/client/ReactMount.js n

我們剛看過React.createElement(),這一步的構建過程應該很好理解。這裡就不贅述了。

instantiateReactComponent()? - ?用 ReactElement[2]創建一個 ReactCompositeComponent

這一步會為頂級組件創建一個初始的ReactCompositeComponent

調用棧:

ReactDOM.rendern|=ReactMount.render(nextElement, container, callback)n|=ReactMount._renderSubtreeIntoContainer()n |-ReactMount._renderNewRootComponent(n nextWrappedElement, // scr: ------> ReactElement[2]n container, // scr: ------> document.getElementById(『root』)n shouldReuseMarkup, // scr: null from ReactDom.render()n nextContext, // scr: emptyObject from ReactDom.render()n )n |-instantiateReactComponent(n node, // scr: ------> ReactElement[2]n shouldHaveDebugID /* false */n )n |-ReactCompositeComponentWrapper(n element // scr: ------> ReactElement[2]n );n |=ReactCompositeComponent.construct(element)n

instantiateReactComponent是唯一一個比較複雜的函數。在這次的上下文中,這個函數會根據這個欄位ReactElement[2].type 的值(TopLevelWrapper),然後創建一個ReactCompositeComponent

function instantiateReactComponent(node, shouldHaveDebugID) {n var instance;n…n } else if (typeof node === 『object』) {n var element = node;n var type = element.type;n…n // Special case string valuesn if (typeof element.type === 『string』) {n…n } else if (isInternalComponentType(element.type)) {n…n } else {n instance = new ReactCompositeComponentWrapper(element);n }n } else if (typeof node === 『string』 || typeof node === 『number』) {n…n } else {n…n }n…n return instance;n}nninstantiateReactComponent@renderers/shared/stack/reconciler/instantiateReactComponent.js n

這裡比較值得注意的是new ReactCompositeComponentWrapper()

…n// To avoid a cyclic dependency, we create the final class in this modulenvar ReactCompositeComponentWrapper = function(element) {n this.construct(element);n};n…n…nObject.assign(n ReactCompositeComponentWrapper.prototype,n ReactCompositeComponent,n {n _instantiateReactComponent: instantiateReactComponent,n },n);n…nnReactCompositeComponentWrapper@renderers/shared/stack/reconciler/instantiateReactComponent.js n

實際會直接調用ReactCompositeComponent的構造函數:

construct: function(element /* scr: ------> ReactElement[2] */) {n this._currentElement = element;n this._rootNodeID = 0;n this._compositeType = null;n this._instance = null;n this._hostParent = null;n this._hostContainerInfo = null;n// See ReactUpdateQueuen this._updateBatchNumber = null;n this._pendingElement = null;n this._pendingStateQueue = null;n this._pendingReplaceState = false;n this._pendingForceUpdate = false;nthis._renderedNodeType = null;n this._renderedComponent = null;n this._context = null;n this._mountOrder = 0;n this._topLevelWrapper = null;n// See ReactUpdates and ReactUpdateQueue.n this._pendingCallbacks = null;n// ComponentWillUnmount shall only be called oncen this._calledComponentWillUnmount = false;n},nnReactCompositeComponent@renderers/shared/stack/reconciler/ReactCompositeComponent.jsn

在後續的步驟里ReactCompositeComponent還會被instantiateReactComponent()創建, 所以我們把這一步生成的對象命名為ReactCompositeComponent[T] (T 代表 top)。

ReactCompositeComponent[T] 創建以後, 下一步React會調用 batchedMountComponentIntoNode, 來初始化這個組件對象,然後渲染它並插入DOM樹中。 這個過程留到下篇討論。

推薦閱讀:

《深入 React 技術棧》章節試讀
koa 實現 react-view 原理
Web Component 和類 React、Angular、Vue 組件化技術誰會成為未來?

TAG:React |