Skip to main content

React 框架设计思想

React 接管了 UI 开发中最为复杂的局部更新部分,擅长在在复杂场景下保证高性能;同时,它引入了基于组件的开发思想,从另一个角度来重新审视 UI 的构成。通过这种方法,不仅能够提高开发效率,而且可以让代码更容易理解,维护和测试。

一、React 原理

在 Web 开发中,总需要将变化的数据实时反应到 UI 上,这时就需要对 DOM 进行操作。而复杂或频繁的 DOM 操作通常是性能瓶颈产生的原因(如何进行高性能的复杂 DOM 操作通常是衡量一个前端开发人员技能的重要指标)。

其实在任何 UI 的变化都是通过整体刷新来完成的,而 React 将这种开发模式以高性能的方式带到了前端,每做一点界面的更新,都可以认为刷新了整个页面。至于如何进行局部更新以保证性能,则是 React 要完成的事情。

React 为此引入了虚拟 DOM(Virtual DOM)的机制:在浏览器端用 Javascript 实现了一套 DOM API。基于 React 进行开发时所有的 DOM 构造都是通过虚拟 DOM 进行,每当数据变化时,React 都会重新构建整个 DOM 树,然后 React 将当前整个 DOM 树和上一次的 DOM 树进行对比,得到 DOM 结构的区别,然后仅仅将需要变化的部分进行实际的浏览器 DOM 更新。换句话说,就是数据变化时,React 用虚拟 DOM 的方式来对比变化前后的 DOM,然后以最高性能的方式去更新看到的界面。

而且 React 能够批处理虚拟 DOM 的刷新,在一个事件循环(Event Loop)内的两次数据变化会被合并。

举个例子,连续的先将节点内容从 A 变成 B,然后又从 B 变成 A,React 会认为 UI 不发生任何变化,而如果通过手动控制,这种逻辑通常是极其复杂的。

尽管每一次都需要构造完整的虚拟 DOM 树,但是因为虚拟 DOM 是内存数据,性能是极高的,而对实际 DOM 进行操作的仅仅是 Diff 部分,因而能达到提高性能的目的。这样,在保证性能的同时,开发者将不再需要关注某个数据的变化如何更新到一个或多个具体的 DOM 元素,而只需要关心在任意一个数据状态下,整个界面是如何 Render 的。

二、React 设计思想

1、转换(Transformation)

设计 React 的核心前提是认为 UI 只是把数据通过映射关系变换成另一种形式的数据。同样的输入必会有同样的输出。这恰好就是纯函数。

function NameBox(name) {
return { fontWeight: 'bold', labelContent: name };
}
'FSX' ->
{ fontWeight: 'bold', labelContent: 'FSX' };

2、抽象(Abstraction)

很难仅用一个函数就实现复杂的 UI,需要把 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' }
]
};

3、组合(Composition)

为了真正达到重用的特性,需要将包含其他抽象的容器再次进行组合,也就是将两个或者多个不同的抽象合并为一个。

function FancyBox(children) {
return {
borderStyle: '1px solid blue',
children: children
};
}

function UserBox(user) {
return FancyBox([
'Name: ',
NameBox(user.firstName + ' ' + user.lastName)
]);
}

4、状态(State)

UI 不单单是对服务器端或业务逻辑状态的复制,实际上还有很多状态是针对具体的渲染目标。

我们倾向于使用不可变的数据模型,把可以改变 state 的函数串联起来作为原点放置在顶层。

function FancyNameBox(user, likes, onClick) {
return FancyBox([
'Name: ', NameBox(user.firstName + ' ' + user.lastName),
'Likes: ', LikeBox(likes),
LikeButton(onClick)
]);
}

// 实现细节

var likes = 0;
function addOneMoreLike() {
likes++;
rerender();
}

// 初始化

FancyNameBox(
{ firstName: 'Sebastian', lastName: 'Markbåge' },
likes,
addOneMoreLike
);

注意:本例更新状态时会带来副作用(addOneMoreLike 函数中)。

5、记忆(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
]);
}

6、列表(Lists)

大部分 UI 都是展示列表数据中不同 item 的列表结构。这是一个天然的层级。

为了管理列表中的每一个 item 的 state ,可以创造一个 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);

注意:现在向 FancyNameBox 传了多个不同的参数。这打破了 memoization,因为每次只能存储一个值。

7、连续性(Continuations)

不幸的是,自从 UI 中有太多的列表,明确的管理就需要大量的重复性样板代码。

可以通过推迟一些函数的执行,进而把一些模板移出业务逻辑。比如,使用“柯里化”(JavaScript 中的 bind)。然后可以从核心的函数外面传递 state,这样就没有样板代码了。

下面这样并没有减少样板代码,但至少把它从关键业务逻辑中剥离。

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
};

8、状态图(State Map)

之前我们知道可以使用组合避免重复执行相同的东西这样一种重复模式。可以把执行和传递 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);

9、记忆图(Memoization Map)

一旦想在一个 memoization 列表中 memoize 多个 item 就会变得很困难。因为需要制定复杂的缓存算法来平衡调用频率和内存占有率。

还好 UI 在同一个位置会相对的稳定。相同的位置一般每次都会接受相同的参数。这样以来,使用一个集合来做 memoization 是一个非常好用的策略。

可以用对待 state 同样的方式,在组合的函数中传递一个 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);

10、代数效应(Algebraic Effects)

多层抽象需要共享琐碎数据时,一层层传递数据非常麻烦。如果能有一种方式可以在多层抽象中快捷地传递数据,同时又不需要牵涉到中间层级,那该有多好。React 中我们把它叫做“context”。

有时候数据依赖并不是严格按照抽象树自上而下进行。举个例子,在布局算法中,需要在实现他们的位置之前了解子节点的大小。

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)
);
}