《Vue3 十》Vue 底层原理
命令式编程和声明式编程:
以计时器为例:
// 原生 JavaScript 实现计数器,是命令式编程
<div>
<h1>当前数字:<span class="count"></span></h1>
<button class="add" @click="handleAdd">+1</button>
<button class="sub" @click="handleDelete">-1</button>
</div>
const countEl = document.querySelector('.count')
const addEl = document.querySelector('.add')
const subEl = document.querySelector('.sub')
let count = 0
countEl.textContent = count
addEl.onclick =function () {
count++;
countEl.textContent = count
}
subEl.onclick =function () {
count--;
countEl.textContent = count
}
// Vue 实现计时器,是声明式编程
<div id="app">
<h1>当前数字:{{count}}</h1>
<button @click="handleAdd">+1</button>
<button @click="handleSub">-1</button>
</div>
const app = Vue.createApp({
data() {
return {
count: 0,
}
},
methods: {
handleAdd: function() {
this.count++;
},
handleSub: function() {
this.count--;
},
}
})
app.mount('#app')
- 命令式编程:需要将要做的每一个步骤都编写一条代码,转化成一个个的命令告知浏览器。关注的是 how to do,自己完成整个 how 的过程。
原生 JavaScript、jQuery 都是命令式编程。
- 声明式编程:需要哪些东西就声明哪些东西,具体如何实现不需要管,框架会帮助完成。关注的是 what to do,由框架完成 how 的过程。
目前,Vue、React、Angular、小程序等都是声明式编程。
MVC 模型和 MVVM 模型:
MVC:
MVC,即 Mode l- View - Controller。
- Model:数据模型。负责保存应用数据,与后端数据进行同步。
- View:UI 视图。负责视图展示,将 Model 中的数据可视化出来。
- Controller:控制器。负责业务逻辑,根据用户行为对 Model 数据进行更改。
MVC 中所有的通信都是单向的。View 传送指令到 Controller;Controller 完成业务逻辑后,要求 Model 改变状态;Model 将新的数据发送到 View,用户得到反馈。
MVVM:
MVVM ,即 Model - View - ViewModel 。
- Model:数据模型。
- View:UI 视图。
- ViewModel:View 和 Model 的连接器。View 和 Model 通过 ViewModel 实现双向绑定,ViewModel 在底层做了大量的事情来实现数据变化更新视图,视图变化更新数据。
React、Vue、Angular 都是 MVVM 框架。
单向数据流:
数据的流动是单向的。父组件传递给子组件数据,子组件只能使用不能修改。
双向数据绑定:
双向数据绑定:即数据变化更新视图,视图变化更新数据。
Vue2 的响应式原理:
Vue2 的响应式原理基于 Object.defineProperty()
实现,利用 Object.defineProperty()
中的 set()
方法对对象的属性进行劫持,监听到对象属性的改变,就自动执行依赖的代码。
手动实现 Vue2 的响应式:
// 1. 定义一个 watchFn 函数,接收一个回调函数,当其内部依赖到的数据发生变化时,自动执行该回调函数
let reactiveFn = null
function watchFn(fn) {
reactiveFn = fn
fn()
reactiveFn = null
}
// 2. 定义一个收集依赖的类
class Depend {
constructor() {
// 一个数据可能会被多次依赖,使用 Set 的目的是为了防止添加的依赖的函数重复
this.reactiveFns = new Set()
}
addDepend(fn) {
if (!fn) return
this.reactiveFns.add(fn)
}
notify() {
this.reactiveFns.forEach(fn => {
fn()
})
}
}
// 3. 定义一个通过对象的属性名获取对应的 depend 依赖的函数。每一个对象的每一个属性对应一个 depend 依赖对象;通过一个 map 对象存储一个对象中所有属性与其 depend 依赖对象的映射关系;再通过一个 objMap 对象存储所有对象与其 map 对象的映射关系
const objMap = new WeakMap()
function getDepend(obj, key) {
// 根据对象,找到对应的 map
let map = objMap.get(obj)
if (!map) {
map = new Map()
objMap.set(obj, map)
}
// 根据属性名,找到对应的 depend
let dep = map.get(key)
if(!dep) {
dep = new Depend()
map.set(key, dep)
}
return dep
}
// 4. 监听对象中数据的访问,自动收集其依赖;监听对象中数据的改变,自动执行依赖的代码
function reactive(obj) {
Object.keys(obj).forEach(key => {
let value = obj[key]
Object.defineProperty(obj, key, {
get: function() {
// 找到对象的属性名对应的 depend,收集依赖
const dep = getDepend(obj ,key)
dep.addDepend(reactiveFn)
return value
},
set: function(newValue) {
value = newValue
// 找到对象的属性名对应的 depend,自动执行
const dep = getDepend(obj ,key)
dep.notify()
}
})
})
return obj
}
// ------------上面的是封装的实现响应式的代码,下面的是业务代码------------------ //
// 1. 定义一个要收集依赖的对象
const user = reactive({
name: 'Lee',
age: 18
})
// 2. 如果对象中的数据发生变化,自动执行传入的回调函数
watchFn(function() {
console.log('name 发生变化:' + user.name)
})
watchFn(function() {
console.log('age 发生变化:' + user.age)
})
// 3. 改变对象中的数据,依赖这个数据的代码将会自动执行
user.name='Mary'
Vue3 的响应式原理:
Vue3 的响应式原理基于 Proxy 实现。
手动实现 Vue3 的响应式:
其他部分的代码和手动实现 Vue2 的响应式中一样,只有监听数据的改变对其进行劫持的方式有变化。
// 4. 监听对象中数据的访问,自动收集其依赖;监听对象中数据的改变,自动执行依赖的代码
function reactive(obj) {
const objProxy = new Proxy(obj, {
get: function(target, key) {
const dep = getDepend(target, key)
dep.addDepend(reactiveFn)
return target[key]
},
set: function(target, key, newValue) {
target[key] = newValue
const dep = getDepend(target, key)
dep.notify()
}
})
return objProxy
}
虚拟 DOM 和 Diff 算法:
虚拟节点 VNode:
VNode:全程是 Virtual Node,虚拟节点。VNode 本质上就是一个 JavaScript 对象。Vue 会先将模板中的元素解析为一个个 VNode,然后再将一个个 VNode 渲染为真实 DOM 节点。
虚拟 DOM VDOM:
VDOM:全称是 Virtual DOM,虚拟 DOM。一堆的元素就会形成一个虚拟节点树,也就是一棵以 JavaScript 对象为基础的树,它是一层对真实 DOM 的抽象,最终可以通过一系列操作使这棵树映射到真实环境上。
VDOM 的优势有:
- 可以进行 Diff 算法:运用 Diff 算法来计算出真正需要更新的节点,最大限度地减少 DOM 操作,从而显著提高性能。
- 方便进行跨平台:本质上只是 JavaScript 对象,只要对其进行不同的解析,就可以方便地进行跨平台。
Diff 算法:
虚拟DOM的最终目标是将虚拟节点渲染到视图上。但是如果直接使用虚拟节点覆盖旧节点的话,会有很多不必要的DOM操作(例如:一个ul标签下很多个li标签,其中只有一个li有变化,这种情况下如果使用新的ul去替代旧的ul,会因为这些不必要的DOM操作而造成了性能上的浪费)。为了避免不必要的DOM操作,虚拟DOM在虚拟节点映射到视图的过程中,将虚拟节点与上一次渲染视图所使用的旧虚拟节点(oldVnode)做对比,找出真正需要更新的节点来进行DOM操作,从而避免操作其他无需改动的DOM。虚拟DOM在Vue.js主要做了两件事:
- 提供与真实DOM节点所对应的虚拟节点vnode;
- 将虚拟节点vnode和旧虚拟节点oldVnode进行对比,然后更新视图;
Vue的diff算法是基于snabbdom改造过来的,仅在同级的vnode间做diff,递归地进行同级vnode的diff,最终实现整个DOM树的更新。
diff 算法包括几个步骤:
- 用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中;
- 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异;
- 把所记录的差异应用到所构建的真正的DOM树上,视图就更新了;