從0實現一個tiny react(二)

從0實現一個tiny react(二)

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

複用DOM

回想一下, 在 (一) 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

原則以下:瀏覽器

  • 不一樣元素類型是沒法複用的, span 是沒法變成 div的。
  • 對於相同元素: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件事情。

  1. renderBefore 和 renderAfter 的屬性交集 若是值不一樣, 更新值 updateAttr
  2. renderBefore 和 renderAfter 的屬性差集 置空 removeAttr
  3. renderAfter 和 renderBefore 的屬性差集 設置新值 setAttr
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操做。

複用子節點 - key

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

  • vnode是字符串, 若是存在olddom, 且能夠複用, 複用之。不然建立textNode節點
  • 當vnode.nodeName是 字符串的時候, 若是存在olddom, 且能夠複用, 複用之。不然建立dom節點, 根據props設置節點屬性, 遍歷render children
  • 當vnode.nodeName是 function的時候, 獲取render方法的返回值 vnode', 執行render(vnode')

代碼git地址

相關文章

相關文章
相關標籤/搜索