這是說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的邏輯可分紅這幾步數組
能夠看做是對當個元素的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時少傳參,壞處是須要作好內存泄露的工做。