譯者序:本文是 React 核心開(kāi)發(fā)者、有 React API 終結(jié)者之稱(chēng)的 Sebastian Markb?ge 撰寫(xiě),闡述了他設(shè)計(jì) React 的初衷。閱讀此文,你能站在更高的高度思考 React 的過(guò)去、現(xiàn)在和未來(lái)。原文地址:https://github.com/reactjs/react-basic
我寫(xiě)此文是想正式地闡述我心中 React 的心智模型。目的是解釋為什么我們會(huì)這樣設(shè)計(jì) React,同時(shí)你也可以根據(jù)這些論點(diǎn)反推出 React。
不可否認(rèn),此文中的部分論據(jù)或前提尚存爭(zhēng)議,而且部分示例的設(shè)計(jì)可能存在 bug 或疏忽。這只是正式確定它的最初階段。如果你有更好的完善它的想法可以隨時(shí)提交 pull request。本文不會(huì)介紹框架細(xì)節(jié)中的奇技淫巧,相信這樣能提綱挈領(lǐng),讓你看清 React 由簡(jiǎn)單到復(fù)雜的設(shè)計(jì)過(guò)程。
React.js 的真實(shí)實(shí)現(xiàn)中充滿(mǎn)了具體問(wèn)題的解決方案,漸進(jìn)式的解法,算法優(yōu)化,歷史遺留代碼,debug 工具以及其他一些可以讓它真的具有高可用性的內(nèi)容。這些代碼可能并不穩(wěn)定,因?yàn)槲磥?lái)瀏覽器的變化和功能權(quán)重的變化隨時(shí)面臨改變。所以具體的代碼很難徹底解釋清楚。
我偏向于選擇一種我能完全 hold 住的簡(jiǎn)潔的心智模型來(lái)作介紹。
變換(Transformation)
設(shè)計(jì) React 的核心前提是認(rèn)為 UI 只是把數(shù)據(jù)通過(guò)映射關(guān)系變換成另一種形式的數(shù)據(jù)。同樣的輸入必會(huì)有同樣的輸出。這恰好就是純函數(shù)。
function NameBox(name) {
return { fontWeight: 'bold', labelContent: name };
}
'Sebastian Markb?ge' ->
{ fontWeight: 'bold', labelContent: 'Sebastian Markb?ge' };
抽象(Abstraction)
你不可能僅用一個(gè)函數(shù)就能實(shí)現(xiàn)復(fù)雜的 UI。重要的是,你需要把 UI 抽象成多個(gè)隱藏內(nèi)部細(xì)節(jié),又可復(fù)用的函數(shù)。通過(guò)在一個(gè)函數(shù)中調(diào)用另一個(gè)函數(shù)來(lái)實(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)建一個(gè)新的容器是不夠的。你還需要可以包含其他抽象的容器再次進(jìn)行組合。我理解的“組合”就是將兩個(gè)或者多個(gè)不同的抽象合并為一個(gè)。
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 不單單是對(duì)服務(wù)器端或業(yè)務(wù)邏輯狀態(tài)的復(fù)制。實(shí)際上還有很多狀態(tài)是針對(duì)具體的渲染目標(biāo)。舉個(gè)例子,舉個(gè)例子,在一個(gè) text field 中打字。它不一定要復(fù)制到其他頁(yè)面或者你的手機(jī)設(shè)備。滾動(dòng)位置這個(gè)狀態(tài)是一個(gè)典型的你幾乎不會(huì)復(fù)制到多個(gè)渲染目標(biāo)的。
我們傾向于使用不可變的數(shù)據(jù)模型。我們把可以改變 state 的函數(shù)串聯(lián)起來(lái)作為原點(diǎn)放置在頂層。
function FancyNameBox(user, likes, onClick) {
return FancyBox([
'Name: ', NameBox(user.firstName + ' ' + user.lastName),
'Likes: ', LikeBox(likes),
LikeButton(onClick)
]);
}
// 實(shí)現(xiàn)細(xì)節(jié)
var likes = 0;
function addOneMoreLike() {
likes++;
rerender();
}
// 初始化
FancyNameBox(
{ firstName: 'Sebastian', lastName: 'Markb?ge' },
likes,
addOneMoreLike
);
注意:本例更新?tīng)顟B(tài)時(shí)會(huì)帶來(lái)副作用(addOneMoreLike 函數(shù)中)。我實(shí)際的想法是當(dāng)一個(gè)“update”傳入時(shí)我們返回下一個(gè)版本的狀態(tài),但那樣會(huì)比較復(fù)雜。此示例待更新
Memoization
對(duì)于純函數(shù),使用相同的參數(shù)一次次調(diào)用未免太浪費(fèi)資源。我們可以創(chuàng)建一個(gè)函數(shù)的 memorized 版本,用來(lái)追蹤最后一個(gè)參數(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)。這是一個(gè)天然的層級(jí)。
為了管理列表中的每一個(gè) item 的 state ,我們可以創(chuàng)造一個(gè) 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 傳了多個(gè)不同的參數(shù)。這打破了我們的 memoization 因?yàn)槲覀兠看沃荒艽鎯?chǔ)一個(gè)值。更多相關(guān)內(nèi)容在下面。
連續(xù)性(Continuations)
不幸的是,自從 UI 中有太多的列表,明確的管理就需要大量的重復(fù)性樣板代碼。
我們可以通過(guò)推遲一些函數(shù)的執(zhí)行,進(jìn)而把一些模板移出業(yè)務(wù)邏輯。比如,使用“柯里化”(JavaScript 中的 bind)。然后我們可以從核心的函數(shù)外面?zhèn)鬟f state,這樣就沒(méi)有樣板代碼了。
下面這樣并沒(méi)有減少樣板代碼,但至少把它從關(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 邏輯挪動(dòng)到被復(fù)用很多的低層級(jí)的函數(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
一旦我們想在一個(gè) memoization 列表中 memoize 多個(gè) item 就會(huì)變得很困難。因?yàn)槟阈枰贫◤?fù)雜的緩存算法來(lái)平衡調(diào)用頻率和內(nèi)存占有率。
還好 UI 在同一個(gè)位置會(huì)相對(duì)的穩(wěn)定。相同的位置一般每次都會(huì)接受相同的參數(shù)。這樣以來(lái),使用一個(gè)集合來(lái)做 memoization 是一個(gè)非常好用的策略。
我們可以用對(duì)待 state 同樣的方式,在組合的函數(shù)中傳遞一個(gè) 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í),一層層傳遞數(shù)據(jù)非常麻煩。如果能有一種方式可以在多層抽象中快捷地傳遞數(shù)據(jù),同時(shí)又不需要牽涉到中間層級(jí),那該有多好。React 中我們把它叫做“context”。
有時(shí)候數(shù)據(jù)依賴(lài)并不是嚴(yán)格按照抽象樹(shù)自上而下進(jìn)行。舉個(gè)例子,在布局算法中,你需要在實(shí)現(xiàn)他們的位置之前了解子節(jié)點(diǎn)的大小。
現(xiàn)在,這個(gè)例子有一點(diǎn)超綱。我會(huì)使用 代數(shù)效應(yīng) 這個(gè)由我發(fā)起的 ECMAScript 新特性提議。如果你對(duì)函數(shù)式編程很熟悉,它們 在避免由 monad 強(qiáng)制引入的儀式一樣的編碼。
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)
);
}