本文 fork 自 react-basic,如遇不明白欢迎提 issue 或者去探索原文。
本文不会照搬原文进行翻译,我尽量采用简单、易理解的话术让大家学习起来更加轻松。
React.js 的实际实现充满了务实的解决方案、增量步骤、算法优化、遗留代码、调试工具以及一些其他所需的东西。这些东西你看过就忘了,而且如果它有价值并且优先级又高,它们会随着时间变化。因此,他们的实现就更难以理解了。
我喜欢有一个更简单的心智模型,这样可以让自己扎根。
React 的核心前提是 UI 只是将数据投影为不同形式的数据。相同的输入给出相同的输出。一个简单的纯函数。
function NameBox(name) {
return { fontWeight: 'bold', labelContent: name };
}'senmu qin' ->
{ fontWeight: 'bold', labelContent: 'senmu qin' };
但是,你无法将复杂的 UI 放入单个函数中。重要的是,UI 可以抽象为可重用的部分,且不会泄露其实现细节。例如在一个函数中调用另一个函数。
function FancyUserBox(user) {
return {
borderStyle: '1px solid blue',
childContent: [
'Name: ',
NameBox(user.firstName + ' ' + user.lastName)
]
};
}{ firstName: 'senmu', lastName: 'qin' } ->
{
borderStyle: '1px solid blue',
childContent: [
'Name: ',
{ fontWeight: 'bold', labelContent: 'senmu qin' }
]
};
要实现真正可重用的特性,仅仅重用叶子并为它们构建新的容器是不够的。你还需要能够从容器构建抽象,这些容器构成其他抽象。我对“组合”的看法是,它们将两个或多个不同的抽象组合成一个新的抽象。
function FancyBox(children) {
return {
borderStyle: '1px solid blue',
children: children
};
}
function UserBox(user) {
return FancyBox([
'Name: ',
NameBox(user.firstName + ' ' + user.lastName)
]);
}UI 不仅仅是业务逻辑状态的复制。实际上有很多状态是特定于精确投影而不是其他状态。类似下面的例子,一个 FancyNameBox 由 Name 和 Likes 组成,并且存在一个状态 likes 来维护某个时刻的特定投影/片段。
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: 'senmu', lastName: 'qin' },
likes,
addOneMoreLike
);注意:这些示例使用副作用来更新状态。我对此的实际心智模型是他们在“更新”过程中返回下一个版本的状态。如果没有它,解释起来更简单,但我们希望在将来更改这些示例。
如果我们知道该函数是纯函数,那么一遍又一遍地调用同一个函数是一种浪费。我们可以创建一个函数的记忆版本来跟踪最后一个参数和最后一个结果。这样,如果我们继续使用相同的值,就不必重新执行它。
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
]);
}大多数 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。这打破了我们的记忆,因为我们一次只能记住一个值。更多关于下面的内容。 *
不幸的是,由于 UI 中到处都有很多列表,显式管理它的样板(样式+逻辑)会变的特别多。
我们可以通过延迟函数的执行将一些样板文件移出我们的关键业务逻辑。例如,通过使用“currying”(在 JavaScript 中绑定)。然后我们从现在没有样板的核心功能外部传递状态。
这并没有减少样板文件,但至少将其从关键业务逻辑中移出。
简而言之就是将逻辑用“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
};我们从前面知道,一旦我们看到重复的模式,我们就可以使用组合来避免一遍又一遍地重新实现相同的模式。我们可以将提取和传递状态的逻辑移至我们经常重用的低级函数。
下面是对于多层结构逻辑的抽离
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);一旦我们想要记忆列表中的多个项目,记忆就会变得更加困难。你必须想出一些复杂的缓存算法来平衡内存使用和频率。
幸运的是,UI 在同一位置往往相当稳定。树中的相同位置每次都获得相同的值。这棵树被证明是一种非常有用的记忆策略。
我们可以使用用于状态的相同技巧,并通过可组合函数传递记忆缓存。
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);事实证明,通过多个抽象级别传递你可能需要的每个小值是一种 PITA(你可以理解为千层饼)。有时有一种捷径可以在两个抽象之间传递事物而不涉及中间体,这很好。在 React 中,我们称之为“上下文/Context”。
有时数据依赖性并不完全遵循抽象树。例如,在 layout 算法中,你需要先了解 children 的大小,然后才能知道他们的位置。
现在,这个例子有点“out there”。我将使用为 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)
);
}整个文档从头到尾看下来是一个由浅入深的过程,有助于我们了解 React 背后做的事情是什么,它的理念是什么。
关键点/重难点应该是抽象组合部分,也正是 React 背后函数式编程的思想核心。
UI/样式与状态的关系是 1 对 1 的,特定不变(浅层状态)样板可以通过记忆化手段来缓存下来以优化性能。
有时候我们会陷入简单 UI 的陷阱,为什么这样说呢?像文中前面的例子所述的那样,我们通常会将简单“样板”的逻辑与样式通过简单的抽象组合到一起,这其实就是一个陷阱。但是通过本文我们可以学到将逻辑与样式的抽离很利于面对复杂的场景,换句话说就是将你的每一个“样板”当作一个最小的单元,最后每个单元组合形成完整结构。
另外,文中最后提到的跨级的数据交互是 React 的 Context 的思想,他背后的逻辑文中有详细链接,有兴趣的同学可以去研究。
本人能力有限,至于文中有理解不到位的地方欢迎大家提 issue 帮我指正~