Vue.js响应式原理回顾
- Proxy对象实现属性监听
- 多层属性嵌套,在访问属性过程中处理下一级属性
- 默认监听动态添加的属性
- 默认监听属性的删除操作
- 默认监听数组索引和 length属性
- 可以作为单独的模块使用
核心方法
- reactive/ref/toRefs/computed
- effect watch/watchEffect是vue3 runtime.core中实现的,内部使用effect底层函数
- track 收集依赖
- trigger 触发更新
响应式系统原理——Proxy
Proxy和Reflect是ES6 为了操作对象而提供的新 API
proxy中有两个需要注意的地方:
-
set 和 deleteProperty 中需要返回布尔类型的值
<script> 'use strict' // set 和 deleteProperty 中需要返回布尔类型的值 // 在严格模式下,如果返回 false 的话会出现 Type Error 的异常 const target = { foo: 'xxx', bar: 'yyy' } // Reflect.getPrototypeOf()相当于Object.getPrototypeOf() const proxy = new Proxy(target, { // receiver代表当前的的Proxy对象或者继承Proxy的对象 get (target, key, receiver) { // return target[key] // Reflect反射,代码运行期间获取对象中的成员 return Reflect.get(target, key, receiver) }, set (target, key, value, receiver) { // target[key] = value // Reflect.set设置成功返回true 设置失败返回false return Reflect.set(target, key, value, receiver) }, deleteProperty (target, key) { // delete target[key] return Reflect.deleteProperty(target, key) } }) proxy.foo = 'zzz' // delete proxy.foo </script>
如果set和deleteProperty返回false时,页面会报错
-
Proxy 和 Reflect 中使用的 receiver指向
// Proxy 中 receiver:Proxy 或者继承 Proxy 的对象 // Reflect 中 receiver:如果 target 对象中设置了 getter,getter 中的 this 指向 receiver const obj = { get foo() { console.log(this) return this.bar }, } const proxy = new Proxy(obj, { get(target, key, receiver) { if (key === 'bar') { return 'value - bar' } return Reflect.get(target, key, receiver) }, }) console.log(proxy.foo)
不传递receiver时,可以看到this返回的是obj对象,proxy.foo返回undefined
当传递了receiver时,this指向Proxy对象
响应式系统原理——reactive
接收一个参数,判断这参数是否是对象,不是直接返回,只能转换对象为响应式对象
创建拦截器对象handler,设置get/set/deleteProperty
-
返回Proxy 对象
// reactivily/index.js const isObject = (val) => val !== null && typeof val === 'object' export function reactive(target) { if (!isObject(target)) return const handler = { get(target, key, receiver) { console.log('get', key, target) }, set(target, key, value, receiver) { console.log('set', key, value) return value }, deleteProperty(target, key) { console.log('delete', key) return target }, } return new Proxy(target, handler) }
测试set和delete,结果如下
reactive实现思路:
- 定义handler对象,用于Proxy的第二个参数(拦截器对象)
- get方法实现
- 收集依赖
- 返回target中对于key的value
- 如果value为对象,需要再次转为响应式对象
- set方法中实现
- 获取key属性的值,判断新旧值是否相同,相同时返回true
- 不同时,先将target中的key对应的value修改为新值
- 最后触发更新
- deleteProperty方法实现
- 首先判断target本身是否存在key
- 删除target中的key,并返回成功或失败
- 删除成功,触发更新
代码示例:
const isObject = (val) => val !== null && typeof val === 'object'
const convert = (val) => (isObject(val) ? reactive(val) : val)
const hasOwnProperty = Object.prototype.hasOwnProperty
const hasOwn = (target, key) => hasOwnProperty.call(target, key)
export function reactive(target) {
if (!isObject(target)) return
const handler = {
get(target, key, receiver) {
// 收集依赖
const value = Reflect.get(target, key, receiver)
return convert(value)
},
set(target, key, value, receiver) {
const oldValue = Reflect.get(target, key, receiver)
let result = true
if (oldValue !== value) {
let result = Reflect.set(target, key, value, receiver)
// 触发更新
}
return result
},
deleteProperty(target, key) {
const hasKey = hasOwn(target, key)
const result = Reflect.deleteProperty(target, key)
if (hasKey && result) {
// 触发更新
}
return result
},
}
return new Proxy(target, handler)
}
测试,创建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 } from './reactivity/index.js'
const obj = reactive({
name: 'zs',
age: 18
})
obj.name = 'lisi'
delete obj.age
console.log(obj)
</script>
</body>
</html>
响应式系统原理——收集依赖
- 依赖收集过程中会创建3个集合,分别是targetMap、depsMap和dep
- targetMap作用是记录目标对象和一个字典(depsMap),使用WeakMap弱引用,当目标对象失去引用之后,可以销毁
- targetMap的值是depsMap,depsMap的key是目标对象的属性名称,值是一个set集合dep
- dep中存储的是effect函数,因为可以多次调用一个effect,在effect中访问同一个属性,这时该属性会收集多次依赖,对应多个effect函数
- 通过这种结构,可以存储目标对象,目标对象属性,以及属性对应的effect函数
- 一个属性可能对应多个函数,当触发更新时,在这个结构中根据目标对象属性找到effect函数然后执行
- 收集依赖的track函数内部,首先根据当前targetMap对象找到depsMap,如果没找到要给当前对象创建一个depsMap,并添加到targetMap中,如果找到了再根据当前使用的属性在depsMap找到对应的dep,dep中存储的是effect函数,如果没有找到时,为当前属性创建对应的dep集合,并且存储到depsMap中,如果找到当前属性对应的dep集合,就把当前的effect函数存储到集合中
effect方法实现
实现思路:
- effect接收函数作为参数
- 执行函数并返回响应式对象去收集依赖,收集依赖过程中将callback存储起来,需要在后面的track函数中能够访问到这里的callback
- 依赖收集完毕设置activeEffect为null
代码实现:
let activeEffect = null
export function effect (callback) {
activeEffect = callback
callback() // 访问响应式对象属性,去收集依赖
activeEffect = null
}
track方法实现
实现思路:
- track接收两个参数,目标对象target和需要跟踪的属性key
- 内部需要将target存储到targetMap中,targetMap定义在外面,除了track使用外,trigger函数也要使用
- activeEffect不存在直接返回,否则需要在targetMap中根据当前target找depsMap
- 判断是否找到depsMap,因为target可能还没有收集依赖
- 未找到,为当前target创建depsMap去存储对应的键和dep对象,并添加到targetMap中
- 根据属性查找对应的dep对象,dep是个集合,存储effect函数
- 判断是否存在,未找到时创建新的dep集合并添加到depsMap中
- 将effect函数添加到dep集合中
- 在收集依赖的get中调用这个函数
代码实现:
let targetMap = new WeakMap()
export function track(target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
dep.add(activeEffect)
}
此时,整个依赖收集过程已经完成
trigger方法实现
依赖收集完成后需要触发更新
实现思路:
- 参数target和key
- 根据target在targetMap中找到depsMap
- 未找到时,直接返回
- 再根据key找对应的dep集合,effect函数
- 如果dep有值,遍历dep集合执行每一个effect函数
- 在set和deleteProperty中触发更新
代码实现:
export function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (dep) {
dep.forEach((effect) => {
effect()
})
}
}
依赖收集和触发更新代码完成,创建html文件进行测试
<body>
<script type="module">
import { reactive, effect } from './reactivity/index.js'
const product = reactive({
name: 'iPhone',
price: 5000,
count: 3
})
let total = 0
effect(() => {
total = product.price * product.count
})
console.log(total)
product.price = 4000
console.log(total)
product.count = 1
console.log(total)
</script>
</body>
打开浏览器控制台,可以看到输出结果如下
响应式系统原理——ref
ref vs reactive
- ref可以把基本数据类型数据,转成响应式对象
- ref返回的对象,重新赋值成对象也是响应式的
- reactive返回的对象,重新赋值丢失响应式
- reactive返回的对象不可以解构
实现原理:
- 判断 raw 是否是ref 创建的对象,如果是的话直接返回
- 判断 raw是否是对象,如果是对象调用reactive创建响应式对象,否则返回原始值
- 创建ref对象并返回,标识是否是ref对象,这个对象只有value属性,并且这个value属性具有set和get
- get中调用track收集依赖,收集依赖的对象是刚创建的r对象,属性是value,也就是当访问对象中的值,返回的是内部的变量value
- set中判断新旧值是否相等,不相等时将新值存储到raw中,并调用convert处理raw,最终把结果存储到value中,如果给value重新赋值为一个对象依然是响应式的,当raw是对象时,convert里调用reactive转换为响应式对象
- 最后触发更新
代码实现:
export function ref(raw) {
// 判断 raw 是否是ref 创建的对象,如果是的话直接返回
if (isObject(raw) && raw.__v_isRef) {
return
}
let value = convert(raw)
const r = {
__v_isRef: true,
get value() {
track(r, 'value')
return value
},
set value(newValue) {
if (newValue !== value) {
raw = newValue
value = convert(raw)
trigger(r, 'value')
}
},
}
return r
}
创建html文件进行测试:
<body>
<script type="module">
import { reactive, effect, ref } from './reactivity/index.js'
const price = ref(5000)
const count = ref(3)
let total = 0
effect(() => {
total = price.value * count.value
})
console.log(total)
price.value = 4000
console.log(total)
count.value = 1
console.log(total)
</script>
</body>
打开控制台可以看到输出结果和上面的相同
响应式系统原理——toRefs
实现思路:
- 接收参数proxy,判断参数是否为reactive创建的对象,如果不是发出警告
- 判断传入参数,如果是数组创建长度是length的数组,否则返回空对象,因为传入的proxy可能是响应式数组或响应式对象
- 接着遍历proxy对象的所有属性,如果是数组遍历索引,将每一个属性都转换为类似ref返回的对象
- 创建toProxyRef函数,接收proxy和key,创建对象并最终返回对象(类似ref返回的对象)
- 创建标识属性__v_isRef,这里的get中不需要收集依赖,因为这里访问的是响应式对象,当访问属性时,内部的getter回去收集依赖,set不需要触发更新,调用代理对象内部的set触发更新
- 调用toProxyRef,将所有属性转换并存储到ret中
- toRefs将reactive返回的对象的所有属性都转换成一个对象,所以当对响应式对象进行解构的时候,解构出的每一个属性都是对象,而对象是引用传递,所以解构的属性依然是响应式的
代码实现:
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() {
return proxy[key]
},
set value(newValue) {
proxy[key] = newValue
},
}
return r
}
创建html进行测试:
<body>
<script type="module">
import { reactive, effect, toRefs } from './reactivity/index.js'
function useProduct () {
const product = reactive({
name: 'iPhone',
price: 5000,
count: 3
})
return toRefs(product)
}
const { price, count } = useProduct()
let total = 0
effect(() => {
total = price.value * count.value
})
console.log(total)
price.value = 4000
console.log(total)
count.value = 1
console.log(total)
</script>
</body>
打开控制台可以看到输出结果和上面的相同
响应式系统原理——computed
实现原理:
- 接收一个有返回值的函数作为参数,函数的返回值就是计算属性的值
- 监听这个函数内部的响应式数据变化,最后将函数执行结果返回
- computed内部会通过effect监听getter内部的响应式数据变化,因为在effect中执行getter访问响应式数据的getter会去收集依赖,当数据变化后,回去重新执行effect函数将getter结果在存储到result中
代码实现:
export function computed(getter) {
const result = ref()
effect(() => (result.value = getter()))
return result
}
创建html文件进行测试:
<body>
<script type="module">
import { reactive, effect, computed } from './reactivity/index.js'
const product = reactive({
name: 'iPhone',
price: 5000,
count: 3
})
let total = computed(() => {
return product.price * product.count
})
console.log(total.value)
product.price = 4000
console.log(total.value)
product.count = 1
console.log(total.value)
</script>
</body>
打开控制台可以看到输出结果和上面的相同