zoukankan      html  css  js  c++  java
  • 手动实现一个虚拟DOM算法

    发现一个好文:《深度剖析:如何实现一个 Virtual DOM 算法源码

    文章写得非常详细,仔细看了一遍代码,加了一些注释。其实还有有一些地方看的不是很懂(毕竟我菜qaq 先码 有时间研究下diff算法

    util.js

    /**
     * 工具..类?
     */
    var _ = exports
    
    /**
     * 获取一个对象的类型
     * 匹配 '[objects' (s 是空白字符) 或 ']' 并替换为空
     * 也就是可以将 [object Array] 变为 Array
     * @param {Object} obj
     */
    _.type = function(obj) {
        return Object.prototype.toString.call(obj).replace(/[objects|]/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
  • 相关阅读:
    桌面工具集
    运维工具集
    使用Maven插件构建Spring Boot应用程序Docker镜像
    解决Ubuntu 17.10设置面板打不开的问题
    防止Web表单重复提交的方法总结
    深入浅出mybatis之启动详解
    yum方式安装mysql
    在Java中调用Python
    UUID在Java中的实现与应用
    VM克隆后找不到eth0的问题解决
  • 原文地址:https://www.cnblogs.com/wenruo/p/8434921.html
Copyright © 2011-2022 走看看