Appearance
Vue3.0响应式系统原理
原理介绍
Vue.js3.0底层采用proxy对象实现属性监听,初始化的时候不需要遍历所有的属性通过defineProperty转化成getter和setter,如果有多层属性嵌套,在访问属性过程中处理下一级属性。所以其性能比Vue2.x的好。
Vue.js3.0的响应式系统:
- 默认可以监听动态添加的属性
- 默认监听属性的删除操作
- 默认监听数组索引和length属性的修改
- 还可以作为单独模块使用
下面我们要实现这些核心方法:
- reactive/ref/toRefs/computed
- effect
- track
- triiger
proxy回顾
问题一
set 和 deleteProperty 中需要返回布尔类型的值
在严格模式下,如果返回 false 的话会出现 Type Error 的异常
PS: 使用esmodule模块默认开启了严格模式,
js
const target = {
foo: 'xxx',
bar: 'yyy'
}
// 创建一个proxy对象
const proxy = new Proxy(target, {
// 访问
// receiver 当前的proxy对象或者继承proxy的对象
get (target, key, receiver) {
// reflect 反射,获取对象成员
return Reflect.get(target, key, receiver)
},
// 赋值
set (target, key, value, receiver) {
Reflect.set(target, key, value, receiver)
},
// 删除
deleteProperty (target, key) {
// 没有写return,就返回了undefined,会报类型错误
Reflect.deleteProperty(target, key)
}
})
proxy.foo = 'zzz'
// delete proxy.foo
这个时候设置值和删除值就会报类型错误
不报错误需要这么改
js
const proxy = new Proxy(target, {
// 访问
get (target, key, receiver) {
return Reflect.get(target, key, receiver)
},
// 赋值
set (target, key, value, receiver) {
// 设置成功返回true,设置失败返回false
return Reflect.set(target, key, value, receiver)
},
// 删除
deleteProperty (target, key) {
// 也加上return
return Reflect.deleteProperty(target, key)
}
})
之后就不会报类型错误了。
问题二
Proxy 和 Reflect 中使用的 receiver
- Proxy 中 receiver:Proxy 或者继承 Proxy 的对象
- Reflect 中 receiver:如果 target 对象中设置了 getter,getter 中的 this 指向 receiver
js
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'
}
// 这里没有设置receiver
return Reflect.get(target, key)
}
})
console.log(proxy.foo)
// undefined
如果在proxy的get添加一个receiver
js
const obj = {
get foo() {
// 代理对象proxy
console.log(this)
return this.bar
}
}
const proxy = new Proxy(obj, {
get (target, key, receiver) {
if (key === 'bar') {
return 'value - bar'
}
// 添加了receiver
return Reflect.get(target, key, receiver)
}
})
console.log(proxy.foo)
// value - bar
所以这里都会使用receiver,防止意外发生
reactive函数的实现
- 接收一个参数,判断这个参数是否是对象,如果不是直接返回,reactive只能把对象转换成响应式对象,和ref不同
- 创建拦截器对象 handler,设置 get/set/deleteProperty
- 返回 Proxy 对象
- 第一步先搭架子
js
// 判断是否是对象
const isObject = val => val !== null && typeof val === 'object'
// 返回一个函数,接收一个参数target
export function reactive (target) {
// 判断是否是对象,如果不是对象就直接返回
if (!isObject(target)) return target
// 定义拦截器对象,包括get、set、deleteProperty
const handler = {
// 接收三个参数
get (target, key, receiver) {
// 收集依赖
console.log('get', key)
},
// 接收四个参数,多一个value
set (target, key, value, receiver) {
},
// 删除:接收两个参数
deleteProperty (target, key) {
}
}
// 返回proxy对象,传入target和handler
return new Proxy(target, handler)
}
- 在get中写收集依赖的函数
js
// 判断是否是对象
const isObject = val => val !== null && typeof val === 'object'
// 判断target是否是对象,如果是对象就调用reactive进行处理,如果不是就直接返回target
const convert = target => isObject(target) ? reactive(target) : target
const handler = {
// 接收三个参数
get (target, key, receiver) {
// 收集依赖
console.log('get', key)
const result = Reflect.get(target, key, receiver)
return convert(result)
},
...
}
- 写set方法
js
export function reactive (target) {
if (!isObject(target)) return target
// 定义拦截器对象,包括get、set、deleteProperty
const handler = {
...
// 接收四个参数,多一个value
set (target, key, value, receiver) {
// 或者旧值的目的要判断新旧值是否相同
const oldValue = Reflect.get(target, key, receiver)
// 返回的值默认是true
let result = true
// 如果不相同设置新值
if (oldValue !== value) {
// 设置的值之后赋给result
result = Reflect.set(target, key, value, receiver)
// 触发更新
console.log('set', key, value)
}
return result
},
...
}
- 写deleteProperty方法
js
// 为了简化书写
const hasOwnProperty = Object.prototype.hasOwnProperty
// 用call改this为target,判断对象中是否有属性
const hasOwn = (target, key) => hasOwnProperty.call(target, key)
export function reactive (target) {
if (!isObject(target)) return target
// 定义拦截器对象,包括get、set、deleteProperty
const handler = {
...
// 删除:接收两个参数
deleteProperty (target, key) {
// 判断是否有该属性
const hadKey = hasOwn(target, key)
// 删除该属性,返回成功还是失败
const result = Reflect.deleteProperty(target, key)
// 有该属性且删除成功
if (hadKey && result) {
// 触发更新
console.log('delete', key)
}
return result
}
}
...
}
- 测试一下,创建一个index.html,将模块导入,可以看到get、set和deleteProperty的功能都能正常工作。
js
import { reactive } from './reactivity/index.js'
const obj = reactive({
name: 'zs',
age: 18
})
obj.name = 'lisi'
delete obj.age
console.log(obj)
// set name lisi
// delete age
// Proxy {name: "lisi"}
收集依赖
下面分析一下收集依赖的思路
举个例子
effect函数,定义的时候首次会执行,每次修改里面的响应式数据,函数就会再次调用。
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)
effect函数中访问 product.price 和 product.count 里面的get方法收集依赖,记录target,target对应的属性,还有其对应的函数。
在赋值给price的时候,就触发其set方法,更新数据,找到其对应的函数,执行函数。
分析思路
下面看一张图: 在收集依赖的时候会创建三个集合:targetMap、depsMap和dep。
其中targetMap的作用:是用来记录目标对象和一个字典(即depsMap),targetMap使用的类型的WeakMap,弱引用的Map,这里的key就是target对象,因为是弱引用,当目标对象失去引用之后可以销毁。
depsMap的作用:这是字典Map类型,字典中的key是目标对象的属性名称,值是一个set集合dep。
dep的作用:set里面存储的元素不会重复,里面存储的就是effect函数,因为我们可以多次调用一个effect,在effect中访问同一个属性,那这个时候该属性会收集多次依赖,对应多个effect函数。
一会要实现的收集依赖的track函数: 其内部首先要根据targetMap对象来找到depsMap
- 如果没有找到的话,就给当前对象创建一个depsMap并添加到targetMap中
- 如果找到了再根据当前的属性来depsMap中找到对应的dep。
然后dep里存储的是effect函数
- 如果没有找到为当前的属性创建dep并且存储到depsMap中
- 如果找到dep集合就将effect存储到dep集合中。
effect-track函数的实现
实现effect函数
js
// 定义一个变量记录callback,默认是null,track函数也可以访问这个变量
let activeEffect = null
// effect,接收一个参数callback
export function effect (callback) {
activeEffect = callback
callback() // 先执行一下callback,访问响应式对象属性,去收集依赖
// 因为收集依赖,有嵌套属性是一个递归的过程
activeEffect = null
}
实现track函数
js
// track要使用,trigger也要使用,所以定义到外面,targetMap是weakMap
let targetMap = new WeakMap()
// track,两个参数,目标对象target和key
export function track (target, key) {
// 要保存activeEffect,如果为null,就直接return
if (!activeEffect) return
// 定义depsMap
let depsMap = targetMap.get(target)
// 如果没有,那么就创建一个新的depsMap并且添加到target中
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 根据属性查找dep对象
let dep = depsMap.get(key)
// dep如果不存在就创建一个新的dep并添加到depsMap中
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
// 最后将activeEffect添加到dep中
dep.add(activeEffect)
}
在reactive中添加依赖
js
get (target, key, receiver) {
// 收集依赖
track(target, key)
const result = Reflect.get(target, key, receiver)
return convert(result)
},
trigger函数的实现
trigger函数和track函数正好相反
js
// trigger函数,两个参数target和key
export function trigger (target, key) {
// 要根据target去targetMap中找到depsMap
const depsMap = targetMap.get(target)
if (!depsMap) return
// depsMap中存的是键key和key对应的effect函数集合dep
const dep = depsMap.get(key)
// 如果存在就循环调用effect函数
if (dep) {
dep.forEach(effect => {
effect()
})
}
}
然后要去reactive中进行调用
js
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) {
const hadKey = hasOwn(target, key)
const result = Reflect.deleteProperty(target, key)
if (hadKey && result) {
// 触发更新
trigger(target, key)
}
return result
}
打开浏览器找到之前的例子,运行结果相同。
ref函数的实现
功能
ref接收一个参数:
- 如果是对象,并且对象是ref创建的对象,直接返回
- 如果是普通对象就用reactive创建并返回
- 如果是原始值,就创建一个有value属性的对象返回。
js
// ref函数,接收一个参数raw
export function ref (raw) {
// 判断 raw 是否是 ref 创建的对象,如果是的话直接返回
if (isObject(raw) && raw.__v_isRef) {
return
}
// 这里直接嗲用convert,判断是不是对象,是就用reactive包装
let value = convert(raw)
// 要创建一个ref对象,只有value属性
const r = {
// 标识属性
__v_isRef: true,
// value属性的getter
get value () {
// 收集依赖,r对象的value属性
track(r, 'value')
return value
},
// value属性的setter
set value (newValue) {
// 判断新值和旧值是否相等,如果不相等新值存储到raw中
if (newValue !== value) {
raw = newValue
// 将新值进行reactive包装
value = convert(raw)
// 触发更新
trigger(r, 'value')
}
}
}
return r
}
测试的时候,除了要导入reactive,effect还要导入ref函数
js
import { reactive, effect, ref } from './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)
reactive vs ref
reactive | ref |
---|---|
不能转基本数据类型 | 可以转成响应式对象 |
直接访问 | 需要访问其value属性 |
返回的对象重新赋值丢失响应式,不可以解构,如果要解构的话需要toRefs函数 | 返回的对象重新赋值成对象也是响应式的 |
如果一个对象成员很多的时候,ref并不方便,因为总要带着value属性 | 如果只有一两个响应式数据,使用ref比较方便,可以解构使用 |
toRefs函数的实现
作用:接收一个reactive对象(即proxy对象),如果传入的不是proxy对象就直接返回,然后把传入对象的所有属性转换成类似ref返回的对象。
js
// toRefs函数,接收一个参数proxy对象
export function toRefs (proxy) {
// 判断是不是proxy对象,reactive对象没有标识,所以这一步跳过
// proxy如果是数组就创建一个新的数组,长度是proxy.length,否则就是空对象
const ret = proxy instanceof Array ? new Array(proxy.length) : {}
// 遍历所有属性、或者数组的索引
for (const key in proxy) {
// 将每一个属性都转换成ref返回的对象
ret[key] = toProxyRef(proxy, key)
}
return ret
}
// 每个属性转换成ref返回的对象,接收proxy和key
function toProxyRef (proxy, key) {
// ref函数内部实现
const r = {
// 标识
__v_isRef: true,
get value () {
// 直接返回proxy的key,这里不需要收集依赖
// 因为代理对象内部已经有收集依赖的了
return proxy[key]
},
set value (newValue) {
// 这里也不需要触发更新,proxy内部自己触发
proxy[key] = newValue
}
}
return r
}
这里如果对对象解构的话,里面每一个属性都是ref返回的对象,直接用value属性访问。
js
import { reactive, effect, toRefs } from './index.js'
function useProduct () {
const product = reactive({
name: 'iPhone',
price: 5000,
count: 3
})
// 将reactive返回的对象用toRefs包装一下
return toRefs(product)
}
// 解构
const { price, count } = useProduct()
let total = 0
effect(() => {
// 这里使用value属性访问
total = price.value * count.value
})
console.log(total)
// 使用value属性赋值
price.value = 4000
console.log(total)
count.value = 1
console.log(total)
computed函数的实现
computed需要接收一个有返回值的函数作为参数,这个函数的返回值就是计算属性的值,并且我们要监听函数内部使用的响应式数据的变化,最后把执行的结果返回。
js
// computed函数 接收一个参数getter
export function computed (getter) {
// 创建一个对象,不传参数默认返回undefined
const result = ref()
// 通过effect监听响应式数据的变化,把getter的结果存储到result的value中
effect(() => (result.value = getter()))
// 将result返回
return result
}
测试的时候将effect用computed替换
js
import { reactive, computed } from './index.js'
const product = reactive({
name: 'iPhone',
price: 5000,
count: 3
})
let total = computed(() => {
return product.price * product.count
})
product.price = 4000
// 这里要用value属性访问
console.log(total.value)
product.count = 1
console.log(total.value)