阅读视图

发现新文章,点击刷新页面。

Vue3中的watch和wactEffect有什么区别,分别适用在什么场景?

在 Vue3 中,watchwatchEffect是用于响应式监听数据变化的两个 API,它们的主要区别和适用场景如下:

核心区别

  1. 触发时机

    • watch:只监听明确指定的数据源,且只有当这些数据源变化时才会触发回调。
    • watchEffect:会自动追踪其内部使用的所有响应式依赖,并在初始执行时立即触发一次,之后依赖变化时再次触发。
  2. 依赖声明方式

    • watch:需要显式指定要监听的数据源,可以是一个或多个响应式引用、计算属性等。
    • watchEffect:不需要显式指定依赖,自动捕获回调函数中使用的所有响应式数据。
  3. 回调参数

    • watch:回调函数接收三个参数:新值、旧值和可选的onCleanup函数(用于清理副作用)。
    • watchEffect:回调函数只接收一个onCleanup函数,不提供新旧值对比。

适用场景

  1. watch 的适用场景

    • 需要访问变化前后的值(例如计算差值、记录变更历史)。
    • 监听特定数据的变化(例如表单输入、路由参数)。
    • 需要延迟或异步执行副作用(例如 API 请求)。
    • 需要在组件销毁时手动清理副作用(例如定时器、WebSocket 连接)。
  2. watchEffect 的适用场景

    • 需要根据多个响应式依赖自动触发副作用(例如自动保存表单数据)。
    • 副作用不需要访问旧值(例如更新 DOM、同步本地存储)。
    • 简化依赖追踪(无需显式列出所有依赖)。

示例对比

以下是一个简单的示例,展示两者的差异:

import { ref, watch, watchEffect } from 'vue'

const count = ref(0)
const double = computed(() => count.value * 2)

// watch示例:监听count变化
watch(count, (newValue, oldValue) => {
  console.log(`count从${oldValue}变为${newValue}`)
  // 可以访问新旧值进行比较
})

// watchEffect示例:自动追踪所有依赖
watchEffect(() => {
  console.log(`count或double变化了:${count.value}, ${double.value}`)
  // 不需要显式声明依赖,只要内部使用的响应式数据变化就会触发
})

// 修改count值
count.value++ // 同时触发watch和watchEffect

总结

  • 使用 watch:当需要精确控制监听的数据源,并需要访问变化前后的值时。
  • 使用 watchEffect:当需要自动响应所有依赖的变化,且不需要新旧值对比时。

最后,以官方文档清晰的说明来结尾:

image.png

选择合适的 API 可以使代码更清晰、更高效,避免不必要的副作用触发。

如何丝滑使用JavaScript的装饰器?

在 JavaScript 里,装饰器(Decorators)是一种能对类、方法、属性的行为进行扩展或者修改的语法。它的核心原理是借助元编程,在不改变原有代码结构的前提下,为目标添加新功能。

基本概念

直接show code,现有如下代码,用来记录log日志:

function log(target, name, descriptor) {
  const original = descriptor.value;
  descriptor.value = function(...args) {
    console.log(`调用 ${name} 方法,参数:${JSON.stringify(args)}`);
    const result = original.apply(this, args);
    console.log(`方法 ${name} 返回:${result}`);
    return result;
  };
  return descriptor;
}

class Calculator {
  @log
  add(a, b) {
    return a + b;
  }
}

// 使用示例
const calc = new Calculator();
calc.add(3, 4); // 控制台会输出调用信息和返回结果

装饰器函数参数解析

在 JavaScript 装饰器中,log 函数的三个参数分别代表:

  1. target:被装饰的类或原型对象。

    • 若装饰的是类方法,target 就是类的原型(prototype)。
    • 若装饰的是类,target 就是类本身。
  2. name:被装饰的方法或属性的名称(字符串类型)。

  3. descriptor:属性描述符对象(与 Object.defineProperty 中的描述符相同),包含以下属性:

    • value:被装饰的方法或属性的值(即原始函数)。
    • writable:是否可修改(布尔值)。
    • enumerable:是否可枚举(布尔值)。
    • configurable:是否可配置(布尔值)。

函数实现原理详解

log 装饰器的核心逻辑是替换原始方法,在执行前后添加日志:


    function log(target, name, descriptor) {
      // 1. 保存原始方法的引用
      const original = descriptor.value;

      // 2. 修改 descriptor.value 为新函数
      descriptor.value = function(...args) {
        // 3. 执行前置逻辑(打印入参)
        console.log(`调用 ${name} 方法,参数:${JSON.stringify(args)}`);
        
        // 4. 执行原始方法并保存结果
        const result = original.apply(this, args);
        
        // 5. 执行后置逻辑(打印返回值)
        console.log(`方法 ${name} 返回:${result}`);
        
        // 6. 返回原始结果
        return result;
      };

      // 7. 返回修改后的描述符
      return descriptor;
    }

为什么要这样实现?

这种写法的关键点在于:

  1. 不改变原始方法的核心逻辑:通过包装原始方法,在不修改其代码的前提下添加新功能。

  2. 保留上下文(this

    • 使用 original.apply(this, args) 确保原始方法在调用时的 this 指向不变。
    • 若直接调用 original(args),可能导致 this 指向全局对象(非严格模式)或 undefined(严格模式)。
  3. 支持任意参数

    • 使用剩余参数 ...args 收集所有传入参数。
    • 使用 JSON.stringify(args) 将参数序列化为字符串(需注意无法处理函数或 undefined 类型的参数)。
  4. 遵循装饰器规范

    • 装饰器必须返回一个描述符对象(或新类)。
    • 通过修改 descriptor.value 替换原始方法。

应用示例

使用该装饰器的类方法会自动添加日志功能:


    class Calculator {
      @log
      add(a, b) {
        return a + b;
      }
    }

    const calc = new Calculator();
    calc.add(3, 4);

    // 输出:
    // 调用 add 方法,参数:[3,4]
    // 方法 add 返回:7

注意事项

  1. 参数序列化限制

    • JSON.stringify 无法处理函数或 undefined 参数,可能导致日志不完整。
    • 改进方案:使用 args.map(arg => String(arg)).join(', ') 或自定义序列化函数。
  2. 异步方法处理

    • 若原始方法返回 Promise,需使用 await 等待结果:

      
          descriptor.value = async function(...args) {
            // ...
            const result = await original.apply(this, args);
            // ...
          };
      
  3. 兼容性

    • 装饰器语法需 Babel 或 TypeScript 支持。

    • 确保项目配置中启用了装饰器(如 @babel/plugin-proposal-decorators)。

通过这种方式,装饰器实现了 ** 横切关注点(Cross-cutting Concerns)** 的分离,让日志、权限等功能与核心业务逻辑解耦。

下面介绍装饰器常见的应用场景:

1. 日志记录

装饰器能够在方法执行的前后添加日志,这样可以对函数的调用情况进行监控。

function log(target, name, descriptor) {
  const original = descriptor.value;
  descriptor.value = function(...args) {
    console.log(`调用 ${name} 方法,参数:${JSON.stringify(args)}`);
    const result = original.apply(this, args);
    console.log(`方法 ${name} 返回:${result}`);
    return result;
  };
  return descriptor;
}

class Calculator {
  @log
  add(a, b) {
    return a + b;
  }
}

// 使用示例
const calc = new Calculator();
calc.add(3, 4); // 控制台会输出调用信息和返回结果

2. 权限验证

可以在执行方法前对用户权限进行检查,防止未授权的访问。


    function auth(requiredRole) {
      return function(target, name, descriptor) {
        const original = descriptor.value;
        descriptor.value = function(...args) {
          if (this.userRole !== requiredRole) {
            throw new Error("权限不足");
          }
          return original.apply(this, args);
        };
        return descriptor;
      };
    }

    class AdminPanel {
      userRole = "admin";

      @auth("admin")
      deleteUser() {
        return "用户已删除";
      }
    }

3. 性能分析

装饰器能够对函数的执行时间进行测量,有助于性能优化。


    function benchmark(target, name, descriptor) {
      const original = descriptor.value;
      descriptor.value = async function(...args) {
        const start = performance.now();
        const result = await original.apply(this, args);
        const end = performance.now();
        console.log(`${name} 方法执行耗时:${end - start}ms`);
        return result;
      };
      return descriptor;
    }

    class DataService {
      @benchmark
      async fetchData() {
        await new Promise(resolve => setTimeout(resolve, 1000));
        return { data: "大量数据" };
      }
    }

4. 自动绑定

在 React 等框架中,装饰器可以解决方法上下文丢失的问题。


    function autobind(target, name, descriptor) {
      const original = descriptor.value;
      return {
        configurable: true,
        get() {
          const bound = original.bind(this);
          Object.defineProperty(this, name, {
            value: bound,
            configurable: true,
            writable: true
          });
          return bound;
        }
      };
    }

    class Component {
      constructor() {
        this.state = { count: 0 };
      }

      @autobind
      increment() {
        this.state.count++;
      }
    }

5. 单例模式实现

装饰器可以确保一个类仅有一个实例。


    function singleton(constructor) {
      let instance;
      return function(...args) {
        if (!instance) {
          instance = new constructor(...args);
        }
        return instance;
      };
    }

    @singleton
    class AppState {
      constructor() {
        this.data = {};
      }
    }

    const state1 = new AppState();
    const state2 = new AppState();
    console.log(state1 === state2); // 输出 true

6. 类型检查

在运行时对函数参数的类型进行验证。


    function validateTypes(target, name, descriptor) {
      const original = descriptor.value;
      descriptor.value = function(...args) {
        const paramTypes = Reflect.getMetadata("design:paramtypes", target, name);
        args.forEach((arg, i) => {
          if (arg && paramTypes[i] && !(arg instanceof paramTypes[i])) {
            throw new TypeError(`参数 ${i} 类型错误,期望 ${paramTypes[i].name}`);
          }
        });
        return original.apply(this, args);
      };
      return descriptor;
    }

    class MathUtils {
      @validateTypes
      add(a: number, b: number) {
        return a + b;
      }
    }

7. 缓存机制

对函数的计算结果进行缓存,避免重复计算。


    function memoize(target, name, descriptor) {
      const original = descriptor.value;
      const cache = new Map();
      descriptor.value = function(...args) {
        const key = args.toString();
        if (cache.has(key)) {
          return cache.get(key);
        }
        const result = original.apply(this, args);
        cache.set(key, result);
        return result;
      };
      return descriptor;
    }

    class Fibonacci {
      @memoize
      calculate(n) {
        return n <= 1 ? n : this.calculate(n - 1) + this.calculate(n - 2);
      }
    }

装饰器使用注意要点

  • 要启用装饰器语法,需要在 Babel 或者 TypeScript 中进行配置。

  • 装饰器的执行顺序是从下往上,例如:

    
        @a
        @b
        method() {} // 先执行 b,再执行 a
    
    
  • 装饰器可以返回一个新的类或者修改原有的描述符(descriptor)。

装饰器的主要价值在于它遵循了开放 - 封闭原则,即对扩展开放,对修改封闭。它能让代码变得更加简洁,同时增强代码的可复用性。

❌