從0實現一個tiny react(三)生命週期

從0實現一個tiny react(三)生命週期

在給tinyreact加生命週期以前,先考慮 組件實例的複用 這個前置問題html

複用組件實例

render函數 只能返回一個根node

class A extends Component{
    render() {
        return (<B>...</B>)  
    }
}
class C extends Component {
    render() {
        return (
            <div>
               <C1>...</C1>
               <C2>...</C2>
               <C3>...</C3>
            </div>
        )
    }
}

因此 最終的組件樹必定是相似這種的 (首字母大寫的表明組件, div/span/a...表明原生DOM類型)
圖片描述react

是絕對不可能 出現下圖這種樹結構 (與render函數返回單根的特性矛盾)git

圖片描述

注意 __rendered引用 指向了一個inst/dom。 因此能夠經過__rendered來複用實例。
下面咱們討論怎麼根據__rendered 複用instgithub

假如在 Father裏面調用 setState? 按照如今render 函數的作法:npm

else if (typeof vnode.nodeName == "function") {
    let func = vnode.nodeName
    let inst = new func(vnode.props)
    ...
}
  1. 新建 Son 實例
  2. 新建 Grandson 實例
  3. diff 渲染 div

再次setState呢? 好吧, 再來一次:segmentfault

  1. 新建 Son 實例
  2. 新建 Grandson 實例
  3. diff 渲染 div

第 3步 就是 (二) 討論的內容, 會用"最少"的dom操做, 來更新dom到最新的狀態。
對於1, 2 每次setState的時候都會新建inst, 在這裏是能夠複用以前建立好的inst實例的。 數組

可是若是一個組件 初始渲染爲 '<A/>', setState 以後渲染爲 '<B/>' 這種狀況呢? 那inst就不能複用了, 類比一下 DOM 裏的 div --> span
。 把render 第四個參數 old ---> olddomOrComp , 經過這個參數來判斷 dom 或者inst 是否能夠複用:app

//inst 是否能夠複用
function render (vnode, parent, comp, olddomOrComp) {
    ...
    } else if(typeof vnode.nodeName === "string") {
        if(!olddomOrComp || olddomOrComp.nodeName !== vnode.nodeName.toUpperCase()) { // <--- dom 能夠複用
             createNewDom(vnode, parent, comp, olddomOrComp, myIndex)
         }
    ...     
    } else if (typeof vnode.nodeName == "function") {
        let func = vnode.nodeName
        let inst
        if(olddomOrComp && olddomOrComp instanceof func) { // <--- inst 能夠複用 
            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

  1. 判斷dom是否能夠複用
  2. parent.replaceChild(dom, olddom), olddom肯定了新的dom的位置
    而 olddomOrComp 是作不到第二點。 即便: parent.replaceChild(dom, getDOM(olddomOrComp)) 也是不行的。 緣由是:

假如初始 CompA --> <Sub1/> setState後 CompA --> <Sub2/>, 那麼inst 不能夠複用, inst.__rendered 是undefined, 就從replaceChild變成了appendChild

怎麼解決呢? 引入第5個參數 myIndex: dom的位置問題都交給這個變量。 olddomOrComp只負責決定 複用的問題

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)              // <--- 根據myIndex設置 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)         // <--- 根據myIndex設置 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裏面調用 setState。 此時已經不會建立新實例了。

那麼 假如如今對 Grandson調用setState呢? 很不幸, 咱們須要建立Granssonson1, Granssonson2, Granssonson3, 調用幾回, 咱們就得跟着新建幾回。
上面的複用方式 並無解決這個問題, 以前 __rendered 引用鏈 到 dom就結束了。
<br/>把__rendered這條鏈 完善吧!!

首先 對__rendered 從新定義以下:

  1. 當X 是組件實例的時候, __rendered 爲X渲染出的 組件實例 或者 dom元素
  2. 當X 是dom元素的時候, __rendered 爲一個數組, 是X的子組件實例 或者 子dom元素
Father --__rendered--> Son  --__rendered--> Grandson --__rendered--> div --__rendered--> [Granssonson1,  Granssonson2, Granssonson3,]

在dom 下建立 "直接子節點" 的時候。 須要把這個紀錄到dom.__rendered 數組中。 或者說, 若是新建的一個dom元素/組件實例 是dom的 "直接子節點", 那麼須要把它紀錄到
parent.__rendered 數組中。 那怎麼判斷 建立出來的是 "直接子節點" 呢? 答案是render 第3個參數 comp爲null的, 很好理解, comp的意思是 "誰渲染了我"
很明顯, 只有 dom下的 "直接子節點" comp纔是null, 其餘的狀況, comp確定不是null, 好比 Son的comp是Father, Gsss1
的comp是Grandsonson1。。。

而且當setState從新渲染的時候, 若是老的dom/inst沒有被複用, 則應該用新的dom/inst 替換

  1. 建立dom的時候。
function createNewDom(vnode, parent, comp, olddomOrComp, myIndex) {
    ...
    if (comp) {
        comp.__rendered = dom
    } else {
        parent.__rendered[myIndex] = dom
    }
    ...
}
  1. 組件實例
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
        }
    }
    ...
}
  1. diffDOM 的時候: a. remove多餘的節點; b. render子節點的時候olddomOrComp = olddom.__rendered[i]
function diffDOM(vnode, parent, comp, olddom) {
    ...
    olddom.__rendered.slice(vnode.children.length)  // <--- 移除多餘 子節點
        .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 = []  // 建立dom的 設置 __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
        }
    }
}

圖片描述

如今 __rendered鏈 完善了, setState觸發的渲染, 都會先去嘗試複用 組件實例。 在線演示

生命週期

前面討論的__rendered 和生命週期有 什麼關係呢? 生命週期是組件實例的生命週期, 以前的工做起碼保證了一點: constructor 只會被調用一次了吧。。。
後面討論的生命週期 都是基於 "組件實例"的 複用纔有意義。tinyreact 將實現如下的生命週期:

  1. componentWillMount
  2. componentDidMount
  3. componentWillReceiveProps
  4. shouldComponentUpdate
  5. componentWillUpdate
  6. componentDidUpdate
  7. componentWillUnmount
    他們 和 react同名函數 含義相同

componentWillMount, componentDidMount, componentDidUpdate

這三個生命週期 是如此之簡單: componentWillMount緊接着 建立實例的時候調用; 渲染完成以後,若是
組件是新建的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

當組件 獲取新的props的時候, 會調用componentWillReceiveProps, 參數爲newProps, 而且在這個方法內部this.props 仍是值向oldProps,
因爲 props的改變 由 只能由 父組件 觸發。 因此只用在 render函數裏面處理就ok。不過 要在 inst.props = vnode.props 以前調用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 以前調用
            
            inst.props = vnode.props
        } else {
            ...
        }
    }

當 組件的 props或者state發生改變的時候,組件必定會渲染嗎?shouldComponentUpdate說了算!! 若是組件沒有shouldComponentUpdate這個方法, 默認是渲染的。
不然是基於 shouldComponentUpdate的返回值。 這個方法接受兩個參數 newProps, newState 。
另外因爲 props和 state(setState) 改變都會引發 shouldComponentUpdate調用, 因此:

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 以前調用
                
                let shoudUpdate
                if(inst.shouldComponentUpdate) {
                    shoudUpdate = inst.shouldComponentUpdate(vnode.props, olddomOrComp.state) // <-- 在 inst.props = vnode.props 以前調用
                } 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() // <-- 須要調用下: componentDidUpdate
    }, 0)
}

當 shoudUpdate 爲false的時候呢, 直接return 就ok了, 可是shoudUpdate 爲false 只是代表 不渲染, 可是在 return以前, newProps和newState必定要設置到組件實例上。
<br/> setState render以後 也是須要調用: componentDidUpdate

當 shoudUpdate == true 的時候。 會調用: componentWillUpdate, 參數爲newProps和newState。 這個函數調用以後,就會把nextProps和nextState分別設置到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 以前調用
    } else {
        shoudUpdate = true
    }
    shoudUpdate && inst.componentWillUpdate && inst.componentWillUpdate(vnode.props, olddomOrComp.state)   // <-- 在 inst.props = vnode.props 以前調用   
    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 以前調用
        this.state = state
        if (!shoudUpdate) {  // <-- 在  this.state = state  以後
            return // do nothing just return
        }
        ...
}

componentWillUnmount

當組件要被銷燬的時候, 調用組件的componentWillUnmount。 inst沒有被複用的時候, 要銷燬。 dom沒有被複用的時候, 也要銷燬, 並且是樹形結構
的遞歸操做。 有點像 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 是這樣的一個 遞歸函數:

  • 當domOrComp 爲組件實例的時候, 首先調用:componentWillUnmount, 而後 recoveryDomOrComp(inst.__rendered) 。 這裏的前後順序關係很重要
  • 當domOrComp 爲DOM節點 (非文本 TextNode), 遍歷 recoveryDomOrComp(子節點)
  • 當domOrComp 爲TextNode,nothing...
    與render同樣, 因爲組件 最終必定會render html的標籤。 因此這個遞歸必定是可以正常返回的。

哪些地方須要調用recoveryComp ?

  1. 全部olddomOrComp 沒有被複用的地方。 由於一旦olddomOrComp 不被複用, 必定有一個新的取得它, 它就要被銷燬
  2. 多餘的 子節點。 div 起初有3個子節點, 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(<App/>, root)

爲了避免要在 調用render以前 設置:__rendered 作個小的改動 :

/**
 * 渲染vnode成實際的dom
 * @param vnode 虛擬dom表示
 * @param parent 實際渲染出來的dom,掛載的父元素
 */
export default function render(vnode, parent) {
    parent.__rendered =[]  //<--- 這裏設置 __rendered
    renderInner(vnode, parent, null, null, 0)
}

function renderInner(vnode, parent, comp, olddomOrComp, myIndex) {
    ...
}

其餘

tinyreact 未實現功能:

  1. context
  2. 事件代理
  3. 多吃調用setState, 只render一次
  4. react 頂層Api
  5. 。。。

tinyreat 有些地方參考了preact

npm包:

npm install tinyreact --save

全部代碼託管在git example 目錄下有blog中的例子

經典的TodoList。 項目 代碼

相關文章
相關標籤/搜索