从零实现 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 并关注公众号"前端游"。

相关推荐
MavenTalk12 分钟前
Move开发语言在区块链的开发与应用
开发语言·python·rust·区块链·solidity·move
fighting ~1 小时前
react17安装html-react-parser运行报错记录
javascript·react.js·html
老码沉思录1 小时前
React Native 全栈开发实战班 - 列表与滚动视图
javascript·react native·react.js
老码沉思录1 小时前
React Native 全栈开发实战班 - 状态管理入门(Context API)
javascript·react native·react.js
老码沉思录4 小时前
写给初学者的React Native 全栈开发实战班
javascript·react native·react.js
老码沉思录4 小时前
React Native 全栈开发实战班 - 第四部分:用户界面进阶之动画效果实现
react native·react.js·ui
奔跑草-10 小时前
【前端】深入浅出 - TypeScript 的详细讲解
前端·javascript·react.js·typescript
林太白17 小时前
❤React-React 组件通讯
前端·javascript·react.js
豆华18 小时前
React 中 为什么多个 JSX 标签需要被一个父元素包裹?
前端·react.js·前端框架
前端熊猫18 小时前
React第一个项目
前端·javascript·react.js