likes
comments
collection
share

从零实现 React v18,但 WASM 版 - [24] 实现 Suspense(一):渲染 Fallback

作者站长头像
站长
· 阅读数 27

模仿 big-react,使用 Rust 和 WebAssembly,从零实现 React v18 的核心功能。深入理解 React 源码的同时,还锻炼了 Rust 的技能,简直赢麻了!

代码地址:github.com/ParadeTo/bi…

本文对应 tag:v24

Suspense 无疑是新版 react 中最吸引人的一个特性,所以我们也来实现一下。本文是第一部分,实现 Suspense 的 Fallback 渲染。

以下面代码为例:

import {Suspense} from 'react'

export default function App() {
  return (
    <Suspense fallback={<div>loading</div>}>
      <Child />
    </Suspense>
  )
}

function Child() {
  throw new Promise((resolve) => setTimeout(resolve, 1000))
}

对于 Suspense 节点来说,他有两条子分支,分别对应 PrimaryFallback,其中 Primary 分支的根节点为 Offscreen 类型的节点,Fallback 分支的根节点为 Fragment 类型的节点:

从零实现 React v18,但 WASM 版 - [24] 实现 Suspense(一):渲染 Fallback

具体到上面的例子则为:

从零实现 React v18,但 WASM 版 - [24] 实现 Suspense(一):渲染 Fallback

首次渲染时,会进入 Primary 分支,当处理到 Child 组件时,由于该组件抛出了 Promise 对象,开始进入 unwind 流程,该流程会往上找到最近的 Suspense 节点,并添加 DidCapture 的标记,接着从该节点继续 render 流程。

这次因为 Suspense 节点上有 DidCapture 标记,所以会进入 Fallback 分支,接下来就是正常的 render 和 commit 流程,最终渲染出 Fallback 中的内容。

这就是本次我们要实现的功能,下面来简单过一下代码。

首先,还是看看 begin_work.rs,需要新增对于 Suspense 的处理:

fn update_suspense_component(
    work_in_progress: Rc<RefCell<FiberNode>>,
) -> Option<Rc<RefCell<FiberNode>>> {
    let current = { work_in_progress.borrow().alternate.clone() };
    let next_props = { work_in_progress.borrow().pending_props.clone() };

    let mut show_fallback = false;
    let did_suspend =
        (work_in_progress.borrow().flags.clone() & Flags::DidCapture) != Flags::NoFlags;

    if did_suspend {
        show_fallback = true;
        work_in_progress.borrow_mut().flags -= Flags::DidCapture;
    }

    let next_primary_children = derive_from_js_value(&next_props, "children");
    let next_fallback_children = derive_from_js_value(&next_props, "fallback");
    push_suspense_handler(work_in_progress.clone());

    if current.is_none() {
        if show_fallback {
            return Some(mount_suspense_fallback_children(
                work_in_progress.clone(),
                next_primary_children.clone(),
                next_fallback_children.clone(),
            ));
        } else {
            return Some(mount_suspense_primary_children(
                work_in_progress.clone(),
                next_primary_children.clone(),
            ));
        }
    } else {
        if show_fallback {
            return Some(update_suspense_fallback_children(
                work_in_progress.clone(),
                next_primary_children.clone(),
                next_fallback_children.clone(),
            ));
        } else {
            return Some(update_suspense_primary_children(
                work_in_progress.clone(),
                next_primary_children.clone(),
            ));
        }
    }
}

这里,根据当前是否显示 Fallback 以及是否为首次更新分为了四个分支来处理。

接下来,看看 work_loop.rs

loop {
  unsafe {
      if WORK_IN_PROGRESS_SUSPENDED_REASON != NOT_SUSPENDED && WORK_IN_PROGRESS.is_some() {
          // unwind process
          ...
      }
  }
  match if should_time_slice {
      work_loop_concurrent()
  } else {
      work_loop_sync()
  } {
      Ok(_) => {
          break;
      }
      Err(e) => handle_throw(root.clone(), e),
  };
}

当组件中抛出异常时,会进入 Err 的分支,这里主要是增加了 handle_throw 流程,目前比较简单:

fn handle_throw(root: Rc<RefCell<FiberRootNode>>, thrown_value: JsValue) {
    unsafe {
        WORK_IN_PROGRESS_SUSPENDED_REASON = SUSPENDED_ON_DATA;
        WORK_IN_PROGRESS_THROWN_VALUE = Some(thrown_value);
    }
}

接着,循环继续,进入 unwind 流程:

loop {
  unsafe {
      if WORK_IN_PROGRESS_SUSPENDED_REASON != NOT_SUSPENDED && WORK_IN_PROGRESS.is_some() {
          let thrown_value = WORK_IN_PROGRESS_THROWN_VALUE.clone().unwrap();

          WORK_IN_PROGRESS_SUSPENDED_REASON = NOT_SUSPENDED;
          WORK_IN_PROGRESS_THROWN_VALUE = None;

          throw_and_unwind_work_loop(
              root.clone(),
              WORK_IN_PROGRESS.clone().unwrap(),
              thrown_value,
              lane.clone(),
          );
      }
  }
  ...
}
fn throw_and_unwind_work_loop(
    root: Rc<RefCell<FiberRootNode>>,
    unit_of_work: Rc<RefCell<FiberNode>>,
    thrown_value: JsValue,
    lane: Lane,
) {
    unwind_unit_of_work(unit_of_work);
}

这里的任务就是往上找到最近的 Suspense 节点,并标记 DidCapture

到这我们的任务就完成了,不过为了给下一篇文章多铺点路,我们再来多实现一点功能。

还是以上面代码为例,首次渲染处理到 Child 组件时,应该要捕获到其抛出的 Promise 对象,并调用它的 then 方法,然后在传入的函数中触发重新渲染的逻辑。

这样,当 Promise 对象状态变成 fullfilled 后,会再次进入 render 流程,此时处理到 Child 组件仍然会抛出异常,结果就是不停重复上面的流程,不过没关系,我们暂时不处理,因为目前我们还没有实现 use hook,暂时只能这样来测试。

我们来看看怎么捕获 Promise 对象并在对象 fullfilled 时,重新开启渲染流程:

首先,我们在 throw_and_unwind_work_loop 中添加 throw_exception 方法:

fn throw_and_unwind_work_loop(
    root: Rc<RefCell<FiberRootNode>>,
    unit_of_work: Rc<RefCell<FiberNode>>,
    thrown_value: JsValue,
    lane: Lane,
) {
    throw_exception(root.clone(), thrown_value, lane.clone());
}

fn attach_ping_listener(root: Rc<RefCell<FiberRootNode>>, wakeable: JsValue, lane: Lane) {
    let then_value = derive_from_js_value(&wakeable, "then");
    let then = then_value.dyn_ref::<Function>().unwrap();
    let closure = Closure::wrap(Box::new(move || {
        root.clone().borrow_mut().mark_root_updated(lane.clone());
        ensure_root_is_scheduled(root.clone());
    }) as Box<dyn Fn()>);
    let ping = closure.as_ref().unchecked_ref::<Function>().clone();
    closure.forget();
    then.call2(&wakeable, &ping, &ping)
        .expect("failed to call then function");
}

pub fn throw_exception(root: Rc<RefCell<FiberRootNode>>, value: JsValue, lane: Lane) {
    if !value.is_null()
        && type_of(&value, "object")
        && derive_from_js_value(&value, "then").is_function()
    {
        let suspense_boundary = get_suspense_handler();
        if suspense_boundary.is_some() {
            let suspense_boundary = suspense_boundary.unwrap();
            suspense_boundary.borrow_mut().flags |= Flags::ShouldCapture;
        }

        attach_ping_listener(root, value, lane)
    }
}

其中 ping 函数就是传入 then 的函数,核心逻辑就是把当前的 lane 作为下次更新的优先级,并调用 ensure_root_is_scheduled 开启新的更新。不过测试发现,这样还不够,因为 begin_work.rs 中性能优化的功能会从根节点开始 bailout 掉这次更新,big-react 也有这个问题(切换到 master 分支并运行 suspense-use 这个例子可以复现,详见 issue)。

解决这个问题,权宜之计是在 unwind 流程之前,把更新优先级再网上冒泡一次,这样当再次从根节点开始更新时,由于 subtree_flags 上的标记,就不会进入 bailout 的流程了。

loop {
    unsafe {
        if WORK_IN_PROGRESS_SUSPENDED_REASON != NOT_SUSPENDED && WORK_IN_PROGRESS.is_some() {
            let thrown_value = WORK_IN_PROGRESS_THROWN_VALUE.clone().unwrap();

            WORK_IN_PROGRESS_SUSPENDED_REASON = NOT_SUSPENDED;
            WORK_IN_PROGRESS_THROWN_VALUE = None;


            mark_update_lane_from_fiber_to_root(
                WORK_IN_PROGRESS.clone().unwrap(),
                lane.clone(),
            );

            ...
        }
    }
    ...
}

本次更新详见这里。跪求 star 并关注公众号“前端游”。

转载自:https://juejin.cn/post/7409271602199904291
评论
请登录