React框架核心原理全面深入解析
前言
這篇文章循序漸進(jìn)地介紹實現(xiàn)以下幾個概念,遵循本篇文章基本就能搞懂為啥需要fiber,為啥需要commit和phases、reconciliation階段等原理。本篇文章又不完全和原文一致,這里會加入我自己的一些思考,比如經(jīng)過performUnitOfWork處理后fiber tree和element tree的聯(lián)系等。
- createElement函數(shù)
- render函數(shù)
- Concurrent Mode
- Fibers
- Render and Commit Phases
- Reconciliation
- Function Components
- Hooks
第一章 基本概念
以下面代碼為例
// 1.jsx語法不是合法的js語法
// 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返回的最終對象大致如下:
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 表示真實的DOM元素節(jié)點(diǎn)。
至此,這段代碼無需經(jīng)過任何編譯已經(jīng)能夠在瀏覽器上跑起來了,不信你可以復(fù)制到瀏覽器控制臺試試
這里有幾點(diǎn)需要注意:
- 先通過
node.appendChild(text)將子元素添加到父元素,然后再通過container.appendChild(node)將父元素添加到容器container中觸發(fā)瀏覽器渲染頁面。這個順序不能反過來,也就是說只有整個真實dom樹構(gòu)建完成才能添加到容器中。假設(shè)這個順序反過來,比如先執(zhí)行container.appendChild(node),則觸發(fā)瀏覽器回流。再執(zhí)行node.appendChild(text)又觸發(fā)瀏覽器回流。性能極差 React.createElement返回的最終的對象就是virtual dom樹,ReactDOM.render根據(jù)這個virtual dom創(chuàng)建真實的dom樹
第二章 createElement 函數(shù)
以下面的代碼為例
React.createElement接收的children有可能是原子值,比如字符串或者數(shù)字等,React.createElement('h1', {title: 'foo'}, 'Hello')。為了簡化我們的代碼,創(chuàng)建一個特殊的TEXT_ELEMENT 類型將其轉(zhuǎn)換成對象
參考React實戰(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語法
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)實現(xiàn)了一個簡單的createElement函數(shù),我們可以通過一段特殊的注釋來告訴babel在將jsx轉(zhuǎn)換成js時使用我們自己的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)建真實的dom元素,然后將各個元素append到其父元素中,最后整個dom樹append到root container中,渲染完成,這個過程一旦開始,中間是無法打斷的,直到整個應(yīng)用渲染完成。這也是React16版本以前的渲染過程
注意,只有當(dāng)整個dom樹append到root container中時,頁面才會顯示
第四章 Concurrent Mode
在第三章中可以看到,當(dāng)前版本的render函數(shù)是遞歸構(gòu)建dom樹,最后才append到root container,最終頁面才渲染出來。這里有個問題,如果dom節(jié)點(diǎn)數(shù)量龐大,遞歸層級過深,這個過程其實是很耗時的,導(dǎo)致render函數(shù)長時間占用主線程,瀏覽器無法響應(yīng)用戶輸入等事件,造成卡頓的現(xiàn)象。
因此我們需要將render過程拆分成小的任務(wù)單元,每執(zhí)行完一個單元,都允許瀏覽器打斷render過程并執(zhí)行高優(yōu)先級的任務(wù),等瀏覽器得空再繼續(xù)執(zhí)行render過程
如果對requestIdleCallback不熟悉的,可以自行了解一下。真實React代碼中并沒有使用這個api,因為有兼容性問題。因此React使用scheduler package模擬這個調(diào)度過程
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)前工作單元,并返回下一個工作單元。工作單元可以理解為就是一個fiber對象節(jié)點(diǎn)
workLoop循環(huán)里會循環(huán)調(diào)用performUnitOfWork,直到所有工作單元都已經(jīng)處理完畢,或者當(dāng)前幀瀏覽器已經(jīng)沒有空閑時間,則循環(huán)終止。等下次瀏覽器空閑時間再接著繼續(xù)執(zhí)行
因此我們需要一種數(shù)據(jù)結(jié)構(gòu),能夠支持任務(wù)打斷并且可以接著繼續(xù)執(zhí)行,很顯然,鏈表就非常適合
第五章 Fibers
Fibers就是一種數(shù)據(jù)結(jié)構(gòu),支持將渲染過程拆分成工作單元,本質(zhì)上就是一個雙向鏈表。這種數(shù)據(jù)結(jié)構(gòu)的好處就是方便找到下一個工作單元
Fiber包含三層含義:
- 作為架構(gòu)來說,之前
React 15的Reconciler采用遞歸的方式執(zhí)行,數(shù)據(jù)保存在遞歸調(diào)用棧中,所以被稱為stack Reconciler。React 16的Reconciler基于Fiber節(jié)點(diǎn)實現(xiàn),被稱為Fiber Reconciler - 作為靜態(tài)的數(shù)據(jù)結(jié)構(gòu)來說,每個Fiber節(jié)點(diǎn)對應(yīng)一個
React Element,保存了該組件的類型(函數(shù)組件/類組件/html標(biāo)簽)、對應(yīng)的DOM節(jié)點(diǎn)信息等 - 作為動態(tài)的工作單元來說,每個Fiber節(jié)點(diǎn)保存了本次更新中該組件改變的狀態(tài)、要執(zhí)行的工作等
Fiber的幾點(diǎn)冷知識:
- 一個Fiber節(jié)點(diǎn)對應(yīng)一個React Element節(jié)點(diǎn),同時也是一個工作單元
- 每個fiber節(jié)點(diǎn)都有指向第一個子元素,下一個兄弟元素,父元素的指針**
以下面代碼為例:
MiniReact.render(
<div>
<h1>
<p />
<a />
</h1>
<h2 />
</div>,
container
)
對應(yīng)的fiber tree如下:
import React from 'react';
// 根據(jù)fiber節(jié)點(diǎn)創(chuàng)建真實的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], // 此時的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)建對應(yīng)的fiber節(jié)點(diǎn)
// 返回下一個工作單元,即下一個fiber節(jié)點(diǎn),查找過程:
// 1.如果有子元素,則返回子元素的fiber節(jié)點(diǎn)
// 2.如果沒有子元素,則返回兄弟元素的fiber節(jié)點(diǎn)
// 3.如果既沒有子元素又沒有兄弟元素,則往上查找其父節(jié)點(diǎn)的兄弟元素的fiber節(jié)點(diǎn)
// 4.如果往上查找到root fiber節(jié)點(diǎn),說明render過程已經(jīng)結(jié)束
function performUnitOfWork(fiber) {
// 第一步 根據(jù)fiber節(jié)點(diǎn)創(chuàng)建真實的dom節(jié)點(diǎn),并保存在fiber.dom屬性中
if(!fiber.dom){
fiber.dom = createDom(fiber)
}
// 第二步 將當(dāng)前fiber節(jié)點(diǎn)的真實dom添加到父節(jié)點(diǎn)中,注意,這一步是會觸發(fā)瀏覽器回流重繪的?。?!
if(fiber.parent){
fiber.parent.dom.appendChild(fiber.dom)
}
// 第三步 給子元素創(chuàng)建對應(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
})
// 第四步,查找下一個工作單元
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)對象Fiber Tree是根據(jù)React Element Tree創(chuàng)建來的樹。每個Fiber節(jié)點(diǎn)保存著真實的DOM節(jié)點(diǎn),并且保存著對React Element Tree中對應(yīng)的Element節(jié)點(diǎn)的應(yīng)用。注意,Element節(jié)點(diǎn)并不會保存對Fiber節(jié)點(diǎn)的應(yīng)用
第六章 Render and Commit Phases
第五章的performUnitOfWork有些問題,在第二步中我們直接將新創(chuàng)建的真實dom節(jié)點(diǎn)掛載到了容器上,這樣會帶來兩個問題:
- 每次執(zhí)行
performUnitOfWork都會造成瀏覽器回流重繪,因為真實的dom已經(jīng)被添加到瀏覽器上了,性能極差 - 瀏覽器是可以打斷渲染過程的,因此可能會造成用戶看到不完整的UI界面
我們需要改造一下我們的代碼,在performUnitOfWork階段不把真實的dom節(jié)點(diǎn)掛載到容器上。保存fiber tree根節(jié)點(diǎn)的引用。等到fiber tree構(gòu)建完成,再一次性提交真實的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], // 此時的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)建真實的dom節(jié)點(diǎn),并保存在fiber.dom屬性中
if(!fiber.dom){
fiber.dom = createDom(fiber)
}
// 第二步 將當(dāng)前fiber節(jié)點(diǎn)的真實dom添加到父節(jié)點(diǎn)中,注意,這一步是會觸發(fā)瀏覽器回流重繪的!?。?
// if(fiber.parent){
// fiber.parent.dom.appendChild(fiber.dom)
// }
// 第三步 給子元素創(chuàng)建對應(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
})
// 第四步,查找下一個工作單元
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)到容器上這一單一場景,更新刪除還沒實現(xiàn)。
我們需要對比最新的React Element Tree和最近一次的Fiber Tree的差異
我們需要給每個fiber節(jié)點(diǎn)添加一個alternate屬性來保存舊的fiber節(jié)點(diǎn)
alternate保存的舊的fiber節(jié)點(diǎn)主要有以下幾個用途:
- 復(fù)用舊fiber節(jié)點(diǎn)上的真實dom節(jié)點(diǎn)
- 舊fiber節(jié)點(diǎn)上的props和新的element節(jié)點(diǎn)的props對比
- 舊fiber節(jié)點(diǎn)上保存有更新的隊列,在創(chuàng)建新的fiber節(jié)點(diǎn)時執(zhí)行這些隊列以獲取最新的頁面
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)過程:
- 本質(zhì)上依然是根據(jù)新的React Element Tree創(chuàng)建新的
Fiber Tree,不過為了節(jié)省內(nèi)存開銷,協(xié)調(diào)過程會判斷新的fiber節(jié)點(diǎn)能否復(fù)用舊的fiber節(jié)點(diǎn)上的真實dom元素,如果能復(fù)用,就不需要再從頭到尾全部重新創(chuàng)建一遍真實的dom元素。同時每個新fiber節(jié)點(diǎn)上還會保存著對舊fiber節(jié)點(diǎn)的引用,方便在commit階段做新舊屬性props的對比。 - 如果
old fiber.type和new element.type相同,則保留舊的dom節(jié)點(diǎn),只更新props屬性 - 如果
type不相同并且有new element,則創(chuàng)建一個新的真實dom節(jié)點(diǎn) - 如果
type不同并且有old fiber節(jié)點(diǎn),則刪除該節(jié)點(diǎn)對應(yīng)的真實dom節(jié)點(diǎn) - 刪除節(jié)點(diǎn)需要有個專門的數(shù)組收集需要刪除的舊的fiber節(jié)點(diǎn)。由于新的element tree創(chuàng)建出來的新的fiber tree不存在對應(yīng)的dom,因此需要收集舊的fiber節(jié)點(diǎn),并在commit階段刪除
注意,協(xié)調(diào)過程,還是以最新的React Element Tree為主去創(chuàng)建一個新的fiber tree,只不過是新的fiber節(jié)點(diǎn)復(fù)用舊的fiber節(jié)點(diǎn)的真實dom元素,畢竟頻繁創(chuàng)建真實dom是很消耗內(nèi)存的。新的fiber節(jié)點(diǎn)還是會保存著對舊的fiber節(jié)點(diǎn)的引用,方便在commit階段進(jìn)行新屬性和舊屬性的比較。這里會有個問題,如果新fiber節(jié)點(diǎn)保留舊fiber節(jié)點(diǎn)的引用,那么隨著更新次數(shù)越來越多,舊的fiber tree是不是也會越來越多,如何銷毀?
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 // 保存著對root fiber的引用
let currentRoot = null // 保存著當(dāng)前頁面對應(yīng)的fiber tree
let deletions = null
function render(element, container){
wipRoot = {
dom: container,
props: {
children: [element], // 此時的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)建真實的dom節(jié)點(diǎn),并保存在fiber.dom屬性中
if(!fiber.dom){
fiber.dom = createDom(fiber)
}
// 第二步 將當(dāng)前fiber節(jié)點(diǎn)的真實dom添加到父節(jié)點(diǎn)中,注意,這一步是會觸發(fā)瀏覽器回流重繪的?。。?
// if(fiber.parent){
// fiber.parent.dom.appendChild(fiber.dom)
// }
// 第三步 給子元素創(chuàng)建對應(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)
// 第四步,查找下一個工作單元
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)過babel編譯后:
function App(props) {
return MiniReact.createElement("h1", null, "Hi ", props.name);
}
const element = MiniReact.createElement(App, {
name: "foo"
});
函數(shù)組件有兩點(diǎn)不同的地方:
- 函數(shù)組件對應(yīng)的fiber節(jié)點(diǎn)沒有對應(yīng)的真實dom元素
- 需要執(zhí)行函數(shù)才能獲取對應(yīng)的children節(jié)點(diǎn),而不是直接從
props.children獲取
由于函數(shù)組件沒有對應(yīng)的fiber節(jié)點(diǎn),因此在commit階段在找父fiber節(jié)點(diǎn)對應(yīng)的dom時,需要判斷是否存在該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 // 保存著對root fiber的引用
let currentRoot = null // 保存著當(dāng)前頁面對應(yīng)的fiber tree
let deletions = null
function render(element, container){
wipRoot = {
dom: container,
props: {
children: [element], // 此時的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ù)組件對應(yīng)的fiber節(jié)點(diǎn)沒有真實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
// 第二步 為每一個新的react element節(jié)點(diǎn)創(chuàng)建對應(yīng)的fiber節(jié)點(diǎn),并判斷舊的fiber節(jié)點(diǎn)上的真實dom元素是否可以復(fù)用。
// 節(jié)省創(chuàng)建真實dom元素的開銷
reconcileChildren(fiber, children)
// 第三步,查找下一個工作單元
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 // 保存著對root fiber的引用
let currentRoot = null // 保存著當(dāng)前頁面對應(yīng)的fiber tree
let deletions = null
function render(element, container){
wipRoot = {
dom: container,
props: {
children: [element], // 此時的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ù)組件對應(yīng)的fiber節(jié)點(diǎn)沒有真實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
// 第二步 為每一個新的react element節(jié)點(diǎn)創(chuàng)建對應(yīng)的fiber節(jié)點(diǎn),并判斷舊的fiber節(jié)點(diǎn)上的真實dom元素是否可以復(fù)用。
// 節(jié)省創(chuàng)建真實dom元素的開銷
reconcileChildren(fiber, children)
// 第三步,查找下一個工作單元
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)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
react如何使用mobx6動態(tài)加載數(shù)據(jù)
MobX是一個強(qiáng)大而簡單的狀態(tài)管理工具,它可以幫助我們更好地組織和管理React應(yīng)用程序中的數(shù)據(jù)流,本文給大家介紹react如何使用mobx6動態(tài)加載數(shù)據(jù),感興趣的朋友跟隨小編一起看看吧2024-02-02
基于PixiJS實現(xiàn)react圖標(biāo)旋轉(zhuǎn)動效
PixiJS是一個開源的基于web的渲染系統(tǒng),為游戲、數(shù)據(jù)可視化和其他圖形密集型項目提供了極快的性能,這篇文章主要介紹了用PixiJS實現(xiàn)react圖標(biāo)旋轉(zhuǎn)動效,需要的朋友可以參考下2022-05-05

