前端-Virtual Dom

这篇具有很好参考价值的文章主要介绍了前端-Virtual Dom。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

为什么需要 Virtual Dom
众所周知,操作 DOM 是很耗费性能的⼀件事情,既然如此,我们可以考虑通过 JS 对象来模拟 DOM 对象,毕竟操作 JS 对象⽐操作
DOM 省时的多
// 假设这⾥模拟⼀个 ul,其中包含了 5 个 li
[1, 2, 3, 4, 5]
// 这⾥替换上⾯的 li
[1, 2, 5, 4]
 从上述例⼦中,我们⼀眼就可以看出先前的 ul 中的第三个 li 被移除了,四五替换了位置。
  • 如果以上操作对应到 DOM 中,那么就是以下代码

// 删除第三个 li
ul.childNodes[2].remove()
// 将第四个 li 和第五个交换位置
let fromNode = ul.childNodes[4]
let toNode = node.childNodes[3]
let cloneFromNode = fromNode.cloneNode(true)
let cloenToNode = toNode.cloneNode(true)
ul.replaceChild(cloneFromNode, toNode)
ul.replaceChild(cloenToNode, fromNode)
  • 当然在实际操作中,我们还需要给每个节点⼀个标识,作为判断是同⼀个节点的依据。所以这也是 Vue 和 React 中官⽅推荐列表⾥的节点使⽤唯⼀的key 来保证性能。
  • 那么既然 DOM 对象可以通过 JS 对象来模拟,反之也可以通过 JS 对象来渲染出对应的 DOM
  • 以下是⼀个 JS 对象模拟 DOM 对象的简单实现

export default class Element {
/**
* @param {String} tag 'div'
* @param {Object} props { class: 'item' }
* @param {Array} children [ Element1, 'text']
* @param {String} key option
*/
constructor(tag, props, children, key) {
this.tag = tag
this.props = props
if (Array.isArray(children)) {

this.children = children
} else if (isString(children)) {
this.key = children
this.children = null
}
if (key) this.key = key
}
// 渲染
render() {
let root = this._createElement(
this.tag,
this.props,
this.children,
this.key
)
document.body.appendChild(root)
return root
}
create() {
return this._createElement(this.tag, this.props, this.children, this.ke
}
// 创建节点
_createElement(tag, props, child, key) {
// 通过 tag 创建节点
let el = document.createElement(tag)
// 设置节点属性
for (const key in props) {
if (props.hasOwnProperty(key)) {
const value = props[key]
el.setAttribute(key, value)
}
}
if (key) {
el.setAttribute('key', key)
	}
// 递归添加⼦节点
if (child) {
		child.forEach(element => {
			let child
			if (element instanceof Element) {
			child = this._createElement(
					element.tag,
					element.props,
					element.children,
					element.key
				)
			} else {
				child = document.createTextNode(element)
			}
			el.appendChild(child)
		})
  }
	return el
	}
}
Virtual Dom 算法简述
  • 既然我们已经通过 JS 来模拟实现了 DOM ,那么接下来的难点就在于如何判断旧的对象和新的对象之间的差异。

  • DOM 是多叉树的结构,如果需要完整的对⽐两颗树的差异,那么需要的时间复杂度会是O(n ^ 3) ,这个复杂度肯定是不能接受的。于是 React 团队优化了算法,实现了O(n) 的复杂度来对⽐差异。

  • 实现 O(n) 复杂度的关键就是只对⽐同层的节点,⽽不是跨层对⽐,这也是考虑到在实际业务中很少会去跨层的移动 DOM 元素

    所以判断差异的算法就分为了两步
    
  • ⾸先从上⾄下,从左往右遍历对象,也就是树的深度遍历,这⼀步中会给每个节点添加索引,便于最后渲染差异

  • ⼀旦节点有⼦元素,就去判断⼦元素是否有不同文章来源地址https://www.toymoban.com/news/detail-629529.html

Virtual Dom 算法实现
树的递归
  • ⾸先我们来实现树的递归算法,在实现该算法前,先来考虑下两个节点对⽐会有⼏种情况
  • 新的节点的 tagName 或者 key 和旧的不同,这种情况代表需要替换旧的节点,并且也不再需要遍历新旧节点的⼦元素了,因为整个旧节点都被删掉了
  • 新的节点的 tagName 和 key (可能都没有)和旧的相同,开始遍历⼦树
  • 没有新的节点,那么什么都不⽤做
import { StateEnums, isString, move } from './util'
import Element from './element'
export default function diff(oldDomTree, newDomTree) {
// ⽤于记录差异
let pathchs = {}
// ⼀开始的索引为 0
dfs(oldDomTree, newDomTree, 0, pathchs)
return pathchs
}
function dfs(oldNode, newNode, index, patches) {
// ⽤于保存⼦树的更改
let curPatches = []
// 需要判断三种情况
// 1.没有新的节点,那么什么都不⽤做
// 2.新的节点的 tagName 和 `key` 和旧的不同,就替换
// 3.新的节点的 tagName 和 key(可能都没有) 和旧的相同,开始遍历⼦树
if (!newNode) {
} else if (newNode.tag === oldNode.tag && newNode.key === oldNode.key) {
// 判断属性是否变更
let props = diffProps(oldNode.props, newNode.props)
if (props.length) curPatches.push({ type: StateEnums.ChangeProps, props
// 遍历⼦树
diffChildren(oldNode.children, newNode.children, index, patches)
} else {
// 节点不同,需要替换
curPatches.push({ type: StateEnums.Replace, node: newNode })
}
if (curPatches.length) {
if (patches[index]) {
patches[index] = patches[index].concat(curPatches)
} else {
patches[index] = curPatches
}
}
}
判断属性的更改
判断属性的更改也分三个步骤
  • 遍历旧的属性列表,查看每个属性是否还存在于新的属性列表中
  • 遍历新的属性列表,判断两个列表中都存在的属性的值是否有变化
  • 在第⼆步中同时查看是否有属性不存在与旧的属性列列表中
function diffProps(oldProps, newProps) {
// 判断 Props 分以下三步骤
// 先遍历 oldProps 查看是否存在删除的属性
// 然后遍历 newProps 查看是否有属性值被修改
// 最后查看是否有属性新增
let change = []
for (const key in oldProps) {
if (oldProps.hasOwnProperty(key) && !newProps[key]) {
change.push({
prop: key
})
}
}
for (const key in newProps) {
if (newProps.hasOwnProperty(key)) {
const prop = newProps[key]
if (oldProps[key] && oldProps[key] !== newProps[key]) {
change.push({
prop: key,
value: newProps[key]
})
} else if (!oldProps[key]) {
change.push({
prop: key,
value: newProps[key]
})
}
}
}
return change
}
判断列表差异算法实现
  • 这个算法是整个 Virtual Dom 中最核⼼的算法,且让我⼀⼀为你道来。 这⾥的主要步骤其实和判断属性差异是类似的,也是分为三步
  • 遍历旧的节点列表,查看每个节点是否还存在于新的节点列表中
  • 遍历新的节点列表,判断是否有新的节点
  • 在第⼆步中同时判断节点是否有移动
  • PS:该算法只对有 key 的节点做处理
function listDiff(oldList, newList, index, patches) {
// 为了遍历⽅便,先取出两个 list 的所有 keys
let oldKeys = getKeys(oldList)
let newKeys = getKeys(newList)
let changes = []
// ⽤于保存变更后的节点数据
// 使⽤该数组保存有以下好处
// 1.可以正确获得被删除节点索引
// 2.交换节点位置只需要操作⼀遍 DOM
// 3.⽤于 `diffChildren` 函数中的判断,只需要遍历
// 两个树中都存在的节点,⽽对于新增或者删除的节点来说,完全没必要
// 再去判断⼀遍
let list = []
oldList &&
oldList.forEach(item => {
let key = item.key
if (isString(item)) {
key = item
}
// 寻找新的 children 中是否含有当前节点
// 没有的话需要删除
let index = newKeys.indexOf(key)
if (index === -1) {
list.push(null)
} else list.push(key)
})
// 遍历变更后的数组
let length = list.length
// 因为删除数组元素是会更改索引的
// 所有从后往前删可以保证索引不变
for (let i = length - 1; i >= 0; i--) {
// 判断当前元素是否为空,为空表示需要删除
if (!list[i]) {
list.splice(i, 1)
changes.push({
type: StateEnums.Remove,
index: i
})
}
}
// 遍历新的 list,判断是否有节点新增或移动
// 同时也对 `list` 做节点新增和移动节点的操作
newList &&
newList.forEach((item, i) => {
let key = item.key


if (isString(item)) {
key = item
}
// 寻找旧的 children 中是否含有当前节点
let index = list.indexOf(key)
// 没找到代表新节点,需要插⼊
if (index === -1 || key == null) {
changes.push({
type: StateEnums.Insert,
node: item,
index: i
})
list.splice(i, 0, key)
} else {
// 找到了,需要判断是否需要移动
if (index !== i) {
changes.push({
type: StateEnums.Move,
from: index,
to: i
})
move(list, index, i)
}
}
})
return { changes, list }
}
function getKeys(list) {
let keys = []
let text
list &&
list.forEach(item => {
let key
if (isString(item)) {
key = [item]
} else if (item instanceof Element) {
key = item.key
}
keys.push(key)
})
return keys
}
遍历⼦元素打标识
  • 对于这个函数来说,主要功能就两个
  • 判断两个列表差异
    • 给节点打上标记
    • 总体来说,该函数实现的功能很简单
function diffChildren(oldChild, newChild, index, patches) {
let { changes, list } = listDiff(oldChild, newChild, index, patches)
if (changes.length) {
if (patches[index]) {
patches[index] = patches[index].concat(changes)
} else {
patches[index] = changes
}
}
// 记录上⼀个遍历过的节点
let last = null
oldChild &&
oldChild.forEach((item, i) => {
let child = item && item.children
if (child) {
index =
last && last.children ? index + last.children.length + 1 : index
let keyIndex = list.indexOf(item.key)
let node = newChild[keyIndex]
// 只遍历新旧中都存在的节点,其他新增或者删除的没必要遍历
if (node) {
dfs(item, node, index, patches)
}
} else index += 1
last = item
})
}
渲染差异
通过之前的算法,我们已经可以得出两个树的差异了。既然知道了差异,就需要局部去更新 DOM 了,下⾯就让我们来看看 Virtual 
Dom 算法的最后⼀步骤
这个函数主要两个功能
  • 深度遍历树,将需要做变更操作的取出来
  • 局部更新 DOM
let index = 0
export default function patch(node, patchs) {
let changes = patchs[index]
let childNodes = node && node.childNodes
// 这⾥的深度遍历和 diff 中是⼀样的
if (!childNodes) index += 1
if (changes && changes.length && patchs[index]) {
changeDom(node, changes)
}
let last = null
if (childNodes && childNodes.length) {
childNodes.forEach((item, i) => {
index =
last && last.children ? index + last.children.length + 1 : index +
patch(item, patchs)
last = item
})
}
}
function changeDom(node, changes, noChild) {
changes &&
changes.forEach(change => {
let { type } = change
switch (type) {
case StateEnums.ChangeProps:
let { props } = change
props.forEach(item => {
if (item.value) {
node.setAttribute(item.prop, item.value)
} else {
node.removeAttribute(item.prop)
}
})
break
case StateEnums.Remove:
node.childNodes[change.index].remove()
break
case StateEnums.Insert:
let dom
if (isString(change.node)) {
dom = document.createTextNode(change.node)
} else if (change.node instanceof Element) {
dom = change.node.create()
}
node.insertBefore(dom, node.childNodes[change.index])
break
case StateEnums.Replace:
node.parentNode.replaceChild(change.node.create(), node)
break
case StateEnums.Move:
let fromNode = node.childNodes[change.from]
let toNode = node.childNodes[change.to]
let cloneFromNode = fromNode.cloneNode(true)
let cloenToNode = toNode.cloneNode(true)
node.replaceChild(cloneFromNode, toNode)
node.replaceChild(cloenToNode, fromNode)
break
default:
break
}
})
}

Virtual Dom 算法的实现也就是以下三步
  • 通过 JS 来模拟创建 DOM 对象
  • 判断两个对象的差异
  • 渲染差异
let test4 = new Element('div', { class: 'my-div' }, ['test4'])
let test5 = new Element('ul', { class: 'my-div' }, ['test5'])
let test1 = new Element('div', { class: 'my-div' }, [test4])
let test2 = new Element('div', { id: '11' }, [test5, test4])
let root = test1.render()
let pathchs = diff(test1, test2)
console.log(pathchs)
setTimeout(() => {
console.log('开始更新')
patch(root, pathchs)
console.log('结束更新')
}, 1000)

到了这里,关于前端-Virtual Dom的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 〖大前端 - 基础入门三大核心之JS篇㉟〗- JavaScript 的DOM简介

    说明:该文属于 大前端全栈架构白宝书专栏, 目前阶段免费 , 如需要项目实战或者是体系化资源,文末名片加V! 作者:不渴望力量的哈士奇(哈哥),十余年工作经验, 从事过全栈研发、产品经理等工作,目前在公司担任研发部门CTO。 荣誉: 2022年度博客之星Top4、2023年度超

    2024年02月04日
    浏览(54)
  • 前端学习记录~2023.8.3~JavaScript重难点实例精讲~第5章 DOM与事件

    本章是第五章DOM与事件相关的内容。 DOM是文档对象模型,全称为Document Object Model。DOM用一个逻辑树来表示一个文档,树的每个分支终点都是一个节点,每个节点都包含着对象。DOM提供了对文档结构化的表述,通过绑定不同的事件可以改变文档的结构、样式和内容,从而能实现

    2024年02月12日
    浏览(49)
  • React原理 - React Virtual DOM 原理

    目录 扩展学习资料  Virtual DOM 是什么【虚拟dom】 React渲染 Virtual DOM VS 原生DOM【vDom是否比原生Dom更高效】 Virtual DOM数据结构 Virtaual DOM Diff【虚拟dom前后比对,更新不同dom的算法】 源码解读 react源码组织方式: React Stack Reconciler【react 栈 协调】 react v15.6.2 源码 练习 名称 链接

    2024年02月11日
    浏览(47)
  • 大型医院云HIS系统:采用前后端分离架构,前端由Angular语言、JavaScript开发;后端使用Java语言开发 融合B/S版电子病历系统

    一套医院云his系统源码 采用前后端分离架构,前端由Angular语言、JavaScript开发;后端使用Java语言开发。融合B/S版电子病历系统,支持电子病历四级,HIS与电子病历系统均拥有自主知识产权。 文末卡片获取联系! 基于云计算技术的B/S架构的医院管理系统(简称云HIS),采用前后

    2024年02月03日
    浏览(50)
  • 前端框架技术革新历程:从原生DOM操作、数据双向绑定到虚拟DOM等框架原理深度解析,Web开发与用户体验的共赢

    前端的发展与前端框架的发展相辅相成,形成了相互驱动、共同演进的关系。前端技术的进步不仅催生了前端框架的产生,也为其发展提供了源源不断的动力。 前端,即Web前端,是指在创建Web应用程序或网站过程中负责用户界面(User Interface, UI)构建与交互的部分,是用户与

    2024年04月26日
    浏览(69)
  • Vue3前端开发,如何获取组件内dom对象以及子组件的属性和方法

    Vue3前端开发,借助Ref来获取组件内dom对象,借助defineExpose编译宏可以获取到子组件的属性和方法。 app入口文件,我们作为父组件,在里面调用了自定义组件TestCom.vue。 先做了一个测试,借助于ref来访问自身的dom对象。如图所示是可以拿到的。 ref又称谓钩子函数,在vue2版本中

    2024年01月22日
    浏览(61)
  • 一文读懂JavaScript DOM节点操作(JavaScript DOM节点操作详解)

    一、什么是节点 DOM模型是树状结构模型,二组成这棵树的就是一个个点,在网络术语中称之为节点。 节点是一个模型中最基本的组成单位。DOM模型是由一个个节点组成的,DOM节点也有其不同的类型。 二、节点类型 DOM节点分为5种类型: 文档节点(就是整个HTML文档,也就是

    2024年01月22日
    浏览(48)
  • 快速认识,前端必学编程语言:JavaScript

    JavaScript是构建Web应用必学的一门编程语言,也是最受开发者欢迎的热门语言之一。所以,如果您还不知道JavaScript的用处、特点的话,赶紧补充一下这块基础知识。 JavaScript 是一种高级、单线程、垃圾收集、解释或即时编译、基于原型、多范式、动态语言,具有非阻塞事件循

    2024年02月05日
    浏览(52)
  • 前端开发——Javascript知识(介绍)

    目录 有关JavaScript的知识  JavaScript的优点   JavaScript的领域 JavaScript的组成 JavaScript的特点 第一个JavaScript程序 在 HTML 文档中嵌入 JavaScript 代码 在脚本文件中编写 JavaScript 代码 JavaScript内容  Html内容  JavaScript 代码执行顺序 JavaScript中的几个重要概念 标识符 保留字 区分

    2024年02月01日
    浏览(47)
  • 前端开发——JavaScript的条件语句

      世界不仅有黑,又或者白 世界而是一道精致的灰  ——Lungcen     目录 条件判断语句 if 语句 if else 语句 if else if else 语句  switch语句 break case 子句 default语句 while循环语句 do while循环语句 for循环语句 for 循环中的三个表达式 for 循环嵌套 for 循环变体——for in for 循环

    2023年04月21日
    浏览(46)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包