熟悉React的朋友都知道,React支持jsx語法,咱們能夠直接將HTML代碼寫到JS中間,而後渲染到頁面上,咱們寫的HTML若是有更新的話,React還有虛擬DOM的對比,只更新變化的部分,而不從新渲染整個頁面,大大提升渲染效率。到了16.x,React更是使用了一個被稱爲Fiber
的架構,提高了用戶體驗,同時還引入了hooks
等特性。那隱藏在React背後的原理是怎樣的呢,Fiber
和hooks
又是怎麼實現的呢?本文會從jsx
入手,手寫一個簡易版的React,從而深刻理解React的原理。javascript
本文主要實現了這些功能:java
簡易版Fiber架構簡易版DIFF算法react
簡易版函數組件git
簡易版Hook:
useState
github娛樂版
Class
組件算法
本文代碼地址:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/React/fiber-and-hooks數組
本文程序跑起來效果以下:瀏覽器
之前咱們寫React要支持JSX還須要一個庫叫JSXTransformer.js
,後來JSX的轉換工做都集成到了babel裏面了,babel還提供了在線預覽的功能,能夠看到轉換後的效果,好比下面這段簡單的代碼:babel
const App = ( <div> <h1 id="title">Title</h1> <a href="xxx">Jump</a> <section> <p> Article </p> </section> </div> );
通過babel轉換後就變成了這樣:數據結構
上面的截圖能夠看出咱們寫的HTML被轉換成了React.createElement
,咱們將上面代碼稍微格式化來看下:
var App = React.createElement( 'div', null, React.createElement( 'h1', { id: 'title', }, 'Title', ), React.createElement( 'a', { href: 'xxx', }, 'Jump', ), React.createElement( 'section', null, React.createElement('p', null, 'Article'), ), );
從轉換後的代碼咱們能夠看出React.createElement
支持多個參數:
- type,也就是節點類型
- config, 這是節點上的屬性,好比
id
和href
- children, 從第三個參數開始就所有是children也就是子元素了,子元素能夠有多個,類型能夠是簡單的文本,也能夠仍是
React.createElement
,若是是React.createElement
,其實就是子節點了,子節點下面還能夠有子節點。這樣就用React.createElement
的嵌套關係實現了HTML節點的樹形結構。
讓咱們來完整看下這個簡單的React頁面代碼:
渲染在頁面上是這樣:
這裏面用到了React的地方其實就兩個,一個是JSX,也就是React.createElement
,另外一個就是ReactDOM.render
,因此咱們手寫的第一個目標就有了,就是createElement
和render
這兩個方法。
對於<h1 id="title">Title</h1>
這樣一個簡單的節點,原生DOM也會附加一大堆屬性和方法在上面,因此咱們在createElement
的時候最好能將它轉換爲一種比較簡單的數據結構,只包含咱們須要的元素,好比這樣:
{ type: 'h1', props: { id: 'title', children: 'Title' } }
有了這個數據結構後,咱們對於DOM的操做其實能夠轉化爲對這個數據結構的操做,新老DOM的對比其實也能夠轉化爲這個數據結構的對比,這樣咱們就不須要每次操做都去渲染頁面,而是等到須要渲染的時候纔將這個數據結構渲染到頁面上。這其實就是虛擬DOM!而咱們createElement
就是負責來構建這個虛擬DOM的方法,下面咱們來實現下:
function createElement(type, props, ...children) { // 核心邏輯不復雜,將參數都塞到一個對象上返回就行 // children也要放到props裏面去,這樣咱們在組件裏面就能經過this.props.children拿到子元素 return { type, props: { ...props, children } } }
上述代碼是React的createElement
簡化版,對源碼感興趣的朋友能夠看這裏:https://github.com/facebook/react/blob/60016c448bb7d19fc989acd05dda5aca2e124381/packages/react/src/ReactElement.js#L348
上述代碼咱們用createElement
將JSX代碼轉換成了虛擬DOM,那真正將它渲染到頁面的函數是render
,因此咱們還須要實現下這個方法,經過咱們通常的用法ReactDOM.render( <App />,document.getElementById('root'));
能夠知道他接收兩個參數:
- 根組件,實際上是一個JSX組件,也就是一個
createElement
返回的虛擬DOM- 父節點,也就是咱們要將這個虛擬DOM渲染的位置
有了這兩個參數,咱們來實現下render
方法:
function render(vDom, container) { let dom; // 檢查當前節點是文本仍是對象 if(typeof vDom !== 'object') { dom = document.createTextNode(vDom) } else { dom = document.createElement(vDom.type); } // 將vDom上除了children外的屬性都掛載到真正的DOM上去 if(vDom.props) { Object.keys(vDom.props) .filter(key => key != 'children') .forEach(item => { dom[item] = vDom.props[item]; }) } // 若是還有子元素,遞歸調用 if(vDom.props && vDom.props.children && vDom.props.children.length) { vDom.props.children.forEach(child => render(child, dom)); } container.appendChild(dom); }
上述代碼是簡化版的render
方法,對源碼感興趣的朋友能夠看這裏:https://github.com/facebook/react/blob/3e94bce765d355d74f6a60feb4addb6d196e3482/packages/react-dom/src/client/ReactDOMLegacy.js#L287
如今咱們能夠用本身寫的createElement
和render
來替換原生的方法了:
能夠獲得同樣的渲染結果:
上面咱們簡單的實現了虛擬DOM渲染到頁面上的代碼,這部分工做被React官方稱爲renderer,renderer是第三方能夠本身實現的一個模塊,還有個核心模塊叫作reconsiler,reconsiler的一大功能就是你們熟知的diff,他會計算出應該更新哪些頁面節點,而後將須要更新的節點虛擬DOM傳遞給renderer,renderer負責將這些節點渲染到頁面上。可是這個流程有個問題,雖然React的diff算法是通過優化的,可是他倒是同步的,renderer負責操做DOM的appendChild
等API也是同步的,也就是說若是有大量節點須要更新,JS線程的運行時間可能會比較長,在這段時間瀏覽器是不會響應其餘事件的,由於JS線程和GUI線程是互斥的,JS運行時頁面就不會響應,這個時間太長了,用戶就可能看到卡頓,特別是動畫的卡頓會很明顯。在React的官方演講中有個例子,能夠很明顯的看到這種同步計算形成的卡頓:
而Fiber就是用來解決這個問題的,Fiber能夠將長時間的同步任務拆分紅多個小任務,從而讓瀏覽器可以抽身去響應其餘事件,等他空了再回來繼續計算,這樣整個計算流程就顯得平滑不少。下面是使用Fiber後的效果:
上面咱們本身實現的render
方法直接遞歸遍歷了整個vDom樹,若是咱們在中途某一步停下來,下次再調用時其實並不知道上次在哪裏停下來的,不知道從哪裏開始,因此vDom的樹形結構並不知足中途暫停,下次繼續的需求,須要改造數據結構。另外一個須要解決的問題是,拆分下來的小任務何時執行?咱們的目的是讓用戶有更流暢的體驗,因此咱們最好不要阻塞高優先級的任務,好比用戶輸入,動畫之類,等他們執行完了咱們再計算。那我怎麼知道如今有沒有高優先級任務,瀏覽器是否是空閒呢?總結下來,Fiber要想達到目的,須要解決兩個問題:
- 新的任務調度,有高優先級任務的時候將瀏覽器讓出來,等瀏覽器空了再繼續執行
- 新的數據結構,能夠隨時中斷,下次進來能夠接着執行
requestIdleCallback
是一個實驗中的新API,這個API調用方式以下:
// 開啓調用 var handle = window.requestIdleCallback(callback[, options]) // 結束調用 Window.cancelIdleCallback(handle)
requestIdleCallback
接收一個回調,這個回調會在瀏覽器空閒時調用,每次調用會傳入一個IdleDeadline
,能夠拿到當前還空餘多久,options
能夠傳入參數最多等多久,等到了時間瀏覽器還不空就強制執行了。使用這個API能夠解決任務調度的問題,讓瀏覽器在空閒時才計算diff並渲染。更多關於requestIdleCallback的使用能夠查看MDN的文檔。可是這個API還在實驗中,兼容性很差,因此React官方本身實現了一套。本文會繼續使用requestIdleCallback
來進行任務調度,咱們進行任務調度的思想是將任務拆分紅多個小任務,requestIdleCallback
裏面不斷的把小任務拿出來執行,當全部任務都執行完或者超時了就結束本次執行,同時要註冊下次執行,代碼架子就是這樣:
function workLoop(deadline) { while(nextUnitOfWork && deadline.timeRemaining() > 1) { // 這個while循環會在任務執行完或者時間到了的時候結束 nextUnitOfWork = performUnitOfWork(nextUnitOfWork); } // 若是任務還沒完,可是時間到了,咱們須要繼續註冊requestIdleCallback requestIdleCallback(workLoop); } // performUnitOfWork用來執行任務,參數是咱們的當前fiber任務,返回值是下一個任務 function performUnitOfWork(fiber) { } requestIdleCallback(workLoop);
上面咱們的performUnitOfWork
並無實現,可是從上面的結構能夠看出來,他接收的參數是一個小任務,同時經過這個小任務還能夠找到他的下一個小任務,Fiber構建的就是這樣一個數據結構。Fiber以前的數據結構是一棵樹,父節點的children
指向了子節點,可是隻有這一個指針是不能實現中斷繼續的。好比我如今有一個父節點A,A有三個子節點B,C,D,當我遍歷到C的時候中斷了,從新開始的時候,其實我是不知道C下面該執行哪一個的,由於只知道C,並無指針指向他的父節點,也沒有指針指向他的兄弟。Fiber就是改造了這樣一個結構,加上了指向父節點和兄弟節點的指針:
上面的圖片仍是來自於官方的演講,能夠看到和以前父節點指向全部子節點不一樣,這裏有三個指針:
- child: 父節點指向第一個子元素的指針。
- sibling:從第一個子元素日後,指向下一個兄弟元素。
- return:全部子元素都有的指向父元素的指針。
有了這幾個指針後,咱們能夠在任意一個元素中斷遍歷並恢復,好比在上圖List
處中斷了,恢復的時候能夠經過child
找到他的子元素,也能夠經過return
找到他的父元素,若是他還有兄弟節點也能夠用sibling
找到。Fiber這個結構外形看着仍是棵樹,可是沒有了指向全部子元素的指針,父節點只指向第一個子節點,而後子節點有指向其餘子節點的指針,這實際上是個鏈表。
如今咱們能夠本身來實現一下Fiber了,咱們須要將以前的vDom結構轉換爲Fiber的數據結構,同時須要可以經過其中任意一個節點返回下一個節點,其實就是遍歷這個鏈表。遍歷的時候從根節點出發,先找子元素,若是子元素存在,直接返回,若是沒有子元素了就找兄弟元素,找完全部的兄弟元素後再返回父元素,而後再找這個父元素的兄弟元素。整個遍歷過程實際上是個深度優先遍歷,從上到下,而後最後一行開始從左到右遍歷。好比下圖從div1
開始遍歷的話,遍歷的順序就應該是div1 -> div2 -> h1 -> a -> div2 -> p -> div1
。能夠看到這個序列中,當咱們return
父節點時,這些父節點會被第二次遍歷,因此咱們寫代碼時,return
的父節點不會做爲下一個任務返回,只有sibling
和child
纔會做爲下一個任務返回。
// performUnitOfWork用來執行任務,參數是咱們的當前fiber任務,返回值是下一個任務 function performUnitOfWork(fiber) { // 根節點的dom就是container,若是沒有這個屬性,說明當前fiber不是根節點 if(!fiber.dom) { fiber.dom = createDom(fiber); // 建立一個DOM掛載上去 } // 若是有父節點,將當前節點掛載到父節點上 if(fiber.return) { fiber.return.dom.appendChild(fiber.dom); } // 將咱們前面的vDom結構轉換爲fiber結構 const elements = fiber.children; let prevSibling = null; if(elements && elements.length) { for(let i = 0; i < elements.length; i++) { const element = elements[i]; const newFiber = { type: element.type, props: element.props, return: fiber, dom: null } // 父級的child指向第一個子元素 if(i === 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.return; } }
React源碼中的performUnitOfWork
看這裏,固然比咱們這個複雜不少。
上面咱們的performUnitOfWork
一邊構建Fiber結構一邊操做DOMappendChild
,這樣若是某次更新好幾個節點,操做了第一個節點以後就中斷了,那咱們可能只看到第一個節點渲染到了頁面,後續幾個節點等瀏覽器空了才陸續渲染。爲了不這種狀況,咱們應該將DOM操做都蒐集起來,最後統一執行,這就是commit
。爲了可以記錄位置,咱們還須要一個全局變量workInProgressRoot
來記錄根節點,而後在workLoop
檢測若是任務執行完了,就commit
:
function workLoop(deadline) { while(nextUnitOfWork && deadline.timeRemaining() > 1) { // 這個while循環會在任務執行完或者時間到了的時候結束 nextUnitOfWork = performUnitOfWork(nextUnitOfWork); } // 任務作完後統一渲染 if(!nextUnitOfWork && workInProgressRoot) { commitRoot(); } // 若是任務還沒完,可是時間到了,咱們須要繼續註冊requestIdleCallback requestIdleCallback(workLoop); }
由於咱們是在Fiber樹徹底構建後再執行的commit
,並且有一個變量workInProgressRoot
指向了Fiber的根節點,因此咱們能夠直接把workInProgressRoot
拿過來遞歸渲染就好了:
// 統一操做DOM function commitRoot() { commitRootImpl(workInProgressRoot.child); // 開啓遞歸 workInProgressRoot = null; // 操做完後將workInProgressRoot重置 } function commitRootImpl(fiber) { if(!fiber) { return; } const parentDom = fiber.return.dom; parentDom.appendChild(fiber.dom); // 遞歸操做子元素和兄弟元素 commitRootImpl(fiber.child); commitRootImpl(fiber.sibling); }
reconcile其實就是虛擬DOM樹的diff操做,須要刪除不須要的節點,更新修改過的節點,添加新的節點。爲了在中斷後能回到工做位置,咱們還須要一個變量currentRoot
,而後在fiber
節點裏面添加一個屬性alternate
,這個屬性指向上一次運行的根節點,也就是currentRoot
。currentRoot
會在第一次render
後的commit
階段賦值,也就是每次計算完後都會把當次狀態記錄在alternate
上,後面更新了就能夠把alternate
拿出來跟新的狀態作diff。而後performUnitOfWork
裏面須要添加調和子元素的代碼,能夠新增一個函數reconcileChildren
。這個函數裏面不能簡單的建立新節點了,而是要將老節點跟新節點拿來對比,對比邏輯以下:
注意刪除老節點的操做是直接將oldFiber
加上一個刪除標記就行,同時用一個全局變量deletions
記錄全部須要刪除的節點:
// 對比oldFiber和當前element const sameType = oldFiber && element && oldFiber.type === element.type; //檢測類型是否是同樣 // 先比較元素類型 if(sameType) { // 若是類型同樣,複用節點,更新props newFiber = { type: oldFiber.type, props: element.props, dom: oldFiber.dom, return: workInProgressFiber, alternate: oldFiber, // 記錄下上次狀態 effectTag: 'UPDATE' // 添加一個操做標記 } } else if(!sameType && element) { // 若是類型不同,有新的節點,建立新節點替換老節點 newFiber = { type: element.type, props: element.props, dom: null, // 構建fiber時沒有dom,下次perform這個節點是才建立dom return: workInProgressFiber, alternate: null, // 新增的沒有老狀態 effectTag: 'REPLACEMENT' // 添加一個操做標記 } } else if(!sameType && oldFiber) { // 若是類型不同,沒有新節點,有老節點,刪除老節點 oldFiber.effectTag = 'DELETION'; // 添加刪除標記 deletions.push(oldFiber); // 一個數組收集全部須要刪除的節點 }
而後就是在commit
階段處理真正的DOM操做,具體的操做是根據咱們的effectTag
來判斷的:
function commitRootImpl(fiber) { if(!fiber) { return; } const parentDom = fiber.return.dom; if(fiber.effectTag === 'REPLACEMENT' && fiber.dom) { parentDom.appendChild(fiber.dom); } else if(fiber.effectTag === 'DELETION') { parentDom.removeChild(fiber.dom); } else if(fiber.effectTag === 'UPDATE' && fiber.dom) { // 更新DOM屬性 updateDom(fiber.dom, fiber.alternate.props, fiber.props); } // 遞歸操做子元素和兄弟元素 commitRootImpl(fiber.child); commitRootImpl(fiber.sibling); }
替換和刪除的DOM操做都比較簡單,更新屬性的會稍微麻煩點,須要再寫一個輔助函數updateDom
來實現:
// 更新DOM的操做 function updateDom(dom, prevProps, nextProps) { // 1. 過濾children屬性 // 2. 老的存在,新的沒了,取消 // 3. 新的存在,老的沒有,新增 Object.keys(prevProps) .filter(name => name !== 'children') .filter(name => !(name in nextProps)) .forEach(name => { if(name.indexOf('on') === 0) { dom.removeEventListener(name.substr(2).toLowerCase(), prevProps[name], false); } else { dom[name] = ''; } }); Object.keys(nextProps) .filter(name => name !== 'children') .forEach(name => { if(name.indexOf('on') === 0) { dom.addEventListener(name.substr(2).toLowerCase(), nextProps[name], false); } else { dom[name] = nextProps[name]; } }); }
updateDom
的代碼寫的比較簡單,事件只處理了簡單的on
開頭的,兼容性也有問題,prevProps
和nextProps
可能會遍歷到相同的屬性,有重複賦值,可是整體原理仍是沒錯的。要想把這個處理寫全,代碼量仍是很多的。
函數組件是React裏面很常見的一種組件,咱們前面的React架構其實已經寫好了,咱們這裏來支持下函數組件。咱們以前的fiber
節點上的type
都是DOM節點的類型,好比h1
什麼的,可是函數組件的節點type
其實就是一個函數了,咱們須要對這種節點進行單獨處理。
首先須要在更新的時候檢測當前節點是否是函數組件,若是是,children
的處理邏輯會稍微不同:
// performUnitOfWork裏面 // 檢測函數組件 function performUnitOfWork(fiber) { const isFunctionComponent = fiber.type instanceof Function; if(isFunctionComponent) { updateFunctionComponent(fiber); } else { updateHostComponent(fiber); } // ...下面省略n行代碼... } function updateFunctionComponent(fiber) { // 函數組件的type就是個函數,直接拿來執行能夠得到DOM元素 const children = [fiber.type(fiber.props)]; reconcileChildren(fiber, children); } // updateHostComponent就是以前的操做,只是單獨抽取了一個方法 function updateHostComponent(fiber) { if(!fiber.dom) { fiber.dom = createDom(fiber); // 建立一個DOM掛載上去 } // 將咱們前面的vDom結構轉換爲fiber結構 const elements = fiber.props.children; // 調和子元素 reconcileChildren(fiber, elements); }
而後在咱們提交DOM操做的時候由於函數組件沒有DOM元素,因此須要注意兩點:
咱們來修改下commitRootImpl
:
function commitRootImpl() { // const parentDom = fiber.return.dom; // 向上查找真正的DOM let parentFiber = fiber.return; while(!parentFiber.dom) { parentFiber = parentFiber.return; } const parentDom = parentFiber.dom; // ...這裏省略n行代碼... if{fiber.effectTag === 'DELETION'} { commitDeletion(fiber, parentDom); } } function commitDeletion(fiber, domParent) { if(fiber.dom) { // dom存在,是普通節點 domParent.removeChild(fiber.dom); } else { // dom不存在,是函數組件,向下遞歸查找真實DOM commitDeletion(fiber.child, domParent); } }
如今咱們能夠傳入函數組件了:
import React from './myReact'; const ReactDOM = React; function App(props) { return ( <div> <h1 id="title">{props.title}</h1> <a href="xxx">Jump</a> <section> <p> Article </p> </section> </div> ); } ReactDOM.render( <App title="Fiber Demo"/>, document.getElementById('root') );
useState
是React Hooks裏面的一個API,至關於以前Class Component
裏面的state
,用來管理組件內部狀態,如今咱們已經有一個簡化版的React
了,咱們也能夠嘗試下來實現這個API。
咱們仍是從用法入手來實現最簡單的功能,咱們通常使用useState
是這樣的:
function App(props) { const [count, setCount] = React.useState(1); const onClickHandler = () => { setCount(count + 1); } return ( <div> <h1>Count: {count}</h1> <button onClick={onClickHandler}>Count+1</button> </div> ); } ReactDOM.render( <App title="Fiber Demo"/>, document.getElementById('root') );
上述代碼能夠看出,咱們的useState
接收一個初始值,返回一個數組,裏面有這個state
的當前值和改變state
的方法,須要注意的是App
做爲一個函數組件,每次render
的時候都會運行,也就是說裏面的局部變量每次render
的時候都會重置,那咱們的state
就不能做爲一個局部變量,而是應該做爲一個所有變量存儲:
let state = null; function useState(init) { state = state === null ? init : state; // 修改state的方法 const setState = value => { state = value; // 只要修改了state,咱們就須要從新處理節點 workInProgressRoot = { dom: currentRoot.dom, props: currentRoot.props, alternate: currentRoot } // 修改nextUnitOfWork指向workInProgressRoot,這樣下次就會處理這個節點了 nextUnitOfWork = workInProgressRoot; deletions = []; } return [state, setState] }
這樣其實咱們就可使用了:
上面的代碼只有一個state
變量,若是咱們有多個useState
怎麼辦呢?爲了能支持多個useState
,咱們的state
就不能是一個簡單的值了,咱們能夠考慮把他改爲一個數組,多個useState
按照調用順序放進這個數組裏面,訪問的時候經過下標來訪問:
let state = []; let hookIndex = 0; function useState(init) { const currentIndex = hookIndex; state[currentIndex] = state[currentIndex] === undefined ? init : state[currentIndex]; // 修改state的方法 const setState = value => { state[currentIndex] = value; // 只要修改了state,咱們就須要從新處理這個節點 workInProgressRoot = { dom: currentRoot.dom, props: currentRoot.props, alternate: currentRoot } // 修改nextUnitOfWork指向workInProgressRoot,這樣下次就會處理這個節點了 nextUnitOfWork = workInProgressRoot; deletions = []; } hookIndex++; return [state[currentIndex], setState] }
來看看多個useState
的效果:
上面的代碼雖然咱們支持了多個useState
,可是仍然只有一套全局變量,若是有多個函數組件,每一個組件都來操做這個全局變量,那相互之間不就是污染了數據了嗎?因此咱們數據還不能都存在全局變量上面,而是應該存在每一個fiber
節點上,處理這個節點的時候再將狀態放到全局變量用來通信:
// 申明兩個全局變量,用來處理useState // wipFiber是當前的函數組件fiber節點 // hookIndex是當前函數組件內部useState狀態計數 let wipFiber = null; let hookIndex = null;
由於useState
只在函數組件裏面能夠用,因此咱們以前的updateFunctionComponent
裏面須要初始化處理useState
變量:
function updateFunctionComponent(fiber) { // 支持useState,初始化變量 wipFiber = fiber; hookIndex = 0; wipFiber.hooks = []; // hooks用來存儲具體的state序列 // ......下面代碼省略...... }
由於hooks
隊列放到fiber
節點上去了,因此咱們在useState
取以前的值時須要從fiber.alternate
上取,完整代碼以下:
function useState(init) { // 取出上次的Hook const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex]; // hook數據結構 const hook = { state: oldHook ? oldHook.state : init // state是每一個具體的值 } // 將全部useState調用按照順序存到fiber節點上 wipFiber.hooks.push(hook); hookIndex++; // 修改state的方法 const setState = value => { hook.state = value; // 只要修改了state,咱們就須要從新處理這個節點 workInProgressRoot = { dom: currentRoot.dom, props: currentRoot.props, alternate: currentRoot } // 修改nextUnitOfWork指向workInProgressRoot,這樣下次requestIdleCallback就會處理這個節點了 nextUnitOfWork = workInProgressRoot; deletions = []; } return [hook.state, setState] }
上面代碼能夠看出咱們在將useState
和存儲的state
進行匹配的時候是用的useState
的調用順序匹配state
的下標,若是這個下標匹配不上了,state
就錯了,因此React
裏面不能出現這樣的代碼:
if (something) { const [state, setState] = useState(1); }
上述代碼不能保證每次something
都知足,可能致使useState
此次render
執行了,下次又沒執行,這樣新老節點的下標就匹配不上了,對於這種代碼,React
會直接報錯:
這個功能純粹是娛樂性功能,經過前面實現的Hooks來模擬實現Class組件,這個並非React
官方的實現方式哈~咱們能夠寫一個方法將Class組件轉化爲前面的函數組件:
function transfer(Component) { return function(props) { const component = new Component(props); let [state, setState] = useState(component.state); component.props = props; component.state = state; component.setState = setState; return component.render(); } }
而後就能夠寫Class了,這個Class長得很像咱們在React裏面寫的Class,有state
,setState
和render
:
import React from './myReact'; class Count4 { constructor(props) { this.props = props; this.state = { count: 1 } } onClickHandler = () => { this.setState({ count: this.state.count + 1 }) } render() { return ( <div> <h3>Class component Count: {this.state.count}</h3> <button onClick={this.onClickHandler}>Count+1</button> </div> ); } } // export的時候用transfer包裝下 export default React.transfer(Count4);
而後使用的時候直接:
<div> <Count4></Count4> </div>
固然你也能夠在React
裏面建一個空的class Component
,讓Count4
繼承他,這樣就更像了。
好了,到這裏咱們代碼就寫完了,完整代碼能夠看我GitHub。
React.createElement
。React.createElement
返回的其實就是虛擬DOM結構。ReactDOM.render
方法是將虛擬DOM渲染到頁面的。父 -> 第一個子
,子 -> 兄
,子 -> 父
這幾個指針,有了這幾個指針,能夠從任意一個Fiber節點找到其餘節點。父 -> 子 -> 兄 -> 父
,也就是從上往下,從左往右。commit
)必須是同步的。由於異步的commit
可能讓用戶看到節點一個一個接連出現,體驗很差。type
是個函數,直接將type
拿來運行就能夠獲得虛擬DOM。useState
是在Fiber節點上添加了一個數組,數組裏面的每一個值對應了一個useState
,useState
調用順序必須和這個數組下標匹配,否則會報錯。妙味課堂大聖老師:手寫react的fiber和hooks架構
這多是最通俗的 React Fiber(時間分片) 打開方式
文章的最後,感謝你花費寶貴的時間閱讀本文,若是本文給了你一點點幫助或者啓發,請不要吝嗇你的贊和GitHub小星星,你的支持是做者持續創做的動力。
做者博文GitHub項目地址: https://github.com/dennis-jiang/Front-End-Knowledges