likes
comments
collection
share

你应该要知道的细粒度更新

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

前言

vuesvelteSolid这些框架,不需要显示的指明依赖就能达到自变量state的改变因变量computed也能随之改变。 但作为react框架的用户为什么需要显示指明依赖以及如何实现一个react HOOKS版本自动追踪依赖技术也称之为细粒度更新

框架的实现原理以及分类

基础原理

在实现细粒度更新之前,我们先来了解一下框架的基础原理,框架最基本的理论可以由如下公式诠释: 你应该要知道的细粒度更新

  1. state代表我们定义的数据状态(自变量和因变量)
  2. f代表处理数据变量的逻辑(框架核心机制)
  3. UI代表你所看到的视图(宿主机的界面)

如上state状态分为自变量因变量。 那么什么是自变量,什么又是因变量呢?

  • 自变量:由自己控制和改变的变量,不受其他变量影响
  • 因变量: 依赖于自变量产生的变量,自变量变更因变量也随着变更

其在react中的定义:

// 自变量
const  [name, setName] = useState('AK');

// 因变量 (随着name自变量的更新,fullName也会跟着变化)
const fullName =  useMemo(() =>{
    return name + 'clown'
}, [name])

如上诉公式UI = f(state)表述只能通过state状态的变化才能导致UI的更新。框架的作者在设计组件时需要提供一些灵活度,使得开发者在定义UI逻辑时能够跳出组件的限制,执行一些有副作用的操作。 例如: 有时候希望直接操作DOM元素或者记录页面渲染次数,无论那个state变量的变化等。

因此在react中提供了useRef这个hooks来实现此需求。useRef产生的值我们通常称之为ref(reference)表示引用的意思,其用于组件多次渲染(render)之间缓存的一个引用类型的值。 因为他的作用可以在下面的那一个箭头里

你应该要知道的细粒度更新

框架分类

框架的类别分为三类: 应用级框架(react)组件级框架(vue)元素级框架(solid) 那么这三类型在在更新上有何差异呢? 接下来定义一下A、B两个组件。 你应该要知道的细粒度更新 接下来看看各个类型是如何更新的

  • 元素级框架(solid)
  1. name变化导致A组件的{name}更新
  2. name变化导致B组件的{name+‘clown’}更新
  3. name变化导致B组件的{'https'+name}更新
  4. age变化导致B组件的{age}更新
  • 组件级框架(vue)
  1. name变化导致A组件更新
  2. name变化导致B组件更新
  3. age变化导致B组件更新
  • 应用级框架(react)
  1. name变化导致应用
  2. age变化导致应用

由上所示: 自变量到UI的变化路径越多,意味着框架在运行时消耗在寻找自变量与UI的对应关系上的时间也就越少

细粒度更新的实现

再了解了框架的基础原理和分类,接下来就来了解一下自动追踪依赖技术也称之为细粒度更新.本质底层实现就是发布订阅设计模式。在state的get值时进行一些effect的订阅。在set值将更新推送给订阅的effct从而实现订阅者的更新。

首先来实现useState来声明自变量

    
function useState(value){
       // 获取值
       const getter = () => value
       // 设置值
       const setter = (newValue) =>{
           value = newValue
       }
       return [getter]
}

初始化的时候传入初始值value,通过getter获取到闭包的value值,通过setter可以修改闭包的value值。

紧接着来实现useEffect,带有副作用的因变量函数。其行为有如下三步:

  1. useEffect执行后会自动调用回调函数
  2. 因变量变化时回调函数也会被执行
  3. 无需显示指定依赖(因变量)

实现上述三个步骤的期望,实际上就是建立useStateuseEffect的发布订阅关系:

  1. 在useEffect的回调中,执行getter函数获取因变量时建立订阅关系。即当前effect会定义该state的变化
  2. 在useState中通过setter设置因变量时,执行所有订阅者effect的回调函数。从而达到useEffect回调函数的执行

你应该要知道的细粒度更新


// 全局effect执行栈
const  effectStack = [];

// 清空effect里的所有依赖项
function cleanup(effect){
    // 从订阅的所有state对应的subs中移除掉effect (就是移除掉useState里的subs保存与该effect的关系)
    for(let subs of effect.deps){
        subs.delete(effect)
    }
    // 清空依赖
    effect.deps.clear()
}


function useEffect(callback){
    const executed = () =>{
        // 清空effect的依赖deps
        cleanup(effect)
        // 将当前effect退入到effectStack顶部  (在useState中才能获取到当前正在执行effect,也才能知道与那个effect建立发布订阅关系)
        effectStack.push(effect)
        try{
            callback()
        }finally{
            // 将当前执行effect从栈顶移除掉
            effectStack.pop()
        }
    }

    const effect = {
        executed,
        deps:new Set()
    }
    // 立刻执行
    effect.executed()
}

紧接着来实现一下useState中是如何与该useEffect建立发布订阅关系的


// 定义关系的实现
function subscribe(effect,subs){
    // 通过这一步就建立了相互关系:   subs  <===>  effect.deps 
    subs.add(effect)
    
    effect.deps.add(subs)
}

function useState(value){
        
       // 记录 关联的effect
       const subs = new Set()

       // 获取值
       const getter = () => {
           // 从全局的effectStack的栈顶获取到当前正在指向的`effect上下文`
           const effect = effectStack.at(-1);
           
           if(effect){
               // 存在effect建立订阅关系  (该state就已经能够拿到useEffect的回调了)
               subscribe(effect,subs)
           }
           
           return value 
       }
       
       // 设置值
       const setter = (newValue) =>{
           value = newValue
           
           // 进行发布行为
           
           for(let sub of [...subs] ){
               // 这一步执行了useEffect的回调
               effect.executed()
           }
           
       }
       return [getter]
}

经过上面步骤我们就完成了useStateuseEffect的发布订阅模式,也就是实现了自动追踪依赖技术(细粒度更新)的功能。那么接下来思考三个问题:

  1. useState中是如何知道当前正在执行那个effect呢? 解答: 通过全局的effectStack栈来记录当前正在执行的useEffect。进入执行useEffect时,将当前effect推入栈顶,执行callback回调函数时会触发到useState里的getter函数获取state值(自变量),那么此时是不是就可以在effectStack的栈顶拿到正在执行的effect了从而建立关系。等callback回调函数执行完成之后,将effecteffectStack的栈顶移除掉。

  2. 为什么在useEffect中每次执行effect.executed都要重新清除订阅关系,再重新订阅呢? 解答:

const [name, setName] = useState('AKclown')
const [age, setAge] = useState(18)
const [isShowAge, setIsShowAge] = useState(true)

useEffect(() =>{
    
    if(!isShowAge()){
        return '名称:' + name()
    }
    
    return '名称:' + name() + '年龄:' + age()
}) 

因为isShowAgetrue那么这个effect会跟name建立发布订阅关系以及跟age也建立发布订阅关系。此时nameage中任意一个自变量发生变化时,在对应的useSteta内部的setter都会执行effect.excuted。因此当前useEffect回调函数就会被执行。 你应该要知道的细粒度更新 那么如果每次执行effect.excuted不重置发布订阅关系带来的效果是如何呢? 假设现在执行setIsShowAge(false),那么本质上只有name的更新才会触发useEffect的回调,而age的更新就不应该触发useEffect的回调了。但因为没有重新建立发布订阅关系useEffect依旧保留了对age的更新的订阅。

因此我们需要在每次执行effect.executed时,重新建立与state(自变量)的订阅关系。最终关系图如下: 你应该要知道的细粒度更新

  1. 为什么React Hooks中没有使用细粒度更新呢? 解答: 原因在于React属于应用级别框架,从而其关注的是自变量与应用之间的关系,从这个角度看,其更细粒度不需要很细,因此无须使用细粒度更新

总结

本章我们学习框架的基础理论UI = F(state)、也学习到了因变量和自变量在框架中的含义。以及知道前端框架的分类,其分为应用级别框架组件级别框架元素级别框架、三大类。最后学习了React HOOKS自动追踪依赖的版本实现

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