赞
踩
大家好,我是程序员蒿里行。浅浅记录一下面试中的高频问题,请你谈一下Vue响应式原理。 必备前置知识,Vue2
官方文档中深入响应式原理及Vue3
官方文档中深入响应式系统。
响应式本质是当数据变化的时候,会自动执行一些相关函数。
const apple = {
price: 2,
amount: 3
}
const totalPrice = () => apple.price * apple.amount;
假设去水果店买苹果,价格为两元,买三个,总价是六元。但是苹果价格调整后,我还得重新计算一遍总价,即调用totalPrice函数。针对我们前端场景,数据的变动,无法直接响应页面变化,还需要做额外操作,如获取DOM,将数据设置到对应节点上。为了减少程序员的开发负担,响应式也就诞生了。
下面的例子实现了极简的响应式,数据变动,会自动更新页面。
<div class="card">
<p id="firstName"></p>
<p id="lastName"></p>
<p id="age"></p>
</div>
<form>
<input oninput="user.name = this.value"/>
<input type="date" onchange="user.birthday = this.value">
</form>
var user = { name: '张三', birthday: '2000-1-1' }; observe(user) // 观察对象 // 显示姓氏 function showFirstName () { var firstName = document.getElementById('firstName'); firstName.textContent = '姓: ' + (user.name[0] || ''); } // 显示名字 function showLastName () { var lastName = document.getElementById('lastName'); lastName.textContent = '名: ' + user.name.slice(1); } // 显示年龄 function showAge () { var age = document.getElementById('age'); var birth = new Date(user.birthday); var now = new Date(); age.textContent = '年龄: ' + (now.getFullYear() - birth.getFullYear()); } autoRun(showFirstName) autoRun(showLastName) autoRun(showAge) /** * 观察某个对象的所有属性 * @param {Object} obj */ function observe(obj) { for(const key in obj) { let internalValue = obj[key]; let funcs = new Set(); Object.defineProperty(obj, key, { get() { // 依赖收集,记录:是哪个函数在用我 if (window.__func && !funcs.has(window.__func)) { funcs.add(window.__func); } return internalValue; }, set(val) { internalValue = val; // 派发更新,运行:执行用我的函数 for (const func of funcs) { func(); } } }); } } function autoRun(fn){ window.__func = fn fn() window.__func = null; }
没有响应式系统时开发者要更新页面,通常需要手动更新DOM,抑或使用模板引擎。下面是最古老的模板引擎mastche.js的用法:
const Mustache = require('mustache');
const view = {
title: "Joe",
calc: () => ( 2 + 4 )
};
const template = `<div>{{title}} spends {{calc}}</div>`
const output = Mustache.render(template, view);
// <div>Joe spends 6</div>
Vue有自己的模板引擎,它的文本插值使用的也是“Mustache”语法 (即双大括号)。
上面两种方式不仅无法自动更新视图,而且含有大量的节点操作,代码可维护性和可读性差。 响应式不仅使得数据变化能够自动更新视图,无需手动操作DOM,而且解耦了数据和视图逻辑,简化了代码结构,提高了代码可维护性和可读性。 说了这么多响应式的好处,下面让我们来简单探究下Vue是如何实现一个响应式。
Vue的数据响应式原理是通过数据劫持结合发布-订阅者模式实现的。大概原理如下:
Object.defineProperty会直接在一个对象上定义一个新属性,或修改其现有属性,并返回此对象。它接收三个参数,分别为
/**
* obj: 要定义属性的对象。
* prop: 一个字符串或 Symbol,指定了要定义或修改的属性键。
* descriptor: 要定义或修改的属性的描述符。
*/
Object.defineProperty(obj, prop, descriptor)
Object.defineProperty自定义 setter 和 getter
function Archiver() { let temperature = null; const archive = []; Object.defineProperty(this, "temperature", { get() { console.log("get!"); return temperature; }, set(value) { temperature = value; archive.push({ val: temperature }); }, }); this.getArchive = () => archive; } const arc = new Archiver(); arc.temperature; // 'get!' arc.temperature = 11; arc.temperature = 13; console.log(arc.getArchive()); // [{ val: 11 }, { val: 13 }]
Proxy对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。它接收两个参数:
/**
* target: 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
* handler: 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。
*/
const p = new Proxy(target, handler)
Proxy自定义 setter 和 getter
// 目标对象 const target = { name: 'John', age: 30 }; // 处理器对象 const handler = { // 自定义 getter get: function(target, property) { console.log(`Getting ${property}`); return target[property]; }, // 自定义 setter set: function(target, property, value) { console.log(`Setting ${property} to ${value}`); target[property] = value; } }; // 创建代理 const p = new Proxy(target, handler); // 访问属性 console.log(p.name); // 输出:Getting name // 输出:John // 修改属性 p.age = 40; // 输出:Setting age to 40
发布-订阅模式(Publish-Subscribe Pattern)是一种设计模式,用于构建对象之间的解耦和通信。在这种模式中,发布者(Publisher)和订阅者(Subscriber)之间通过一个中介者(或称为主题、事件通道等)进行通信,而不直接相互关联。 简而言之,发布者发布事件,而订阅者订阅这些事件。当事件发生时,发布者会将事件发送给所有订阅者。这样,订阅者就能够接收到事件并做出相应的响应。 举个例子,比如DOM 事件处理:
<script>
// 发布者(Publisher): HTML 元素。
const body = document.body;
// 订阅者(Subscriber): JavaScript 事件处理函数。
const handleClick = () => {
alert('body clicked!');
}
const handleDBClick = () => {
alert('body dbclicked');
}
// 订阅(Subscribe)
body.addEventListener('click', handleClick);
body.addEventListener('dbclick', handleDBClick);
// 发布(Publish): HTML 元素触发了特定事件,所有订阅者的处理函数将被调用
</script>
// 依赖收集器 class Dep { constructor() { this.subscribers = []; } // 添加依赖 depend() { if (Dep.target) { // 将当前Watcher添加到依赖列表 this.subscribers.push(Dep.target); } } // 通知依赖更新 notify() { this.subscribers.forEach(subscriber => { subscriber(); }); } } // 全局变量,当前正在计算的Watcher Dep.target = null; // Watcher class Watcher { constructor(vm, update) { this.vm = vm; this.update = update; // 设置当前Watcher为Dep的target Dep.target = this.update; // 初始化时触发getter,收集依赖 this.update(); // 清空Dep的target,防止notify触发时,重复绑定Watcher与Dep Dep.target = null; } } // 数据响应式 function defineReactive(obj, key) { const dep = new Dep(); let val = obj[key]; Object.defineProperty(obj, key, { enumerable: true, configurable: true, get() { console.log('Getter:', key); dep.depend(); // 添加依赖 return val; }, set(newVal) { if (newVal === val) { return; } val = newVal; console.log('Setter:', key); dep.notify(); // 通知依赖更新 } }); } // 递归遍历对象,使其所有属性都变成响应式 function observe(obj) { if (typeof obj !== 'object' || obj === null) { return; } Object.keys(obj).forEach(key => { defineReactive(obj, key); observe(obj[key]); }); } // 创建响应式对象 function reactive(obj) { observe(obj); return obj; } // 示例 const data = reactive({ message: 'Hello, Vue!', }); // Watcher监听message属性 new Watcher(data, () => { console.log('Message Updated:', data.message); }); // 模拟视图中读取数据 console.log('Initial Message:', data.message); // 模拟视图中修改数据 data.message = 'Hello, Reactive!'; // 可以在不同地方添加Watcher new Watcher(data, () => { console.log('Another Watcher:', data.message); }); // 修改数据,触发所有相关Watcher更新 data.message = 'New Message!';
上述代码中Dep 类可以被视为发布者(Publisher),而 Watcher 类则是订阅者(Subscriber),具体来说:
为什么要使用 Dep 和 Watcher 呢?
主要是为了实现数据响应式的机制,具体原因如下:
// 响应式核心逻辑 function reactive(obj) { const handlers = { // 拦截对象属性的读取操作 get(target, key, receiver) { track(target, key); // 收集依赖 return Reflect.get(target, key, receiver); }, // 拦截对象属性的设置操作 set(target, key, value, receiver) { Reflect.set(target, key, value, receiver); trigger(target, key); // 触发更新 return true; } }; // 创建并返回代理对象 return new Proxy(obj, handlers); } // 依赖收集相关逻辑 let activeEffect = null; // 当前活跃的响应式副作用 // 创建响应式副作用 function effect(callback) { activeEffect = callback; // 将当前回调函数设为活跃的响应式副作用 callback(); // 调用一次以收集依赖 activeEffect = null; // 调用结束后,将活跃的副作用置空 } // 使用 WeakMap 来存储对象的依赖关系 const targetMap = new WeakMap(); // 收集依赖 function track(target, key) { if (!activeEffect) return; // 如果当前没有活跃的响应式副作用,则不进行依赖收集 let depsMap = targetMap.get(target); // 获取目标对象的依赖映射 if (!depsMap) { depsMap = new Map(); // 若不存在依赖映射,则创建一个新的 Map 对象 targetMap.set(target, depsMap); // 将依赖映射存储到 WeakMap 中 } let dep = depsMap.get(key); // 获取属性对应的依赖集合 if (!dep) { dep = new Set(); // 若不存在依赖集合,则创建一个新的 Set 对象 depsMap.set(key, dep); // 将依赖集合存储到 Map 中 } dep.add(activeEffect); // 将当前活跃的副作用添加到依赖集合中 } // 触发更新 function trigger(target, key) { const depsMap = targetMap.get(target); // 获取目标对象的依赖映射 if (!depsMap) return; // 若不存在依赖映射,则直接返回 const dep = depsMap.get(key); // 获取属性对应的依赖集合 if (dep) { dep.forEach(effect => { effect(); // 遍历依赖集合并执行每个副作用函数 }); } } // 示例 const state = reactive({ count: 0 }); // 创建响应式副作用 effect(() => { console.log("Count updated:", state.count); }); console.log("Initial count:", state.count); state.count = 1; // 触发更新 state.count = 2; // 触发更新
为何Vue 3 的响应式代码实现比 Vue 2 更简洁?
响应式是现代前端开发中非常重要的概念,它使得数据与视图能够保持同步,同时也提高了代码的可维护性和可读性。通过深入理解 Vue 的响应式原理,我们可以更加高效地利用 Vue 来构建复杂的前端应用程序。 希望本文能够帮助大家更好地理解 Vue 的响应式实现方式,并在面试和实际项目开发中有所帮助。下篇开始从源码层面去分析Vue的响应式原理。
项目附件:点此下载
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。