React 算法之栈操作

855 阅读6分钟

React源码中使用到了很多算法技巧, 主要包含位运算, 深度优先遍历, 链表操作, 栈操作, 堆排序等. 本文基于react@17.0.1, 梳理出React源码中栈操作的使用技巧.

概念

来自 wiki 上的解释: 堆栈(stack)又称为堆叠, 是计算机科学中的一种抽象资料类型, 只允许在有序的线性资料集合的一端(称为堆栈顶端top)进行加入数据(push)和移除数据(pop)的运算. 因而按照后进先出(LIFO, Last In First Out)的原理运作.

注意:

  • (stack)又叫做堆栈, 这里特指数据结构中的(另一种程序内存分配中的栈, 本系列不做介绍, 读者可自行了解).
  • 堆栈中虽带有一个字, 只是命名, 不要和混淆.
  • 常说的有 2 种指代, 一种是数据结构中的堆(在React 算法之堆排序中有介绍), 另一种是程序内存分配中的堆(本系列不做介绍, 读者可自行了解).

特性

  1. 先入后出, 后入先出.
  2. 除头尾节点之外, 每个元素有一个前驱, 一个后继.

基本使用

  1. 压栈: push()
  2. 弹栈: pop((
  3. 预览栈顶元素: peek()
class Stack {
  constructor() {
    this.dataStore = [];
    this.top = 0;
  }

  // 压栈
  push(element) {
    this.dataStore[this.top++] = element;
  }

  // 弹栈
  pop() {
    return this.dataStore[--this.top];
  }

  // 预览栈顶元素
  peek() {
    return this.dataStore[this.top - 1];
  }

  // 检测栈内存储了多少个元素
  length() {
    return this.top;
  }

  // 清空栈
  clear() {
    this.top = 0;
  }
}

测试代码:

const test = () => {
  const stack = new Stack();
  console.log('压栈a: ');
  stack.push('a');
  console.log('压栈b: ');
  stack.push('b');
  console.log('压栈c: ');
  stack.push('c');
  console.log('栈高度: ', stack.length());
  console.log('栈顶元素: ', stack.peek());
  console.log('弹出: ', stack.pop());
  console.log('栈顶元素: ', stack.peek());
  console.log('压栈d: ');
  stack.push('d');
  console.log('栈顶元素: ', stack.peek());
  console.log('清空栈: ');
  stack.clear();
  console.log('栈高度: ', stack.length());
  console.log('压栈e: ');
  stack.push('e');
  console.log('栈顶元素: ', stack.peek());
};

利用栈先进后出的特性, 在实际编码中应用非常广泛. 如回溯,递归,深度优先搜索等经典算法都可以利用栈的特性来实现. 由于本文的目的是讲解栈react中的使用场景, 所以与栈相关的经典案例本文不再列举, 请读者移步其他算法资料.

React 当中的使用场景

Context 状态管理

fiber树创建过程中, 如果使用了Context api(具体来说是使用Context.Provider, Class.contextType, Context.Consumerapi), react内部会维护一个来保存提供者(Context.Provider)的状态, 供给消费者(Context.Consumer)使用.

首先看stack的定义(ReactFiberStack.js中):

export type StackCursor<T> = {| current: T |};

// 维护一个全局stack
const valueStack: Array<any> = [];
let index = -1;

// 一个工厂函数, 创建StackCursor对象
function createCursor<T>(defaultValue: T): StackCursor<T> {
  return {
    current: defaultValue,
  };
}
function isEmpty(): boolean {
  return index === -1;
}
// 出栈
function pop<T>(cursor: StackCursor<T>, fiber: Fiber): void {
  if (index < 0) {
    return;
  }
  cursor.current = valueStack[index];
  valueStack[index] = null;
  index--;
}
// 入栈
function push<T>(cursor: StackCursor<T>, value: T, fiber: Fiber): void {
  index++;
  // 注意: 这里存储的是 cursor当前值, 随后更新了cursor.current为
  valueStack[index] = cursor.current;
  cursor.current = value;
}

ReactFiberStack.js源码中, 定义的valueStack作为全局变量, 用来存储所有的StackCursor.current(不仅仅存储context api相关的StackCursor, 在context 原理章节中详细解读, 本节只讨论与contex api相关的栈操作).

注意StackCursor是一个泛型对象, 与context api相关的StackCursor定义在ReactFiberNewContext.js:

// 定义全局 valueCursor, 用于管理<Context.Provider/>组件的value
const valueCursor: StackCursor<mixed> = createCursor(null);

// ...省略无关代码

// 将context当前的值保存到valueCursor中, 并设置context._currentValue为最新值
// 运行完成之后context为最新状态
export function pushProvider<T>(providerFiber: Fiber, nextValue: T): void {
  const context: ReactContext<T> = providerFiber.type._context;
  push(valueCursor, context._currentValue, providerFiber);
  context._currentValue = nextValue;
}

// 取出valueCursor中保存的旧值, 设置到context._currentValue上.
// 运行完成之后context恢复到上一个状态
export function popProvider(providerFiber: Fiber): void {
  const currentValue = valueCursor.current;
  pop(valueCursor, providerFiber);
  const context: ReactContext<any> = providerFiber.type._context;
  context._currentValue = currentValue;
}

假设有如下组件结构(平时开发很难有这样的代码, 此处完全是为了演示context api中涉及到的栈操作):

const MyContext = React.createContext(0);

export default function App() {
  return (
    // 第一级
    <MyContext.Provider value={1}>
      <MyContext.Consumer>
        {value1 => (
          //第二级嵌套
          <MyContext.Provider value={2}>
            <MyContext.Consumer>
              {value2 => (
                // 第三级嵌套
                <MyContext.Provider value={3}>
                  <MyContext.Consumer>
                    {value3 => (
                      <span>
                        {value1}-{value2}-{value3}
                      </span>
                    )}
                  </MyContext.Consumer>
                </MyContext.Provider>
              )}
            </MyContext.Consumer>
          </MyContext.Provider>
        )}
      </MyContext.Consumer>
    </MyContext.Provider>
  );
}

可在codesandbox中查看运行结果.

fiber树构造过程中MyContext对象在栈中的变化情况表示出来:

  1. beginWork阶段: 入栈

    • reconciler之前, 由于const MyContext = React.createContext(0);已经创建了MyContext对象, 所以其初始值是0.
    • reconciler过程中, 每当遇到Context.Provider类型的节点, 则会执行pushProvider.

  1. completeWork阶段: 出栈
    • reconciler过程中, 每当遇到Context.Provider类型的节点, 则会执行popProvider.
    • reconciler之后, valueStackvalueCursor以及MyContext都恢复到了初始状态.

注意:

  • 本节只分析context实现源码中与相关的部分, 所以只涉及到了Context.Provider(供应者)节点.
  • 对于Context.Consumer(消费者)以及更新阶段context的运行机制的深入解读放在context原理章节中.

executionContext 执行上下文

executionContext是在ReactFiberWorkLoop.js中定义的一个全局变量(相对于该闭包), 且定义成二进制变量, 通过位运算来维护其状态(在React 算法之位运算一文中已有介绍).

表面上看executionContext和栈并没有直接关系, 但实际在改变executionContext的时候, 巧妙的利用了函数调用栈, 实现executionContext状态的维护.

本节主要是体现executionContext函数调用栈之间的配合运用(具体源码), 这里以batchedUpdatesunbatchedUpdates为例进行分析.

export function batchedUpdates<A, R>(fn: A => R, a: A): R {
  // 在执行回调之前, 先改变 executionContext
  const prevExecutionContext = executionContext;
  executionContext |= BatchedContext;
  try {
    return fn(a);
  } finally {
    // 回调执行完毕之后, 再恢复到以前的值 prevExecutionContext
    executionContext = prevExecutionContext;
    // ... 省略无关代码
  }
}

export function unbatchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
  const prevExecutionContext = executionContext;
  executionContext &= ~BatchedContext;
  executionContext |= LegacyUnbatchedContext;
  try {
    return fn(a);
  } finally {
    executionContext = prevExecutionContext;
    // ... 省略无关代码
  }
}

// ... 省略其他函数

这些函数的共性:

  1. 执行回调之前, 先保存当前值为prevExecutionContext, 再改变 executionContext.
  2. 在执行回调fn期间, 无论函数fn调用栈有多深, 被改变过的executionContext始终有效.
  3. 回调执行完毕之后, 恢复到以前的值 prevExecutionContext.

总结

本节主要介绍了react源码中的使用情况. 涉及入栈出栈等基本操作(Context 状态管理), 以及对函数调用栈的巧妙运用(改变executionContext执行上下文).

由于reconciler过程是一个深度优先遍历过程, 对于fiber树来讲, 向下探寻(beginWork阶段)和向上回溯(completeWork阶段)天然就和栈的入栈(push)和出栈(pop)能够无缝配合(context 机制就是在这个特性上建立起来的).

参考资料

写在最后

本文属于图解react原理系列中的算法板块, 本系列近 20 篇文章, 不是为了面试, 真的是为了搞懂React源码, 进而提升架构和编码能力.