事情的起因是這樣的,在使用react開發(fā)項(xiàng)目的時(shí)候,郵箱規(guī)則列表頁(yè)面有一個(gè)Switch按鈕,點(diǎn)擊按鈕可以切換規(guī)則的啟用或者禁用狀態(tài),剛開始的做法是:用戶點(diǎn)擊了按鈕之后先改變IsEnabled的狀態(tài),然后調(diào)用update接口,更新服務(wù)器端的接口,使用await等到update接口調(diào)用成功之后,再去調(diào)用獲取郵箱規(guī)則列表的接口,更新列表狀態(tài):
const handleEdit = async (flag: boolean, item: MailRuleItem) => {
// 1. 創(chuàng)建更新后的對(duì)象(使用函數(shù)式更新確保最新狀態(tài))
const updateItem = {
...item,
IsEnabled: flag ? 1 : 0,
Actions: {
...item.Actions,
MoveToFolder: item.Actions.MoveToFolder?.UniqueId,
Delete: item.Actions.Delete ? '1' : '0',
MarkAsRead: item.Actions.MarkAsRead ? '1' : '0',
CopyToFolder: item.Actions.CopyToFolder?.UniqueId,
},
Conditions: { ...item.Conditions }
};
await updatemailrule(updateItem);//更新的接口
onReload();//刷新列表
};
遇到的問(wèn)題是:update接口響應(yīng)比較慢,導(dǎo)致點(diǎn)擊完按鈕之后沒(méi)有反應(yīng),好像是按鈕沒(méi)被點(diǎn)擊一樣,用戶體驗(yàn)不好
需求:點(diǎn)擊完之后,立馬更新按鈕的狀態(tài),如果接口發(fā)生錯(cuò)誤,需要恢復(fù)按鈕的狀態(tài)
這里通過(guò)各種搜索,了解到了樂(lè)觀更新,這個(gè)詞聽著很陌生,但其實(shí)很簡(jiǎn)單,原理就是點(diǎn)擊完按鈕之后,立馬把被點(diǎn)擊的數(shù)據(jù)的狀態(tài)更改,給人的感覺(jué)是接口已經(jīng)調(diào)用成功了,但實(shí)際僅僅是前端意義的更改,關(guān)于樂(lè)觀更新解釋如下:
樂(lè)觀更新是一種提升用戶體驗(yàn)的前端優(yōu)化策略,其核心思想是:在等待服務(wù)器響應(yīng)前,先假設(shè)操作會(huì)成功,立即更新本地UI狀態(tài)。如果最終請(qǐng)求失敗,再回滾到之前的狀態(tài)。
核心特點(diǎn)
-
即時(shí)反饋
- 用戶操作后UI立即變化,無(wú)需等待網(wǎng)絡(luò)延遲
- 典型場(chǎng)景:點(diǎn)贊、開關(guān)切換、列表項(xiàng)操作
-
異步補(bǔ)償
- 請(qǐng)求失敗時(shí)自動(dòng)回滾狀態(tài)
- 通常會(huì)配合錯(cuò)誤提示
-
狀態(tài)可逆
- 必須保留操作前的狀態(tài)副本
- 回滾時(shí)需要準(zhǔn)確恢復(fù)上下文
實(shí)現(xiàn)原理(三步流程)
用戶->>UI: 觸發(fā)操作(如點(diǎn)擊開關(guān))
UI->>UI: 立即更新本地狀態(tài)(樂(lè)觀更新)
UI->>服務(wù)端: 發(fā)送異步請(qǐng)求
alt 請(qǐng)求成功
服務(wù)端-->>UI: 返回200
UI->>UI: 保持更新后狀態(tài)(可選二次確認(rèn))
else 請(qǐng)求失敗
服務(wù)端-->>UI: 返回錯(cuò)誤
UI->>UI: 自動(dòng)回滾狀態(tài)+錯(cuò)誤提示
end
典型實(shí)現(xiàn)代碼(React示例)
const [items, setItems] = useState(data);
const handleToggle = async (id) => {
// 1. 保存原始狀態(tài)
const originalItems = [...items];
// 2. 樂(lè)觀更新:立即切換UI狀態(tài)
setItems(prev => prev.map(item =>
item.id === id ? { ...item, active: !item.active } : item
));
try {
// 3. 發(fā)送真實(shí)請(qǐng)求
await api.toggleItem(id);
} catch (error) {
// 4. 失敗時(shí)回滾
setItems(originalItems);
toast.error("更新失敗");
}
};
適用場(chǎng)景 vs 不適用場(chǎng)景
| 適合場(chǎng)景 | 不適合場(chǎng)景 |
|---|---|
| ? 高頻交互操作(點(diǎn)贊/收藏) | ? 金融交易等關(guān)鍵操作 |
| ? 延遲敏感型功能(開關(guān)切換) | ? 依賴服務(wù)端復(fù)雜計(jì)算的場(chǎng)景 |
| ? 冪等性操作(可重復(fù)執(zhí)行) | ? 非冪等性操作 |
最后使用樂(lè)觀更新問(wèn)題得以解決:
const handleEdit = async (flag: boolean, item: MailRuleItem) => {
// 1. 創(chuàng)建更新后的對(duì)象(使用函數(shù)式更新確保最新狀態(tài))
const updateItem = {
...item,
IsEnabled: flag ? 1 : 0,
Actions: {
...item.Actions,
MoveToFolder: item.Actions.MoveToFolder?.UniqueId,
Delete: item.Actions.Delete ? '1' : '0',
MarkAsRead: item.Actions.MarkAsRead ? '1' : '0',
CopyToFolder: item.Actions.CopyToFolder?.UniqueId,
},
Conditions: { ...item.Conditions }
};
// 2. 樂(lè)觀更新 - 使用函數(shù)式更新確?;谧钚聽顟B(tài)
setRules(prevRules => {
// 返回更新后的狀態(tài)
return prevRules.map(rule => {
if (rule.Id === item.Id) {
return updateItem;
} else {
return rule;
}
}
);
});
try {
// 3. 發(fā)送異步請(qǐng)求
await updatemailrule(updateItem);
// 4. 請(qǐng)求成功后,可以調(diào)用onReload獲取最新數(shù)據(jù)
// 或者直接使用返回的更新后數(shù)據(jù)(更推薦)
onReload();
} catch (error) {
// 5. 請(qǐng)求失敗時(shí),回滾到之前保存的快照
setRules(prevRules => {
// 找到當(dāng)前項(xiàng)的原始狀態(tài)
const originalItem = prevRules.find(r => r.id === item.id);
if (!originalItem) return prevRules;
return prevRules.map(rule => {
if (rule.id === item.id) {
return originalItem;
} else {
return rule;
}
}
);
});
console.error("更新失敗:", error);
}
};