17611538698
webmaster@21cto.com

React 作者的构思和演绎

资讯 0 3169 2017-10-23 11:56:19

21CTO社区导读:React证书问题已解决,越来越多的公司使用React,前端交互式编程技术已经越来越火热了。本文是React作者在React设计之初,对整个框架的思考,希望对各位前端开发者有益。



react-native.jpeg

 
我通过这篇文章试图阐述我对React模型的理解,阐述我们是如何用【演绎推导】来帮助我们得到最后的设计。
当然,这里有很多的前置条件是有争议的,而且这篇文章中的例子是有缺陷和漏洞。 但这是我们正式地去规范化它。如果你有更好的想法去形式化它,请随意向我们发PR。即便在没有太多库文件和细节的情况下,从简单到复杂的演绎应该是有意义的。

最终的React实现版本,需要大量务实的解决方案、增量迭代、算法优化、遗留代码、调试工具——这些如果有实用价值,都更新太快。所以react的实现过程是很难推导的。

所以我想给大家展示一个简单的构思模型让我可以立足其中。

Transformation(变换)
React的一个核心假设就是UI是数据到试图的映射。同样的输入总是可以得到同样的输出。
 
functionNameBox(name){
return{fontWeight:'bold',labelContent:name };
}
'Sebastian Markbåge'->
{fontWeight:'bold',labelContent:'Sebastian Markbåge'};

抽象

你不可以在单个函数中使用复杂的UI。你需要先把界面抽象成为可以复用的部分,而且每个部分不泄露自己的实现细节(作为一个函数)。这样你就可以从一个函数调用另一个函数。
 
functionFancyUserBox(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'}
]
};
 

上述代码FancyUserBox()和NameBox() 嵌套


Composition(组合)
为了实现真正可重用的特性,仅仅重用叶子节点,并在叶子节点上构造新的容器是不够的。 你需要在抽象的容器下面构造一个抽象容器的组合。 所谓的组合,就是将多个抽象合并成为一个新的抽象。
functionFancyBox(children){
return{
borderStyle:'1px solid blue',
children:children
};
}
functionUserBox(user){
returnFancyBox([
'Name: ',
NameBox(user.firstName +' '+user.lastName)
]);
}
 

其实这里说的组合就是多个组件(数组)可以作为一个整体渲染;这里说的抽象,其实就是组件。


State(状态)

UI不是简单地复制服务端的业务逻辑和状态,有很多状态是针对一个特殊的投射。比如在文本框中输入文字不会投射到其他tab或者设备上。 滚动位置不会在多个投射间复用。(小编:投射应该就是状态流过组件函数,最终被渲染的过程。输入框中被输入的文字和滚动位置这些状态太特殊了,是针对一种特殊的投射,没有办法从服务端复用,所以才需要状态)。

我们倾向于选择的数据模型是不可变的(immutable),我们将函数串联起来,并且认为更新状态的函数在最顶端。
 
functionFancyNameBox(user,likes,onClick){
returnFancyBox([
'Name: ',NameBox(user.firstName +' '+user.lastName),
'Likes: ',LikeBox(likes),
LikeButton(onClick)
]);
}
// Implementation Details
varlikes =0;
functionaddOneMoreLike(){
likes++;
rerender();
}
// Init
FancyNameBox(
{firstName:'Sebastian',lastName:'Markbåge'},
likes,
addOneMoreLike
);
 

这个例子在更新状态的时候有副作用。我真实的构思是在更新过程中函数返回下一个状态。这里是用最简单的方式给大家阐述原理,以后我们会更新这个例子。


Memoization(记忆)

调用一个纯函数一遍又一遍调用它其实非常浪费性能。我们可以把这个计算过程的输入和结果缓存起来。这样就不用重复计算。
 
functionmemoize(fn){
varcachedArg;
varcachedResult;
returnfunction(arg){
if(cachedArg ===arg){
returncachedResult;
}
cachedArg =arg;
cachedResult =fn(arg);
returncachedResult;
};
}
varMemoizedNameBox =memoize(NameBox);
functionNameAndAgeBox(user,currentTime){
returnFancyBox([
'Name: ',
MemoizedNameBox(user.firstName +' '+user.lastName),
'Age in milliseconds: ',
currentTime -user.dateOfBirth
]);
}

Lists

很多UI都是列表——列表中的每一项有不同的数据,这构成了一种天然的结构。
为了管理每一个元素的状态,我们可以创造一个Map把每个元素的状态存起来。
 
functionUserList(users,likesPerUser,updateUserLikes){
returnusers.map(user =>FancyNameBox(
user,
likesPerUser.get(user.id),
()=>updateUserLikes(user.id,likesPerUser.get(user.id)+1)
));
}
varlikesPerUser =newMap();
functionupdateUserLikes(id,likeCount){
likesPerUser.set(id,likeCount);
rerender();
}
UserList(data.users,likesPerUser,updateUserLikes);
 

我们将多个不同的参数传给了FancyNameBox,这个功能和记忆模块冲突了,因为我们每次只能记住一个值.接下来我们会讨论这点 .


Continuations(连续性)

不幸的是,UI中有太多的列表的列表,最后需要写很多的重复代码(boilerplate code)。
我们可以通过延迟执行函数,把一部分重复代码移出关键业务。例如:我们可以使用柯里化方法(bind in Java)。这样当我们把状态从外部传入核心函数时不会遇到重复代码。

这种方法虽然没有消除重复代码,但至少将重复代码移出了核心业务逻辑。
 
functionFancyUserList(users){
returnFancyBox(
UserList.bind(null,users)
);
}
constbox =FancyUserList(data.users);
constresolvedChildren =box.children(likesPerUser,updateUserLikes);
constresolvedBox ={
...box,
children:resolvedChildren
};

State Map(状态映射)

我们很早就知道,如果我们看到重复的模式,我们可以使用组合的方法避免重复实现这个模式。 我们可以把提取和传递状态的逻辑,移动到一个底层的函数中去。
 
functionFancyBoxWithState(
children,
stateMap,
updateState
){
returnFancyBox(
children.map(child =>child.continuation(
stateMap.get(child.key),
updateState
))
);
}
functionUserList(users){
returnusers.map(user =>{
continuation:FancyNameBox.bind(null,user),
key:user.id
});
}
functionFancyUserList(users){
returnFancyBoxWithState.bind(null,
UserList(users)
);
}
constcontinuation =FancyUserList(data.users);
continuation(likesPerUser,updateUserLikes);


Memoization Map

当我们试图记住列表中的多个项时,记忆变得更加困难。你需要找到一些复杂的缓存算法去平衡内存使用和频率。
庆幸的是,UI在同一位置是相对稳定的。树的相同位置总是得到相同的结果。这让树成为记忆的有效策略。
这样我们可以用同样的技术去缓存组合函数。
 
functionmemoize(fn){
returnfunction(arg,memoizationCache){
if(memoizationCache.arg ===arg){
returnmemoizationCache.result;
}
constresult =fn(arg);
memoizationCache.arg =arg;
memoizationCache.result =result;
returnresult;
};
}
functionFancyBoxWithState(
children,
stateMap,
updateState,
memoizationCache
){
returnFancyBox(
children.map(child =>child.continuation(
stateMap.get(child.key),
updateState,
memoizationCache.get(child.key)
))
);
}
constMemoizedFancyNameBox =memoize(FancyNameBox);

Algebraic Effects

React看上去像个PITA饼,一层又一层的,一个很小的值也需要这样传递。 这样我们在不同层之间需要一个短路——"context"

有时候数据依赖并没有很好地遵循抽象树,举个例子,在布局算法中你需要事先知道子元素的的大小。

接下来的这个例子有点超出我们目前讨论的范围。我用Algebraic Effects(类似副作用的一个词汇),ECMA提出的来解释。如果你对函数式编程非常熟悉,他们在避免monads实践过程中炸了(they're avoiding the intermediate ceremony imposed by monads)。
 
functionThemeBorderColorRequest(){}
functionFancyBox(children){
constcolor =raise newThemeBorderColorRequest();
return{
borderWidth:'1px',
borderColor:color,
children:children
};
}
functionBlueTheme(children){
returntry{
children();
}catcheffect ThemeBorderColorRequest ->[,continuation]{
continuation('blue');
}
}
functionApp(data){
returnBlueTheme(
FancyUserList.bind(null,data.users)
);
}

点评:


大巧不工——被作者这样一说,复杂的东西其实也很简单。

千里之行始于足下,学好简单的知识点,可以搞大事情



译者:ramroll
译文:https://zhuanlan.zhihu.com/p/30277192
原文:https://github.com/reactjs/rea ... ME.md


评论