likes
comments
collection
share

从零实现 React v18,但 WASM 版 - [17] 实现 Concurrent 模式

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

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

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

本文对应 tag:v17

改动最大的部分还是 reconciler 库的 work_loop.rs,先回顾下之前的流程:

schedule_update_on_fiber -> ensure_root_is_scheduled -> perform_sync_work_on_root -> work_loop -> commit_root

现在需要改成这样:

schedule_update_on_fiber -> ensure_root_is_scheduled -> perform_sync_work_on_root -> render_root -> work_loop_sync -> commit_root
                                     |                                                  ^   |                            ^
                                     | --->   perform_concurrent_work_on_root ----------|   |---> work_loop_concurrent --|

也是就增加了一条 Concurrent 模式的支线,另外增加了 render_root,这样 Render 和 Commit 两大过程也更直观了。

其中 perform_concurrent_work_on_root 需要使用之前实现的 scheduler 来进行调度:

let scheduler_priority = lanes_to_scheduler_priority(cur_priority.clone());
let closure = Closure::wrap(Box::new(move |did_timeout_js_value: JsValue| {
    let did_timeout = did_timeout_js_value.as_bool().unwrap();
    perform_concurrent_work_on_root(root_cloned.clone(), did_timeout)
}) as Box<dyn Fn(JsValue) -> JsValue>);
let function = closure.as_ref().unchecked_ref::<Function>().clone();
closure.forget();
new_callback_node = Some(unstable_schedule_callback_no_delay(
    scheduler_priority,
    function,
))

而在 perform_concurrent_work_on_root 中,我们需要根据 Render 阶段结束时的返回状态来判断,Render 工作是否完成。

如果返回状态为 ROOT_INCOMPLETE,说明没完成,也就是时间切片用完了,Render 工作暂时停止,此时需要再次返回一个函数:

let exit_status = render_root(root.clone(), lanes.clone(), should_time_slice);
if exit_status == ROOT_INCOMPLETE {
    let root_cloned = root.clone();
    let closure = Closure::wrap(Box::new(move |did_timeout_js_value: JsValue| {
        let did_timeout = did_timeout_js_value.as_bool().unwrap();
        perform_concurrent_work_on_root(root_cloned.clone(), did_timeout)
    }) as Box<dyn Fn(JsValue) -> JsValue>);
    let function = closure.as_ref().unchecked_ref::<Function>().clone();
    closure.forget();
    return function.into();
}

因为 scheduler 有这样的特性,比如下面这个例子:

import Scheduler from 'react/packages/scheduler'

function func2(didTimeout) {
  if (!didTimeout) console.log(2)
}

function func1() {
  console.log(1)
  return func2
}

const task = Scheduler.unstable_scheduleCallback(1, func1)

func1 结束后返回了 func2 则两个函数会共用 task 的过期时间。

什么意思呢?比如 task 的过期时间是 3 秒,func1 执行用了 2 秒,那么执行到 func2 时,task 还未过期,didTimeoutfalse。 如果 func1 执行用了 4 秒,那么执行到 func2 时,task 已过期,didTimeouttrue

否则,如果返回状态为 ROOT_COMPLETED,则说明 Render 流程已完全完成,则进行 Commit 流程,这个与之前就是一样的了。

接下来看看 render_root,该方法新增了一个参数 should_time_slice,如果为 true 就调用 work_loop_concurrent 方法,否则调用 work_loop_sync 方法:

loop {
    match if should_time_slice {
        work_loop_concurrent()
    } else {
        work_loop_sync()
    } {
        Ok(_) => {
            break;
        }
        Err(e) => unsafe {
            log!("work_loop error {:?}", e);
            WORK_IN_PROGRESS = None
        },
    };
}

其中 work_loop_concurrentwork_loop_sync 不同之处仅在于增加了 unstable_should_yield_to_host 的约束,即判断时间切片是否已用完:

fn work_loop_concurrent() -> Result<(), JsValue> {
    unsafe {
        while WORK_IN_PROGRESS.is_some() && !unstable_should_yield_to_host() {
            log!("work_loop_concurrent");
            perform_unit_of_work(WORK_IN_PROGRESS.clone().unwrap())?;
        }
    }
    Ok(())
}

当跳出循环时,如果 should_time_slicetrueWORK_IN_PROGRESS 不为空,说明 Render 阶段还未完成,此时 render_root 返回 ROOT_INCOMPLETE

unsafe {
    EXECUTION_CONTEXT = prev_execution_context;
    WORK_IN_PROGRESS_ROOT_RENDER_LANE = Lane::NoLane;

    if should_time_slice && WORK_IN_PROGRESS.is_some() {
        return ROOT_INCOMPLETE;
    }

    ...
}

这样,整个流程就串起来了。

那什么时候需要使用 Concurrent 模式呢?这个取决于更新的优先级,一般来说,不那么紧急的更新可以使用 Concurrent 模式。

big-react 中目前是这么规定的:

const eventTypeToEventPriority = (eventType: string) => {
  switch (eventType) {
    case 'click':
    case 'keydown':
    case 'keyup':
      return SyncLane
    case 'scroll':
      return InputContinuousLane
    // TODO 更多事件类型
    default:
      return DefaultLane
  }
}

在调用事件的回调函数之前会将 scheduler 中的全局变量 currentPriorityLevel 改成对应的值:

// react-dom
const triggerEventFlow = (paths: EventCallback[], se: SyntheticEvent) => {
  for (let i = 0; i < paths.length; i++) {
    const callback = paths[i]
    runWithPriority(eventTypeToEventPriority(se.type), () => {
      callback.call(null, se)
    })

    if (se.__stopPropagation) {
      break
    }
  }
}

// scheduler
function unstable_runWithPriority(priorityLevel, eventHandler) {
  ...

  var previousPriorityLevel = currentPriorityLevel
  currentPriorityLevel = priorityLevel

  try {
    return eventHandler()
  } finally {
    currentPriorityLevel = previousPriorityLevel
  }
}

不过,他这里貌似有点小问题,eventTypeToEventPriority 返回的是 Lane,需要转换为 scheduler 中的 Priority 才行,所以我这里做了下修改:

fn event_type_to_event_priority(event_type: &str) -> Priority {
    let lane = match event_type {
        "click" | "keydown" | "keyup" => Lane::SyncLane,
        "scroll" => Lane::InputContinuousLane,
        _ => Lane::DefaultLane,
    };
    lanes_to_scheduler_priority(lane)
}

但是这里只是更新了 scheduler 中的 Priority 而已,我们还需要更新 reconciler 中的 Lane,这个是怎么实现的呢?

答案就在 fiber_hooksuseState 返回的第二个值是个函数,当它被调用时,会执行下面这个方法:

fn dispatch_set_state(
    fiber: Rc<RefCell<FiberNode>>,
    update_queue: Rc<RefCell<UpdateQueue>>,
    action: &JsValue,
) {
    let lane = request_update_lane();
    let update = create_update(action.clone(), lane.clone());
    enqueue_update(update_queue.clone(), update);
    unsafe {
        schedule_update_on_fiber(fiber.clone(), lane);
    }
}

这里有个 request_update_lane,它的作用就是根据 scheduler 中的 Priority 来得到对应的 Lane:

pub fn request_update_lane() -> Lane {
    let current_scheduler_priority_level = unstable_get_current_priority_level();
    let update_lane = scheduler_priority_to_lane(current_scheduler_priority_level);
    update_lane
}

这样,当我们触发事件时,就可以将 scheduler 中的 Priority 和当次更新的 Lane 都修改成对应的值了。

一切就绪,我们来测试一下,为了方便,我暂时将 click 的优先级改低一点:

fn event_type_to_event_priority(event_type: &str) -> Priority {
    let lane = match event_type {
        "click" | "keydown" | "keyup" => Lane::InputContinuousLane,
        "scroll" => Lane::InputContinuousLane,
        _ => Lane::DefaultLane,
    };
    lanes_to_scheduler_priority(lane)
}

然后用下面这个例子:

import {useState} from 'react'
function App() {
  const [num, updateNum] = useState(0)
  const len = 100

  return (
    <ul
      onClick={(e) => {
        updateNum((num: number) => num + 1)
      }}>
      {Array(len)
        .fill(1)
        .map((_, i) => {
          return <Child i={`${i} ${num}`} />
        })}
    </ul>
  )
}

function Child({i}) {
  return <p>i am child {i}</p>
}

export default App

运行后可以得到如下结果:

从零实现 React v18,但 WASM 版 - [17] 实现 Concurrent 模式

其中左边这一部分是首次渲染,没有开启时间切片,右边是点击后的更新,可以看到有很多小的 Task,证明我们的时间切片功能成功实现了。

此次更新代码可以查看这里

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

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