发现一个好文:《深度剖析:如何实现一个 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

手动实现一个虚拟DOM算法的更多相关文章

  1. 如何快速实现一个虚拟 DOM 系统

    虚拟 DOM 是目前主流前端框架的技术核心之一,本文阐述如何实现一个简单的虚拟 DOM 系统. 为什么需要虚拟 DOM? 虚拟 DOM 就是一棵由虚拟节点组成的树,这棵树展现了真实 DOM 的结构.这 ...

  2. 手撸一个虚拟DOM,不错

    大家好,我是半夏,一个刚刚开始写文的沙雕程序员.如果喜欢我的文章,可以关注 点赞 加我微信:frontendpicker,一起学习交流前端,成为更优秀的工程师-关注公众号:搞前端的半夏,了解更多前端知 ...

  3. 手写一个虚拟DOM库,彻底让你理解diff算法

    所谓虚拟DOM就是用js对象来描述真实DOM,它相对于原生DOM更加轻量,因为真正的DOM对象附带有非常多的属性,另外配合虚拟DOM的diff算法,能以最少的操作来更新DOM,除此之外,也能让Vue和 ...

  4. 深度剖析:如何实现一个 Virtual DOM 算法

    本文转载自:https://github.com/livoras/blog/issues/13 目录: 1 前言 2 对前端应用状态管理思考 3 Virtual DOM 算法 4 算法实现 4.1 步 ...

  5. 实现一个 Virtual DOM 算法

    1 前言 本文会在教你怎么用 300~400 行代码实现一个基本的 Virtual DOM 算法,并且尝试尽量把 Virtual DOM 的算法思路阐述清楚.希望在阅读本文后,能让你深入理解 Virt ...

  6. 深入理解react中的虚拟DOM、diff算法

    文章结构: React中的虚拟DOM是什么? 虚拟DOM的简单实现(diff算法) 虚拟DOM的内部工作原理 React中的虚拟DOM与Vue中的虚拟DOM比较 React中的虚拟DOM是什么?   ...

  7. 虚拟DOM与DOM diff算法

    虚拟DOM是什么? 一个虚拟DOM(元素)是一个一般的js对象, 准确的说是一个对象树(倒立的) 虚拟DOM保存了真实DOM的层次关系和一些基本属性,与真实DOM一一对应,如果只是更新虚拟DOM, 页 ...

  8. 虚拟dom和diff算法

    https://github.com/livoras/blog/issues/13 这里简单记录一些要点和理解: 一个dom元素中有许多属性,操作dom是很耗资源的,而操作自定义的js对象是很高效.所 ...

  9. 【React 7/100 】 虚拟DOM和Diff算法

    虚拟DOM和Diff算法 React更新视图的思想是:只要state变化就重新渲染视图 特点:思路非常清晰 问题:组件中只有一个DOM元素需要更新时,也得把整个组件的内容重新渲染吗? 不是这样的 理想 ...

随机推荐

  1. WCF系列_WCF常用绑定选择

    一.五种常用绑定常用绑定的传输协议以及编码格式 名称 传输协议 编码格式 互操作性 BasicHttpBinding HTTP/HTTPS Text,MTOM Yes NetTcpBinding TC ...

  2. kvm 客户机加载移动硬盘

    1,宿主机安装usbutils yum install usbutils -y 2,插入U盘或者移动硬盘并查看 [root@localhost ~]# lsusb Bus Device : ID 10 ...

  3. Moving or disabling the package cache for Visual Studio 2017

    Moving or disabling the package cache for Visual Studio 2017 | Setup & Install by Heath Stewart ...

  4. spring+struts+hibernate整合

    spring整合: 1:添加配置文件和相应的spring jar包(记得一定要加上commons-logging的jar包,有坑****) 2:创建date对象,如果成功则spring的环境ok

  5. P3047 [USACO12FEB]附近的牛Nearby Cows

    https://www.luogu.org/problemnew/show/P304 1 #include <bits/stdc++.h> 2 #define up(i,l,r) for( ...

  6. SAS 循环与数组

    SAS 循环与数组 SAS提供了循环语句以满足在编程中需要多次执行相同操作的情 况.有时还需要对不同的变量执行相同的操作,此时可定义SAS数组,并通过数组名和下标来引用这些变量. 1 循环 SAS循环 ...

  7. 82、iOS 基本算法

    “冒泡排序.选择排序.快速排序.归并排序.逆序.二分查找.求两个整数的最大公约数和最小公倍数.” 一.冒泡排序 1.比较相邻的元素.如果第一个比第二个大,就交换他们两个. 2.对每一对相邻元素作同样的 ...

  8. 信号量(Semaphore)

    在python的多线程体系中,一共有4种锁: 同步锁(互斥锁):Lock: 递归锁:RLock: 信号量:Semaphore: 同步条件锁:Condition. 信号量(semaphore)是一种可以 ...

  9. java上传文件获取跟目录的办法

    在java中获得文件的路径在我们做上传文件操作时是不可避免的.web 上运行1:this.getClass().getClassLoader().getResource("/"). ...

  10. JavaScriptDOM

    DOM简介 1.HTML DOM:网页被加载时,浏览器会创建文档对象模型 2.DOM操作HTML:改变HTML的元素.属性.CSS样式.对所有事件作出反应 DOM操作HTML 1.改变HTML输出流 ...