React deep div 01 - 原始 API 設計概念

Reference https://github.com/reactjs/react-basic
此篇的重點在解釋 React 的 API 設計之心理模型,
意圖解釋清楚這樣設計的來龍去脈。
關於 實際上 React 的實作細節
充滿像是 pragmatic solutions, incremental steps, algorithmic optimizations, legacy code, debug tooling 這些隨著時間變動部分。
如果少了核心的觀念,很容易就會迷失方向。
一個簡單的心智模型,會幫助你更加了解真相。
Transformation
React 最核心的概念前提就是,畫面單純就是資料轉換到不同形式的映照關係。 同樣的 input 會產生 相同的 output,就是 functional programming 中的 pure function。
function NameBox(name) {
return { fontWeight: 'bold', labelContent: name };
}
'Sebastian Markbåge' ->
{ fontWeight: 'bold', labelContent: 'Sebastian Markbåge' };
Abstraction
非常重要的一點,UI 要能夠被抽象拆分成可重複利用的部件, 尤其在 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
因為每次都還是需要建立新的容器才能處理不同組合, 所以單只是函式呼叫函式是不夠的。 為了達成真正的重用,容器必須可以組合其他的容器。 我們認為,所謂的組合是可以將兩個以上不同的抽象組合成為一個。
function FancyBox(children) {
return {
borderStyle: '1px solid blue',
children: children
};
}
function UserBox(user) {
return FancyBox([
'Name: ',
NameBox(user.firstName + ' ' + user.lastName)
]);
}
State
請理解這件事, UI 並不是單純呈現 server / business logic 的狀態而已。
實際上有很多的狀態是只屬於 UI 自身, 例如, 文字輸入匡。 scroll 位置。 Toggler。
我們意圖讓資料模型是 immutable, 並且只能透過指定的函式才能更動那個狀態。
function FancyNameBox(user, likes, onClick) {
return FancyBox([
'Name: ', NameBox(user.firstName + ' ' + user.lastName),
'Likes: ', LikeBox(likes),
LikeButton(onClick)
]);
}
// Implementation Details
var likes = 0;
function addOneMoreLike() {
likes++;
rerender();
}
// Init
FancyNameBox(
{ firstName: 'Sebastian', lastName: 'Markbåge' },
likes,
addOneMoreLike
);
注意:這邊的範例是透過 side-effects 更新狀態,為了簡化範例。 但實際心智模型是他要回傳 下個版本的 state。
Memoization
連續用同樣的參數呼叫純函式是一種浪費, 透過 memoized 我們可以避免不必要的重複運算。
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 是某種形式的列表,然後為列表中的每個項目對應生成不同的值。 這形成了一個自然的結構。
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);
Continuations (HOC)
但不幸的,當專案已經有了太多種類的清單時, 我們需要一個 boilerplate 用於處理各種情況,
將 business logic 從函式中移出, 並透過 currying 延遲函式的執行, 再根據使用情況從外面拋入使用。
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
前面看到的,我們可以透過 composition 來避免重工。 現在我們可以將 logic 抽出,並透過傳入 state 至低階級的函式。
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
當我們需要 memoize 列表會更加困難, 所幸,UI 在畫面的呈現位置上是比較穩定的, 同樣的值會出現在同樣的位置,所以整個樹狀結構可以直接 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);
Algebraic Effects
會變得有點煩,當你需要把每個資料傳過不同 level 的抽象層。 如果有方法可以將資料直接在兩個抽象層傳遞而中介層不會知道,這樣會更好。 在 React 這被稱作 "context"。
有的時候,資料的依賴關係並不符合抽象樹。
如果你熟悉 functional programming 的話,他們是透過 monads 處理這些。
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)
);
}

