跳到主要内容
Jason Rong
前端糕手
View All Authors

useSyncExternalStore

· 阅读需 5 分钟
Jason Rong
前端糕手

前言

在一个逻辑相对比较复杂的模块中,尤其是 React 项目中,我不会选择将一些涉及复杂计算的逻辑内联到 Hook 中,而是根据最佳实践方式去运用不同的设计模式去进行封装,尤其是 单例模式

但是我也需要考虑将其数据状态暴露给 React 组件,实现这个需求,有许多种方式

  1. 使用 useState 并借助 useEffect 完成数据订阅,借助监听回调完成数据更新

  2. 直接使用 useRef 实时获取当前最新数据,不过需要注意使用场景,仅仅依靠 useRef 不能触发视图更新。

  3. 使用 useSyncExternalStore完成外部数据快照订阅,部分场景下与第一种方案类似。

而本文主要就是围绕 useSyncExternalStore 展开

概念抽象

  • store 外部数据源,脱离 React 的数据源

  • subscribe 订阅数据更新, store 内部实现的订阅机制,用于其数据更新时,通知其订阅者

  • getSnapshot 获取数据快照, store 内部实现的数据获取机制,用于获取当前数据状态

  • updateAction 更新数据操作, store 内部实现的数据更新机制,用于更新数据状态

  • immutableData 不可突变数据,即不可突变自身的数据。简单来说就是数据若需要更新,需要创建一个全新的数据来替代以视作一个全新的个体,而不是修改自身的属性。

实践

import { useSyncExternalStore } from 'react';

interface StudentState {
name: string;
age: number;
friends: Student[];
}

class Student {
private snapshot: StudentState;
private listeners: (() => void)[] = [];

constructor(name: string, age: number) {
this.subscribe = this.subscribe.bind(this);
this.getSnapshot = this.getSnapshot.bind(this);

this.snapshot = {
name,
age,
friends: []
};
}

get getName() {
return this.snapshot.name;
}

get getAge() {
return this.snapshot.age;
}

get getFriends() {
return this.snapshot.friends;
}

public setName(name: string) {
if (this.snapshot.name === name) {
return;
}

this.snapshot = { ...this.snapshot, name };
this._notify();
}

public setAge(age: number) {
if (this.snapshot.age === age){
return;
}

this.snapshot = { ...this.snapshot, age };
this._notify();
}

public addFriend(newFriend: Student) {
this.snapshot = {
...this.snapshot,
friends: [...this.snapshot.friends, newFriend]
};
this._notify();
}

public removeFriend(student: Student) {
const newFriends = this.snapshot.friends.filter(f => f !== student);

this.snapshot = {
...this.snapshot,
friends: newFriends
};
this._notify();
}

public subscribe(listener: () => void) {
this.listeners.push(listener);

return () => {
this.listeners = this.listeners.filter(l => l !== listener);
};
}

public getSnapshot() {
return this.snapshot;
}

private _notify() {
this.listeners.forEach(listener => listener());
}
}

const studentInstance = new Student('John', 20);

function useStudent() {
const studentState = useSyncExternalStore(studentInstance.subscribe, studentInstance.getSnapshot);

return {
student: studentState,
actions: studentInstance
};
}

updateAction

在示例中,updateAction 就是 studentInstancesetNamesetAgeaddFriendremoveFriend 等方法。在有效更新数据后都触发 notify,通知订阅者数据更新。

subscribe

在示例中,subscribe 就是 studentInstancesubscribe 方法,添加一个监听回调函数,当数据更新时,触发该回调函数。并且返回一个取消订阅的函数以支持订阅者取消订阅。

getSnapshot

在示例中,getSnapshot 就是 studentInstancegetSnapshot 方法,获取 studentInstance 当前数据快照。

核心注意事项

  1. getSnapshot 所返回的数据必须是不可突变的,即每次返回的数据快照必须是全新的,而不是修改自身属性。

在示例中每个 updateAction 都是通过创建全新的数据快照来实现的,而不是修改自身属性。

        this.snapshot = {
...this.snapshot,
friends: [...this.snapshot.friends, newFriend]
};

this.snapshot = { ...this.snapshot, name };

this.snapshot = {
...this.snapshot,
friends: [...this.snapshot.friends, newFriend]
};
  1. store 的数据没有发生更新之前,无论调用多少次 getSnapshot,返回的数据快照都是相同的。且使用 Object.is 判断为相等

要避免出现下面的情况,每一次调用 getSnapshot 返回的数据都是全新的对象,会让 useSyncExternalStore 每次都认为数据发生了变化,从而触发不必要的重新渲染。

public getSnapshot() {
return {
...this.snapshot
};
}

自定义主题色

· 阅读需 5 分钟
Jason Rong
前端糕手

前言

出于个性化或者是商业上的考虑,很多项目在 UI 设计之初就会考虑多种配色方案,有时候是为了配合节假日切换不同的配色方案,有时候是为了迎合不同的用户群体允许用户自定义配置,还有种情况就是迅速换皮套壳出售相关的项目。

无论出于何种考虑,自定义主题色都是一个很好的选择。不同于明确的 浅色/深色模式,自定义主题需要保持足够的灵活性,想要很好地实现这一点,其实就没办法使用固定的配置在编译时完成这一切。

其实大部分主流方案都是通过一开始就明确规划相应的 CSS 变量,然后在编译时通过不同的配置文件来切换不同的变量值,在CSS样式中访问相应的CSS变量取代硬编码固定值从而实现不同的主题色。在若主题色中有相当多的变量,我们相应需要自定义的 CSS 变量也会增多。但如果你的主题色配色方案中,有相当多的颜色是通过某一个关键色推导而来,比如不同比例的混入白色或者黑色,那么我们也可以考虑不全部采用硬编码的方式定义 CSS 变量,而是通过计算派生值的方式来获取其他值。

实践

首先我们先假设主题配色方案中有三个关键色

:root {
--primary-color:rgb(179, 30, 30);
--secondary-color: #00ff00;
--accent-color: #0000ff;
}

假设我们在项目中,对于 primary-color有多个变种值,比如浅一点又或者深一点,如果采用一一定义的方式,那么还要继续定义另外的CSS变量

:root {
--primary-color-light: #ff0000;
--primary-color-dark:rgb(112, 4, 4);
}

其实这样也没啥问题,可是一旦变种色多达十几个,那么定义的CSS变量也会增多,我们还需要考虑为CSS变量注入值时,值是否符合我们一开始设定混入的色值比例。

而这个情况下我们就可以考虑最近新增的color-mix()函数,

:root {
--primary-color-light: color-mix(in srgb, var(--primary-color) 90%, #FFF);
--primary-color-dark: color-mix(in srgb, var(--primary-color) 90%, #000);
}

让计算机自己去计算派生值,而不是我们自己去计算然后定义注入其中。当然,这会损失一些灵活性,毕竟我们没办法完全掌握所有CSS变量的值了。

另外color-mix()目前还有一个绕不开的问题--- 兼容性

color-mix(can i use)

90.82%的支持率属实算不上高,想要推广使用还是需要考虑其他方案。

如果不采用color-mix(),那么剩下就是 PostCSS 还有SassLess 以及 JS。但我们之前有提到,考虑其灵活性,我们没办法在编译时就确定其值,那么在编译时处理CSS样式的方案就无法满足我们的需求,即PostCSSSassLess 都无法满足我们的需求。

那么剩下的就是JS了,

import { colord, extend } from 'colord';
import mixPlugin from 'colord/plugins/mix';
extend([mixPlugin]);

function generateThemeColors(baseColor) {
const colors = {};
const base = colord(baseColor);
colors['primary-50'] = base.mix('#FFFFFF', 0.95).toHex();
colors['primary-100'] = base.mix('#FFFFFF', 0.9).toHex();
colors['primary-200'] = base.mix('#FFFFFF', 0.75).toHex();
colors['primary-300'] = base.mix('#FFFFFF', 0.5).toHex();
colors['primary-400'] = base.mix('#FFFFFF', 0.25).toHex();
colors['primary-500'] = base.toHex(); // 主色本身
colors['primary-600'] = base.mix('#000000', 0.1).toHex();
colors['primary-700'] = base.mix('#000000', 0.25).toHex();
colors['primary-800'] = base.mix('#000000', 0.5).toHex();
colors['primary-900'] = base.mix('#000000', 0.75).toHex();
colors['primary-950'] = base.mix('#000000', 0.9).toHex();
return colors;
}

这样的处理方式就是暴力了点

模式判断,更具阅读性的条件分支判断

· 阅读需 9 分钟
Jason Rong
前端糕手

面临多分支判断,如果面对结构数据只能 if,else。switch 无法很好地解决。同时,大量的 if,else 判断会让其条件判断分支没有那么直观,尤其是复杂数据中多个属性进行层级、通配符判断。本文提出基于函数链式判断进行条件分支管理。