摘要:需要把這個紀(jì)錄到數(shù)組中。后面討論的生命周期都是基于組件實例的復(fù)用才有意義。所以這個遞歸一定是能夠正常返回的。多出來的要被銷毀到這里,就有生命周期了之前的代碼由于會用到。
從0實現(xiàn)一個tiny react(三)生命周期
在給tinyreact加生命周期之前,先考慮 組件實例的復(fù)用 這個前置問題
復(fù)用組件實例render函數(shù) 只能返回一個根
class A extends Component{ render() { return (...) } } class C extends Component { render() { return () } }... ... ...
所以 最終的組件樹一定是類似這種的 (首字母大寫的代表組件, div/span/a...代表原生DOM類型)
是絕對不可能 出現(xiàn)下圖這種樹結(jié)構(gòu) (與render函數(shù)返回單根的特性矛盾)
注意 __rendered引用 指向了一個inst/dom。 所以可以通過__rendered來復(fù)用實例。
下面我們討論怎么根據(jù)__rendered 復(fù)用inst
假如在 Father里面調(diào)用 setState? 按照現(xiàn)在render 函數(shù)的做法:
else if (typeof vnode.nodeName == "function") { let func = vnode.nodeName let inst = new func(vnode.props) ... }
新建 Son 實例
新建 Grandson 實例
diff 渲染 div
再次setState呢? 好吧, 再來一次:
新建 Son 實例
新建 Grandson 實例
diff 渲染 div
第 3步 就是 (二) 討論的內(nèi)容, 會用"最少"的dom操作, 來更新dom到最新的狀態(tài)。
對于1, 2 每次setState的時候都會新建inst, 在這里是可以復(fù)用之前創(chuàng)建好的inst實例的。
但是如果一個組件 初始渲染為 "", setState 之后渲染為 "" 這種情況呢? 那inst就不能復(fù)用了, 類比一下 DOM 里的 div --> span
。 把render 第四個參數(shù) old ---> olddomOrComp , 通過這個參數(shù)來判斷 dom 或者inst 是否可以復(fù)用:
//inst 是否可以復(fù)用 function render (vnode, parent, comp, olddomOrComp) { ... } else if(typeof vnode.nodeName === "string") { if(!olddomOrComp || olddomOrComp.nodeName !== vnode.nodeName.toUpperCase()) { // <--- dom 可以復(fù)用 createNewDom(vnode, parent, comp, olddomOrComp, myIndex) } ... } else if (typeof vnode.nodeName == "function") { let func = vnode.nodeName let inst if(olddomOrComp && olddomOrComp instanceof func) { // <--- inst 可以復(fù)用 inst = olddomOrComp olddomOrComp.props = vnode.props } .... render(innerVnode, parent, inst, inst.__rendered)
這里 在最后的 render(innerVnode, parent, inst, olddom) 被改為了: render(innerVnode, parent, inst, inst.__rendered)。 這樣是符合 olddomOrComp定義的。
但是 olddom 其實是有2個作用的
判斷dom是否可以復(fù)用
parent.replaceChild(dom, olddom), olddom確定了新的dom的位置
而 olddomOrComp 是做不到第二點。 即使: parent.replaceChild(dom, getDOM(olddomOrComp)) 也是不行的。 原因是:
假如初始 CompA -->
怎么解決呢? 引入第5個參數(shù) myIndex: dom的位置問題都交給這個變量。 olddomOrComp只負(fù)責(zé)決定 復(fù)用的問題
so, 加入myIndex的代碼如下:
/** * 替換新的Dom, 如果沒有在最后插入 * @param parent * @param newDom * @param myIndex */ function setNewDom(parent, newDom, myIndex) { const old = parent.childNodes[myIndex] if (old) { parent.replaceChild(newDom, old) } else { parent.appendChild(newDom) } } function render(vnode, parent, comp, olddomOrComp, myIndex) { let dom if(typeof vnode === "string" || typeof vnode === "number" ) { ... } else { dom = document.createTextNode(vnode) setNewDom(parent, dom, myIndex) // <--- 根據(jù)myIndex設(shè)置 dom } } else if(typeof vnode.nodeName === "string") { if(!olddomOrComp || olddomOrComp.nodeName !== vnode.nodeName.toUpperCase()) { createNewDom(vnode, parent, comp, olddomOrComp, myIndex) } else { diffDOM(vnode, parent, comp, olddomOrComp, myIndex) } } else if (typeof vnode.nodeName === "function") { ... let innerVnode = inst.render() render(innerVnode, parent, inst, inst.__rendered, myIndex) // <--- 傳遞 myIndex } } function createNewDom(vnode, parent, comp, olddomOrComp, myIndex) { ... setAttrs(dom, vnode.props) setNewDom(parent, dom, myIndex) // <--- 根據(jù)myIndex設(shè)置 dom for(let i = 0; i < vnode.children.length; i++) { render(vnode.children[i], dom, null, null, i) // <--- i 就是myIndex } } function diffDOM(vnode, parent, comp, olddom) { ... for(let i = 0; i < vnode.children.length; i++) { render(vnode.children[i], olddom, null, renderedArr[i], i) // <--- i 就是myIndex } ... }
重新考慮 Father里面調(diào)用 setState。 此時已經(jīng)不會創(chuàng)建新實例了。
那么 假如現(xiàn)在對 Grandson調(diào)用setState呢? 很不幸, 我們需要創(chuàng)建Granssonson1, Granssonson2, Granssonson3, 調(diào)用幾次, 我們就得跟著新建幾次。
上面的復(fù)用方式 并沒有解決這個問題, 之前 __rendered 引用鏈 到 dom就結(jié)束了。
把__rendered這條鏈 完善吧!!
首先 對__rendered 重新定義如下:
當(dāng)X 是組件實例的時候, __rendered 為X渲染出的 組件實例 或者 dom元素
當(dāng)X 是dom元素的時候, __rendered 為一個數(shù)組, 是X的子組件實例 或者 子dom元素
Father --__rendered--> Son --__rendered--> Grandson --__rendered--> div --__rendered--> [Granssonson1, Granssonson2, Granssonson3,]
在dom 下創(chuàng)建 "直接子節(jié)點" 的時候。 需要把這個紀(jì)錄到dom.__rendered 數(shù)組中。 或者說, 如果新建的一個dom元素/組件實例 是dom的 "直接子節(jié)點", 那么需要把它紀(jì)錄到
parent.__rendered 數(shù)組中。 那怎么判斷 創(chuàng)建出來的是 "直接子節(jié)點" 呢? 答案是render 第3個參數(shù) comp為null的, 很好理解, comp的意思是 "誰渲染了我"
很明顯, 只有 dom下的 "直接子節(jié)點" comp才是null, 其他的情況, comp肯定不是null, 比如 Son的comp是Father, Gsss1
的comp是Grandsonson1。。。
并且當(dāng)setState重新渲染的時候, 如果老的dom/inst沒有被復(fù)用, 則應(yīng)該用新的dom/inst 替換
創(chuàng)建dom的時候。
function createNewDom(vnode, parent, comp, olddomOrComp, myIndex) { ... if (comp) { comp.__rendered = dom } else { parent.__rendered[myIndex] = dom } ... }
組件實例
else if (typeof vnode.nodeName == "function") { ... if(olddomOrComp && olddomOrComp instanceof func) { inst = olddomOrComp } else { inst = new func(vnode.props) if (comp) { comp.__rendered = inst } else { parent.__rendered[myIndex] = inst } } ... }
diffDOM 的時候: a. remove多余的節(jié)點; b. render子節(jié)點的時候olddomOrComp = olddom.__rendered[i]
function diffDOM(vnode, parent, comp, olddom) { ... olddom.__rendered.slice(vnode.children.length) // <--- 移除多余 子節(jié)點 .forEach(element => { olddom.removeChild(getDOM(element)) }) olddom.__rendered = olddom.__rendered.slice(0, vnode.children.length) for(let i = 0; i < vnode.children.length; i++) { render(vnode.children[i], olddom, null, olddom.__rendered[i], i) } olddom.__vnode = vnode }
所以完整的代碼:
function render(vnode, parent, comp, olddomOrComp, myIndex) { let dom if(typeof vnode === "string" || typeof vnode === "number" ) { if(olddomOrComp && olddomOrComp.splitText) { if(olddomOrComp.nodeValue !== vnode) { olddomOrComp.nodeValue = vnode } } else { dom = document.createTextNode(vnode) parent.__rendered[myIndex] = dom //comp 一定是null setNewDom(parent, dom, myIndex) } } else if(typeof vnode.nodeName === "string") { if(!olddomOrComp || olddomOrComp.nodeName !== vnode.nodeName.toUpperCase()) { createNewDom(vnode, parent, comp, olddomOrComp, myIndex) } else { diffDOM(vnode, parent, comp, olddomOrComp) } } else if (typeof vnode.nodeName === "function") { let func = vnode.nodeName let inst if(olddomOrComp && olddomOrComp instanceof func) { inst = olddomOrComp inst.props = vnode.props } else { inst = new func(vnode.props) if (comp) { comp.__rendered = inst } else { parent.__rendered[myIndex] = inst } } let innerVnode = inst.render() render(innerVnode, parent, inst, inst.__rendered, myIndex) } } function createNewDom(vnode, parent, comp, olddomOrComp, myIndex) { let dom = document.createElement(vnode.nodeName) dom.__rendered = [] // 創(chuàng)建dom的 設(shè)置 __rendered 引用 dom.__vnode = vnode if (comp) { comp.__rendered = dom } else { parent.__rendered[myIndex] = dom } setAttrs(dom, vnode.props) setNewDom(parent, dom, myIndex) for(let i = 0; i < vnode.children.length; i++) { render(vnode.children[i], dom, null, null, i) } } function diffDOM(vnode, parent, comp, olddom) { const {onlyInLeft, bothIn, onlyInRight} = diffObject(vnode.props, olddom.__vnode.props) setAttrs(olddom, onlyInLeft) removeAttrs(olddom, onlyInRight) diffAttrs(olddom, bothIn.left, bothIn.right) olddom.__rendered.slice(vnode.children.length) .forEach(element => { olddom.removeChild(getDOM(element)) }) const __renderedArr = olddom.__rendered.slice(0, vnode.children.length) olddom.__rendered = __renderedArr for(let i = 0; i < vnode.children.length; i++) { render(vnode.children[i], olddom, null, __renderedArr[i], i) } olddom.__vnode = vnode } class Component { constructor(props) { this.props = props } setState(state) { setTimeout(() => { this.state = state const vnode = this.render() let olddom = getDOM(this) const myIndex = getDOMIndex(olddom) render(vnode, olddom.parentNode, this, this.__rendered, myIndex) }, 0) } } function getDOMIndex(dom) { const cn = dom.parentNode.childNodes for(let i= 0; i < cn.length; i++) { if (cn[i] === dom ) { return i } } }
現(xiàn)在 __rendered鏈 完善了, setState觸發(fā)的渲染, 都會先去嘗試復(fù)用 組件實例。 在線演示
生命周期前面討論的__rendered 和生命周期有 什么關(guān)系呢? 生命周期是組件實例的生命周期, 之前的工作起碼保證了一點: constructor 只會被調(diào)用一次了吧。。。
后面討論的生命周期 都是基于 "組件實例"的 復(fù)用才有意義。tinyreact 將實現(xiàn)以下的生命周期:
componentWillMount
componentDidMount
componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate
componentDidUpdate
componentWillUnmount
他們 和 react同名函數(shù) 含義相同
這三個生命周期 是如此之簡單: componentWillMount緊接著 創(chuàng)建實例的時候調(diào)用; 渲染完成之后,如果
組件是新建的componentDidMount , 否則:componentDidUpdate
else if (typeof vnode.nodeName === "function") { let func = vnode.nodeName let inst if(olddomOrComp && olddomOrComp instanceof func) { inst = olddomOrComp inst.props = vnode.props } else { inst = new func(vnode.props) inst.componentWillMount && inst.componentWillMount() if (comp) { comp.__rendered = inst } else { parent.__rendered[myIndex] = inst } } let innerVnode = inst.render() render(innerVnode, parent, inst, inst.__rendered, myIndex) if(olddomOrComp && olddomOrComp instanceof func) { inst.componentDidUpdate && inst.componentDidUpdate() } else { inst.componentDidMount && inst.componentDidMount() } }componentWillReceiveProps, shouldComponentUpdate, componentWillUpdate
當(dāng)組件 獲取新的props的時候, 會調(diào)用componentWillReceiveProps, 參數(shù)為newProps, 并且在這個方法內(nèi)部this.props 還是值向oldProps,
由于 props的改變 由 只能由 父組件 觸發(fā)。 所以只用在 render函數(shù)里面處理就ok。不過 要在 inst.props = vnode.props 之前調(diào)用componentWillReceiveProps:
else if (typeof vnode.nodeName === "function") { let func = vnode.nodeName let inst if(olddomOrComp && olddomOrComp instanceof func) { inst = olddomOrComp inst.componentWillReceiveProps && inst.componentWillReceiveProps(vnode.props) // <-- 在 inst.props = vnode.props 之前調(diào)用 inst.props = vnode.props } else { ... } }
當(dāng) 組件的 props或者state發(fā)生改變的時候,組件一定會渲染嗎?shouldComponentUpdate說了算!! 如果組件沒有shouldComponentUpdate這個方法, 默認(rèn)是渲染的。
否則是基于 shouldComponentUpdate的返回值。 這個方法接受兩個參數(shù) newProps, newState 。
另外由于 props和 state(setState) 改變都會引起 shouldComponentUpdate調(diào)用, 所以:
function render(vnode, parent, comp, olddomOrComp, myIndex) { ... else if (typeof vnode.nodeName === "function") { let func = vnode.nodeName let inst if(olddomOrComp && olddomOrComp instanceof func) { inst = olddomOrComp inst.componentWillReceiveProps && inst.componentWillReceiveProps(vnode.props) // <-- 在 inst.props = vnode.props 之前調(diào)用 let shoudUpdate if(inst.shouldComponentUpdate) { shoudUpdate = inst.shouldComponentUpdate(vnode.props, olddomOrComp.state) // <-- 在 inst.props = vnode.props 之前調(diào)用 } else { shoudUpdate = true } inst.props = vnode.props if (!shoudUpdate) { // <-- 在 inst.props = vnode.props 之后 return // do nothing just return } } else { ... } } ... } setState(state) { setTimeout(() => { let shoudUpdate if(this.shouldComponentUpdate) { shoudUpdate = this.shouldComponentUpdate(this.props, state) } else { shoudUpdate = true } this.state = state if (!shoudUpdate) { // <-- 在 this.state = state 之后 return // do nothing just return } const vnode = this.render() let olddom = getDOM(this) const myIndex = getDOMIndex(olddom) render(vnode, olddom.parentNode, this, this.__rendered, myIndex) this.componentDidUpdate && this.componentDidUpdate() // <-- 需要調(diào)用下: componentDidUpdate }, 0) }
當(dāng) shoudUpdate 為false的時候呢, 直接return 就ok了, 但是shoudUpdate 為false 只是表明 不渲染, 但是在 return之前, newProps和newState一定要設(shè)置到組件實例上。
注 setState render之后 也是需要調(diào)用: componentDidUpdate
當(dāng) shoudUpdate == true 的時候。 會調(diào)用: componentWillUpdate, 參數(shù)為newProps和newState。 這個函數(shù)調(diào)用之后,就會把nextProps和nextState分別設(shè)置到this.props和this.state中。
function render(vnode, parent, comp, olddomOrComp, myIndex) { ... else if (typeof vnode.nodeName === "function") { ... let shoudUpdate if(inst.shouldComponentUpdate) { shoudUpdate = inst.shouldComponentUpdate(vnode.props, olddomOrComp.state) // <-- 在 inst.props = vnode.props 之前調(diào)用 } else { shoudUpdate = true } shoudUpdate && inst.componentWillUpdate && inst.componentWillUpdate(vnode.props, olddomOrComp.state) // <-- 在 inst.props = vnode.props 之前調(diào)用 inst.props = vnode.props if (!shoudUpdate) { // <-- 在 inst.props = vnode.props 之后 return // do nothing just return } ... } setState(state) { setTimeout(() => { ... shoudUpdate && this.componentWillUpdate && this.componentWillUpdate(this.props, state) // <-- 在 this.state = state 之前調(diào)用 this.state = state if (!shoudUpdate) { // <-- 在 this.state = state 之后 return // do nothing just return } ... }componentWillUnmount
當(dāng)組件要被銷毀的時候, 調(diào)用組件的componentWillUnmount。 inst沒有被復(fù)用的時候, 要銷毀。 dom沒有被復(fù)用的時候, 也要銷毀, 而且是樹形結(jié)構(gòu)
的遞歸操作。 有點像 render的遞歸, 直接看代碼:
function recoveryComp(comp) { if (comp instanceof Component) { // <--- component comp.componentWillUnmount && comp.componentWillUnmount() recoveryComp(comp.__rendered) } else if (comp.__rendered instanceof Array) { // <--- dom like div/span comp.__rendered.forEach(element => { recoveryComp(element) }) } else { // <--- TextNode // do nothing } }
recoveryComp 是這樣的一個 遞歸函數(shù):
當(dāng)domOrComp 為組件實例的時候, 首先調(diào)用:componentWillUnmount, 然后 recoveryDomOrComp(inst.__rendered) 。 這里的先后順序關(guān)系很重要
當(dāng)domOrComp 為DOM節(jié)點 (非文本 TextNode), 遍歷 recoveryDomOrComp(子節(jié)點)
當(dāng)domOrComp 為TextNode,nothing...
與render一樣, 由于組件 最終一定會render html的標(biāo)簽。 所以這個遞歸一定是能夠正常返回的。
哪些地方需要調(diào)用recoveryComp ?
所有olddomOrComp 沒有被復(fù)用的地方。 因為一旦olddomOrComp 不被復(fù)用, 一定有一個新的取得它, 它就要被銷毀
多余的 子節(jié)點。 div 起初有3個子節(jié)點, setState之后變成了2個。 多出來的要被銷毀
function diffDOM(vnode, parent, comp, olddom) { const {onlyInLeft, bothIn, onlyInRight} = diffObject(vnode.props, olddom.__vnode.props) setAttrs(olddom, onlyInLeft) removeAttrs(olddom, onlyInRight) diffAttrs(olddom, bothIn.left, bothIn.right) const willRemoveArr = olddom.__rendered.slice(vnode.children.length) const renderedArr = olddom.__rendered.slice(0, vnode.children.length) olddom.__rendered = renderedArr for(let i = 0; i < vnode.children.length; i++) { render(vnode.children[i], olddom, null, renderedArr[i], i) } willRemoveArr.forEach(element => { recoveryComp(element) olddom.removeChild(getDOM(element)) }) olddom.__vnode = vnode }
到這里, tinyreact 就有 生命周期了
之前的代碼 由于會用到 dom.__rendered。 所以:
const root = document.getElementById("root") root.__rendered = [] render(, root)
為了不要在 調(diào)用render之前 設(shè)置:__rendered 做個小的改動 :
/** * 渲染vnode成實際的dom * @param vnode 虛擬dom表示 * @param parent 實際渲染出來的dom,掛載的父元素 */ export default function render(vnode, parent) { parent.__rendered =[] //<--- 這里設(shè)置 __rendered renderInner(vnode, parent, null, null, 0) } function renderInner(vnode, parent, comp, olddomOrComp, myIndex) { ... }其他
tinyreact 未實現(xiàn)功能:
context
事件代理
多吃調(diào)用setState, 只render一次
react 頂層Api
。。。
tinyreat 有些地方參考了preact
npm包:
npm install tinyreact --save
所有代碼托管在git example 目錄下有blog中的例子
經(jīng)典的TodoList。 項目 代碼
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/91784.html
摘要:組件裝載過程裝載過程依次調(diào)用的生命周期函數(shù)中每個類的構(gòu)造函數(shù),創(chuàng)造一個組件實例,當(dāng)然會調(diào)用對應(yīng)的構(gòu)造函數(shù)。組件需要構(gòu)造函數(shù),是為了以下目的初始化,因為生命周期中任何函數(shù)都有可能訪問,構(gòu)造函數(shù)是初始化的理想場所綁定成員函數(shù)的環(huán)境。 React系列---React(一)初識ReactReact系列---React(二)組件的prop和stateReact系列---之React(三)組件的生...
摘要:因為版本將真正廢棄這三生命周期到目前為止,的渲染機制遵循同步渲染首次渲染,更新時更新時卸載時期間每個周期函數(shù)各司其職,輸入輸出都是可預(yù)測,一路下來很順暢。通過進一步觀察可以發(fā)現(xiàn),預(yù)廢棄的三個生命周期函數(shù)都發(fā)生在虛擬的構(gòu)建期間,也就是之前。 showImg(https://segmentfault.com/img/bVbweoj?w=559&h=300); 背景 前段時間準(zhǔn)備前端招聘事項...
摘要:已經(jīng)被廢除,具體缺陷可以參考二為了解決的缺陷,第二種解決方案是高階組件簡稱。我們定義了父組件,存在自身的,并且將自身的通過的方式傳遞給了子組件。返回一個標(biāo)識該的變量,以及更新該的方法。 ??為了實現(xiàn)分離業(yè)務(wù)邏輯代碼,實現(xiàn)組件內(nèi)部相關(guān)業(yè)務(wù)邏輯的復(fù)用,在React的迭代中針對類組件中的代碼復(fù)用依次發(fā)布了Mixin、HOC、Render props等幾個方案。此外,針對函數(shù)組件,在Reac...
摘要:已經(jīng)被廢除,具體缺陷可以參考二為了解決的缺陷,第二種解決方案是高階組件簡稱。我們定義了父組件,存在自身的,并且將自身的通過的方式傳遞給了子組件。返回一個標(biāo)識該的變量,以及更新該的方法。 ??為了實現(xiàn)分離業(yè)務(wù)邏輯代碼,實現(xiàn)組件內(nèi)部相關(guān)業(yè)務(wù)邏輯的復(fù)用,在React的迭代中針對類組件中的代碼復(fù)用依次發(fā)布了Mixin、HOC、Render props等幾個方案。此外,針對函數(shù)組件,在Reac...
摘要:已經(jīng)被廢除,具體缺陷可以參考二為了解決的缺陷,第二種解決方案是高階組件簡稱。我們定義了父組件,存在自身的,并且將自身的通過的方式傳遞給了子組件。返回一個標(biāo)識該的變量,以及更新該的方法。 ??為了實現(xiàn)分離業(yè)務(wù)邏輯代碼,實現(xiàn)組件內(nèi)部相關(guān)業(yè)務(wù)邏輯的復(fù)用,在React的迭代中針對類組件中的代碼復(fù)用依次發(fā)布了Mixin、HOC、Render props等幾個方案。此外,針對函數(shù)組件,在Reac...
閱讀 1833·2021-11-25 09:43
閱讀 1335·2021-11-22 15:08
閱讀 3735·2021-11-22 09:34
閱讀 3225·2021-09-04 16:40
閱讀 3002·2021-09-04 16:40
閱讀 542·2019-08-30 15:54
閱讀 1335·2019-08-29 17:19
閱讀 1752·2019-08-28 18:13