likes
comments
collection
share

从零实现 React v18,但 WASM 版 - [20] 实现 Context

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

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

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

本文对应 tag:v20

Context 也是 React 中非常重要的一个特性,所以我们的 WASM 版也得安排上。回想下,我们平时一般都会这样来使用 Context:

import {createContext, useContext} from 'react'

const ctx = createContext('A')

export default function App() {
  return (
    <ctxA.Provider value={'B'}>
      <Cpn />
    </ctxA.Provider>
  )
}

function Cpn() {
  const value = useContext(ctx)
  return <div>{value}</div>
}

所以,我们需要先从 react 库中导出两个方法:

#[wasm_bindgen(js_name = useContext)]
pub unsafe fn use_context(context: &JsValue) -> Result<JsValue, JsValue> {
  ...
}

#[wasm_bindgen(js_name = createContext)]
pub unsafe fn create_context(default_value: &JsValue) -> JsValue {
    let context = Object::new();
    Reflect::set(
        &context,
        &"$$typeof".into(),
        &JsValue::from_str(REACT_CONTEXT_TYPE),
    );
    Reflect::set(&context, &"_currentValue".into(), default_value);
    let provider = Object::new();
    Reflect::set(
        &provider,
        &"$$typeof".into(),
        &JsValue::from_str(REACT_PROVIDER_TYPE),
    );
    Reflect::set(&provider, &"_context".into(), &context);
    Reflect::set(&context, &"Provider".into(), &provider);
    context.into()
}

其中,create_context 中的代码翻译成 JS,是下面这样:

const context {
  $$typeof: REACT_CONTEXT_TYPE,
  Provider: null,
  _currentValue: defaultValue,
}
context.Provider = {
  $$typeof: REACT_PROVIDER_TYPE,
  _context: context,
}
return context

可见,ctxA.Provider 是一种新的 FiberNode 类型,我们需要新增分支进行处理,按照流程顺序,首先是 begin_work

fn update_context_provider(
    work_in_progress: Rc<RefCell<FiberNode>>,
) -> Option<Rc<RefCell<FiberNode>>> {
    let provider_type = { work_in_progress.borrow()._type.clone() };
    let context = derive_from_js_value(&provider_type, "_context");
    let new_props = { work_in_progress.borrow().pending_props.clone() };
    push_provider(&context, derive_from_js_value(&new_props, "value"));
    let next_children = derive_from_js_value(&new_props, "children");
    reconcile_children(work_in_progress.clone(), Some(next_children));
    work_in_progress.clone().borrow().child.clone()
}

这里难以理解的是 push_provider

static mut PREV_CONTEXT_VALUE: JsValue = JsValue::null();
static mut PREV_CONTEXT_VALUE_STACK: Vec<JsValue> = vec![];

pub fn push_provider(context: &JsValue, new_value: JsValue) {
    unsafe {
        PREV_CONTEXT_VALUE_STACK.push(PREV_CONTEXT_VALUE.clone());
        PREV_CONTEXT_VALUE = Reflect::get(context, &"_currentValue".into()).unwrap();
        Reflect::set(context, &"_currentValue".into(), &new_value);
    }
}

与之对应的,还有一个 pop_provider

pub fn pop_provider(context: &JsValue) {
    unsafe {
        Reflect::set(context, &"_currentValue".into(), &PREV_CONTEXT_VALUE);
        let top = PREV_CONTEXT_VALUE_STACK.pop();
        if top.is_none() {
            PREV_CONTEXT_VALUE = JsValue::null();
        } else {
            PREV_CONTEXT_VALUE = top.unwrap();
        }
    }
}

它会在 complete_work 中调用:

WorkTag::ContextProvider => {
  let _type = { work_in_progress.borrow()._type.clone() };
  let context = derive_from_js_value(&_type, "_context");
  pop_provider(&context);
  self.bubble_properties(work_in_progress.clone());
  None
}

我们通过下面这个例子来搞清楚这部分代码:

const ctxA = createContext('A0')
const ctxB = createContext('B0')

export default function App() {
  return (
    <ctxA.Provider value='A1'>
      <ctxB.Provider value='B1'>
        <ctxA.Provider value='A2'>
          <ctxB.Provider value='B2'>
            <Child />
          </ctxB.Provider>
          <Child />
        </ctxA.Provider>
        <Child />
      </ctxB.Provider>
      <Child />
    </ctxA.Provider>
  )
}

function Child() {
  const a = useContext(ctxA)
  const b = useContext(ctxB)
  return (
    <div>
      A: {a} B: {b}
    </div>
  )
}

上述例子结果显而易见,应该是:

A: A2 B: B2
A: A2 B: B1
A: A1 B: B1
A: A1 B: B0

我们来分析一下,根据流程,当 begin_work 执行到最底层的 Child 时,此时经过了四次 push_providerFiberNode 状态如下:

从零实现 React v18,但 WASM 版 - [20] 实现 Context

到了第三层的 Child 时,会执行一次 pop_provider,状态如下:

从零实现 React v18,但 WASM 版 - [20] 实现 Context

到了第二层的 Child 时,再执行一次 pop_provider,状态如下:

从零实现 React v18,但 WASM 版 - [20] 实现 Context

到了第一层的 Child 时,最后执行一次 pop_provider,状态如下:

从零实现 React v18,但 WASM 版 - [20] 实现 Context

这里不好理解的原因在于它把多个 Context 的值都存到一个 stack 里面了,可以对着这个例子多看几遍。

理解了这个,Context 的基本流程就介绍差不多了。不过还有一个 useContext,它也很简单,按照之前其他 Hooks 的流程添加相关代码即可,最后的核心是 fiber_hooks 中的 read_context 方法:

fn read_context(context: JsValue) -> JsValue {
  let consumer = unsafe { CURRENTLY_RENDERING_FIBER.clone() };
  if consumer.is_none() {
      panic!("Can only call useContext in Function Component");
  }
  let value = derive_from_js_value(&context, "_currentValue");
  value
}

这样,上面的例子就可以跑起来了,本次更新详见这里

不过,目前的 Context 还不够完善,一旦它跟性能优化相关的特性结合起来使用,就会有问题了,比如下面这个例子:

const ctx = createContext(0)

export default function App() {
  const [num, update] = useState(0)
  const memoChild = useMemo(() => {
    return <Child />
  }, [])
  console.log('App render ', num)
  return (
    <ctx.Provider value={num}>
      <div
        onClick={() => {
          update(1)
        }}>
        {memoChild}
      </div>
    </ctx.Provider>
  )
}

function Child() {
  console.log('Child render')
  const val = useContext(ctx)

  return <div>ctx: {val}</div>
}

点击后 Child 组件不会重新渲染,页面没有得到更新。原因在于 Child 命中了 bailout 策略,但其实 Child 中使用了 context,而 context 的值发生了变化,Child 应该重新渲染才对,这个问题就留到下篇文章再解决吧。

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

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