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的標籤。 因此這個遞歸必定是可以正常返回的)