您的位置:首页 > 其它

手动实现一个虚拟DOM算法

2018-02-09 12:10 190 查看

发现一个好文:《深度剖析:如何实现一个 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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: