譯者序:本文是 React 核心開發(fā)者躲舌、有 React API 終結(jié)者之稱的 Sebastian Markb?ge 撰寫,闡述了他設(shè)計 React 的初衷。閱讀此文绍绘,你能站在更高的高度思考 React 的過去、現(xiàn)在和未來迟赃。原文地址:https://github.com/reactjs/react-basic
我寫此文是想正式地闡述我心中 React 的心智模型陪拘。目的是解釋為什么我們會這樣設(shè)計 React,同時你也可以根據(jù)這些論點反推出 React纤壁。
不可否認(rèn)左刽,此文中的部分論據(jù)或前提尚存爭議,而且部分示例的設(shè)計可能存在 bug 或疏忽酌媒。這只是正式確定它的最初階段欠痴。如果你有更好的完善它的想法可以隨時提交 pull request。本文不會介紹框架細(xì)節(jié)中的奇技淫巧秒咨,相信這樣能提綱挈領(lǐng)喇辽,讓你看清 React 由簡單到復(fù)雜的設(shè)計過程。
React.js 的真實實現(xiàn)中充滿了具體問題的解決方案拭荤,漸進(jìn)式的解法茵臭,算法優(yōu)化,歷史遺留代碼舅世,debug 工具以及其他一些可以讓它真的具有高可用性的內(nèi)容旦委。這些代碼可能并不穩(wěn)定,因為未來瀏覽器的變化和功能權(quán)重的變化隨時面臨改變雏亚。所以具體的代碼很難徹底解釋清楚缨硝。
我偏向于選擇一種我能完全 hold 住的簡潔的心智模型來作介紹。
變換(Transformation)
設(shè)計 React 的核心前提是認(rèn)為 UI 只是把數(shù)據(jù)通過映射關(guān)系變換成另一種形式的數(shù)據(jù)罢低。同樣的輸入必會有同樣的輸出查辩。這恰好就是純函數(shù)胖笛。
function NameBox(name) {
return { fontWeight: 'bold', labelContent: name };
}
'Sebastian Markb?ge' ->
{ fontWeight: 'bold', labelContent: 'Sebastian Markb?ge' };
抽象(Abstraction)
你不可能僅用一個函數(shù)就能實現(xiàn)復(fù)雜的 UI。重要的是宜岛,你需要把 UI 抽象成多個隱藏內(nèi)部細(xì)節(jié)长踊,又可復(fù)用的函數(shù)。通過在一個函數(shù)中調(diào)用另一個函數(shù)來實現(xiàn)復(fù)雜的 UI萍倡,這就是抽象身弊。
function FancyUserBox(user) {
return {
borderStyle: '1px solid blue',
childContent: [
'Name: ',
NameBox(user.firstName + ' ' + user.lastName)
]
};
}
{ firstName: 'Sebastian', lastName: 'Markb?ge' } ->
{
borderStyle: '1px solid blue',
childContent: [
'Name: ',
{ fontWeight: 'bold', labelContent: 'Sebastian Markb?ge' }
]
};
組合(Composition)
為了真正達(dá)到重用的特性,只重用葉子然后每次都為他們創(chuàng)建一個新的容器是不夠的列敲。你還需要可以包含其他抽象的容器再次進(jìn)行組合阱佛。我理解的“組合”就是將兩個或者多個不同的抽象合并為一個。
function FancyBox(children) {
return {
borderStyle: '1px solid blue',
children: children
};
}
function UserBox(user) {
return FancyBox([
'Name: ',
NameBox(user.firstName + ' ' + user.lastName)
]);
}
狀態(tài)(State)
UI 不單單是對服務(wù)器端或業(yè)務(wù)邏輯狀態(tài)的復(fù)制戴而。實際上還有很多狀態(tài)是針對具體的渲染目標(biāo)凑术。舉個例子,舉個例子所意,在一個 text field 中打字淮逊。它不一定要復(fù)制到其他頁面或者你的手機設(shè)備。滾動位置這個狀態(tài)是一個典型的你幾乎不會復(fù)制到多個渲染目標(biāo)的扶踊。
我們傾向于使用不可變的數(shù)據(jù)模型壮莹。我們把可以改變 state 的函數(shù)串聯(lián)起來作為原點放置在頂層。
function FancyNameBox(user, likes, onClick) {
return FancyBox([
'Name: ', NameBox(user.firstName + ' ' + user.lastName),
'Likes: ', LikeBox(likes),
LikeButton(onClick)
]);
}
// 實現(xiàn)細(xì)節(jié)
var likes = 0;
function addOneMoreLike() {
likes++;
rerender();
}
// 初始化
FancyNameBox(
{ firstName: 'Sebastian', lastName: 'Markb?ge' },
likes,
addOneMoreLike
);
注意:本例更新狀態(tài)時會帶來副作用(addOneMoreLike 函數(shù)中)姻檀。我實際的想法是當(dāng)一個“update”傳入時我們返回下一個版本的狀態(tài),但那樣會比較復(fù)雜涝滴。此示例待更新
Memoization
對于純函數(shù)绣版,使用相同的參數(shù)一次次調(diào)用未免太浪費資源。我們可以創(chuàng)建一個函數(shù)的 memorized 版本歼疮,用來追蹤最后一個參數(shù)和結(jié)果杂抽。這樣如果我們繼續(xù)使用同樣的值,就不需要反復(fù)執(zhí)行它了韩脏。
function memoize(fn) {
var cachedArg;
var cachedResult;
return function(arg) {
if (cachedArg === arg) {
return cachedResult;
}
cachedArg = arg;
cachedResult = fn(arg);
return cachedResult;
};
}
var MemoizedNameBox = memoize(NameBox);
function NameAndAgeBox(user, currentTime) {
return FancyBox([
'Name: ',
MemoizedNameBox(user.firstName + ' ' + user.lastName),
'Age in milliseconds: ',
currentTime - user.dateOfBirth
]);
}
列表(Lists)
大部分 UI 都是展示列表數(shù)據(jù)中不同 item 的列表結(jié)構(gòu)缩麸。這是一個天然的層級。
為了管理列表中的每一個 item 的 state 赡矢,我們可以創(chuàng)造一個 Map 容納具體 item 的 state杭朱。
function UserList(users, likesPerUser, updateUserLikes) {
return users.map(user => FancyNameBox(
user,
likesPerUser.get(user.id),
() => updateUserLikes(user.id, likesPerUser.get(user.id) + 1)
));
}
var likesPerUser = new Map();
function updateUserLikes(id, likeCount) {
likesPerUser.set(id, likeCount);
rerender();
}
UserList(data.users, likesPerUser, updateUserLikes);
注意:現(xiàn)在我們向 FancyNameBox 傳了多個不同的參數(shù)。這打破了我們的 memoization 因為我們每次只能存儲一個值吹散。更多相關(guān)內(nèi)容在下面弧械。
連續(xù)性(Continuations)
不幸的是,自從 UI 中有太多的列表空民,明確的管理就需要大量的重復(fù)性樣板代碼刃唐。
我們可以通過推遲一些函數(shù)的執(zhí)行,進(jìn)而把一些模板移出業(yè)務(wù)邏輯。比如画饥,使用“柯里化”(JavaScript 中的 bind
)衔瓮。然后我們可以從核心的函數(shù)外面?zhèn)鬟f state,這樣就沒有樣板代碼了抖甘。
下面這樣并沒有減少樣板代碼热鞍,但至少把它從關(guān)鍵業(yè)務(wù)邏輯中剝離。
function FancyUserList(users) {
return FancyBox(
UserList.bind(null, users)
);
}
const box = FancyUserList(data.users);
const resolvedChildren = box.children(likesPerUser, updateUserLikes);
const resolvedBox = {
...box,
children: resolvedChildren
};
State Map
之前我們知道可以使用組合避免重復(fù)執(zhí)行相同的東西這樣一種重復(fù)模式单山。我們可以把執(zhí)行和傳遞 state 邏輯挪動到被復(fù)用很多的低層級的函數(shù)中去碍现。
function FancyBoxWithState(
children,
stateMap,
updateState
) {
return FancyBox(
children.map(child => child.continuation(
stateMap.get(child.key),
updateState
))
);
}
function UserList(users) {
return users.map(user => {
continuation: FancyNameBox.bind(null, user),
key: user.id
});
}
function FancyUserList(users) {
return FancyBoxWithState.bind(null,
UserList(users)
);
}
const continuation = FancyUserList(data.users);
continuation(likesPerUser, updateUserLikes);
Memoization Map
一旦我們想在一個 memoization 列表中 memoize 多個 item 就會變得很困難。因為你需要制定復(fù)雜的緩存算法來平衡調(diào)用頻率和內(nèi)存占有率米奸。
還好 UI 在同一個位置會相對的穩(wěn)定昼接。相同的位置一般每次都會接受相同的參數(shù)。這樣以來悴晰,使用一個集合來做 memoization 是一個非常好用的策略慢睡。
我們可以用對待 state 同樣的方式,在組合的函數(shù)中傳遞一個 memoization 緩存铡溪。
function memoize(fn) {
return function(arg, memoizationCache) {
if (memoizationCache.arg === arg) {
return memoizationCache.result;
}
const result = fn(arg);
memoizationCache.arg = arg;
memoizationCache.result = result;
return result;
};
}
function FancyBoxWithState(
children,
stateMap,
updateState,
memoizationCache
) {
return FancyBox(
children.map(child => child.continuation(
stateMap.get(child.key),
updateState,
memoizationCache.get(child.key)
))
);
}
const MemoizedFancyNameBox = memoize(FancyNameBox);
代數(shù)效應(yīng)(Algebraic Effects)
多層抽象需要共享瑣碎數(shù)據(jù)時漂辐,一層層傳遞數(shù)據(jù)非常麻煩。如果能有一種方式可以在多層抽象中快捷地傳遞數(shù)據(jù)棕硫,同時又不需要牽涉到中間層級髓涯,那該有多好。React 中我們把它叫做“context”哈扮。
有時候數(shù)據(jù)依賴并不是嚴(yán)格按照抽象樹自上而下進(jìn)行纬纪。舉個例子,在布局算法中滑肉,你需要在實現(xiàn)他們的位置之前了解子節(jié)點的大小包各。
現(xiàn)在,這個例子有一點超綱靶庙。我會使用 代數(shù)效應(yīng) 這個由我發(fā)起的 ECMAScript 新特性提議问畅。如果你對函數(shù)式編程很熟悉,它們 在避免由 monad 強制引入的儀式一樣的編碼六荒。
function ThemeBorderColorRequest() { }
function FancyBox(children) {
const color = raise new ThemeBorderColorRequest();
return {
borderWidth: '1px',
borderColor: color,
children: children
};
}
function BlueTheme(children) {
return try {
children();
} catch effect ThemeBorderColorRequest -> [, continuation] {
continuation('blue');
}
}
function App(data) {
return BlueTheme(
FancyUserList.bind(null, data.users)
);
}