vue3的 reactivity 是一个独立的包,这是一个比较大的改动,全部响应式相关的实现都在里面,我主要讲的也就是这一块的。vue
1.proxy: es6的代理实现方式 2.reflect: 将object对象一些明显属于语言内部方法,放到Reflect上, 3.weakMap: WeakMap 的 key 只能是 Object 类型。 4.weakSet: WeakSet 对象是一些对象值的集合, 而且其中的每一个对象值都只能出现一次. 响应式简要实现 咱们曾经的书写响应式数据是这样的react
data () {
return {
count: 0
}
}复制代码
而后vue3新的响应式书写方式(老的也兼容)es6
数组
setup() { const state = { count: 0, double: computed(() => state.count * 2) } function increment() { state.count++ }复制代码onMounted(() => { console.log(state.count) }) watch(() => { document.title = `count ${state.count}` 复制代码}) return { state, increment } }复制代码onMounted(() => { console.log(state.count) }) watch(() => { document.title = `count ${state.count}` 复制代码
感受setup这块就有点像 react hooks 理解成一个带有数据的逻辑复用模块,再也不以vue组件为单位的代码复用了 和React钩子不一样,setup()函数仅被调用一次。 因此新的响应书数据两种声明方式: 1.Ref 前提:声明一个类型 Ref 函数
export interface Ref<T> {
[refSymbol]: true
value: UnwrapNestedRefs<T>
}复制代码
ref()函数源码:ui
function ref(raw: unknown) {
if (isRef(raw)) {
return raw
}
// convert 内容:判断 raw是否是对象,是的话 调用reactive把raw响应化
raw = convert(raw)
const r = {
_isRef: true,
get value() {
// track 理解为依赖收集
track(r, OperationTypes.GET, '')
return raw
},
set value(newVal) {
raw = convert(newVal)
// trigger 理解为触发监听,就是触发页面更新好了
trigger(r, OperationTypes.SET, '')
}
}
return r as Ref
}复制代码
仍是看下 convert 吧spa
const convert = val => isObject(val) ? reactive(val) : val复制代码
能够看得出 ref类型 只会包装最外面一层,内部的对象最终仍是调用reactive,生成Proxy对象进行响应式代理。 疑问 可能有人想问,为何不都用proxy, 内部对象都用proxy,最外层还要搞个 Ref类型,画蛇添足吗? 理由可能比较简单,那就是proxy代理的都是对象,对于基本数据类型,函数传递或对象结构是,会丢失原始数据的引用。 官方解释:prototype
However, the problem with going reactive-only is that the consumer of a composition function must keep the reference to the returned object at all times in order to retain reactivity. The object cannot be destructured or spread:设计
2.Reactive 前提:先了解下 weakMap代理
// WeakMaps that store {raw <-> observed} pairs.
const rawToReactive = new WeakMap<any, any>() // key:原始对象 value: Proxy
const reactiveToRaw = new WeakMap<any, any>()
const rawToReadonly = new WeakMap<any, any>()
const readonlyToRaw = new WeakMap<any, any>()
reactive(target)复制代码
源码以下: 注:target必定是一个对象,否则会报警告
function reactive(target) {
// 若是target是一个只读响应式数据
if (readonlyToRaw.has(target)) {
return target
}
// 若是是被用户标记的只读数据,那经过readonly函数去封装
if (readonlyValues.has(target)) {
return readonly(target)
}
// go ----> step2
return createReactiveObject(
target,
rawToReactive,
reactiveToRaw,
mutableHandlers, // 注意传递
mutableCollectionHandlers
)
}复制代码
createReactiveObject(target,toProxy,toRaw,baseHandlers,collectionHandlers)
function createReactiveObject(
target: unknown,
toProxy: WeakMap<any, any>,
toRaw: WeakMap<any, any>,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
// 判断target不是对象就 警告 并退出
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// 经过原始数据 -> 响应数据的映射,获取响应数据
let observed = toProxy.get(target)
if (observed !== void 0) {
return observed
}
// 若是原始数据自己就是个响应数据了,直接返回自身
if (toRaw.has(target)) {
return target
}
// 若是是不可观察的对象,则直接返回原对象
if (!canObserve(target)) {
return target
}
// 集合数据与(对象/数组) 两种数据的代理处理方式不一样
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlers
// 声明一个代理对象 ----> step3
observed = new Proxy(target, handlers)
// 两个weakMap 存target observed
toProxy.set(target, observed)
toRaw.set(observed, target)
if (!targetMap.has(target)) {
targetMap.set(target, new Map())
}
return observed
}复制代码
baseHandles (咱们以对象类型为例,集合类型的handlers稍复杂点) handlers以下,new Proxy(target, handles)的 handles就是下面这个对象
export const mutableHandlers = {
get: createGetter(false),
set,
deleteProperty,
has,
ownKeys
}复制代码
createGetter(false) 问题:如何代理多层嵌套的对象 关键词:利用 proxy 的 get 思路:当咱们代理get获取到res时,判断res 是不是对象,若是是那么 继续reactive(res),能够说是一个递归
reactive(target) -> createReactiveObject(target,handlers) -> new Proxy(target, handlers) -> createGetter(readonly) -> get() -> res -> isObject(res) ? reactive(res) : res
function createGetter(isReadonly: boolean) {
// isReadonly 用来区分是不是只读响应式数据
// receiver便是被建立出来的代理对象
return function get(target: object, key: string | symbol, receiver: object) {
// 获取原始数据的响应值
const res = Reflect.get(target, key, receiver)
if (isSymbol(key) && builtInSymbols.has(key)) {
return res
}
if (isRef(res)) {
return res.value
}
// 收集依赖
track(target, OperationTypes.GET, key)
// 这里判断上面获取的res 是不是对象,若是是对象 则调用reactive而且传递的是获取到的res,
// 则造成了递归
return isObject(res)
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
readonly(res)
: reactive(res)
: res
}
}复制代码
set set的一个主要做用去触发监听,使试图更新,须要注意的是控制何时才是视图须要真的更新
function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
// 拿到新值的原始数据
value = toRaw(value)
// 获取旧值
const oldValue = (target as any)[key]
// 若是旧值是Ref类型,新值不是,那么直接更新值,并返回
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
const hadKey = hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// 若是是原始数据原型链上的数据操做,不作任何触发监听函数的行为。
if (target === toRaw(receiver)) {
// 更新的两种条件
// 1. 不存在key,即当前操做是在新增属性
// 2. 旧值和新值不等
if (!hadKey) {
trigger(target, OperationTypes.ADD, key)
} else if (hasChanged(value, oldValue)) {
trigger(target, OperationTypes.SET, key)
}
}
return result
}复制代码
问题2: 对于数据的set操做会出发屡次traps, 这里有个前提了解:就是咱们平常修改数组,好比 let a = [1], a.push(2), 这个push操做,咱们是其实是对a作了2个属性的修改,1,set length 1; 2. set value 2 因此咱们的set traps会出发屡次 思路:经过属性值和value控制,好比当 set key是 length的时候,咱们能够判断当前数组 已经有此属性,因此不须要出发更新,当新设置的值和老值同样是也不须要更新(说辞不够严谨)
问题3: set的源码里面有 有一个 target === toRaw(receiver)条件下才继续操做 trigger更新视图 这里就暴露出一个东西,即存在 target !== toRaw(receiver) Receiver: 最初被调用的对象。一般是 proxy 自己,但 handler 的 set 方法也有可能在原型链上或以其余方式被间接地调用(所以不必定是 proxy 自己) 其实源码有注释
// don't trigger if target is something up in the prototype chain of original
即若是咱们的操做是操做原始数据原型链上的数据操做,target 就不等于 toRaw(receiver) 什么状况下 target !== toRaw(receiver) 例如:
const child = new Proxy( {}, { // 其余 traps 省略 set(target, key, value, receiver) { Reflect.set(target, key, value, receiver) console.log('child', receiver) return true } } )const parent = new Proxy( { a: 10 }, { // 其余 traps 省略 set(target, key, value, receiver) { Reflect.set(target, key, value, receiver) console.log('parent', receiver) return true } } )
Object.setPrototypeOf(child, parent) // child.proto === parent true
child.a = 4
复制代码// 结果 // parent Proxy {a: 4} // Proxy {a: 4}复制代码
从结果能够看出,理论上 parent的set应该不会触发,但实际是触发了,此时
target: {a: 10}
receiver: Proxy {a: 4}
// 在vue3中
toRaw(receiver): {a: 4} 复制代码
为何有了proxy作响应式还须要一个Ref呢? 由于Proxy没法劫持基础数据类型,因此设计了这么一个对象——Ref,其实仍是有不少设计细节,就不一一赘述了,官网也给了他们不一样点,能够本身去好好了解。