“反应性”是指系统对数据变化的反应。反应性有很多种,但在本文中,反应性是指 当数据发生变化时,你采取的行动。
响应式模式是 Web 开发的核?心
由于浏览器是一个完全异步的环境,因此我们在网站和 Web 应用中使用 JavaScript 处理大量事务。我们必须响应用户输入、与服务器通信、记录、执行等。所有这些任务都涉及 UI 更新、Ajax 请求、浏览器 URL 和导航更改,因此级联数据更改成为 Web 开发的核??心方面。
作为一个行业,我们将反应性与框架联系在一起,但您可以通过在纯 JavaScript 中实现反应性学到很多东西。我们可以混合搭配这些模式,将行为与数据变化联系起来。
无论您使用什么工具或框架,学习纯 JavaScript 的核心模式都会减少代码量并提高 Web 应用程序的性能。
我喜欢学习模式,因为它们适用于任何语言和系统。模式可以组合起来解决您应用的确切需求,通常可以提高代码的性能和可维护性。
希望您能学到新的模式来添加到您的工具箱中,无论您使用什么框架和库!
PubSub 模式(发布订阅者)
PubSub 是响应式的最基础模式之一。触发事件 publish() 允许任何人监听该事件 subscribe() ,并在与触发该事件无关的情况下做任何他们想做的事情。
const pubSub = {
events: {},
subscribe(event, callback) {
if (!this.events[event]) this.events[event] = [];
this.events[event].push(callback);
},
publish(event, data) {
if (this.events[event]) this.events[event].forEach(callback => callback(data));
}
};
pubSub.subscribe('update', data => console.log(data));
pubSub.publish('update', 'Some update'); // Some update
JavaScript
请注意,发布者 不知道 正在收听什么,因此无法通过这种简单的实现来取消订阅或进行清理。
自定义事件:PubSub 的本机浏览器 API
浏览器具有用于触发和订阅自定义事件的 JavaScript API。它允许您使用 随自定义事件一起发送数据 dispatchEvent。
const pizzaEvent = new CustomEvent("pizzaDelivery", {
detail: {
name: "supreme",
},
});
window.addEventListener("pizzaDelivery", (e) => console.log(e.detail.name));
window.dispatchEvent(pizzaEvent);
JavaScript
您可以将这些自定义事件的范围限定为任何 DOM 节点。在代码示例中,我们使用全局 window 对象(也称为全局事件总线),因此我们应用中的任何内容都可以监听事件数据并执行某些操作。
<div id="pizza-store"></div>
const pizzaEvent = new CustomEvent("pizzaDelivery", {
detail: {
name: "supreme",
},
});
const pizzaStore = document.querySelector('#pizza-store');
pizzaStore.addEventListener("pizzaDelivery", (e) => console.log(e.detail.name));
pizzaStore.dispatchEvent(pizzaEvent);
类实例自定义事件:子类化 EventTarget
我们可以将 EventTarget 子类化,以便在类实例上发送事件,以供我们的应用程序绑定:
class PizzaStore extends EventTarget {
constructor() {
super();
}
addPizza(flavor) {
// fire event directly on the class
this.dispatchEvent(new CustomEvent("pizzaAdded", {
detail: {
pizza: flavor,
},
}));
}
}
const Pizzas = new PizzaStore();
Pizzas.addEventListener("pizzaAdded", (e) => console.log('Added Pizza:', e.detail.pizza));
Pizzas.addPizza("supreme");
JavaScript
很酷的一点是,您的事件不会在窗口上全局触发。您可以直接在类上触发事件;应用中的任何内容都可以将事件侦听器直接连接到该类。
观察者模式
观察者模式与 PubSub 模式的基本前提相同。它允许您将行为“订阅”到主题。当主题触发该 notify 方法时,它会通知所有订阅的内容。
class Subject {
constructor() {
this.observers = [];
}
addObserver(observer) {
this.observers.push(observer);
}
removeObserver(observer) {
const index = this.observers.indexOf(observer);
if (index > -1) {
this.observers.splice(index, 1);
}
}
notify(data) {
this.observers.forEach(observer => observer.update(data));
}
}
class Observer {
update(data) {
console.log(data);
}
}
const subject = new Subject();
const observer = new Observer();
subject.addObserver(observer);
subject.notify('Everyone gets pizzas!');
JavaScript
它与 PubSub 的主要区别在于,Subject 知道其观察者并可以将其移除。它们并不 像 PubSub 中那样完全解耦。
具有代理的反应性对象属性
JavaScript 中的代理可以作为设置或获取对象属性后执行反应性的基础。
const handler = {
get: function(target, property) {
console.log(`Getting property ${property}`);
return target[property];
},
set: function(target, property, value) {
console.log(`Setting property ${property} to ${value}`);
target[property] = value;
return true; // indicates that the setting has been done successfully
}
};
const pizza = { name: 'Margherita', toppings: ['tomato sauce', 'mozzarella'] };
const proxiedPizza = new Proxy(pizza, handler);
console.log(proxiedPizza.name); // Outputs "Getting property name" and "Margherita"
proxiedPizza.name = 'Pepperoni'; // Outputs "Setting property name to Pepperoni"
JavaScript
当您访问或修改 上的属性时 proxiedPizza,它会将一条消息记录到控制台。但您可以想象将任何功能连接到对象的属性访问。
反应性单个属性:Object.defineProperty
您可以使用 对特定属性执行相同的操作 Object.defineProperty。您可以为属性定义 getter 和 setter,并在访问或修改属性时运行代码。
const pizza = {
_name: 'Margherita', // Internal property
};
Object.defineProperty(pizza, 'name', {
get: function() {
console.log(`Getting property name`);
return this._name;
},
set: function(value) {
console.log(`Setting property name to ${value}`);
this._name = value;
}
});
// Example usage:
console.log(pizza.name); // Outputs "Getting property name" and "Margherita"
pizza.name = 'Pepperoni'; // Outputs "Setting property name to Pepperoni"
JavaScript
这里,我们使用 Object.defineProperty getter 和 setter 来为 pizza 对象的 name 属性定义一个 getter 和 setter。实际值存储在一个私有 _name 属性中,getter 和 setter 可以在将消息记录到控制台时访问该值。
Object.defineProperty 比使用 更冗长 Proxy,特别是当您想将相同的行为应用于许多属性时。但它是一种为各个属性定义自定义行为的强大而灵活的方法。
使用 Promises 实现异步响应数据
让我们异步使用观察者!这样我们就可以更新数据并让多个观察者异步运行。
class AsyncData {
constructor(initialData) {
this.data = initialData;
this.subscribers = [];
}
// Subscribe to changes in the data
subscribe(callback) {
if (typeof callback !== 'function') {
throw new Error('Callback must be a function');
}
this.subscribers.push(callback);
}
// Update the data and wait for all updates to complete
async set(key, value) {
this.data[key] = value;
// Call the subscribed function and wait for it to resolve
const updates = this.subscribers.map(async (callback) => {
await callback(key, value);
});
await Promise.allSettled(updates);
}
}
这是一个包装数据对象并在数据改变时触发更新的类。
等待我们的异步观察者
假设我们要等到所有异步反应数据的订阅都处理完毕:
const data = new AsyncData({ pizza: 'Pepperoni' });
data.subscribe(async (key, value) => {
await new Promise(resolve => setTimeout(resolve, 500));
console.log(`Updated UI for ${key}: ${value}`);
});
data.subscribe(async (key, value) => {
await new Promise(resolve => setTimeout(resolve, 1000));
console.log(`Logged change for ${key}: ${value}`);
});
// function to update data and wait for all updates to complete
async function updateData() {
await data.set('pizza', 'Supreme'); // This will call the subscribed functions and wait for their promises to resolve
console.log('All updates complete.');
}
updateData();
JavaScript
我们的 updateData 函数现在是异步的,因此我们可以等待所有订阅的函数解析后再继续执行程序。这种模式可以让异步响应变得更简单一些。
反应系统
许多更复杂的响应式系统都是流行库和框架的基础:React 中的钩子、Solid 中的信号、Rx.js 中的可观察对象等等。它们通常具有相同的基本原理,即当数据发生变化时,重新渲染组件或相关的 DOM 片段。
可观察对象(Rx.js 模式)
尽管可观察对象和观察者模式几乎是同一个词,但事实并不相同,哈哈。
Observables 允许您定义一种随时间产生一系列值的方法。这是一个简单的 Observable 原语,它提供了一种向订阅者发出一系列值的方法,允许订阅者在产生这些值时做出反应。
class Observable {
constructor(producer) {
this.producer = producer;
}
// Method to allow a subscriber to subscribe to the observable
subscribe(observer) {
// Ensure the observer has the necessary functions
if (typeof observer !== 'object' || observer === null) {
throw new Error('Observer must be an object with next, error, and complete methods');
}
if (typeof observer.next !== 'function') {
throw new Error('Observer must have a next method');
}
if (typeof observer.error !== 'function') {
throw new Error('Observer must have an error method');
}
if (typeof observer.complete !== 'function') {
throw new Error('Observer must have a complete method');
}
const unsubscribe = this.producer(observer);
// Return an object with an unsubscribe method
return {
unsubscribe: () => {
if (unsubscribe && typeof unsubscribe === 'function') {
unsubscribe();
}
},
};
}
}
JavaScript
使用方法如下:
// Create a new observable that emits three values and then completes
const observable = new Observable(observer => {
observer.next(1);
observer.next(2);
observer.next(3);
observer.complete();
// Optional: Return a function to handle any cleanup if the observer unsubscribes
return () => {
console.log('Observer unsubscribed');
};
});
// Define an observer with next, error, and complete methods
const observer = {
next: value => console.log('Received value:', value),
error: err => console.log('Error:', err),
complete: () => console.log('Completed'),
};
// Subscribe to the observable
const subscription = observable.subscribe(observer);
// Optionally, you can later unsubscribe to stop receiving values
subscription.unsubscribe();
JavaScript
Observable 的关键组件是 next() 向观察者发送数据的方法。Observable complete() 流关闭时的方法。以及 error() 发生错误时的方法。此外,还必须有一种方法来 subscribe() 监听更改并 unsubscribe() 停止从流中接收数据。
使用此模式的最流行的库是 Rx.js 和 MobX。
“信号” (SolidJS 模式)
const context = [];
export function createSignal(value) {
const subscriptions = new Set();
const read = () => {
const observer = context[context.length - 1]
if (observer) subscriptions.add(observer);
return value;
}
const write = (newValue) => {
value = newValue;
for (const observer of subscriptions) {
observer.execute()
}
}
return [read, write];
}
export function createEffect(fn) {
const effect = {
execute() {
context.push(effect);
fn();
context.pop();
}
}
effect.execute();
}
使用反应系统:
import { createSignal, createEffect } from "./reactive";
const [count, setCount] = createSignal(0);
createEffect(() => {
console.log(count());
}); // 0
setCount(10); // 10
UI 的反应式渲染
以下是一些从 DOM 和 CSS 写入和读取的模式。
将数据渲染为 HTML 字符串文字
这是一个根据数据渲染一些披萨 UI 的简单示例。
function PizzaRecipe(pizza) {
return `<div class="pizza-recipe">
<h1>${pizza.name}</h1>
<h3>Toppings: ${pizza.toppings.join(', ')}</h3>
<p>${pizza.description}</p>
</div>`;
}
function PizzaRecipeList(pizzas) {
return `<div class="pizza-recipe-list">
${pizzas.map(PizzaRecipe).join('')}
</div>`;
}
var allPizzas = [
{
name: 'Margherita',
toppings: ['tomato sauce', 'mozzarella'],
description: 'A classic pizza with fresh ingredients.'
},
{
name: 'Pepperoni',
toppings: ['tomato sauce', 'mozzarella', 'pepperoni'],
description: 'A favorite among many, topped with delicious pepperoni.'
},
{
name: 'Veggie Supreme',
toppings: ['tomato sauce', 'mozzarella', 'bell peppers', 'onions', 'mushrooms'],
description: 'A delightful vegetable-packed pizza.'
}
];
// Render the list of pizzas
function renderPizzas() {
document.querySelector('body').innerHTML = PizzaRecipeList(allPizzas);
}
renderPizzas(); // Initial render
// Example of changing data and re-rendering
function addPizza() {
allPizzas.push({
name: 'Hawaiian',
toppings: ['tomato sauce', 'mozzarella', 'ham', 'pineapple'],
description: 'A tropical twist with ham and pineapple.'
});
renderPizzas(); // Re-render the updated list
}
// Call this function to add a new pizza and re-render the list
addPizza();
JavaScript
addPizza 演示如何通过向列表添加新的披萨食谱来更改数据,然后重新呈现列表以反映更改。
这种方法的主要缺点是每次渲染时都会毁掉整个 DOM。您可以使用 lit-html之类的库 (lit-html 使用指南)更智能地仅更新发生变化的 DOM 位。我们在 Frontend Masters 上使用几个高度动态的组件(例如我们的数据网格组件)来实现这一点。
反应式 DOM 属性:MutationObserver
使 DOM 具有响应性的一种方法是添加和删除属性。我们可以使用 API 监听属性的变化 MutationObserver 。
const mutationCallback = (mutationsList) => {
for (const mutation of mutationsList) {
if (
mutation.type !== "attributes" ||
mutation.attributeName !== "pizza-type"
) return;
console.log('old:', mutation.oldValue)
console.log('new:', mutation.target.getAttribute("pizza-type"))
}
}
const observer = new MutationObserver(mutationCallback);
observer.observe(document.getElementById('pizza-store'), { attributes: true });
JavaScript
现在我们可以从程序中的任何位置更新披萨类型属性,并且元素本身可以具有附加更新该属性的行为!
Web 组件中的反应性属性
使用 Web 组件,有一种本机的方式来监听和响应属性更新。
class PizzaStoreComponent extends HTMLElement {
static get observedAttributes() {
return ['pizza-type'];
}
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `<p>${this.getAttribute('pizza-type') || 'Default Content'}</p>`;
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'my-attribute') {
this.shadowRoot.querySelector('div').textContent = newValue;
console.log(`Attribute ${name} changed from ${oldValue} to ${newValue}`);
}
}
}
customElements.define('pizza-store', PizzaStoreComponent);
JavaScript
<pizza-store pizza-type="Supreme"></pizza-store>
document.querySelector('pizza-store').setAttribute('pizza-type', 'BBQ Chicken!');
JavaScript
这个有点简单,但是我们必须使用 Web 组件才能使用这个 API。
响应式滚动:IntersectionObserver
我们可以将响应性连接到滚动到视图中的 DOM 元素。我已将其用于营销页面上的流畅动画。
var pizzaStoreElement = document.getElementById('pizza-store');
var observer = new IntersectionObserver(function(entries, observer) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
entry.target.classList.add('animate-in');
} else {
entry.target.classList.remove('animate-in');
}
});
});
observer.observe(pizzaStoreElement);
动画和游戏循环:requestAnimationFrame
在进行游戏开发、Canvas、WebGL 或那些疯狂的营销网站时,动画通常需要写入缓冲区,然后在渲染线程可用时在给定循环中写入结果。我们使用 来实现这一点 requestAnimationFrame。
function drawStuff() {
// This is where you'd do game or animation rendering logic
}
// function to handle the animation
function animate() {
drawStuff();
requestAnimationFrame(animate); // Continually calls animate when the next render frame is available
}
// Start the animation
animate();
JavaScript
这是游戏以及任何涉及实时渲染的内容在帧可用时渲染场景的方法。
响应式动画:Web 动画 API
您还可以使用 Web Animations API 创建响应式动画。在这里,我们将使用动画 API 为元素的比例、位置和颜色设置动画。
const el = document.getElementById('animatedElement');
// Define the animation properties
const animation = el.animate([
// Keyframes
{ transform: 'scale(1)', backgroundColor: 'blue', left: '50px', top: '50px' },
{ transform: 'scale(1.5)', backgroundColor: 'red', left: '200px', top: '200px' }
], {
// Timing options
duration: 1000,
fill: 'forwards'
});
// Set the animation's playback rate to 0 to pause it
animation.playbackRate = 0;
// Add a click event listener to the element
el.addEventListener('click', () => {
// If the animation is paused, play it
if (animation.playbackRate === 0) {
animation.playbackRate = 1;
} else {
// If the animation is playing, reverse it
animation.reverse();
}
});
JavaScript
其响应性在??于,动画可以在发生交互时(在本例中为反转方向)相对于其所在位置播放。标准 CSS 动画和过渡不相对于其当前位置。
响应式 CSS:自定义属性和 calc
最后,我们可以通过组合自定义属性和来编写反应式 CSS calc。
barElement.style.setProperty('--percentage', newPercentage);
JavaScript
在 JavaScript 中,您可以设置自定义属性值。
.bar {
width: calc(100% / 4 - 10px);
height: calc(var(--percentage) * 1%);
background-color: blue;
margin-right: 10px;
position: relative;
}
CSS
在 CSS 中,我们现在可以根据该百分比进行计算。我们可以直接在 CSS 中添加计算,让 CSS 完成其样式设置工作,而无需将所有渲染逻辑保留在 JavaScript 中,这真是太酷了。
仅供参考:如果您想创建相对于当前值的更改,您也可以读取这些属性。
getComputedStyle(barElement).getPropertyValue('--percentage');