從零造 React (2) - Mounting
來自專欄 編程小思
在做好一定的預備知識的學習後,本篇我們只研究一個問題:
React 是如何把 Component 中的 JSX 映射到頁面上真正的 DOM 節點的。這個流程是怎樣的?
面向測試編程
我們首先寫一個小 demo,用於測試我們最終的代碼:
const Dilithium = require(../dilithium)class App extends Dilithium.Component { render() { return ( <div> <div> <h1 style={{ color: red }} >Heading 1</h1> <SmallHeader /> <h2 style={{ color: yellow }} >Heading 2</h2> </div> <h3>Heading 3</h3> </div> ) }}class SmallHeader extends Dilithium.Component { render() { return ( <h5>SmallHeader</h5> ) }}Dilithium.render(<App />, document.getElementById(root))
可以看出,基本用法是跟 React 一致的。
至於 Dilithium.render
函數,我們有如下的實現:
function render(element, node) { // todo: add update mount(element, node)}
The Mounting Process Overview
在我們開始分析之前,首先給出這個答案的流程圖:
根據這個流程,我們給出 mount
的實現:
function mount(element, node) { const component = instantiateComponent(element) const renderedNode = component.mountComponent() // these are just helper functions of native DOM functions // you can check them out in dilithium/src/DOM.js DOM.empty(node) DOM.appendChildren(node, renderedNode)}
然後根據流程的各個環節逐步開始分析。
JSX -> Element
在 React 中,我們使用的組件有兩種,class component 或是 functional component. 對於 class component 來說,render 函數是組件內必不可少的;而對於 functional component,組件沒有生命周期和 local state,組件函數返回值等同於 class component 中 render 函數的返回值。
無論是 render 函數的返回值,還是函數是組件的返回值,它們都是 JSX。JSX 是 React.createElement(type, props, ...children)
函數的語法糖,如果你還不熟悉,建議先閱讀 JSX in Depth,然後可以在 Try it out 中試一下 JSX 和 createElement
的映射關係。
我們知道,JSX 只是調用了函數 React.createElement
,並把對應的 JSX 結構映射到了 createElement
相應的參數中去。例如:
<div className="container"> good <span>Hello world</span></div>
會被編譯成:
React.createElement( "div", { className: "container" }, "good", React.createElement( "span", null, "Hello world" ));
那麼 createElement
這個函數又做了什麼事情呢?
function createElement(type, config, children) { const props = Object.assign({}, config) const childrenLength = [].slice.call(arguments).length - 2 if (childrenLength > 1) { props.children = [].slice.call(arguments, 2) } else if (childrenLength === 1) { props.children = children } return { type, props }}
一言以蔽之,createElement
就是將 children
合併進了當前 Element 對象,成為了其中的 children
屬性。
這樣,對於一個 JSX 結構,我們最終得到了一個數據結構如下的純 JS Object,也就是 Element。
{ type: string | function | class props: { children }}
(Note: 暫時不支持函數式組件)
Element -> Component
有了 Element 後,我們需要將 Element 中對應的組件類型(type
)實例化,也就是 instantiateComponent
。在前文提到,element type 有三種:
- string, 例如
"div", "ul"
等原生 DOM 結構。 - function, 函數式組件(暫不支持)
- class component
但是我們也要考慮另一種情況,element 本身是一個字元串或數字(並沒有被組件包裹)。這樣,我們根據不同情況,分別生成不同的組件類型:
function instantiateComponent(element) { let componentInstance if (typeof element.type === function) { // todo: add functional component // only supports class component for now componentInstance = new element.type(element.props) componentInstance._construct(element) } else if (typeof element.type === string) { componentInstance = new DOMComponent(element) } else if (typeof element === string || typeof element === number) { // to reduce overhead, we wrap the text with a span componentInstance = new DOMComponent({ type: span, props: { children: element } }) } return componentInstance}
Component -> DOM Nodes
在討論這點之前,我們先討論一下 「多態」(polymorphism)。這是 OOP 中很重要的一個概念,在 instantiateComponent
中,我們根據參數 element
類型的不同,調用了不同的方法,本質上是一種 「函數多態」。而在這一環節,我們專門將多態抽離出來,構成一個 `Reconciller:
// Reconcillerfunction mountComponent(component) { return component.mountComponent()}
同時,我們在不同類型中的 component 中,分別實現同名的方法(mountComponent
):
在 Class Component 中,我們看到,mountComponent
和實際的 mount
流程非常相似,都是 element -> component -> node。這裡由於 class component 本身沒有對應的 DOM 映射,所以 mount 的過程 defer 到了下一層組件。
// Componentclass Component { constructor(props) { this.props = props this.currentElement = null this._renderedComponent = null this._renderedNode = null } _construct(element) { this.currentElement = element } mountComponent() { // we simply assume the render method returns a single element const renderedElement = this.render() const renderedComponent = instantiateComponent(renderedElement) this._renderedComponent = renderedComponent const renderedNode = Reconciler.mountComponent(renderedComponent) this._renderedNode = renderedNode return renderedNode }}
class DOMComponent { constructor(element) { this._currentElement = element this._domNode = null } mountComponent() { // create real dom nodes const node = document.createElement(this._currentElement.type) this._domNode = node this._updateNodeProperties({}, this._currentElement.props) this._createInitialDOMChildren(this._currentElement.props) return node }}
我們暫不分析 _updateNodeProperties
和 _createInitialDOMChildren
這兩個函數方法的細節(留到下篇博客),從字面意思可以看出,這兩個函數分別是將 element.props
掛載到真正的 DOM 節點上,以及遞歸 mount 子節點。最終返回當前這個 DOM 節點。
回顧一下 mount
函數:
function mount(element, node) { const component = instantiateComponent(element) const renderedNode = component.mountComponent() DOM.empty(node) DOM.appendChildren(node, renderedNode)}
到這裡 const renderedNode = component.mountComponent()
,我們已經拿到了真正的 DOM 節點,剩下的工作非常簡單。首先清空 container 里的內容,然後將 renderedNode append 上去。
如下是兩個 DOM helper function:
function empty(node) { [].slice.call(node.childNodes).forEach((child) => { node.removeChild(child) })}function appendChildren(node, children) { if (Array.isArray(children)) { children.forEach((child) => { node.appendChild(child) }) } else { node.appendChild(children) }}
至此,我們已經走完了 mounting 整個的流程。完整的代碼實現(僅 mounting 部分)在這裡
在理解這個流程的時候,我個人認為有這幾個個關鍵點,你也可以把它們作為檢驗你是否真正理解這個過程的幾個題目。
- Element, Component, Instance 的區別是什麼
- 四種不同的 Element 類型分別是怎樣 mount 成真正的 DOM 節點的
- Class Component 是怎樣 defer mount 的
- DOM Component 是怎樣實現真正的 mount 的
但是,在最後的 DOM Component 中,我們有兩個問題 / 函數還沒有講,分別是:
updateNodeProperties
createInitialDOMChildren
其中正是 createInitialDOMChildren
實現了 Element tree 的遞歸 mount。我們將在下一篇博客中完成最後這部分的分析。
推薦閱讀:
※如何看待 React-to-Vue 工具?
※jQuery === 麵條式代碼?
※React 中的函數式思想
※沒有安卓和ios開發經驗的前端適合學rn嗎?
※如何評價微軟開源的ReactXP?