在給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) ... }
再次setState呢? 好吧, 再來一次:segmentfault
第 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
假如初始 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 從新定義以下:
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 替換
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 } } ... }
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 將實現如下的生命週期:
這三個生命週期 是如此之簡單: 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() } }
當組件 獲取新的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。 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 是這樣的一個 遞歸函數:
哪些地方須要調用recoveryComp ?
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 未實現功能:
tinyreat 有些地方參考了preact
npm包:
npm install tinyreact --save
全部代碼託管在git example 目錄下有blog中的例子
經典的TodoList。 項目 代碼