数据驱动
在我们学习Vue.js的过程中,我们经常看到三个概念
- 数据驱动
- 数据响应式
- 双向数据绑定
核心原理分析
- Vue 2.x版本与Vue 3.x版本的响应式实现有所不同,我们可以进行分别讲解
- Vue 2.x响应式基于ES5的Object.defineProperty实现
- Vue 3.x响应式基于ES6的Proxy实现
回顾defineProperty
我们先定义一个对象
var obj = {
name: 'willam',
age: 18
}
在defineProperty中,第一个参数为需要进行操作的对象,第二个参数为属性,第三个为对应的操作
Object.defineProperty(obj, 'gender', {
// 值
value: '男',
// 是否可写
writable: true,
// 控制是否可以枚举(遍历
enumerable: true,
// 本次定义之后,再次进行重新配置
configurable: true
})
Object.defineProperty(obj, 'gender', {
enumerable: false
})
解释一下代码:
赋予值:value
是否可以编辑:writable(这条属性默认值为false,表示只可以读,不可以写入)
是否可以枚举(遍历):enumerable(这条属性默认值也为false)
for (var k in obj) {
console.log(k, obj[k])
}
在本次定义之后,可否再次进行重新配置:configurable:默认值为false,true时可以进行再次的配置
进行属性操作时,可以通过getter,setter实现,访问器和设置器,在访问和设置时进行相应的功能设置
value,writable和get,set无法共存,逻辑冲突
getter指的是:
当我们访问对象的属性时,会执行这个函数
Object.defineProperty(obj, 'gender', {
get () {
// 甚至可以进行额外的操作
console.log('任意需要的自定义操作')
return '男'
},
setter指的是:
当我们设置某个属性时触发的函数
set (newValue) {
console.log('新的值是',newValue)
this.gender = newValue
}
这样写是一个误区,设置时触发setter,就会造成递归
解决办法:
通过第三方数据,来存取数据
var genderValue = '男'
Object.defineProperty(obj, 'gender', {
get () {
console.log('任意需要的自定义操作')
return genderValue
},
set (newValue) {
console.log('新的值是',newValue)
genderValue = newValue
}
})
模拟Vue2响应式原理
- Vue2.x的数据响应式就是由Object.defineProperty()实现的
- 设置data之后,遍历所有的属性,转换为getter和setter,从而在数据变化时进行视图更新操作
我们来写写模拟代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">原始内容</div>
<script>
// 声明一个对象用于进行数据存储
let data = {
msg: 'hello'
}
// 模拟一个vue实例
let vm = {}
// 通过数据劫持的方式,将data的属性设置给getter与setter,并且设置给vm
Object.defineProperty(vm, 'msg', {
// 可遍历
enumerable: true,
// 可配置
configurable: true,
// get方法
get () {
console.log('访问数据')
return data.msg
},
// set方法
set (newValue) {
// 更新数据
data.msg = newValue
// 数据更改,更新视图中DOM元素内容
document.querySelector('#app').textContent = data.msg
}
})
</script>
</body>
</html>
解释一下代码,vm的作用就是通过数据劫持将data中的数据设置给get与set,并且设置给vm,最后更改的还是data
改进
- 操作中只监听了一个属性,多个属性无法处理
- 无法监听数组变化(Vue里也是同样存在这个问题)
- 无法处理属性也为对象的情况
处理多个属性的情况
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">原始内容</div>
<script>
// 声明一个对象用于进行数据存储
let data = {
msg1: 'hello',
msg2: 'world'
}
// 模拟一个vue实例
let vm = {}
Object.keys(data).forEach(key => {
// 通过数据劫持的方式,将data的属性设置给getter与setter,并且设置给vm
Object.defineProperty(vm, key, {
enumerable: true,
configurable: true,
// get方法
get () {
console.log('访问数据')
return data[key]
},
// set方法
set (newValue) {
// 更新数据
data[key] = newValue
// 数据更改,更新视图中DOM元素内容
document.querySelector('#app').textContent = data[key]
}
})
})
</script>
</body>
</html>
这里我们使用到了Object.keys()
方法,该方法可以返回一个由内部参数对象的自身可枚举属性构成的一个数组,然后我们再将其进行forEach遍历,得到每一个属性,然后进行多个属性的处理,详细逻辑可以通过代码看的一清二楚
检测数组的方法
对数组的操作是无法实现响应式数据实现的
Vue通过特定的方法处理可以解决这种问题
- 添加数组方法支持:
const arrMethodName = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
- 准备一个用于存储处理结果的对象,准备替换掉数组属性的原型指针
// 存储处理结果的对象,准备替换到数组数组实例的原型指针 _proto_
const customProto = {}
- 为了确保原始功能能够被使用
// 确保原始功能可以使用,this为数组实例
const result = Array.prototype[method].apply(this, arguments)
- 进行其他自定义设置,比如更新视图
// 进行其他自定义功能设置,比如,更新视图
document.querySelector('#app').textContent = this
return result
- 为了避免数组实例无法再使用我们处理的方法以外的方法:
// 为了避免数组实例无法再使用其他的数组方法
customProto.__proto__ = Array.prototype
- 那么如何将这些设置与拦截写在一起呢?
答案很简单:判断一下就行了
完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">原始内容</div>
<script>
// 声明一个对象用于进行数据存储
let data = {
msg1: 'hello',
msg2: 'world',
arr: [1, 2, 3]
}
// 模拟一个vue实例
let vm = {}
// 添加数组方法的支持
const arrMethodName = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
// 存储处理结果的对象,准备替换到数组数组实例的原型指针 _proto_
const customProto = {}
// 为了避免数组实例无法再使用其他的数组方法
customProto.__proto__ = Array.prototype
arrMethodName.forEach(method => {
customProto[method] = function () {
// 确保原始功能可以使用,this为数组实例
const result = Array.prototype[method].apply(this, arguments)
// 进行其他自定义功能设置,比如,更新视图
document.querySelector('#app').textContent = this
return result
}
})
Object.keys(data).forEach(key => {
// 检测是否为数组,是的话单独处理
if (Array.isArray(data[key])) {
// 将当前数组实例的__proto__更换为customProto就行了
data[key].__proto__ = customProto
}
// 通过数据劫持的方式,将data的属性设置给getter与setter,并且设置给vm
Object.defineProperty(vm, key, {
enumerable: true,
configurable: true,
// get方法
get () {
console.log('访问数据')
return data[key]
},
// set方法
set (newValue) {
// 更新数据
data[key] = newValue
// 数据更改,更新视图中DOM元素内容
document.querySelector('#app').textContent = data[key]
}
})
})
</script>
</body>
</html>
改进:封装与递归
使用立即执行函数,全部包裹起来,如果对象内部还含有对象的话就进行递归处理,很简单的逻辑:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">原始内容</div>
<script>
// 声明数据对象,模拟 Vue 实例的 data 属性
let data = {
msg1: 'hello',
msg2: 'world',
arr: [1, 2, 3],
obj: {
name: 'jack',
age: 18
}
}
// 模拟 Vue 实例的对象
let vm = {}
// 封装为函数,用于对数据进行响应式处理
const createReactive = (function () {
const arrMethodName = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
const customProto = {}
customProto.__proto__ = Array.prototype
arrMethodName.forEach(method => {
customProto[method] = function () {
const result = Array.prototype[method].apply(this, arguments)
document.querySelector('#app').textContent = this
return result
}
})
// 需要进行数据劫持的主体功能,也是递归时需要的功能
return function (data, vm) {
// 遍历被劫持对象的所有属性
Object.keys(data).forEach(key => {
// 检测是否为数组
if (Array.isArray(data[key])) {
// 将当前数组实例的 __proto__ 更换为 customProto 即可
data[key].__proto__ = customProto
} else if (typeof data[key] === 'object' && data[key] !== null) {
// 检测是否为对象,如果为对象,进行递归操作
vm[key] = {}
createReactive(data[key], vm[key])
return
}
// 通过数据劫持的方式,将 data 的属性设置为 getter/setter
Object.defineProperty(vm, key, {
enumerable: true,
configurable: true,
get () {
console.log('访问了属性')
return data[key]
},
set (newValue) {
// 更新数据
data[key] = newValue
// 数据更改,更新视图中 DOM 元素的内容
document.querySelector('#app').textContent = data[key]
}
})
})
}
})()
createReactive(data, vm)
</script>
</body>
</html>
这就是Vue2版本的响应式原理分析
回顾Proxy
ES6提供的一个功能,对一个对象提供代理操作
<script>
const data = {
msg1: '内容',
arr: [1, 2, 3],
obj: {
name: 'willam',
age: 19
}
}
const P = new Proxy(data, {
get (target, property, receiver) {
console.log(target, property, receiver)
return target[property]
},
set (target, property, value, receiver) {
console.log(target, property, value, receiver)
target[property] = value
}
})
</script>
通过代理,访问P也就是访问了data的代理,同样的数据,get方法中,target参数表示原数据data,property表示访问的哪条属性,receiver表示通过代理之后的数据
set方法中新添了一个value参数,表示当前设置的数值
我们来通过控制台打印一探究竟
Vue3响应式原理
与2版本的区别为数据响应式是Proxy实现的,其他相同,接下来进行演示
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">原始内容</div>
<script>
const data = {
msg1: '内容',
arr: [1, 2, 3],
content: 'world',
obj: {
name: 'willam',
age: 19
}
}
const vm = new Proxy(data, {
get (target, key) {
return target[key]
},
set (target, key, newValue) {
// 数据更新
target[key] = newValue
// 视图更新
document.querySelector('#app').textContent = target[key]
}
})
</script>
</body>
</html>
对深层监控啊,属性监控啊,遍历啊都不需要在Vue3进行操作了,通过Proxy代理可以轻松解决,但是由于ES6的Proxy方法兼容性不是那么的好,所以市面上Vue3的普及度并不是太高,一切走向都需要根据市场来确定
相关设计模式
设计模式:针对软件设计中普遍存在的各种问题所提出的解决方案
观察者模式
指的是在对象间定义一个一对多(被观察者与多个观察者)的关联,当一个对象改变了状态,所有其他相关的对象会被通知并且自动刷新
就像是超市有一堆顾客,超市出了促销活动,会通知顾客(观察者),又因为当前是否想要购物,进行不同的选择行动
- 核心概念:
- 观察者Observer
- 被观察者(观察目标)Subject
设计的核心点就是设置一个被观察者,设置一个或者多个的观察者,在被观察者中设置一个遍历进行操作
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
// 被观察者(观察目标)
// 1.需要能够添加观察者
// 2.通知所有观察者的功能
class Subject {
constructor () {
// 存储所有的观察者
this.observers = []
}
// 添加观察者功能
addObserver (observer) {
// 检测传入的参数是否为观察者实例
if (observer && observer.update) {
this.observers.push(observer)
}
}
// 通知所有的观察者
notify () {
// 调用观察者列表中的每个观察者的更新方法
this.observers.forEach(observer => {
observer.update()
})
}
}
// 观察者
// 1.被观察者发生状态变化时,做一些对应的操作“更新”
class Observer {
update () {
console.log('事件发生了,进行一个相应的处理...')
}
}
// 功能测试
const subject = new Subject()
const ob1 = new Observer()
const ob2 = new Observer()
// 将观察者添加给要观察的观察目标
subject.addObserver(ob1)
subject.addObserver(ob2)
// 通知观察者进行操作(某些具体的场景下)
subject.notify()
</script>
</body>
</html>
通过观察者模式为不同的数据设置不同的观察者,监视被观察者的情况,通过特定的方法进行更新操作等等
发布-订阅模式
可以认为是为观察者模式的解耦的进阶版本,特点是:
- 在发布者和订阅者之间添加一个消息中心,所有的消息均通过消息中心管理,而发布者与订阅者不会直接联系,实现了两张的解耦
核心概念:
- 消息中心Dep
- 订阅者Subscriber
-
发布者Publisher
<body>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.12"></script>
<script>
// 创建了一个Vue实例(消息中心)
const eventBus = new Vue()
// 注册事件(设置订阅者)
eventBus.$on('dataChange', () => {
console.log('事件处理功能1')
})
eventBus.$on('dataChange', () => {
console.log('事件处理功能2')
})
// 触发事件(设置发布者)
eventBus.$emit('dataChange')
</script>
</body>
设计模式小结
- 观察者模式是由观察者和观察目标组成的,适合组件内部操作(功能简单就可以)
- 特性:特殊事件发生后,观察目标统一通知所有的观察者
- 发布/订阅模式由发布者与订阅者以及消息中心组成,更加适合消息类型复杂的情况
- 特性:特殊事件发生,消息中心接到发布指令后,会根据事件类型给对应的订阅者发送信息
响应式原理模拟
整体分析
要模拟Vue实现响应式数据,首先我们需要观察一下Vue实例的结构,分析要实现哪些属性和功能
- Vue:
- 目标:将data数据注入到Vue实例,便于方法内操作
- Observer(发布者)
- 目标:数据劫持,监听数据变化,并在变化时通知Dep
- Dep(消息中心)
- 目标:存储订阅者以及管理消息的发送
- Watcher(订阅者)
- 目标:当订阅数据变化,进行视图更新
- Compiler
- 目标:解析模板中的指令与插值表达式,并替换成相应的数据
Vue类
- 功能:
- 接受配置信息
- 将data的属性转换为Getter、setter,并且注入到Vue实例中
- *监听data中所有属性的变化,设置成响应式数据
-
*调用解析功能(解析模板内的插值表达式,指令等等)
n _proxyData (target, data) {
Object.keys(data).forEach(key => {
Object.defineProperty(target, key,{
enumerable: true,
configurable: true,
get () {
return data[key]
},
set (newValue) {
data[key] = newValue
}
})
})
}
Observer类
- 功能:
- 通过数据劫持方式监视data中的属性变化,变化时通知消息中心Dep
-
需要考虑data的属性也可能为对象,也要转换成响应式数据
Dep类
- Dep是dependency的简写,含义是“依赖”,指的是Dep用于收集与管理订阅者与发布者之间的依赖关系
- 功能:
- *为每个数据收集对应的依赖,存储依赖
- 添加并存储订阅者
-
数据变化时,通知所有的观察者
Watcher 类
- 功能:
- 实例化Watch时,往dep对象中添加自己
-
当数据变化触发dep,dep通知所有对应的Watcher实例更新视图
Complier类
- 功能:
- 进行编译模板,并解析内部指令与插值表达式
- 进行页面的首次渲染
-
数据变化后,重新渲染视图
功能回顾与总结
- Vue类
- 把data的属性注入到Vue实例
- 调用Observer实现数据响应式处理
- 调用Compiler编译模板
- Observer
- 将data的属性转换为Getter/setter
- 为Dep添加订阅者Watcher
- 数据变化发送时通知Dep
- Dep
- 收集依赖,添加订阅者(Watcher)
- 通知订阅者
- Watcher
- 编译模板时创建订阅者,订阅数据变化
- 接到Dep通知时,调用Compiler中的模板功能更新视图
- Compiler
- 编译模板,解析指令与插值表达式
-
负责页面首次渲染与数据变化后重新渲染