内容输出来源:拉勾教育大前端高薪训练营
1.Vue3.0响应式特性
- Proxy对象实现属性监听
- 多层属性嵌套,在访问属性过程中处理下一级属性
- 默认监听动态添加的属性
- 默认监听属性的删除操作
- 默认监听数组索引和length属性
- 可以作为单独的模块使用
2.核心方法
- reactive/ref/toRefs/computed
- effect
- track
- trigger
3.Proxy对象回顾
Proxy 与 Reflect 是 ES6 为了操作对象引入的 API 。更多关于Proxy和Reflect的内容
- 在严格模式下,Proxy的set和deleteProperty中需要返回布尔类型的值得返回布尔类型的值,否则会报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'
}
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'
- 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) // 如果不设置receiver,则this指向的是原本的对象obj,this.bar返回undefined,如果设置了receiver,this指向的是代理对象proxy,this.bar返回'value - bar'
}
})
console.log(proxy.foo) // value - bar
4.自己实现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) {
// 收集依赖
console.log('get', key)
track(target, key) // track见下文
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)
trigger(target, key) // 见下文trigger
}
return result
},
deleteProperty(target, key) {
const hasKey = hasOwn(target, key)
const result = Reflect.deleteProperty(target, key)
if (hasKey && result) {
// 触发更新
console.log('delete', key)
trigger(target, key) // 见下文trigger
}
return result
}
}
return new Proxy(target, handler)
}
使用
<body>
<div id="app"></div>
<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”}
5.收集依赖
收集目标对象,目标对象的属性,属性对应的箭头函数
6.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)
}
7.trigger
export function trigger(target, key) { // 触发更新
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (dep) {
dep.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>
8.ref
reactive vs ref
- ref可以把基本类型的数据,转换成响应式对象(获取数据时使用value属性,模板中使用可以省略value)
- ref返回的对象重新给value赋值成对象以后也是响应式的
- reactive返回的属性,重新赋值丢失响应式
- reactive返回的对象不可以解构
export function ref(raw) {
// 判断raw是否是对象,如果是对象且是ref创建的对象则直接返回
if (isObject(raw) && raw.__v_isRef) return
let value = convert(raw) // 如果raw是普通对象,convert函数会调用reactive把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'
let price = ref(5000)
let 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>
9. toRefs
export function toRefs(proxy) {
let 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是reactive对象
return proxy[key]
},
set value (newValue) {
proxy[key] = newValue
// 此处不再需要触发更新因为proxy是reactive对象
}
}
return r
}
使用:
<body>
<script type="module">
import { reactive, effect, toRefs } from './reactivity/index.js'
const usePrice = () => {
const product = reactive({
price: 5000,
count: 3
})
return toRefs(product) // 如果直接返回product,解构之后的属性并非响应式的,所以调用toRefs将reactive对象的每个属性都转换成ref对象
}
const { price, count } = usePrice()
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>
10.computed
export function computed(getter) {
let result = ref()
effect(() => (result.value = getter()))
return result
}
使用:
<body>
<script type="module">
import { reactive, 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/effect是底层函数,一般不用。使用computed代替effect的使用