# 一、数据响应式
# 1、需求分析
当我们的数据 A 发现改变时,希望所以受数据 A 影响的方法等会同步重新执行。
提示
可以结合 react 的数据更新机制思考一下
// 数据
let num = 6
// 视图
const render = () => {
return console.log(num * num) // 36
}
render()
// 数据更新
num = 8 // 数据并没有响应式更新,console并没有重新执行
但是更多的情况下,我们是需要对象能够实现响应式:
// 数据
let numb = 18
let person = {
name: '张三',
set: '男',
age: numb
}
// 视图
const render = () => {
return console.log(person.age) // 18
}
render()
// 数据更新
person.age = numb = 20 // 数据并没有响应式更新,console并没有重新执行
# 2、实现逻辑(手动)
我们可以编写一个函数去监听数据的变化,一旦发生变化就让其执行 render 函数
下面我们可以简单的实现一下(使用手动触发 render 重新执行)
基本实现
// 监听所有render
const reactiveFns = []
function watchFn(fn) {
reactiveFns.push(fn)
fn()
}
// ……
// 视图
const render = () => {
return console.log(person.age)
}
// render()
watchFn(render)
// ……
// 重新触发所有render
reactiveFns.forEach((fn) => {
fn()
})
基本实现 Plus(使用 class 封装)
- 封装
class Depend {
constructor() {
this.reactiveFns = []
}
// 监听render
addDepend(fn) {
if (fn) {
this.reactiveFns.push(fn)
}
}
// 触发render更新
notify() {
this.reactiveFns.forEach((fn) => {
fn()
})
}
}
- 使用
const dep = new Depend()
// 监听所有render
function watchFn(fn) {
dep.addDepend(fn)
fn()
}
// ……
// 重新触发所有render
dep.notify()
为了方便与后面的内容做对比,先截取部分代码:
const dep = new Depend()
// 监听所有render
function watchFn(fn) {
dep.addDepend(fn)
fn()
}
// ……
// 重新触发所有render
dep.notify()
# 3、vue2 响应式
使用 Object.defineProperty() ----> vue2
简约版
const dep = new Depend()
// 2、重新触发所有render(自动✍执行)
Object.keys(obj).forEach((key) => {
// 知识点:闭包、防递归
let value = obj[key]
Object.defineProperty(obj, key, {
set: function (newValue) {
value = newValue
dep.notify()
},
get: function (value) {
return value
}
})
})
上面的简约版中,还有一个问题就是,它每次都会重新触发所有的 render,而不是与更改数据相关的 render:
解决方案
使用数据结构:
- 核心代码
// const dep = new Depend()
const objMap = new WeakMap()
function getDepend(obj, key) {
// 根据对象obj,找对应的map对象
let map = objMap.get(obj)
if (!map) {
map = new Map()
objMap.set(obj, map)
}
// 根据key,找对应的depend对象
let dep = map.get(key)
if (!dep) {
dep = new Depend()
map.set(key, dep)
}
return dep
}
// 1、监听所有render
let reactiveFn = null
function watchFn(fn) {
reactiveFn = fn
fn()
reactiveFn = null
}
// ……
// 2、重新触发所有render(自动✍执行)
// 这里其实还有优化的空间(提示:将这里封装成一个函数reactive(obj),这样obj的位置就没有限制了)
Object.keys(obj).forEach((key) => {
// 知识点:闭包、防递归
let value = obj[key]
Object.defineProperty(obj, key, {
set: function (newValue) {
value = newValue
const dep = getDepend(obj, key)
dep.notify()
},
get: function (value) {
const dep = getDepend(obj, key)
dep.addDepend(reactiveFn) // 这里其实还有优化的空间(提示:使用Set代替原class中的[])
return value
}
})
})
优化 1
class Depend {
constructor() {
this.reactiveFns = new Set()
}
// 监听render
addDepend(fn) {
if (fn) {
this.reactiveFns.add(fn)
}
}
depend() {
if (reactiveFn) {
this.reactiveFns.add(reactiveFn)
}
}
// 触发render更新
notify() {
this.reactiveFns.forEach((fn) => {
fn()
})
}
}
# 4、vue3 响应式
使用 new Proxy() ---- vue3
其实 vue2 中已经完成了基本所有的功能,vue3 中对其的优化点大方向有两点:
- 组合式 api
数据对象可以放在任何位置
优化 2
function reactive(obj) {
Object.keys(obj).forEach((key) => {
// 使用 Object.defineProperty()
// ……
})
return obj
}
- 新采用 new Proxy
new Proxy 支持 obj,不用像 Object.defineProperty()那样对 obj 监听还要使用 foreach
function reactive(obj) {
const objProxy = new Proxy(obj, {
set: function (target, key, newValue, receiver) {
// target[key] = newValue
Reflect.set(target, key, newValue, receiver)
const dep = getDepend(obj, key)
dep.notify()
},
get: function (target, key, receiver) {
const dep = getDepend(obj, key)
dep.depend()
// return target[key]
return Reflect.get(target, key, receiver)
}
})
return objProxy
}
# 二、响应式体验
# 1、数据代理
一听到代理,我们就可以想到我在 JavaScript 的 ES6 部分做的笔记(里面做了详细的介绍):
在开始前,我们先补充一个知识点:
# 前提:参考下面的案例
vm._data === data # true
# 所以操作data可以有两种方式:
# 方式1
vm._data.username = 'one' # vue构造函数收集的data数据(并且数据是响应式的:数据劫持)
# 方式2
vm.username = 'one' # 数据代理
上面只能是一个猜想,下面来验证一下是否存在数据代理:
vm 对象
代理对_data 对象
中的属性进行操作(读/写)
<div id="app">{{username}}</div>
<script>
let data = {
username: 'lencamo'
}
let vm = new Vue({
el: '#app',
data
})
// 1、getter验证
data.username = 'ren'
console.log(vm.username) // "ren"
// 2、setter验证
// vm对象可以🤔对data进行操作
vm.username = 'lili'
console.log(data.username) // "lili"
</script>
结论:
所以 vue 对
vm._data
使用数据代理,可以简化或者说更方便的操作 data 中的数据
<div id="app">
<p>{{_data.username}}</p>
<!-- 简化 -->
<p>{{username}}</p>
</div>
# 2、数据劫持
vue 中最核心的一个点就是响应式数据,上面的案例中,我们可以发现一个问题:
vm._data 的内容并不是一个 data 的一个拷贝,转而好像做了数据代理(但实际上是为了实现响应式数据而做的数据拦截)
前面我们也说了,在 vue 中 数据驱动视图。也就是说 vue 实现:
例如:如果 data 数据中的
username
发生改变,视图中的\{\{username}}
也要同步更新
vue 中最核心的一个点就是响应式数据(若数据改变,视图要同步更新)。
<div id="app">
<p>{{username}}</p>
</div>
<script>
let vm = new Vue({
el: '#app',
data: {
username: 'lencamo'
}
})
console.log(vm)
</script>
其实,我们通过观察它们的 getter 和 setter 命名也可以看出不同(前面:reactiveSetter 响应式、后面:proxySetter 代理式):
vm.username
如图(数据代理):
# 3、实现原理
数据代理主要用于将组件的数据和属性访问(_data)委托给 Vue 实例本身,以便在访问和修改组件数据时可以触发 Vue 的响应式系统;
数据代理
效果如下:
let vm = {
//
_data: {
title: '数据代理'
}
}
// 数据代理🤔
Object.defineProperty(vm, 'title', {
get: function proxyGetter() {
return vm._data.title
},
set: function proxySetter(val) {
vm._data.title = val
}
})
console.log(vm)
console.log(vm.title) // 数据代理
// 1、getter验证
vm._data.title = 'one'
console.log(vm.title) // one
// 2、setter验证
vm.title = 'two'
console.log(vm._data.title) // two
let vm = {
//
_data: {
username: 'lencamo',
age: 21
}
}
// 数据代理🤔
Object.keys(vm._data).forEach((key) => {
Object.defineProperty(vm, key, {
get: function proxyGetter() {
return vm._data[key]
},
set: function proxySetter(val) {
vm._data[key] = val
}
})
})
console.log(vm)
数据劫持主要用于拦截和处理组件数据的访问和修改操作,以便在数据变化时可以通知 Vue 响应式系统更新视图
数据劫持
效果如下:
let msg = '数据劫持'
let vm = {
//
_data: {
title: msg
}
}
// 数据代理🤔
Object.defineProperty(vm._data, 'title', {
get: function reactiveGetter() {
return msg
},
set: function reactiveSetter(newVal) {
msg = newVal
}
})
console.log(vm)
// 验证:
msg = 'lencamo'
console.log(vm._data.title)
let username = '数据劫持'
let age = 21
let vm = {
//
_data: {
username: username,
age: age
}
}
// 数据代理🤔
Object.keys(vm._data).forEach((key) => {
Object.defineProperty(vm._data, key, {
get: function reactiveGetter() {
return key // 注意这里的意思
},
set: function reactiveSetter(newVal) {
key = newVal
}
})
})
console.log(vm)
通过上面对数据劫持和数据代理的代码还原,我们发现如果要对对象和数组进行响应式的话,是不可以的。解决办法:见后面
完整模拟版
// 视图
let username = 'lencamo'
let age = 21
// new Vue中的data
let data = {
username: username,
age: age
}
// vm实例对象
let vm = {
//
_data: data
}
// 数据劫持
Object.keys(data).forEach((key) => {
Object.defineProperty(this, key, {
get: function reactiveGetter() {
return key // 这就是为什么要使用this🤔的原因
},
set: function reactiveSetter(newVal) {
key = newVal
}
})
})
// 数据代理
Object.keys(data).forEach((key) => {
Object.defineProperty(vm, key, {
get: function proxyGetter() {
return data[key]
},
set: function proxySetter(val) {
data[key] = val
}
})
})
console.log(data)
console.log(vm)
// 验证:
// 2、数据双向绑定
username = 'success'
console.log(data.username)
console.log(vm.username)
// 1、数据驱动视图
data.username = 'ren'
console.log(data.username)
console.log(vm.username)
# 4、全局 set ✨
# ① 问题描述:
前面我们看到了,vue 会自动对我们的 data 数据进行处理(数据代理、数据拦截等)。
但是如果我们的数据没有在 data 中定义,而且通过后续追加的话,我们发现这些追加的新数据并没有实现响应式 🤔。
<div id="app">
<p>{{username}}</p>
<button @click="addAge">追加age数据</button>
</div>
<script>
let vm = new Vue({
el: '#app',
data: {
username: 'lencamo'
},
methods: {
addAge() {
// vm._data.age = 21
this._data.age = 21
}
}
})
console.log(vm)
</script>
# ② vm.$set
对于已经创建的实例,Vue 不允许动态添加根级别的响应式 property。但是,可以使用 Vue.set(object, propertyName, value)
方法向嵌套对象添加响应式 property。
<div id="app">
<p>{{person.username}}</p>
<button @click="addAge">追加age数据</button>
<p>{{person.age}}</p>
<p>{{person.password}}</p>
</div>
<script>
let vm = new Vue({
el: '#app',
data: {
person: {
username: 'lencamo'
}
},
methods: {
addAge() {
// 方式1
// Vue.set(this._data.person, 'age', 21)
// 简写
Vue.set(this.person, 'age', 21)
// 方式2
// this.$set(this._data.person, 'password', '123456')
// 简写
this.$set(this.person, 'password', '123456')
}
}
})
console.log(vm)
</script>
# 三、数据监测 🎈
提示
但是,在 vue2 的响应式中,还是存在一些问题的:
- 比如 vue2 不能检测数组和对象的变化。
这些问题,vue2 也给我们提供了解决方案:
对象 | 数组 |
---|---|
Vue.set(object, propertyName, value) | Vue.set(vm.items, indexOfItem, newValue) |
Vue.delete(object, propertyName) | 经过 vue 包裹的 Array 数组方法 |
# 1、监测对象
注意
Vue 无法检测 property 的添加或移除
Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。
即:Vue 不允许动态添加根级别的响应式 property。
但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式 property。
有时你可能需要为已有对象赋值多个新 property,要保持响应式,必须创建一个新的对象。
let vm = new Vue({
el: '#app',
data: {
person: {
name: 'ren',
age: 20
}
},
methods: {
changeName() {
// 1、新增属性
// 无效
// this.person.sex = '男'
// 有效
this.$set(this.person, 'sex', '男')
// 2、删除属性
// 无效
// delete this.person.age
// 有效
this.$delete(this.person, 'age')
// 3、拓展(新增对个)
// 无效
// this.person = Object.assign(this.person, { a: 1, b: 2 })
// 有效
this.person = Object.assign({}, this.person, { a: 1, b: 2 })
}
}
})
# 2、监测数组
注意
Vue 不能检测以下数组的变动:
- 当你利用索引直接设置一个数组项时,例如:
vm.items[indexOfItem] = newValue
- 当你修改数组的长度时,例如:
vm.items.length = newLength
# ① 官方方案
方式 1:使用 vue 包裹后的 Array 数组方法操作
方式 2:使用前面讲的
# ② 代码实操
数组数据下是没有 getter 和 setter 的,转而是使用经过 vue 包裹的 Array 数组方法。
原因分析:
代码演示:
<script>
let vm = new Vue({
el: '#app',
data: {
list: [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' },
{ id: 3, name: '王五' }
]
},
methods: {
changeName() {
// 无效
// this.list[0] = { id: 1, name: '李明' }
// 有效
// 方式1
this.list.splice(0, 1, { id: 4, name: '李明' })
// 方式2
// this.$set(this.list, 0, { id: 4, name: '李明' })
}
}
})
console.log(vm._data.list.splice === Array.prototype.splice) // false
</script>