React 概念模型——脱离React谈谈它的设计思想

本文翻译自react-basic,本文从属于笔者的Web 前端入门与最佳实践前端

在正式学习React以前,咱们但愿能脱离React自己来了解下React的设计思想,这有助于咱们更好地运用React与进行更好地架构设计。固然,这里讨论的一些设计理念确定仍是有争论的,见仁见智,各有所感。React.js自己的学习与实现是偏重于工程解决方案、算法优化、代码兼容以及调试工具这些方法论,不过,这些都是会随着时间以及应用长久的变迁发生改变,惟有设计思想可以绵延流长。术道相济,方能长久。react

Transformation(转换)

React的核心前提便是改变了jQuery这种以DOM操做为核心到以数据流驱动为核心,View是不一样的数据的投射。而且对于数据的处理函数应该是纯函数,即相同的输入有相同的输出而不会产生其余反作用:git

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

'Sebastian Markbåge' ->
{ fontWeight: 'bold', labelContent: 'Sebastian Markbåge' };

这样能够极大地方便对于View构造函数的重用与单元测试等。github

Abstraction(抽象)

对于一个复杂的UI,确定不能全都塞到一个函数里处理,这就是React另外一个重要的思想,将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(组合)

为了达到真正意义上的重用目标,并不单单就是把那个叶子组件组合进一个新的容器,咱们也须要在容器中构建出可以组合其余抽象组件的抽象组件。这里我认为的组合要点在于如何把两个或者更多的抽象组件合并成一个新的:segmentfault

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

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

State

一个UI并不单单是服务端或者业务逻辑的重现,实际上有不少特定的状态会被投射到UI上。譬如,若是你正在输入一个文本框,这个并不会复制到其余的Tab或者你的手机浏览器中。另外,滚动位置也是一个典型的你并不想投射到其余地方的状态。咱们但愿咱们的数据模型会更加地固定,所以,咱们从顶部组件开始将更新函数一级一级地注入到实际的显示的那个模块上。设计模式

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

注意,这里的例子仍是用了带反作用的函数来更新状态,不过我本意是想采用纯函数,即每次返回最新的状态来完成这个工做。我会在下面的例子里阐述这个观点。浏览器

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

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

注意,在这个函数里咱们传入了多个值,这样就不能缓存结果了。

Continuations

B狗的事情发生了,由于存在着不少的列表,咱们也须要维护不少的模板,不一样的列表显示的数据有交集有差别,譬如用户列表和你关注的用户列表,它们可能就是操做按钮上的不一样。咱们能够将部分模板和业务逻辑解耦合如下,譬如使用柯里化这种构造高阶函数的手段。这种手段自己并不能减小业务逻辑或者最终模板的复杂度,不过可以将一部分代码移出业务逻辑:

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

早前咱们就知道著名的23种设计模式里会避免重复的实现一些通用模式,咱们也能够将一些状态管理的逻辑函数移到统一的初级函数里,这样就方便重复使用了:

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

上面提到过,当存在多个输入参数的状况下要再想进行缓存就会麻烦一点,咱们要使用一些复杂的缓存策略来平衡内存使用与频次。幸运的是不少地方View仍是比较固定的,整个树上的相同位置的值通常都是相同的,所以能够用树结构来进行缓存。

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

若是咱们在一个嵌套多层的UI体系里每次都把一些参数一级一级的传递下去,那约莫是很是麻烦的。所以咱们须要创造一些捷径来在两个不直接相连的抽象组件之间传递数据,而不须要经过中间层。在React里面叫他Context。(官方文档里Context仍是属于测试阶段)。有时候这个数据依赖的关系并不严格按照抽象树的逻辑,譬如在一个布局算法里你须要知道你的子元素的大小你才可以完整地决定他们的位置。我在这里使用 Algebraic Effects 做为 proposed for ECMAScript。

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

Further Reading

相关文章
相关标签/搜索