在给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。 项目 代码