react源碼分析——自己實現diff算法

在前面我們實現了_render方法,就是把虛擬dom轉換爲真實的dom,下面我們需要優化一下這個方法,不要讓它傻乎乎的渲染整個dom樹,對比需要變化的地方,只渲染需要變化的地方,這樣的過程,就是diff算法。

對比當前真實的dom跟虛擬的dom,一邊對比一邊更新。只對比同一級的dom。

主要是拿真實的dom跟虛擬的dom對比。我們之前的_render方法,就沒有那麼好用了,現在我們來實現一下diff方法

react-dom/diff.js

/**
 * 
 * @param {*} dom 真實的dom
 * @param {*} vnode 虛擬dom
 * @param {*} container 
 */

export function diff(dom, vnode, container) {
    // 對比節點的變化
    const ret = diffNode(dom, vnode)
    if(container) container.appendChild(ret);
    return ret;
}

跟render方法很像,都是最後一步才把生成的dom樹,添加到html中,ret應該是我們需要dom元素,與_render方法不同的是,這裏的dom元素是我們對比更新過的,找個最小的需要更新的單元,更新後生成的dom元素。這裏的核心就在diffNode方法的實現。

第一個參數就是真實的dom,第二個參數是虛擬的dom

就是類似上面的結構。

jsx就是下面的這種數據結構

(<div className='active'>
   1
   <h1>react</h1>
   <button key='9' onClick={this.handlerClick.bind(this)}>改變狀態 </button>
 </div>)

可以看到,tag:div的這個虛擬dom還有一個children是一個數組,分別點開發現,結構類似最外層的虛擬dom,實時上,它們也是虛擬的dom對象,可以看到,虛擬的dom對象可能是數值,也可能是對象,也可能是字符串。

如果說是數值,就是一個文本節點,但是這裏需要轉換爲字符串;

如果說字符串,也是一個文本節點;

如果說是對象,就是一個標籤,帶有屬性,或者還帶有children,子節點;

還有一種是函數,也就是我們的函數組件,留到下面再說。

先來看下前2種

export function diffNode(dom, vnode) {
    let out = dom;
    if(vnode === undefined || vnode === null || typeof vnode === 'boolean') return document.createTextNode('');
    // 如果是數值
    if(typeof vnode === 'number') vnode = String(vnode)
    // 如果是字符串
    if(typeof vnode === 'string') {
        // 是文本節點
        if(dom && dom.nodeType === 3) {
            // 如果真是的文本跟虛擬的文本不相等,需要更新
            if(dom.textContent !== vnode) {
                // 更新文本內容
                dom.textContent = vnode
            }
        } else {
           out = document.createTextNode(vnode);
           if(dom && dom.parentNode) {
               dom.parentNode.replaceNode(out, dom)
           }
        }
        return out;
    }
}
  • 首先需要判斷虛擬dom的類型,如果說vnode不存在,就可以返回一個空的文本節點,注意,這裏要是一個文本節點,不然後面在插入dom 的時候會報錯。
  • 然後如果是數值直接轉換爲字符串
  • 如果是字符串,我們需要看下有有真實的dom
    • 有,說明不是第一次渲染,是第n次的更新。需要確認一個dom的節點類型,3說明是問文本節點,這個時候需要對比一個真實dom的文本節點,其實也就是文本內容,是否等於傳遞進來的虛擬dom的文本內容,這時的vnode是一個字符串,如果不等,直接替換就行
    • 如果不是文本節點,dom不存在,這時需要把傳遞進來的字符串變成文本節點,然後返回,或者dom存在的話,把這個位置的節點替換掉。
  • 如果是對象。那麼必然會有一個dom元素,看下面的兩種情況對比:

如果是串jsx

const ele = (
    <div className='active'>
        <h1>nihao</h1>
        <p>內容</p>
    </div>
)
console.log(ele)

 

如果是一個組件

import React from './react'
import ReactDOM  from './react-dom'

class Home extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            num: 1
        }
    }
  
    handlerClick() {
        this.setDate({
            num: this.state.num + 1,
        })
    }
    render() {
        return (
            <div className='active'>
              1
              <h1>react</h1>
              <button key='9' onClick={this.handlerClick.bind(this)}>改變狀態</button>
            </div>
        )
    }
}
ReactDOM.render(<Home title='home' />, document.getElementById('app'));

由上面的對比可以看出,tag是不一樣的,tag是函數的時候,渲染的是一個組件。所以代碼需要做一下區分: 

export function diffNode(dom, vnode) {
    let out = dom;
    if(vnode === undefined || vnode === null || typeof vnode === 'boolean') return document.createTextNode('');
    // 如果是數值
    if(typeof vnode === 'number') vnode = String(vnode)
    // 如果是字符串
    if(typeof vnode === 'string') {
        if(dom && dom.nodeType === 3) {
            if(dom.textContent !== vnode) {
                // 更新文本內容
                dom.textContent = vnode
            }
        } else {
           out = document.createTextNode(vnode);
           if(dom && dom.parentNode) {
               dom.parentNode.replaceNode(out, dom)
           }
        }
        return out;
    }
    if(typeof vnode.tag === 'function') {
       // tag是函數
    }
    // 非文本dom節點

    if(!dom) {
        out = document.createElement(vnode.tag)
    }
   // 對比並更改屬性
    diffAttribute(out, vnode)
    return out;
}

先不看是函數的情況,如果說是一串jsx我們需要對比tag標籤屬性,如果dom不存在,我們創建一個dom(第一次渲染的時候),如果dom存在,我們需要對比這兩個dom之間的屬性是否一致,這裏就交給diffAttribute方法,就是拿到dom的屬性跟vnode中的attrs做下對比,一致就不做改變,不一致就改變,有則改,無則移除。

怎麼拿到一個真是dom的屬性,看下下面打印:

<body>
    <div id='app' data-uri= '892' style="color: black"></div>
</body>
<script>
    var app = document.getElementById('app');
    console.log(app.attributes);
</script>

得到一個類數組,屬性值是dom元素上的屬性。 

 

    var app = document.getElementById('app');
    var domAttrs = app.attributes;
    console.log([...domAttrs].forEach(item => {console.log(item.name, item.value)}));

可以拿到屬性和屬性值,這樣就可以拿到真是dom上所有的屬性了。

 

function diffAttribute(dom, vnode) {
    // dom原來的節點,vnode虛擬的節點
    const oldAttris = {};
    const newAttris = vnode.attrs;
    const domAttrs = dom.attributes;
    [...domAttrs].forEach(item => {
        oldAttris[item.name] = item.value
    })
    // 對比屬性,老屬性不在新屬性中,移除老的屬性
    for(let key in oldAttris) {
        if(!(key in  newAttris)) {
            setArribute(dom, key, undefined)
        }
    }
    // 屬性不一致,重置屬性
    for(let key in newAttris) {
        if(oldAttris[key] !== newAttris[key]) {
            setArribute(dom, key, newAttris[key])
        }
    }
}

 setArribute方法不需要做調整,還是之前寫好的。

export function setArribute(dom, key, value) {
    // class
    if(key === 'className') {
        key = 'class';
    }
    // 事件
    if(/on\w+/.test(key)) {
        key = key.toLowerCase()
        dom[key] = value || ''
    } else if(key === 'style') { // 樣式
        if(!value || typeof value === 'string') {
            dom.style.cssText = value || ''
        } else if(value && typeof value === 'object') {
            for(let k in value) {
                if(typeof value[k] === 'number') {
                    dom.style[k] = value[k] + 'px'
                } else {
                    dom.style[k] = value[k] 
                }
            }
        }
    } else {
        if(key in dom) {
            dom[key] = value;
        }
        if(value) {
            dom.setAttribute(key,value)
        } else {
            dom.removeAttribute(key)
        }
    }

}

這裏我們處理完了,tag和attrs,可以看到vnode中可能還會有childrem,是一個數組,是tag元素的子節點形成的虛擬dom,這裏的dom也是需要我們做對比生成的。diffNode方法做下面調整

if(!dom) {
        out = document.createElement(vnode.tag)
    }

if(vnode.children && vnode.children.length>0 || (out.childNodes && out.childNodes.length>0)) {
    diffChildren(out, vnode.children)
 }
// 對比並更改屬性
diffAttribute(out, vnode)
return out;

子節點的比較交給diffChildren方法來處理,就是拿tag標籤所包含的子節點,跟新生成vnode中的children做對比。

react中有一個很關鍵的提升性能的點,就是key,因爲react是以key來區分組件和元素的,如果有key會提高效率。

在我們不知一個元素下面有多少子節點的情況下,會通過原生方法獲取所有的子節點,然後再循環所有的子節點,如果說有key 的話,我們只需要找到相應的key,然後拿有相同key的元素取比較就可以了。

這裏在創建虛擬dom的時候,需要加上key

import Component from './component'

const React = {
    createElement,
    Component
}
function createElement(tag, attrs, ...children) {
    attrs = attrs || {}
    return {
        tag,
        attrs,
        children,
        key: attrs.key || null
    }
}
export default React

然後拿到dom首先把有key 跟 沒有 key 的區分開。 

function diffChildren(dom, vChildren) {
    const domChildren = dom.childNodes;
    const children = [];
    const keyed = {};
    // 把有key 的dom 跟沒有key 的dom 區分開
    if(domChildren && domChildren.length > 0) {
        domChildren.forEach((domChild) => {
            const key = domChild.getAttribute && domChild.getAttribute('key');
            if(key) keyed[key] = domChild;
            else children.push(domChild);
        })
    }
}

然後再遍歷vnode中的children,如果虛擬dom中也存在key,就去取相應的真實com,然後diffNode對比。如果沒有key,需要取真實的dom,從第一個開始取,每取到一個跳出循環,diffNode對比。對比完成後,再來進行增刪改的操作。

function diffChildren(dom, vChildren) {
    const domChildren = dom.childNodes;
    const children = [];
    const keyed = {};
    // 把有key 的dom 跟沒有key 的dom 區分開
    if(domChildren && domChildren.length > 0) {
        domChildren.forEach((domChild) => {
            const key = domChild.getAttribute && domChild.getAttribute('key');
            if(key) keyed[key] = domChild;
            else children.push(domChild);
        })
    }
    if(vChildren && vChildren.length > 0) {
        let min = 0;
        let childrenLen = children.length;
        [...vChildren].forEach((vchild,i) => {
            // 拿到虛擬dom中的key
            const key = vchild.key;
            let child;
            if(key) {
                if(keyed[key]) {
                    child = keyed[key];
                    keyed[key] = undefined;
                }
            } else if(childrenLen > min) {
                for(let j = min; j < childrenLen; j++) {
                    let c = children[j];
                    if(c) {
                        child = c;
                        children[j] = undefined;
                        if(j === childrenLen -1) childrenLen --;
                        if(j === min) min++;
                        break;
                    }
                }
            }

            child = diffNode(child, vchild);

            const f = domChildren[i]
            if(child && child !== dom && child !== f) {
                if(!f){
                    dom.appendChild(child);
                } else if (child === f.nextSibling) {
                    removeNode()
                } else {
                    dom.insertBefore(child, f)
                }
            }
        })
    }

}

到這裏我們已經把tag是dom元素的情況分析完成了。

還有一中情況,就是tag是函數的情況:我們可以先思考一下,如果說更新的是組件,如果組件改變了,react會卸載組件,然後加載新的組件,所以這一步就是組件的對比,卸載,加載的過程。

if(typeof vnode.tag === 'function') {
    return diffComponet(out, vnode)
 }

對比組件有沒有發生變化,主要是對比構造函數有沒有變; 


function diffComponet(dom, vnode) {
    // 如果組件沒有變化
    let comp = dom;
    if(comp && comp.constructor === vnode.tag) {
        // 重新設置屬性
        setComeponentProps(comp, vnode.attrs);
        dom = comp.base
    } else {
        // 組件發生了變化
        if(comp) {
            // 先移除舊的組件
            unmountComponnet(comp)
            comp = null
        }
        // 1.創建新的組件
        comp = createComponent(vnode.tag, vnode.attrs)
        // 2.設置組件屬性
        setComeponentProps(comp, vnode.attrs)
        // 3.給當前組件掛base
        dom = comp.base
    }
    return dom;
}
function unmountComponnet(comp) {
    removeNode(comp.base)
}
function removeNode(dom) {
    if(dom && dom.parentNode) {
        dom.parentNode.removeNode(dom)
    }
}

我們的react/index.js中,render函數不再調用_render了,而是

export function render(v, container, dom) {
     diff(dom, v, container)
}

 渲染組件的過程,換成了diffNode來執行。

export function renderComponent(comp) {
    // v虛擬的dom對象
    const v = comp.render();
    const base = diffNode(comp.base,v);
    if(comp.base && comp.componentWillUpdate) comp.componentWillUpdate();
    if(comp.base && comp.componentDidUpdate) comp.componentDidUpdate();
    else if (comp.componentDidMount) comp.componentDidMount();
    // if(comp.base && comp.base.parentNode) {
    //     comp.base.parentNode.replaceChild(base, comp.base)
    // }
    // 生成真實的dom
    comp.base = base;
}

這樣就完成了react的diff算法更新dom。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章