• 作者:老汪软件技巧
  • 发表时间:2024-09-07 11:05
  • 浏览量:

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

代码地址:/ParadeTo/bi…

本文对应 tag:v25

在 React 新版本中,Suspense 一个非常大的作用就是结合 use 来获取数据,今天我们来实现一下,本次改动见这里。

我们用这个例子来解释一下本次改动:

import {Suspense, use} from 'react'
const delay = (t) =>
  new Promise((resolve, reject) => {
    setTimeout(reject, t)
  })
const cachePool = []
function fetchData(id, timeout) {
  const cache = cachePool[id]
  if (cache) {
    return cache
  }
  return (cachePool[id] = delay(timeout).then(() => {
    return {data: Math.random().toFixed(2) * 100}
  }))
}
export default function App() {
  return (
    <Suspense fallback={<div>loadingdiv>}>
      <Child />
    Suspense>
  )
}
function Child() {
  const {data} = use(fetchData(1, 1000))
  return <span>{data}span>
}

我们先按照之前新增 hooks 的流程把相关代码都加上,最后会来到 fiber_hooks.rs:

fn _use(usable: JsValue) -> Result {
  if !usable.is_null() && type_of(&usable, "object") {
      if derive_from_js_value(&usable, "then").is_function() {
          return track_used_thenable(usable);
      } else if derive_from_js_value(&usable, "$$typeof") == REACT_CONTEXT_TYPE {
          return Ok(read_context(usable));
      }
  }
  Err(JsValue::from_str("Not supported use arguments"))
}

从代码可以看到 use 这个函数即可传入一个 Promise 对象,也可传入一个 Context 对象,这里暂时只讨论 Promise 对象,所以我们看看 track_used_thenable:

#[wasm_bindgen]
extern "C" {
    pub static SUSPENSE_EXCEPTION: JsValue;
}
pub fn track_used_thenable(thenable: JsValue) -> Result {
    ...
    unsafe { SUSPENDED_THENABLE = Some(thenable.clone()) };
    Err(SUSPENSE_EXCEPTION.__inner.with(JsValue::clone))
}

中间的部分先略过,最后会返回一个 Result 的变体 Err,里面的 payload 为 SUSPENSE_EXCEPTION,这个 SUSPENSE_EXCEPTION 会在构建的时候插入到结果之中:

const SUSPENSE_EXCEPTION = new Error(
  "It's not a true mistake, but part of Suspense's job. If you catch the error, keep throwing it out"
)

这里不直接返回 thenable 而是返回 SUSPENSE_EXCEPTION 是为了后续好区分用户代码抛出的异常和 react 自己的异常,我们真正关心的值存在 SUSPENDED_THENABLE 里面。

之后,会来到 work_loop.rs 这里:

loop {
    ...
    match if should_time_slice {
        work_loop_concurrent()
    } else {
        work_loop_sync()
    } {
        Ok(_) => {
            break;
        }
        Err(e) => handle_throw(root.clone(), e),
    };
}

这个 e 就是前面所说的 SUSPENSE_EXCEPTION,来看看 handle_throw 是怎么处理的:

fn handle_throw(root: Rc>, mut thrown_value: JsValue) {
    /*
        throw possibilities:
            1. use thenable
            2. error (Error Boundary)
    */
    if Object::is(&thrown_value, &SUSPENSE_EXCEPTION) {
        unsafe { WORK_IN_PROGRESS_SUSPENDED_REASON = SUSPENDED_ON_DATA };
        thrown_value = get_suspense_thenable();
    } else {
        // TODO
    }
    unsafe {
        WORK_IN_PROGRESS_THROWN_VALUE = Some(thrown_value);
    }
}

这里会判断异常是不是 SUSPENSE_EXCEPTION,如果是的,就把真正的值重新拿出来,这就跟前面说的对上了。

这个值最后会传给 throw_and_unwind_work_loop:

    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;
                // TODO
                mark_update_lane_from_fiber_to_root(
                    WORK_IN_PROGRESS.clone().unwrap(),
                    lane.clone(),
                );
                throw_and_unwind_work_loop(
                    root.clone(),
                    WORK_IN_PROGRESS.clone().unwrap(),
                    thrown_value,
                    lane.clone(),
                );
            }
        }
        ...
    }

这个我们上篇文章已经介绍过了,这里就不啰嗦了。我们再回到 track_used_thenable:

pub fn track_used_thenable(thenable: JsValue) -> Result {
    let status = derive_from_js_value(&thenable, "status");
    if status.is_string() {
      ...
    } else {
        Reflect::set(&thenable, &"status".into(), &"pending".into());
        let v = derive_from_js_value(&thenable, "then");
        let then = v.dyn_ref::().unwrap();
        let thenable1 = thenable.clone();
        let on_resolve_closure = Closure::wrap(Box::new(move |val: JsValue| {
            if derive_from_js_value(&thenable1, "status") == "pending" {
                Reflect::set(&thenable1, &"status".into(), &"fulfilled".into());
                Reflect::set(&thenable1, &"value".into(), &val);
            }
        }) as Box<dyn Fn(JsValue) -> ()>);
        let on_resolve = on_resolve_closure
            .as_ref()
            .unchecked_ref::()
            .clone();
        on_resolve_closure.forget();
        let thenable2 = thenable.clone();
        let on_reject_closure = Closure::wrap(Box::new(move |err: JsValue| {
            if derive_from_js_value(&thenable2, "status") == "pending" {
                Reflect::set(&thenable2, &"status".into(), &"rejected".into());
                Reflect::set(&thenable2, &"reason".into(), &err);
            }
        }) as Box<dyn Fn(JsValue) -> ()>);
        let on_reject = on_reject_closure
            .as_ref()
            .unchecked_ref::()
            .clone();
        on_reject_closure.forget();
        then.call2(&thenable, &on_resolve, &on_reject);
    }
}

这里首次进来会走 else,核心逻辑就是给 thenable 添加 on_resolve 和 on_reject 方法,修改它上面的 status,value 和 reason 属性。

等到 Promise 对象的状态不再是 pending 后,会触发重新渲染,当再次来到这个函数时,它的 status 上也有值了,此时会进入 if:

if status.is_string() {
  if status == "fulfilled" {
      return Ok(derive_from_js_value(&thenable, "value"));
  } else if status == "rejected" {
      return Err(derive_from_js_value(&thenable, "reason"));
  }
  ...
}

如果其状态为 filfilled,就返回 value 的值,否则抛出 reason 上的异常。

Suspense 结合 use hook 获取数据的实现就介绍到这,不过调试发现 bailout 的逻辑会影响该流程的正常工作,所以目前只能暂时注释掉这一部分的代码,后面有时间再来看看如何解决。

跪求 star 并关注公众号“前端游”。


上一条查看详情 +PromptBench库的强大评估与理解能力
下一条 查看详情 +没有了