首发于开源界目

React作者的构思和演绎

这是React作者在React设计之初,对整个框架的思考,个人觉得非常受启发,这是原文:
reactjs/react-basic
以下是我的翻译

我通过这篇文章试图阐述我对React模型的理解,阐述我们是如何用【演绎推导】来帮助我们得到最后的设计。

当然,这里有很多假设是有争议的(小编理解:比如说UI是纯函数等理论层面的东西),而且这篇文章中的例子是有缺陷和漏洞。 但这是我们正式确定react架构的开始。如果你有更好的想法去标准化这样的设计,要向我们发PR哈!我认为即便在没有太多库文件和细节的情况下,本文这样从简单到复杂的去演绎推理一个未来的产品是有意义的。

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

所以我想给大家展示一个简单的构思模型让我们可以立足其中(去思考学习)。

Transformation(变换)

React的一个核心假设就是UI是数据到视图的映射。同样的输入总是可以得到同样的输出。

function NameBox(name) {
  return { fontWeight: 'bold', labelContent: name };
}
'Sebastian Markbåge' ->
{ fontWeight: 'bold', labelContent: 'Sebastian Markbåge' };

抽象

用单个函数来描述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)
  ]);
}
小编:代码中UserBox这个抽象容器是由一组FancyBox抽象组合而成的。这样就形成了一个树形结构。

State(状态)

UI状态不是对服务端的业务逻辑和状态的复制。有很多状态是特定于某一个投射而不是其他的。比如当你在文本框中输入文字时(文字内容状态)不会投射到其他tab或者设备上。 滚动位置(状态)是一个典型的例子,因为你通常不会在多个投射间复用。(小编:投射应该指的是数据到视图的映射,是一种现在对未来的推导)

我们倾向于选择不可变的(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
);

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


小编: addOneMoreLike->FancyNameBox

Memoization(记忆)

一遍又一遍调用同一个纯函数非常浪费性能。我们可以把计算过程的输入和结果缓存起来。这样就不用重复计算。

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
  ]);
}
小编: memoize(NameBox), 将NameBox这个抽象(纯函数)缓存起来, 所以NameAndAgeBox中的FancyBox列表中的每一项,需要NameBox的时候会先从memoize中查找。

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

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


小编:likesPerUser这个Map将UserList中的每个用户状态缓存了起来

Continuations(连续性)

不幸的是,UI中有太多的列表的列表,最后需要显式管理很多的重复代码(boilerplate code)。

我们可以通过延迟执行函数,把一部分重复代码移出关键业务。例如:我们可以使用柯里化方法(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
};
小编: 这应该是最紧凑的一种写法了,虽然还是需要显示声明FancyUserList和FancyBox的区别,但已经无法再简化了。box.children这里其实就是一次延迟执行(具体没有写的非常清楚,原文说的应该就是处理这里的过程被移出了业务代码)

State Map(状态映射)

我们很早就知道,如果我们看到重复的模式,我们可以使用组合的方法避免重复实现这个模式。 我们可以把提取和传递状态的逻辑,移动到一个底层的函数中去。

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);
小编:FancyBoxWithState没有介入原来的组件关系,就定义了状态的传递逻辑,看了好几遍,这个设计对我启发很大。

Memoization Map

当我们试图记住列表中的多个项时,记忆变得更加困难。你需要找到一些复杂的缓存算法去平衡内存使用和频率。

庆幸的是,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);
小编:利用了树形结构的特点,缓存了列表项

Algebraic Effects

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

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

接下来的这个例子有点超出我们目前讨论的范围。我用Algebraic Effects(类似副作用的一个词汇)ECMAScript提出的来解释。如果你对函数式编程非常熟悉,他们在避免monads实践过程中炸了(they're avoiding the intermediate ceremony imposed by 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)
  );
}


小编:APP的主题定义为蓝色(BlueTheme),之后子组件通过ThemeBorderColorRequest获取这种蓝色的主题。


点评:

  1. 大巧不工——被作者这样一说,复杂的东西其实也很简单。
  2. 千里之行始于足下,学好简单的知识点,可以搞大事情
编辑于 2017-11-08

文章被以下专栏收录