实现一个vdom

334 阅读3分钟
感谢原作者
github.com/livoras/blo…

utils.js

本次demo用到的所有工具
var _ = exports

_.type = function (obj) {
  return Object.prototype.toString.call(obj).replace(/\[object\s|\]/g, '')
}

_.isArray = function isArray (list) {
  return _.type(list) === 'Array'
}

_.slice = function slice (arrayLike, index) {
  return Array.prototype.slice.call(arrayLike, index)
}

_.truthy = function truthy (value) {
  return !!value
}

_.isString = function isString (list) {
  return _.type(list) === 'String'
}

_.each = function each (array, fn) {
  for (var i = 0, len = array.length; i < len; i++) {
    fn(array[i], i)
  }
}

_.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
}

_.setAttr = function setAttr (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 {
        // if it is not a input or textarea, use `setAttribute` to set
        node.setAttribute(key, value)
      }
      break
    default:
      node.setAttribute(key, value)
      break
  }
}

element.js(处理元素)
通过记录标签名,属性,子节点来生成新的节点
  1. 原生dom上的属性
var div = document.createElement("div");
var str = "";
for (var key in div) {
    console.log(key)
    str = str + key + "";
}
  1. 简单实现一个render
var ul = new Element('ul', {
    id: 'list'
}, [
    new Element('li', {
        class: 'item'
    }, ['Item 1']),
    new Element('li', {
        class: 'item'
    }, ['Item 2']),
    new Element('li', {
        class: 'item'
    }, ['Item 3'])
])

Element.prototype.render = function() {
    var el = document.createElement(this.tagName);
    var props = this.props;

    for(var propName in props) {
        var propValue = props[propName];
        el.setAttribute(propName,propValue)
    }

    var children = this.children || [];

    children.forEach(function(child){
        var childEL= (child instanceof Element)?
        child.render():
        document.createTextNode(child);
        el.appendChild(childEL)
    })

    return el;
}

var ulRoot = ul.render();
document.body.appendChild(ulRoot);
  1. 在拿到节点描述后准确生成相应元素
var _ = require("./utils")

/**
 * @param {String} tagName
 * @param {Object} props  记录标签的属性
 * @param {Array<Element | String>} children  子属性或者内容
 */

function Element(tagName, props, children) {
    if (!(this instanceof Element)) {
        // 如果子属性不是数组并且不为空 那么就是一个内容节点
        if (!_.isArray(children) && children !== null) {
            children = _.slice(arguments, 2).filter(_.truthy)
        }
        return new Element(tagName, props, children)
    }
    // 当子属性为数组的时候 即拥有子节点

    if (_.isArray(props)) {
        children = props;
        props = {}
    }

    this.tagName = tagName;
    this.props = props || {};
    this.children = children || []
    this.key = props ? props.key : void 23333;

    var count = 0;

    _.each(this.children, function (child, i) {
        if (child instanceof Element) {
            count += child.count
        } else {
            children[i] = '' + child
        }
        count++
    })

    this.count = count
}

Element.prototype.render = function () {
    var el = document.createElement(this.tagName);
    var props = this.props;
    console.log(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

differ.js(处理新旧元素不同,返回需要更新的补丁包)

var _ = require('./utils')
var patch = require('./patch')
var listDiff = require('list-diff2')


function diff(oldTree, newTree) {
    var index = 0;
    var patches = {}
    dfsWalk(oldTree, newTree, index, patches)
    return 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) {
        // 节点名和索引一致  比较节点的属性和子节点
        // 比较属性
        var propsPathes = diffProps(oldNode, newNode);
        if (propsPathes) {
            // 以属性形式更新内容
            currentPatch.push({
                type: patch.PROPS,
                props: propsPathes
            })
        }
        console.log("currentPatch----",currentPatch)
        // 比较子节点
        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
    }
}

// 比较属性
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]
        }
    }

    // 如果属性一致 则跳出
    if (count === 0) {
        return null
    }

    return propsPatches
}

// 作为优化 判断节点是否有忽略属性 有则不修改
function isIgnoreChildren(node) {
    return (node.props && node.props.hasOwnProperty('ignore'))
}

// 比较子节点

function diffChildren(oldChildren, newChildren, index, patches, currentPatch) {
    // 利用一个插件 简单比较新旧节点的不同
    var diffs = listDiff(oldChildren, newChildren, 'key')
    console.log(diffs)
    newChildren = diffs.children

    if (diffs.moves.length) {
        var reorderPatch = {
            type: patch.REORDER,
            moves: diffs.moves
        }
        currentPatch.push(reorderPatch)
    }
    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
    })
}

module.exports = diff

patch.js(收到补丁包,返回新的节点)

var _ = require('./utils')

// 1.替换掉原来的节点
// 2.移动、删除、新增子节点
// 3.修改了节点的属性
// 4.对于文本节点,文本内容可能会改变
var REPLACE = 0
var REORDER = 1
var PROPS = 2
var TEXT = 3

// 对节点进行打补丁
function patch(node, patches) {
    var walker = {
        index: 0
    }
    dfsWalk(node, walker, 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 {
                    // fuck ie
                    node.nodeValue = currentPatch.content
                }
                break
            default:
                throw new Error('Unknown patch type ' + currentPatch.type)
        }
    })
}

function setProps(node, props) {
    for (var key in props) {
        if (props[key] === void 666) {
            node.removeAttribute(key)
        } else {
            var value = props[key]
            _.setAttr(node, key, value)
        }
    }
}

function reorderChildren(node, moves) {
    var staticNodeList = _.toArray(node.childNodes)
    var maps = {}

    _.each(staticNodeList, function (node) {
        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) { // remove item
            if (staticNodeList[index] === node.childNodes[index]) { // maybe have been removed for inserting
                node.removeChild(node.childNodes[index])
            }
            staticNodeList.splice(index, 1)
        } else if (move.type === 1) { // insert item
            var insertNode = maps[move.item.key] ?
                maps[move.item.key].cloneNode(true) // reuse old item
                :
                (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

demo实践

index.js

exports.el = require('./lib/element')
exports.diff = require('./lib/differ')
exports.patch = require('./lib/patch')

bundle.js

window.vdom = require('./index')

html

   var el = vdom.el
    var diff = vdom.diff
    var patch = vdom.patch
    var count = 0

    function renderTree() {
        count++
        var items = []
        var color = (count % 2 === 0) ?
            'blue' :
            'red'
        for (var i = 0; i < count; i++) {
            items.push(el('li', ['Item #' + i]))
        }
        return el('div', {
            'id': 'container'
        }, [            el('h1', {                style: 'color: ' + color            }, ['myVdom']),
            el('p', ['the count is :' + count]),
            el('ul', items)
        ])
    }
    var tree = renderTree()
    var root = tree.render()
    document.body.appendChild(root)
    setInterval(function () {
        var newTree = renderTree()
        var patches = diff(tree, newTree)
        console.log(patches)
        patch(root, patches)
        tree = newTree
    }, 1000)