React框架核心原理全面深入解析
前言
這篇文章循序漸進(jìn)地介紹實(shí)現(xiàn)以下幾個(gè)概念,遵循本篇文章基本就能搞懂為啥需要fiber,為啥需要commit和phases、reconciliation階段等原理。本篇文章又不完全和原文一致,這里會(huì)加入我自己的一些思考,比如經(jīng)過(guò)performUnitOfWork
處理后fiber tree和element tree的聯(lián)系等。
- createElement函數(shù)
- render函數(shù)
- Concurrent Mode
- Fibers
- Render and Commit Phases
- Reconciliation
- Function Components
- Hooks
第一章 基本概念
以下面代碼為例
// 1.jsx語(yǔ)法不是合法的js語(yǔ)法 // const element = <h1 title="foo">Hello</h1> // 2.經(jīng)babel等編譯工具將jsx轉(zhuǎn)換成js,將jsx轉(zhuǎn)換成createElement函數(shù)調(diào)用的方式 // const element = React.createElement( // "h1", // { title: "foo" }, // "Hello" // ) // 3.React.createElement返回的最終對(duì)象大致如下: const element = { type: "h1", props: { title: "foo", children: "Hello", }, } const container = document.getElementById("root") // ReactDOM.render(element, container) // 4.替換ReactDOM.render函數(shù)的邏輯,ReactDOM.render大致處理邏輯: const node = document.createElement(element.type) node['title'] = element.props.title const text = document.createTextNode("") text["nodeValue"] = element.props.children node.appendChild(text) container.appendChild(node)
為了避免歧義,這里使用 element
表示 React elements
,node
表示真實(shí)的DOM元素節(jié)點(diǎn)。
至此,這段代碼無(wú)需經(jīng)過(guò)任何編譯已經(jīng)能夠在瀏覽器上跑起來(lái)了,不信你可以復(fù)制到瀏覽器控制臺(tái)試試
這里有幾點(diǎn)需要注意:
- 先通過(guò)
node.appendChild(text)
將子元素添加到父元素,然后再通過(guò)container.appendChild(node)
將父元素添加到容器container
中觸發(fā)瀏覽器渲染頁(yè)面。這個(gè)順序不能反過(guò)來(lái),也就是說(shuō)只有整個(gè)真實(shí)dom樹構(gòu)建完成才能添加到容器中。假設(shè)這個(gè)順序反過(guò)來(lái),比如先執(zhí)行container.appendChild(node)
,則觸發(fā)瀏覽器回流。再執(zhí)行node.appendChild(text)
又觸發(fā)瀏覽器回流。性能極差 React.createElement
返回的最終的對(duì)象就是virtual dom
樹,ReactDOM.render
根據(jù)這個(gè)virtual dom
創(chuàng)建真實(shí)的dom樹
第二章 createElement 函數(shù)
以下面的代碼為例
React.createElement
接收的children有可能是原子值,比如字符串或者數(shù)字等,React.createElement('h1', {title: 'foo'}, 'Hello')
。為了簡(jiǎn)化我們的代碼,創(chuàng)建一個(gè)特殊的TEXT_ELEMENT
類型將其轉(zhuǎn)換成對(duì)象
參考React實(shí)戰(zhàn)視頻講解:進(jìn)入學(xué)習(xí)
React.createElement = (type, props, ...children) => { return { type, props: { ...props, children: children.map(child => { if(typeof child === 'object'){ return child } return { type: 'TEXT_ELEMENT', props: { nodeValue: child, children: [], } } }) } } } // const element = ( // <div id="foo"> // <a>bar</a> // <b /> // </div> // ) // 將jsx轉(zhuǎn)換成js語(yǔ)法 const element = React.createElement( "div", { id: "foo" }, React.createElement("a", null, "bar"), React.createElement("b") ) const container = document.getElementById("root") ReactDOM.render(element, container)
好了,現(xiàn)在我們已經(jīng)實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的createElement
函數(shù),我們可以通過(guò)一段特殊的注釋來(lái)告訴babel在將jsx轉(zhuǎn)換成js時(shí)使用我們自己的createElement
函數(shù):
const MiniReact = { createElement: (type, props, ...children) => { return { type, props: { ...props, children: children.map(child => { if(typeof child === 'object'){ return child } return { type: 'TEXT_ELEMENT', props: { nodeValue: child, children: [], } } }) } } } } /** @jsx MiniReact.createElement */ const element = ( <div id="foo"> <a>bar</a> <b /> </div> ) console.log('element======', element) const container = document.getElementById("root") ReactDOM.render(element, container)
第三章 render函數(shù)
import React from 'react'; function render(element, container) { const dom = element.type === 'TEXT_ELEMENT' ? document.createTextNode("") : document.createElement(element.type) const isProperty = key => key !== 'children' Object.keys(element.props) .filter(isProperty) .forEach(name => { dom[name] = element.props[name] }) element.props.children.forEach(child => { render(child, dom) }); container.appendChild(dom) } const MiniReact = { createElement: (type, props, ...children) => { return { type, props: { ...props, children: children.map(child => { if(typeof child === 'object'){ return child } return { type: 'TEXT_ELEMENT', props: { nodeValue: child, children: [], } } }) } } }, render } /** @jsx MiniReact.createElement */ const element = ( <div id="foo"> <a>bar</a> <b /> </div> ) console.log('element======', element) const container = document.getElementById("root") MiniReact.render(element, container)
render
函數(shù)遞歸創(chuàng)建真實(shí)的dom元素,然后將各個(gè)元素append到其父元素中,最后整個(gè)dom樹append到root container中,渲染完成,這個(gè)過(guò)程一旦開始,中間是無(wú)法打斷的,直到整個(gè)應(yīng)用渲染完成。這也是React16
版本以前的渲染過(guò)程
注意,只有當(dāng)整個(gè)dom樹append到root container中時(shí),頁(yè)面才會(huì)顯示
第四章 Concurrent Mode
在第三章中可以看到,當(dāng)前版本的render
函數(shù)是遞歸構(gòu)建dom樹,最后才append到root container,最終頁(yè)面才渲染出來(lái)。這里有個(gè)問(wèn)題,如果dom節(jié)點(diǎn)數(shù)量龐大,遞歸層級(jí)過(guò)深,這個(gè)過(guò)程其實(shí)是很耗時(shí)的,導(dǎo)致render
函數(shù)長(zhǎng)時(shí)間占用主線程,瀏覽器無(wú)法響應(yīng)用戶輸入等事件,造成卡頓的現(xiàn)象。
因此我們需要將render
過(guò)程拆分成小的任務(wù)單元,每執(zhí)行完一個(gè)單元,都允許瀏覽器打斷render
過(guò)程并執(zhí)行高優(yōu)先級(jí)的任務(wù),等瀏覽器得空再繼續(xù)執(zhí)行render
過(guò)程
如果對(duì)requestIdleCallback
不熟悉的,可以自行了解一下。真實(shí)React代碼中并沒有使用這個(gè)api,因?yàn)橛屑嫒菪詥?wèn)題。因此React使用scheduler package
模擬這個(gè)調(diào)度過(guò)程
let nextUnitOfWork = null function workLoop(deadline) { let shouldYield = false while (nextUnitOfWork && !shouldYield) { nextUnitOfWork = performUnitOfWork( nextUnitOfWork ) shouldYield = deadline.timeRemaining() < 1 } requestIdleCallback(workLoop) } requestIdleCallback(workLoop) function performUnitOfWork(nextUnitOfWork) { // TODO }
performUnitOfWork
接收當(dāng)前工作單元,并返回下一個(gè)工作單元。工作單元可以理解為就是一個(gè)fiber對(duì)象節(jié)點(diǎn)
workLoop
循環(huán)里會(huì)循環(huán)調(diào)用performUnitOfWork
,直到所有工作單元都已經(jīng)處理完畢,或者當(dāng)前幀瀏覽器已經(jīng)沒有空閑時(shí)間,則循環(huán)終止。等下次瀏覽器空閑時(shí)間再接著繼續(xù)執(zhí)行
因此我們需要一種數(shù)據(jù)結(jié)構(gòu),能夠支持任務(wù)打斷并且可以接著繼續(xù)執(zhí)行,很顯然,鏈表就非常適合
第五章 Fibers
Fibers就是一種數(shù)據(jù)結(jié)構(gòu),支持將渲染過(guò)程拆分成工作單元,本質(zhì)上就是一個(gè)雙向鏈表。這種數(shù)據(jù)結(jié)構(gòu)的好處就是方便找到下一個(gè)工作單元
Fiber包含三層含義:
- 作為架構(gòu)來(lái)說(shuō),之前
React 15
的Reconciler
采用遞歸的方式執(zhí)行,數(shù)據(jù)保存在遞歸調(diào)用棧中,所以被稱為stack Reconciler
。React 16
的Reconciler
基于Fiber節(jié)點(diǎn)實(shí)現(xiàn),被稱為Fiber Reconciler
- 作為靜態(tài)的數(shù)據(jù)結(jié)構(gòu)來(lái)說(shuō),每個(gè)Fiber節(jié)點(diǎn)對(duì)應(yīng)一個(gè)
React Element
,保存了該組件的類型(函數(shù)組件/類組件/html標(biāo)簽)、對(duì)應(yīng)的DOM節(jié)點(diǎn)信息等 - 作為動(dòng)態(tài)的工作單元來(lái)說(shuō),每個(gè)Fiber節(jié)點(diǎn)保存了本次更新中該組件改變的狀態(tài)、要執(zhí)行的工作等
Fiber的幾點(diǎn)冷知識(shí):
- 一個(gè)Fiber節(jié)點(diǎn)對(duì)應(yīng)一個(gè)React Element節(jié)點(diǎn),同時(shí)也是一個(gè)工作單元
- 每個(gè)fiber節(jié)點(diǎn)都有指向第一個(gè)子元素,下一個(gè)兄弟元素,父元素的指針**
以下面代碼為例:
MiniReact.render( <div> <h1> <p /> <a /> </h1> <h2 /> </div>, container )
對(duì)應(yīng)的fiber tree如下:
import React from 'react'; // 根據(jù)fiber節(jié)點(diǎn)創(chuàng)建真實(shí)的dom節(jié)點(diǎn) function createDom(fiber) { const dom = fiber.type === 'TEXT_ELEMENT' ? document.createTextNode("") : document.createElement(fiber.type) const isProperty = key => key !== 'children' Object.keys(fiber.props) .filter(isProperty) .forEach(name => { dom[name] = fiber.props[name] }) return dom } let nextUnitOfWork = null // render函數(shù)主要邏輯: // 根據(jù)root container容器創(chuàng)建root fiber // 將nextUnitOfWork指針指向root fiber // element是react element tree function render(element, container){ nextUnitOfWork = { dom: container, props: { children: [element], // 此時(shí)的element還只是React.createElement函數(shù)創(chuàng)建的virtual dom樹 }, } } function workLoop(deadline) { let shouldYield = false while (nextUnitOfWork && !shouldYield) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork) shouldYield = deadline.timeRemaining() < 1 } requestIdleCallback(workLoop) } requestIdleCallback(workLoop) // performUnitOfWork函數(shù)主要邏輯: // 將element元素添加到DOM // 給element的子元素創(chuàng)建對(duì)應(yīng)的fiber節(jié)點(diǎn) // 返回下一個(gè)工作單元,即下一個(gè)fiber節(jié)點(diǎn),查找過(guò)程: // 1.如果有子元素,則返回子元素的fiber節(jié)點(diǎn) // 2.如果沒有子元素,則返回兄弟元素的fiber節(jié)點(diǎn) // 3.如果既沒有子元素又沒有兄弟元素,則往上查找其父節(jié)點(diǎn)的兄弟元素的fiber節(jié)點(diǎn) // 4.如果往上查找到root fiber節(jié)點(diǎn),說(shuō)明render過(guò)程已經(jīng)結(jié)束 function performUnitOfWork(fiber) { // 第一步 根據(jù)fiber節(jié)點(diǎn)創(chuàng)建真實(shí)的dom節(jié)點(diǎn),并保存在fiber.dom屬性中 if(!fiber.dom){ fiber.dom = createDom(fiber) } // 第二步 將當(dāng)前fiber節(jié)點(diǎn)的真實(shí)dom添加到父節(jié)點(diǎn)中,注意,這一步是會(huì)觸發(fā)瀏覽器回流重繪的!?。? if(fiber.parent){ fiber.parent.dom.appendChild(fiber.dom) } // 第三步 給子元素創(chuàng)建對(duì)應(yīng)的fiber節(jié)點(diǎn) const children = fiber.props.children let prevSibling children.forEach((child, index) => { const newFiber = { type: child.type, props: child.props, parent: fiber, dom: null } if(index === 0){ fiber.child = newFiber } else { prevSibling.sibling = newFiber } prevSibling = newFiber }) // 第四步,查找下一個(gè)工作單元 if(fiber.child){ return fiber.child } let nextFiber = fiber while(nextFiber){ if(nextFiber.sibling){ return nextFiber.sibling } nextFiber = nextFiber.parent } } const MiniReact = { createElement: (type, props, ...children) => { return { type, props: { ...props, children: children.map(child => { if(typeof child === 'object'){ return child } return { type: 'TEXT_ELEMENT', props: { nodeValue: child, children: [], } } }) } } }, render } /** @jsx MiniReact.createElement */ const element = ( <div> <h1> <p /> <a /> </h1> <h2 /> </div> ) // const element = ( // <div id="foo"> // <a>bar</a> // <b /> // </div> // ) console.log('element======', element) const container = document.getElementById("root") MiniReact.render(element, container)
這里有一點(diǎn)值得細(xì)品,React.createElement
返回的element tree
和performUnitOfWork
創(chuàng)建的fiber tree
有什么聯(lián)系。如下圖所示:
React Element Tree
是由React.createElement
方法創(chuàng)建的樹形結(jié)構(gòu)對(duì)象Fiber Tree
是根據(jù)React Element Tree
創(chuàng)建來(lái)的樹。每個(gè)Fiber節(jié)點(diǎn)保存著真實(shí)的DOM節(jié)點(diǎn),并且保存著對(duì)React Element Tree
中對(duì)應(yīng)的Element
節(jié)點(diǎn)的應(yīng)用。注意,Element
節(jié)點(diǎn)并不會(huì)保存對(duì)Fiber
節(jié)點(diǎn)的應(yīng)用
第六章 Render and Commit Phases
第五章的performUnitOfWork
有些問(wèn)題,在第二步中我們直接將新創(chuàng)建的真實(shí)dom節(jié)點(diǎn)掛載到了容器上,這樣會(huì)帶來(lái)兩個(gè)問(wèn)題:
- 每次執(zhí)行
performUnitOfWork
都會(huì)造成瀏覽器回流重繪,因?yàn)檎鎸?shí)的dom已經(jīng)被添加到瀏覽器上了,性能極差 - 瀏覽器是可以打斷渲染過(guò)程的,因此可能會(huì)造成用戶看到不完整的UI界面
我們需要改造一下我們的代碼,在performUnitOfWork
階段不把真實(shí)的dom節(jié)點(diǎn)掛載到容器上。保存fiber tree根節(jié)點(diǎn)的引用。等到fiber tree構(gòu)建完成,再一次性提交真實(shí)的dom節(jié)點(diǎn)并且添加到容器上。
import React from 'react'; function createDom(fiber) { const dom = fiber.type === 'TEXT_ELEMENT' ? document.createTextNode("") : document.createElement(fiber.type) const isProperty = key => key !== 'children' Object.keys(fiber.props) .filter(isProperty) .forEach(name => { dom[name] = fiber.props[name] }) return dom } let nextUnitOfWork = null let wipRoot = null function render(element, container){ wipRoot = { dom: container, props: { children: [element], // 此時(shí)的element還只是React.createElement函數(shù)創(chuàng)建的virtual dom樹 }, } nextUnitOfWork = wipRoot } function commitRoot(){ commitWork(wipRoot.child) wipRoot = null } function commitWork(fiber){ if(!fiber){ return } const domParent = fiber.parent.dom; domParent.appendChild(fiber.dom) commitWork(fiber.child) commitWork(fiber.sibling) } function workLoop(deadline) { let shouldYield = false while (nextUnitOfWork && !shouldYield) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork) shouldYield = deadline.timeRemaining() < 1 } if(!nextUnitOfWork && wipRoot){ commitRoot() } requestIdleCallback(workLoop) } requestIdleCallback(workLoop) function performUnitOfWork(fiber) { // 第一步 根據(jù)fiber節(jié)點(diǎn)創(chuàng)建真實(shí)的dom節(jié)點(diǎn),并保存在fiber.dom屬性中 if(!fiber.dom){ fiber.dom = createDom(fiber) } // 第二步 將當(dāng)前fiber節(jié)點(diǎn)的真實(shí)dom添加到父節(jié)點(diǎn)中,注意,這一步是會(huì)觸發(fā)瀏覽器回流重繪的?。?! // if(fiber.parent){ // fiber.parent.dom.appendChild(fiber.dom) // } // 第三步 給子元素創(chuàng)建對(duì)應(yīng)的fiber節(jié)點(diǎn) const children = fiber.props.children let prevSibling children.forEach((child, index) => { const newFiber = { type: child.type, props: child.props, parent: fiber, dom: null } if(index === 0){ fiber.child = newFiber } else { prevSibling.sibling = newFiber } prevSibling = newFiber }) // 第四步,查找下一個(gè)工作單元 if(fiber.child){ return fiber.child } let nextFiber = fiber while(nextFiber){ if(nextFiber.sibling){ return nextFiber.sibling } nextFiber = nextFiber.parent } } const MiniReact = { createElement: (type, props, ...children) => { return { type, props: { ...props, children: children.map(child => { if(typeof child === 'object'){ return child } return { type: 'TEXT_ELEMENT', props: { nodeValue: child, children: [], } } }) } } }, render } /** @jsx MiniReact.createElement */ const element = ( <div> <h1> <p /> <a /> </h1> <h2 /> </div> ) // const element = ( // <div id="foo"> // <a>bar</a> // <b /> // </div> // ) console.log('element======', element) const container = document.getElementById("root") MiniReact.render(element, container)
第七章 Reconciliation
目前為止,我們只考慮添加dom節(jié)點(diǎn)到容器上這一單一場(chǎng)景,更新刪除還沒實(shí)現(xiàn)。
我們需要對(duì)比最新的React Element Tree
和最近一次的Fiber Tree
的差異
我們需要給每個(gè)fiber節(jié)點(diǎn)添加一個(gè)alternate屬性來(lái)保存舊的fiber節(jié)點(diǎn)
alternate保存的舊的fiber節(jié)點(diǎn)主要有以下幾個(gè)用途:
- 復(fù)用舊fiber節(jié)點(diǎn)上的真實(shí)dom節(jié)點(diǎn)
- 舊fiber節(jié)點(diǎn)上的props和新的element節(jié)點(diǎn)的props對(duì)比
- 舊fiber節(jié)點(diǎn)上保存有更新的隊(duì)列,在創(chuàng)建新的fiber節(jié)點(diǎn)時(shí)執(zhí)行這些隊(duì)列以獲取最新的頁(yè)面
const children = fiber.props.children reconcileChildren(fiber, children) function reconcileChildren(wipFiber, elements) { let index = 0 let oldFiber = wipFiber.alternate && wipFiber.alternate.child let prevSibling = null while (index < elements.length || oldFiber != null) { const element = elements[index] let newFiber = null const sameType = oldFiber && element && element.type == oldFiber.type if (sameType) { newFiber = { type: oldFiber.type, props: element.props, dom: oldFiber.dom, parent: wipFiber, alternate: oldFiber, effectTag: "UPDATE", } } if (element && !sameType) { newFiber = { type: element.type, props: element.props, dom: null, parent: wipFiber, alternate: null, effectTag: "PLACEMENT", } } if (oldFiber && !sameType) { oldFiber.effectTag = "DELETION" deletions.push(oldFiber) } if (oldFiber) { oldFiber = oldFiber.sibling } if (index === 0) { wipFiber.child = newFiber } else if (element) { prevSibling.sibling = newFiber } prevSibling = newFiber index++ } }
如上代碼所示:
協(xié)調(diào)過(guò)程:
- 本質(zhì)上依然是根據(jù)新的React Element Tree創(chuàng)建新的
Fiber Tree
,不過(guò)為了節(jié)省內(nèi)存開銷,協(xié)調(diào)過(guò)程會(huì)判斷新的fiber節(jié)點(diǎn)能否復(fù)用舊的fiber節(jié)點(diǎn)上的真實(shí)dom元素,如果能復(fù)用,就不需要再?gòu)念^到尾全部重新創(chuàng)建一遍真實(shí)的dom元素。同時(shí)每個(gè)新fiber節(jié)點(diǎn)上還會(huì)保存著對(duì)舊fiber節(jié)點(diǎn)的引用,方便在commit階段做新舊屬性props的對(duì)比。 - 如果
old fiber.type
和new element.type
相同,則保留舊的dom節(jié)點(diǎn),只更新props屬性 - 如果
type
不相同并且有new element
,則創(chuàng)建一個(gè)新的真實(shí)dom節(jié)點(diǎn) - 如果
type
不同并且有old fiber
節(jié)點(diǎn),則刪除該節(jié)點(diǎn)對(duì)應(yīng)的真實(shí)dom節(jié)點(diǎn) - 刪除節(jié)點(diǎn)需要有個(gè)專門的數(shù)組收集需要?jiǎng)h除的舊的fiber節(jié)點(diǎn)。由于新的element tree創(chuàng)建出來(lái)的新的fiber tree不存在對(duì)應(yīng)的dom,因此需要收集舊的fiber節(jié)點(diǎn),并在commit階段刪除
注意,協(xié)調(diào)過(guò)程,還是以最新的React Element Tree為主去創(chuàng)建一個(gè)新的fiber tree,只不過(guò)是新的fiber節(jié)點(diǎn)復(fù)用舊的fiber節(jié)點(diǎn)的真實(shí)dom元素,畢竟頻繁創(chuàng)建真實(shí)dom是很消耗內(nèi)存的。新的fiber節(jié)點(diǎn)還是會(huì)保存著對(duì)舊的fiber節(jié)點(diǎn)的引用,方便在commit階段進(jìn)行新屬性和舊屬性的比較。這里會(huì)有個(gè)問(wèn)題,如果新fiber節(jié)點(diǎn)保留舊fiber節(jié)點(diǎn)的引用,那么隨著更新次數(shù)越來(lái)越多,舊的fiber tree是不是也會(huì)越來(lái)越多,如何銷毀?
import React from 'react'; function createDom(fiber) { const dom = fiber.type === 'TEXT_ELEMENT' ? document.createTextNode("") : document.createElement(fiber.type) updateDom(dom, {}, fiber.props) return dom } let nextUnitOfWork = null let wipRoot = null // 保存著對(duì)root fiber的引用 let currentRoot = null // 保存著當(dāng)前頁(yè)面對(duì)應(yīng)的fiber tree let deletions = null function render(element, container){ wipRoot = { dom: container, props: { children: [element], // 此時(shí)的element還只是React.createElement函數(shù)創(chuàng)建的virtual dom樹 }, alternate: currentRoot, } deletions = [] nextUnitOfWork = wipRoot } function commitRoot(){ deletions.forEach(commitWork) commitWork(wipRoot.child) currentRoot = wipRoot wipRoot = null } const isEvent = key => key.startsWith("on") const isProperty = key => key !== "children" && !isEvent(key) const isNew = (prev, next) => key => prev[key] !== next[key] const isGone = (prev, next) => key => !(key in next) function updateDom(dom, prevProps, nextProps) { //Remove old or changed event listeners Object.keys(prevProps) .filter(isEvent) .filter( key => !(key in nextProps) || isNew(prevProps, nextProps)(key) ) .forEach(name => { const eventType = name .toLowerCase() .substring(2) dom.removeEventListener( eventType, prevProps[name] ) }) // Remove old properties Object.keys(prevProps) .filter(isProperty) .filter(isGone(prevProps, nextProps)) .forEach(name => { dom[name] = "" }) // Set new or changed properties Object.keys(nextProps) .filter(isProperty) .filter(isNew(prevProps, nextProps)) .forEach(name => { dom[name] = nextProps[name] }) // Add event listeners Object.keys(nextProps) .filter(isEvent) .filter(isNew(prevProps, nextProps)) .forEach(name => { const eventType = name .toLowerCase() .substring(2) dom.addEventListener( eventType, nextProps[name] ) }) } function commitWork(fiber){ if(!fiber){ return } const domParent = fiber.parent.dom; if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) { domParent.appendChild(fiber.dom) } else if (fiber.effectTag === "UPDATE" && fiber.dom != null) { updateDom( fiber.dom, fiber.alternate.props, fiber.props ) } else if (fiber.effectTag === "DELETION") { domParent.removeChild(fiber.dom) } commitWork(fiber.child) commitWork(fiber.sibling) } function workLoop(deadline) { let shouldYield = false while (nextUnitOfWork && !shouldYield) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork) shouldYield = deadline.timeRemaining() < 1 } if(!nextUnitOfWork && wipRoot){ commitRoot() } requestIdleCallback(workLoop) } requestIdleCallback(workLoop) function reconcileChildren(wipFiber, elements) { let index = 0 let oldFiber = wipFiber.alternate && wipFiber.alternate.child let prevSibling = null while (index < elements.length || oldFiber != null) { const element = elements[index] let newFiber = null const sameType = oldFiber && element && element.type == oldFiber.type if (sameType) { newFiber = { type: oldFiber.type, props: element.props, dom: oldFiber.dom, parent: wipFiber, alternate: oldFiber, effectTag: "UPDATE", } } if (element && !sameType) { newFiber = { type: element.type, props: element.props, dom: null, parent: wipFiber, alternate: null, effectTag: "PLACEMENT", } } if (oldFiber && !sameType) { oldFiber.effectTag = "DELETION" deletions.push(oldFiber) } if (oldFiber) { oldFiber = oldFiber.sibling } if (index === 0) { wipFiber.child = newFiber } else if (element) { prevSibling.sibling = newFiber } prevSibling = newFiber index++ } } function performUnitOfWork(fiber) { // 第一步 根據(jù)fiber節(jié)點(diǎn)創(chuàng)建真實(shí)的dom節(jié)點(diǎn),并保存在fiber.dom屬性中 if(!fiber.dom){ fiber.dom = createDom(fiber) } // 第二步 將當(dāng)前fiber節(jié)點(diǎn)的真實(shí)dom添加到父節(jié)點(diǎn)中,注意,這一步是會(huì)觸發(fā)瀏覽器回流重繪的!??! // if(fiber.parent){ // fiber.parent.dom.appendChild(fiber.dom) // } // 第三步 給子元素創(chuàng)建對(duì)應(yīng)的fiber節(jié)點(diǎn) const children = fiber.props.children // let prevSibling // children.forEach((child, index) => { // const newFiber = { // type: child.type, // props: child.props, // parent: fiber, // dom: null // } // if(index === 0){ // fiber.child = newFiber // } else { // prevSibling.sibling = newFiber // } // prevSibling = newFiber // }) reconcileChildren(fiber, children) // 第四步,查找下一個(gè)工作單元 if(fiber.child){ return fiber.child } let nextFiber = fiber while(nextFiber){ if(nextFiber.sibling){ return nextFiber.sibling } nextFiber = nextFiber.parent } } const MiniReact = { createElement: (type, props, ...children) => { return { type, props: { ...props, children: children.map(child => { if(typeof child === 'object'){ return child } return { type: 'TEXT_ELEMENT', props: { nodeValue: child, children: [], } } }) } } }, render } /** @jsx MiniReact.createElement */ const container = document.getElementById("root") const updateValue = e => { rerender(e.target.value) } const rerender = value => { const element = ( <div> <input onInput={updateValue} value={value} /> <h2>Hello {value}</h2> </div> ) MiniReact.render(element, container) } rerender("World")
第八章 Function Components
本章以下面的代碼為例:
/** @jsx MiniReact.createElement */ const container = document.getElementById("root") function App(props){ return <h1>Hi { props.name }</h1> } const element = <App name="foo" /> MiniReact.render(element, container)
jsx經(jīng)過(guò)babel編譯后:
function App(props) { return MiniReact.createElement("h1", null, "Hi ", props.name); } const element = MiniReact.createElement(App, { name: "foo" });
函數(shù)組件有兩點(diǎn)不同的地方:
- 函數(shù)組件對(duì)應(yīng)的fiber節(jié)點(diǎn)沒有對(duì)應(yīng)的真實(shí)dom元素
- 需要執(zhí)行函數(shù)才能獲取對(duì)應(yīng)的children節(jié)點(diǎn),而不是直接從
props.children
獲取
由于函數(shù)組件沒有對(duì)應(yīng)的fiber節(jié)點(diǎn),因此在commit階段在找父fiber節(jié)點(diǎn)對(duì)應(yīng)的dom時(shí),需要判斷是否存在該dom元素
本章完整代碼:
import React from 'react'; function createDom(fiber) { const dom = fiber.type === 'TEXT_ELEMENT' ? document.createTextNode("") : document.createElement(fiber.type) updateDom(dom, {}, fiber.props) return dom } let nextUnitOfWork = null let wipRoot = null // 保存著對(duì)root fiber的引用 let currentRoot = null // 保存著當(dāng)前頁(yè)面對(duì)應(yīng)的fiber tree let deletions = null function render(element, container){ wipRoot = { dom: container, props: { children: [element], // 此時(shí)的element還只是React.createElement函數(shù)創(chuàng)建的virtual dom樹 }, alternate: currentRoot, } deletions = [] nextUnitOfWork = wipRoot } function commitRoot(){ deletions.forEach(commitWork) commitWork(wipRoot.child) currentRoot = wipRoot wipRoot = null } const isEvent = key => key.startsWith("on") const isProperty = key => key !== "children" && !isEvent(key) const isNew = (prev, next) => key => prev[key] !== next[key] const isGone = (prev, next) => key => !(key in next) function updateDom(dom, prevProps, nextProps) { //Remove old or changed event listeners Object.keys(prevProps) .filter(isEvent) .filter( key => !(key in nextProps) || isNew(prevProps, nextProps)(key) ) .forEach(name => { const eventType = name .toLowerCase() .substring(2) dom.removeEventListener( eventType, prevProps[name] ) }) // Remove old properties Object.keys(prevProps) .filter(isProperty) .filter(isGone(prevProps, nextProps)) .forEach(name => { dom[name] = "" }) // Set new or changed properties Object.keys(nextProps) .filter(isProperty) .filter(isNew(prevProps, nextProps)) .forEach(name => { dom[name] = nextProps[name] }) // Add event listeners Object.keys(nextProps) .filter(isEvent) .filter(isNew(prevProps, nextProps)) .forEach(name => { const eventType = name .toLowerCase() .substring(2) dom.addEventListener( eventType, nextProps[name] ) }) } function commitWork(fiber){ if(!fiber){ return } let domParentFiber = fiber.parent while(!domParentFiber.dom){ domParentFiber = domParentFiber.parent } const domParent = domParentFiber.dom; if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) { domParent.appendChild(fiber.dom) } else if (fiber.effectTag === "UPDATE" && fiber.dom != null) { updateDom(fiber.dom, fiber.alternate.props, fiber.props) } else if (fiber.effectTag === "DELETION") { // domParent.removeChild(fiber.dom) commitDeletion(fiber, domParent) } commitWork(fiber.child) commitWork(fiber.sibling) } function commitDeletion(fiber, domParent){ if(fiber.dom){ domParent.removeChild(fiber.dom) } else { commitDeletion(fiber.child, domParent) } } function workLoop(deadline) { let shouldYield = false while (nextUnitOfWork && !shouldYield) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork) shouldYield = deadline.timeRemaining() < 1 } if(!nextUnitOfWork && wipRoot){ commitRoot() } requestIdleCallback(workLoop) } requestIdleCallback(workLoop) function reconcileChildren(wipFiber, elements) { let index = 0 let oldFiber = wipFiber.alternate && wipFiber.alternate.child let prevSibling = null while (index < elements.length || oldFiber != null) { const element = elements[index] let newFiber = null const sameType = oldFiber && element && element.type == oldFiber.type if (sameType) { newFiber = { type: oldFiber.type, props: element.props, dom: oldFiber.dom, parent: wipFiber, alternate: oldFiber, effectTag: "UPDATE", } } if (element && !sameType) { newFiber = { type: element.type, props: element.props, dom: null, parent: wipFiber, alternate: null, effectTag: "PLACEMENT", } } if (oldFiber && !sameType) { oldFiber.effectTag = "DELETION" deletions.push(oldFiber) } if (oldFiber) { oldFiber = oldFiber.sibling } if (index === 0) { wipFiber.child = newFiber } else if (element) { prevSibling.sibling = newFiber } prevSibling = newFiber index++ } } function performUnitOfWork(fiber) { // 1.函數(shù)組件對(duì)應(yīng)的fiber節(jié)點(diǎn)沒有真實(shí)dom元素 // 2.函數(shù)組件需要運(yùn)行函數(shù)獲取children const isFunctionComponent = fiber.type instanceof Function if(!isFunctionComponent && !fiber.dom){ fiber.dom = createDom(fiber) } const children = isFunctionComponent ? [fiber.type(fiber.props)] : fiber.props.children // 第二步 為每一個(gè)新的react element節(jié)點(diǎn)創(chuàng)建對(duì)應(yīng)的fiber節(jié)點(diǎn),并判斷舊的fiber節(jié)點(diǎn)上的真實(shí)dom元素是否可以復(fù)用。 // 節(jié)省創(chuàng)建真實(shí)dom元素的開銷 reconcileChildren(fiber, children) // 第三步,查找下一個(gè)工作單元 if(fiber.child){ return fiber.child } let nextFiber = fiber while(nextFiber){ if(nextFiber.sibling){ return nextFiber.sibling } nextFiber = nextFiber.parent } } const MiniReact = { createElement: (type, props, ...children) => { return { type, props: { ...props, children: children.map(child => { if(typeof child === 'object'){ return child } return { type: 'TEXT_ELEMENT', props: { nodeValue: child, children: [], } } }) } } }, render } /** @jsx MiniReact.createElement */ const container = document.getElementById("root") function App(props){ return <h1>Hi { props.name }</h1> } const element = <App name="foo" /> MiniReact.render(element, container)
第九章 Hooks
本章完整代碼
import React from 'react'; function createDom(fiber) { const dom = fiber.type === 'TEXT_ELEMENT' ? document.createTextNode("") : document.createElement(fiber.type) updateDom(dom, {}, fiber.props) return dom } let nextUnitOfWork = null let wipRoot = null // 保存著對(duì)root fiber的引用 let currentRoot = null // 保存著當(dāng)前頁(yè)面對(duì)應(yīng)的fiber tree let deletions = null function render(element, container){ wipRoot = { dom: container, props: { children: [element], // 此時(shí)的element還只是React.createElement函數(shù)創(chuàng)建的virtual dom樹 }, alternate: currentRoot, } deletions = [] nextUnitOfWork = wipRoot } function commitRoot(){ deletions.forEach(commitWork) commitWork(wipRoot.child) currentRoot = wipRoot wipRoot = null } const isEvent = key => key.startsWith("on") const isProperty = key => key !== "children" && !isEvent(key) const isNew = (prev, next) => key => prev[key] !== next[key] const isGone = (prev, next) => key => !(key in next) function updateDom(dom, prevProps, nextProps) { //Remove old or changed event listeners Object.keys(prevProps) .filter(isEvent) .filter( key => !(key in nextProps) || isNew(prevProps, nextProps)(key) ) .forEach(name => { const eventType = name .toLowerCase() .substring(2) dom.removeEventListener( eventType, prevProps[name] ) }) // Remove old properties Object.keys(prevProps) .filter(isProperty) .filter(isGone(prevProps, nextProps)) .forEach(name => { dom[name] = "" }) // Set new or changed properties Object.keys(nextProps) .filter(isProperty) .filter(isNew(prevProps, nextProps)) .forEach(name => { dom[name] = nextProps[name] }) // Add event listeners Object.keys(nextProps) .filter(isEvent) .filter(isNew(prevProps, nextProps)) .forEach(name => { const eventType = name .toLowerCase() .substring(2) dom.addEventListener( eventType, nextProps[name] ) }) } function commitWork(fiber){ if(!fiber){ return } let domParentFiber = fiber.parent while(!domParentFiber.dom){ domParentFiber = domParentFiber.parent } const domParent = domParentFiber.dom; if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) { domParent.appendChild(fiber.dom) } else if (fiber.effectTag === "UPDATE" && fiber.dom != null) { updateDom(fiber.dom, fiber.alternate.props, fiber.props) } else if (fiber.effectTag === "DELETION") { // domParent.removeChild(fiber.dom) commitDeletion(fiber, domParent) } commitWork(fiber.child) commitWork(fiber.sibling) } function commitDeletion(fiber, domParent){ if(fiber.dom){ domParent.removeChild(fiber.dom) } else { commitDeletion(fiber.child, domParent) } } function workLoop(deadline) { let shouldYield = false while (nextUnitOfWork && !shouldYield) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork) shouldYield = deadline.timeRemaining() < 1 } if(!nextUnitOfWork && wipRoot){ commitRoot() } requestIdleCallback(workLoop) } requestIdleCallback(workLoop) function reconcileChildren(wipFiber, elements) { let index = 0 let oldFiber = wipFiber.alternate && wipFiber.alternate.child let prevSibling = null while (index < elements.length || oldFiber != null) { const element = elements[index] let newFiber = null const sameType = oldFiber && element && element.type == oldFiber.type if (sameType) { newFiber = { type: oldFiber.type, props: element.props, dom: oldFiber.dom, parent: wipFiber, alternate: oldFiber, effectTag: "UPDATE", } } if (element && !sameType) { newFiber = { type: element.type, props: element.props, dom: null, parent: wipFiber, alternate: null, effectTag: "PLACEMENT", } } if (oldFiber && !sameType) { oldFiber.effectTag = "DELETION" deletions.push(oldFiber) } if (oldFiber) { oldFiber = oldFiber.sibling } if (index === 0) { wipFiber.child = newFiber } else if (element) { prevSibling.sibling = newFiber } prevSibling = newFiber index++ } } function performUnitOfWork(fiber) { // 1.函數(shù)組件對(duì)應(yīng)的fiber節(jié)點(diǎn)沒有真實(shí)dom元素 // 2.函數(shù)組件需要運(yùn)行函數(shù)獲取children const isFunctionComponent = fiber.type instanceof Function if(!isFunctionComponent && !fiber.dom){ fiber.dom = createDom(fiber) } const children = isFunctionComponent ? updateFunctionComponent(fiber) : fiber.props.children // 第二步 為每一個(gè)新的react element節(jié)點(diǎn)創(chuàng)建對(duì)應(yīng)的fiber節(jié)點(diǎn),并判斷舊的fiber節(jié)點(diǎn)上的真實(shí)dom元素是否可以復(fù)用。 // 節(jié)省創(chuàng)建真實(shí)dom元素的開銷 reconcileChildren(fiber, children) // 第三步,查找下一個(gè)工作單元 if(fiber.child){ return fiber.child } let nextFiber = fiber while(nextFiber){ if(nextFiber.sibling){ return nextFiber.sibling } nextFiber = nextFiber.parent } } let wipFiber = null let hookIndex = null function updateFunctionComponent(fiber){ wipFiber = fiber hookIndex = 0 wipFiber.hooks = [] return [fiber.type(fiber.props)] } function useState(initial){ const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex] const hook = { state: oldHook ? oldHook.state : initial, queue: [], } const actions = oldHook ? oldHook.queue : [] actions.forEach(action => { hook.state = action(hook.state) }) const setState = action => { hook.queue.push(action) wipRoot = { dom: currentRoot.dom, props: currentRoot.props, alternate: currentRoot, } nextUnitOfWork = wipRoot deletions = [] } wipFiber.hooks.push(hook) hookIndex++ return [hook.state, setState] } const MiniReact = { createElement: (type, props, ...children) => { return { type, props: { ...props, children: children.map(child => { if(typeof child === 'object'){ return child } return { type: 'TEXT_ELEMENT', props: { nodeValue: child, children: [], } } }) } } }, render, useState, } /** @jsx MiniReact.createElement */ const container = document.getElementById("root") function Counter(){ const [state, setState] = MiniReact.useState(1) return ( <h1 onClick={() => setState(c => c + 1)}> Count: { state } </h1> ) } const element = <Counter /> MiniReact.render(element, container)
到此這篇關(guān)于React框架核心原理全面深入解析的文章就介紹到這了,更多相關(guān)React框架核心內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
react如何使用mobx6動(dòng)態(tài)加載數(shù)據(jù)
MobX是一個(gè)強(qiáng)大而簡(jiǎn)單的狀態(tài)管理工具,它可以幫助我們更好地組織和管理React應(yīng)用程序中的數(shù)據(jù)流,本文給大家介紹react如何使用mobx6動(dòng)態(tài)加載數(shù)據(jù),感興趣的朋友跟隨小編一起看看吧2024-02-02基于PixiJS實(shí)現(xiàn)react圖標(biāo)旋轉(zhuǎn)動(dòng)效
PixiJS是一個(gè)開源的基于web的渲染系統(tǒng),為游戲、數(shù)據(jù)可視化和其他圖形密集型項(xiàng)目提供了極快的性能,這篇文章主要介紹了用PixiJS實(shí)現(xiàn)react圖標(biāo)旋轉(zhuǎn)動(dòng)效,需要的朋友可以參考下2022-05-05antd4里table滾動(dòng)的實(shí)現(xiàn)
本文主要介紹了antd4里table滾動(dòng)的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-03-03