preact源碼學習(3)

這是說preact的diff機制。preact在diff的過程當中建立,更新與移除真實DOM。diff機制是preact中最難懂的部分。javascript

咱們先看render方法。java

//render.js
import { diff } from './vdom/diff';

export function render(vnode, parent, merge) {
    return diff(merge, vnode, {}, false, parent, false);
}

vnode爲虛擬DOM,parent爲做爲容器的元素節點,merge是另外一個真實DOM,但也可能不存在。從這個render方法,咱們能夠看到,它與官方React出入比較大,由於官方react的render第三個參數是回調。node

//用於收集那些等待被調用componentDidMount回調的組件
export const mounts = [];

//斷定遞歸的層次
export let diffLevel = 0;
//斷定當前的DOM樹是否爲SVG
let isSvgMode = false;

//斷定這個元素是否已經緩存了以前的虛擬DOM數據
let hydrating = false;
//批量觸發componentDidMount與afterMount
export function flushMounts() {
    let c;
    while ((c=mounts.pop())) {
        if (options.afterMount) options.afterMount(c);
        if (c.componentDidMount) c.componentDidMount();
    }
}

export function diff(dom, vnode, context, mountAll, parent, componentRoot) {
    if (!diffLevel++) {
        //從新斷定DOM樹的類型
        isSvgMode = parent!=null && parent.ownerSVGElement!==undefined;

        // 斷定是否緩存了數據
        hydrating = dom!=null && !(ATTR_KEY in dom);
    }
    //更新dom 或返回新的dom
    let ret = idiff(dom, vnode, context, mountAll, componentRoot);

    // 插入父節點
    if (parent && ret.parentNode!==parent) parent.appendChild(ret);

    if (!--diffLevel) {
        hydrating = false;
        // 執行全部DidMount鉤子
        if (!componentRoot) flushMounts();
    }

    return ret;
}

從用戶通常的使用來看,傳到diff裏面的參數通常是react

diff(undefined, vnode, {}, false, parent, false);

它的參數嚴重不足,咱們再看idiff。設計模式

function idiff(dom, vnode, context, mountAll, componentRoot) {
    let out = dom,
        prevSvgMode = isSvgMode;

    // 轉換null, undefined, boolean爲空字符
    if (vnode==null || typeof vnode==='boolean') vnode = '';
    //將字符串與數字轉換爲文本節點
    if (typeof vnode==='string' || typeof vnode==='number') {

        // 若是已經存在,注意在IE6-8下,文本節點是不能添加自定義屬性,所以dom._component老是爲undefined
        if (dom && dom.splitText!==undefined && dom.parentNode && (!dom._component || componentRoot)) {
            
            if (dom.nodeValue!=vnode) {
                dom.nodeValue = vnode;
            }
        }
        else {
            // 建立新的虛擬DOM
            out = document.createTextNode(vnode);
            if (dom) {
                if (dom.parentNode) dom.parentNode.replaceChild(out, dom);
                recollectNodeTree(dom, true);
            }
        }

        out[ATTR_KEY] = true;

        return out;
    }


    // 若是是組件
    let vnodeName = vnode.nodeName;
    if (typeof vnodeName==='function') {
        return buildComponentFromVNode(dom, vnode, context, mountAll);
    }


    // 更新isSvgMode
    isSvgMode = vnodeName==='svg' ? true : vnodeName==='foreignObject' ? false : isSvgMode;


    //這個應該是防護性代碼,由於到這裏都是div, p, span這樣的標籤名
    vnodeName = String(vnodeName);
    //若是沒有DOM,或標籤類型不一致
    if (!dom || !isNamedNode(dom, vnodeName)) {
        out = createNode(vnodeName, isSvgMode);

        if (dom) {
            // 轉移裏面的真實DOM
            while (dom.firstChild) out.appendChild(dom.firstChild);

            // 插入到父節點
            if (dom.parentNode) dom.parentNode.replaceChild(out, dom);

            // GC
            recollectNodeTree(dom, true);
        }
    }


    let fc = out.firstChild,
    //取得以前的虛擬DOM的props
        props = out[ATTR_KEY],
        vchildren = vnode.children;

    if (props==null) {
        //將元素節點的attributes轉換爲props,方便進行比較
        //不過這裏有一個致命的缺憾在IE6-7中,由於IE6-7不區分attributes與property,這裏會存在大量的屬性,致使巨耗性能
        props = out[ATTR_KEY] = {};
        for (let a=out.attributes, i=a.length; i--; ) props[a[i].name] = a[i].value;
    }

    // Optimization: fast-path for elements containing a single TextNode:
    // 若是當前位置的真實DOM 是文本節點,並無緩存任何數據,而虛擬DOM 則是一個字符串,那麼直接修改nodeValue
    if (!hydrating && vchildren && vchildren.length===1 && typeof vchildren[0]==='string' && fc!=null && fc.splitText!==undefined && fc.nextSibling==null) {
        if (fc.nodeValue!=vchildren[0]) {
            fc.nodeValue = vchildren[0];
        }
    }
    //更新這個真實DOM 的孩子
    else if (vchildren && vchildren.length || fc!=null) {
        innerDiffNode(out, vchildren, context, mountAll, hydrating || props.dangerouslySetInnerHTML!=null);
    }


    // 更新這個真實DOM 的屬性
    diffAttributes(out, vnode.attributes, props);


    // 還原isSvgMode
    isSvgMode = prevSvgMode;

    return out;
}

idiff的邏輯可分紅這幾步數組

  1. 保存現有的文檔爲型
  2. 更新或建立文本節點
  3. 更新或建立組件對應的真實DOM
  4. 更新普通元素節點
  5. 收集元素當前的真實屬性
  6. 更新元素的內部(孩子)
  7. diff元素的屬性
  8. 還原以前的文檔類型

能夠看做是對當個元素的diff實現。緩存

而更外圍的diff方法,主要經過diffLevel這個變量,控制全部插入組件的DidMount鉤子的調用。app

idiff內部有一個叫innerDiffNode的方法,若是是我做主,我更願意命名爲diffChildren.dom

innerDiffNode方法是很是長,好像每次我閱讀它,它都變長一點。一點點猴子補丁往上加,徹底不考慮用設計模式對它進行拆分。svg

function innerDiffNode(dom, vchildren, context, mountAll, isHydrating) {
    let originalChildren = dom.childNodes,
        children = [],
        keyed = {},
        keyedLen = 0,
        min = 0,
        len = originalChildren.length,
        childrenLen = 0,
        vlen = vchildren ? vchildren.length : 0,
        j, c, f, vchild, child;

    // 若是真實DOM 存在孩子,能夠進行diff,這時要收集設置到key屬性的孩子到keyed對象,剩餘的則放在children數組中
    if (len!==0) {
        for (let i=0; i<len; i++) {
            let child = originalChildren[i],
                props = child[ATTR_KEY],
                key = vlen && props ? child._component ? child._component.__key : props.key : null;
            if (key!=null) {
                keyedLen++;
                keyed[key] = child;
            }
            else if (props || (child.splitText!==undefined ? (isHydrating ? child.nodeValue.trim() : true) : isHydrating)) {
                children[childrenLen++] = child;
            }
        }
    }

    if (vlen!==0) {
//遍歷當前虛擬DOM children
        for (let i=0; i<vlen; i++) {
            vchild = vchildren[i];
            child = null;

            // 先嚐試根據key來尋找已有的DOM
            let key = vchild.key;
            if (key!=null) {
                if (keyedLen && keyed[key]!==undefined) {
                    child = keyed[key];
                    keyed[key] = undefined;
                    keyedLen--;
                }
            }
            // 若是沒有key ,那麼就根據nodeName來尋找最近的那個節點
            else if (!child && min<childrenLen) {
                for (j=min; j<childrenLen; j++) {
                    if (children[j]!==undefined && isSameNodeType(c = children[j], vchild, isHydrating)) {
                        child = c;
                        children[j] = undefined;
                        if (j===childrenLen-1) childrenLen--;
                        if (j===min) min++;
                        break;
                    }
                }
            }

            // 更新它的孩子與屬性
            child = idiff(child, vchild, context, mountAll);

            f = originalChildren[i];
            if (child && child!==dom && child!==f) {
                 //各類形式的插入DOM樹
                if (f==null) {
                    dom.appendChild(child);
                }
                else if (child===f.nextSibling) {
                    removeNode(f);
                }
                else {
                    dom.insertBefore(child, f);
                }
            }
        }
    }


    // GC
    if (keyedLen) {
        for (let i in keyed) if (keyed[i]!==undefined) recollectNodeTree(keyed[i], false);
    }

    // GC
    while (min<=childrenLen) {
        if ((child = children[childrenLen--])!==undefined) recollectNodeTree(child, false);
    }
}


export function isSameNodeType(node, vnode, hydrating) {
    if (typeof vnode==='string' || typeof vnode==='number') {  
//文本節點與字符串,文本節點是對等的,但我不明白爲何不用nodeType === 3來斷定文本節點
        return node.splitText!==undefined;
    }
    if (typeof vnode.nodeName==='string') {
        return !node._componentConstructor && isNamedNode(node, vnode.nodeName);
    }
    return hydrating || node._componentConstructor===vnode.nodeName;
}

innerDiffNode方法在建立keyed對象中其實存在巨大的缺憾,它沒法阻止用戶在同一組孩子 使用兩個相同的key的狀況,所以會出錯。而官方react,其實還結合父節點的深度,所以能夠規避。

好比下面的JSX ,preact在diff時就會出錯:

<div>{[1,2,3].map((el,index)=>{ <span key={"x"+index}>{el}</span>  })}xxx
{[4,5,6].map((el,index)=>{ <span key={"x"+index}>{el}</span>  })}
</div>

這裏咱們比較一下官方react與preact的diff差別。官方react是有兩組虛擬DOM 樹在diff,diff完畢再將差別點應用於真實DOM 中。在preact則是先從真實DOM樹中還原出以前的虛擬DOM出來,而後新舊vtree進行邊diff邊patch的操做。

之於怎麼還原呢,利用緩存數據與nodeValue!

真實DOM 擁有_component對象的元素節點 擁有ATTR_KET對象的元素節點 擁有ATTR_KET布爾值的文本節點
對應的prevVNode 組件虛擬DOM 元素虛擬DOM 簡單類型的虛擬DOM

這種深度耦合DOM 樹的實現的優缺點都很明顯,好處是它老是最真實地反映以前的虛擬DOM樹的狀況,diff時少傳參,壞處是須要作好內存泄露的工做。

相關文章
相關標籤/搜索