• home > webfront > ECMAS > react >

    react hook context 管理全局状态

    Author:zhoulujun Date:

    全局变量共享是一个复杂的问题,在并发访问时尤为突出。不过只要保证组件足够纯,再隔离副作用就好了。纯函数的优势就是并发,副作用IO可以交给异步任务队列执行,或者是用Monad来处理IO,保证IO操作次序,IO操作是需

    React context 

    React.js 的 context 就是这么一个东西,某个组件只要往自己的 context 里面放了某些状态,这个组件之下的所有子组件都直接访问这个状态而不需要通过中间组件的传递。一个组件的 context 只有它的子组件能够访问,它的父组件是不能访问到的,你可以理解每个组件的 context 就是瀑布的源头,只能往下流不能往上飞。

    Fiber

    解决主线程长时间被 JS 运算占用这一问题的基本思路,是将运算切割为多个步骤,分批完成。也就是说在完成一部分任务之后,将控制权交回给浏览器,让浏览器有时间进行页面的渲染。等浏览器忙完之后,再继续之前未完成的任务。

    旧版 React 通过递归的方式进行渲染,使用的是 JS 引擎自身的函数调用栈,它会一直执行到栈空为止。而Fiber实现了自己的组件调用栈,它以链表的形式遍历组件树,可以灵活的暂停、继续和丢弃执行的任务。实现方式是使用了浏览器的requestIdleCallback这一 API。官方的解释是这样的:

    window.requestIdleCallback()会在浏览器空闲时期依次调用函数,这就可以让开发者在主事件循环中执行后台或低优先级的任务,而且不会对像动画和用户交互这些延迟触发但关键的事件产生影响。函数一般会按先进先调用的顺序执行,除非函数在浏览器调用它之前就到了它的超时时间。

    Fiber 其实指的是一种数据结构,它可以用一个纯 JS 对象来表示:

    const fiber = {
        stateNode,    // 节点实例
        child,        // 子节点
        sibling,      // 兄弟节点
        return,       // 父节点
    }

    为了加以区分,以前的 Reconciler 被命名为Stack Reconciler。Stack Reconciler 运作的过程是不能被打断的,必须一条道走到黑

    而 Fiber Reconciler 每执行一段时间,都会将控制权交回给浏览器,可以分段执行

    为了达到这种效果,就需要有一个调度器 (Scheduler) 来进行任务分配。任务的优先级有六种:

    • synchronous,与之前的Stack Reconciler操作一样,同步执行

    • task,在next tick之前执行

    • animation,下一帧之前执行

    • high,在不久的将来立即执行

    • low,稍微延迟执行也没关系

    • offscreen,下一次render时或scroll时才执行

    优先级高的任务(如键盘输入)可以打断优先级低的任务(如Diff)的执行,从而更快的生效

    Fiber 树

    Fiber Reconciler 在阶段一进行 Diff 计算的时候,会生成一棵 Fiber 树。这棵树是在 Virtual DOM 树的基础上增加额外的信息来生成的,它本质来说是一个链表。

    1583760739-5c6f9cff9189e_articlex.png


    Fiber 树在首次渲染的时候会一次过生成。在后续需要 Diff 的时候,会根据已有树和最新 Virtual DOM 的信息,生成一棵新的树。这颗新树每生成一个新的节点,都会将控制权交回给主线程,去检查有没有优先级更高的任务需要执行。如果没有,则继续构建树的过程:

    1838998360-5c6fa25d808cb_articlex.png


    如果过程中有优先级更高的任务需要进行,则 Fiber Reconciler 会丢弃正在生成的树,在空闲的时候再重新执行一遍。

    在构造 Fiber 树的过程中,Fiber Reconciler 会将需要更新的节点信息保存在Effect List当中,在阶段二执行的时候,会批量更新相应的节点。



    redux是dispatch一个action,被reducer处理,产生一个新的state,然后通知订阅者,并提供状态拉取接口。(getState对state惰性求值或者是异步求值,也可以利用指针,总之就是getState总是能得到最新的状态)

    react里的useReducer返回一个dispatch,接受一个action,被reducer处理得到新的state然后传给setState并执行,setState执行会调用ReactDispatcher,从当前hook fiber开始向后遍历所有fiber,并执行每个hook fiber节点的实例,得到hook fiber的children fibers,然后reconcile、链接alternate、标记effectTag、向上链接effectFiberList、最后commit...。这个过程中执行hook fiber的实例就相当于通知订阅者,hook fiber的实例(instance)就是你的函数组件!所以,目前的react距离redux只差一个全局状态共享,显然就是context,可以使用createContext定义一个全局state,在reducers执行后,将新的全局state赋值给context(可看作一个用于访问全局state的指针,其实就是一个对象),然后在函数组件中使用useContext访问context指针,拿到全局state。这样基本实现了redux的核心feature。

    比较难解决的是thunk,redux中在dispatch执行前对action做判断,如果是异步action则传入middlewareAPI并执行,如果是同步action则立即dispatch。react中的dispatcher是一个用于启动performWork的scheduler(用于安排调度任务到任务队列)。redux的dispatch是原子操作,只有当所有reducers执行完毕才会通知订阅者进行下一步操作(redux理念中reducers是纯函数,subscriptions是副作用),确保getState不会脏读state,但是react是吗?ReactDispatcher执行单元是一个fiber,每个hook fiber实例(使用了useReducer)执行reducer后进行setState操作,组件实例执行的同时也等于通知订阅,它并不会关心(或者等)其他组件是否执行完毕,也就是在reducers没有全部执行完就去读全局state,造成脏读。(如果此时往全局state写入新值就更加错误了,也可以说没有保证事务隔离。)这也是concurrent并发调度模式所存在的难题。所以,既然难以保证IO操作拥有足够的隔离性,所以可以使用惰性求值(或者异步)来进行IO操作,即将所有组件pure纯化(访问全局context就不纯),将所有IO操作推迟到纯函数执行之后。譬如ReactDOM.render的第三个参数表示在一次完整的render之后执行一次操作,此时进行副作用IO。

    总之,全局变量共享是一个复杂的问题,在并发访问时尤为突出。不过只要保证组件足够纯,再隔离副作用就好了。纯函数的优势就是并发,副作用IO可以交给异步任务队列执行,或者是用Monad来处理IO,保证IO操作次序,IO操作是需要保证先后顺序的,纯函数不需要。

    扯的有点远,总之,如果是createContext然后useContext再立即重写context,肯定有问题。(如果context写入顺序不会因为并发调度机制打乱的话,应该也行..吧,不过你要保证是“读已提交”,知道哪些操作已经提交哪些还在等待执行。)


    参考文章:

    React.js 的 context  huziketang.mangojuice.top/books/react/lesson29



    转载本站文章《react hook context 管理全局状态》,
    请注明出处:https://www.zhoulujun.cn/html/webfront/ECMAScript/jsBase/2020_0527_8441.html

    延伸阅读: