上篇文章裏大概的講了Preact的渲染機制:node
這裏的render函數,其實沒有咱們寫的那麼簡單.熟悉react渲染機制的人,都會知道,react
在state/props發生改變的時候,從新渲染全部的節點,構造出新的虛擬Dom tree跟原來的Dom tree用Diff算法進行比較,獲得須要更新的地方在批量造做在真實的Dom上,因爲這樣作就減小了對Dom的頻繁操做,從而提高的性能。
而實際的過程當中,第一次的渲染,會直接調用diff函數。git
// @example // render a div into <body>: // render(<div id="hello">hello!</div>, document.body); export function render(vnode, parent, merge) { return diff(merge, vnode, {}, false, parent, false) }
解析:github
這裏 vnode 是虛擬 DOM, parent 是容器的 DOM。web
merge 可選,是另一個已經存在的 dom 樹,若是指定 merge 則會將虛擬 DOM 生成的 DOM 樹替換到 merge 上。若是不指定的話,將會把生成的 DOM 樹添加到 parent 裏面。算法
而 React 的第三個參數是一個回調函數,在渲染時觸發。app
咱們先從簡單的渲染一個dom元素提及:render(<div id="foo"> hello !</div>, document.body, null);
dom
下面是乞丐版diff.js函數
/** Apply differences in a given vnode (and it's deep children) to a real DOM Node. * @param {Element} [dom=null] * 指當前的vnode所對應的以前未更新的真實dom。 * 有兩種狀況,第一就是render的第三個參數,若爲空,則是null,空的這種狀況表面是首次渲染, 如今考慮首次渲染的狀況 針對本例,dom = undefined * 第二種就是vnode的對應的未更新的真實DOM,即表示渲染刷新界面。 * @param {VNode} vnode 主要是須要渲染的虛擬dom節點 * 對於本例來講 vnode = { attributes: {id: "foo"} children:[" hello !"] key:undefined nodeName:"div" } * @param {Element} context 用於全局的屬性,跟React相似 * @returns {Element} dom The created/mutated element * @private */ export function diff(dom, vnode, context, mountAll, parent, componentRoot) { // idiff函數就是diff算法的內部實現,idiff會返回虛擬dom對應建立的真實dom節點 let ret = idiff(dom, vnode, context, mountAll, componentRoot); // append the element if its a new parent // 若是父節點以前沒有建立這個子節點,則添加到父節點上,而不是替換 if (parent && ret.parentNode !== parent) parent.appendChild(ret) // 最終返回真實dom節點 return ret; }
上面的函數比較簡單,就是經過idiff的到真實dom節點以後,添加到父節點上。性能
/** Internals of `diff()`, separated to allow bypassing diffLevel / mount flushing. */ /** * 1. 首先判斷vnode是否爲空值,若是是將vnode設定爲空字符串 * 2. 再次判斷vnode是否爲字符串或者數字,內部會判斷是否爲文本節點,最後進行更新或者替換工做 * 3. 若是vnode.nodeName是一個component則進行組件的渲染,因爲這裏咱們的vnode是一個對象,因此走這一條邏輯 */ function idiff(dom, vnode, context, mountAll, componentRoot) { let out = dom, let vnodeName = vnode.nodeName, // empty values (null, undefined, booleans) render as empty Text nodes // 將null, undefined, boolean轉換爲空字符 if (vnode == null || typeof vnode === 'boolean') vnode = '' // Fast case: Strings & Numbers create/update Text nodes. // 將字符串和數字轉化爲文本節點 if (typeof vnode === 'string' || typeof vnode === 'number') { // 直接建立文本節點 out = document.createTextNode(vnode) } if (!dom || !isNamedNode(dom, vnodeName)) { // createNode經過document.createElement(nodeName);返回一個真實的dom節點 out = createNode(vnodeName, isSvgMode) } // 因爲out爲新建的dom元素,fc = null let fc = out.firstChild, // 注意這裏:此時out戴上了'__preactattr_'屬性,說明它是由preact建立的 props = out[ATTR_KEY], // vchildren = ['hello !'] vchildren = vnode.children; // 對child進行比對,遞歸更新全部child節點 innerDiffNode(out, vchildren, context, mountAll, hydrating || props.dangerouslySetInnerHTML!=null); // Apply attributes/props from VNode to the DOM Element: // 在VNode和DOM之間比較props和atrributes diffAttributes(out, vnode.attributes, props) return out; }
idiff目前只處理三種類型的vnode:空值,字符串,數字,原生dom節點,也就是還不包含組件類型的。
能夠看到:凡是由preact建立的標籤,都帶有'__preactattr_'屬性,建立完畢以後,還須要
- 對子節點進行建立和更新
- 更新props
/** Apply child and attribute changes between a VNode and a DOM Node to the DOM. * 內部diff,比較子節點和屬性的變化 * @param {Element} dom Element whose children should be compared & mutated * @param {Array} vchildren Array of VNodes to compare to `dom.childNodes` * @param {Object} context Implicitly descendant context object (from most recent `getChildContext()`) * @param {Boolean} mountAll * @param {Boolean} isHydrating If `true`, consumes externally created elements similar to hydration */ 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 if (vlen !== 0) { for (let i = 0; i < vlen; i++) { vchild = vchildren[i] child = null // morph the matched/found/created DOM child to match vchild (deep) // 經過idiff獲得真實的dom節點,目前因爲vchild是字符串'hello',因此 // 這裏返回的是一個字符串節點 child = idiff(child, vchild, context, mountAll) // 子節點爲空,直接append dom.appendChild(child); } } }
/** Apply differences in attributes from a VNode to the given DOM Element. * @param {Element} dom 虛擬dom對應的真實dom * @param {Object} attrs 指望的最終鍵值屬性對 * @param {Object} old 當前或者以前的屬性 */ function diffAttributes(dom, attrs, old) { let name // remove attributes no longer present on the vnode by setting them to undefined // 移除屬性由於如今的name爲空了 for (name in old) { // 若是old[name]存在,可是attrs[name]不存在 if (!(attrs && attrs[name] != null) && old[name] != null) { setAccessor(dom, name, old[name], (old[name] = undefined), isSvgMode) } } // add new & update changed attributes // 添加或更新改變的屬性 for (name in attrs) { if ( name !== 'children' && name !== 'innerHTML' && (!(name in old) || attrs[name] !== (name === 'value' || name === 'checked' ? dom[name] : old[name])) ) { setAccessor(dom, name, old[name], (old[name] = attrs[name]), isSvgMode) } } }
上面就是preact初次渲染的一個簡單流程,總結來講:
經過diff.js
能夠獲得一個完整的dom節點A,而在這個過程當中,idiff
負責建立父節點A,innerDiffNode
用於遞歸調用idiff,從而獲得A的全部child
節點並將它添加到A裏,最後再將虛擬dom的新屬性更新到A節點。
可是還有遺留的問題沒有解決:
一、渲染react組件的過程
二、目前還沒有考慮diff節點間的對比邏輯
下篇文章會講這兩塊
參考文檔: