响应式

ref

  1. 可以把「基本数据类型」数据转成响应式的,一般用来定义一个基本类型的响应式数据:
    • 如果用 ref(对象/数组), 内部会自动将「对象/数组」转换为 reactive 的代理对象;
    • ref 内部: 通过给 value 属性添加 getter/setter 来实现对数据的劫持;
  2. 如何获取 ref 包装的值:
    • js 中操作数据: 属性.value
    • 模板中操作数据: {{ 属性 }}
  3. 示例代码:
    import { ref } from "vue";
    export default {
      setup() {
        const count = ref(1);
    
        console.log(count);
    
        return { count };
      },
    };
    

reactive

  1. 可以把「引用类型数据」转成响应式的,创建一个深度响应的对象;

  2. 缺点:

    • reactive 返回的对象不能解构,解构后的属性不是响应式的;
    • 可以使用 toRef/toRefs 把属性转成响应式的;
  3. 示例代码:

    import { reactive } from "vue";
    export default {
      setup() {
        const state = reactive({
          name: "tom",
          age: 25,
        });
    
        return { state };
      },
    };
    
  4. reactive 对比 ref

    1. ref 可以把「基本数据类型」数据转换成响应式对象,reactive 可以把「引用数据类型」数据转成响应式;
    2. ref 返回的对象,重新赋值成对象也是响应式的,reactive 返回的对象,重新赋值丢失响应式;
    3. ref 返回的对象结构后还是响应式的,reactive 返回的对象解构后不是响应式的;

computed

  1. 创建一个基于其他响应式状态的计算属性;

  2. 根据已知的响应式数据得到一个新的响应式数据;

  3. 示例代码:

    JavaScript
    JavaScript
    // 第一种用法:只是完成了读的功能
    import { computed, reactive } from 'vue';
    export default {
      setup() {
        let per = reactive({
          surname: '勇敢',
          name: '小陈',
        });
    
        per.fullName = computed(() => {
          return per.surname + '~' + per.name;
        });
    
        return {
          per,
        };
      },
    };
    
    // 第二种用法:完成了读和写的功能
    import { computed, reactive } from 'vue';
    export default {
      setup() {
        let per = reactive({
          surname: '勇敢',
          name: '小陈',
        });
    
        per.fullName = computed({
          get() {
            return per.surname + '~' + per.name;
          },
          set(value) {
            var arr = value.split('~');
            per.surname = arr[0];
            per.name = arr[1];
          },
        });
    
        return {
          per,
        };
      },
    };
    

readonly

  1. 创建一个只读的响应式对象,使用方式和 reactive 相同;

  2. 保护状态数据不被外部修改;

  3. 示例代码:

    import { reactive, readonly } from 'vue';​
    
    ​const state = reactive({ count: 0 });
    ​const readonlyState = readonly(state);
    

响应式工具

isRef

  1. 检查一个值是否为 ref 对象;

  2. 示例代码:

    import { ref, isRef } from 'vue';
    
    const count = ref(0);
    console.log(isRef(count)); // true
    console.log(isRef(0));     // false
    

unref

  1. 如果参数是 ref 则返回其内部值,否则返回参数本身;这是 val = isRef(val) ? val.value : val 的语法糖;

  2. 示例代码:

    import { ref, unref } from 'vue';
    
    const count = ref(1);
    console.log(unref(count)); // 1
    console.log(unref(2));     // 2
    

toRef

  1. 基于响应式对象的一个属性创建一个 ref,且与该属性保持同步 (同一个引用地址)

  2. 示例代码:

    import { reactive, toRef } from 'vue';
    
    const state = reactive({ foo: 1 });
    const fooRef = toRef(state, 'foo');
    
    state.foo++;
    console.log(fooRef.value); // 2
    
    fooRef.value++;
    console.log(state.foo);    // 3
    

toValue

  1. refgetter 规范化为值,Vue 3.3+ 新增;

  2. 示例代码:

    import { ref, toValue } from 'vue';
    
    const count = ref(1);
    console.log(toValue(count));    // 1
    console.log(toValue(() => 2));  // 2
    

toRefs

  1. 将响应式对象转换为普通对象,其中每个属性都是指向原始对象相应属性的 ref

  2. 示例代码:

    import { reactive, toRefs } from 'vue';
    
    const state = reactive({ foo: 1, bar: 2 });
    const refs = toRefs(state);
    
    console.log(refs.foo.value); // 1
    console.log(refs.bar.value); // 2
    

isProxy

  1. 检查一个对象是否是由 reactivereadonly 创建的代理;

  2. 示例代码:

    import { reactive, readonly, isProxy } from 'vue';
    
    const state = reactive({});
    const readOnlyState = readonly({});
    
    console.log(isProxy(state));          // true
    console.log(isProxy(readOnlyState));  // true
    console.log(isProxy({}));             // false
    

isReactive

  1. 检查一个对象是否是由 reactive 创建的响应式代理;

  2. 示例代码:

    import { reactive, readonly, isReactive } from 'vue';
    
    const state = reactive({});
    const readOnlyState = readonly(state);
    
    console.log(isReactive(state));         // true
    console.log(isReactive(readOnlyState)); // false
    

isReadonly

  1. 检查一个对象是否是由 readonly 创建的只读代理;

  2. 示例代码:

    import { reactive, readonly, isReadonly } from 'vue';
    
    const state = reactive({});
    const readOnlyState = readonly({});
    
    console.log(isReadonly(state));         // false
    console.log(isReadonly(readOnlyState)); // true
    

响应式进阶

shallowRef

  1. 创建一个 ref,只跟踪 .value 的变化,不深度响应其内部值;

  2. 使用场景:当只需要浅层响应而不需要深度响应时使用;

  3. 示例代码:

    import { shallowRef } from 'vue';
    
    const state = shallowRef({ count: 1 });
    
    // 不会触发响应式更新
    state.value.count = 2;
    
    // 会触发响应式更新
    state.value = { count: 2 };
    

triggerRef

  1. 手动触发与 shallowRef 关联的副作用;

  2. 使用场景:当需要手动触发 ref 更新时使用;

  3. 示例代码:

    import { shallowRef, triggerRef } from 'vue';
    
    const state = shallowRef({ count: 1 });
    
    // 修改内部值不会自动触发更新
    state.value.count = 2;
    
    // 手动触发更新
    triggerRef(state);
    

customRef

  1. 创建一个自定义的 ref,可以显式控制其依赖追踪和更新触发;

  2. 使用场景:需要自定义 ref 的行为时使用;

  3. 示例代码:

    import { customRef } from 'vue';
    
    function useDebouncedRef(value, delay = 200) {
      let timeout;
      return customRef((track, trigger) => {
        return {
          get() {
            track();
            return value;
          },
          set(newValue) {
            clearTimeout(timeout);
            timeout = setTimeout(() => {
              value = newValue;
              trigger();
            }, delay);
          }
        }
      })
    }
    
    const text = useDebouncedRef('hello');
    
  4. 实际案例

    • 防抖 Ref(Debounce Ref)
    import { customRef } from 'vue';
    
    function useDebouncedRef(value, delay = 500) {
      let timeout;
      return customRef((track, trigger) => {
        return {
          get() {
            track(); // 依赖收集
            return value;
          },
          set(newValue) {
            clearTimeout(timeout); // 清除之前的定时器
            timeout = setTimeout(() => {
              value = newValue;
              trigger(); // 延迟后触发更新
            }, delay);
          }
        };
      });
    };
    
    // 使用
    const searchText = useDebouncedRef('');
    
    • 异步数据 Ref
    import { customRef } from 'vue';
    
    function useAsyncRef(url) {
      const data = ref(null);
      const loading = ref(false);
      const error = ref(null);
    
      customRef((track, trigger) => {
        const fetchData = async () => {
          loading.value = true;
          try {
            const response = await fetch(url);
            data.value = await response.json();
          } catch (err) {
            error.value = err;
          } finally {
            loading.value = false;
            trigger(); // 数据更新后触发重新渲染
          }
        }
    
        fetchData();
    
        return {
          get() {
            track(); // 依赖收集
            return { data, loading, error };
          },
          set() {} // 通常不需要 set,因为是只读的
        }
      })
    
      return { data, loading, error };
    }
    
    // 使用
    const { data, loading, error } = useAsyncRef('https://api.example.com/data');
    
    • 本地存储 Ref(自动同步 localStorage)
    import { customRef } from 'vue';
    
    function useLocalStorageRef(key, defaultValue) {
      return customRef((track, trigger) => {
        // 从 localStorage 读取初始值
        let value = localStorage.getItem(key) ?? defaultValue;
    
        return {
          get() {
            track(); // 依赖收集
            return value;
          },
          set(newValue) {
            value = newValue;
            localStorage.setItem(key, newValue); // 存入 localStorage
            trigger(); // 触发更新
          }
        }
      });
    };
    
    // 使用
    const username = useLocalStorageRef('username', 'Guest');
    

shallowReactive

  1. 创建一个响应式代理,只对根级别属性进行响应式转换;

  2. 使用场景:当只需要浅层响应而不需要深度响应时使用;

  3. 示例代码:

    import { shallowReactive } from 'vue';
    
    const state = shallowReactive({ 
      foo: 1,
      nested: { bar: 2 }
    });
    
    // 响应式
    state.foo++;
    
    // 非响应式
    state.nested.bar++;
    

shallowReadonly

  1. 创建一个只读代理,只对根级别属性设为只读;

  2. 使用场景:当只需要浅层只读而不需要深度只读时使用;

  3. 示例代码:

    import { shallowReadonly } from 'vue';
    
    const state = shallowReadonly({ 
      foo: 1,
      nested: { bar: 2 }
    });
    
    // 报错
    state.foo++;
    
    // 允许
    state.nested.bar++;
    

toRaw

  1. 返回 reactivereadonly 代理的原始对象;

  2. 使用场景:需要获取响应式对象的原始非响应对象时使用;

  3. 示例代码:

    import { reactive, toRaw } from 'vue';
    
    const obj = {};
    const proxy = reactive(obj);
    
    console.log(toRaw(proxy) === obj); // true
    

markRaw

  1. 标记一个对象,使其永远不会被转换为代理;

  2. 使用场景:当不希望某个对象被 Vue 转换为响应式对象时使用;

  3. 示例代码:

    import { reactive, markRaw } from 'vue';
    
    const obj = { foo: 1 };
    markRaw(obj);
    
    const state = reactive({ nested: obj });
    // obj 不会成为响应式对象
    console.log(isReactive(state.nested)); // false
    

effectScope

  1. effectScopeVue 3.2+ 引入的一个 API,用于更精细地控制 effect 的作用域和生命周期;它是 Vue 响应式系统的一个高级特性,主要用于解决 effect 的批量管理和清理问题 ("effect"指的是响应式数据变化时触发的副作用函数)

  2. effectScope 的作用:

    • 集中管理多个 effect
    • 统一停止这些 effect
    • 控制 effect 的生命周期;
  3. 主要 API

    API 描述
    effectScope(detached?: boolean) · 创建一个 effect 作用域
    · detached: 是否创建独立的作用域 (不继承父作用域)
    scope.run(fn: () => T): T 在作用域内运行函数,函数内创建的所有 effect 都会被该作用域收集
    scope.stop() 停止作用域内所有的 effect
    getCurrentScope() 获取当前活动的 effect 作用域
    onScopeDispose(fn: () => void) 在当前作用域停止时调用的回调函数
  4. 嵌套作用域、独立作用域

    <template>
        <div class="view">
            <h2>父计数器: {{ parentCount }}</h2>
            <h3>子计数器: {{ childCount }}</h3>
            <button @click="incrementParent">incrementParent</button>
            <button @click="incrementChild">incrementChild</button>
            <button @click="stopParent">停止父作用域</button>
            <button @click="stopChild">停止子作用域</button>
        </div>
    </template>
    
    <script setup lang="ts">
    import { effectScope, ref, watchEffect } from 'vue';
    
    const parentScope = effectScope();
    let childScope;
    const parentCount = ref(0);
    const childCount = ref(0);
    
    // 父作用域
    parentScope.run(() => {
        watchEffect(() => {
            console.log('父计数器:', parentCount.value)
        });
    
        // 子作用域 (默认继承父作用域)
        childScope = effectScope();
        childScope.run(() => {
            watchEffect(() => {
                console.log('子计数器:', childCount.value);
            });
        });
    
        // 独立作用域 (不受父作用域影响)
        const detachedScope = effectScope(true);
        detachedScope.run(() => {
            watchEffect(() => {
                console.log('独立作用域计数器:', parentCount.value + childCount.value);
            });
        });
    });
    
    const incrementParent = () => {
        parentCount.value++;
    }
    
    const incrementChild = () => {
        childCount.value++;
    }
    
    const stopParent = () => {
        parentScope.stop(); // 会停止父子作用域,但不会停止独立作用域
    }
    
    const stopChild = () => {
        childScope.stop(); // 会停止子作用域,但不会停止独立作用域
    }
    </script>
    
  5. 使用场景:

    // 1. 可复用的组合式函数
    export function useMouseTracker() {
      const scope = effectScope();
      const x = ref(0);
      const y = ref(0);
      
      scope.run(() => {
        watchEffect(() => {
          // 鼠标跟踪逻辑
          const update = (e: MouseEvent) => {
            x.value = e.pageX;
            y.value = e.pageY;
          }
          window.addEventListener('mousemove', update);
          onScopeDispose(() => {
            window.removeEventListener('mousemove', update);
          });
        });
      });
      
      return {
        x,
        y,
        stop: scope.stop
      };
    }
    
  6. 与普通 effect 管理的对比:

    TypeScript
    TypeScript
    // 1. 传统方式
    const disposables = [];
    
    const stop1 = watchEffect(() => { /* ... */ });
    disposables.push(stop1);
    
    const stop2 = watchEffect(() => { /* ... */ });
    disposables.push(stop2);
    
    // 清理时需要逐个调用
    disposables.forEach(stop => stop());
    
    // 2. 使用 effectScope
    const scope = effectScope();
    
    scope.run(() => {
      watchEffect(() => { /* ... */ });
      watchEffect(() => { /* ... */ });
    });
    
    // 一键清理
    scope.stop();
    
  7. 注意事项:

    • 自动收集:scope.run() 内的所有 effect 都会被自动收集,无需手动管理;
    • 嵌套关系:默认情况下,子作用域会继承父作用域,父作用域停止时子作用域也会停止;
    • 独立作用域:使用 effectScope(true) 创建独立作用域,不受父作用域影响;
    • 生命周期:effectScope 不会自动绑定到组件生命周期,需要手动调用 stop()
    • 调试:在开发模式下,作用域会提供更好的调试信息;

getCurrentScope

  1. 返回当前活跃的 effect 作用域(如果有);

  2. 使用场景:在 effect 作用域内部获取当前作用域实例;

  3. 示例代码:

    import { effectScope, getCurrentScope } from 'vue';
    
    effectScope(() => {
      console.log(getCurrentScope()); // 返回当前作用域
    });
    

onScopeDispose

  1. 在当前 effect 作用域被销毁时调用回调(类似 onUnmounted,但用于作用域);

  2. 使用场景:在 effect 作用域被停止时执行清理操作;

  3. 示例代码:

    import { effectScope, onScopeDispose } from 'vue';
    
    effectScope((scope) => {
      onScopeDispose(() => {
        console.log('作用域销毁');
      })
      
      scope.stop(); // 触发回调
    })
    

面试题

Vue3 为什么用 Proxy 替代 defineProperty?

  1. JS 中做属性拦截常见的方式有三种:defineProperty、getter/setter、Proxy;

  2. Vue2 中使用 defineProperty 的原因是,2013 年时只能用这种方式,由于该 API 存在一些局限性,比如:

    • 对于数组的拦截有问题,为此 vue 需要专门为数组响应式做一套实现;无法监听原生数组,需要特殊处理,重写覆盖 ‘push’, ‘pop’, ‘shift’, ‘unshift’, ‘splice’, ‘sort’, ‘reverse’ 的原生方法;
    • 另外不能拦截那些新增、删除属性,需要使用 vue.set, vue.delete;
    • 最后 defineProperty 方案在初始化时需要深度递归遍历待处理的对象才能对它进行完全拦截,明显增加了初始化的时间;
  3. 以上两点在 Proxy 出现之后迎刃而解,不仅可以对数组实现拦截,还能对 Map、Set 实现拦截:另外 Proxy 的拦截也是懒处理行为,如果用户没有访问嵌套对象,那么也不会实施拦截,这就让初始化的速度和内存占用都改善了;

  4. 当然 Proxy 是有兼容性问题的,IE 完全不支持,所以如果需要 IE 兼容就不合适;

ref 定义数组和 reactive 定义数组的区别?

  1. ref 定义数组

    • 初始化数组
      const arr = ref([1,2,3])
      
      watch(arr.value, () => { // 这个时候通过直接修改和利用数组的方法修改都可以监测到
        console.log('数组变化了')
      })
      const pushArray = () => emptyArray.value.splice(0, 0, 19)
      const changeArrayItem = () => emptyArray.value[0] = 10
      
    • 未初始化数组
      const arr = ref([])
      
      // 必须是深度监听,这种写法不仅可以监听数组本身的变化,也可以监听数组元素的变化
      watch( 
        arr,
        () => {
          console.log('空数组变化了')
        },
        {
          deep: true
        }
      )
      const pushArray = () => arr.value.splice(0, 0, { value: 12 })
      const changeArrayItem = () => arr.value[0] = { value: 32 }
      
      onMounted(() => arr.value = [{ value: 5 }, { value: 2 }, { value: 3 }, { value: 4 }]
      
  2. reactive 定义数组

    • 问题:arr = newArr 这一步使得 arr 失去了响应式的效果
      let arr = reactive([])
      
      function change() {
        let newArr = [1, 2, 3]
        arr = newArr
      }
      
    • 解决:使用 ref 定义、使用 push 方法、数组外层嵌套一个对象
      // 方法一:使用 ref
      let arr = ref([])
      function change() {
        let newArr = [1, 2, 3]
        arr.value = newArr
      }
      
      // 方法二:使用 push 方法
      let arr = reactive([])
      function change() {
        let newArr = [1, 2, 3]
        arr.push(...newArr)
      }
      
      // 方法三:外层嵌套一个对象
      let arr = reactive({ list: [] })
      function change() {
        let newArr = [1, 2, 3]
        arr.list = newArr
      }
      
打赏作者
您的打赏是我前进的动力
微信
支付宝
评论

你好👏🏻,我是 ✍🏻   疯狂 codding 中...

粽子

这有关于前端开发的技术文档和你分享。

相信你可以在这里找到对你有用的知识和教程。

了解更多

目录

  1. 1. 响应式
    1. 1.1. ref
    2. 1.2. reactive
    3. 1.3. computed
    4. 1.4. readonly
  2. 2. 响应式工具
    1. 2.1. isRef
    2. 2.2. unref
    3. 2.3. toRef
    4. 2.4. toValue
    5. 2.5. toRefs
    6. 2.6. isProxy
    7. 2.7. isReactive
    8. 2.8. isReadonly
  3. 3. 响应式进阶
    1. 3.1. shallowRef
    2. 3.2. triggerRef
    3. 3.3. customRef
    4. 3.4. shallowReactive
    5. 3.5. shallowReadonly
    6. 3.6. toRaw
    7. 3.7. markRaw
    8. 3.8. effectScope
    9. 3.9. getCurrentScope
    10. 3.10. onScopeDispose
  4. 4. 面试题
    1. 4.1. Vue3 为什么用 Proxy 替代 defineProperty?
    2. 4.2. ref 定义数组和 reactive 定义数组的区别?