资讯专栏INFORMATION COLUMN

【译】React及React Fiber基本的设计理念

lewif / 2747人阅读

摘要:基础的理论概念这篇文章是我的一次尝试,希望能够形式化的介绍关于本身的一些理念模型。我对于此实际的理念模型是在每次的更新过程中返回下一个阶段的状态。的目标是提升对在动画,布局以及手势方面的友好度。我已经邀请了团队的成员来对本文档的准确性进行。

前言

本文主要是对收集到的一些官方或者其他平台的文章进行翻译,中间可能穿插一些个人的理解,如有错误疏漏之处,还望批评指正。笔者并未研究过源码,只是希望本文成为那些inspire你的东西的一部分,从而在今后一起去探讨和研究React Fiber。

注:绝大多数情况下,以下的第一人称不代表译者,而是对应文章的作者,请注意区分。

React basic 基础的理论概念

  这篇文章是我的一次尝试,希望能够形式化的介绍关于react本身的一些理念模型。目的在于基于演绎推理的方式,描述那些给我们灵感让我们进行这样的设计的源泉。

  当然,这里的一些设想是具有争议的,实际的设计也许也会有bug或者疏漏。但是,这也是一个好的开始让我们去形式化地谈论这些。同时,如果你有更好的想法,也欢迎pr。以下让我们沿着这个思路,从简单到复杂的去思考这一系列问题,不必担心,这里没有太多具体的框架细节。

  实际的关于React的实现是充满务实主义的,渐进式的,算法优化的,新老代码交替的,各种调试工具以及任何你能想到的让他变成更加有用的东西。当然,这些东西也像版本迭代一样,它们的存在是短暂的,如果它们足够有用,我们就会不断的更新他们。再次声明,实际的实现是非常非常复杂的。

转换

  React最核心的前提是,UI仅仅是数据->数据的映射。相同的输入意味着相同输出。非常简单的纯函数。

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

  但是,并不是所有的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" }
  ]
};
组合

  为了实现可复用这一特性,仅仅只是简单复用叶子节点,每次都为它们创建一个新的容器是远远不够的。同时我们需要在容器(container)这一层面构建抽象,并且组合其它抽象。在我看来,组合就是将两个甚至多个抽象变成一个新的抽象。

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

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

  UI并不仅仅是简单的服务或者说业务中的逻辑状态。事实上,对于一个特定的投影而言,很多状态是具体的,但是对于其他投影,可能不是这样。例如,如果你正在文本框中输入,这些输入的字符可以被复制到另外的tab或者移动设备上(当然你不想复制也没问题,主要是为了和下一句的例子进行区分)。但是,诸如滚动条的位置这样的数据,你几乎从来不会想把它在多个投影中复制(因为在这台设备上比如滚动条位置是200,但是在其他设备上滚动到200的内容通常来说肯定是不同的)。

  我们更趋向于将我们的数据模型变为不可变的。我们在最顶端将所有能更新状态的函数串起来,把它们当作一个原子(说成事务可能更容易明白)来对待

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

注意:这个例子通过副作用去更新状态。我对于此实际的理念模型是在每次的更新过程中返回下一个阶段的状态。当然,不这样做看起来要更简单一点,但是在以后我们最终还是会选择改变这个例子采用的方式(因为副作用的缺点太多了)。

缓存

  我们知道,对于纯函数而言,一次又一次相同的调用是非常浪费时间和空间的。我们可以对这些函数建立缓存的版本,追踪最近一次调用的输入和输出。下一次就可以直接返回结果,不用再次计算。

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都是通过很多个列表组成,通过列表中的每个元素产生不同的值(比如data.map(item => ))。这样就产生了一种天然的层次结构。

  为了管理每个列表元素的状态,我们可以创建一个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。那会破坏我们上一节提到的缓存策略,因为我们一次只能记忆一个值。(因为上面的memoize函数的形参只有一个)

续延

  不幸的是,在UI中有太多的list相互嵌套,我们不得不用大量的模板代码去显式的管理它们。

  我们可以通过延迟执行将一部分的模板代码移到我们的主要逻辑之外。例如,通过利用currying(可以通过bind实现)(当然我们知道这样bind并没有完整的实现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 FancyUserList(users) {
  return FancyBox(
    UserList(users, likesPerUser, updateUserLikes)
  );
}

  但是这样扩展起来就很麻烦,想增加,删除我们都需要去改FancyUserList里的代码。最重要的是,如果我们想将likesPerUserupdateUserLikes换成其他的集合和函数的话,我们必须再创建一个函数,如:

function FancyUserList2(users) {
  return FancyBox(
    UserList(users, likesPerUser2, updateUserLikes2)
  );
}

当然,你肯定会想到,直接给FancyUserList设置成接收多个参数不就行了。但是这样依然存在一个问题,那就是每次你需要用到FancyUserList的时候,都需要带上所有的参数。要解决也是可以的,比如const foo = FancyUserList.bind(null, data.users),后面需要用的话,直接foo(bar1, func1), foo(bar2, func2)就行了。也实现了设计模式中我们常谈到的分离程序中变与不变的部分。但是这样的实现将bind操作交给了调用者,这一点上可以改进,就像示例中提到的那样。

状态映射

  我们很早就知道,一旦我们看见相同的部分,我们能够使用组合去避免一次又一次重复的去实现相同的部分。我们可以将提取出来那部分逻辑移动并传递给更低等级或者说更低层级的函数,这些函数就是我们经常复用的那些函数。

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通常是比较稳定的,不会变化的。

  在这里我们依然可以采用像刚刚那种缓存state的技巧,通过组合的方式传递memoizationCache

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.

  有时候数据之间的依赖并不像抽象树那样整齐一致。例如,在布局算法中,在完整的确定所有字节点的位置之前,你需要知道各个子节点矩形区域的大小。

Now, this example is a bit "out there". I"ll use Algebraic Effects as proposed for ECMAScript. If you"re familiar with functional programming, they"re avoiding the intermediate ceremony imposed by monads.

译注:FP理解不深,所以上面段就不翻译了,以免误导

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 Fiber体系结构

译注:为了比较形象的阐释,故这里将React Stack vs Fiber的视频贴在这,而不是放在阅读更多里面。由于在youtube上,为了方便查看,这里录制了一张gif(有点大,18M,下载时请耐心等待)。

简介

  React Fiber是一个正在进行中的对React核心算法的重写。它是过去两年React团队研究成果的一个顶峰。

  React Fiber的目标是提升对在动画,布局以及手势方面的友好度。它最重要的特性叫做"增量式/渐进式"渲染:即,将渲染工作分割为多个小块进行,并在各个帧之间传播。

  其它关键的特性包括,1.拥有了暂停,中止以及当有更新来临的时候重新恢复工作的能力。2.不同的能力对于不同类型的更新分配不同的优先级。3.新的并发原语。

关于本文档

  在Fiber中引入了几个新的概念,这些概念仅仅只看代码是很难真的体会的。本文档最初只是我在React项目组时的收集,收集一些我整理Fiber的实现的时候的笔记。随着笔记的增多,我意识到这可能对其他人来说也是一个有益的资源。(译注:本文档的作者acdlite是Facebook开发组的一名成员,并不属于React框架的开发组(这里指实际工作中,而不是gh上的team)。React团队的leader,旧的核心算法及新的核心算法的提出者是sebmarkbage)

  我将尝试尽可能用简单的语言来描述,避免一些不必要的术语。在必要时也会给出一些资源的链接。

  请注意我并不是React团队的一员,也不具备足够的权威。所以这并不是一份官方文档。我已经邀请了React团队的成员来对本文档的准确性进行review。

  Fiber是一项还在进行中的工作,在它完成前都很可能进行重改。所以本文档也是如此,随着时间很可能发生变化。欢迎任何的建议。

  我的目标是,在阅读本文档后,在Fiber完成的时候,顺着它的实现你能更好的理解它。甚至最终回馈React(译注:意思是fix bug,pr新特性,解决issue等等)。

准备

  在继续阅读前,我强烈建议你确保自己对以下内容已经非常熟悉:

  React Components, Elements, and Instances - "组件"通常来说是一个范围很大的术语。牢固的掌握这些术语是至关重要的。

  Reconciliation - 对React的协调/调度算法的一个高度概括。

  React基础理论概念 - 对React中的一些概念模型的抽象描述,第一次读的时候可能不太能体会。没关系,以后终会明白的。

  React设计原则 - 请注意其中的scheduling这一小节,非常好的解释了React Fiber。

回顾

  如果你还没准备好的话,请重新阅读上面的"准备"一节。在我们探索之前,让我们来了解几个概念。

什么是协调(reconciliation)

  reconciliation:是一种算法,React使用它去区分两棵树,从而决定到底哪一部分需要改变。

  update:数据的变化会导致渲染,通常这是setState的结果,最终会触发重新渲染。

  React API的核心理念是思考/决定/调度怎样去update,就好像它会导致整个app重新渲染一样。它让开发者能够声明式地去思考,而不用去担心如何高效的将app从一个状态过渡到另一个状态(A到B,B到C,C再到A等等)。

  事实上,每次变化都重新渲染整个app的方式只能工作在非常小的app上。在现实世界真正的app中,这在性能上花费的代价太大了。React已经在这方面做了优化,在保持好性能的前提下创造出app重新渲染之后的样子。绝大部分的优化都属于reconciliation这个过程的一部分。

  Reconciliation是一个隐藏在被广为熟知的称作"virtual DOM"的背后的算法。概括起来就是:当你渲染一个React应用的时候,就产生了一棵描述这个应用的节点树,并存储在内存中。接下来这棵树会被刷新,然后翻译到具体的某个环境中。例如,在浏览器环境,它被翻译成一系列的DOM操作。当app有更新的时候(通常是通过setState),一棵新的树就产生了。这棵新树会与之前的树进行diff,然后计算出更新整个app需要哪些操作。

  虽然Fiber是一个对reconciler完全的重写,但是React文档中对核心算法的概括描述仍然是适用的。几个关键点为:

不同的组件类型被假定为会产生本质上不同类型的树。React不会尝试对它们进行diff,而是完全地替换旧的树。(译注:如

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/82487.html

相关文章

  • ELSE 技术周刊(2017.12.11期)

    摘要:业界动态发布版本,同时发布了版本以及首个稳定版本的。程序人生如何用人类的方式进行二关于如何在中进行良好的沟通,避免陷入一些潜在的陷阱。技术周刊由小组出品,汇聚一周好文章,周刊原文。 业界动态 Angular 5.1 & More Now Available Angular发布5.1版本,同时发布了Angular CLI 1.6版本以及首个稳定版本的Angular Material。CL...

    tylin 评论0 收藏0
  • 2017-07-12 前端日报

    摘要:前端日报精选借助和缓存及离线开发中和走进之实现分析总是一知半解的中个常见的陷阱发布核心成员发布了免费的学习视频中文译的函数式编程是一种反模式掘金译更好的表单设计每一页,一件事实例研究掘金打印龙墨并不简单结合实现简单的加载动画 2017-07-12 前端日报 精选 借助Service Worker和cacheStorage缓存及离线开发JavaScript中toString()和valu...

    zhoutk 评论0 收藏0
  • React系列——React Fiber 架构介绍资料汇总(翻+中文资料)

    摘要:它的主体特征是增量渲染能够将渲染工作分割成块,并将其分散到多个帧中。实际上,这样做可能会造成浪费,导致帧丢失并降低用户体验。当一个函数被执行时,一个新的堆栈框架被添加到堆栈中。该堆栈框表示由该函数执行的工作。 原文 react-fiber-architecture 介绍 React Fibre是React核心算法正在进行的重新实现。它是React团队两年多的研究成果。 React ...

    taohonghui 评论0 收藏0
  • Deep In React之浅谈 React Fiber 架构(一)

    摘要:在上面我们已经知道浏览器是一帧一帧执行的,在两个执行帧之间,主线程通常会有一小段空闲时间,可以在这个空闲期调用空闲期回调,执行一些任务。另外由于这些堆栈是可以自己控制的,所以可以加入并发或者错误边界等功能。 文章首发于个人博客 前言 2016 年都已经透露出来的概念,这都 9102 年了,我才开始写 Fiber 的文章,表示惭愧呀。不过现在好的是关于 Fiber 的资料已经很丰富了,...

    Jiavan 评论0 收藏0

发表评论

0条评论

lewif

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<