大前端学习 -- 虚拟 DOM 和 Diff 算法 学习笔记
虚拟 DOM 和 Diff 算法
Virtual DOM的实现原理
一、Virtual DOM介绍
1. 什么是Virtual DOM
-
Virtual DOM(虚拟DOM),是由普通的JS对象描述DOM对象,因为不是真实的DOM对象,所以叫做Virtual DOM
-
真实的DOM成员 非常非常多,所以创建一个DOM对象的成本非常高
-
可以通过Virtual DOM来描述真实DDOM,示例:
{ sel: 'div', data: {}, text: 'Hello Virtual DOM', elm: undefined, key: undefined }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
Virtual DOM对象是非常小的,我们创建一个Virtual DOM的成本比创建一个真实DOM的成本小很多
2. 为什么使用Virtual DOM
- 手动操作DOM比较麻烦,还需要考虑浏览器兼容性问题,虽然有jQuery等库简化DOM操作,但是随着项目的复杂
DOM操作复杂提升 - 为了简化DOM的复杂操作于是出现了各种MVVM框架,MVVM 框架解决了视图和状态的同步问题
- 为了简化视图的操作我们可以使用模板引擎,但是模板引擎没有解决跟踪状态变化的问题,于是Virtual DOM出现了
Virtual DOM的好处是当状态改变时不需要立即更新DOM,只需要创建一个虚拟树来描述DOM, Virtual DOM内部
将弄清楚如何有效(diff)的更新DOM - 参考github上virtual-dom的描述
- 虚拟DOM可以维护程序的状态,跟踪上一次的状态
- 通过比较前后两次状态的差异更新真实DOM
3. 虚拟DOM的作用
- 维护视图和状态的关系
- 复杂视图情况下提升渲染性能
- 除了渲染DOM以外,还可以实现SSR(Nuxt.js/Next.js)、原生应用(Weex/React Native )、小程序(mpvue/uni-app)等
4. Virtual DOM库
- Snabbdom
- Vue2.x内部使用的Virtual DOM 就是改造的Snabbdom
- 大约200SLOC(Single Line Of Code)
- 通过模块可扩展
- 源码使用TypeScript开发
- 最快的Virtual DOM之一
- Virtual DOM
二、Snabbdom基本使用
1. 创建项目
-
打包工具为了方便使用Parcel
-
创建项目,并安装Parcel
# 创建项目目录 mkdir snabbdom-demo # 进入项目目录 cd snabbdom-demo # 创建package.json yarn init -y # 本地安装Parcel yarn add parcel-bundler # 安装snabbdom@0.7.4 yarn add snabbdom@0.7.4
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
-
配置package.json的script
"scripts": { "dev": "parcel index.html --open", "build": "parcel build index.html" }
- 1
- 2
- 3
- 4
-
创建目录结构
|-index.html |-package.json |-src |-01.basicusage.js
- 1
- 2
- 3
- 4
2. 导入Snabbdom
-
Snabbdom的官网demo中导入使用的是CommonJS模块化语法,我们使用的是更流行的ES6模块化的语法import
-
关于模块化的语法可以参考阮一峰老师的《Module的语法》
-
ES6模块化与CommonJS模块的差异
import { h, init, thunk } from 'snabbdom'
- 1
-
Snabbdom的核心仅提供最基本的功能,只导出了三个函数init()、h()、thunk()
-
init()是一个高阶函数,返回patch()
-
h()返回虚拟节点VNode,这个函数我们在使用Vue.js的时候用过
new Vue({ router, store, render: h => h(App) }).$mount('#app')
- 1
- 2
- 3
- 4
- 5
-
thunk()是一种优化策略,可以在处理不可变数据时使用
-
-
注意:导入时候不能使用
import snabbdom from 'snabbdom'
- 原因:node_modules/src/snabbdom.ts末尾导出使用的语法是export导出API,没有使用export default导出默认导出
4. Snabbdom的基本用法
Snabbdom开源仓库地址:
https://github.com/snabbdom/snabbdom
-
init函数、h函数、patch函数的使用
-
01-basicusage.js
import { h, init } from 'snabbdom' // 1. hello world // init方法,参数:数组,模块 // 返回值:patch函数,作用是对比两个vnode的差异更新到真实DOM let patch = init([]) // h函数: // 第一个参数:标签+选择器, // 第二个参数:如果是字符串的话,就是标签中的内容 // 返回值:vnode let vnode = h('div#container.cls', 'Hello world') // 要占位的DOM元素 let app = document.querySelector('#app') // patch函数 // 第一个参数:可以是DOM元素,内部会把DOM元素转化成VNode // 第二个参数:VNode // 返回值:VNode let oldVNode = patch(app, vnode) // 假设的时刻 vnode = h('div', 'Hello Snabbdom') patch(oldVNode, vnode)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
-
02-basicusage.js
// 2. div中放置子元素 h1, p import { h, init } from 'snabbdom' let patch = init([]) let vnode = h('div#container', [h('h1', 'hello snabbdom'), h('p', '这是一个p标签')]) let app = document.querySelector('#app') let oldVnode = patch(app, vnode) setInterval(() => { vnode = h('div#container', [h('h1', 'hello world'), h('p', 'hello p')]) patch(oldVnode, vnode) // 清空页面内容--官网上的写法是错误的 // patch(oldVnode, null) // 这是正确的写法,第二个参数是一个注释节点 patch(oldVnode, h('!')) }, 2000);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
-
5. Snabbdom中的模块
Snabbdom的核心库并不能处理元素的属性/样式/事件等,如果需要处理的话,可以使用模块
常用模块
-
官方提供了6个模块
- attributes
- 设置了DOM元素的属性,使用setAttribute()
- 处理布尔类型的属性
- props
- 和attributes模块相似,设置DOM元素的属性element[attr] = value
- 不处理布尔类型的属性
- class
- 切换类样式
- 注意:给元素设置类样式是通过set选择器
- dataset
- 设置data-*的自定义属性
- eventlisteners
- 注册和移除事件
- style
- 设置行内样式,支持动画
- delayed/remove/destroy
- attributes
-
模块使用
-
模块会用步骤:
- 导入需要的模块
- init()中注册模块
- 使用h()函数创建VNode的时候,可以把第二个参数设置为对象,其他参数往后移
import { init, h } from 'snabbdom' // 1. 导入需要的模块 import style from 'snabbdom/modules/style' import eventlisteners from 'snabbdom/modules/eventlisteners' // 2. init()中注册模块 let patch = init([ style, eventlisteners ]) // 3. 使用h()函数创建VNode的时候,可以把第二个参数设置为对象,其他参数往后移 let vnode = h('div', { style: { backgroundColor: 'red' }, on: { click: eventHandler } }, [h('h1', 'Hello Snabbdom'), h('p', 'this is a p')]) let app = document.querySelector('#app') patch(app, vnode) function eventHandler () { console.log('click~~') }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
-
6. Snabbdom源码解析
- 如何学习源码
- 先宏观了解
- 待着目标看源码
- 看源码的过程要不求甚解
- 调试
- 参考资料
- Snabbdom的核心
- 使用h()函数创建JavaScript对象(VNode)描述真实DOM
- init设置模块,创建patch()
- patch()比较新旧两个VNode
- 把变化的内容更新到真实DOM树上
- Snabbdom源码
- 源码地址:
- Https://github.com/snabbdom/snabbdom
- 源码地址:
7. h函数
-
h函数介绍
-
在使用Vue的时候见过h函数
new Vue({ router, store, render: h => h(App) }).$mount('#app')
- 1
- 2
- 3
- 4
- 5
-
h函数最早见于hyperscript,使用JavaScript创建超文本
-
Snabbdom中的h函数不是用来创建超文本,而是创建VNode
-
-
函数重载
-
概念
- 参数个数或者类型不同的函数
- JavaScript中没有重载的概念
- TypeScript中有从在,不过重载的实现还是通过代码调整参数
-
重载的示意
function add (a, b) { console.log(a + b) } function add (a, b, c) { console.log(a + b + c) } add(1, 2) add(1, 2, 3)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
-
-
源码位置:src/h.ts
8. patch函数
- patch(oldVnode, newVnode)
- 打补丁,把新节点中变化的内容渲染到真实DOM,最后返回新节点作为下一次处理的旧节点
- 对比新旧VNode是否相同节点(节点的key和sel相同)
- 如果不是相同节点,删除之前的内容,重新渲染
- 如果是相同节点,再判断新的VNode是否有text, 如果有并且和oldVnode的text不同,直接更新文本内容
- 如果新的VNode有children,判断子节点是否有变化,判断子节点的过程使用的就是diff算法
- diff 过程只进行同层级比较
9. init函数
- 源码位置:src/snabbdom.ts
文章来源: blog.csdn.net,作者:爱玲姐姐,版权归原作者所有,如需转载,请联系作者。
原文链接:blog.csdn.net/jal517486222/article/details/107444630
- 点赞
- 收藏
- 关注作者
评论(0)