從頭實現一個簡易版React(一)地址:https://segmentfault.com/a/11...
上一節,咱們詳細介紹了實現一個簡易React的思路以及總體的結構,可是對於渲染和更新的原理,卻尚未說起,所以,本節咱們將重點放在vDom的渲染上。javascript
咱們把React元素分爲text,basic,custom三種,並分別封裝了三種vDom的ReactComponent,用來處理各自的渲染和更新,在這裏,咱們將重心放在各自ReactComponet的mount方法上。html
ReactTextComponent用來處理文本節點,爲了標識方便,在返回的內容上加了span標籤。java
// 用來表示文本節點在渲染,更新,刪除時應該作的事情 class ReactTextComponent extends ReactComponent { // 渲染 mountComponent(rootId) { this._rootNodeId = rootId return `<span data-reactid="${rootId}">${this._vDom}</span>` } } //代碼地址:src/react/component/ReactTextComponent.js
ReactTextComponent的mount方法很是簡單,打上標識符,將內容插入標籤內,並把標籤內容返回就能夠了。react
這個類用來處理原生節點的vDom,在將vDom渲染爲原生DOM時,要考慮3點:git
代碼以下:github
// 用來表示原生節點在渲染,更新,刪除時應該作的事情 class ReactDomComponent extends ReactComponent { constructor(vDom) { super(vDom) this._renderedChildComponents = null } // 渲染 mountComponent(rootId) { this._rootNodeId = rootId const { props, type, props: { children = [] } } = this._vDom, childComponents = [] // 設置tag,加上標識 let tagOpen = `${type} data-reactid=${this._rootNodeId}`, tagClose = `/${type}`, content = '' // 拼湊屬性 for (let propKey in props) { // 事件 if (/^on[A-Za-z]/.test(propKey)) { const eventType = propKey.replace('on', '') $(document).delegate(`[data-reactid="${this._rootNodeId}"]`, `${eventType}.${this._rootNodeId}`, props[propKey]) } // 普通屬性,排除children與事件 if (props[propKey] && propKey !== 'children' && !/^on[A-Za-z]/.test(propKey)) { tagOpen += ` ${propKey}=${props[propKey]}` } } // 獲取子節點渲染出的內容 children.forEach((item, index) => { // 再次使用工廠方法實例化子節點的component,拼接好返回 const childComponent = instantiateReactComponent(item) childComponent._mountIndex = index childComponents.push(childComponent) // 子節點的rootId是父節點的rootId加上索引拼接的值 const curRootId = `${this._rootNodeId}.${index}` // 獲得子節點的渲染內容 const childMarkup = childComponent.mountComponent(curRootId) // 拼接 content += childMarkup // 保存全部子節點的component this._renderedChildComponents = childComponents }) return `<${tagOpen}>${content}<${tagClose}>` } } //代碼地址:src/react/component/ReactDomComponent.js
在React的官方實現中,本身實現了一套事件系統,這裏用了jQuery的事件代替。
在樣式上,須要基於傳入的style對象建立樣式,這裏也暫時忽略了。算法
在建立自定義組件時,一般會這樣建立segmentfault
import React from 'react' class App extends React.Component { render() { return ( ) } }
因此,第一步,咱們先實現Component這個父類dom
// 全部自定義組件的父類 class Component { constructor(props) { this.props = props } setState(newState) { this._reactInternalInstance.updateComponent(null, newState) } } //代碼地址:src/react/Component.js
Component類上咱們主要實現了setState方法,至於有什麼用,咱們放在更新裏說。
在自定義組件的vDom中,type保存的是咱們建立的Component的引用,因此在ReactCompositeComponent的mount方法中。咱們首先根據vDom的type建立組件的實例,在以此調用它初始渲染的生命週期方法,render方法。
在render方法中,返回了組件渲染內容的vDom,咱們根據這個vDom建立它的ReactComponent並調用mount(),就獲得了真實的渲染內容。
貼代碼:this
export default class extends ReactComponent { constructor(element) { super(element) // 存放對應的組件實例 this._instance = null this._renderedComponent = null } // 渲染 mountComponent(rootId) { this._rootNodeId = rootId const { type: Component, props } = this._vDom // 獲取自定義組件的實例 const inst = new Component(props) this._instance = inst // 保留對當前component的引用,下面更新時會用到 inst._reactInternalInstance = this inst.componentWillMount && inst.componentWillMount() // 調用自定義組件的render方法,返回一個Vdom const renderedVdom = inst.render() // 獲取renderedComponent的component const renderedComponent = instantiateReactComponent(renderedVdom) this._renderedComponent = renderedComponent // 獲得渲染以後的內容 const renderMarkup = renderedComponent.mountComponent(this._rootNodeId) // 在React.render方法最後觸發了mountReady事件,所在在這裏監聽,在渲染完成後觸發 $(document).on('mountReady', () => { inst.componentDidMount && inst.componentDidMount() }) return renderMarkup } } // 代碼地址:src/react/component/ReactCompositeComponent.js
從這裏能夠看出,自定義組件的mount方法並不負責具體的渲染,這些都交給了它的render,它把重心放在了建立對象和調用生命週期上。
文章到這,咱們的簡易版react已經初步實現了虛擬DOM的建立,生命週期的調用,虛擬DOM的遞歸渲染和事件處理。
總結一下,每個vDom都有ReactComponent相對應,遞歸渲染的本質無非就是獲取每一個vDom的ReactComponent,並調用它的mount方法。
以上就是整個渲染的思路,下節咱們將實現它的diff算法以及更新。
下一節地址:https://segmentfault.com/a/11...
參考資料,感謝幾位前輩的分享:
https://www.cnblogs.com/sven3...
https://github.com/purplebamb...陳屹 《深刻React技術棧》