常见问题三

在前端开发中,Vue 的数据响应机制、脚本加载策略以及函数式编程技巧是高频考点和日常开发的核心基础。本文将围绕这几个关键点展开详细解析,帮助开发者深入理解其原理与应用。

一、Vue2 与 Vue3 的数据响应原理对比

Vue 的核心特性之一是数据响应式------ 当数据变化时,视图自动更新。但 Vue2 和 Vue3 实现这一特性的底层原理存在显著差异。

1. Vue2 的数据响应原理:Object.defineProperty

Vue2 通过 **Object.defineProperty劫持对象的 getter 和 setter** 实现响应式。其核心逻辑是:

初始化时遍历data中的属性,为每个属性设置getter(获取值时收集依赖)和setter(修改值时触发更新)。

实现核心步骤:
  • 递归遍历data中的所有属性(包括嵌套对象);
  • 对每个属性调用Object.defineProperty,重写getset方法;
  • 当属性被访问时(get),收集当前依赖(如组件渲染函数);
  • 当属性被修改时(set),通知所有依赖更新(触发视图重新渲染)。
代码示例(简化版):
复制代码
function defineReactive(obj, key, value) {
  // 递归处理嵌套对象
  observe(value);
  Object.defineProperty(obj, key, {
    get() {
      console.log(`获取${key}的值: ${value}`);
      // 收集依赖(实际中会关联Dep和Watcher)
      Dep.target && dep.addSub(Dep.target);
      return value;
    },
    set(newVal) {
      if (newVal !== value) {
        console.log(`更新${key}的值: ${newVal}`);
        value = newVal;
        // 通知依赖更新
        dep.notify();
      }
    }
  });
}

// 遍历对象属性,批量设置响应式
function observe(obj) {
  if (typeof obj !== 'object' || obj === null) return;
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key]);
  });
}
局限性:
  • 无法监听数组索引变化 (如arr[0] = 1)和对象新增属性 (如obj.newKey = 1);
  • 需通过Vue.setthis.$set手动触发响应式(本质是为新增属性重新设置getter/setter);
  • 对数组的监听通过重写push/pop/splice等 7 个方法实现(修改数组时触发更新)。

2. Vue3 的数据响应原理:Proxy

Vue3 改用 **Proxy代理对象 ** 实现响应式,解决了 Vue2 的局限性。Proxy可以直接代理整个对象,而非单个属性,支持监听更多场景。

实现核心优势:
  • 代理整个对象:无需递归遍历属性,初始化性能更好;
  • 支持监听新增属性 / 删除属性 :如obj.newKey = 1delete obj.key
  • 原生支持数组索引修改 :如arr[0] = 1可直接被监听;
  • 支持MapSet等复杂数据结构的响应式。
代码示例(简化版):
复制代码
function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      console.log(`获取${key}的值: ${target[key]}`);
      // 收集依赖(类似Vue2的Dep)
      track(target, key);
      return target[key];
    },
    set(target, key, value) {
      if (target[key] !== value) {
        console.log(`更新${key}的值: ${value}`);
        target[key] = value;
        // 通知更新(类似Vue2的Watcher)
        trigger(target, key);
      }
    },
    deleteProperty(target, key) {
      console.log(`删除${key}`);
      delete target[key];
      trigger(target, key); // 删除也触发更新
    }
  });
}
总结:
特性 Vue2(Object.defineProperty) Vue3(Proxy)
监听范围 仅已定义的属性 整个对象(含新增 / 删除)
数组支持 需重写方法,不支持索引修改 原生支持索引修改和方法调用
初始化性能 递归遍历属性,性能较差 懒代理,性能更优
复杂数据结构(Map) 不支持 支持

二、Vue2 如何监听 data 里的数据变化

Vue2 对data的监听是一个递归劫持 + 依赖收集 的过程,核心通过ObserverDepWatcher三个类协同实现。

1. 核心流程:

  1. 初始化data :组件初始化时,data函数返回的对象会被传入observe函数;
  2. 创建Observer实例observe函数会为对象创建Observer实例,负责劫持属性;
  3. 劫持属性Observer通过Object.defineProperty重写对象的getter/setter
  4. 收集依赖(Dep :当属性被访问时(如渲染时),getter会将当前Watcher(依赖)添加到Dep(依赖管理器)中;
  5. 触发更新 :当属性被修改时,setter会通知DepDep再通知所有Watcher执行更新(如重新渲染组件)。

2. 数组的特殊处理:

由于Object.defineProperty无法监听数组索引变化,Vue2 通过重写数组原型方法实现监听:

复制代码
// 重写数组的7个变更方法
const arrayMethods = Object.create(Array.prototype);
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(method => {
  arrayMethods[method] = function(...args) {
    // 执行原数组方法
    const result = Array.prototype[method].apply(this, args);
    // 通知更新(触发Observer的dep)
    this.__ob__.dep.notify();
    return result;
  };
});

// 为数组设置新原型
function observeArray(arr) {
  arr.__proto__ = arrayMethods; // 覆盖数组原型
  for (let i = 0; i < arr.length; i++) {
    observe(arr[i]); // 递归监听数组元素
  }
}

三、watch 与 computed 的区别

watchcomputed都是 Vue 中监听数据变化的工具,但应用场景截然不同。

1. 核心差异对比:

特性 computed(计算属性) watch(监听器)
本质 基于依赖的衍生数据(类似 "变量") 数据变化后的回调函数(类似 "事件")
缓存机制 有缓存,依赖不变则不重新计算 无缓存,数据变化即触发回调
返回值 必须有返回值(用于页面渲染) 无返回值(用于执行副作用,如异步操作)
异步支持 不支持(不能包含异步逻辑,否则缓存失效) 支持(可执行异步操作,如接口请求)
适用场景 简单的衍生数据计算(如拼接字符串、计算总价) 复杂的副作用处理(如数据变化后请求接口)

2. 代码示例:

复制代码
// computed示例:计算全名(有缓存)
computed: {
  fullName() {
    // 依赖firstName和lastName,只有它们变化时才重新计算
    return `${this.firstName} ${this.lastName}`;
  }
}

// watch示例:监听name变化,执行异步操作
watch: {
  name(newVal, oldVal) {
    // 支持异步(如请求接口)
    this.$axios.get(`/user?name=${newVal}`).then(res => {
      this.userInfo = res.data;
    });
  }
}

3. 总结:

  • 当需要根据已有数据生成新数据 时,用computed(利用缓存提升性能);
  • 当需要在数据变化时执行异步操作或复杂逻辑 时,用watch

四、script 标签的 defer 和 async 区别

script标签的deferasync属性用于控制脚本的加载与执行时机,解决默认加载阻塞 HTML 解析的问题。

1. 默认行为:

不添加deferasync时,脚本加载会阻塞 HTML 解析 :浏览器遇到script标签时,会暂停 HTML 解析,下载脚本并立即执行,执行完成后再继续解析 HTML。

2. defer 与 async 的差异:

特性 async defer
加载时机 并行下载脚本(不阻塞 HTML 解析) 并行下载脚本(不阻塞 HTML 解析)
执行时机 下载完成后立即执行(可能打断 HTML 解析) 下载完成后等待 HTML 解析完毕再执行
执行顺序 不保证顺序(哪个先下载完就先执行) 严格按照 HTML 中脚本的顺序执行
适用场景 独立脚本(如统计脚本、广告脚本) 依赖 DOM 或顺序执行的脚本(如 jQuery 插件)

3. 执行流程图示:

  • 默认(无属性):加载阻塞解析 → 执行阻塞解析;
  • async:加载不阻塞解析 → 执行阻塞解析(顺序不确定);
  • defer:加载不阻塞解析 → 执行在 HTML 解析完成后(顺序与标签一致)。

4. 总结:

  • 若脚本无需依赖 DOM 且无顺序要求 (如独立工具库),用async
  • 若脚本依赖 DOM 或需要按顺序执行 (如 jQuery 后加载插件),用defer

五、函数柯里化及常见应用场景

函数柯里化(Currying) 是将多参数函数转化为一系列单参数函数的技术,形如f(a,b,c)f(a)(b)(c)

1. 核心原理:

通过闭包收集参数,当参数数量满足原函数需求时,执行原函数;否则返回一个新函数继续收集参数。

实现示例:
复制代码
// 将多参数函数柯里化
function curry(fn) {
  const argsLength = fn.length; // 原函数的参数数量
  return function curried(...args) {
    // 若收集的参数足够,执行原函数
    if (args.length >= argsLength) {
      return fn.apply(this, args);
    }
    // 否则返回新函数,继续收集参数
    return function(...nextArgs) {
      return curried.apply(this, args.concat(nextArgs));
    };
  };
}

// 测试:柯里化add函数
const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);

curriedAdd(1)(2)(3); // 6
curriedAdd(1, 2)(3); // 6
curriedAdd(1)(2, 3); // 6

2. 常见应用场景:

  • 参数复用 :固定部分参数,简化函数调用。

    例:计算商品税后价格(固定税率):

    复制代码
    const calculateTax = (taxRate, price) => price * (1 + taxRate);
    const withTax = curry(calculateTax)(0.1); // 固定税率10%
    withTax(100); // 110(无需重复传税率)
  • 延迟执行 :分步传递参数,直到条件满足再执行。

    例:事件绑定中分步传参:

    复制代码
    // 柯里化事件处理函数
    const handleClick = curry((id, event) => {
      console.log(`点击了ID为${id}的元素,事件:`, event);
    });
    
    // 绑定事件时先传id,事件触发时传event
    document.getElementById('btn1').onclick = handleClick(1);
    document.getElementById('btn2').onclick = handleClick(2);
  • 函数式编程 :配合composepipe等工具实现函数组合。

总结

本文解析了前端开发中的 5 个核心知识点:

  • Vue2 通过Object.defineProperty劫持属性,Vue3 通过Proxy代理对象,后者更全面;
  • Vue2 对data的监听是递归劫持 + 依赖收集的过程,数组需特殊处理;
  • computed适用于衍生数据计算(有缓存),watch适用于异步副作用(无缓存);
  • async脚本加载完立即执行(乱序),defer等待 HTML 解析后执行(顺序);
  • 柯里化通过闭包分步收集参数,适用于参数复用、延迟执行等场景。
相关推荐
德育处主任2 分钟前
p5.js 正方形square的基础用法
前端·数据可视化·canvas
烛阴3 分钟前
Mix - Bilinear Interpolation
前端·webgl
90后的晨仔7 分钟前
Vue 3 应用实例详解:从 createApp 到 mount,你真正掌握了吗?
前端·vue.js
德育处主任14 分钟前
p5.js 矩形rect绘制教程
前端·数据可视化·canvas
前端工作日常34 分钟前
我学习到的babel插件移除Flow 类型注解效果
前端·babel·前端工程化
SY_FC35 分钟前
uniapp input 聚焦时键盘弹起滚动到对应的部分
javascript·vue.js·elementui
爱吃香蕉的阿豪39 分钟前
SignalR 全解析:核心原理、适用场景与 Vue + .NET Core 实战
vue.js·microsoft·c#·.netcore·signalr
前端工作日常1 小时前
我学习到的 Babel 配置
前端·babel·前端工程化
xw52 小时前
uni-app项目跑APP报useStore报错
前端·uni-app
!win !2 小时前
uni-app项目跑APP报useStore报错
前端·uni-app