ui = f(d)! 这是react考虑ui的方式,开发者能够把重心放到d 数据上面来了。 从开发者的角度来说 d一旦改变,react将会把ui从新渲染,使其再次知足
ui = f(d), 开发者没有任何dom操做, 交给react就好!!css
怎么从新渲染呢? (一)文 中咱们实现了一种方式, state改变的时候,用新的dom树替换一下老的dom树, 这是彻底可行的。
考虑一下这个例子 在线演示地址:html
class AppWithNoVDOM extends Component { constructor(props) { super(props) } testApp3() { let result = [] for(let i = 0; i < 10000 ; i++) { result.push(<div style={{ width: '30px', color: 'red', fontSize: '12px', fontWeight: 600, height: '20px', textAlign: 'center', margin:'5px', padding: '5px', border:'1px solid red', position: 'relative', left: '10px', top: '10px', }} title={i} >{i}</div>) } return result } render() { return ( <div width={100}> <a onClick={e => { this.setState({}) }}>click me</a> {this.testApp3()} </div> ) } } const startTime = new Date().getTime() render(<App/>, document.getElementById("root")) console.log("duration:", new Date().getTime() - startTime) ... setState(state) { setTimeout(() => { this.state = state const vnode = this.render() let olddom = getDOM(this) const startTime = new Date().getTime() render(vnode, olddom.parentNode, this, olddom) console.log("duration:", new Date().getTime() - startTime) }, 0) } ...
咱们在 render, setState 设置下时间点。 在10000万个div的状况下, 第一次render和setState触发的render 耗时大概在180ms (可能跟机器配置有关)
当点击的时候, 因为调用this.setState({})
, 页面将会从新渲染, 再次创建10000万个div, 可是实际上这里的DOM一点也没改。
应用越复杂, 无用功越多,卡顿越明显node
为了解决这个问题, react提出了virtual-dom的概念:vnode(纯js对象) '表明' dom, 在渲染以前, 先比较出oldvnode和newvode的 区别。 而后增量的
更新dom。 virtual-dom 使得ui=f(d) 得以在实际项目上使用。
(注意: virtual-dom 并不会加快应用速度, 只是让应用在不直接操做dom的状况下,经过暴力的比较,增量更新 让应用没有那么慢)react
如何增量更新呢?git
回想一下, 在 (一) render函数 里面对于每个断定为 dom类型的VDOM, 是直接建立一个新的DOM:github
... else if(typeof vnode.nodeName == "string") { dom = document.createElement(vnode.nodeName) ... } ...
必定要建立一个 新的DOM 结构吗?<br/>
考虑这种状况:假如一个组件, 初次渲染为 renderBefore, 调用setState再次渲染为 renderAfter 调用setState再再次渲染为 renderAfterAfter。 VNODE以下redux
const renderBefore = { tagName: 'div', props: { width: '20px', className: 'xx' }, children:[vnode1, vnode2, vnode3] } const renderAfter = { tagName: 'div', props: { width: '30px', title: 'yy' }, children:[vnode1, vnode2] } const renderAfterAfter = { tagName: 'span', props: { className: 'xx' }, children:[vnode1, vnode2, vnode3] }
renderBefore 和renderAfter 都是div, 只不过props和children有部分区别,那咱们是否是能够经过修改DOM属性, 修改DOM子节点,把 rederBefore 变化为renderAfter呢?, 这样就避开了DOM建立。 而 renderAfter和renderAfterAfter
属于不一样的DOM类型, 浏览器还没提供修改DOM类型的Api,是没法复用的, 是必定要建立新的DOM的。segmentfault
原则以下:浏览器
对于相同元素:react-router
因此,如今的代码多是这样的:
... else if(typeof vnode.nodeName == "string") { if(!olddom || olddom.nodeName != vnode.nodeName.toUpperCase()) { createNewDom(vnode, parent, comp, olddom) } else { diffDOM(vnode, parent, comp, olddom) // 包括 更新属性, 子节点复用 } } ...
对于 renderBefore => renderAfter 。 属性部分须要作3件事情。
const {onlyInLeft, bothIn, onlyInRight} = diffObject(newProps, oldProps) setAttrs(olddom, onlyInLeft) removeAttrs(olddom, onlyInRight) diffAttrs(olddom, bothIn.left, bothIn.right) function diffObject(leftProps, rightProps) { const onlyInLeft = {} const bothLeft = {} const bothRight = {} const onlyInRight = {} for(let key in leftProps) { if(rightProps[key] === undefined) { onlyInLeft[key] = leftProps[key] } else { bothLeft[key] = leftProps[key] bothRight[key] = rightProps[key] } } for(let key in rightProps) { if(leftProps[key] === undefined) { onlyInRight[key] = rightProps[key] } } return { onlyInRight, onlyInLeft, bothIn: { left: bothLeft, right: bothRight } } } function setAttrs(dom, props) { const allKeys = Object.keys(props) allKeys.forEach(k => { const v = props[k] if(k == "className") { dom.setAttribute("class", v) return } if(k == "style") { if(typeof v == "string") { dom.style.cssText = v //IE } if(typeof v == "object") { for (let i in v) { dom.style[i] = v[i] } } return } if(k[0] == "o" && k[1] == "n") { const capture = (k.indexOf("Capture") != -1) dom.addEventListener(k.substring(2).toLowerCase(), v, capture) return } dom.setAttribute(k, v) }) } function removeAttrs(dom, props) { for(let k in props) { if(k == "className") { dom.removeAttribute("class") continue } if(k == "style") { dom.style.cssText = "" //IE continue } if(k[0] == "o" && k[1] == "n") { const capture = (k.indexOf("Capture") != -1) const v = props[k] dom.removeEventListener(k.substring(2).toLowerCase(), v, capture) continue } dom.removeAttribute(k) } } /** * 调用者保证newProps 与 oldProps 的keys是相同的 * @param dom * @param newProps * @param oldProps */ function diffAttrs(dom, newProps, oldProps) { for(let k in newProps) { let v = newProps[k] let ov = oldProps[k] if(v === ov) continue if(k == "className") { dom.setAttribute("class", v) continue } if(k == "style") { if(typeof v == "string") { dom.style.cssText = v } else if( typeof v == "object" && typeof ov == "object") { for(let vk in v) { if(v[vk] !== ov[vk]) { dom.style[vk] = v[vk] } } for(let ovk in ov) { if(v[ovk] === undefined){ dom.style[ovk] = "" } } } else { //typeof v == "object" && typeof ov == "string" dom.style = {} for(let vk in v) { dom.style[vk] = v[vk] } } continue } if(k[0] == "o" && k[1] == "n") { const capture = (k.indexOf("Capture") != -1) let eventKey = k.substring(2).toLowerCase() dom.removeEventListener(eventKey, ov, capture) dom.addEventListener(eventKey, v, capture) continue } dom.setAttribute(k, v) } }
'新'的dom结构 属性和 renderAfter对应了。<br/>
可是 children部分 仍是以前的
以前 操做子节点的代码:
for(let i = 0; i < vnode.children.length; i++) { render(vnode.children[i], dom, null, null) }
render 的第3个参数comp '谁渲染了我', 第4个参数olddom '以前的旧dom元素'。如今复用旧的dom, 因此第4个参数多是有值的 代码以下:
let olddomChild = olddom.firstChild for(let i = 0; i < vnode.children.length; i++) { render(vnode.children[i], olddom, null, olddomChild) olddomChild = olddomChild && olddomChild.nextSibling } //删除多余的子节点 while (olddomChild) { let next = olddomChild.nextSibling olddom.removeChild(olddomChild) olddomChild = next }
综上所述 完整的diffDOM 以下:
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) let olddomChild = olddom.firstChild for(let i = 0; i < vnode.children.length; i++) { render(vnode.children[i], olddom, null, olddomChild) olddomChild = olddomChild && olddomChild.nextSibling } while (olddomChild) { //删除多余的子节点 let next = olddomChild.nextSibling olddom.removeChild(olddomChild) olddomChild = next } olddom.__vnode = vnode }
因为须要在diffDOM的时候 从olddom获取 oldVNODE(即 diffObject(vnode.props, olddom.__vnode.props))。 因此:
// 在建立的时候 ... let dom = document.createElement(vnode.nodeName) dom.__vnode = vnode ... // diffDOM ... const {onlyInLeft, bothIn, onlyInRight} = diffObject(vnode.props, olddom.__vnode.props) ... olddom.__vnode = vnode // 更新完以后, 须要把__vnode的指向 更新 ...
另外 对于 TextNode的复用:
... if(typeof vnode == "string" || typeof vnode == "number") { if(olddom && olddom.splitText) { if(olddom.nodeValue !== vnode) { olddom.nodeValue = vnode } } else { dom = document.createTextNode(vnode) if(olddom) { parent.replaceChild(dom, olddom) } else { parent.appendChild(dom) } } } ...
从新 跑一下开头 的例子 新的复用DOM演示 setState后渲染时间变成了 20ms 左右。 从 180ms 到20ms 差很少快有一个数量级的差距了。
到底快了多少,取决于先后结构的类似程度, 若是先后结构基本相同,diff是有意义的减小了DOM操做。
初始渲染 ... render() { return ( <div> <WeightCompA/> <WeightCompB/> <WeightCompC/> </div> ) } ... setState再次渲染 ... render() { return ( <div> <span>hi</span> <WeightCompA/> <WeightCompB/> <WeightCompC/> </div> ) } ...
咱们以前的子节点复用顺序就是按照DOM顺序, 显然这里若是这样处理的话, 可能致使组件都复用不了。 针对这个问题, React是经过给每个子组件提供一个 "key"属性来解决的
对于拥有 一样key的节点, 认为结构相同。 因此问题变成了:
f([{key: 'wca'}, {key: 'wcb}, {key: 'wcc}]) = [{key:'spanhi'}, {key: 'wca'}, {key: 'wcb}, {key: 'wcc}]
函数f 经过删除, 插入操做,把olddom的children顺序, 改成和 newProps里面的children同样 (按照key值同样)。相似与 字符串距离,
对于这个问题, 我将会另开一篇文章
经过 diff 比较渲染先后 DOM的差异来复用实际的, 咱们的性能获得了提升。如今 render方法的描述: <br/>
render 方法是根据的vnode, 渲染到实际的dom,若是存在olddom会先尝试复用的 一个递归方法 (因为组件 最终必定会render html的标签。 因此这个递归必定是可以正常返回的)