最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • React 算法之栈操作

    正文概述 掘金(公里柒)   2021-01-02   426

    概念

    来自 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.

    React 算法之栈操作

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

    React 算法之栈操作

    注意:

    • 本节只分析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源码, 进而提升架构和编码能力.


    起源地下载网 » React 算法之栈操作

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元