Vue.js 3.0响应式系统原理
文章内容输出来源:大前端高薪训练营
一、介绍
1. Vue.js响应式回顾
- Proxy对象实现属性监听
- 多层属性嵌套,在访问属性过程中处理下一级属性
- 默认监听动态添加的属性
- 默认监听属性的删除操作
- 默认监听数组索引和length属性
- 可以作为单独的模块使用
2. 核心函数
- eactive/ref/toRefs/computed
- effect
- track
- trigger
二、Proxy对象回顾
1. 在严格模式下,Proxy的函数得返回布尔类型的值,否则会报TypeError
Uncaught TypeError: ‘set’ on proxy: trap returned falsish for property ‘foo’
'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
},
deleteProperty(target, key) {
// delete target[key]
return Reflect.deleteProperty(target, key) // 这里得写return
}
})
proxy.foo = 'zzz'
2. Proxy和Reflect中使用receiver
Proxy中receiver:Proxy或者继承Proxy的对象
React中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) // 执行this.bar的时候,this指向代理对象,也就是获取target.bar
}
})
console.log(proxy.foo) // value - bar
如果return Reflect.get(target, key, receiver)
写成return Reflect.get(target, key)
的话,则响应式属性foo里面的this还是指向原本的对象obj,this.bar就是undefined,而传入了receiver之后,响应式属性里的this就指向新的响应式对象proxy,this.bar返回value - bar
。
三、reactive
- 接受一个参数,判断这个参数是否是对象
- 创建拦截器对象handler,设置get/set/deleteProperty
- 返回Proxy对象
自己实现reactive
function isObject(value) {
return value !== null && typeof value === 'object'
}
function convert(target) {
return isObject(target) ? reactive(target) : target
}
const hasOwnProperty = Object.prototype.hasOwnProperty
function hasOwn(target, key) {
return hasOwnProperty.call(target, key)
}
export function reactive(target) {
if (!isObject(target)) return target
const handler = {
get (target, key, receiver) {
// 收集依赖
// track(target, key) // 稍后解注释
console.log('get', key)
const ret = Reflect.get(target, key, receiver)
return convert(ret)
},
set (target, key, value, receiver) {
const oldValue = Reflect.get(target, key, receiver)
let ret = true
if (oldValue !== value) {
ret = Reflect.set(target, key, value, receiver)
// 触发更新
// trigger(target, key) // 稍后解注释
console.log('set', key, value)
}
return ret
},
deleteProperty (target, key) {
const hasKey = hasOwn(target, key)
const ret = Reflect.deleteProperty(target, key)
if (hasKey && ret) {
// 触发更新
// trigger(target, key) // 稍后解注释
console.log('detele', key)
}
return ret
}
}
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
index.js:39 detele age
index.html:17 Proxy {name: “lisi”}
四、收集依赖
五、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)
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
export function trigger(target, key) {
// 触发依赖
const depsMap = targetMap.get(target)
if(!depsMap)return
const dept = depsMap.get(key)
if(dept) {
dept.forEach(effect => {
effect()
})
}
}
使用:
<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) // 15000
product.price = 4000
console.log(total) // 12000
product.count = 1
console.log(total) // 4000
</script>
</body>
七、ref
eactive 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)
实现ref:
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
}
使用:
<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) // 15000
price.value = 4000
console.log(total) // 12000
count.value = 1
console.log(total) // 4000
</script>
</body>
八、toRefs
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
}
使用
<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) // 直接返回解构的product不是响应式对象,所以调用toRefs将reactive对象的每个属性都转化成ref对象
}
const {
price, count } = useProduct()
let total = 0
effect(() => {
total = price.value * count.value
})
console.log(total) // 15000
price.value = 4000
console.log(total) // 12000
count.value = 1
console.log(total) // 4000
</script>
</body>
九、computed
export function computed(getter) {
const result = ref()
effect(() => (result.value = getter()))
return result
}
使用
<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) // 15000
product.price = 4000
console.log(total.value) // 12000
product.count = 1
console.log(total.value) // 4000
</script>
</body>
备注:trigger/track/effct是底层的函数,一般不用。使用computed代替effect的使用