Vue 3.0响应式系统编程概述
随着Vue3.0的正式发布,前端又多了一门需要学习的功课,本文主要是对vue3.0响应式系统的简单剖析及实现:
一、 3.0响应式
- 使用Proxy对象实现属性监听
- 多层属性嵌套,在访问属性过程中处理下一级属性
- 默认监听动态添加的属性
- 默认监听属性的删除操作
- 默认监听数组索引和length属性
- 可以作为单独的模块使用
二、 核心方法
- reactive (将对象转换为响应式对象)
- ref (将基本类型的值转为具有一个value属性的响应式对象)
- toRefs (解构响应式对象数据)
- computed
- effect(watch依赖的的底层函数)
- track(收集依赖)
- trigger (触发更新)
三、 proxy回顾
1. set和deleteProperty中需要返回布尔类型的值
'use strict'
// 问题1: set和deleteProperty中需要返回布尔类型的值
// 严格模式下,如果返回false的话,会出现TypeError的异常
const target = {
foo: 'xxx',
bar: 'yyy'
}
// Reflect.getPrototypeOf()
// Object.getPrototypeOf()
const proxy = new Proxy(target, {
get (target, key, receiver) {
// return target[key]
return Reflect.get(target, key, receiver)
},
set (target, key, value, receiver) {
// target[key] = value
return Reflect.set(target, key, value, receiver)
// 这里得写return 不写默认返回undefined
//Reflect.set执行成功或者失败会返回布尔类型的值
},
deleteProperty(target, key) {
// delete target[key]
return Reflect.deleteProperty(target, key) // 这里得写return
}
})
proxy.foo = 'zzz'
2. Proxy和Reflect中使用receiver
- Proxy中receiver: Proxy或者继承Proxy的对象
- Reflect中receiver:如果target对象设置了getter,getter中的this指向receiver
执行
this.bar
的时候,如果第三个参数receiver
不传,getter中this指向obj对象,此时proxy.foo
为undefined
执行this.bar
的时候,如果第三个参数receiver
传, this指向代理对象,也就是获取target.bar
, 此时proxy.foo
为value - bar
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'
}
// 执行this.bar的时候,如果第三个参数receiver不传,getter中this指向obj对象,此时proxy.foo 为undefined
// 执行this.bar的时候,如果第三个参数receiver传, this指向代理对象,也就是获取target.bar, 此时proxy.foo 为value - bar
return Reflect.get(target, key, receiver)
}
})
console.log(proxy.foo) // value - bar
四、 源码实现
4.1 reactive(只能转换对象)
- 接受一个参数,判断这个参数是否是对象
- 创建拦截器对象handler,设置get/set/deleteProperty
- 返回Proxy对象
代码实现:
// 工具方法
const isObject = val => val !==null && typeof val === 'object'
const convert = target => isObject(target) ? reactive(target) : target
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) {
//此处收集依赖 - track
console.log('get', key)
const result = Reflect.get(target, key, receiver);
return convert(result)
},
set (target, key, value, receiver) {
let result = true;
const oldValue = Reflect.get(target, key, receiver)
if (oldValue !== value) {
result = Reflect.set(target, key, value, receiver)
//此处触发更新 - trigger
console.log('set', key, value)
}
return result
},
deleteProperty(target, key) {
const haskey = hasOwn(target, key)
const result = Reflect.deleteProperty(target, key)
if (haskey && result) {
//此处触发更新 - trigger
console.log('delete', key)
}
return result
}
}
return new Proxy(target, handler);
}
实际使用:
<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>
输出结果:
set name lisi
delete age
Proxy {name: “lisi”}
4.2 依赖收集
weakMap:弱引用,当失去引用会被销毁
vue3.0中依赖是一个三层的树形结构,我们会在最外层定义一个new weakMap()
的集合targetMap
,当我们触发get时使用track
方法收集依赖时首先判断当前是否存在一个activeEffect
对象,不存在直接返回,存在则首先判断当前target
在targetMap
的集合中是否存在,如果不存在就在targetMap
的集合中创建对应的集合depsMap
,然后判断当前key
在depsMap
中是否存在,如果不存在就在depsMap
的集合中创建对应的集合dep
,将对应的依赖activeEffect
收集到对应key
值对应的dep
集合中。
4.3 effect、 track
- effect:参数为一个函数(第一次会执行一次),当函数的响应式对象发生改变,就会重新执行一次函数
- track: 收集依赖的函数
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) //获取当前依赖集合中target对应的值
if(!depsMap) {
targetMap.set(target, (depsMap = new Map())) //不存在则设置一个target对应的new Map()值
}
let dep = depsMap.get(key) //获取当前依赖集合中target对应的集合中 key属性对应的值
if(!dep) {
depsMap.set(key, (dep = new Set()))//不存在则在depsMap中设置一个key属性对应的new Set()值
}
dep.add(activeEffect) //将当前的活动对象添加到key属性对应的 Set集合中
}
4.4 trigger 触发更新
export function trigger(target, key) {//触发更新
const depsMap = targetMap.get(target); // 找到target对象对应的集合
if (!depsMap) return;
const dep = depsMap.get(key) //找到key对应的dep集合
if(dep) {//执行每个依赖于key(响应式对象的值)值函数
dep.forEach(effect => effect())
}
}
4.5 ref
可以接受对象或者基础类型,如果是响应式对象直接返回,是对象则内部会调用reactive将其转换为响应式对象,如果是普通的值则转为具有一个value属性的响应式对象
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
}
以上我们可以知道 reactive vs ref
-
ref可以把基本数据类型数据转换成响应式对象
-
ref返回的对象,重新赋值成对象也是响应式的
-
reactive返回的对象,重新赋值丢失响应式
-
reactive返回的对象不可解构
-
reactive
const product = reactive({ name: 'iPhone', price: 5000, count: 3 })
-
ref
const price = ref(5000) const count = ref(3)
4.6 toRefs
传入的对象必须是reactive返回的响应式对象(proxy对象)然后将传入对象的属性转换为类似ref返回的对象然后将属性挂载在一个新的对象上返回,如果不是响应式对象(proxy对象)直接返回.
export function toRefs (proxy) {
//如果是数组我们创建一个新的数组 否则返回空对象
const ret = proxy instanceof Array ? new Array(proxy.length) : {}
for (const key in proxy) {
// 将每一项转换为类似ref的对象
ret[key] = toProxyRef(proxy, key)
}
return ret
}
function toProxyRef (proxy, key) {
const r = {
__v_isRef: true,
get value () {
return proxy[key]//这里不收集依赖是因为proxy是响应式对象,当我们访问响应式对象属性会触发get方法自动收集依赖
},
set value (newValue) {
proxy[key] = newValue//这里不需要触发更新是因为proxy是响应式对象,当我们重新赋值会触发响应式对象的set方法触发更新
}
}
return r
}
4.7 computed
接受一个有返回值的函数作为参数,返回的值就是计算属性的值并且会监听函数中的响应式数据的变化
export function computed (getter) {
const result = ref()
effect(() => (result.value = getter()))
return result
}