常见问题三
在前端开发中,Vue 的数据响应机制、脚本加载策略以及函数式编程技巧是高频考点和日常开发的核心基础。本文将围绕这几个关键点展开详细解析,帮助开发者深入理解其原理与应用。
一、Vue2 与 Vue3 的数据响应原理对比
Vue 的核心特性之一是数据响应式—— 当数据变化时,视图自动更新。但 Vue2 和 Vue3 实现这一特性的底层原理存在显著差异。
1. Vue2 的数据响应原理:Object.defineProperty
Vue2 通过 **Object.defineProperty
劫持对象的 getter 和 setter** 实现响应式。其核心逻辑是:
初始化时遍历data
中的属性,为每个属性设置getter
(获取值时收集依赖)和setter
(修改值时触发更新)。
实现核心步骤:
- 递归遍历
data
中的所有属性(包括嵌套对象); - 对每个属性调用
Object.defineProperty
,重写get
和set
方法; - 当属性被访问时(
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.set
或this.$set
手动触发响应式(本质是为新增属性重新设置getter/setter
); - 对数组的监听通过重写
push/pop/splice
等 7 个方法实现(修改数组时触发更新)。
2. Vue3 的数据响应原理:Proxy
Vue3 改用 **Proxy
代理对象 ** 实现响应式,解决了 Vue2 的局限性。Proxy
可以直接代理整个对象,而非单个属性,支持监听更多场景。
实现核心优势:
- 代理整个对象:无需递归遍历属性,初始化性能更好;
- 支持监听新增属性 / 删除属性:如
obj.newKey = 1
或delete obj.key
; - 原生支持数组索引修改:如
arr[0] = 1
可直接被监听; - 支持
Map
、Set
等复杂数据结构的响应式。
代码示例(简化版):
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 如何监听 data 里的数据变化
Vue2 对data
的监听是一个递归劫持 + 依赖收集的过程,核心通过Observer
、Dep
、Watcher
三个类协同实现。
1. 核心流程:
- 初始化
data
:组件初始化时,data
函数返回的对象会被传入observe
函数; - 创建
Observer
实例:observe
函数会为对象创建Observer
实例,负责劫持属性; - 劫持属性:
Observer
通过Object.defineProperty
重写对象的getter/setter
; - 收集依赖(
Dep
):当属性被访问时(如渲染时),getter
会将当前Watcher
(依赖)添加到Dep
(依赖管理器)中; - 触发更新:当属性被修改时,
setter
会通知Dep
,Dep
再通知所有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 的区别
watch
和computed
都是 Vue 中监听数据变化的工具,但应用场景截然不同。
1. 核心差异对比:
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
标签的defer
和async
属性用于控制脚本的加载与执行时机,解决默认加载阻塞 HTML 解析的问题。
1. 默认行为:
不添加defer
或async
时,脚本加载会阻塞 HTML 解析:浏览器遇到script
标签时,会暂停 HTML 解析,下载脚本并立即执行,执行完成后再继续解析 HTML。
2. defer 与 async 的差异:
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); // 6curriedAdd(1, 2)(3); // 6curriedAdd(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,事件触发时传eventdocument.getElementById(\'btn1\').onclick = handleClick(1);document.getElementById(\'btn2\').onclick = handleClick(2);
-
函数式编程:配合
compose
、pipe
等工具实现函数组合。
总结
本文解析了前端开发中的 5 个核心知识点:
- Vue2 通过
Object.defineProperty
劫持属性,Vue3 通过Proxy
代理对象,后者更全面; - Vue2 对
data
的监听是递归劫持 + 依赖收集的过程,数组需特殊处理; computed
适用于衍生数据计算(有缓存),watch
适用于异步副作用(无缓存);async
脚本加载完立即执行(乱序),defer
等待 HTML 解析后执行(顺序);- 柯里化通过闭包分步收集参数,适用于参数复用、延迟执行等场景。