手動實現一個虛擬DOM算法


發現一個好文:《深度剖析:如何實現一個 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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM