后面说的话
在 Vue2 中,集体感觉对于数据的操作比拟 “黑盒” 。
而 Vue3 把响应式零碎更显式地裸露进去,使得咱们对数据的操作有了更多的灵活性。
所以,对于 Vue3 的几个响应式的 API ,咱们须要更加的了解把握,能力在实战中运用自如。
先理解,什么是响应式 ?
- Vue3 官网有举过一个例子
var val1 = 2var val2 = 3var sum = val1 + val2
咱们心愿 val1 或 val2 的值扭转的时候,sum 也会响应的做出正确的扭转。
- 大白话
我依赖了你,你变了。你就告诉我让我晓得,好让我做点 “操作” 。
- 从 Vue3 的源码来讲
让咱们记住三个要害的英语单词,它们的程序也是实现一个响应式的程序。
effect > track > trigger > effect
浅浅的解释一下:在组件渲染过程中,假如以后正在走一个 “effect”(副作用),这个 effect 会在过程中把它接触到的值(也就是说会触发到值的 get 办法),从而对值进行 track(追踪)。当值产生扭转,就会进行 trigger(触发),执行 effect 来实现一个响应!
- 用代码来解释
在 Vue 中,有三种 effect ,且说是视图渲染effect、计算属性effect、侦听器effect
<template> <div>count:{{count}}</div> <div>computedCount:{{computedCount}}</div> <button @click="handleAdd">add</button></template>// ...setup() { const count = ref(1); const computedCount = computed(() => { return count.value + 1; }); watch(count, (val, oldVal) => { console.log('val :>> ', val); }); const handleAdd = () => { count.value++; }; return { count, computedCount, handleAdd };}// ...
下面这段代码,对于依赖值的追踪之后会被寄存于这样的一个汇合中,如图:
注:以上的最内层汇合数组里的 reactiveEffect 办法别离是 侦听器effect、视图渲染effect、计算属性effect。
当执行 handleAdd 动作时,就会触发 count.value
的 set 办法,进行 trigger 响应式调用汇合相干的 3 个 effect ,而后别离去更新视图,更新 computedCount 的值,调用 watch 侦听器的回调办法进行输入。
不太了解没关系,脑袋瓜先有个大体的构造即可 ~
简略的介绍了响应式是什么之后,让咱们来进入本文的主题,进一步理解 Vue3 的响应式 API ~
Vue3 内置的 20 个响应式 API
1. reactive
先看 Proxy
在理解 reactive 之前,咱们先来理解一波实现 reactive API 的要害类 > ES6 的 Proxy ,它还有一个好基友 Reflect。这里咱们先看一个简略的例子:
const targetObj = { id: 1, name: 'front-refined', childObj: { hobby: 'coding' }};const proxyObj = new Proxy(targetObj, { get(target, key, receiver) { console.log(`get key:${key}`); return Reflect.get(...arguments); }, set(target, key, value, receiver) { console.log(`set key:${key},value:${value}`); return Reflect.set(...arguments); }});
咱们来剖析两件事:
- 在浏览器打印一下代理之后的对象
[[Handler]]
:处理器,目前拦挡了 get
、set
[[Target]]
:代理的指标对象[[IsRevoked]]
:代理是否撤销
第一次接触 [[IsRevoked]]
的时候,有点好奇它的作用。也好奇的小伙伴看下这段代码:
// 用 Proxy 的静态方法 revocable 代理一个对象const targetObj = { id: 1, name: 'front-refined' };const { proxy, revoke } = Proxy.revocable(targetObj, {});revoke();console.log('proxy-after :>> ', proxy);proxy.id = 2;
输入如图:
报错:因为代理曾经被撤回了,所以不能对 id 进行 set 动作
- 对下面的代码在控制台打印看看输入了啥?
proxyObj.name// get key:nameproxyObj.name="hello~"// set key:name,value:hello~proxyObj.childObj.hobby// get key:childObjproxyObj.childObj.hobby="play"// get key:childObj
咱们能够看到对于 hobby 的 get/set
输入只到了 childObj 。如果是这样的话,不就拦挡不了 hobby 的 get/set
了,那怎么进行追踪,触发更新?让咱们带着疑难持续往下看。
reactive 源码(深层对象的代理)
咱们能够看到不论对 hobby 进行 get 或 set,都会先去 get childObj // get key:childObj
,那么咱们就能够在 get 拜访器里做点操作,这里拿 reactive 相干源码举个例子(我晓得看源码简单,所以我曾经精简了,并且加上了正文。这段代码能够间接 copy 运行哦~):
// 工具办法:判断是否是一个对象(注:typeof 数组 也等于 'object'const isObject = val => val !== null && typeof val === 'object';// 工具办法:值是否扭转,扭转才触发更新const hasChanged = (value, oldValue) => value !== oldValue && (value === value || oldValue === oldValue);// 工具办法:判断以后的 key 是否是曾经存在的const hasOwn = (val, key) => hasOwnProperty.call(val, key);// 闭包:生成一个 get 办法function createGetter() { return function get(target, key, receiver) { const res = Reflect.get(target, key, receiver); console.log(`getting key:${key}`); // track(target, 'get' /* GET */, key); // 深层代理对象的要害!!!判断这个属性是否是一个对象,是的话持续代理动作,使对象外部的值可追踪 if (isObject(res)) { return reactive(res); } return res; };}// 闭包:生成一个 set 办法function createSetter() { return function set(target, key, value, receiver) { const oldValue = target[key]; const hadKey = hasOwn(target, key); const result = Reflect.set(target, key, value, receiver); // 判断以后 key 是否曾经存在,不存在的话示意为新增的 key ,后续 Vue “标记”新的值使它其成为响应式 if (!hadKey) { console.log(`add key:${key},value:${value}`); // trigger(target, 'add' /* ADD */, key, value); } else if (hasChanged(value, oldValue)) { console.log(`set key:${key},value:${value}`); // trigger(target, 'set' /* SET */, key, value, oldValue); } return result; };}const get = createGetter();const set = createSetter();// 根底的处理器对象const mutableHandlers = { get, set // deleteProperty};// 裸露进来的办法,reactivefunction reactive(target) { return createReactiveObject(target, mutableHandlers);}// 创立一个响应式对象function createReactiveObject(target, baseHandlers) { const proxy = new Proxy(target, baseHandlers); return proxy;}const proxyObj = reactive({ id: 1, name: 'front-refined', childObj: { hobby: 'coding' }});proxyObj.childObj.hobby// get key:childObj// get key:hobbyproxyObj.childObj.hobby="play"// get key:childObj// set key:hobby,value:play
能够看见通过 Vue 的“洗礼”之后,咱们就能够拦挡到 hobby 的 get/set
了。
不须要 Vue.set()
了
在 Vue3 咱们曾经不须要用 Vue.set 办法来动静增加一个响应式 property,因为背地的实现机制曾经不同:
在 Vue2,应用了 Object.defineProperty 只能事后对某些属性进行拦挡,粒度较小。
在 Vue3,应用的 Proxy,拦挡的是整个对象。
简略用代码解释如:
// Object.definePropertyconst obj1 = {};Object.defineProperty(obj1, 'a', { get() { console.log('get1'); }, set() { console.log('set1'); }});obj1.b = 2;
下面的代码无任何输入!
// Proxyconst obj2 = {};const proxyObj2 = new Proxy(obj2, { get() { console.log('get2'); }, set() { console.log('set2'); }});proxyObj2.b = 2;// set2
触发了 set 拜访器。
2. shallowReactive
第一次看见这个 shallow
的字眼,我就联想到了 React 中经典的浅比拟,这个「浅」的概念是统一的,让咱们来看下:
const shallowReactiveObj = shallowReactive({ id: 1, name: 'front-refiend', childObj: { hobby: 'coding' }});// 扭转 id 是响应式的shallowReactiveObj.id = 2;// 扭转嵌套对象的属性是非响应式的,然而自身的值是有被扭转的shallowReactiveObj.childObj.hobby = 'play';
咱们看看在源码中是怎么管制的,让咱们对下面的 reactive 精简过的源码加点货色(这里简略用 // +++ 正文来示意新增的代码块):
// ...// +++ 新增了 shallow 入参// 闭包:生成一个 get 办法function createGetter(shallow = false) { return function get(target, key, receiver) { const res = Reflect.get(target, key, receiver); console.log(`get key:${key}`); // track(target, 'get' /* GET */, key); // +++ // shallow=true,就间接 return 后果,所以不会深层追踪 if (shallow) { return res; } // 深层代理对象的要害!!!判断这个属性是否是一个对象,是的话持续代理动作,使对象外部的值可追踪 if (isObject(res)) { return reactive(res); } return res; };}// +++const shallowGet = createGetter(true);// +++// 浅处理器对象,合并笼罩根底的处理器对象const shallowReactiveHandlers = Object.assign({}, mutableHandlers, { get: shallowGet});// +++// 裸露进来的办法,shallowReactivefunction shallowReactive(target) { return createReactiveObject(target, shallowReactiveHandlers);}// ...
3. readonly
官网:获取一个对象 (响应式或纯对象) 或 ref 并返回原始 proxy 的只读 proxy。只读 proxy 是深层的:拜访的任何嵌套 property 也是只读的。
举例:
const proxyObj = reactive({ childObj: { hobby: 'coding' }});const readonlyObj = readonly(proxyObj);// 如果被拷贝对象 proxyObj 做了批改,打印 readonlyObj.childObj.hobby 也会看到有变更proxyObj.childObj.hobby = 'play';console.log('readonlyObj.childObj.hobby :>> ', readonlyObj.childObj.hobby);// readonlyObj.childObj.hobby :>> play// 只读对象被扭转,正告readonlyObj.childObj.hobby = 'play';// ⚠️ Set operation on key "hobby" failed: target is readonly.
在这个例子中,readonlyObj 与 proxyObj 共享所有,除了不能被扭转。它的所有属性也都是响应式的,让咱们再看下源码,咱们仍然是对下面 reactive 精简过的源码加点货色:
// +++ 新增了 isReadonly 参数// 闭包:生成一个 get 办法function createGetter(shallow = false, isReadonly = false) { return function get(target, key, receiver) { const res = Reflect.get(target, key, receiver); console.log(`get key:${key}`); // +++ // 以后是只读的状况,本人不会被扭转,所以就没必要进行追踪变动 if (!isReadonly) { // track(target, "get" /* GET */, key); } // shallow=true,就间接 return 后果,所以不会深层追踪 if (shallow) { return res; } // 深层代理对象的要害!!!判断这个属性是否是一个对象,是的话持续代理动作,使对象外部的值可追踪 if (isObject(res)) { // +++ // 如果是只读,也要同步进行深层代理 return isReadonly ? readonly(res) : reactive(res); } return res; };}// +++const readonlyGet = createGetter(false, true);// +++// 只读处理器对象const readonlyHandlers = { get: readonlyGet, // 只读,不容许 set ,所以这里正告 set(target, key) { { console.warn( `Set operation on key "${String( key )}" failed: target is readonly.`, target ); } return true; }};// +++// 裸露进来的办法,readonlyfunction readonly(target) { return createReactiveObject(target, readonlyHandlers);}
如上,新增了一个 isReadonly 参数,用来标记是否进行深层代理。
下面的 readonly 例子就相似是“代理一个代理”,即:proxy(proxy(原始对象))
,如图:
咱们平时接触最多的子组件接管父组件传递的 props。它就是用 readonly 创立的,所以放弃了只读。要批改的话只能通过 emit 提交至父组件,从而保障了 Vue 传统的单向数据流。
4. shallowReadonly
顾名思义,就是这个代理对象 shallow=true & readonly=true
,那这样会产生什么呢?
举个例子:
const shallowReadonlyObj = shallowReadonly({ id: 1, name: 'front-refiend', childObj: { hobby: 'coding' }});shallowReadonlyObj.id = 2;// ⚠️ Set operation on key "id" failed: target is readonly. // 对象自身的属性不能被批改shallowReadonlyObj.childObj.hobby = 'runnnig';// 嵌套对象的属性能够被批改,然而是非响应式的!
咱们看看在源码中是怎么管制的,让咱们持续对下面的 reactive 精简过的源码加点货色:
// ...// +++// shallow=true & readonly=trueconst shallowReadonlyGet = createGetter(true, true);// +++// 浅只读处理器对象,合并笼罩 readonlyHandlers 处理器对象const shallowReadonlyHandlers = Object.assign({}, readonlyHandlers, { get: shallowReadonlyGet});// +++// 裸露进来的办法,shallowReadonlyfunction shallowReadonly(target) { return createReactiveObject(target, shallowReadonlyHandlers);}// ...
5. ref
集体感觉,ref 办法更加晋升咱们去了解 js 中的援用类型。简略的来讲就是把一个简略类型包装成一个对象,使它能够被追踪(响应式)。
ref 返回的是一个蕴含 .value 属性的对象。
例子:
const refNum = ref(1);refNum.value++;
让咱们来扒一扒背地的实现原理(精简了 ref 相干源码):
// 工具办法:值是否扭转,扭转才触发更新const hasChanged = (value, oldValue) => value !== oldValue && (value === value || oldValue === oldValue);// 工具办法:判断是否是一个对象(注:typeof 数组 也等于 'object'const isObject = val => val !== null && typeof val === 'object';// 工具办法:判断传入的值是否是一个对象,是的话就用 reactive 来代理const convert = val => (isObject(val) ? reactive(val) : val);function toRaw(observed) { return (observed && toRaw(observed['__v_raw' /* RAW */])) || observed;}// ref 实现类class RefImpl { constructor(_rawValue, _shallow = false) { this._rawValue = _rawValue; this._shallow = _shallow; this.__v_isRef = true; this._value = _shallow ? _rawValue : convert(_rawValue); } get value() { // track(toRaw(this), 'get' /* GET */, 'value'); return this._value; } set value(newVal) { if (hasChanged(toRaw(newVal), this._rawValue)) { this._rawValue = newVal; this._value = this._shallow ? newVal : convert(newVal); // trigger(toRaw(this), 'set' /* SET */, 'value', newVal); } }}// 创立一个 reffunction createRef(rawValue, shallow = false) { return new RefImpl(rawValue, shallow);}// 裸露进来的办法,reffunction ref(value) { return createRef(value);}// 裸露进来的办法,shallowReffunction shallowRef(value) { return createRef(value, true);}
外围类 RefImpl
,咱们能够看到在类中应用了经典的 get/set
存取器,来进行追踪和触发。convert
办法让咱们晓得了 ref 不仅仅用来包装一个值类型,也能够是一个对象/数组,而后把对象/数组再交给 reactive
进行代理。间接看个例子:
const refArr = ref([1, 2, 3]);const refObj = ref({ id: 1, name: 'front-refined' });// 操作它们refArr.value.push(1);refObj.value.id = 2;
6. unref
开展一个 ref:判断参数为 ref ,则返回 .value
,否则返回参数自身。
源码:
function isRef(r) { return Boolean(r && r.__v_isRef === true);}function unref(ref) { return isRef(ref) ? ref.value : ref;}
为了不便开发,Vue 解决了在 template 中用到的 ref 将会被主动开展,也就是不必写 .value 了,背地的实现,让咱们一起来看一下:
这里用「模仿」的形式来论述,外围逻辑没有扭转~
// 模仿:在 setup 内定义一个 refconst num = ref(1);// 模仿:在 setup 返回,提供 template 应用function setup() { return { num };}// 模仿:接管了 setup 返回的对象const setupReturnObj = setup();// 定义处理器对象,get 拜访器里的 unref 是要害const shallowUnwrapHandlers = { get: (target, key, receiver) => unref(Reflect.get(target, key, receiver)), set: (target, key, value, receiver) => { const oldValue = target[key]; if (isRef(oldValue) && !isRef(value)) { oldValue.value = value; return true; } else { return Reflect.set(target, key, value, receiver); } }};// 模仿:返回组件实例上下文const ctx = new Proxy(setupReturnObj, shallowUnwrapHandlers);// 模仿:template 最终被编译成 render 函数/* <template> <input v-model="num" /> <div>num:{{num}}</div> </template> */function render(ctx) { with (ctx) { // 模仿:在template中,进行赋值动作 "onUpdate:modelValue": $event => (num = $event) // num = 666; // 模仿:在template中,进行读取动作 {{num}} console.log('num :>> ', num); }}render(ctx);// 模仿:在 setup 外部进行赋值动作num.value += 1;// 模仿: num 扭转 trigger 视图渲染effect,更新视图render(ctx);
7. shallowRef
ref 的介绍曾经蕴含了 shallowRef 办法的实现:this._value = _shallow ? _rawValue : convert(_rawValue);
如果传入的 shallow 值为 true 那么间接返回传入的原始值,也就是说,不会再去深层代理对象了,让咱们来看两个场景:
- 传入的是一个对象
const shallowRefObj = shallowRef({ id: 1, name: 'front-refiend',});
下面的对象加工之后,咱们能够简略的了解成:
const shallowRefObj = { value: { id: 1, name: 'front-refiend' }};
既然是 shallow(浅层)那就止于 value ,不再进行深层代理。
也就是说,对于嵌套对象的属性不会进行追踪,然而咱们批改 shallowRefObj 自身的 value 属性还是响应式的,如:shallowRefObj.value = 'hello~';
- 传入的是一个简略类型
const shallowRefNum = shallowRef(1);
当传入的值是一个简略类型时候,联合这两句代码:const convert = val => (isObject(val) ? reactive(val) : val);
,this._value = _shallow ? _rawValue : convert(_rawValue);
咱们就能够晓得 shallowRef 和 ref 对于入参是一个简略类型时,其最终成果是统一的。
8. triggerRef
集体感觉这个 API 了解起来较为形象,小伙伴们一起认真推敲推敲~
triggerRef 是和 shallowRef 配合应用的,例子:
const shallowRefObj = shallowRef({ name: 'front-refined'});// 这里不会触发副作用,因为是这个 ref 是浅层的shallowRefObj.value.name = 'hello~';// 手动执行与 shallowRef 关联的任何副作用,这样子就能触发了。triggerRef(shallowRefObj);
看下背地的实现原理:
在开篇咱们有讲到的 effect 这个概念,假如以后正在走 视图渲染effect 。
template 绑定的了值,如:
<template> {{shallowRefObj.name}} </template>
当执行 “render” 时,就会读取到了 shallowRefObj.value.name ,因为以后的 ref 是浅层的,只能追踪到 value 的变动,所以在 value 的 get 办法进行 track 如:track(toRaw(this), "get" /* GET */, 'value');
track 办法源码精简:
// targetMap 是一个大汇合// activeEffect 示意以后正在走的 effect ,假如以后是 视图渲染effectfunction track(target, type, key) { 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())); } if (!dep.has(activeEffect)) { dep.add(activeEffect); }}
打印 targetMap
也就是说,如果 shallowRefObj.value 有扭转就能够 trigger 视图渲染effect 来更新视图,或着咱们也能够手动 trigger 它。
然而,咱们目前扭转的是 shallowRefObj.value.name = 'hello~';
,所以咱们要 “骗” trigger 办法。手动 trigger,只有咱们的入参对了,就会响应式更新视图了,看一下 triggerRef 与 trigger 的源码:
function triggerRef(ref) { trigger(toRaw(ref), 'set' /* SET */, 'value', ref.value);}// trigger 响应式触发function trigger(target, type, key, newValue, oldValue, oldTarget) { const depsMap = targetMap.get(target); if (!depsMap) { // 没有被追踪,间接 return return; } // 拿到了 视图渲染effect 就能够进行排队更新 effect 了 const run = depsMap.get(key); /* 开始执行 effect,这里做了很多事... */ run(); }
咱们用 target 和 key 拿到了 视图渲染的effect。至此,就能够实现一个手动更新了~
9. customRef
自定义的 ref 。这个 API 就更显式的让咱们理解 track 与 trigger,看个例子:
<template> <div>name:{{name}}</div> <input v-model="name" /></template>// ...setup() { let value = 'front-refined'; // 参数是一个工厂函数 const name = customRef((track, trigger) => { return { get() { // 收集依赖它的 effect track(); return value; }, set(newValue) { value = newValue; // 触发更新依赖它的所有 effect trigger(); } }; }); return { name };}
让咱们看下源码实现:
// 自定义ref 实现类class CustomRefImpl { constructor(factory) { this.__v_isRef = true; const { get, set } = factory( () => track(this, 'get' /* GET */, 'value'), () => trigger(this, 'set' /* SET */, 'value') ); this._get = get; this._set = set; } get value() { return this._get(); } set value(newVal) { this._set(newVal); }}function customRef(factory) { return new CustomRefImpl(factory);}
联合咱们下面有提过的 ref 源码相干,咱们能够看到 customRef 只是把 ref 外部的实现,更显式的裸露进去,让咱们更灵便的管制。比方能够提早 trigger ,如:
// ...set(newValue) { clearTimeout(timer); timer = setTimeout(() => { value = newValue; // 触发更新依赖它的所有 effect trigger(); }, 2000);}// ...
10. toRef
能够用来为响应式对象上的 property 新创建一个 ref ,从而放弃对其源 property 的响应式连贯。举个例子:
假如咱们传递给一个组合式函数一个响应式数据,在组合式函数外部就能够响应式的批改它:
// 1. 传递整个响应式对象function useHello(state) { state.name = 'hello~';}// 2. 传递一个具体的 reffunction useHello2(name) { name.value = 'hello~';}export default { setup() { const state = reactive({ id: 1, name: 'front-refiend' }); // 1. 间接传递整个响应式对象 useHello(state); // 2. 传递一个新创建的 ref useHello2(toRef(state, 'name')); }};
让咱们看下源码实现:
// ObjectRef 实现类class ObjectRefImpl { constructor(_object, _key) { this._object = _object; this._key = _key; this.__v_isRef = true; } get value() { return this._object[this._key]; } set value(newVal) { this._object[this._key] = newVal; }}// 裸露进来的办法function toRef(object, key) { return new ObjectRefImpl(object, key);}
即便 name
属性不存在,toRef 也会返回一个可用的 ref,如:咱们在下面那个例子指定了一个对象没有的属性:
useHello2(toRef(state, 'other'));
这个动作就相当于往对象新增了一个属性 other,且会响应式。
11. toRefs
toRefs 底层就是 toRef。
将响应式对象转换为一般对象,其中后果对象的每个 property 都是指向原始对象相应 property 的 ref,放弃对其源 property 的响应式连贯。
toRefs 的呈现其实也是为了开发上的便当。让咱们间接来看看它的几个应用场景:
- 解构 props
export default { props: { id: Number, name: String }, setup(props, ctx) { const { id, name } = toRefs(props); watch(id, () => { console.log('id change'); }); // 没有应用 toRefs 的话,须要通过这种形式监听 watch( () => props.id, () => { console.log('id change'); } ); }};
这样子咱们就能保障能监听到 id 的变动(没有应用 toRefs 的解构是不行的),因为通过 toRefs 办法之后,id 其实就是一个 ref 对象。
- setup return 时转换
<template> <div>id:{{id}}</div> <div>name:{{name}}</div></template>// ...setup() { const state = reactive({ id: 1, name: 'front-refiend' }); return { ...toRefs(state) };}
这样的写法咱们就更加不便的在模板上间接写对应的值,而不须要 {{state.id}}
, {{state.name}}
让咱们看下源码:
function toRefs(object) { const ret = {}; for (const key in object) { ret[key] = toRef(object, key); } return ret;}
12. compouted
结尾有讲过,compouted 是一个 “计算属性effect” 。它依赖响应式根底数据,当数据变动时候会触发它的更新。computed 次要的靓点就是缓存了,能够缓存性能开销比拟大的计算。它返回一个 ref 对象。
让咱们一起来看一个 computed 闭环的精简源码(次要是理解思路,尽管精简了,但代码还是有一丢丢多,不够看完你必定有播种。间接 copy 能够运行哦~):
<body> <fieldset> <legend>蕴含get/set办法的 computed</legend> <button onclick="handleChangeFirsttName()">changeFirsttName</button> <button onclick="handleChangeLastName()">changeLastName</button> <button onclick="handleSetFullName()">setFullName</button> </fieldset> <fieldset> <legend>只读 computed</legend> <button onclick="handleAddCount1()">handleAddCount1</button> <button onclick="handleSetCount()">handleSetCount</button> </fieldset> <script> // 大汇合,寄存依赖相干 const targetMap = new WeakMap(); // 以后正在走的 effect let activeEffect; // 精简:创立一个 effect const createReactiveEffect = (fn, options) => { const effect = function reactiveEffect() { try { activeEffect = effect; return fn(); } finally { // 以后的 effect 走完之后(相干的依赖收集结束之后),就退出 activeEffect = undefined; } }; effect.options = options; // 该副作用的依赖汇合 effect.deps = []; return effect; }; //#region 精简:ref 办法 // 工具办法:值是否扭转,扭转才触发更新 const hasChanged = (value, oldValue) => value !== oldValue && (value === value || oldValue === oldValue); // ref 实现类 class RefImpl { constructor(_rawValue) { this._rawValue = _rawValue; this.__v_isRef = true; this._value = _rawValue; } get value() { track(this, 'get', 'value'); return this._value; } set value(newVal) { if (hasChanged(newVal, this._rawValue)) { this._rawValue = newVal; this._value = newVal; trigger(this, 'set', 'value', newVal); } } } // 创立一个 ref function createRef(rawValue) { return new RefImpl(rawValue); } // 裸露进来的办法,ref function ref(value) { return createRef(value); } //#endregion //#region 精简:track、trigger const track = (target, type, key) => { if (activeEffect === undefined) { 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())); } if (!dep.has(activeEffect)) { dep.add(activeEffect); // 存储该副作用相干依赖汇合 activeEffect.deps.push(dep); } }; const trigger = (target, type, key, newValue) => { const depsMap = targetMap.get(target); if (!depsMap) { // 没有被追踪,间接 return return; } const effects = depsMap.get(key); const run = effect => { if (effect.options.scheduler) { // 调度执行 effect.options.scheduler(); } }; effects.forEach(run); }; //#endregion //#region 精简:computed 办法 const isFunction = val => typeof val === 'function'; // 裸露进来的办法 function computed(getterOrOptions) { let getter; let setter; if (isFunction(getterOrOptions)) { getter = getterOrOptions; setter = () => { // 提醒,以后的 computed 如果是只读的,也就是说没有在调用的时候传入 set 办法 console.warn('Write operation failed: computed value is readonly'); }; } else { getter = getterOrOptions.get; setter = getterOrOptions.set; } return new ComputedRefImpl(getter, setter); } // computed 外围办法 class ComputedRefImpl { constructor(getter, _setter) { this._setter = _setter; this._dirty = true; this.effect = createReactiveEffect(getter, { scheduler: () => { // 依赖的数据扭转了,标记为脏值,等 get value 时进行计算获取 if (!this._dirty) { this._dirty = true; } } }); } get value() { // 脏值须要计算 _dirty=true 代表须要计算 if (this._dirty) { console.log('脏值,须要计算...'); this._value = this.effect(); // 标记脏值为 false,进行缓存值(下次获取时,不须要计算) this._dirty = false; } return this._value; } set value(newValue) { this._setter(newValue); } } //#endregion //#region 例子 // 1. 创立一个只读 computed const count1 = ref(0); const count = computed(() => { return count1.value * 10; }); const handleAddCount1 = () => { count1.value++; console.log('count.value :>> ', count.value); }; const handleSetCount = () => { count.value = 1000; }; // 2. 创立一个蕴含 get/set 办法的 computed // 获取的 computed 数据 const consoleFullName = () => console.log('fullName.value :>> ', fullName.value); const firsttName = ref('san'); const lastName = ref('zhang'); const fullName = computed({ get: () => firsttName.value + '.' + lastName.value, set: val => { lastName.value += val; } }); // 扭转依赖的值触发 computed 更新 const handleChangeFirsttName = () => { firsttName.value = 'si'; consoleFullName(); }; // 扭转依赖的值触发 computed 更新 const handleChangeLastName = () => { lastName.value = 'li'; consoleFullName(); }; // 触发 fullName set,如果 computed 为只读就正告 const handleSetFullName = () => { fullName.value = ' happy niu year~'; consoleFullName(); }; // 必须要有读取行为,才会进行依赖收集。当依赖扭转时候,才会响应式更新! consoleFullName(); //#endregion </script></body>
computed 的闭环流程是这样子的:
computed 创立的 ref 对象首次被调用 get(读 computed 的 value),会进行依赖收集,当依赖扭转时,调度执行触发 dirty = true
,标记脏值,须要计算。下一次再去调用 computed 的 get 时候,就须要从新计算获取新值,如此重复。
13. watch
对于 watch ,这里间接先上一段稍长的源码例子(代码挺长,然而都是精简过的,而且有正文分块。小伙伴们急躁看,copy 能够间接运行哦~)
<body> <button onclick="handleChangeCount()">点我触发watch</button> <button onclick="handleChangeCount2()">点我触发watchEffect</button> <script> // 大汇合,寄存依赖相干 const targetMap = new WeakMap(); // 以后正在走的 effect let activeEffect; // 精简:创立一个 effect const createReactiveEffect = (fn, options) => { const effect = function reactiveEffect() { try { activeEffect = effect; return fn(); } finally { // 以后的 effect 走完之后(相干的依赖收集结束之后),就退出 activeEffect = undefined; } }; effect.options = options; // 该副作用的依赖汇合 effect.deps = []; return effect; }; //#region 精简:ref 办法 // 工具办法:判断是否是一个 ref 对象 const isRef = r => { return Boolean(r && r.__v_isRef === true); }; // 工具办法:值是否扭转,扭转才触发更新 const hasChanged = (value, oldValue) => value !== oldValue && (value === value || oldValue === oldValue); // 工具办法:判断是否是一个办法 const isFunction = val => typeof val === 'function'; // ref 实现类 class RefImpl { constructor(_rawValue) { this._rawValue = _rawValue; this.__v_isRef = true; this._value = _rawValue; } get value() { track(this, 'get', 'value'); return this._value; } set value(newVal) { if (hasChanged(newVal, this._rawValue)) { this._rawValue = newVal; this._value = newVal; trigger(this, 'set', 'value', newVal); } } } // 创立一个 ref function createRef(rawValue) { return new RefImpl(rawValue); } // 裸露进来的办法,ref function ref(value) { return createRef(value); } //#endregion //#region 精简:track、trigger const track = (target, type, key) => { if (activeEffect === undefined) { 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())); } if (!dep.has(activeEffect)) { dep.add(activeEffect); // 存储该副作用相干依赖汇合 activeEffect.deps.push(dep); } }; const trigger = (target, type, key, newValue) => { const depsMap = targetMap.get(target); if (!depsMap) { // 没有被追踪,间接 return return; } const effects = depsMap.get(key); const run = effect => { if (effect.options.scheduler) { // 调度执行 effect.options.scheduler(); } }; effects.forEach(run); }; //#endregion //#region 进行监听相干 // 进行侦听,如果有 onStop 办法一并调用,onStop 也就是 onInvalidate 回调办法 function stop(effect) { cleanup(effect); if (effect.options.onStop) { effect.options.onStop(); } } // 清空改 effect 收集的依赖相干,这样子扭转了就不再持续触发了,也就是“进行侦听” function cleanup(effect) { const { deps } = effect; if (deps.length) { for (let i = 0; i < deps.length; i++) { deps[i].delete(effect); } deps.length = 0; } } //#endregion //#region 裸露进来的 watchEffect 办法 function watchEffect(effect, options) { return doWatch(effect, null, options); } //#endregion //#region 裸露进来的 watch 办法 function watch(source, cb, options) { return doWatch(source, cb, options); } function doWatch(source, cb, { immediate, deep } = {}) { let getter; // 判断是否 ref 对象 if (isRef(source)) { getter = () => source.value; } // 判断是一个 reactive 对象,默认递归追踪 deep=true else if (/*isReactive(source)*/ 0) { // 省略... // getter = () => source; // deep = true; } // 判断是一个数组,也就是 Vue3 新的个性,watch 能够以数组的形式侦听 else if (/*isArray(source)*/ 0) { // 省略... } // 判断是否是一个办法,这样子的入参 else if (isFunction(source)) { debugger; // 这里是相似这样子的入参,() => proxyObj.id if (cb) { // 省略... } else { // cb 为 null,示意以后为 watchEffect getter = () => { if (cleanup) { cleanup(); } return source(onInvalidate); }; } } // 判断是否 deep 就会递归追踪 if (/*cb && deep*/ 0) { // const baseGetter = getter; // getter = () => traverse(baseGetter()); } // 清理 effect let cleanup; const onInvalidate = fn => { cleanup = runner.options.onStop = () => { fn(); }; }; let oldValue = undefined; const job = () => { if (cb) { // 获取扭转扭转后的新值 const newValue = runner(); if (hasChanged(newValue, oldValue)) { if (cleanup) { cleanup(); } // 触发回调 cb(newValue, oldValue, onInvalidate); // 把新值赋值给旧值 oldValue = newValue; } } else { // watchEffect runner(); } }; // 调度 let scheduler; // default: 'pre' scheduler = () => { job(); }; // 创立一个 effect,调用 runner 其实就是在进行依赖收集 const runner = createReactiveEffect(getter, { scheduler }); // 初始化 run if (cb) { if (immediate) { job(); } else { oldValue = runner(); } } else { // watchEffect 默认立刻执行 runner(); } // 返回一个办法,调用即进行侦听 return () => { stop(runner); }; } //#endregion //#region 例子 // 1. watch 例子 const count = ref(0); const myStop = watch( count, (val, oldVal, onInvalidate) => { onInvalidate(() => { console.log('watch-clear...'); }); console.log('watch-val :>> ', val); console.log('watch-oldVal :>> ', oldVal); }, { immediate: true } ); // 扭转依赖的值触发 触发侦听器回调 const handleChangeCount = () => { count.value++; }; // 进行侦听 // myStop(); // 2. watchEffect 例子 const count2 = ref(0); watchEffect(() => { console.log('watchEffect-count2.value :>> ', count2.value); }); // 扭转依赖的值触发 触发侦听器回调 const handleChangeCount2 = () => { count2.value++; }; //#endregion </script></body>
以上的代码简略的实现了 watch 监听 ref 对象的例子,那么咱们该如何去正确的应用 watch 呢?让咱们一起联合源码一起看两点:
- 对于侦听源的写法,官网有形容,能够是返回值的 getter 函数,也能够间接是 ref,也就是:
const state = reactive({ id: 1 });// 应用() => state.id// 或const count = ref(0);// 应用 countcount// 看完源码,咱们也能够这样子写~() => count.value
联合源码,咱们发现也能够间接侦听一个 reactive 对象,而且默认会进进行深度监听(deep=true
),会对对象进行递归遍历追踪。然而侦听一个数组的话,只有当数组被替换时才会触发回调。如果你须要在数组扭转时触发回调,必须指定 deep
选项。当没有指定 deep = true
:
const arr = ref([1, 2, 3]);// 只有这种形式才会失效arr.value = [4, 5, 6];// 其余的无奈触发回调arr.value[0] = 111;arr.value.push(4);
集体倡议尽量避免深度侦听,因为这可能会影响性能,大部分场景咱们都能够应用侦听一个 getter 的形式,比方须要侦听数组的变动 () => arr.value.length
。如果你想要同时监听一个对象多个值的变动,Vue3 提供了数组的操作:
watch( [() => state.id, () => state.name], ([id, name], [oldId, oldName]) => { /* ... */ });
- watch 返回值也就是一个进行侦听的办法,它与 onInvalidate 实质是不同的,当咱们调用了进行侦听,底层是做了移除以后清空该 effect 收集的依赖汇合,这样子依赖数据扭转了就不再持续触发了,也就是“进行侦听”。而
onInvalidate
,集体认为,它就是提供了一个在回调之前的操作,具体的例子,能够参考之前写过的一篇文章
[Vue3丨从 5 个维度来讲 Vue3 变动
](https://juejin.cn/post/691000... 详情看 watchEffect vs watch 内容。
14. watchEffect
和 watch 共享底层代码,在 watch 剖析中咱们曾经有体现了,小伙伴们能够往上再看看,这里不再赘述~
看了那么多有些许简单的源码之后,让咱们来轻松一下,来看下 Vue3 一些响应式 API 的小工具。小伙伴应该都有看到一些源码中带有 __v_
前缀的属性,其实这些属性是用来做一些判断的标识,让咱们一起来看看:
15. isReadonly
查看对象是否是由 readonly 创立的只读 proxy。
function isReadonly(value) { return !!(value && value["__v_isReadonly" /* IS_READONLY */]);}// readonlyconst originalObj = reactive({ id: 1 });const copyObj = readonly(originalObj);isReadonly(copyObj); // true// 只读 computed const firsttName = ref('san');const lastName = ref('zhang');const fullName = computed( () => firsttName.value + ' ' + lastName.value);isReadonly(fullName); // true
其实在创立一个 get 拜访器的时候,利用闭包就曾经记录了,而后通过对应的 key 去获取,如:
function createGetter(isReadonly = false, shallow = false) { return function get(target, key, receiver) { // ... if (key === '__v_isReadonly') { return isReadonly; } // ... };}
16. isReactive
查看对象是否是 reactive 创立的响应式 proxy。
function isReactive(value) { if (isReadonly(value)) { return isReactive(value["__v_raw" /* RAW */]); } return !!(value && value["__v_isReactive" /* IS_REACTIVE */]);}
createGetter 办法判断相干:
// ...if (key === '__v_isReactive' /* IS_REACTIVE */) { return !isReadonly;} else if (key === '__v_isReadonly' /* IS_READONLY */) { return isReadonly;}// ...
17. isProxy
查看对象是否是由 reactive 或 readonly 创立的 proxy。
function isProxy(value) { return isReactive(value) || isReadonly(value);}
18. toRaw
toRaw 能够用来打印原始对象,有时候咱们在调试查看控制台的时候,就比拟不便。
function toRaw(observed) { return ((observed && toRaw(observed["__v_raw" /* RAW */])) || observed);}
toRaw 对于转换 ref 对象,依然保留包装过的对象,例子:
const obj = reactive({ id: 1, name: 'front-refiend' });console.log(toRaw(obj));// {id: 1, name: "front-refiend"}const count = ref(0);console.log(toRaw(count));// {__v_isRef: true, _rawValue: 0, _shallow: false, _value: 0, value: 0}
createGetter 办法判断相干:
// ...if ( key === '__v_raw' /* RAW */ && receiver === reactiveMap.get(target)) { return target;}// ...
咱们能够在 createGetter 时就会把对象用 {key:原始对象,value:proxy 代理对象}
这样子的模式寄存于 reactiveMap ,而后依据键来取值。
19. markRaw
标记一个对象,使其永远不会转换为 proxy。返回对象自身。
const def = (obj, key, value) => { Object.defineProperty(obj, key, { configurable: true, enumerable: false, value });};function markRaw(value) { // 标记跳过对该对象的代理 def(value, "__v_skip" /* SKIP */, true); return value;}
createReactiveObject 办法相干:
function createReactiveObject(target) { //... // 判断对象中是否含有 __v_skip 属性是的话,间接返回对象自身 if (target['__v_skip']) { return target; } const proxy = new Proxy(target); // ... return proxy;}
20. isRef
判断是否是 ref 对象。__v_isRef
标识就是咱们在创立 ref 的时候在 RefImpl实现类里赋值的 this.__v_isRef = true;
function isRef(r) { return Boolean(r && r.__v_isRef === true);}
总结
以上的 20 个API,在咱们我的项目实战中,有些兴许简直没有用到。因为有局部API,是 Vue3 整个框架设计有应用到的。对于咱们的业务场景来说,目前应用频次较高的应该是 reactive
,ref
,computed
,watch
,toRefs
...
了解所有响应式 API 对于咱们在编码会更加有自信,不会有那么多的纳闷。也帮忙咱们更加了解框架的底层,如:proxy 怎么用的?Vue3 怎么追踪一个简略类型的?怎么去编码能力让咱们零碎更优。这才是本文剖析这几个 API 的初衷。
怎么样,你理解这 20 个响应式 API 了吗?
???? 前端精,求关注~
2021年,公众号关注「前端精」(front-refined),咱们一起学 Vue3,用 Vue3,深刻 Vue3 。
最初,祝小伙伴们新年快乐,开开心心过春节~