从零实现 React v18,但 WASM 版 - [15] 实现 useEffect

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

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

本文对应 tag:v15

本次更新详见这里,下面来过一遍整个流程。

useState 一样,首先需要在 react 包中导出这个方法,它接收两个参数:

rust 复制代码
#[wasm_bindgen(js_name = useEffect)]
pub unsafe fn use_effect(create: &JsValue, deps: &JsValue) {
    let use_effect = &CURRENT_DISPATCHER.current.as_ref().unwrap().use_effect;
    use_effect.call2(&JsValue::null(), create, deps);
}

然后,我们需要分别为首次渲染和更新实现 mount_effectupdate_effect。其中 mount_effect 会给 FiberNode 的 Hook 链表上新增一个 Hook 节点,其 memoized_state 属性指向一个 Effect 对象,同时这个对象还会被加入 FiberNode 上的 update_queue, 它是一个环形队列。另外,FiberNode 还被被标记为 PassiveEffect

update_effect 的工作与 mount_effect 类似,会更新 Effect 节点,不过他会把传入的 deps 与之前的 prev_deps 中的元素依次进行浅比较,如果全部相同就不会给 FiberNode 标记 PassiveEffect

Effect 中的属性包含这些:

rust 复制代码
pub struct Effect {
  pub tag: Flags, // 副作用标记
  pub create: Function, // 传入 useEffect 的第一个参数,必须为函数
  pub destroy: JsValue, // useEffect 的第一个参数执行后返回的函数
  pub deps: JsValue, // 出入 useEffect 的第二个参数
  pub next: Option<Rc<RefCell<Effect>>>,
}

接着,Render 阶段不需要更改,Commit 阶段我们需要在 commit_mutation_effects 前新增处理 useEffect 的逻辑:

rust 复制代码
// useEffect
let root_cloned = root.clone();
let passive_mask = get_passive_mask();
if flags.clone() & passive_mask.clone() != Flags::NoFlags
    || subtree_flags.clone() & passive_mask != Flags::NoFlags
{
    if unsafe { !ROOT_DOES_HAVE_PASSIVE_EFFECTS } {
        unsafe { ROOT_DOES_HAVE_PASSIVE_EFFECTS = true }
        let closure = Closure::wrap(Box::new(move || {
            flush_passive_effects(root_cloned.borrow().pending_passive_effects.clone());
        }) as Box<dyn Fn()>);
        let function = closure.as_ref().unchecked_ref::<Function>().clone();
        closure.forget();
        unstable_schedule_callback_no_delay(Priority::NormalPriority, function);
    }
}

这里,我们使用上一篇文章实现的 scheduler 来调度一个任务执行 flush_passive_effects 方法:

rust 复制代码
fn flush_passive_effects(pending_passive_effects: Rc<RefCell<PendingPassiveEffects>>) {
    unsafe {
        if EXECUTION_CONTEXT
            .contains(ExecutionContext::RenderContext | ExecutionContext::CommitContext)
        {
            log!("Cannot execute useEffect callback in React work loop")
        }

        for effect in &pending_passive_effects.borrow().unmount {
            CommitWork::commit_hook_effect_list_destroy(Flags::Passive, effect.clone());
        }
        pending_passive_effects.borrow_mut().unmount = vec![];

        for effect in &pending_passive_effects.borrow().update {
            CommitWork::commit_hook_effect_list_unmount(
                Flags::Passive | Flags::HookHasEffect,
                effect.clone(),
            );
        }
        for effect in &pending_passive_effects.borrow().update {
            CommitWork::commit_hook_effect_list_mount(
                Flags::Passive | Flags::HookHasEffect,
                effect.clone(),
            );
        }
        pending_passive_effects.borrow_mut().update = vec![];
    }
}

这里的 pending_passive_effectsFiberRootNode 上的一个属性,用于保存此次需要执行的 Effect

rust 复制代码
pub struct PendingPassiveEffects {
    pub unmount: Vec<Rc<RefCell<Effect>>>,
    pub update: Vec<Rc<RefCell<Effect>>>,
}

其中,因为组件卸载需要处理的 Effect 保存在 unmount 中,因为更新需要处理的 Effect 保存在 update 中。从代码中看到,这里会先处理因组件卸载需要处理的 Effect,即使这个组件顺序比较靠后,比如这个例子:

js 复制代码
function App() {
  const [num, updateNum] = useState(0)
  return (
    <ul
      onClick={(e) => {
        updateNum((num: number) => num + 1)
      }}>
      <Child1 num={num} />
      {num === 1 ? null : <Child2 num={num} />}
    </ul>
  )
}
function Child1({num}: {num: number}) {
  useEffect(() => {
    console.log('child1 create')
    return () => {
      console.log('child1 destroy')
    }
  }, [num])
  return <div>child1 {num}</div>
}

function Child2({num}: {num: number}) {
  useEffect(() => {
    console.log('child2 create')
    return () => {
      console.log('child2 destroy')
    }
  }, [num])
  return <div>child2 {num}</div>
}

点击后会先执行 Child2useEffectdestroy,打印 child2 destroy。而如果换成这样:

js 复制代码
function App() {
  const [num, updateNum] = useState(0)
  return (
    <ul
      onClick={(e) => {
        updateNum((num: number) => num + 1)
      }}>
      <Child1 num={num} />
      <Child2 num={num} />
    </ul>
  )
}

点击后会先执行 Child1useEffectdestroy,打印 child1 destroy

pending_passive_effects 里面的 Effect 是什么时候加进去的呢?答案是在 commit_mutation_effects 中,有两种情况:

  1. 如果 FiberNode 节点被标记需要删除且为 FunctionComponent 类型,则需要把 update_queue 中的 Effect 加入 pending_passive_effects 中的 unmount 列表中。
rust 复制代码
fn commit_deletion(
    &self,
    child_to_delete: Rc<RefCell<FiberNode>>,
    root: Rc<RefCell<FiberRootNode>>,
) {
  let first_host_fiber: Rc<RefCell<Option<Rc<RefCell<FiberNode>>>>> =
      Rc::new(RefCell::new(None));
  self.commit_nested_unmounts(child_to_delete.clone(), |unmount_fiber| {
      let cloned = first_host_fiber.clone();
      match unmount_fiber.borrow().tag {
          WorkTag::FunctionComponent => {
              CommitWork::commit_passive_effect(
                  unmount_fiber.clone(),
                  root.clone(),
                  "unmount",
              );
          }
          ...
      }
  }
}
  1. 如果 FiberNode 节点被标记为 PassiveEffect,则需要把 update_queue 中的 Effect 加入 pending_passive_effects 中的 update 列表中。
rust 复制代码
if flags & Flags::PassiveEffect != Flags::NoFlags {
  CommitWork::commit_passive_effect(finished_work.clone(), root, "update");
  finished_work.borrow_mut().flags -= Flags::PassiveEffect;
}

大致流程介绍完毕,更多细节请参考这里

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

相关推荐
姜学迁1 小时前
Rust-枚举
开发语言·后端·rust
凌云行者1 小时前
rust的迭代器方法——collect
开发语言·rust
zqx_76 小时前
随记 前端框架React的初步认识
前端·react.js·前端框架
QMCY_jason8 小时前
Ubuntu 安装RUST
linux·ubuntu·rust
碳苯12 小时前
【rCore OS 开源操作系统】Rust 枚举与模式匹配
开发语言·人工智能·后端·rust·操作系统·os
zaim114 小时前
计算机的错误计算(一百一十四)
java·c++·python·rust·go·c·多项式
凌云行者1 天前
使用rust写一个Web服务器——单线程版本
服务器·前端·rust
TonyH20021 天前
webpack 4 的 30 个步骤构建 react 开发环境
前端·css·react.js·webpack·postcss·打包
掘金泥石流1 天前
React v19 的 React Complier 是如何优化 React 组件的,看 AI 是如何回答的
javascript·人工智能·react.js