Vue.js 3.0 响应式回顾
与Vue.js 2.x 的区别
- Proxy 对象实现属性监听
- Vue.js 2.x 需要遍历所有属性,转化成响应式
- 多层属性嵌套,在访问属性过程中处理下一级属性
- Vue 2.x 需要在初始化的时候转化
- 默认监听动态添加的属性
- Vue 2.x 需要调用 Vue.set()
- 默认监听属性的删除操作
- Vue 2.x 不能
- 默认监听数组索引和 length 属性
- Vue 2.x 不能
- 可以作为单独的模块使用
- Vue 2.x 没有将响应式系统单独提取为一个模块
核心方法
稍后将模拟 Vue 3 的核心方法来了解 Vue 3 中响应式系统的原理。
- reactive/ref/toRefs/computed
- effect
- watch/watchEffect 是 Vue 3 的 runtime-core 中实现的
- 使用了一个 effect 的底层函数
- track/trigger
- Vue 3 中收集依赖和触发更新的函数
Proxy 对象回顾
'use strict'
// 创建目标对象 - 将被代理的对象
const target = {
foo: 'xxx',
bar: 'yyy'
}
// 通过Proxy代理target对象
// 创建Proxy对象的时候,传入了第二个对象参数(handler),这个对象可以称为处理器或监听器
const proxy = new Proxy(target, {
// 监听属性的访问
get(target, key, receiver) {
// return target[key]
return Reflect.get(target, key, receiver)
},
// 监听属性的赋值
set(target, key, value, receiver) {
// return target[key] = value
return Reflect.set(target, key, value, receiver)
},
// 监听属性的删除
deleteProperty(target, key) {
// return delete target[key]
return Reflect.deleteProperty(target, key)
}
})
- Reflect
- 翻译是
反射
的意思,是 ES6 新增的成员,Java 中也有 Reflect。 - Reflect 是在代码运行期间用来获取或设置对象中的成员。从 Java 中借鉴而来。
- 因为 JavaScript 在运行过程中可以随意向对象增加成员或者获取成员信息,所以过去的时候 JavaScript 中并没有 Reflect
- 过去 JavaScript 随意的把很多方法挂载到 Object 中,例如 Object.getPrototyoeOf()。Reflect 也有对应的方法:Reflect.getPrototypeOf(),方法的作用是一样的,只是表达语义的问题。
- 如果 Reflect 中有对应的 Object 的方法,建议使用 Reflect 中的方法,Vue 3 中就是如此。
- 翻译是
- 严格模式
set
和deleteProperty
方法都应当返回一个布尔值,返回true
代表操作成功。- 严格模式下,如果没有返回
true
则会抛出要给TypeError
异常。- ESM 默认开启严格模式。
- receiver
- get/set 方法中可以接收的可选参数
- 代表当前的Proxy对象(proxy)
- 如果 target 目标对象中设置了 getter,getter 中的 this 指向 receiver
- Vue 3 中的响应式源码中,在获取或设置值的时候都会传入 receiver,防止意外发生
'use strict'
// 创建目标对象 - 将被代理的对象
const target = {
get foo() {
console.log(this === proxy) // true
return this.bar // target 并没有 bar 属性,这里返回 proxy.bar
}
}
const proxy = new Proxy(target, {
get(target, key, receiver) {
if (key === 'bar') {
return 'value - bar'
}
return Reflect.get(target, key, receiver)
}
})
console.log(proxy.foo) // value - bar
// return Reflect.get(target, key) 将返回 undefined
reactive 创建响应式对象
- 接收一个参数
- 首先会判断参数是否是对象,如果不是,直接返回
- reactive 只能把对象转化成响应式对象,原始类型的属性要使用 ref 转化
- 然后创建拦截器对象 handler,设置 get/set/deleteProperty
- 最后返回 Proxy 对象,并传入拦截器对象
// /reactivity/index.js
// 判断是否为对象
const isObject = val => val !== null && typeof val === 'object'
// 用于 reactive 内部递归转化响应式对象
const convert = target => (isObject(target) ? reactive(target) : target)
// 接收 Object 原型上的 hasOwnProperty 方法
const hasOwnProperty = Object.prototype.hasOwnProperty
// 判断对象本身是否有指定属性
const hasOwn = (target, key) => hasOwnProperty.call(target, key)
export function reactive(target) {
if (!isObject(target)) return target
const handler = {
get(target, key, receiver) {
// 收集依赖
// ...
console.log('get', key)
const result = Reflect.get(target, key, receiver)
// 如果获取的属性值也是一个对象,也要将其转化成响应式对象
return convert(result)
},
set(target, key, value, receiver) {
const oldValue = Reflect.get(target, key, receiver)
let result = true
if (oldValue !== value) {
result = Reflect.set(target, key, value, receiver)
// 触发更新
// ...
console.log('set', key, value)
}
return result
},
deleteProperty(target, key) {
// 判断 target 中是否有自己的 key 属性
const hasKey = hasOwn(target, key)
// 判断是否删除成功(如果不存在 key 属性,也会返回成功)
const result = Reflect.deleteProperty(target, key)
if (hasKey && result) {
// 触发更新
// ...
console.log('delete', key)
}
return result
}
}
return new Proxy(target, handler)
}
<!-- /index.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>
<!-- ESM 方式加载js(需要启动一个web服务) -->
<script type="module">
import { reactive } from './reactivity/index.js'
const obj = reactive({
name: '张三',
age: 18
})
obj.name = '李四'
delete obj.age
console.log(obj)
</script>
</body>
</html>
effect 收集依赖 & 触发更新
收集依赖思路
通过演示 Vue 3 中响应式系统模块 reactivity
的使用,总结实现依赖收集的思路。
安装 @vue/reactivity
npm install @vue/reactivity
实现一个简单功能
<!-- effct.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>
<script type="module">
import { reactive, effect } from './node_modules/@vue/reactivity/dist/reactivity.esm-browser.js'
// 创建响应式对象
const product = reactive({
name: 'iPhone',
price: '12000',
count: 3
})
let total = 0
// effect 和 watchEffect 用法一样
// watchEffect 内部就是调用 effect 实现的
// effect 接收的函数首先会执行一次
// 当函数中引用的响应式数据发生变化,就会再次执行
effect(() => {
total = product.price * product.count
})
console.log(total)
product.price = 10000
console.log(total)
product.count = 1
console.log(total)
</script>
</body>
</html>
分析过程
关注 effect(fn)
函数
- 首次加载时,首先会执行
effect(fn)
函数,effect()
内部首先会调用接收的箭头函数fn
- 箭头函数
fn
中又访问了reactive
() 创建的响应式对象(代理对象)product
- 当访问
product.price
的时候,会执行它的get
方法,在get
方法中要收集依赖。 - 收集依赖的过程,就是存储 目标对象、对应的属性
price
和 回调函数fn
。- 注意:存储的目标对象是
product
代理的目标对象,不是product
本身。目标对象将在get
方法中被传递给收集依赖的方法track
- 注意:存储的目标对象是
- 在触发更新的时候,再根据这个属性
price
找到对应的 effect 回调函数fn
- 访问
product.count
的过程与product.price
一样 - 当给
product.price
赋值的时候,会执行price
属性对应的set
方法,在set
方法中会触发更新。 - 触发更新,就是找到依赖收集过程中存储的目标对象的
price
属性对应的effect 回调函数fn
,并立即执行。
以上是 依赖收集 和 触发更新 的简单过程。
图解过程
在依赖收集的过程中,会创建三个集合:
targetMap
(WeakMap 类型) - 用来记录[目标对象]:[depsMap]
的字典。- key 是 目标对象
- value 是 对应的 depsMap
- WeakMap 弱引用的类型,当目标对象失去引用后可以销毁。
depsMap
(Map 类型) - 用来记录[目标对象中的属性的名称]:[dep]
的字典。- key 是 目标对象中属性的名称
- value 是 dep
- Map 类型
dep
(Set 类型) - 用来存储 属性对应的 effect 回调函数- 一个属性可以存储多个 effect 回调函数
- Set 集合中存储的元素不会重复
在触发更新的时候,根据目标对象的属性在这个结构中,找到 effect 回调函数,然后执行。
稍后要实现的 收集依赖的 track
函数,内部首先要根据当前的 targetMap
找到 depsMap
。
如果没有找到,要给当前对象创建一个 depsMap
并添加到 targetMap
中。
如果找到了,再去根据当前使用的属性,在 depsMap
中找到对应的 dep
。
dep
中存储的是 effect 回调函数,如果没有找到,为当前属性创建对应的 dep
,并存储到 depsMap
中。
如果找到了,就把当前的effect 回调函数,存储到 dep
集合中。
收集依赖实现 effect && track
编写 effect
、track
方法,并在 get
方法中调用。
// /reactivity/index.js
/*...*/
export function reactive(target) {
if (!isObject(target)) return target
const handler = {
get(target, key, receiver) {
// 收集依赖
track(target, key)
const result = Reflect.get(target, key, receiver)
// 如果获取的属性值也是一个对象,也要将其转化成响应式对象
return convert(result)
},
set(target, key, value, receiver) { /*...*/ },
deleteProperty(target, key) { /*...*/ }
}
return new Proxy(target, handler)
}
// 当前活动的 effect 函数
let activeEffect = null
export function effect(callback) {
activeEffect = callback
// 首先执行一次接收的函数
// 访问响应式对象属性,去收集依赖
callback()
// 重置
activeEffect = null
}
let targetMap = new WeakMap()
// 收集依赖
export function track(target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
// 如果没有,创建 depsMap 并添加到字典中
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
// 如果没有,创建 dep 并添加到字典中
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
// 添加 effect 回调函数
dep.add(activeEffect)
}
触发更新实现 trigger
编写 trigger
方法,并在 set
方法中调用。
// /reactivity/index.js
/* ... */
export function reactive(target) {
if (!isObject(target)) return target
const handler = {
get(target, key, receiver) {/* ... */},
set(target, key, value, receiver) {
const oldValue = Reflect.get(target, key, receiver)
let result = true
if (oldValue !== value) {
result = Reflect.set(target, key, value, receiver)
// 触发更新
trigger(target, key)
}
return result
},
deleteProperty(target, key) {
// 判断 target 中是否有自己的 key 属性
const hasKey = hasOwn(target, key)
// 判断是否删除成功(如果不存在 key 属性,也会返回成功)
const result = Reflect.deleteProperty(target, key)
if (hasKey && result) {
// 触发更新
trigger(target, key)
}
return result
}
}
return new Proxy(target, handler)
}
// 当前活动的 effect 函数
let activeEffect = null
export function effect(callback) {/* ... */}
let targetMap = new WeakMap()
// 收集依赖
export function track(target, key) {/* ... */}
// 触发更新
export function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (!dep) return
// 遍历 dep 集合,执行 effect 回调函数
dep.forEach(effect => {
effect()
})
}
测试
替换之前示例的 reactivity
模块
<!-- effct.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>
<script type="module">
// import { reactive, effect } from './node_modules/@vue/reactivity/dist/reactivity.esm-browser.js'
import { reactive, effect } from './reactivity/index.js'
/* ... */
</script>
</body>
</html>
ref 创建响应式对象
实现 ref
-
reactive
只能将对象转化成响应式对象。 -
ref
可以接收 原始值 和 对象。- 如果接收的对象是
ref
创建的,则直接返回。 - 如果接收的是普通对象,内部调用
reactive
创建响应式对象并返回。 - 如果是原始值,则创建一个只有
value
属性的 响应式对象,并返回。
- 如果接收的对象是
// /reactivity/index.js
/* ... */
export function reactive(target) {/* ... */}
// 当前活动的 effect 函数
let activeEffect = null
export function effect(callback) {/* ... */}
let targetMap = new WeakMap()
// 收集依赖
export function track(target, key) {/* ... */}
// 触发更新
export function trigger(target, key) {/* ... */}
export function ref(raw) {
// 判断 raw 是否是 ref 创建的对象,如果是,直接返回
if (isObject(raw) && raw.__v_isRef) return raw
// convert 判断是否是对象,如果是,就调用reactive,如果不是,直接返回
let value = convert(raw)
const r = {
__v_isRef: true, // 标识,表示该对象是 ref 创建的
get value() {
track(r, 'value')
return value
},
set value(newValue) {
// 判断新旧值是否相等
if (newValue !== value) {
raw = newValue
value = convert(raw)
trigger(r, 'value')
}
}
}
return r
}
<!-- ref.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>
<script type="module">
import { reactive, effect, ref } from './reactivity/index.js'
// 创建响应式对象
// const product = reactive({
// name: 'iPhone',
// price: '12000',
// count: 3
// })
const price = ref(12000)
const count = ref(3)
let total = 0
effect(() => {
// ref 创建的响应式对象,要使用它的 value 属性
total = price.value * count.value
})
console.log(total)
price.value = 10000
console.log(total)
count.value = 1
console.log(total)
</script>
</body>
</html>
reactive
vs ref
ref
可以把基本数据类型数据,转化成响应式对象ref
返回的对象们重新赋值成对象也是响应式的reactive
返回的对象,重新赋值丢失响应式reactive
返回的对象不可以解构,可以通过toRefs
将代理对象的属性转化成类似ref
创建的响应式对象(包含value
属性的对象),才可以使用解构语法。
toRefs
toRefs
把 reactive
返回的对象的每一个属性,转换成类似 ref
返回的对象,从而可以对 reactive
返回的对象进行解构。
// /reactivity/index.js
/* ... */
export function reactive(target) {/* ... */}
// 当前活动的 effect 函数
let activeEffect = null
export function effect(callback) {/* ... */}
let targetMap = new WeakMap()
// 收集依赖
export function track(target, key) {/* ... */}
// 触发更新
export function trigger(target, key) {/* ... */}
export function ref(raw) {/* ... */}
export function toRefs(proxy) {
// 判断是否是响应式的数组
const ret = proxy instanceof Array ? new Array(proxy.length) : {}
for (const key in proxy) {
ret[key] = toProxyRef(proxy, key)
}
return ret
}
function toProxyRef(proxy, key) {
const r = {
__v_isRef: true,
get value() {
// proxy 是响应式对象,所以这里不需要收集依赖
return proxy[key]
},
set value(newValue) {
proxy[key] = newValue
}
}
return r
}
<!-- toRefs.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>
<script type="module">
import { reactive, effect, toRefs } from './reactivity/index.js'
function useProduct() {
// 创建响应式对象
const product = reactive({
name: 'iPhone',
price: '12000',
count: 3
})
return toRefs(product)
}
const { price, count } = useProduct()
let total = 0
effect(() => {
// ref 创建的响应式对象,要使用它的 value 属性
total = price.value * count.value
})
console.log(total)
price.value = 10000
console.log(total)
count.value = 1
console.log(total)
</script>
</body>
</html>
computed
computed
需要接收一个有返回值的函数作为参数,这个函数的返回值就是计算属性的值。
并且要监听这个函数中使用的响应式数据的变化,最后将这个函数执行的结果返回。
// /reactivity/index.js
/* ... */
export function reactive(target) {/* ... */}
// 当前活动的 effect 函数
let activeEffect = null
export function effect(callback) {/* ... */}
let targetMap = new WeakMap()
// 收集依赖
export function track(target, key) {/* ... */}
// 触发更新
export function trigger(target, key) {/* ... */}
export function ref(raw) {/* ... */}
export function toRefs(proxy) {/* ... */}
function toProxyRef(proxy, key) {/* ... */}
export function computed(getter) {
const result = ref()
// 通过 effect 监听响应式数据的变化
// 内部调用 getter 并将结果赋值给 result.value
effect(() => {
result.value = getter()
})
return result
}
<!-- computed.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>
<script type="module">
import { reactive, effect, computed } from './reactivity/index.js'
// 创建响应式对象
const product = reactive({
name: 'iPhone',
price: '12000',
count: 3
})
let total = computed(() => {
return product.price * product.count
})
// computed 返回的是 ref 创建的对象,所以要用属性 value
console.log(total.value)
product.price = 10000
console.log(total.value)
product.count = 1
console.log(total.value)
</script>
</body>
</html>