什么是虚拟dom
虚拟dom就是用普通的js对象来描述dom对象,因为不是真实的dom对象,所以叫virtual dom
为什么要使用虚拟dom
- 手动操作虚拟dom比较麻烦,项目复杂程度越高,dom操作复杂度越高
- 大多数时候,操作虚拟dom的开销比真实dom的开销小很多
- 传统的视图操作我们使用的是模板引擎,但是当数据状态发生变化的时候,我们是不能解决跟踪视图操作的问题的,我们只能删除后,重新渲染
- virtual dom是当状态改变的时候,不需要立即更新dom,只需要创建一个虚拟dom树来描述dom,然后内部通过diff 算法计算如何有效的更新dom
虚拟dom的作用
- 维护视图和状态的关系
- 复杂视图情况下,提升渲染性能
- 除了渲染dom外 ssr weex rn mpvue uni-app
virtual dom库
在研究vue
的Virtual dom
之前,我们先研究一下Snabbdom
,vue2.x
中使用的Virtual dom
就是改造自Snabbdom
,由于Snabbdom
只有大概200行左右的代码,所以学习起来比较容易,模块可扩展,是最快的Virtual dom
之一
Snabbdom创建项目
打包工具为了方便使用,选择parcel
- 创建项目目录
mkdir snabbdom-demo
- 进入项目目录
cd snabbdom-demo
- 创建package.json
npm init -y
- 本地安装parcel
npm i parcel-bundler
- 配置package.json的scripts
"scripts" : { "dev":"parcel index.html --open", "build":"parcel build index.html" }
- 创建目录结构
模块导入的问题
// import snabbdom from 'snabbdom' 错误
import { h, init } from 'snabbdom' //版本是0.7.x的版本
// 1. hello world
// 参数: 数组、模块
// 返回patch函数,作用对比两个vnode的差异,更新到真实dom中
let patch = init([])
// 第一个参数 标签+ 选择器
// 第二个参数 如果是字符串的话就是标签中的内容
let vnode = h('div#container.cls', 'hello world')
let app = document.querySelector('#app')
// 第一个参数: 可以是dom元素,内部会吧dom元素转换成vnode
// 第二个参数: VNode
// 返回VNode
let oldVnode = patch(app, vnode)
// 假设的时刻
vnode = h('div', 'hello snabbdom')
patch(oldVnode, vnode)
patch函数
是通过导出来的init
函数返回的,然后patch接受两个参数
关于init函数
关于patch函数
h函数--处理参数,并且调用vnode函数创建一个vnode对象返回
snabbdom中会导出h
函数
函数的重载
- 函数的重载指的是两个同名函数中参数个数或者参数类型不同的函数,和参数相关和返回值无关
- js中是没有重载的概念的,ts中有重载,不过还是通过代码调整参数来实现的
上面代码在js中运行是后面的函数会覆盖前面的函数,然而在支持重载的ts中可以通过参数个数的不同,来区分这两个同名函数。参数类型不同的区分同理。
Snabbdom模块
Snabbdom的核心库并不能处理元素的属性/样式/事件,如果需要处理的话,可以使用模块
模块的作用
- Snabbdom的核心库并不能处理dom元素的属性/样式/事件等,可以通过注册Snabbdom默认提供的模块来实现
- Snabbdom中的模块可以用来Snabbdom的功能
- Snabbdom中的模块的实现是通过注册全局的钩子函数来实现的
官方提供了6个常用模块
- attributes
- 设置DOM元素的属性,使用setAttribute()
- 处理布尔类型的属性
- props
- 和attributes模块类似,设置DOM元素的属性 element[attr]= value
- 不处理布尔类型的属性
- class
- 切换类样式
- 注意给元素设置类样式是通过
sel
选择器
- dataset
- 设置
data-*
的自定义属性
- 设置
- eventlisteners
- 注册和移除事件
- style
- 设置行内样式,支持动画
- delayed/remove/destory
模块的使用
- 导入模块
- init()中注册模块,参数中有一个数组,数组就是用来注册模块的
- 使用h()函数创建VNode的时候,可以吧第二个参数设置为对象(通过这个对象设置行内样式事件等等),其他参数后移
import { init } from 'snabbdom/build/package/init'
// snabbdom版本@2.1.0
import { h } from 'snabbdom/build/package/h'
// 导入模块
import { styleModule } from 'snabbdom/build/package/modules/style'
import { eventListenersModule} from 'snabbdom/build/package/modules/eventlisteners'
// 注册模块
const patch = init([
styleModule,
eventListenersModule
])
// 使用h() 函数的第二个参数传入模块需要的数据(对象的形式)
let vnode = h('div', [
h('h1',{ style: { backgroundColor: 'red' } }, 'hello snabbdom'),
h('p', { on: { click: eventHandler } },'这是P标签')
])
function eventHandler() {
console.log('点击我了');
}
let app = document.querySelector('#app')
patch(app, vnode)
patch的整体执行过程
- patch(oldVnode, newVnode)
- 由于Vnode也是树形结构的,所以patch函数内部就是要找这两棵树的差异,过程就是我们常说的diff算法,diff算法就是用来实现查找两棵树的差异
- 核心就是把新节点中变化的内容渲染到真实dom,最后返回新节点作为下一次处理的旧节点
- patch内部,首先会判断对比新旧VNode是否是相同节点(节点的key和节点的选择器sel相同),如果不是相同节点,就不用去对比子节点了,直接删除旧节点,重新渲染新节点
- 如果是相同节点,再判断新的VNode是否有text,如果有并且跟旧的不同,直接更新文本
- 如果新的VNode的children是否有值,判断有没有子节点,依次对比新旧节点的子节点,判断子节点是否有变化
init函数
返回patch函数,使用高阶函数把本该是四个参数patch函数变成了只需要传两个参数的函数,同时创建了多个钩子函数,不同时间做不同的事
patch函数的工作过程
调用了patchVnode、createElm函数进行新旧节点的对比
- 先对比新旧两个节点是不是相同节点
sameVnode()
,是的话调用patchVnode
sameVnode
的原理就是对比key
和sel
是否相同
- 不相同的话调用
createElm
创建新节点,删除旧节点 - 执行插入队列中的insert钩子函数,遍历触发post钩子函数
- 返回 新的 vnode 节点
patchVnode函数的工作过程
- 对比新旧两个节点之前会触发两个钩子函数
- prepatch
- update
- 对比新旧两个节点差异,更新真实dom
- 新节点有text属性,并且不等于老节点
- 如果老节点有children,则移除老节点children对应的dom元素
- 并且设置新节点对应dom元素textContent
- 新老节点都有children并且不相等
- 调用updateChildren钩子函数,对比子节点,并且更新子节点的差异
- 只有新节点有children属性
- 如果老节点有text属性,清空对应dom元素的textContent
- 添加新节点的所有的子节点
- 只有老节点有children属性,移除所有的老节点
- 只有老节点有text属性,清空对应dom元素的textContent
- 新节点有text属性,并且不等于老节点
- 触发postpatch钩子函数
updateChildren函数的整体执行过程
同级别节点比较,比较开始和结束的四种情况
- 使用
sameVnode()
比较oldStartVnode
和newStartVnode
是否是相同节点,比较key和sel- 是相同节点,则调用patchVnode(),比较新旧节点的差异,更新到真实dom上
- 新旧开始节点的索引++ ,让oldStartVnode/newStartVnode指向各自数组的第二个节点
- 使用
sameVnode()
比较oldEndVnode
和newEndVnode
是否是相同节点,比较key和sel- 是相同节点,则调用patchVnode(),比较新旧节点的差异,更新到真实dom上
- 新旧开始节点的索引-- ,让
oldEndVnode
/newEndVnode
指向各自数组的倒数第二个节点
- 使用
sameVnode()
比较oldStartVnode
和newEndVnode
是否是相同节点,比较key和sel- 是相同节点,则调用patchVnode(),比较新旧节点的差异,更新到真实dom上
- 旧开始节点的索引++ ,新的结束节点--,让
oldStartVnode
指向第二个节点,newEndVnode
指向倒数第二个 - 把旧的开始节点对应的dom元素
oldStartVnode.elm
移动到旧的结束节点之后oldEndVnode.elm
之后
- 使用
sameVnode()
比较oldEndVnode
和newStartVnode
是否是相同节点,比较key和sel- 是相同节点,则调用patchVnode(),比较新旧节点的差异,更新到真实dom上
- 把
oldEndVnode.elm
对应的dom元素oldStartVnode.elm
对应的dom元素之前 - 移动相对应的索引
开始和结束节点的比较结束之后,要移动对应的dom元素,可能是进行倒序排序的操作
以上四种情况都不满足的时候,走到下面的第五种情况
- 遍历所有新节点使用新节点的key到老节点数组中找相同key的节点
- 找不到的话就使用
createElm()
在旧的数组队列的oldStartVnode
之前插入一个newStartVnode
- 找到的话,先比较两个节点是不是相同节点,如果不是相同节点,继续走上一步,新生成一个几点,插入
oldStartVnode
之前 - 如果是相同节点,使用
patchVnode()
比较两个节点,然后清空老节点对应索引的值,然后把找到的老节点对的dom元素移动到对应的oldStartVnode.elm
开始节点对应的元素之前 - 索引++ 重新给newStartVnode赋值
- 找不到的话就使用
最后收尾工作当老节点数组或者新节点数组被遍历完的时候,我们需要在老节点数组中移除剩余旧节点或者再新节点数组中新增剩余几点
key的意义
给所有具有相同父元素的子元素设置具有唯一值的key,否则可能造成渲染错误
给Vnode设置key之后,当在对元素列表排序,或者给列表出入项的时候会重用上一次对应的dom对象,减少渲染次数,因此会提高性能
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!