[译]React - 基本理论概念
date
Apr 26, 2018
slug
react-basic
status
Published
tags
React
翻译
summary
type
Post
原文:https://github.com/reactjs/react-basic
作者:Sebastian Markbåge
注:这篇文章的翻译在中文互联网上已有多个版本。但为了加强自己对这篇文章的理解,我还是选择自己再翻一遍。翻译可能有所纰漏,欢迎指正。
React - 基本理论概念
我想通过这个文档来解释我关于 React 的心智模型。我打算用演绎推理的方式来描述它,从而引导我们去理解 React 的设计。
当然,这篇文章可能存在一些有争议,并且这个实例的实际设计可能存在 bug 和缺陷。这只是正式确定它的开始阶段。如果您对如何将它变得更加完善有很好的想法,随时欢迎 pull request。即使除去了周边的其他库,React 本身从简单 => 复杂的设计过程也很具有研究的价值。
React.js 的实际实现伴随着很多实用的解决方案、增量步骤、算法优化、遗留代码、调试工具以及其他能够使其变得实际可用的库。但这些方案并不会长久不变,如果有更有价值且有足够高优先级的解决方案出现,它们就会随着时间发生变化。
我倾向于建立一个更简单的心智模型,可以让自己更好地理解 React。
Transformation
React 的核心前提是 UI 仅仅是把数据通过映射关系变换成另一种形式的数据。相同的输入产出相同输出——就像一个纯函数一样:
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 不仅仅是服务端/业务逻辑状态的复制。实际上有很多状态是特指于某个具体的 UI projection 而不是其他的。
举个例子,如果你开始在一个 text field 中输入文字,这个操作可能会也可能不会同步到其他的 tab 中或你的移动设备中。Scroll position 也是一个典型的例子,你几乎从没想过让它在多个 UI 组件中进行复制。
我们倾向于将我们的 data modal 变得不可变的( 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 来更新状态。我的实际心智模型是,它们在
update
过程中返回下一个阶段的状态。如果没有这个过程那么就是解释起来就更简单了,但是我们会在以后改变这些示例。Memoization
如果某个函数是一个纯函数,那么重复调用那个函数是很浪费的。我们可以创建一个 memorized 版本的函数,它可以追踪最后一个参数和最后一个结果。这样的话,如果我们继续使用相同的值,就不必重新执行函数。
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 都是某种形式的列表,列表中的每个列表项会有多个不同的值,这创建了一个自然的层次。
为了管理每一个列表项的状态,我们可以创建一个可以包含某个特定列表项状态的 Map。
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);
注:我们现在有多个不同的参数传递给 FancyNameBox。这打破了我们的 memoization ,因为我们一次只能存储一个值。更多的内容在后续会提到。
Continuations
不幸的是,由于在用户界面中有很多列表,所以要明确地管理它们会产生很多样板代码( boilerplate ) 。
我们可以通过推迟执行某个函数来将这些样板代码从我们的核心业务逻辑中移出。比如说,使用柯里化( JavaScript 中的
bind
函数)。然后我们将状态从我们脱离了模板代码的核心函数外部转移过来。这种方案并没有减少样板代码,但是它至少将样板代码从核心业务逻辑中抽离出来了。
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
我们很早就知道,当我们遇到重复的模式时,我们可以通过组合来避免反复实现相同的模式。我们可以将提取和传递状态的逻辑移动到一个我们可以重复使用的 lowl-level 函数。
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
一旦我们想缓存列表中的多个列表项,缓存操作就会变得更加复杂。你必须设计复杂的缓存算法,用以平衡内存使用与频率。
幸运的是,(在 DOM 树中)处于相同位置的 UI 往往相对稳定。树中相同位置的节点总是获得相同的值。这让树变成了一个非常有用的 Memoization 策略。
我们可以使用我们用于状态的相同技巧,并通过可组合函数传递 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
事实证明,通过多个层次的抽象来传递你可能需要的每个值是比较痛苦的(原文是 PITA: Pain In the Ass)的。理想的情况是,有这样一条捷径可以在两个抽象之间传递数据而不涉及中间层。在 React 中,我们称之为 context。
有时候,数据依赖并不纯粹地依照抽象树分布。比如说,在布局算法中你需要先了解子元素的尺寸,然后你才可以真正地确定他们的位置。
现在,这个例子有点跑题。我将用到代数效应( Algebraic Effects )——这个由我发起的 ECMAScript提议,如果你比较熟悉函数式编程,你应该知道函数式编程会避免由 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)
);
}