乐趣区

关于前端:Vue3丨进一步了解这-20-个响应式-API写码如有神

后面说的话

在 Vue2 中,集体感觉对于数据的操作比拟“黑盒”。
而 Vue3 把响应式零碎更显式地裸露进去,使得咱们对数据的操作有了更多的灵活性。
所以,对于 Vue3 的几个响应式的 API,咱们须要更加的了解把握,能力在实战中运用自如。

先理解,什么是响应式?

  • Vue3 官网有举过一个例子
var val1 = 2
var val2 = 3
var 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);
  }
});

咱们来剖析两件事:

  1. 在浏览器打印一下代理之后的对象

[[Handler]]:处理器,目前拦挡了 getset
[[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 动作

  1. 对下面的代码在控制台打印看看输入了啥?
proxyObj.name
// get key:name
proxyObj.name="hello~"
// set key:name,value:hello~

proxyObj.childObj.hobby
// get key:childObj
proxyObj.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
};
// 裸露进来的办法,reactive
function 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:hobby
proxyObj.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.defineProperty
const obj1 = {};
Object.defineProperty(obj1, 'a', {get() {console.log('get1');
  },
  set() {console.log('set1');
  }
});
obj1.b = 2;

下面的代码无任何输入!

// Proxy
const 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});
// +++
// 裸露进来的办法,shallowReactive
function 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;
  }
};
// +++
// 裸露进来的办法,readonly
function 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=true
const shallowReadonlyGet = createGetter(true, true);
// +++
// 浅只读处理器对象,合并笼罩 readonlyHandlers 处理器对象
const shallowReadonlyHandlers = Object.assign({}, readonlyHandlers, {get: shallowReadonlyGet});
// +++
// 裸露进来的办法,shallowReadonly
function 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);
    }
  }
}
// 创立一个 ref
function createRef(rawValue, shallow = false) {return new RefImpl(rawValue, shallow);
}
// 裸露进来的办法,ref
function ref(value) {return createRef(value);
}
// 裸露进来的办法,shallowRef
function 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 内定义一个 ref
const 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 那么间接返回传入的原始值,也就是说,不会再去深层代理对象了,让咱们来看两个场景:

  1. 传入的是一个对象
const shallowRefObj = shallowRef({
  id: 1,
  name: 'front-refiend',
});

下面的对象加工之后,咱们能够简略的了解成:

const shallowRefObj = {
  value: {
    id: 1,
    name: 'front-refiend'
  }
};

既然是 shallow(浅层)那就止于 value,不再进行深层代理。
也就是说,对于嵌套对象的属性不会进行追踪,然而咱们批改 shallowRefObj 自身的 value 属性还是响应式的,如:shallowRefObj.value = 'hello~';

  1. 传入的是一个简略类型
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,假如以后是 视图渲染 effect
function 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. 传递一个具体的 ref
function 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 的呈现其实也是为了开发上的便当。让咱们间接来看看它的几个应用场景:

  1. 解构 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 对象。

  1. 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);
// 应用 count
count
// 看完源码,咱们也能够这样子写~
() => 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 */]);
}

// readonly
const 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 整个框架设计有应用到的。对于咱们的业务场景来说,目前应用频次较高的应该是 reactiverefcomputedwatchtoRefs
了解所有响应式 API 对于咱们在编码会更加有自信,不会有那么多的纳闷。也帮忙咱们更加了解框架的底层,如:proxy 怎么用的?Vue3 怎么追踪一个简略类型的?怎么去编码能力让咱们零碎更优。这才是本文剖析这几个 API 的初衷。
怎么样,你理解这 20 个响应式 API 了吗?

???? 前端精,求关注~

2021 年,公众号关注「前端精」(front-refined),咱们一起学 Vue3,用 Vue3,深刻 Vue3。
最初,祝小伙伴们新年快乐,开开心心过春节~

退出移动版