感谢原作者
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(处理元素)
通过记录标签名,属性,子节点来生成新的节点
- 原生dom上的属性
var div = document.createElement("div");
var str = "";
for (var key in div) {
console.log(key)
str = str + key + "";
}
- 简单实现一个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);
- 在拿到节点描述后准确生成相应元素
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)