美文网首页Vue3前端开发那些事儿Vue
Vue3丨进一步了解这 20 个响应式 API,写码如有神

Vue3丨进一步了解这 20 个响应式 API,写码如有神

作者: 前端精 | 来源:发表于2021-02-08 14:33 被阅读0次

    前面说的话

    在 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 变化
      详情看 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 。
    最后,祝小伙伴们新年快乐,开开心心过春节~

    相关文章

      网友评论

        本文标题:Vue3丨进一步了解这 20 个响应式 API,写码如有神

        本文链接:https://www.haomeiwen.com/subject/aovytltx.html