感謝你們又被個人標題黨騙進來了😂。這是我最近幾個月親身探尋過的一趟旅途,感覺頗深。途中也遇到許多困難,但堅持到底,相信最終必定會讓各位小夥伴受益不淺,不枉此行!同時也感恩你們對以前一個系列文章的喜歡和修正😘,提了很多的建議和問題,我也會用心寫個人每一系列文章,與你們共同成長!!前端
跪求點贊、關注、Star!更多文章猛戳 ->vue
以前面試三部曲簡明地梳理了前端知識結構體系,淺嘗輒止。這個系列則要進一步研究和領會 內在的奧妙。今天打算以一個比較新穎的角度切入,深刻地梳理下 React
的內部實現。node
那如何深刻研究一個玩具呢?最好的方式即是: 拆解 - 重組裝。react
衆所周知,React
是一款很是神奇的 Web UI
框架,得益於強大的架構,邏輯層 與 視圖層 的解耦,使其思想及開發模式能夠很好地移植到其它平臺,例如 React-Native
。因此今天,我打算跳出常規的 Web-DOM
,目標是 以類 React 的模式來進行 Web 遊戲開發。其實就是咱們須要實現一套 React
上層,並把底層對接 WebGL API
。期待這樣不一樣的視角,能給你們帶來新的啓發與幫助。😄git
Tips:
WebGL
不是本文關注的重點,使用到的 API 也很是有限,並不須要你具有相關的知識儲備。github
做爲一名前端,咱們須要實現一個個展現給用戶的頁面。所以視圖層即是咱們的工做之始。前端領域不斷地快速發展,最終都是爲了解答 如何更高效地開發更完美的頁面。而 React
就是答案之一,其中 JSX
即是重要的第一站。web
那什麼是 JSX
呢?面試
JSX
就是在 JS
環境中約定的一種類 HTML
或 XML
的 動態模板語法,有着極高的 可讀性 與 可拓展性,目的是 爲了能更便捷地使用 JS 搭建視圖結構與佈局。算法
// 這就是 JSX const jsx = <JSX>Hello World</JSX>
這是一種新全新的 JS
語法,不屬於標準,成功地把類 HTML
的標籤型模板語法引入 JS 中,創造了一種全新高效的開發模式。但即便最新版的 V8 引擎也沒法支持,那怎麼執行呢?鑰匙就是 預編譯!得益於 Babel
的強大,咱們能夠經過 預編譯,將代碼編譯成瀏覽器看得懂的 JS
。編程
這是 Babel
的一款插件,主要的功能就是編譯 JSX
,直接配置便可 (對編譯感興趣的童鞋,能夠繼續深刻下了解下 Babel
的編譯原理,這裏就不做展開了)。
// 打包工具中的 babel loader 配置 use: { loader: 'babel-loader', options: { plugins: [["transform-jsx", { "function": "ReactWebGL.createElement", "useVariables": true }]], } }
配置完畢,咱們先來寫段 JSX
試試。因爲咱們不使用 DOM
了,視圖層被直接繪製於 canvas
上,就天然不能使用常規的 HTML
標籤了。咱們先來定個容器標籤 (<Container>
)。
const jsx = ( <Container name="parent"> ReactWebGL Hello World <Container name="child"> Child </Container> </Container> )
咦。秒報錯。先無論,咱們來看下編譯後的文件:
var jsx = ReactGL.createElement({ elementName: Container, attributes: { name: 'parent' }, children: [ "ReactWebGL Hello World", ReactWebGL.createElement({ elementName: Container, attributes: { name: "child" }, children: ["Child"] }), ] });
原來如此,其實 Babel
作的,就是把上面的 JSX
模板代碼, 解析並提取出標籤的信息後,轉換成常規的函數形式。這個函數就是咱們配置中指定的 ReactWebGL.createElement
。
接下來咱們來看下報錯。很明顯,是由於在上下文中有一些變量沒有進行定義,那接下來咱們先定義上:
// 因爲編譯是直接 變量傳遞 // 所以標籤名做爲一個變量須要先定義爲 string; const Container = 'Container' // 匹配配置中的 `ReactWebGL.createElement` const ReactWebGL = { createElement(tag) { console.log(tag) } }
來到第二站: 什麼是 虛擬DDM 呢?
顧名思義,它並非真正的視圖元素,而是將真實的視圖元素抽象爲一個 Javascript 對象,包含完整描述了一個真實元素的全部信息,但並不具有渲染功能。同時因爲 DOM
自己即是 樹形結構,所以使用 Javascript
對象便能很好對整個頁面結構進行描述。
剛纔 JSX
編譯後,參數即是一個最簡單的 虛擬DOM 對象(咱們稱爲 VNode
):
// 一個最簡單的 VNode { // 類型 type: 'Container' // 屬性 props: { name: 'parent' } // 子級列表 children: [...] }
這其實只是一個普通的 Javascript
對象,那爲何要設計它呢?可能不少人都會有這種觀念: 虛擬DOM 快啊,diff 算法很是厲害,直接操做真實的 DOM 消耗很大。
掐指一算,事情並無那麼簡單,聽我細細道來🧐。想象下,當出現如下場景時:
初次渲染:
JSX
,生成 虛擬DOM 樹,而後通過各類計算,最終調用 DOM
繪製一個個視圖元素;HTML
或 innerHTML
直接建立元素會更快,且白屏時間更短,多了上層的 計算消耗與內存消耗,反而是一種 性能損耗;極小更新:
setState
觸發更新。此時,React
並不知道新的 state
會引發多大的變更,須要通過全局逐一的 diff
肯定變更的元素再觸發更新。diff
,會更直接高效。那這麼說來,虛擬DOM 反而變慢了,那爲何還要用它呢?若是你的場景是 大量且分散 地更新頁面中的元素,那 虛擬DOM 能大大減小其業務邏輯的複雜度,達到一個比較高的消耗性價比。直接操做 DOM
元素,一來邏輯複雜,代碼健壯性弱;二來錯誤的操做反而可能致使更差的性能。
因此快慢並非衡量 虛擬DOM 價值的因素,其價值更多在於:
DOM
的操做進行 集中化管理,更加安全穩定且高效;RN / SSR
等方案;JSX
僅僅是一種 獨立的動態模板語法,經過編譯轉化爲 虛擬DOM,跟 React
並沒有耦合。所以能夠將其 接入到任意環境或者框架,例如咱們也能夠在 Vue
中使用 JSX
。
聊完 虛擬DOM,咱們迴歸主線。須要先來設計一個最簡單的 VNode
結構,以剛纔 Babel
編譯出的結構爲基礎,加上額外的值,方便渲染。
// VNode 定義 interface VNode { // 標籤類型 type: any, // 標籤屬性 props: { [key: string]: any }, // 子級列表 children: VNode[], // 惟一標識 key: string // 獲取視圖元素 ref: any // 視圖元素 elm: any // 文本內容 text: string | number | undefined } // VNode 生產函數 function createVNode(type, props, ref, key, children, elm, text) { return { type, props, children, ref, key, elm, text, } }
如今定義了 VNode
後,咱們就能夠開始編寫對應的生成函數(createElement
)了。
這個函數就是傳說中的 h
函數,用於 模板 到 虛擬DOM 之間的橋樑。在如今的大多數主流 虛擬DOM 庫中,都擁有該函數。在 React
中,它是經過 Babel
將 JSX
編譯成 h
函數,即 React.createElement
。而在 Vue
中,則是經過 vue-loader
將 <template>
編譯成其 h
函數。該函數主要功能是:
加工生成完整的 虛擬DOM樹,用於後面的渲染。
function createElement(tag) { const { elementName: type, attributes: data, children } = tag const { key, ref, ...props } = data // 處理文本內容 let text // 處理子級列表中的 string or number // 一樣轉換爲 VNode if (children && children.length) { let i, l = children.length for (i = 0; i < l; ++i) { const child = children[i] if (['string', 'number'].includes(typeof child)) { if (type === 'Text') { // 基於 WebGL 的須要,區別於 DOM // 這裏新增一個 <Text> 標籤,vnode.type === 'Text' // 需特殊處理 if (text === undefined) text = '' text += String(child) } else { // 非標籤的文字節點, vnode.type === undefined // 例如 <Container>Text</Container> // 中間的 Text 實際上是一樣須要一個文字元素 children[i] = vnode( undefined, {}, undefined, undefined, undefined, undefined, String(children[i]) ) } } } } return vnode(type, props, ref, key, children, undefined, text) }
執行,Perfect!打印下 JSX
,能夠看到一棵轉換後的 VNode Tree
,而且包含咱們模板標籤中的全部完整信息。
<p style="text-align: center; font-weight: bold;">圖1. VNode Tree</p>
爲了更好地 解耦視圖層,咱們把須要用到的一些與 WebGL
相關的 API
簡單包裹下:
const Api = { // 根據 標籤類型 建立 視圖元素 createElement(vnode) { return new PIXI[vnode.type]() }, // 建立、設置 文本元素 createTextElement(vnode) { const { text: content = '', style } = vnode return new PIXI.Text(content, style) }, setTextContent(elm, content) { if (elm && ['string', 'number'].includes(typeof content)) { elm.text = content } }, // 獲取父級 parentNode(elm) { return elm && elm.parent }, // 添加、刪除子級 appendChild(parent, child) { parent.addChild(child) }, removeChild(parent, child) { if (child && child.parent) { parent.removeChild(child) } }, // 獲取下一個兄弟元素 nextSibling(elm) { const parent = Api.parentNode(elm) if (parent) { const index = parent.children.indexof(elm) return parent.children[index + 1] } else { return undefined } }, // 插入到指定元素以前 insertBefore(parentElm, newElm, referenceElm) { if (referenceElm) { const refIndex = parentElm.children.indexOf(referenceElm) parentElm.addChildAt(newElm, refIndex) } else { Api.appendChild(parentElm, newElm) } }, }
這一部分能夠稱爲 對接層,能夠對接到各個平臺,若是使用原生 DOM
的 API
,則就是 Web
渲染,有點相似於 react-dom
所完成的事。
爲了演示方便,咱們就使用 pixi.js
來做爲 渲染接口。這裏與 WebGL
庫無關,能夠對接到 任意渲染框架。
有了 接口層 API 與 VNode
後,能夠開始 建立真實視圖元素,並同時根據 vnode.props
同步屬性並綁定事件。最後 遞歸建立子級:
// 根據 vnode 建立 視圖元素 function createElm(vnode) { const { children, type, text, props } = vnode // vnode 的 type 爲字符串時,表示其爲 元素節點 // 依照 type 建立 視圖元素 if (type && typeof type === 'string') { // 調用接口 vnode.elm = Api.createElement(vnode) // 遞歸建立子級元素,並添加到父元素中 if (Array.isArray(children)) { // 建立子級 let i, l = children.length for (i = 0; i < l; ++i) { Api.appendChild(vnode.elm, createElm(children[i])) } } } else if (type === undefined && text) { // 被非 <Text> 包裹的文字節點 vnode.elm = Api.createTextElement(vnode) } // 元素建立成功時,執行設置屬性 if (vnode.elm) setProps(vnode.elm, props) return vnode.elm } // 屬性處理 // 將 虛擬DOM 上的屬性同步設置到 上面建立的真實元素 elm 上 function setProps(elm, props) { if (elm && typeof props === 'object') { const keys = Object.keys(props) let l = keys.length, i for (i = 0; i < l; i++) { const key = keys[i] const value = props[key] if (key.startsWith('on')) { // 事件綁定 if (typeof value === 'function') { const evName = key.substring(2).toLowerCase() elm.on(evName, value) } } else { // 屬性設置 elm[key] = value } } } }
最後,咱們能夠開始渲染 VNode
:
function render(vnode, parent) { // 根據 vnode 建立出對應的元素 const elm = createElm(vnode) // 並添加到容器中便可 Api.appendChild(parent, elm) return elm }
大功告成,到這裏咱們已經成功完成把 JSX
的初次渲染了。那下一步的需求就是: 如何更新視圖呢? 這裏就是傳說中的 Diff
算法的用武之地了!
說到 虛擬DOM 就立刻能提到其核心的 diff
算法。當咱們經過 setState
去更新組件時,是從新生成一棵全新的完整 虛擬DOM樹,此時就須要對比 新舊兩棵樹的差別點,再針對性更新。也就是一種 計算得出兩個對象差別 的算法。
這種 diff
算法其實使用的場景不少,例如咱們很熟悉的代碼版本控制。先後兩份提交的代碼須要比對出差別點,而後進行更新保存。只不過這裏的 代碼文件 變成了 虛擬DOM。因爲 虛擬DOM 至關複雜,包含很是多的屬性,而且可能擁有很是深的層級,所以若是用常規的循環遞歸去比較,時間複雜度爲 O(n^3)
,這性能是沒法接受的。
所以天才工程師們基於 Web 視圖渲染的一些特徵,在比對上經過 制定規則,選擇性取捨,大大優化了算法的效率。包含如下三種優化策略:
因爲在大部分 Web 視圖渲染中,咱們不多會去跨層級移動元素,移動元素一般出如今同層級的列表中,所以這裏能夠有一個優化策略:
只作同層級的比對,忽略跨層級的元素移動。
傳統的 diff
算法須要兩層循環,每兩個節點之間都須要進行對比。而制定了同層比對後,節點只須要跟同一層級的節點進行比對,以下圖所示。
<p style="text-align: center; font-weight: bold;">圖2. diff 比對策略</p>
此時,性能已經大大的提高了,時間複雜度優化成 O(n^2)
。另外,若是真出現跨層級移動時,會直接將舊元素刪除,在新的位置從新建立,也能保證更新的準確行。但可能會致使狀態的丟失。
雖然咱們作了同層比對的優化,但此時有一個問題:
例如圖2,當 C1
/ C2
交換位置,咱們在循環比對時,因爲它們均屬於相同類型的節點,單單經過 type 並沒有法正確區分,沒法識別出位置的移動。只能作到把 C1
修改爲 C2
,把 C2
修改爲 C1
。這樣不只會 損耗性能,並且可能致使 狀態丟失。
最優的方式應該就是: 把 C1
與 C2
正確交換位置。關鍵點就在於: 如何正確識別與區分節點。所以這裏便引入了 key
做爲惟一標識,用 type
+ key
即可確切地識別出節點的準確位置,從而將時間複雜度優化成了 O(n)
。
在複雜度方面,已經有了有效的優化。接下來,即是從邏輯層進行優化。
首先一個最大的損耗就是: 沒法準肯定位目標節點,須要 樹遍歷 尋找更新的目標節點。
如圖2,咱們只要更新 D
節點,卻必須從最根級的 A
節點 逐層 diff
,完整比對新舊兩棵 虛擬樹,這裏有明顯的無謂損耗。若是將 D
節點 抽離成一個獨立的模塊,則能夠只調用 D
節點 自身的 diff
。 所以便引入了 組件模式,可以 碎片化 虛擬DOM。
若是咱們確實須要同時更新 A
/ D
節點呢?其實左邊分支 B1
節點並不須要更新,不須要 diff
。若是能讓 B1
節點擁有一個標識標識本身爲非更新目標,在更新流中能夠 主動打斷更新流。那就能夠只 diff
A
-> B2
-> D
這條線了。這裏,即是咱們熟知的 shouldComponentUpdate
。
首先咱們先來梳理下兩個 VNode diff
可能出現的狀況:
非同類型節點:
同類型節點:
<p style="text-align: center;font-weight: bold;">圖3. diff 流程圖</p>
根據這個流程圖,咱們能夠先從入口開始實現。
function diff(oldVNode, newVNode) { if (isSameVNode(oldVNode, newVNode)) { // 開始 diff // diffVNode ... } else { // 新節點替換舊節點 // replaceVNode ... } } // 根據 type || key 判斷是否爲同類型節點 function isSameVNode(oldVNode, newVNode) { return oldVNode.key === newVNode.key && oldVNode.type === newVNode.type }
replaceVNode
)function replaceVNode(oldVNode, newVNode) { // 移除舊元素 const { elm: oldElm } = oldVNode const parent = Api.parentNode(oldElm) Api.removeChild(parent, oldElm) // 建立新元素,並添加到父級中 const newElm = createElm(newVNode) Api.appendChild(parent, newElm) }
diffVNode
)根據上面的流程圖,咱們也能明白這裏咱們要作如下這些事:
diffProps
);遞歸比對子級列表: 這裏也有三種狀況;
diffChildren
);addVNodes
);removeVNodes
);根據以上梳理,咱們先來實現 diffVNode
這個函數。
function diffVNode(oldVNode, newVNode) { const { elm, children: oldChild, text: oldText, props: oldProps } = oldVNode const { children: newChild, text: newText, props: newProps } = newVNode if (oldVNode === newVNode || !elm) return // 已判斷爲同一節點,目的爲 更新元素 // 所以直接複用舊元素 newVNode.elm = elm // 比對屬性與事件 diffProps(elm, oldProps, newProps) const hasOldChild = !!(oldChild && oldChild.length) const hasNewChild = !!(newChild && newChild.length) // 判斷爲 元素節點 或者 文字節點 if (newText === undefined) { // 元素節點 // 判斷如何更新子級 if (hasOldChild && hasNewChild) { // 新舊節點均存在子級列表時,直接 diff 列表 if (oldChild !== newChild) { // diff 列表 diffChildren(elm, oldChild, newChild) } } else if (hasNewChild) { // 舊節點 不包含子級,而新節點包含子級 // 則直接新增新子級 addVNodes(elm, null, newChild, 0, newChild.length - 1) } else if (hasOldChild) { // 新子級不包含元素,而舊節點包含子級 // 則須要刪除舊子級 removeVNodes(elm, oldChild, 0, oldChild.length - 1) } else if (oldText !== undefined) { // 當新舊均無子級 // 這裏有可能存在 <Text> 標籤,且新內容爲空 // 所以直接清空舊元素文字 Api.setTextContent(elm, '') } } else if (oldText !== newText) { // 文字節點 // 當新舊文字內容不一樣時,直接修改內容 Api.setTextContent(elm, newText) } } // 更新屬性 function diffProps(elm, oldProps, newProps) { if (oldProps === newProps || !elm) return if (typeof oldProps === 'object' && typeof newProps === 'object') { let keys = Object.keys(oldProps), i, l = keys.length // 重置被刪除的舊屬性 for (i = 0; i < l; i++) { const key = keys[i] const oldValue = oldProps[key], newValue = newProps[key] if (key.startsWith('on')) { /* * 當存在舊事件,且新舊值不一致時 * 事件解綁 */ if (typeof oldValue === 'function' && oldValue !== newValue) { const evName = key.substring(2).toLowerCase() elm.off(evName, oldValue) } } else { /* * 屬性被賦值時會被自動重置 * 只須要重置被刪除的屬性便可 */ if (newValue === undefined) { // 元素屬性默認值 elm[key] = DEFAULT_PROPS[key] } } } // 設置新屬性 setProps(elm, newProps) } }
diffChildren
)其實比對屬性、事件都是相對簡單的,而 子級列表的比對,纔是整個 diff 算法中最核心且最考驗性能的部分,所以這裏的列表比對算法決定了整個更新渲染的性能。在 虛擬DOM 剛出現時,使用的是比較簡單的 深度優先(DFS) + 排序比對 的方式。後來出現了更爲高效且沿用至今的 兩端比對算法 + Key值比對,直接把 diff
的效率提升了一個層級,且更好理解,有三種優先級不一樣的比對策略:
若是均不匹配,則嘗試 key 值比對;
這裏這麼說你們是否是一臉懵🤣。沒事,先稍微理解下就行。咱們接下來直接用動畫來更直觀地看下兩端比較算法的具體過程。這裏就以剛纔 圖2 的子級列表爲例,即:
[A, B, C, D, E]
;[F, C, B, D, A]
;Tips:如新舊列表中的 A,表明這兩個節點爲 同類型節點,即節點的
type / key
均相等;
<p style="text-align: center; font-weight: bold;">圖4. diff 第一輪循環</p>
A !== F
且 E !== A
;2. 兩端交叉比對,發現 舊列表的第一項與新列表的末項相同 (isSameVNode
):
A
;<p style="text-align: center; font-weight: bold;">圖5. diff 第二輪循環</p>
B !== F & E !== D
;B !== D & E !== F
;3. 進入 key 比對:
F
);<p style="text-align: center; font-weight: bold;">圖6. diff 第三輪循環</p>
2. 進入 key 比對;
C
);C
;<p style="text-align: center; font-weight: bold;">圖7. diff 第四五輪循環</p>
1. 首項比對,匹配成功;
B
;2. 一樣首項比對匹配;
D
;通過了五輪的比對,舊列表已經被成功更新。爲了包含咱們上面解釋的三種策略,舉例時用的是複雜度較高,較少出現的場景。在平常業務中,大部分都會更簡單,性能表現會更好。接下來,咱們把這個算法用代碼實現下,採用 雙列表遊標 + while 循環 的方式:
function diffChildren(parentElm, oldChild, newChild) { /** * 更新子級列表 * 雙列表遊標 + while */ // 初始化遊標 let oldStartIdx = 0, newStartIdx = 0 let oldEndIdx = oldChild.length - 1, newEndIdx = newChild.length - 1 // 列表首尾節點 let oldStartVNode = oldChild[0], oldEndVNode = oldChild[oldEndIdx] let newStartVNode = newChild[0], newEndVNode = newChild[newEndIdx] let oldKeyToIdx, idxInOld, elmToMove, before /** * 當起始遊標 < 終止遊標時, * 表示列表中仍有未 diff 的節點 * 進入循環 */ while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { /** * 1. 排除非有效的節點 * 剔除列表中包含的 undefined || false || null */ if (oldStartVNode == null) { oldStartVNode = oldChild[++oldStartIdx] } else if (oldEndVNode == null) { oldEndVNode = oldChild[--oldEndIdx] } else if (newStartVNode == null) { newStartVNode = newChild[++newStartIdx] } else if (newEndVNode == null) { newEndVNode = newChild[--newEndIdx] } else if (isSameVNode(oldStartVNode, newStartVNode)) { /** * 2. 正反向兩兩比對列表首項與末項匹配成功 * 移動遊標,遞歸 diff 兩個節點 * 均未匹配上,則進入 3. key 值比對 */ diff(oldStartVNode, newStartVNode) oldStartVNode = oldChild[++oldStartIdx] newStartVNode = newChild[++newStartIdx] } else if (isSameVNode(oldEndVNode, newEndVNode)) { diff(oldEndVNode, newEndVNode) oldEndVNode = oldChild[--oldEndIdx] newEndVNode = newChild[--newEndIdx] } else if (isSameVNode(oldStartVNode, newEndVNode)) { Api.insertBefore(parentElm, oldStartVNode.elm, Api.nextSibling(oldEndVNode.elm)) diff(oldStartVNode, newEndVNode) oldStartVNode = oldChild[++oldStartIdx] newEndVNode = newChild[--newEndIdx] } else if (isSameVNode(oldEndVNode, newStartVNode)) { Api.insertBefore(parent, oldEndVNode.elm, oldStartVNode.elm) diff(oldEndVNode, newStartVNode) oldEndVNode = oldChild[--oldEndIdx] newStartVNode = newChild[++newStartIdx] } else { /** * 3. 兩端比對均不匹配 * 進入 key 值比對 */ // 根據剩餘的舊列表建立 key list if (!oldKeyToIdx) { oldKeyToIdx = createKeyList(oldChild, oldStartIdx, oldEndIdx) } // 判斷新列表項的 key值 是否存在 idxInOld = oldKeyToIdx[newStartVNode.key || ''] if (!idxInOld) { /* * 4. 新 key 值在舊列表中不存在 * 直接將該節點插入 */ Api.insertBefore(parentElm, createElm(newStartVNode), oldStartVNode.elm) newStartVNode = newChild[++newStartIdx] } else { /* * 5. 新 key 在舊列表中存在時 * 繼續判斷是否爲同類型節點 */ elmToMove = oldChild[idxInOld] if (isSameVNode(elmToMove, newStartVNode)) { /* * 6. 新舊節點類型一致 * key 有效,直接移動並 diff */ Api.insertBefore(parentElm, elmToMove.elm, oldStartVNode.elm) diff(elmToMove, newStartVNode) // 清空舊列表項 // 後續的比對能夠直接跳過 oldChild[idxInOld] = undefined } else { /* * 7. 新舊節點類型不一致 * key 效,直接建立元素並插入 */ Api.insertBefore(parentElm, createElm(newStartVNode), oldStartVNode.elm) } newStartVNode = newChild[++newStartIdx] } } } /* * 8. 當有遊標列表爲空時,則結束循環,進入策略3 * 當 舊列表爲空 時,則建立並插入新列表中的剩餘節點 * 當 新列表爲空 時,則刪除舊列表中的剩餘節點 */ if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) { if (oldStartIdx > oldEndIdx) { // 新增節點 const vnode = newChild[newEndIdx + 1] before = vnode ? vnode.elm : null addVNodes(parentElm, before, newChild, newStartIdx, newEndIdx) } else { // 刪除節點 removeVNodes(parentElm, oldChild, oldStartIdx, oldEndIdx) } } }
恭喜童鞋們~ 咱們完成了傳說中的 兩端比對算法
咯🥳。其實只要按比對的優先級把邏輯理清楚,逐一執行判斷,思路仍是比較清晰的。
經過上面真正的代碼編寫以及瞭解實現原理後,咱們其實能從中找到一些好的實踐方式,從而優化咱們的代碼,提升性能。
props
的傳遞JSX
中標籤能夠傳遞屬性,最簡單的方式就是 值傳遞:
const tmp = <Container text="text" data={{ a: 1 }}>Text</Container>
因爲在比對的時候,在判斷 props
是否發生變化時,採用 全等 的比較。所以,當出現值是 引用對象 時,若是直接像上面這樣, data
寫在 JSX
中,則每次 diff
時 data
的值都是全新的對象,不會相等。即便對象屬性所有一致,但每次均須要循環比對每一項屬性。
所以建議:
Object
、Array
、Function
等,直接使用 引用傳遞:const data = { a: 1 } const tmp = <Container text="text" data={data}>Text</Container>
這樣的建議能夠有效下降 diff
時的性能損耗,當場景複雜時,收益就比較可觀了。
diff
算法採用的 同層比對 的策略,所以若是是跨層級的移動,就會 從新建立新節點並刪除原來的節點,並非真正的移動。因此保證 渲染樹結構穩定 能夠有效提升性能。
diff
時須要額外的循環比對,應該減小沒必要要的頻繁移動;當須要列表中 VNode
的 同層移動 時,加上惟一標識 key
能有效提升 diff
性能,避免元素的 重渲染。
VNode.key
在 diff
先後的 一致,這樣纔可有效提高性能,避免使用 index
、 Math.random
、時間戳等值;key
值一樣會生效,不一樣的 key
會致使節點被斷定爲非同類節點,從而進行替換;VNode Tree
碎片化,從而能更有效地進行 局部更新,減小觸發 diff
的節點數量,提升性能且提升代碼複用率;diff
相比普通節點來講更爲 複雜,須要執行例如生命週期,組件比對 等,因此須要 合理規劃,避免 過度組件化 致使 內存的浪費和影響性能,一些 複用率低的靜態元素 直接使用元素節點更爲合理;受篇幅所限,本文暫且完成到這裏。先來總結回顧下咱們完成的部分:
JSX
是一種 動態模板語法,經過 Babel
編譯爲 createElement
函數;createElement
將 JSX
轉換爲 虛擬Dom(VNode),包含完整的標籤信息 (類型、屬性、子級列表);createElm
與 render
,完成 VNode
的 初次渲染;Diff
是一種 計算得出兩個 VNode 差別 的算法,使用 同層比對、惟一標識、組件模式 優化了算法比對性能,將時間複雜度下降爲 O(n)
;diffChildren
) 採用 兩端比對算法 + Key值比對 算法,大大提升了 Diff
效率;diffVNode
、diffProps
、diffChildren
,完成 VNode
的 動態更新;咱們完成了框架最核心的 JSX
- Render
- Diff
的渲染更新的主機制,奠基了最底層的基礎。在下一篇文章中,咱們將繼續基於此完成 組件化(Component
)、組件更新(setState
) 以及 生命週期。但願能幫助你們更瞭解 React,掌握一些優秀的編程思惟。
<!--[React 實踐揭祕之旅,中高級前端必備(下) ->]()-->
這篇文章所完成的類 React
框架是過去幾個月我本身在 Web
遊戲方面的嘗試和探索,目的是指望能 下降傳統前端工程師開發遊戲的門檻 和 引入前端開發模式提高開發效率,使 前端開發 與 遊戲開發 造成必定程度的優點互補。固然這個目標很大,也並不簡單,規劃中仍然有許多工做要作。有興趣的小夥伴移步 github 查看完整代碼:
最後,咱們去年下半年在廈門創辦了一家技術公司,主要是在 遊戲領域 和 K12 STEAM 教育 方向上的努力。很是歡迎 有興趣想深刻了解 或者 想跟我探討 的小夥伴直接聯繫我!🥳~~
Tips:看博主寫得這麼辛苦下,跪求點贊、關注、Star!更多文章猛戳 ->
郵箱: 159042708@qq.com 微信/QQ: 159042708
[祝福#感恩#武漢加油##RIP KOBE#]()