本文基於 https://pomb.us/build-your-own-react/ 實現簡單版 React。javascript
本文學習思路來自 卡頌-b站-React源碼,你在第幾層。html
模擬的版本爲 React 16.8。java
將實現如下功能:node
下面上正餐,請繼續閱讀。react
先來看看一個簡單的 React Demo,代碼以下:git
const element = <div title="foo">hello</div> const container = document.getElementById('container') ReactDOM.render(element, container);
本例完整源碼見:reactDemogithub
在瀏覽器中打開 reactDemo.html,展現以下:web
咱們須要實現本身的 React,那麼就須要知道上面的代碼到底作了什麼。面試
const element = <div>123</div>
其實是 JSX 語法。算法
React 官網 對 JSX 的解釋以下:
JSX 是一個 JavaScript 語法擴展。它相似於模板語言,但它具備 JavaScript 的所有能力。JSX 最終會被 babel 編譯爲 React.createElement() 函數調用。
經過 babel 在線編譯 const element = <div>123</div>
。
可知 const element = <div>123</div>
通過編譯後的實際代碼以下:
const element = React.createElement("div", { title: "foo" }, "hello");
再來看看上文的 React.createElement 實際生成了一個怎麼樣的對象。
在 demo 中打印試試:
const element = <div title="foo">hello</div> console.log(element) const container = document.getElementById('container') ReactDOM.render(element, container);
能夠看到輸出的 element 以下:
簡化一下 element:
const element = { type: 'div', props: { title: 'foo', children: 'hello' } }
簡單總結一下,React.createElement
其實是生成了一個 element 對象,該對象擁有如下屬性:
ReactDOM.render()
將 element 添加到 id 爲 container 的 DOM 節點中,下面咱們將簡單手寫一個方法代替 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)
本例完整源碼見:reactDemo2
運行源碼,結果以下,和引入 React 的結果一致:
上文經過模擬 React,簡單代替了 React.createElement、ReactDOM.render 方法,接下來將真正開始實現 React 的各個功能。
上面有了解到 createElement 的做用是建立一個 element 對象,結構以下:
// 虛擬 DOM 結構 const element = { type: 'div', // 標籤名 props: { // 節點屬性,包含 children title: 'foo', // title 屬性 children: 'hello' // 子節點,注:實際上這裏應該是數組結構,幫助咱們存儲更多子節點 } }
根據 element 的結構,設計了 createElement 函數,代碼以下:
/** * 建立虛擬 DOM 結構 * @param {type} 標籤名 * @param {props} 屬性對象 * @param {children} 子節點 * @return {element} 虛擬 DOM */ function createElement (type, props, ...children) { return { type, props: { ...props, children: children.map(child => typeof child === 'object' ? child : createTextElement(child) ) } } }
這裏有考慮到,當 children 是非對象時,應該建立一個 textElement 元素, 代碼以下:
/** * 建立文本節點 * @param {text} 文本值 * @return {element} 虛擬 DOM */ function createTextElement (text) { return { type: "TEXT_ELEMENT", props: { nodeValue: text, children: [] } } }
接下來試一下,代碼以下:
const myReact = { createElement } const element = myReact.createElement( "div", { id: "foo" }, myReact.createElement("a", null, "bar"), myReact.createElement("b") ) console.log(element)
本例完整源碼見:reactDemo3
獲得的 element 對象以下:
const element = { "type": "div", "props": { "id": "foo", "children": [ { "type": "a", "props": { "children": [ { "type": "TEXT_ELEMENT", "props": { "nodeValue": "bar", "children": [ ] } } ] } }, { "type": "b", "props": { "children": [ ] } } ] } }
JSX
實際上咱們在使用 react 開發的過程當中,並不會這樣建立組件:
const element = myReact.createElement( "div", { id: "foo" }, myReact.createElement("a", null, "bar"), myReact.createElement("b") )
而是經過 JSX 語法,代碼以下:
const element = ( <div id='foo'> <a>bar</a> <b></b> </div> )
在 myReact 中,能夠經過添加註釋的形式,告訴 babel 轉譯咱們指定的函數,來使用 JSX 語法,代碼以下:
/** @jsx myReact.createElement */ const element = ( <div id='foo'> <a>bar</a> <b></b> </div> )
本例完整源碼見:reactDemo4
render 函數幫助咱們將 element 添加至真實節點中。
將分爲如下步驟實現:
/** * 將虛擬 DOM 添加至真實 DOM * @param {element} 虛擬 DOM * @param {container} 真實 DOM */ function render (element, container) { const dom = document.createElement(element.type) container.appendChild(dom) }
element.props.children.forEach(child => render(child, dom) )
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] })
以上咱們實現了將 JSX 渲染到真實 DOM 的功能,接下來試一下,代碼以下:
const myReact = { createElement, render } /** @jsx myReact.createElement */ const element = ( <div id='foo'> <a>bar</a> <b></b> </div> ) myReact.render(element, document.getElementById('container'))
本例完整源碼見:reactDemo5
結果如圖,成功輸出:
再來看看上面寫的 render 方法中關於子節點的處理,代碼以下:
/** * 將虛擬 DOM 添加至真實 DOM * @param {element} 虛擬 DOM * @param {container} 真實 DOM */ function render (element, container) { // 省略 // 遍歷全部子節點,並進行渲染 element.props.children.forEach(child => render(child, dom) ) // 省略 }
這個遞歸調用是有問題的,一旦開始渲染,就會將全部節點及其子節點所有渲染完成這個進程纔會結束。
當 dom tree 很大的狀況下,在渲染過程當中,頁面上是卡住的狀態,沒法進行用戶輸入等交互操做。
可分爲如下步驟解決上述問題:
使用 requestIdleCallback 來解決容許中斷渲染工做的問題。
window.requestIdleCallback 將在瀏覽器的空閒時段內調用的函數排隊。這使開發者可以在主事件循環上執行後臺和低優先級工做,而不會影響延遲關鍵事件,如動畫和輸入響應。
window.requestIdleCallback 詳細介紹可查看文檔:文檔
代碼以下:
// 下一個工做單元 let nextUnitOfWork = null /** * workLoop 工做循環函數 * @param {deadline} 截止時間 */ function workLoop(deadline) { // 是否應該中止工做循環函數 let shouldYield = false // 若是存在下一個工做單元,且沒有優先級更高的其餘工做時,循環執行 while (nextUnitOfWork && !shouldYield) { nextUnitOfWork = performUnitOfWork( nextUnitOfWork ) // 若是截止時間快到了,中止工做循環函數 shouldYield = deadline.timeRemaining() < 1 } // 通知瀏覽器,空閒時間應該執行 workLoop requestIdleCallback(workLoop) } // 通知瀏覽器,空閒時間應該執行 workLoop requestIdleCallback(workLoop) // 執行單元事件,並返回下一個單元事件 function performUnitOfWork(nextUnitOfWork) { // TODO }
performUnitOfWork 是用來執行單元事件,並返回下一個單元事件的,具體實現將在下文介紹。
上文介紹了經過 requestIdleCallback 讓瀏覽器在空閒時間渲染工做單元,避免渲染太久致使頁面卡頓的問題。
注:實際上 requestIdleCallback 功能並不穩定,不建議用於生產環境,本例僅用於模擬 React 的思路,React 自己並非經過 requestIdleCallback 來實現讓瀏覽器在空閒時間渲染工做單元的。
另外一方面,爲了讓渲染工做能夠分離成一個個小單元,React 設計了 fiber。
每個 element 都是一個 fiber 結構,每個 fiber 都是一個渲染工做單元。
因此 fiber 既是一種數據結構,也是一個工做單元。
下文將經過簡單的示例對 fiber 進行介紹。
假設須要渲染這樣一個 element 樹:
myReact.render( <div> <h1> <p /> <a /> </h1> <h2 /> </div>, container )
生成的 fiber tree 如圖:
橙色表明子節點,黃色表明父節點,藍色表明兄弟節點。
每一個 fiber 都有一個連接指向它的第一個子節點、下一個兄弟節點和它的父節點。這種數據結構可讓咱們更方便的查找下一個工做單元。
上圖的箭頭也代表了 fiber 的渲染過程,渲染過程詳細描述以下:
下面將渲染過程用代碼實現。
/** * createDom 建立 DOM 節點 * @param {fiber} fiber 節點 * @return {dom} dom 節點 */ function createDom (fiber) { // 若是是文本類型,建立空的文本節點,若是不是文本類型,按 type 類型建立節點 const dom = fiber.type === 'TEXT_ELEMENT' ? document.createTextNode("") : document.createElement(fiber.type) // isProperty 表示不是 children 的屬性 const isProperty = key => key !== "children" // 遍歷 props,爲 dom 添加屬性 Object.keys(fiber.props) .filter(isProperty) .forEach(name => { dom[name] = fiber.props[name] }) // 返回 dom return dom }
fiber 根節點僅包含 children 屬性,值爲參數 fiber。
// 下一個工做單元 let nextUnitOfWork = null /** * 將 fiber 添加至真實 DOM * @param {element} fiber * @param {container} 真實 DOM */ function render (element, container) { nextUnitOfWork = { dom: container, props: { children: [element] } } }
/** * workLoop 工做循環函數 * @param {deadline} 截止時間 */ function workLoop(deadline) { // 是否應該中止工做循環函數 let shouldYield = false // 若是存在下一個工做單元,且沒有優先級更高的其餘工做時,循環執行 while (nextUnitOfWork && !shouldYield) { nextUnitOfWork = performUnitOfWork( nextUnitOfWork ) // 若是截止時間快到了,中止工做循環函數 shouldYield = deadline.timeRemaining() < 1 } // 通知瀏覽器,空閒時間應該執行 workLoop requestIdleCallback(workLoop) } // 通知瀏覽器,空閒時間應該執行 workLoop requestIdleCallback(workLoop)
/** * performUnitOfWork 處理工做單元 * @param {fiber} fiber * @return {nextUnitOfWork} 下一個工做單元 */ function performUnitOfWork(fiber) { // TODO 添加 dom 節點 // TODO 新建 filber // TODO 返回下一個工做單元(fiber) }
4.1 添加 dom 節點
function performUnitOfWork(fiber) { // 若是 fiber 沒有 dom 節點,爲它建立一個 dom 節點 if (!fiber.dom) { fiber.dom = createDom(fiber) } // 若是 fiber 有父節點,將 fiber.dom 添加至父節點 if (fiber.parent) { fiber.parent.dom.appendChild(fiber.dom) } }
4.2 新建 filber
function performUnitOfWork(fiber) { // ~~省略~~ // 子節點 const elements = fiber.props.children // 索引 let index = 0 // 上一個兄弟節點 let prevSibling = null // 遍歷子節點 while (index < elements.length) { const element = elements[index] // 建立 fiber const newFiber = { type: element.type, props: element.props, parent: fiber, dom: null, } // 將第一個子節點設置爲 fiber 的子節點 if (index === 0) { fiber.child = newFiber } else if (element) { // 第一個以外的子節點設置爲該節點的兄弟節點 prevSibling.sibling = newFiber } prevSibling = newFiber index++ } }
4.3 返回下一個工做單元(fiber)
function performUnitOfWork(fiber) { // ~~省略~~ // 若是有子節點,返回子節點 if (fiber.child) { return fiber.child } let nextFiber = fiber while (nextFiber) { // 若是有兄弟節點,返回兄弟節點 if (nextFiber.sibling) { return nextFiber.sibling } // 不然繼續走 while 循環,直到找到 root。 nextFiber = nextFiber.parent } }
以上咱們實現了將 fiber 渲染到頁面的功能,且渲染過程是可中斷的。
如今試一下,代碼以下:
const element = ( <div> <h1> <p /> <a /> </h1> <h2 /> </div> ) myReact.render(element, document.getElementById('container'))
本例完整源碼見:reactDemo7
如預期輸出 dom,如圖:
因爲渲染過程被咱們作了可中斷的,那麼中斷的時候,咱們確定不但願瀏覽器給用戶展現的是渲染了一半的 UI。
對渲染提交階段優化的處理以下:
function performUnitOfWork(fiber) { // 把這段刪了 if (fiber.parent) { fiber.parent.dom.appendChild(fiber.dom) } }
// 根節點 let wipRoot = null function render (element, container) { wipRoot = { dom: container, props: { children: [element] } } // 下一個工做單元是根節點 nextUnitOfWork = wipRoot }
function workLoop (deadline) { // 省略 if (!nextUnitOfWork && wipRoot) { commitRoot() } // 省略 }
// 所有工做單元完成後,將 fiber tree 渲染爲真實 DOM; function commitRoot () { commitWork(wipRoot.child) // 須要設置爲 null,不然 workLoop 在瀏覽器空閒時不斷的執行。 wipRoot = null } /** * performUnitOfWork 處理工做單元 * @param {fiber} fiber */ function commitWork (fiber) { if (!fiber) return const domParent = fiber.parent.dom domParent.appendChild(fiber.dom) // 渲染子節點 commitWork(fiber.child) // 渲染兄弟節點 commitWork(fiber.sibling) }
本例完整源碼見:reactDemo8
源碼運行結果如圖:
當 element 有更新時,須要將更新前的 fiber tree 和更新後的 fiber tree 進行比較,獲得比較結果後,僅對有變化的 fiber 對應的 dom 節點進行更新。
經過協調,減小對真實 DOM 的操做次數。
新增 currentRoot 變量,保存根節點更新前的 fiber tree,爲 fiber 新增 alternate 屬性,保存 fiber 更新前的 fiber tree;
let currentRoot = null function render (element, container) { wipRoot = { // 省略 alternate: currentRoot } } function commitRoot () { commitWork(wipRoot.child) currentRoot = wipRoot wipRoot = null }
將 performUnitOfWork 中關於新建 fiber 的邏輯,抽離到 reconcileChildren 函數;
/** * 協調子節點 * @param {fiber} fiber * @param {elements} fiber 的 子節點 */ function reconcileChildren (fiber, elements) { // 用於統計子節點的索引值 let index = 0 // 上一個兄弟節點 let prevSibling = null // 遍歷子節點 while (index < elements.length) { const element = elements[index] // 新建 fiber const newFiber = { type: element.type, props: element.props, parent: fiber, dom: null, } // fiber的第一個子節點是它的子節點 if (index === 0) { fiber.child = newFiber } else if (element) { // fiber 的其餘子節點,是它第一個子節點的兄弟節點 prevSibling.sibling = newFiber } // 把新建的 newFiber 賦值給 prevSibling,這樣就方便爲 newFiber 添加兄弟節點了 prevSibling = newFiber // 索引值 + 1 index++ } }
在 reconcileChildren 中對比新舊 fiber;
保留 dom,僅更新 props,設置 effectTag 爲 UPDATE;
function reconcileChildren (wipFiber, elements) { // ~~省略~~ // oldFiber 能夠在 wipFiber.alternate 中找到 let oldFiber = wipFiber.alternate && wipFiber.alternate.child while (index < elements.length || oldFiber != null) { const element = elements[index] let newFiber = null // fiber 類型是否相同 const sameType = oldFiber && element && element.type == oldFiber.type // 若是類型相同,僅更新 props if (sameType) { newFiber = { type: oldFiber.type, props: element.props, dom: oldFiber.dom, parent: wipFiber, alternate: oldFiber, effectTag: "UPDATE", } } // ~~省略~~ } // ~~省略~~ }
建立一個新的 dom 節點,設置 effectTag 爲 PLACEMENT;
function reconcileChildren (wipFiber, elements) { // ~~省略~~ if (element && !sameType) { newFiber = { type: element.type, props: element.props, dom: null, parent: wipFiber, alternate: null, effectTag: "PLACEMENT", } } // ~~省略~~ }
刪除舊 fiber,設置 effectTag 爲 DELETION;
function reconcileChildren (wipFiber, elements) { // ~~省略~~ if (oldFiber && !sameType) { oldFiber.effectTag = "DELETION" deletions.push(oldFiber) } // ~~省略~~ }
新建 deletions 數組存儲需刪除的 fiber 節點,渲染 DOM 時,遍歷 deletions 刪除舊 fiber;
let deletions = null function render (element, container) { // 省略 // render 時,初始化 deletions 數組 deletions = [] } // 渲染 DOM 時,遍歷 deletions 刪除舊 fiber function commitRoot () { deletions.forEach(commitWork) }
在 commitWork 中對 fiber 的 effectTag 進行判斷,並分別處理。
當 fiber 的 effectTag 爲 PLACEMENT 時,表示是新增 fiber,將該節點新增至父節點中。
if ( fiber.effectTag === "PLACEMENT" && fiber.dom != null ) { domParent.appendChild(fiber.dom) }
當 fiber 的 effectTag 爲 DELETION 時,表示是刪除 fiber,將父節點的該節點刪除。
else if (fiber.effectTag === "DELETION") { domParent.removeChild(fiber.dom) }
當 fiber 的 effectTag 爲 UPDATE 時,表示是更新 fiber,更新 props 屬性。
else if (fiber.effectTag === 'UPDATE' && fiber.dom != null) { updateDom(fiber.dom, fiber.alternate.props, fiber.props) }
updateDom 函數根據不一樣的更新類型,對 props 屬性進行更新。
const isProperty = key => key !== "children" // 是不是新屬性 const isNew = (prev, next) => key => prev[key] !== next[key] // 是不是舊屬性 const isGone = (prev, next) => key => !(key in next) function updateDom(dom, prevProps, nextProps) { // 刪除舊屬性 Object.keys(prevProps) .filter(isProperty) .filter(isGone(prevProps, nextProps)) .forEach(name => { dom[name] = "" }) // 更新新屬性 Object.keys(nextProps) .filter(isProperty) .filter(isNew(prevProps, nextProps)) .forEach(name => { dom[name] = nextProps[name] }) }
另外,爲 updateDom 添加事件屬性的更新、刪除,便於追蹤 fiber 事件的更新。
function updateDom(dom, prevProps, nextProps) { // ~~省略~~ const isEvent = key => key.startsWith("on") //刪除舊的或者有變化的事件 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] ) }) // 註冊新事件 Object.keys(nextProps) .filter(isEvent) .filter(isNew(prevProps, nextProps)) .forEach(name => { const eventType = name .toLowerCase() .substring(2) dom.addEventListener( eventType, nextProps[name] ) }) // ~~省略~~ }
替換 creactDOM 中設置 props 的邏輯。
function createDom (fiber) { const dom = fiber.type === 'TEXT_ELEMENT' ? document.createTextNode("") : document.createElement(fiber.type) // 看這裏鴨 updateDom(dom, {}, fiber.props) return dom }
新建一個包含輸入表單項的例子,嘗試更新 element,代碼以下:
/** @jsx myReact.createElement */ const container = document.getElementById("container") const updateValue = e => { rerender(e.target.value) } const rerender = value => { const element = ( <div> <input onInput={updateValue} value={value} /> <h2>Hello {value}</h2> </div> ) myReact.render(element, container) } rerender("World")
本例完整源碼見:reactDemo9
輸出結果如圖:
先來看一個簡單的函數式組件示例:
myReact 還不支持函數式組件,下面代碼運行會報錯,這裏僅用於比照函數式組件的常規使用方式。
/** @jsx myReact.createElement */ const container = document.getElementById("container") function App (props) { return ( <h1>hi~ {props.name}</h1> ) } const element = ( <App name='foo' /> ) myReact.render(element, container)
函數式組件和 html 標籤組件相比,有如下兩點不一樣:
經過下列步驟實現函數組件:
function performUnitOfWork(fiber) { // 是不是函數類型組件 const isFunctionComponent = fiber && fiber.type && fiber.type instanceof Function // 若是是函數組件,執行 updateFunctionComponent 函數 if (isFunctionComponent) { updateFunctionComponent(fiber) } else { // 若是不是函數組件,執行 updateHostComponent 函數 updateHostComponent(fiber) } // 省略 }
非函數式組件可直接將 fiber.props.children 做爲參數傳遞。
function updateHostComponent(fiber) { if (!fiber.dom) { fiber.dom = createDom(fiber) } reconcileChildren(fiber, fiber.props.children) }
函數組件須要運行來得到 fiber.children。
function updateFunctionComponent(fiber) { // fiber.type 就是函數組件自己,fiber.props 就是函數組件的參數 const children = [fiber.type(fiber.props)] reconcileChildren(fiber, children) }
4.1 修改 domParent 的獲取邏輯,經過 while 循環不斷向上尋找,直到找到有 dom 節點的父 fiber;
function commitWork (fiber) { // 省略 let domParentFiber = fiber.parent // 若是 fiber.parent 沒有 dom 節點,則繼續找 fiber.parent.parent.dom,直到有 dom 節點。 while (!domParentFiber.dom) { domParentFiber = domParentFiber.parent } const domParent = domParentFiber.dom // 省略 }
4.2 修改刪除節點的邏輯,當刪除節點時,須要不斷向下尋找,直到找到有 dom 節點的子 fiber;
function commitWork (fiber) { // 省略 // 若是 fiber 的更新類型是刪除,執行 commitDeletion else if (fiber.effectTag === "DELETION") { commitDeletion(fiber.dom, domParent) } // 省略 } // 刪除節點 function commitDeletion (fiber, domParent) { // 若是該 fiber 有 dom 節點,直接刪除 if (fiber.dom) { domParent.removeChild(fiber.dom) } else { // 若是該 fiber 沒有 dom 節點,則繼續找它的子節點進行刪除 commitDeletion(fiber.child, domParent) } }
下面試一下上面的例子,代碼以下:
/** @jsx myReact.createElement */ const container = document.getElementById("container") function App (props) { return ( <h1>hi~ {props.name}</h1> ) } const element = ( <App name='foo' /> ) myReact.render(element, container)
本例完整源碼見:reactDemo10
運行結果如圖:
下面繼續爲 myReact 添加管理狀態的功能,指望是函數組件擁有本身的狀態,且能夠獲取、更新狀態。
一個擁有計數功能的函數組件以下:
function Counter() { const [state, setState] = myReact.useState(1) return ( <h1 onClick={() => setState(c => c + 1)}> Count: {state} </h1> ) } const element = <Counter />
已知須要一個 useState 方法用來獲取、更新狀態。
這裏再重申一下,渲染函數組件的前提是,執行該函數組件,所以,上述 Counter 想要更新計數,就會在每次更新都執行一次 Counter 函數。
經過如下步驟實現:
// 當前工做單元 fiber let wipFiber = null function updateFunctionComponent(fiber) { wipFiber = fiber // 當前工做單元 fiber 的 hook wipFiber.hook = [] // 省略 }
// initial 表示初始參數,在本例中,initial=1 function useState (initial) { // 是否有舊鉤子,舊鉤子存儲了上一次更新的 hook const oldHook = wipFiber.alternate && wipFiber.alternate.hook // 初始化鉤子,鉤子的狀態是舊鉤子的狀態或者初始狀態 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.hook = hook // 返回鉤子的狀態和設置鉤子的函數 return [hook.state, setState] }
下面運行一下計數組件,代碼以下:
function Counter() { const [state, setState] = myReact.useState(1) return ( <h1 onClick={() => setState(c => c + 1)}> Count: {state} </h1> ) } const element = <Counter />
本例完整源碼見:reactDemo11
運行結果如圖:
本章節簡單實現了 myReact 的 hooks 功能。
撒花完結,react 還有不少實現值得咱們去學習和研究,但願有下期,和你們一塊兒手寫 react 的更多功能。
本文參考 pomb.us 進行學習,實現了包括虛擬 DOM、Fiber、Diff 算法、函數式組件、hooks 等功能的自定義 React。
在實現過程當中小編對 React 的基本術語及實現思路有了大概的掌握,pomb.us 是很是適合初學者的學習資料,能夠直接經過 pomb.us 進行學習,也推薦跟着本文一步步實現 React 的常見功能。
本文源碼: github源碼 。
建議跟着一步步敲,進行實操練習。
但願能對你有所幫助,感謝閱讀~
別忘了點個贊鼓勵一下我哦,筆芯❤️
歡迎關注凹凸實驗室博客:aotu.io
或者關注凹凸實驗室公衆號(AOTULabs),不定時推送文章: