摘要:本系列文章在實現一個的同時理順框架的主干內容虛擬組件生命周期算法從到實現系列和從到實現系列組件和生命周期先來回顧的生命周期,用流程圖表示如下該流程圖比較清晰地呈現了的生命周期。它們的目的都是降低空間復雜度。
本系列文章在實現一個 (x)react 的同時理順 React 框架的主干內容(JSX/虛擬DOM/組件/生命周期/diff算法/...)
從 0 到 1 實現 React 系列 —— JSX 和 Virtual DOM
從 0 到 1 實現 React 系列 —— 組件和 state|props
生命周期先來回顧 React 的生命周期,用流程圖表示如下:
該流程圖比較清晰地呈現了 react 的生命周期。其分為 3 個階段 —— 生成期,存在期,銷毀期。
因為生命周期鉤子函數存在于自定義組件中,將之前 _render 函數作些調整如下:
// 原來的 _render 函數,為了將職責拆分得更細,將 virtual dom 轉為 real dom 的函數多帶帶抽離出來 function vdomToDom(vdom) { if (_.isFunction(vdom.nodeName)) { // 為了更加方便地書寫生命周期邏輯,將解析自定義組件邏輯和一般 html 標簽的邏輯分離開 const component = createComponent(vdom) // 構造組件 setProps(component) // 更改組件 props renderComponent(component) // 渲染組件,將 dom 節點賦值到 component return component.base // 返回真實 dom } ... }
我們可以在 setProps 函數內(渲染前)加入 componentWillMount,componentWillReceiveProps 方法,setProps 函數如下:
function setProps(component) { if (component && component.componentWillMount) { component.componentWillMount() } else if (component.base && component.componentWillReceiveProps) { component.componentWillReceiveProps(component.props) // 后面待實現 } }
而后我們在 renderComponent 函數內加入 componentDidMount、shouldComponentUpdate、componentWillUpdate、componentDidUpdate 方法
function renderComponent(component) { if (component.base && component.shouldComponentUpdate) { const bool = component.shouldComponentUpdate(component.props, component.state) if (!bool && bool !== undefined) { return false // shouldComponentUpdate() 返回 false,則生命周期終止 } } if (component.base && component.componentWillUpdate) { component.componentWillUpdate() } const rendered = component.render() const base = vdomToDom(rendered) if (component.base && component.componentDidUpdate) { component.componentDidUpdate() } else if (component && component.componentDidMount) { component.componentDidMount() } if (component.base && component.base.parentNode) { // setState 進入此邏輯 component.base.parentNode.replaceChild(base, component.base) } component.base = base // 標志符 }測試生命周期
測試如下用例:
class A extends Component { componentWillReceiveProps(props) { console.log("componentWillReceiveProps") } render() { return ({this.props.count}) } } class B extends Component { constructor(props) { super(props) this.state = { count: 1 } } componentWillMount() { console.log("componentWillMount") } componentDidMount() { console.log("componentDidMount") } shouldComponentUpdate(nextProps, nextState) { console.log("shouldComponentUpdate", nextProps, nextState) return true } componentWillUpdate() { console.log("componentWillUpdate") } componentDidUpdate() { console.log("componentDidUpdate") } click() { this.setState({ count: ++this.state.count }) } render() { console.log("render") return ( ) } } ReactDOM.render( , document.getElementById("root") )
頁面加載時輸出結果如下:
componentWillMount render componentDidMount
點擊按鈕時輸出結果如下:
shouldComponentUpdate componentWillUpdate render componentDidUpdatediff 的實現
在 react 中,diff 實現的思路是將新老 virtual dom 進行比較,將比較后的 patch(補丁)渲染到頁面上,從而實現局部刷新;本文借鑒了 preact 和 simple-react 中的 diff 實現,總體思路是將舊的 dom 節點和新的 virtual dom 節點進行了比較,根據不同的比較類型(文本節點、非文本節點、自定義組件)調用相應的邏輯,從而實現頁面的局部渲染。代碼總體結構如下:
/** * 比較舊的 dom 節點和新的 virtual dom 節點: * @param {*} oldDom 舊的 dom 節點 * @param {*} newVdom 新的 virtual dom 節點 */ function diff(oldDom, newVdom) { ... if (_.isString(newVdom)) { return diffTextDom(oldDom, newVdom) // 對比文本 dom 節點 } if (oldDom.nodeName.toLowerCase() !== newVdom.nodeName) { diffNotTextDom(oldDom, newVdom) // 對比非文本 dom 節點 } if (_.isFunction(newVdom.nodeName)) { return diffComponent(oldDom, newVdom) // 對比自定義組件 } diffAttribute(oldDom, newVdom) // 對比屬性 if (newVdom.children.length > 0) { diffChild(oldDom, newVdom) // 遍歷對比子節點 } return oldDom }
下面根據不同比較類型實現相應邏輯。
對比文本節點首先進行較為簡單的文本節點的比較,代碼如下:
// 對比文本節點 function diffTextDom(oldDom, newVdom) { let dom = oldDom if (oldDom && oldDom.nodeType === 3) { // 如果老節點是文本節點 if (oldDom.textContent !== newVdom) { // 這里一個細節:textContent/innerHTML/innerText 的區別 oldDom.textContent = newVdom } } else { // 如果舊 dom 元素不為文本節點 dom = document.createTextNode(newVdom) if (oldDom && oldDom.parentNode) { oldDom.parentNode.replaceChild(dom, oldDom) } } return dom }對比非文本節點
對比非文本節點,其思路為將同層級的舊節點替換為新節點,代碼如下:
// 對比非文本節點 function diffNotTextDom(oldDom, newVdom) { const newDom = document.createElement(newVdom.nodeName); [...oldDom.childNodes].map(newDom.appendChild) // 將舊節點下的元素添加到新節點下 if (oldDom && oldDom.parentNode) { oldDom.parentNode.replaceChild(oldDom, newDom) } }對比自定義組件
對比自定義組件的思路為:如果新老組件不同,則直接將新組件替換老組件;如果新老組件相同,則將新組件的 props 賦到老組件上,然后再對獲得新 props 前后的老組件做 diff 比較。代碼如下:
// 對比自定義組件 function diffComponent(oldDom, newVdom) { if (oldDom._component && (oldDom._component.constructor !== newVdom.nodeName)) { // 如果新老組件不同,則直接將新組件替換老組件 const newDom = vdomToDom(newVdom) oldDom._component.parentNode.insertBefore(newDom, oldDom._component) oldDom._component.parentNode.removeChild(oldDom._component) } else { setProps(oldDom._component, newVdom.attributes) // 如果新老組件相同,則將新組件的 props 賦到老組件上 renderComponent(oldDom._component) // 對獲得新 props 前后的老組件做 diff 比較(renderComponent 中調用了 diff) } }遍歷對比子節點
遍歷對比子節點的策略有兩個:一是只比較同層級的節點,二是給節點加上 key 屬性。它們的目的都是降低空間復雜度。代碼如下:
// 對比子節點 function diffChild(oldDom, newVdom) { const keyed = {} const children = [] const oldChildNodes = oldDom.childNodes for (let i = 0; i < oldChildNodes.length; i++) { if (oldChildNodes[i].key) { // 將含有 key 的節點存進對象 keyed keyed[oldChildNodes[i].key] = oldChildNodes[i] } else { // 將不含有 key 的節點存進數組 children children.push(oldChildNodes[i]) } } const newChildNodes = newVdom.children let child for (let i = 0; i < newChildNodes.length; i++) { if (keyed[newChildNodes[i].key]) { // 對應上面存在 key 的情形 child = keyed[newChildNodes[i].key] keyed[newChildNodes[i].key] = undefined } else { // 對應上面不存在 key 的情形 for (let j = 0; j < children.length; j++) { if (isSameNodeType(children[i], newChildNodes[i])) { // 如果不存在 key,則優先找到節點類型相同的元素 child = children[i] children[i] = undefined break } } } diff(child, newChildNodes[i]) // 遞歸比較 } }測試
在生命周期的小節中,componentWillReceiveProps 方法還未跑通,稍加修改 setProps 函數即可:
/** * 更改屬性,componentWillMount 和 componentWillReceiveProps 方法 */ function setProps(component, attributes) { if (attributes) { component.props = attributes // 這段邏輯對應上文自定義組件比較中新老組件相同時 setProps 的邏輯 } if (component && component.base && component.componentWillReceiveProps) { component.componentWillReceiveProps(component.props) } else if (component && component.componentWillMount) { component.componentWillMount() } }
來測試下生命周期小節中最后的測試用例:
生命周期測試
diff 測試
項目地址,關于如何 pr
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/96305.html
摘要:因為版本將真正廢棄這三生命周期到目前為止,的渲染機制遵循同步渲染首次渲染,更新時更新時卸載時期間每個周期函數各司其職,輸入輸出都是可預測,一路下來很順暢。通過進一步觀察可以發現,預廢棄的三個生命周期函數都發生在虛擬的構建期間,也就是之前。 showImg(https://segmentfault.com/img/bVbweoj?w=559&h=300); 背景 前段時間準備前端招聘事項...
摘要:當組件要被卸載之前,框架會調用函數,之后就會卸載組件。開發者可以在這幾個生命周期函數中定義一些你想組件變化的操作或者做一些數據的改變。 react組件有兩個狀態,一個是渲染狀態,一個是卸載狀態,而渲染狀態又分為初始渲染狀態(也可以說是創建狀態)和重新渲染狀態(也可以說是存在狀態,說明組件一直存在,會發生多次重新渲染)。這三個狀態下又會產生一系列的生命周期函數,開發人員一般只需要了解其中...
摘要:前言的基本概念組件的構建方法以及高級用法這背后的一切如何運轉深入內部的實現機制和原理初探源碼代碼組織結構包含一系列的工具方法插件包含一系列同構方法包含一些公用或常用方法如等包含一些測試方法等包含一些邊界錯誤的測試用例是代碼的核心部分它包含了 前言 React的基本概念,API,組件的構建方法以及高級用法,這背后的一切如何運轉,深入Virtual DOM內部的實現機制和原理. 初探Rea...
摘要:異步渲染利用事件循環,延遲渲染函數的調用調用回調函數處理后跟函數的情況淺合并邏輯事件循環,關于的事件循環和的事件循環后續會單獨寫篇文章。 showImg(https://segmentfault.com/img/remote/1460000015785464?w=640&h=280); 看源碼一個痛處是會陷進理不順主干的困局中,本系列文章在實現一個 (x)react 的同時理順 Rea...
摘要:背景介紹入門實例教程起源于的內部項目,因為該公司對市場上所有框架,都不滿意,就決定自己寫一套,用來架設的網站。做出來以后,發現這套東西很好用,就在年月開源了。也就是說,通過鉤子函 react - JSX React 背景介紹 React 入門實例教程 React 起源于 Facebook 的內部項目,因為該公司對市場上所有 JavaScript MVC 框架,都不滿意,就決定自己寫一套...
閱讀 2566·2021-10-11 10:58
閱讀 1148·2021-09-29 09:34
閱讀 1486·2021-09-26 09:46
閱讀 3830·2021-09-22 15:31
閱讀 730·2019-08-30 15:54
閱讀 1458·2019-08-30 13:20
閱讀 1251·2019-08-30 13:13
閱讀 1486·2019-08-26 13:52