1.什么是响应式?
我们先来看一下响应式意味着什么?我们来看一段代码:
m有一个初始化的值,有一段代码使用了这个值
那么在
m
有一个新的值时,这段代码可以自动重新执行
上面的这样一种可以自动响应数据变量的代码机制,我们就称之为是响应式的
- 那么我们再来看一下对象的响应式:

2.响应式函数设计
- 首先,执行的代码中可能不止一行代码,所以我们可以将这些代码放到一个函数中:
- 那么我们的问题就变成了,当数据发生变化时,自动去执行某一个函数

- 但是有一个问题:在开发中我们是有很多的函数的,我们如何区分一个函数需要响应式,还是不需要响应式呢?
- 很明显,下面的函数中
foo
需要在obj
的name
发生变化时,重新执行,做出相应 bar
函数是一个完全独立于obj
的函数,它不需要执行任何响应式的操作
- 很明显,下面的函数中

3.响应式函数的实现watchFn
- 但是我们怎么区分呢?
- 这个时候我们封装一个新的函数
watchFn
- 凡是传入到
watchFn
的函数,就是需要响应式的 - 其他默认定义的函数都是不需要响应式的
- 这个时候我们封装一个新的函数

4.响应式依赖的收集
- 目前我们收集的依赖是放到一个数组中来保存的,但是这里会存在数据管理的问题:
- 我们在实际开发中需要监听很多对象的响应式
- 这些对象需要监听的不只是一个属性,它们很多属性的变化,都会有对应的响应式函数
- 我们不可能在全局维护一大堆的数组来保存这些响应函数
- 所以我们要设计一个类,这个类用于管理某一个对象的某一个属性的所有响应式函数:
- 相当于替代了原来的简单
reactiveFns
的数组
- 相当于替代了原来的简单

5.监听对象的变化
- 那么我们接下来就可以通过之前学习的方式来监听对象的变量:
- 方式一:通过
Object.defineProperty
的方式(vue2
采用的方式) - 方式二:通过
new Proxy
的方式(vue3
采用的方式)
- 方式一:通过
- 我们这里先以
Proxy
的方式来监听:

6.对象的依赖管理
- 我们目前是创建了一个
Depend
对象,用来管理对于name
变化需要监听的响应函数:- 但是实际开发中我们会有不同的对象,另外会有不同的属性需要管理
- 我们如何可以使用一种数据结构来管理不同对象的不同依赖关系呢?
- 在前面我们刚刚学习过
WeakMap
,并且在学习WeakMap
的时候我讲到了后面通过WeakMap
如何管理这种响应式的数据依赖:
- 为什么外面一层用
weakMap
对象呢?- 如果用
map
,某个对象置为null
时,map
对象中的key
这里还有个强引用指向之前的对象,就不会被销毁
- 如果用
- 为什么里面的不用
WeakMap
对象呢?- 因为
WeakMap
的key
只能是对象
- 因为
7.对象依赖管理的实现
- 我们可以写一个
getDepend
函数专门来管理这种依赖关系:

8.正确的依赖收集
- 我们之前收集依赖的地方是在
watchFn
中:- 但是这种收集依赖的方式我们根本不知道是哪一个
key
的哪一个depend
需要收集依赖 - 你只能针对一个单独的
depend
对象来添加你的依赖对象
- 但是这种收集依赖的方式我们根本不知道是哪一个
- 那么正确的应该是在哪里收集呢?应该在我们调用了
Proxy
的get
捕获器时- 因为如果一个函数中使用了某个对象的
key
,那么它应该被收集依赖
- 因为如果一个函数中使用了某个对象的

9.对Depend
重构
- 但是这里有两个问题:
- 问题一:如果函数中有用到两次
key
,比如name
,那么这个函数会被收集两次 - 问题二:我们并不希望将添加
reactiveFn
放到get
中,以为它是属于Dep
的行为
- 问题一:如果函数中有用到两次
- 所以我们需要对
Depend
类进行重构:- 解决问题一的方法:不使用数组,而是使用
Set
- 解决问题二的方法:添加一个新的方法,用于收集依赖
- 解决问题一的方法:不使用数组,而是使用

10.创建响应式对象
- 我们目前的响应式是针对于
obj
一个对象的,我们可以创建出来一个函数,针对所有的对象都可以变成响应式对象:

11.Vue
响应式原理
- 我们前面所实现的响应式的代码,其实就是
Vue3
中的响应式原理:Vue3
主要是通过Proxy
来监听数据的变化以及收集相关的依赖的Vue2
中通过我们前面学习过的Object.defineProerty
的方式来实现对象属性的监听
- 我们可以将
reactive
函数进行如下的重构:- 在传入对象时,我们可以遍历所有的
key
,并且通过属性存储描述符来监听属性的获取和修改 - 在
setter
和getter
方法中的逻辑和前面的Proxy
是一致的
- 在传入对象时,我们可以遍历所有的
12.代码实现
Vue2
js// const obj = { // name: 'later', // age: 18 // } // const obj2 = { // name: 'later', // age: 22 // } let dependFn = null // 用来记录当前传入的函数,方便get操作的时候,对其进行收集 function watch(fn) { dependFn = fn fn() // 执行一次,把对应的依赖添加到对应的依赖收集对象中 dependFn = null } class Deped { constructor() { this.dependFns = new Set() // 使用Set 减少某个依赖函数中因为多次访问某个属性,而导致添加多个相同的依赖函数 } // addDepend(fn) { // if(fn) this.dependFns.push(fn) // } addDepend() { // if(dependFn) this.dependFns.push(dependFn) if(dependFn) this.dependFns.add(dependFn) // Set 使用 add } notify() { this.dependFns.forEach(fn => fn()) } } /** * 1.dep对象数据结构的管理(最难理解) * 每一个对象的每一个属性都会对应一个dep对象 * 同一个对象的多个属性的dep对象是存放一个map对象中 * 多个对象的map对象, 会被存放到一个objMap的对象中 * 2.依赖收集: 当执行get函数, 自动的添加fn函数 */ // 为对象的每一个属性都建立一个单独的依赖收集数组(depend对象),方便获取该对象中的某个属性所对应的依赖数组 // 对象中每个属性和其所对应的依赖收集数组,这种映射关系通过一个map对象来管理 // 然后所有对象的map对象由一个objMap来管理 const objMap = new WeakMap() // 通过弱引用,后续某个对象置为null时,不会对之前的对象保持强引用,会自动销毁掉 function getDepend(obj, key) { // 1.根据对象obj, 找到对应的map对象 let map = objMap.get(obj) // 判断某个属性在objMap中是否有其对应的map,没有则创建 if (!map) { map = new Map() objMap.set(obj, map) } // 2.根据key, 找到对应的depend对象 let dep = map.get(key) // 判断某个属性在map中是否有其对应的depend对象,没有则创建 if (!dep) { dep = new Deped() map.set(key, dep) } return dep } function reactive(obj) { Object.keys(obj).forEach(key => { let value = obj[key] Object.defineProperty(obj, key, { get: () => { // 获取属性的时候,不需要调用该属性所对应的依赖函数,只需要在访问的时候,执行添加依赖的处理 const deps = getDepend(obj, key) // deps.addDepend(dependFn) deps.addDepend() // 减少deps对外部变量的引用 上面写法也是欧克的 return value }, set: newValue => { // 只有设置属性的时候,才需要调用该属性所对应的依赖函数 进行更新处理 value = newValue const deps = getDepend(obj, key) deps.notify() } }) }) return obj } const obj = reactive({ name: 'later', age: 18 }) const obj2 = reactive({ name: 'later', age: 22 }) function foo1() { console.log('foo1------') console.log(obj.name) } function foo2() { console.log('foo2------') console.log(obj.age) } function foo3() { console.log('foo3------') console.log(obj.name, obj.age) console.log(obj2.age) } watch(foo1) watch(foo2) watch(foo3) console.log('----------------------') // obj.name = 'hehe' // obj.name obj2.age = 20
Vue3
js// const obj = { // name: 'later', // age: 18 // } // const obj2 = { // name: 'later', // age: 22 // } let dependFn = null // 用来记录当前传入的函数,方便get操作的时候,对其进行收集 function watch(fn) { dependFn = fn fn() // 执行一次,把对应的依赖添加到对应的依赖收集对象中 dependFn = null } class Deped { constructor() { this.dependFns = new Set() // 使用Set 减少某个依赖函数中因为多次访问某个属性,而导致添加多个相同的依赖函数 } // addDepend(fn) { // if(fn) this.dependFns.push(fn) // } addDepend() { // if(dependFn) this.dependFns.push(dependFn) if(dependFn) this.dependFns.add(dependFn) // Set 使用 add } notify() { this.dependFns.forEach(fn => fn()) } } /** * 1.dep对象数据结构的管理(最难理解) * 每一个对象的每一个属性都会对应一个dep对象 * 同一个对象的多个属性的dep对象是存放一个map对象中 * 多个对象的map对象, 会被存放到一个objMap的对象中 * 2.依赖收集: 当执行get函数, 自动的添加fn函数 */ // 为对象的每一个属性都建立一个单独的依赖收集数组(depend对象),方便获取该对象中的某个属性所对应的依赖数组 // 对象中每个属性和其所对应的依赖收集数组,这种映射关系通过一个map对象来管理 // 然后所有对象的map对象由一个objMap来管理 const objMap = new WeakMap() // 通过弱引用,后续某个对象置为null时,不会对之前的对象保持强引用,会自动销毁掉 function getDepend(obj, key) { // 1.根据对象obj, 找到对应的map对象 let map = objMap.get(obj) // 判断某个属性在objMap中是否有其对应的map,没有则创建 if (!map) { map = new Map() objMap.set(obj, map) } // 2.根据key, 找到对应的depend对象 let dep = map.get(key) // 判断某个属性在map中是否有其对应的depend对象,没有则创建 if (!dep) { dep = new Deped() map.set(key, dep) } return dep } function reactive(obj) { const objProxy = new Proxy(obj, { get: (target, key, receiver) => { const deps = getDepend(obj, key) deps.addDepend() return Reflect.get(target, key, receiver) }, set: (target, key, newValue, receiver) => { // target[key] = newValue // 外部操作的是proxy对象,内部设置的是源对象,所以不会产生递归 Reflect.set(target, key, newValue, receiver) const deps = getDepend(obj, key) deps.notify() } }) return objProxy } const obj = reactive({ name: 'later', age: 18 }) const obj2 = reactive({ name: 'later', age: 22 }) function foo1() { console.log('foo1------') console.log(obj.name) } function foo2() { console.log('foo2------') console.log(obj.age) } function foo3() { console.log('foo3------') console.log(obj.name, obj.age) console.log(obj2.age) } watch(foo1) watch(foo2) watch(foo3) console.log('----------------------') // obj.name = 'hehe' // obj.name obj2.age = 20
13. nextTick
nextTick
函数接收一个callback
,该callback
会在DOM
下次更新完成时回调nextTick
传入的回调函数是宏任务or
微任务:Vue2
中因版本不同,有的版本是宏任务,有的是微任务Vue3
中的是微任务(有传入回调函数的话,是放入到Promise.then
方法中)typescript// Vue3中的源码 export function nextTick<T = void>( this: T, fn?: (this: T) => void ): Promise<void> { const p = currentFlushPromise || resolvedPromise return fn ? p.then(this ? fn.bind(this) : fn) : p }