手动实现一个虚拟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

 

posted @ 2018-02-09 12:10  我不吃饼干呀  阅读(1420)  评论(0编辑  收藏  举报