『從零開始寫一個 React』 將會是一個小系列,記錄學習 React 源碼的過程,並逐步實現一個簡易的類 React 庫。html
這是本系列的第一篇文章,該文章將閱讀 React 初始化渲染相關的代碼,並實現一個簡單的將 JSX 渲染到頁面的功能。(不包括組件生命週期與事件處理相關部分)node
咱們知道在 React 組件render()
返回的是 JSX,而 JSX 將會被 babel 轉換。JSX 將被轉換爲 React.createElement(type, config, children)
的形式。react
// App.js // 轉換前 Class App extends Component { render() { return <h1 id='title'>Hello World<h1> } } // 轉換後 var App = React.createClass({ render() { return React.createElement('h1', { id: 'title' }, 'hello world') } })
React.createElement()
的實現位於 /src/isomorphic/classic/element/ReactElement.jsgit
這裏的 React.createElement()
是用來生成虛擬 DOM 元素,該函數對組件的屬性,事件,子組件等進行了處理,並返回值爲一個 ReactElement
對象(單純的 JavaScript 對象,僅包括 type, props, key, ref 等屬性)。github
這剛好說明了 JSX 中的 <h1 id='title'>hello world</h1>
其實是 JavaScript 對象,而不是咱們一般寫的 HTML 標籤。react-native
單單聲明瞭組件而沒有渲染到頁面上咱們是看不見的(廢話),因此咱們須要使用 ReactDOM.render()
將其渲染到頁面指定位置上。瀏覽器
// index.html <html> // ... <body> <div id='root'></div> </body> </html> // index.js import React from 'react' import ReactDOM from 'react-dom' import App from './App.js' ReactDOM.render(<App />, document.getElementById('root'))
ReactDOM.render()
的實現位於 /src/renderers/dom/client/ReactMount.jsbabel
ReactDOM.render()
函數將會根據 ReactElement
的類型生成相對應的ReactComponent
實例,並調用其 mountComponent()
函數進行組件加載(返回 HTML片斷),遞歸加載全部組件後,經過 setInnerHTML 將 HTML 渲染到頁面上。dom
判斷須要生成那種 ReactComponent
實例根據 ReactElement
對象的 type 屬性來決定。對應 HTML 標籤的 type 通常爲字符串,而自定義的組件則是大寫字母開頭的組件函數(自定義組件須要 import,而 HTML 標籤不須要)。函數
React 中生成對應的 ReactComponent
實例由 instantiateReactComponent()
完成,其實現位於 /src/renderers/shared/stack/reconciler/instantiateReactComponent.js
ReactComponent
分爲 3 種:
ReactEmptyComponent
: 空組件(ReactElement 的 type 屬性爲 null 或 false 的組件),在瀏覽器中返回 ReactDOMEmptyComponent
。
ReactHostComponent
: 原生組件(ReactElement 爲string,number 或 ReactElement 的 type 屬性爲 string 的組件)。
createInternalComponent()
:該函數用於建立原生組件,在瀏覽器中返回 ReactDOMComponent
。
createInstanceForText()
: 該函數用於建立純文本組件,在瀏覽器中返回 ReactDOMTextComponent
。
ReactCompositeComponent
: 自定義組件(ReactElement 的 type 屬性爲 function)
能夠發現 React 與平臺解耦,使用 ReactEmptyComponent
與 ReactHostComponent
。而這兩種組件會根據平臺的不一樣生成不一樣的組件對象,在瀏覽器中則爲 ReactDOMEmptyComponent
、ReactDOMComponent
與 ReactDOMTextComponent
。
它們經過 /src/renderers/dom/stack/client/ReactDOMStackInjection.js 進行注入。
( /src/renderers 路徑下包含各個平臺上不一樣的 ReactComponent 實現,包括 react-art/react-dom/react-native。)
首先咱們須要瞭解 babel 如何轉換 JSX:React JSX transform。
babel 能夠經過transform-react-jsx
插件來設置解析 JSX 以後調用的函數,默認解析爲調用 React.createElement()
。因此這就是爲何雖然在 JSX 代碼中沒有使用到 React,卻仍然須要導入它。
經過配置 transform-react-jsx
插件的 pragma
選項能夠修改解析後調用的函數。
// 修改解析爲調用 dom() 函數 { "plugins": [ ["transform-react-jsx", { "pragma": "dom" // 默認 pragma 爲 React.createElement }] ] }
babel 將會把 JSX 中的標籤名做爲第一個參數,把 JSX 中的標籤屬性做爲第二個參數,將標籤內容做爲剩餘的參數。傳遞這些參數給 pragma
選項設置的函數。
PS: 爲了方便起見,咱們使用默認的解析爲 React.createElement()
createElement()
接受至少 2 個參數:元素類型 type(字符串表示原生元素,函數表示自定義元素),元素設置 config。其餘參數視爲元素的子元素 children。而且該函數返回的是一個 ReactElement
對象,屬性包括 type, props, key, ref。
// element.js class ReactElement { constructor(type, props, key, ref) { this.type = type this.props = props this.key = key this.ref = ref } } export function createElement(type, config, ...children){ // ... return new ReactElement(type, props, key, ref) }
而後須要導出 createElement
,纔可以經過 React.createElement()
的方式調用。
// index.js import { createElement } from './element' const React = { createElement, } export default React
ReactElement
須要 props, key 與 ref 參數,這三個參數將經過處理 config 與 children 獲得。
咱們將從 config 中獲取 key 與 ref(若它們存在的話),而且根據 config 獲得 props (去除一些沒必要要的屬性),同時將 children 添加到 props 當中。
export function createElement(type, config, ...children) { let props = {} let key = null let ref = null if (config != null) { ref = config.ref === undefined ? null : config.ref // 當 key 爲數字時,將 key 轉換爲字符串 key = config.key === undefined ? null : '' + config.key for (let propsName in config) { // 剔除一些不須要的屬性(key, ref, __self, __source) if (RESERVED_PROPS.hasOwnProperty(propsName)) { continue } if (config.hasOwnProperty(propsName)) { props[propsName] = config[propsName] } } props.children = children } return new ReactElement(type, props, key, ref) }
除此以外,添加對 defaultProps
的支持。defaultProps
的使用方式以下:
// App.js class App extends Component { } App.defaultProps = { name: "ahonn" }
當傳入 App 組件的 props 中不包含 name 時,設置默認的 name 爲 "ahonn"。具體實現:當 ReactElement 的 type 屬性爲組件函數且包含 defaultProps 時遍歷 props,若 props 中不包含 defaultProps 中的屬性時,設置默認的 props。
export function createElement(type, config, ...children) { // ... if (type && type.defaultProps) { let defaultProps = type.defaultProps for (let propsName in defaultProps) { if (props[propsName] === undefined) { props[propsName] = defaultProps[propsName] } } } }
目前爲止完成了將 JSX 解析爲函數調用(這部分由 babel 完成),調用 React.createElement()
生成 ReactElement
對象。
接下來將實現 instantiateReactComponent()
,經過 ReactELemnt 生成相對應的 ReactComponent
實例。
instantiateReactComponent(element)
接受一個參數 element,該參數能夠是 ReactElement 對象,string,number,false 或者 null。
咱們將只考慮 Web 端,而不像 React 同樣使用適配器模式進行解耦。
ReactElement 生成相應 ReactComponent 實例的規則:
element 爲 null 或 false 時,生成 ReactDOMEmptyComponent 對象實例
element 爲 string 或者 number 時,生成 ReactDOMTextComponent 對象實例
element 爲 object
element.type 爲 string 時,生成 ReactDOMComponent 對象實例
element.type 爲 function(組件函數)時,生成 ReactCompositeComponent 對象實例
// virtual-dom.js export function instantiateReactComponent(element) { let instance = null if (element === null || element === false) { instance = new ReactDOMEmptyComponent() } if (typeof element === 'string' || typeof element === 'number') { instance = new ReactDOMTextComponent(element) } if (typeof element === 'object') { let type = element.type if (typeof type === 'string') { instance = new ReactDomComponent(element) } else if (typeof type === 'function'){ instance = new ReactCompositeComponent(element) } } return instance }
如今,咱們須要有不一樣的 ReactComponent
類以供 instantiateReactComponent()
使用。同時須要實現每一個類的 mountComponent()
方法來返回對應的 HTML 片斷。
ReactDOMEmptyComponent
ReactDOMEmptyComponent
表示空組件, mountComponent()
方法返回空字符串。
class ReactDOMEmptyComponent { constructor() { this._element = null } mountComponent() { return '' } }
ReactDOMTextComponent
ReactDOMTextComponent 表示 DOM 文本組件,mountComponent()
方法返回對應的字符串。
class ReactDOMTextComponent { constructor(text) { this._element = text this._stringText = '' + text this._rootID = 0 } mountComponent(rootID) { this._rootID = rootID return this._stringText } }
ReactDOMComponent
ReactDOMComponent 表示原生組件,即瀏覽器支持的標籤(div, p, h1, etc.)。mountConponent()
方法返回對應的 HTML 字符串。
class ReactDomComponent { constructor(element) { let tag = element.type this._element = element this._tag = tag.toLowerCase() this._rootID = 0 } mountComponent(rootID) { this._rootID = rootID if (typeof this._element.type !== 'string') { throw new Error('DOMComponent\'s Element.type must be string') } let ret = `<${this._tag} ` let props = this._element.props for (var propsName in props) { if (propsName === 'children') { continue } let propsValue = props[propsName] ret += `${propsName}=${propsValue}` } ret += '>' let tagContent = '' if (props.children) { tagContent = this._mountChildren(props.children) } ret += tagContent ret += `</${this._tag}>` return ret } }
ReactDOMComponent
的 mountComponent()
方法會相對複雜一點。具體實現思路是,經過 ReactElement
的 type 與 props 屬性拼接對應的 HTML 標籤。處理 props 的時候須要跳過 children 屬性,由於須要將子組件放在當前組件中。
當存在子組件(children)時,調用 _mountChildren(children)
將組件轉換爲對應的 HTML 片斷。具體過程是遍歷 children,轉換爲 ReactComponent
並調用其 mountComponent()
方法。
_mountChildren(children) { let result = '' for (let index in children) { const child = children[index] const childrenComponent = instantiateReactComponent(child) result += childrenComponent.mountComponent(index) } return result }
ReactCompositeComponent
ReactCompositeComponent 表示自定義的組件,mountComponent()
方法將根據提供的組件函數(element.type)實例化,並調用該組件的 render()
方法返回 ReactElement
對象。再經過instantiateReactComponent()
生成對應的 ReactComponent
,最後執行該 ReactComponent
的mountComponent()
方法。
class ReactCompositeComponent { constructor(element) { this._element = element this._rootId = 0 } mountComponent(rootID) { this._rootId = rootID if (typeof this._element.type !== 'function') { throw new Error('CompositeComponent\'s Element.type must be function') } const Component = this._element.type const props = this._element.props const instance = new Component(props) const renderedElement = instance.render() const renderedComponent = instantiateReactComponent(renderedElement) const renderedResult = renderedComponent.mountComponent(rootID) return renderedResult } }
經過 ReactCompositeComponent 將以前的 ReactComponent 聯繫起來,並遞歸調用 mountComponent
方法獲得一段 HTML。最後 render()
經過 node.innerHTML 將 HTML 字符串填到頁面上對應的容器中。
最後將以前的實現串起來,利用 innerHTML 將組件渲染到頁面上。
export function render(element, container) { const rootID = 0 const mainComponent = instantiateReactComponent(element) const containerContent = mainComponent.mountComponent(rootID) container.innerHTML = containerContent }
到這裏就基本上簡單的實現了 React 中將組件渲染到頁面上的部分。能夠經過一個簡單的例子驗證一下。
// index.js import React from './tiny-react' import ReactDOM from './tiny-react' import App from './App' ReactDOM.render(<App />, document.getElementById('root')) // App.js import React, { Component } from './tiny-react' class App extends Component { render() { return ( <div> <span>It is Work!</span> </div> ) } } export default App
頁面上將顯示 It is Work!
雖然沒有涉及到組件更新與組件生命週期,經過閱讀 React 的源碼基本上也對初始化渲染的過程有了必定的瞭解,但願對你有所幫助。
在此感謝 preact, react-lite, react-tiny 等項目,它們爲本文提供了很大幫助。
文中的全部代碼均於 tiny-react init-render ,感謝閱讀。