摘要:寫在開頭從頭實現一個簡易版二地址在上一節,我們的已經具備了渲染功能。參考資料,感謝幾位前輩的分享陳屹深入技術棧
寫在開頭
從頭實現一個簡易版React(二)地址:https://segmentfault.com/a/11...
在上一節,我們的react已經具備了渲染功能。
在這一節我們將著重實現它的更新,說到更新,大家可能都會想到React的diff算法,它可以說是React性能高效的保證,同時也是最神秘,最難理解的部分(個人覺得),想當初我也是看了好多文章,敲了N次代碼,調試了幾十遍,才總算理解了它的大概。在這也算是把我的理解闡述出來。
同樣,我們會實現三種ReactComponent的update方法。不過在這之前,我們先想想,該如何觸發React的更新呢?沒錯,就是setState方法。
// 所有自定義組件的父類 class Component { constructor(props) { this.props = props } setState(newState) { this._reactInternalInstance.updateComponent(null, newState) } } //代碼地址:src/react/Component.js
這里的reactInternalInstance就是我們在渲染ReactCompositeComponent時保存下的自身的實例,通過它調用了ReactCompositeComponent的update方法,接下來,我們就先實現這個update方法。
ReactCompositeComponent這里的update方法同mount有點類似,都是調用生命周期和render方法,先上代碼:
class ReactCompositeComponent extends ReactComponent { constructor(element) { super(element) // 存放對應的組件實例 this._instance = null this._renderedComponent = null } mountComponent(rootId) { //內容略 } // 更新 updateComponent(nextVDom, newState) { // 如果有新的vDom,就使用新的 this._vDom = nextVDom || this._vDom const inst = this._instance // 獲取新的state,props const nextState = { ...inst.state, ...newState } const nextProps = this._vDom.props // 判斷shouldComponentUpdate if (inst.shouldComponentUpdate && (inst.shouldComponentUpdate(nextProps, nextState) === false)) return inst.componentWillUpdate && inst.componentWillUpdate(nextProps, nextState) // 更改state,props inst.state = nextState inst.props = nextProps const prevComponent = this._renderedComponent // 獲取render新舊的vDom const prevRenderVDom = prevComponent._vDom const nextRenderVDom = inst.render() // 判斷是需要更新還是重新渲染 if (shouldUpdateReactComponent(prevRenderVDom, nextRenderVDom)) { // 更新 prevComponent.updateComponent(nextRenderVDom) inst.componentDidUpdate && inst.componentDidUpdate() } else { // 重新渲染 this._renderedComponent = instantiateReactComponent(nextRenderVDom) // 重新生成對應的元素內容 const nextMarkUp = this._renderedComponent.mountComponent(this._rootNodeId) // 替換整個節點 $(`[data-reactid="${this._rootNodeId}"]`).replaceWith(nextMarkUp) } } } //代碼地址:src/react/component/ReactCompositeComponent.js
有兩點要說明:
熟悉React的都知道,很多時候組件的更新,vDom并沒有變化,我們可以通過shouldComponentUpdate這個生命周期來優化這點,當shouldComponentUpdate為false時,直接return,不執行下面的代碼。
當調用render獲取到新的vDom時,將會比較新舊的vDom類型是否相同,這也屬于diff算法優化的一部分,如果類型相同,則執行更新,反之,就重新渲染。
// 判斷是更新還是渲染 function shouldUpdateReactComponent(prevVDom, nextVDom) { if (prevVDom != null && nextVDom != null) { const prevType = typeof prevVDom const nextType = typeof nextVDom if (prevType === "string" || prevType === "number") { return nextType === "string" || nextType === "number" } else { return nextType === "object" && prevVDom.type === nextVDom.type && prevVDom.key === nextVDom.key } } } //代碼地址:src/react/component/util.js
注意,這里我們使用到了key,當type相同時使用key可以快速準確得出兩個vDom是否相同,這是為什么React要求我們在循環渲染時必須添加key這個props。
ReactTextComponentReactTextComponent的update方法非常簡單,判斷新舊文本是否相同,不同則更新內容,直接貼代碼:
class ReactTextComponent extends ReactComponent { mountComponent(rootId) { //省略 } // 更新 updateComponent(nextVDom) { const nextText = "" + nextVDom if (nextText !== this._vDom) { this._vDom = nextText } // 替換整個節點 $(`[data-reactid="${this._rootNodeId}"]`).html(this._vDom) } // 代碼地址:src/react/component/ReactTextComponent.js }ReactDomComponent
ReactDomComponent的update最復雜,可以說diff的核心都在這里,本文的重心也就放在這。
整個update分為兩塊,props的更新和children的更新。
class ReactDomComponent extends ReactComponent { mountComponent(rootId) { //省略 } // 更新 updateComponent(nextVDom) { const lastProps = this._vDom.props const nextProps = nextVDom.props this._vDom = nextVDom // 更新屬性 this._updateDOMProperties(lastProps, nextProps) // 再更新子節點 this._updateDOMChildren(nextVDom.props.children) } // 代碼地址:src/react/component/ReactDomComponent.js }
props的更新非常簡單,無非就是遍歷新舊props,刪除不在新props里的老props,添加不在老props里的新props,更新新舊都有的props,事件特殊處理。
_updateDOMProperties(lastProps, nextProps) { let propKey = "" // 遍歷,刪除已不在新屬性集合里的老屬性 for (propKey in lastProps) { // 屬性在原型上或者新屬性里有,直接跳過 if (nextProps.hasOwnProperty(propKey) || !lastProps.hasOwnProperty(propKey)) { continue } // 對于事件等特殊屬性,需要多帶帶處理 if (/^on[A-Za-z]/.test(propKey)) { const eventType = propKey.replace("on", "") // 針對當前的節點取消事件代理 $(document).undelegate(`[data-reactid="${this._rootNodeId}"]`, eventType, lastProps[propKey]) continue } } // 對于新的屬性,需要寫到dom節點上 for (propKey in nextProps) { // 更新事件屬性 if (/^on[A-Za-z]/.test(propKey)) { var eventType = propKey.replace("on", "") // 以前如果已經有,需要先去掉 lastProps[propKey] && $(document).undelegate(`[data-reactid="${this._rootNodeId}"]`, eventType, lastProps[propKey]) // 針對當前的節點添加事件代理 $(document).delegate(`[data-reactid="${this._rootNodeId}"]`, `${eventType}.${this._rootNodeId}`, nextProps[propKey]) continue } if (propKey === "children") continue // 更新普通屬性 $(`[data-reactid="${this._rootNodeId}"]`).prop(propKey, nextProps[propKey]) } } // 代碼地址:src/react/component/ReactDomComponent.js
children的更新則相對復雜了很多,陳屹老師的《深入React技術?!分刑岬剑琩iff算法分為3塊,分別是
tree diff
component diff
element diff
上文中的shouldUpdateReactComponent就屬于component diff,接下來,讓我們依據這三種diff實現updateChildren。
// 全局的更新深度標識,用來判定觸發patch的時機 let updateDepth = 0 // 全局的更新隊列 let diffQueue = [] _updateDOMChildren(nextChildVDoms) { updateDepth++ // diff用來遞歸查找差異,組裝差異對象,并添加到diffQueue中 this._diff(diffQueue, nextChildVDoms) updateDepth-- if (updateDepth === 0) { // 具體的dom渲染 this._patch(diffQueue) diffQueue = [] }
這里通過updateDepth對vDom樹進行層級控制,只會對相同層級的DOM節點進行比較,只有當一棵DOM樹全部遍歷完,才會調用patch處理差異。也就是所謂的tree diff。
確保了同層次后,我們要實現_diff方法。
已經渲染過的子ReactComponents在這里是數組,我們要遍歷出里面的vDom進行比較,這里就牽扯到上文中的key,在有key時,我們優先用key來獲取vDom,所以,我們首先遍歷數組,將其轉為map(這里先用object代替,以后會更改成es6的map),如果有key值的,就用key值作標識,無key的,就用index。
下面是array到map的代碼:
// 將children數組轉化為map export function arrayToMap(array) { array = array || [] const childMap = {} array.forEach((item, index) => { const name = item && item._vDom && item._vDom.key ? item._vDom.key : index.toString(36) childMap[name] = item }) return childMap }
部分diff方法:
// 將之前子節點的component數組轉化為map const prevChildComponents = arrayToMap(this._renderedChildComponents) // 生成新的子節點的component對象集合 const nextChildComponents = generateComponentsMap(prevChildComponents, nextChildVDoms)
將ReactComponent數組轉化為map后,用老的ReactComponents集合和新vDoms數組生成新的ReactComponents集合,這里會使用shouldUpdateReactComponent進行component diff,如果相同,則直接更新即可,反之,就重新生成ReactComponent
/** * 用來生成子節點的component * 如果是更新,就會繼續使用以前的component,調用對應的updateComponent * 如果是新的節點,就會重新生成一個新的componentInstance */ function generateComponentsMap(prevChildComponents, nextChildVDoms = []) { const nextChildComponents = {} nextChildVDoms.forEach((item, index) => { const name = item.key ? item.key : index.toString(36) const prevChildComponent = prevChildComponents && prevChildComponents[name] const prevVdom = prevChildComponent && prevChildComponent._vDom const nextVdom = item // 判斷是更新還是重新渲染 if (shouldUpdateReactComponent(prevVdom, nextVdom)) { // 更新的話直接遞歸調用子節點的updateComponent prevChildComponent.updateComponent(nextVdom) nextChildComponents[name] = prevChildComponent } else { // 重新渲染的話重新生成component const nextChildComponent = instantiateReactComponent(nextVdom) nextChildComponents[name] = nextChildComponent } }) return nextChildComponents }
經歷了以上兩步,我們已經獲得了新舊同層級的ReactComponents集合。需要做的,只是遍歷這兩個集合,進行比較,同屬性的更新一樣,進行移動,新增,和刪除,當然,在這個過程中,我會包含我們的第三種優化,element diff。它的策略是這樣的:首先對新集合的節點進行循環遍歷,通過唯一標識可以判斷新老集合中是否存在相同的節點,如果存在相同節點,則進行移動操作,但在移動前需要將當前節點在老集合中的位置與 lastIndex 進行比較,if (prevChildComponent._mountIndex < lastIndex),則進行節點移動操作,否則不執行該操作。這是一種順序優化手段,lastIndex 一直在更新,表示訪問過的節點在老集合中最右的位置(即最大的位置),如果新集合中當前訪問的節點比 lastIndex 大,說明當前訪問節點在老集合中就比上一個節點位置靠后,則該節點不會影響其他節點的位置,因此不用添加到差異隊列中,即不執行移動操作,只有當訪問的節點比 lastIndex 小時,才需要進行移動操作。
上完整的diff方法代碼:
// 差異更新的幾種類型 const UPDATE_TYPES = { MOVE_EXISTING: 1, REMOVE_NODE: 2, INSERT_MARKUP: 3 } // 追蹤差異 _diff(diffQueue, nextChildVDoms) { // 將之前子節點的component數組轉化為map const prevChildComponents = arrayToMap(this._renderedChildComponents) // 生成新的子節點的component對象集合 const nextChildComponents = generateComponentsMap(prevChildComponents, nextChildVDoms) // 重新復制_renderChildComponents this._renderedChildComponents = [] for (let name in nextChildComponents) { nextChildComponents.hasOwnProperty(name) && this._renderedChildComponents.push(nextChildComponents[name]) } let lastIndex = 0 // 代表訪問的最后一次老的集合位置 let nextIndex = 0 // 代表到達的新的節點的index // 通過對比兩個集合的差異,將差異節點添加到隊列中 for (let name in nextChildComponents) { if (!nextChildComponents.hasOwnProperty(name)) continue const prevChildComponent = prevChildComponents && prevChildComponents[name] const nextChildComponent = nextChildComponents[name] // 相同的話,說明是使用的同一個component,需要移動 if (prevChildComponent === nextChildComponent) { // 添加差異對象,類型:MOVE_EXISTING prevChildComponent._mountIndex < lastIndex && diffQueue.push({ parentId: this._rootNodeId, parentNode: $(`[data-reactid="${this._rootNodeId}"]`), type: UPDATE_TYPES.MOVE_EXISTING, fromIndex: prevChildComponent._mountIndex, toIndex: nextIndex }) lastIndex = Math.max(prevChildComponent._mountIndex, lastIndex) } else { // 如果不相同,說明是新增的節點 // 如果老的component在,需要把老的component刪除 if (prevChildComponent) { diffQueue.push({ parentId: this._rootNodeId, parentNode: $(`[data-reactid="${this._rootNodeId}"]`), type: UPDATE_TYPES.REMOVE_NODE, fromIndex: prevChildComponent._mountIndex, toIndex: null }) // 去掉事件監聽 if (prevChildComponent._rootNodeId) { $(document).undelegate(`.${prevChildComponent._rootNodeId}`) } lastIndex = Math.max(prevChildComponent._mountIndex, lastIndex) } // 新增加的節點 diffQueue.push({ parentId: this._rootNodeId, parentNode: $(`[data-reactid="${this._rootNodeId}"]`), type: UPDATE_TYPES.INSERT_MARKUP, fromIndex: null, toIndex: nextIndex, markup: nextChildComponent.mountComponent(`${this._rootNodeId}.${name}`) }) } // 更新_mountIndex nextChildComponent._mountIndex = nextIndex nextIndex++ } // 對于老的節點里有,新的節點里沒有的,全部刪除 for (let name in prevChildComponents) { const prevChildComponent = prevChildComponents[name] if (prevChildComponents.hasOwnProperty(name) && !(nextChildComponents && nextChildComponents.hasOwnProperty(name))) { diffQueue.push({ parentId: this._rootNodeId, parentNode: $(`[data-reactid="${this._rootNodeId}"]`), type: UPDATE_TYPES.REMOVE_NODE, fromIndex: prevChildComponent._mountIndex, toIndex: null }) // 如果渲染過,去掉事件監聽 if (prevChildComponent._rootNodeId) { $(document).undelegate(`.${prevChildComponent._rootNodeId}`) } } } } // 代碼地址:src/react/component/ReactDomCompoent.js
調用diff方法后,會回到tree diff那一步,當一整棵樹遍歷完后,就需要通過Patch將更新的內容渲染出來了,patch方法相對比較簡單,由于我們把更新的內容都放入了diffQueue中,只要遍歷這個數組,根據不同的類型進行相應的操作就行。
// 渲染 _patch(updates) { // 處理移動和刪除的 updates.forEach(({ type, fromIndex, toIndex, parentNode, parentId, markup }) => { const updatedChild = $(parentNode.children().get(fromIndex)) switch (type) { case UPDATE_TYPES.INSERT_MARKUP: insertChildAt(parentNode, $(markup), toIndex) // 插入 break case UPDATE_TYPES.MOVE_EXISTING: deleteChild(updatedChild) // 刪除 insertChildAt(parentNode, updatedChild, toIndex) break case UPDATE_TYPES.REMOVE_NODE: deleteChild(updatedChild) break default: break } }) } // 代碼地址:src/react/component/ReactDomComponent.js總結
以上,整個簡易版React就完成了,可以試著寫些簡單的例子跑跑看了,是不是非常有成就感呢?
總結下更新:
ReactCompositeComponent:負責調用生命周期,通過component diff將更新都交給了子ReactComponet
ReactTextComponent:直接更新內容
ReactDomComponent:先更新props,在更新children,更新children分為三步,tree diff保證同層級比較,使用shouldUpdateReactComponent進行component diff,最后在element diff通過lastIndex順序優化
至此,整個從頭實現簡易版React就結束了,感謝大家的觀看。
參考資料,感謝幾位前輩的分享:
https://www.cnblogs.com/sven3...
https://github.com/purplebamb...
陳屹 《深入React技術?!?/p>
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/93134.html
摘要:既然看不懂,那就看看社區前輩們寫的一些源碼分析文章以及實現思路吧,又這么過了幾天,總算是摸清點思路,于是在參考了前輩們的基礎上,實現了一個簡易版的??偨Y以上就是實現一個的總體思路,下節我們重點放在不同的上。 寫在開頭 工作中使用react也很長一段時間了,雖然對它的用法,原理有了一定的了解,但是總感覺停留在表面。本著知其然知其所以然的態度,我試著去看了react源碼,幾天下來,發現并不...
摘要:寫在開頭從頭實現一個簡易版一地址上一節,我們詳細介紹了實現一個簡易的思路以及整體的結構,但是對于渲染和更新的原理,卻還沒有提及,因此,本節我們將重點放在的渲染上。 寫在開頭 從頭實現一個簡易版React(一)地址:https://segmentfault.com/a/11...上一節,我們詳細介紹了實現一個簡易React的思路以及整體的結構,但是對于渲染和更新的原理,卻還沒有提及,因此...
摘要:登錄視圖登陸失敗用戶名或密碼不能為空彈出提示框成功是點擊登錄按鈕后調用的函數,這里的功能比較簡單。通過把發出去密碼登錄聲明組件需要整個中的哪一部分數據作為自己的將和組件聯系在一起編寫是負責生成的,所以在大項目中還會用到合并。 本豬說 本豬豬剛學react,也剛看RN,就叫寫這個,苦不堪言,搭環境就搭了好久??淳W上教程也是改了好多小地方才寫完了。本著雷鋒精神手把手教你寫(假的)。 sho...
摘要:五六月份推薦集合查看最新的請點擊集前端最近很火的框架資源定時更新,歡迎一下。蘇幕遮燎沈香宋周邦彥燎沈香,消溽暑。鳥雀呼晴,侵曉窺檐語。葉上初陽乾宿雨,水面清圓,一一風荷舉。家住吳門,久作長安旅。五月漁郎相憶否。小楫輕舟,夢入芙蓉浦。 五、六月份推薦集合 查看github最新的Vue weekly;請::點擊::集web前端最近很火的vue2框架資源;定時更新,歡迎 Star 一下。 蘇...
閱讀 3121·2021-09-28 09:42
閱讀 3457·2021-09-22 15:21
閱讀 1129·2021-07-29 13:50
閱讀 3582·2019-08-30 15:56
閱讀 3375·2019-08-30 15:54
閱讀 1202·2019-08-30 13:12
閱讀 1180·2019-08-29 17:03
閱讀 1204·2019-08-29 10:59