發現一個好文:《深度剖析:如何實現一個 Virtual DOM 算法》 源碼
文章寫得非常詳細,仔細看了一遍代碼,加了一些注釋。其實還有有一些地方看的不是很懂(畢竟我菜qaq 先碼 有時間研究下diff算法
util.js
/** * 工具..類? */ var _ = exports /** * 獲取一個對象的類型 * 匹配 '[object\s' (\s 是空白字符) 或 ']' 並替換為空 * 也就是可以將 [object Array] 變為 Array * @param {Object} obj */ _.type = function(obj) { return Object.prototype.toString.call(obj).replace(/\[object\s|\]/g, '') } /** * 判斷一個對象是否是數組 * @param {Object} list */ _.isArray = function isArray(list) { return _.type(list) === 'Array' } /** * 判斷一個對象是否為 String */ _.isString = function isString(list) { return _.type(list) === 'String' } /** * 用於將 類數組對象 變為數組 比如 nodeList, argument 等帶有 length 屬性的對象 * @param {*} arrayLike * @param {int} index 從第幾個元素開始 */ _.slice = function slice(arrayLike, index) { return Array.prototype.slice.call(arrayLike, index) } /** * 獲取 value 表達式的布爾值 * @param {*} value */ _.truthy = function truthy(value) { return !!value } /** * 對數組中每一個元素執行 fn (相當於map? * @param {*} array * @param {*} fn */ _.each = function each(array, fn) { for (var i = 0, len = array.length; i < len; i++) { fn(array[i], i) } } /** * 為 DOM 節點設置屬性 */ _.setAttr = function(node, key, value) { switch(key) { case 'style': node.style.cssText = value break case 'value': var tagName = node.tagName || '' tagName = tagName.toLowerCase() if (tagName === 'input' || tagName === 'textarea') { node.value = value } else { node.setAttribute(key, value) } break default: node.setAttribute(key, value) break } } /** * 將類數組類型轉化為數組類型 * @param {Object} listLike * ( 和 slice 有什么區別呢????? */ _.toArray = function toArray(listLike) { if (!listLike) return [] var list = [] for (var i = 0, len = listLike.length; i < len; i++) { list.push(listLike[i]) } return list }
element.js
var _ = require('./util') /** * 用來表示虛擬 DOM 節點的數據結構 * @param {String} tagName 節點類型 * @param {Object} props 節點屬性 鍵值對形式 可以選填 * @param {Array<Element|String>} children 節點的子元素 或者文本 * @example Element('div', {'id': 'container'}, [Element('p', ['the count is :' + count])]) */ function Element(tagName, props, children) { // var e = Element(tagName, props, children) // 並不會讓 e instanceof Element 為 true 要加 new 關鍵字才可以哦 if (!(this instanceof Element)) { // 如果 children 不是數組且不為空 就把第三個參數以及后面的參數都作為 children if (!_.isArray(children) && children != null) { // children 去掉非空子元素 children = _.slice(arguments, 2).filter(_.truthy) } return new Element(tagName, props, children) } // 如果屬性是數組類型 證明沒有傳屬性 第二個參數就是 children if (_.isArray(props)) { children = props props = {} } this.tagName = tagName this.props = props || {} this.children = children || [] // void后面跟一個表達式 void操作符會立即執行后面的表達式 並且統一返回undefined // 可以為節點添加一個屬性 key 以便重新排序的時候 判斷節點位置的變化 this.key = props ? props.key : void 0 // count 統計不包含文本元素 一共有多少子元素 var count = 0 _.each(this.children, function(child, i) { if (child instanceof Element) { count += child.count } else { children[i] = '' + child } count++ }) this.count = count } /** * 將虛擬DOM 渲染成真實的DOM元素 */ Element.prototype.render = function() { // 根據 tag 創建元素 var el = document.createElement(this.tagName) var props = this.props // 為元素添加屬性 for (var propName in props) { var propValue = props[propName] _.setAttr(el, propName, propValue) } // 先渲染子節點 然后添加到當前節點 _.each(this.children, function(child) { var childEl = (child instanceof Element) ? child.render() : document.createTextNode(child) el.appendChild(childEl) }) return el } module.exports = Element
diff.js
var _ = require('./util') var patch = require('./patch.js') var listDiff = require('list-diff2') /** * 統計更新前后 DOM 樹的改變 * @param {Element} oldTree 更新前 DOM 樹 * @param {Element} newTree 更新后 DOM 樹 */ function diff(oldTree, newTree) { var index = 0 var patches = {} dfsWalk(oldTree, newTree, index, patches) return patches } /** * dfs 遍歷新舊 DOM 樹 * patches 記錄差異 */ function dfsWalk(oldNode, newNode, index, patches) { var currentPatch = [] if (newNode === null) { // 如果該節點被刪除 不需要做任何事情 } else if (_.isString(oldNode) && _.isString(newNode)) { // 如果改變前后該節點都是文本類型 if (newNode !== oldNode) { currentPatch.push({ type: patch.TEXT, content: newNode }) } } else if (oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) { // 當節點的類型以及key都相同的時候 判斷兩個節點的屬性是否有變化 var propsPatches = diffProps(oldNode, newNode) if (propsPatches) { currentPatch.push({ type: patch.PROPS, props: propsPatches }) } // 當新節點包含ignore屬性的時候 不比較其子節點 // (也就是說 如果子節樹不會有變化的話 手動添加 ignore 屬性來防止比較子節點降低效率??? if (!isIgnoreChildren(newNode)) { diffChildren(oldNode.children, newNode.children, index, patches, currentPatch) } } else { // 節點的類型不同 直接替換 currentPatch.push({ type: patch.REPLACE, node: newNode }) } if (currentPatch.length) { patches[index] = currentPatch } } /** * 比較兩個元素的子節點列表 * @param {Array<Element|String>} oldChildren * @param {Array<Element|String>} newChildren */ function diffChildren(oldChildren, newChildren, index, patches, currentPatch) { // 此處未實現 diff 算法 直接引用 list-diff2 的 listDiff 函數 var diffs = listDiff(oldChildren, newChildren, 'key') newChildren = diffs.children // 如果有移動 就為當前節點標記改變 if (diffs.moves.length) { // diffs.moves 記錄節點的移動順序 var reorderPatch = { type: patch.RECORDER, moves: diffs.moves } currentPatch.push(recorderPatch) } // leftNode 記錄的是前一個子節點 根據dfs遍歷的順序為每個節點標號(index var leftNode = null var currentNodeIndex = index _.each(oldChildren, function(child, i) { // 對於每一個子節點 進行比較 var newChild = newChildren[i] currentNodeIndex = (leftNode && leftNode.count) ? currentNodeIndex + leftNode.count + 1 : currentNodeIndex + 1 dfsWalk(child, newChild, currentNodeIndex, patches) leftNode = child }) } /** * 比較新舊節點的屬性變化 */ function diffProps(oldNode, newNode) { var count = 0 var oldProps = oldNode.props var newProps = newNode.props var key, value var propsPatches = {} // 記錄寫與原節點相比 值改變的屬性 for (key in oldProps) { value = oldProps[key] if (newProps[key] !== value) { count++ propsPatches[key] = newProps[key] } } // 記錄之前不存在的屬性 for (key in newProps) { value = newProps[key] if (!oldProps.hasOwnProperty(key)) { count++ propsPatches[key] = newProps[key] } } // 改變前后節點屬性完全相同 返回 null if (count === 0) return null return propsPatches } function isIgnoreChildren(node) { return (node.props && node.props.hasOwnProperty('ignore')) } module.exports = diff
patch.js
/** * 根據改變前后節點的差異 渲染頁面 */ var _ = require('./util') var REPLACE = 0 // 替換元素 var REORDER = 1 // 移動 刪除 新增 子節點 var PROPS = 2 // 修改節點屬性 var TEXT = 3 // 修改文本內容 /** * * @param {element} node 改變之前的渲染結果 * @param {Object} patches 通過 diff 計算出的差異集合 */ function patch(node, patches) { var walker = { index: 0 } dfsWalk(node, walker, patches) } /** * dfs 遍歷dom樹 根據舊節點和patches渲染新節點 * @param {element} node 更改之前的 dom 元素 * @param {*} walker 記錄走到第幾個節點(so...為什么不直接傳index... * @param {Object} patches 節點之間的差異集合 */ function dfsWalk(node, walker, patches) { var currentPatches = patches[walker.index] var len = node.childNodes ? node.childNodes.length : 0 // 先渲染子節點 for (var i = 0; i < len; i++) { var child = node.childNodes[i] walker.index++ dfsWalk(child, walker, patches) } // 如果當前節點存在差異 就重新渲染 if (currentPatches) { applyPatches(node, currentPatches) } } function applyPatches(node, currentPatches) { // 根據差異類型的不同 進行不同的渲染 _.each(currentPatches, function(currentPatch) { switch (currentPatch.type) { case REPLACE: // 替換 重新創建節點 並替換原節點 var newNode = (typeof currentPatch.node === 'string') ? document.createTextNode(currentPatch.node) : currentPatch.node.render() node.parentNode.replaceChild(newNode, node) break case REORDER: // 子節點重新排序 reorderChildren(node, currentPatch.moves) break case PROPS: // 重新設置屬性 setProps(node, currentPatch.props) break case TEXT: // 改變文本值 if (node.textContent) { node.textContent = currentPatch.content } else { // IE node.nodeValue = currentPatch.content } break default: throw new Error('Unknown patch type ' + currentPatch.type) } }) } /** * 為節點重新設置屬性 屬性值為undefined表示該屬性被刪除了 * @param {element} node * @param {Object} props */ function setProps(node, props) { for (var key in props) { // 所以到底為什么不使用 undefined // undefined 並不是保留詞(reserved word),它只是全局對象的一個屬性,在低版本 IE 中能被重寫 if (props[key] === void 0) { node.removeAttribute(key) } else { var value = props[key] _.setAttr(node, key, value) } } } /** * 將節點根據moves重新排序 * @param {element} node DOM元素 * @param {Obejct} moves diff算法根據新舊子樹以及key算出的移動順序 */ function reorderChildren(node, moves) { var staticNodeList = _.toArray(node.childNodes) var maps = {} _.each(staticNodeList, function(node) { // nodeType 屬性返回以數字值返回指定節點的節點類型。 // nodeType === 1 表示 元素element if (node.nodeType === 1) { var key = node.getAttribute('key') if (key) { maps[key] = node } } }) _.each(moves, function(move) { var index = move.index if (move.type === 0) { // 刪除節點 if (staticNodeList[index] === node.childNodes[index]) { node.removeChild(node.childNodes[index]) } // splice() 方法可刪除從 index 處開始的零個或多個元素,並且用參數列表中聲明的一個或多個值來替換那些被刪除的元素。 // arrayObject.splice(index,howmany,item1,.....,itemX) staticNodeList.splice(index, 1) } else if (move.type === 1) { // 新增節點 如果之前就存在相同的key 就將之前的拷貝 否則創建新節點 // cloneNode() 創建節點的拷貝 並返回該副本 參數為true表示深拷貝 var insertNode = maps[move.item.key] ? maps[move.item.key].cloneNode(true) : ( (typeof move.item === 'object') ? move.item.render() : document.createTextNode(move.item)) staticNodeList.splice(index, 0, insertNode) node.insertBefore(insertNode, node.childNodes[index] || null) } }) } patch.REPLACE = REPLACE patch.REORDER = REORDER patch.PROPS = PROPS patch.TEXT = TEXT module.exports = patch