冰山一角
首发于冰山一角

浅入 React16/fiber - Part 1 前期准备

Tip:以下内容未经整理,是一个还未完成的笔记。由于是 md 写的,一部分内容分享到了有道行,可以点击这里,排版会好一点


免责声明:以下真的只是随手瞎写笔记,仅仅仅仅供参考。。


[源码结构](Fix cDU to correctly make async request by nateq314 · Pull Request #745 · reactjs/reactjs.org):

  • ReactFiberBeginWork (“进入” 一个组件)
  • ReactFiberCompleteWork (“离开” 一个组件)
  • ReactFiberCommitWork (flushing changes to DOM)
  • ReactFiberScheduler (选择下一个需要 begin work 的组件)
  • ReactChildFiber (对 children 插入/删除的 diff 过程)
  • Entry 结尾的文件[代表公共 api](Expiration times by acdlite · Pull Request #10426 · facebook/react)


整体计划


[Fiber] Umbrella for remaining features / bugs · Issue #7925 · facebook/react


框架总览


注:以下都是针对浏览器端而已,native 没有考虑


整体思路


之前的[文章](人类身份验证 - SegmentFault)已经讲过一些基本的


手动模拟函数栈的执行过程


facebook.com/groups/200


JS Bin



为什么需要进行这样的重写,因为 `requestIdleCallback` 和 `requestAnimationFrame` 都是用来执行一个**小**任务,你不能花太多的时间去执行一个任务,否则会卡主,这也是为什么它们有一个 `deadline` 形参参数。但是任务肯定不可能全是小的,于是我们需要将一个大的任务划分成多个小的任务,但是这是非常非常麻烦,因为很多任务都具有连续性和原子性,所以我们才需要进行这样的重写,来

  • 任意地中断调用栈并且手动操作栈帧
  • 能够在内存中保留栈帧
  • 在任何时候通过任意方式执行
  • 手动地处理栈帧,也许能够让我们拥有一些潜在的特性,例如并发以及错误边界处理

基本概念


  • Reconciliation:即diff算法,对新旧两颗树进行比较。属于空间维度。决定该创建一个新的dom还是仅仅替换内容,该删除还是由于有相同的key而不删除。
  • Scheduling:到底应该什么时候去做这件事(所以自然而言就需要引入优先级机制,比如界面外的,不可见的,是不是现在就不应该做,那么优先级自然就低),属于时间维度,类似于操作系统的调度算法,分配时间片的几种机制(先来先服务,短进程优先等等)

既然Fiber这种数据结构是模拟函数栈的,同时一个fiber(首字母小写)代表一个Fiber栈中的栈帧,那么我们有必须先了解一下函数栈里的栈帧是怎样的,以便我们更好的理解Fiber里为什么要设置这些字段,以及各个字段的含义。


我们可以看看wiki对函数调用栈的描述:

https://upload.wikimedia.org/wikipedia/commons/thumb/d/d3/Call_stack_layout.svg/684px-Call_stack_layout.svg.png
function DrawLine(point1, point2) {
   // 调用系统提供的GUI API......
}

function DrawSquare(startPoint, edge) {
   // 画一个正方形可以分解为以下4步
   // 1.
   //    ------
   //
   // 2.
   //    ------
   //         |
   //         |
   //   
   // 3. 
   //    ------
   //         |
   //         |
   //    ------
   //
   // 4.
   //    ------
   //    |    |
   //    |    |
   //    ------

   DrawLine(startPoint, new Ponint(startPoint.x + edge, startPoint.y), edge);
   DrawLine(new Ponint(startPoint.x + edge, startPoint.y), new Ponint(startPoint.x + edge, startPoint.y + edge), edge);
   DrawLine(new Ponint(startPoint.x + edge, startPoint.y + edge), new Ponint(startPoint.x, startPoint.y + edge), edge);
   DrawLine(new Ponint(startPoint.x, startPoint.y + edge), new Ponint(startPoint.x, startPoint.y), edge);
}

DrawSquare(new Point(100, 200), 50);

console.log('done!');


我们可以看到,一个栈帧至少需要保存3个信息。

  • 我们执行`DrawSquare(new Point(100, 200), 50)`的时候,首先会压入函数的实参(即`new Point(100, 200), 50`)。
  • 然后会压入函数的返回值的地址(以便系统知道程序该从哪里继续运行,对上面的程序来说,就是`DrawSquare(new Point(100, 200), 50)`这一句执行完毕后的地址,知道这个我们的程序才能继续运行下去,以便完成后面的工作,如执行后面的`console.log('done!')`)。
  • 最后是压入DrawSquare这个函数内部的局部变量。


除此之外,可能还会保存其它信息,如:

  • 计算栈(Evaluation stack):对于算术和逻辑运算的操作数而言,通常会被放置到寄存器并在那里进行运算。然而,在某些情况下,操作数可能会叠加到一个很深的深度,这意味着我们需要的寄存器将会超过系统的限制(这种情况叫做寄存器溢出)。为了解决这个问题,会创建一个新的栈用于处理这些操作数,就像在逆波兰计算器的栈一样,这被称作计算栈,也许会在调用栈中占据空间。
  • 指向当前实例的指针(Pointer to current instance):一些面向对象语言(如C++),在调用方法的时候,会将这个指针和函数的参数一起存储在栈中。`this`指针指向当前实例的对象,这个对象与被调用的方法相关联。
  • 封闭的函数上下文(Enclosing subroutine context):一些程序语言(如Pascal和Ada)支持嵌套函数声明(笔注:即在函数里声明函数),允许访问包裹它们的函数的上下文,即外部的函数的形参和局部变量。这样的静态嵌套可以重复(即嵌套n层)。支持这样的语法的实现必须提供一种手段,让一个处于任何层次嵌套的函数能够指向包裹它的任何函数的栈帧。通常这种引用是通过一个指向最近一次封闭函数内激活的实例的指针实现的,这被称作下栈链接("downstack link")或者静态链接("static link"),以便为了与指向直接调用者的动态链接("dynamic link")区分,动态链接不需要静态的父函数。(笔注:动态链接应该是指在一个嵌套函数内调用通过`new Function(...)`创造的函数,为什么它们不需要静态的父函数呢,因为在它们的函数体内只能访问形参变量,外部的也就是父函数里的变量它们是访问不到的,它们的"父"函数实际上是直接调用者,也就是window/global,所以它们只能访问全局变量或者自己的形参变量。)
  • 其它返回状态(Other return state):除了"返回地址"外,在某些环境下,当一个函数返回的时候,也许会需要存储某些其它的机器或者软件状态。比如优先级,异常处理信息,算法模式等等。如果有需要的话,这些信息会像返回地址一样被存储在调用栈里。


## 数据结构


### React Element

const element = {
  $$typeof: REACT_ELEMENT_TYPE, // react 内部的类型系统,与各种 Symbol 对应。ELEMENT || CALL || RETURN || PORTAL || FRAGMENT || STRICT_MODE || PROVIDER || CONTEXT || ASYNC_MODE || FORWARD_REF
  type: type, // 原生 html 标签,function,或者 react 内部定义的某些 symbol,如 Fragment
  key: key,
  ref: ref,
  props: props,
  _owner: ReactCurrentOwner.current,
  
  // dev mode
  _store: {
    validated: false
  }
  _self: null,
  _source: null // 文件名和行数
}
```

### React Fiber

一个 Fiber 就是一个在需要完成或者已经完成的组件上进行的任务,一个组件可能有多个 Fiber 


const fiber = {
  // Instance
  tag = tag; // fiber 的类型。 
    - IndeterminateComponent
    - FunctionalComponent
    - ClassComponent // Menu, Table
    - HostRoot // ReactDOM.render 的第二个参数
    - HostPortal
    - HostComponent // div, span
    - HostText // 纯文本节点,即 dom  的 nodeName 等于 '#text'
    - CallComponent // 对应 call return 中的 call
    - CallHandlerPhase // call 中的 handler 阶段
    - ReturnComponent // 对应 call return 中的 return
    - Fragment 
    - Mode // AsyncMode || StrictMode
    - ContextConsumer
    - ContextProvider
    - ForwardRef
  key = key; // fiber 的唯一标识
  type = null; // 与 react element 里的 type 一致
  stateNode = null; // 对应组件或者 dom 的实例

  // Fiber
  return = null; // 等价于栈帧中的函数调用后的返回地址,这里即是父 fiber
  child = null; // 即组件的 render 的返回值,是一个单链表(因为返回值不一定是一个单一的元素)
  sibling = null; // 单链表
  index = 0;

  ref = null;

  // props 等价于一个函数的 arguments
  pendingProps = pendingProps; // 新的 props(要么是当前的 props,要么是 wip 的 props),默认就等于 element.props,对于 Fragment 和 Portal,则等于 props.children
  memoizedProps = null; // 旧的 props,等于 wipFiber.pendingProps 或者 wipFiber.pendingProps.children
    - 一般 oldProps = workInProgress.memoizedProps
    - 一般 newProps = workInProgress.pendingProps
  updateQueue = null; // 状态更新和回调的函数队列
  memoizedState = null; // 组件实例的 state

  mode = mode; // 用于描述处理 fiber 和它的子树的方式,创建后就不应被改变,如未指定则从父 fiber 继承。NoContext || AsyncMode || StrictMode

  // Effects
  effectTag = NoEffect; // 当需要变化的时候,具体需要进行的操作的类型
    - NoEffect // 初始值
    - PerformedWork // 开始处理后置为 PerformedWork
    - Placement // 插入,保持原位,移动 dom 节点
    - Update // 对 dom 结构的改变。mount 或者 update 后置为 Update
    - PlacementAndUpdate
    - Deletion
    - ContentReset // 将一个只包含字符串的 dom 节点,或者 textarea,或者 dangerouslySetInnerHTML 替换成其它类型的节点
    - Callback // setState 的回调
    - DidCapture // 渲染出错,准备捕获出错信息
    - Ref // 准备执行 ref 回调
    - ErrLog // 渲染出错,执行 componentDidCatch
    - Snapshot // getSnapshotBeforeUpdate
    - HostEffectMask // 暂时没用
    - Incomplete // 任何造成 fiber 的工作无法完成的情况
    - ShouldCapture //  需要处理错误
    
  nextEffect = null; // 下一个需要处理的有副作用的 fiber

  // 本 fiber 的子树中有副作用的第一个和最后一个 fiber
  firstEffect = null; // 
  lastEffect = null; //

  expirationTime = NoWork; // 将来的某个时间点,在那之前必须完成所有工作

  alternate = null; // WIP 树里面的 fiber,如果不在更新期间,那么就等于当前的 fiber,如果是新创建的节点,那么就没有 alternate
  
  // dev mode
  _debugID; // 自增的标识每一个 fiber 的 id
  _debugSource = null; // 文件名和行数,与 React Element 的 source 一致
  _debugOwner = null; // 与 React Element 的 owner 一致
  _debugIsCurrentlyTiming = false;
}


### React Element 到 React Fiber 的转换


  • createWorkInProgress
  • createHostRootFiber
  • createFiberFromElement
  • createFiberFromFragment
  • createFiberFromText
  • createFiberFromHostInstanceForDeletion
  • createFiberFromPortal
  • assignFiberPropertiesInDEV


### 调度算法是怎样演进的?



基于的假设:更优先级的任务或者说更新执行的时间比低优先级的**更短**


https://github.com/facebook/react/pull/10715:


所以在处理低优先级任务之前,先尽可能的处理完高优先级任务是比较好的。所以高优先级的任务会被 batch,即便这个任务在队列里面处于一个低优先级之后。但是这样就会出现一个问题(因为我们不是按照任务的顺序来执行的,我们跳着执行了),这样可能会导致最终的 state 不一致。


所以采用的解决办法是:按照代码执行的顺序往更新队列里插入任务(而不是按照任务的优先级不断调整队列里面任务的顺序,也就是说这就是一个普通的 list,而不是优先队列),然后每次执行队列的时候,,**跳过**优先级「不高」的任务。同时即便队列里某个任务被执行了,如果在它的**前面**还有**未被执行**的任务,那么这个任务也不会从队列里移除(为了确保 state 的确定性)。在这种情况下,我们需要追踪那些没有被执行的任务的 state,其中的第一个作为我们的 baseState



为什么有的声明周期钩子可能被调用多次?


正如上面所说,正因为需要确保最终 state 的确定性,我们并没有从更新队列里面移除某些已经被执行的任务,所以在下一次执行队列的时候,它们仍然会被执行,从而使最终 state 是确定的


同时,也是为了探测不该有副作用的钩子中是否出现了副作用(这个探测只在开发模式下有)


具体哪些会被调用多次?


ExpirationTime Time


一个 expiration time 代表将来的某个时间点,在那个时间点我们需要 flush。一个任务的优先级取决于当前的时间和 expiration time 之间的差值。随着时间的推移,会增加任务的优先级来防止出现饥饿。


  • NoWork
  • Sync
  • Never // 暂时(甚至永远)不应该去更新的任务,比视窗外的任务


过期时间越久,代表任务的优先级更低


react 会批处理一定时间范围内的同一 fiber 上的同一优先级的任务,超出过期时间后,会将这部分任务 flush,但是接近或者超出过期时间提交的任务不会被 flush


React-reconfilier and React-schualer


Async render


最开始官方的[想法](API for prerendering a top-level update and deferring the commit by acdlite · Pull Request #11346 · facebook/react)是提供一个 `prerender`的方法,但是这种 api 有个问题,root 之间会相互影响,prerender 也没办法在不同的 root 互不影响的情况下让某一个能多次 prerender。


const root = ReactDOM.createRoot(container);

const work1 = root.prerender('a');
work1.then(onComplete1);

const work2 = root.prerender('b');
work2.then(onComplete2);

// Flush all work up to and including work2
work2.commit();
// Should onComplete1 have fired? Should work1 be aborted? Unclear.


新的 `createBatch` api 一是能实现创建出来的不同的 batch 在 commit 的时候互不影响,二是对于同一个 batch 的多次 render 能够实现 batch 化:


const batch1 = root.createBatch();
batch1.render('a');
batch1.then(onComplete1);

const batch2 = root.createBatch();
batch2.render('b');
batch2.then(onComplete2);
// Update again at the same expiration time
batch2.render('c');

// Both `b` and `c` are flushed
// onComplete2 fires, but not onComplete1
batch2.commit();


`ReactDOM.createRoot(container).render` 和 `ReactDOM.createRoot(container).createBatch().render` 的区别在于,前者在 `flush` 之后,`work`(即 `render` 方法的返回值)的 `work.then` 回调会立即被调用并且更新已经被应用到了 dom 上,而后者必须调用 `batch.commit()` 之后更新才会被应用到 dom 上


下面的不确定,记得改!!!!!!


`flush()` 的作用是把 wip tree 替换成 current tree,并且在非 creatBatch 情况下会自动 commit,然后自然就会触发 `batch.then` 或者 `work = root.render` 返回的 `work.then` 回调


`batch.commit` 的作用是触发 `work = batch.render` 返回的 `work.then` 回调



Tip: `work = root.rener or batch.render`


**没有**调用 `expire(xxx)` 的情况下的执行顺序:

`React.createRoot -> root.render -> call flush at some time -> component render -> dom update -> work.then `


`React.createRoot.createBatch -> batch.render -> call flush at some time -> component render -> batch.then -> call batch.commit at some time -> dom update -> work.then`


需要注意:

  • 一个 batch 在commit 的过程中不能再次被 commit
  • flush 和 commit 都会触发组件的 render(调用 commit 的时候如果组件已经 render 过了,则不会再去 render),但是按理应该用 flush 来去渲染,用 commit 去应用更新到 dom


既有 `root.render`,又有 `batch.render` 的情况下,组件的 render 以 `batch.render` 为准(即不会去调用 `root.render` 里的组件 方法),除此之外,`root.render` 的返回值 `const work = root.render` 的回调 `work.then` 也不再是通过 `flush()` 来触发,而是通过 `batch.commit()` 来触发,且是在 `const work2 = batch.render` 的 `work2.then` 的回调被触发之后触发:


`root.render; batch.render -> call flush at some time -> component of batched render -> batch.then -> call batch.commit at some time -> dom update -> root.render.then -> batch.render.then`


调用了 `expire(xxx)` 的情况下的执行顺序:



所以,要想 `root.render` 的组件的渲染能够执行(而不是以 `batch.remder` 为准),有两种方式,一是多加一次 `flush` 在 `root.render` 后面,二是使用足够大的 `expire(ms)` ,让这两次 `render` 分离开来


测试1


const root = ReactDOM.unstable_createRoot(container);
    console.log(1111, container.textContent)
    const work = root.render(<div>Hi</div>); // 或者 root.render(<AsyncMode>Hi</AsyncMode>) 也一样
    console.log(2222, container.textContent)
    work.then(() => {
      console.log(3333, container.textContent)
    });
    console.log(4444, container.textContent)
    flush();
    console.log(5555, container.textContent)
    expect(container.textContent).toEqual('Hi');
    
    // 结果
    1111 ''

    2222 ''

    4444 ''

    3333 'Hi'

    5555 'Hi'


测试2

const root = ReactDOM.unstable_createRoot(container);
    console.log(1111, container.textContent)
    const batch = root.createBatch();
    const work = batch.render(<div>Hi</div>); // 或者 batch.render(<AsyncMode>Hi</AsyncMode>) 也一样
    console.log(2222, container.textContent)
    work.then(() => {
      console.log(3333, container.textContent)
    });
    console.log(4444, container.textContent)
    flush();
    console.log(5555, container.textContent)
    expect(container.textContent).toEqual('Hi');
    
    // 结果
    1111 ''

    2222 ''

    4444 ''

    5555 ''


测试3

const root = ReactDOM.unstable_createRoot(container);
    console.log(1111, container.textContent)
    const batch = root.createBatch();
    const work = batch.render(<div>Hi</div>); // 或者 batch.render(<AsyncMode>Hi</AsyncMode>) 也一样
    console.log(2222, container.textContent)
    work.then(() => {
      console.log(3333, container.textContent)
    });
    console.log(4444, container.textContent)
    batch.commit();
    console.log(5555, container.textContent)
    expect(container.textContent).toEqual('Hi');
    
    // 结果
    1111 ''

    2222 ''

    4444 ''
    
    3333 'Hi'

    5555 'Hi'

#### 测试4


#### 测试5


值得注意的是,虽然 api 用了 `.then` 的风格,但是其还是一个普通的同步的回调,并不是 `promise`,所以 event loop 的顺序需要注意下不要被误导了。



Error Boundary


Error Boundary 在 16.3 [被重写了](Capture and recover from errors within a single render phase by acdlite · Pull Request #11901 · facebook/react),重写后 Error Boundary 与 Suspense 共用一部分逻辑(因为它们本质上都是从某种状态恢复过来,即,先是某一种状态,然后出现某种情况导致无法继续渲染,然后问题解决后又需要继续渲染)。


重构后:

  • 当一个组件 throw 的时候,会[继续](Add stack unwinding phase for handling errors by acdlite · Pull Request #12201 · facebook/react)渲染它的兄弟组件,而不是立即跳出堆栈到最近的 Error Boundary,具体的逻辑**貌似**为:
  • - 如果是初始渲染的时候有组件 throw 了,那么先按正常渲染,然后重新从 `constructor` 开始调用或者说渲染 ErrorBoundary 里面的除了有 throw 的组件外的其它组件(注意没有 unmount 过程),所以最后的结果应该是所有组件都会渲染到页面上,只是出错的组件变成了自己设置的更友好的组件如「加载失败」之类的组件
  • - 如果初始渲染的时候没有组件 throw,而是随后的一些操作引起了 throw,那么更新的时候,先按正常渲染,然后重新 render ErrorBoundary 里面的除了有 throw 的组件外的其它组件(注意和上面的区别,这里只有 render,并不会从 `constructor` 开始进行),同时**最后还会 unmount** 除了有 throw 的组件外的其它组件
  • 如果 throw 的不是 Error 对象,那么这个「错误」会被冒泡到根节点,从而使整个 root unmount (当然这里不考虑 Suspense)
  • 添加 `getDerivedStateFromCatch` 钩子,让 recover/retry 机制在 render 阶段执行,而不是之前的 commit 阶段。之后 cDC 只建议用来打印日志
  • 不再追踪 Error Boundary 里面出现的错误,所以有可能出现死循环


ComponentDidCatch 会被调用多次吗


[会](Add stack unwinding phase for handling errors by acdlite · Pull Request #12201 · facebook/react),因为同一个 Error Boundary 下的兄弟节点抛出的错误都会触发这个 Error Boundary 的 cDC


为什么在 cWU 里面 throw,cWU 会被调用多次


参考[这里](Add stack unwinding phase for handling errors by acdlite · Pull Request #12201 · facebook/react),因为第一次是正常的 react 的更新过程,某个之前 render 的组件不在 render 方法中出现了,自然就会被卸载。而第二次是 Error Boundary 自己的机制,即 Error Boundary 会去卸载抛出错误的组件。`componentDidCatch` 会在第二次 cWU 被调用后执行


Suspense


如何评价React的新功能Time Slice 和Suspense? - Heaven的回答 - 知乎

zhihu.com/question/2680


事件


事件拆分


[拆分](Interactive updates by acdlite · Pull Request #12100 · facebook/react)后事件类型由原来的两种变成了三种,主要是将顶层事件拆成了**交互事件**(Interactive events)和**非交互事件**(Non-interactive events)


  • Controlled events. Updates are always flushed synchronously. 即 dispatchEvent 之后组件和 dom 都会同步立即更新
  • Interactive events. Updates are async, unless a subsequent event is fired before it can complete, as described above. They are also slightly higher priority than a normal async update. dispatchEvent 之后组的 handler 会被立即调用,但是会**异步的**调用 render 方法,自然 dom 也不会立即更新
  • Non-interactive events. These are treated as normal, low-priority async updates.


root.render 的话,如果 click 了,组件的更新是异步的,而如果是 ReactDOM.render 的话,click 之后组件的更新是同步的


生命周期


Commit 阶段会改变 DOM. 这些操作通常非常快,Reconcile 阶段会调用 render 方法,找出需要被渲染的东西,调用下一个 render 方法等等。[Twitter](twitter.com/dan_abramov )


commit 阶段的具体行为甚至可以与第三方交互:gist.github.com/NE-Smal


Render 阶段


Commit 阶段


钩子函数


getDerivedStateFromProps


为什么要设计成静态的?


`prevProps` in `getDerivedStateFromProps()` by catamphetamine · Pull Request #40 · reactjs/rfcs


- As Sebastian recently said, there are rules to React. At times they may feel limiting, but following them allows React to do many complex things for you (even more so with upcoming async features).

You can break these rules, but if you do- React will likely not work like it's supposed to, and you'll potentially lose a lot of time debugging and investigating why.


写法没有太大变化也确实存在哈哈,不过他们觉得本身这些 api 就不应该有太多场景去用,所以你用到的时候写点模板代码不是啥问题


另外 prevProps 那个还有三个原因:

一是初次渲染的时候,prevProps 是 undefined/null,这样你得做一次 empty check,太麻烦了。。而且与已有的 cDU 的 prevProps 表现形式不一致。

二是将状态变化和昂贵操作区分开,更加便于 render 和 commit 阶段操作或者说优化。

三是没有 prevProps,react 可以不保留所有组件的 prevProps,让它们被 GC 掉。

> 以确保当开发者用到 getDerivedStateFromProps 这个生命周期函数时,就是在根据当前的 props 来更新组件的 state,而不是去做其他一些让组件自身状态变得更加不可预测的事情。

感觉这里强调一下**当前**更好一点,如果不是 static 或者是 cWR 的话,自己给 this 瞎改东西,到时候出现状态不一致就知道后悔了哈哈。总的来说还是 async safe/一致性是最主要的原因,而且是必须要解决的,所以也是为什么不允许新旧 api 同时使用


执行过程


ReactDOM.render(element, domContainer, callback)


  • 非 Hydrate 模式下移除 container 里面的所有节点


domContainer._reactRootContainer 等于下面的 ReactRoot

ReactDOM.render 的返回值为 ReactRoot = {
    _internalRoot: {
        current, // 就是普通的 fiber,tag 为 HostRoot 而已
        containerInfo, // domContainer
        pendingChildren: null,
        pendingCommitExpirationTime: NoWork,
        finishedWork: null,
        context: null,
        pendingContext: null,
        hydrate,
        remainingExpirationTime: NoWork,
        firstBatch: null,
        nextScheduledRoot: null,
        stateNode // _internalRoot本身
    },
    render: function,
    unmount: function,
    legacy_renderSubtreeIntoContainer: function,
    createBatch: function
}


Renderer


个人认为,把 renderer 单独抽象出来,除了适配多端以外,另外我感觉也是极大的方便了测试,测试的时候其实并不需要关心 dom 层面的东西,需要测试的是最终的 vdom 对象,state 等的值是否和真实情况一致,所以用 ReactNoopRenderer 就特别方便


DOMRenderer


DOMRenderer = {
    getRootHostContext, //
    getChildHostContext, //
    getPublicInstance, //
    prepareForCommit, //
    resetAfterCommit, //
    createInstance, //
    appendInitialChild, //
    finalizeInitialChildren, //
    prepareUpdate, //
    shouldSetTextContent, //
    shouldDeprioritizeSubtree, //
    createTextInstance, //
    now, //
    mutation = {
        commitMount, //
        commitUpdate, //
        resetTextContent, //
        commitTextUpdate, //
        appendChild, //
        appendChildToContainer, //
        insertBefore, //
        insertInContainerBefore, //
        removeChild, //
        removeChildFromContainer, //
    },
    hydration: {
        canHydrateInstance, //
        canHydrateTextInstance, //
        getNextHydratableSibling, //
        getFirstHydratableChild, //
        hydrateInstance, //
        hydrateTextInstance, //
        didNotMatchHydratedContainerTextInstance, //
        didNotMatchHydratedTextInstance, //
        didNotHydrateContainerInstance, //
        didNotHydrateInstance, //
        didNotFindHydratableContainerInstance, //
        didNotFindHydratableContainerTextInstance, //
        didNotFindHydratableInstance, //
        didNotFindHydratableTextInstance, //
    },
    scheduleDeferredCallback, //
    cancelDeferredCallback //
}


Reconciler


ReactFiberReconciler


Scheduler


ReactFiberScheduler



Sync VS Async


Demo

React Fiber Time Slicing Sample


React App




最后再说说 SSR Suspense 吧,acd 演讲之后留意了下,好像国内没有看到有相关的讨论。



https://www.pscp.tv/w/1BRKjrvoyYZKwwww.pscp.tv




第一点:传统的做法是先用 Promise.all 发出所有请求,然后必须等到最慢点一个请求返回数据后,再 renderToString 等进行渲染。这是很浪费时间的,因为就像在之前的文章里面我说过的,浏览器都会利用 TTFB 进行优化,为什么 SSR 不行呢。 suspense 之前的版本确实是不行的,因为 react 不支持 renderToString 中的异步行为。而现在支持(用新的 rederToNodeStream from 'react-dom/server.suspense')了以后,配合已经开发完成(虽然还没 merge)的 client suspense api,就能够做到。即,在组件里面发起请求,然后继续渲染,碰到有 throw 的,就按 client suspense 一样的流程处理。所以节省了大量时间(没有 throw 的组件的渲染,有 throw 的组件的创建,协调过程等)

第二,三,四点: 和之前的 client suspense 一致(废话。。都是用的 simple-cache-provider 和 TimeOut fiber)


第一点:上面已经说过了,不再解释。可以用图表示为:


现状

第一点:以前需要 PlaceHolder 一直转一直转,要么就是 flash(一闪),原因都是因为渲染(指 react 的渲染/render 工作,不是指浏览器的渲染)和请求数据是串行的,而现在可以并行了。

第二点:需要自己先把首屏的请求拧出来提升到整个渲染逻辑之前执行,这两点在上面都详细说了。

第三点:这点其实上面也说了,以前的 SSR 渲染过程不能有异步任务,虽然有状态但是实际没用(因为无法进行 setState),同时也不会执行除了 cWM(现在应该是 gDSFP)之外的其它钩子。所以很难进行封装,因为和 client 逻辑完全不一致,能服用的有限,所以需要单独为此搞个 SSR 框架来做(即图里的封装困难)。

这条 tweet twitter.com/acdlite/sta 非常详细的解释了。



其它的改动还比如 React Profile API, render 函数静态化,class component 和 (去除)functional component 大统一, 等等暂时也没时间研究了。。另外还有一些新的思考,争取这两天也一并先发一下。


为什么有那么多疑问还没有弄清楚就要发呢?这个问题也犹豫了很久,最后还是觉得,最近都在搞毕设,实在是抽不出时间了,存着也是存着。另一方面,虽然瞎写了这么多内,如果能够给其它也在研究的朋友带来一点点参考价值,也就满足了。


以上内容基本都参考自 react repo 以及相关成员的 github, twitter, youtube。

编辑于 2018-05-16

文章被以下专栏收录