重拾Vue3响应式系统

868 阅读8分钟

大家好,我是王大傻.接上篇,看了VueJS设计与实现里面的响应式系统设计之后,有了很大的感悟.

响应式数据与副作用函数

响应式数据,相信用过Vue2的各位小伙伴们并不模糊,就是我们之前的defineProperty监听的数据,那么另外一个副作用函数,什么是副作用函数呢

function effect(){
    document.body.innerHtml = '<div> hello world </div>'
}
// 再比如

let val = 1
function effect(){
    val = 2
}

因此我们说副作用函数就是产生副作用的函数(当然不能这么说,说了好像没说一样), 具体来讲就是如我们第一个例子,我们调用effect 执行了 代码,将页面替换了一个标签,但是如果我们不通过effect仍然可以修改这个标签,也就是说effect函数直接或者间接影响了其他函数的执行结果,这个产生的作用就叫做副作用,又例如第二个例子我们的effect修改了全局的val变量,这同样对全局变量产生了影响,这也称之为副作用,那么响应式数据是什么呢

const obj = {
    text:'hello user'
}
function effect(){
   document.getElementByTagName('div')[0].innerText = obj.text
}

当我们obj里面的text内容更新时候,我们同样希望我们页面中这个位置的内容也被替换掉.上述讲了在Vue2中我们通过defineProperty来对Obj设置getter setter 访问器从而达到响应式的目的,那么Vue3中又是怎么做的呢?

image.png

响应式数据的基本实现

首先我们对上面描述内容进行一个思考

  • 当我们去修改obj中text属性的值的时候,我们会对obj.text产生一个 读取和设置 操作
  • 同样当我们去执行effect函数时候,我们就会产生一个 读取 操作

此时我们的读取操作执行的effect函数其实就是一个副作用函数,而我们通常期望的就是,副作用交给用户自己来处理,从而确保我们系统的容错率.因此我们通过这个理念可以想,我们把副作用函数假设放到一个容器里面,当我们需要操作的时候再把它拿出来进行使用,这样我们通过提供一个容器来确保我们系统完善,带着这个思想我们可以用Proxy进行一个简单的实现

// 储存副作用的容器
const bucket = new Set()
const data = { text:'hello world' }
const obj = new Proxy(data,{
    get(target,key){
        // 储存副作用函数
        bucket.add(effect)
        return target[key]
    },
    set(target,key,val){
        target[key] = val
        // 当我们设置新值的时候 取出副作用函数并执行
        bucket.forEach(fn=>fn())
        return true
    }
})
// 副作用函数
const effect =()=>{ document.body.innerText = obj.text }
// 先来手动触发一次 用来放进我们容器里面
effect()
// 我们模拟请求或者是一些延迟数据去修改一下obj.text 触发我们的设置操作 并取出我们的副作用函数
seTimeout(()=>{
    obj.text = 'hello newVal'
},2000)

这样一来我们就完成了一个基础的响应式数据,那么我们还有那些细节没有考虑到呢?比如说如果我们副作用函数是个匿名函数那该怎么办呢?带着这个问题,我们往下看.

完善一下我们的响应式系统

  1. 首先我们要考虑的就是上述提及到的,如果我们是一个匿名函数怎么办

    // 用一个全局变量来储存我们的副作用函数
    let acctiveEffect
    // effect 函数来储存副作用函数
    function effect(fn){
        acctiveEffect = fn
        fn()
    }
    // 接下来我们试一下传入一个匿名的副作用函数
    effect(()=>{
        docuemnt.body.innerText = obj.text
    })
    

    此时还需要更改下我们的响应式系统代码

    const bucket = new Set()
    const data = { text:'hello world' }
    const obj = new Proxy(data,{
        get(target,key){
            // 储存副作用函数
            if(acctiveEffect){
            bucket.add(activeEffect)            
            }
            return target[key]
        },
        set(target,key,val){
            target[key] = val
            // 当我们设置新值的时候 取出副作用函数并执行
            bucket.forEach(fn=>fn())
            return true
        }
    })
    

    那么到此为止,我们也解决了匿名函数的问题,那么又出现了个问题,当我们尝试写一个obj里面原来没有的属性时候该怎么办,到底会不会进行更改呢?

  2. 我们接着来解决新的问题

    // 用一个全局变量来储存我们的副作用函数
    let acctiveEffect
    // effect 函数来储存副作用函数
    function effect(fn){
        acctiveEffect = fn
        fn()
    }
    const bucket = new WeakMap()
    const obj = new Proxy(data,{
        get(target,key){
            // 没有副作用函数时候 不需要保存返回就行
            if(!acctiveEffect)return
         // 从容器中取出我们的depsMap 是一个Map类型  key--->effects   
            let depsMap = bucket.get(target)
            // 不存在的话 我们新建一个 并与target 关联起来
            if(!depsMap){
                bucket.set(target,(depsMap = new Map()))
            }
            // 去取我们的副作用函数集合
            let deps = depsMap.get(key)
            // 如果没有 就新建立一个 并与 key进行关联  
            if(!deps){
                depsMap.set(key,(deps = new Set()))
            }
            // 添加新的副作用函数
            deps.add(activeEffect)
            return target(key)
        },
        set(target,key,val){
            target[key] = val
            const depsMap = bucket.get(target)
            if(!depsMap)return
            const effects = depsMap.get(key)
            effects && effects.forEach(fn=>fn())
        }
    })
    

    到了此步,我们的响应式系统已经完善了大部分,那么我们该考虑如何能把一些可以公用的函数进行抽离.

  3. 抽离可复用的函数

    const obj = new Proxy(data,{
        get(target,key){
            track(target,key)
        	return target[key]
        },
        set(target,key,val){
            target[key] = val
            trigger(target,key)
        }
    })
    function track(target,key){
          if(!acctiveEffect)return
         // 从容器中取出我们的depsMap 是一个Map类型  key--->effects   
            let depsMap = bucket.get(target)
            // 不存在的话 我们新建一个 并与target 关联起来
            if(!depsMap){
                bucket.set(target,(depsMap = new Map()))
            }
            // 去取我们的副作用函数集合
            let deps = depsMap.get(key)
            // 如果没有 就新建立一个 并与 key进行关联  
            if(!deps){
                depsMap.set(key,(deps = new Set()))
            }
            // 添加新的副作用函数
            deps.add(activeEffect)
    }
    function trigger(targetr,key){
          const depsMap = bucket.get(target)
          if(!depsMap)return
          const effects = depsMap.get(key)
          effects && effects.forEach(fn=>fn())
    }
    

    此时我们也抽离了相关的函数,那么又一个问题出现了,当我们出现通过状态更改值时候怎么办?例如:

    const data = {ok:true,text:'hello world'}
    effect(()=>{
        obj.text = obj.ok === true ? 'hello world' : 'hello dasha'
    })
    

    此时如果说我们初始值设定完毕后,当我们更改obj.ok时候,我们期望text也跟着变化,显然目前我们的响应式是无法满足的,那么我们该如何去做呢?

image.png 4. 分支切换和CleanUp

// 用一个全局变量来储存我们的副作用函数
let acctiveEffect
// effect 函数来储存副作用函数
function effect(fn){
    const effectFn = () => {
        acctiveEffect = fn
    	fn()
    }
    // 新建一个依赖集合 用来收集当前相关的依赖函数
    effectFn.deps = []
    effectFn()
}
function track(target,key){
      if(!acctiveEffect)return
     // 从容器中取出我们的depsMap 是一个Map类型  key--->effects   
        let depsMap = bucket.get(target)
        // 不存在的话 我们新建一个 并与target 关联起来
        if(!depsMap){
            bucket.set(target,(depsMap = new Map()))
        }
        // 去取我们的副作用函数集合
        let deps = depsMap.get(key)
        // 如果没有 就新建立一个 并与 key进行关联  
        if(!deps){
            depsMap.set(key,(deps = new Set()))
        }
        // 添加新的副作用函数
        deps.add(activeEffect)
 		// 将我们当前存在依赖关系的副作用函数保存到我们刚才建的依赖集合中   
        activeEffect.deps.push(deps)
}

有了这个依赖联系之后,我们可以在每次副作用函数执行的时候,根据effectFn.deps获取相关依赖集合,并将副作用函数从依赖中移除

// 用一个全局变量来储存我们的副作用函数
let acctiveEffect
// effect 函数来储存副作用函数
function effect(fn){
    const effectFn = () => {
		cleanUp(effectFn) 
        acctiveEffect = fn
    	fn()
    }
    // 新建一个依赖集合 用来收集当前相关的依赖函数
    effectFn.deps = []
    effectFn()
}
function cleanUp(effectFn){
    for(let i = 0; i < effectFn.length; i++){
        const deps = effectFn.deps[i]
        deps.delete(effectFn)
    }
    effectFn.deps.length = 0
}

但是现在当我们运行时候会出现死循环调用,这是因为

function trigger(target,key){
      const depsMap = bucket.get(target)
      if(!depsMap)return
      const effects = depsMap.get(key)
      effects && effects.forEach(fn=>fn())// 问题在这个代码中
}
// 例子
let set = new Set(6)
set.forEach(v=>{
    set.delete(6)
    set.add(6)
    // 死循环
})
// 我们应该怎么做呢
function trigger(target,key){
      const depsMap = bucket.get(target)
      if(!depsMap)return
      const effects = depsMap.get(key)
      const effectsRun = new Set(effects)
      effectsRun.forEach(fn=>fn())
}

这时候看似我们代码已经完善了,但是如果我们想要嵌套副作用函数,该怎么办呢?例如:

effection(()=>{
    obj.text1 = '1'
    effection(()=>{
        obj.ext2 = '2'
    })
})
// 具体业务场景 比如

const Foo = {
    render:()=>{
        return '<div></div>'
    }
}
const Bar = {
    render:()=>{
        return <Foo>
    }
}
  1. 副作用函数嵌套

    // 用一个全局变量来储存我们的副作用函数
    let acctiveEffect
    const effectStack = []
    // effect 函数来储存副作用函数
    function effect(fn){
        const effectFn = () => {
    		cleanUp(effectFn) 
            acctiveEffect = fn
            effectStack.push(effectFn)
        	fn()
            effectStack.pop()
            acctiveEffect = effectStack[effectStack.length-1]
        }
        // 新建一个依赖集合 用来收集当前相关的依赖函数
        effectFn.deps = []
        effectFn()
    }
    
    

    到此为止,我们还需要在设置下避免无线循环调用,栈溢出的事情,例如:

    effect(()=>obj.txt++)
    
  2. 解决调用栈溢出

    function trigger(target,key){
          const depsMap = bucket.get(target)
          if(!depsMap)return
          const effects = depsMap.get(key)
          const effectsRun = new Set()
          effects && effects.forEach(fn=>{
          	if(fn!==activeEffect){
          	   effectsRun.add(effectFn)
          	}
          })
      	  effectsRun.forEach(fn=>fn())
    }
    

    看似已经完善了不少,但是还有一个主要概念,调度器,如果我们想控制执行顺序怎么办?

  3. 调度执行

    effect(
    ()=>{
        
    },
    options:{
             scheduler(fn){
            //调度器
        		setTimeout(fn)
            }
        }
    )
    function trigger(target,key){
          const depsMap = bucket.get(target)
          if(!depsMap)return
          const effects = depsMap.get(key)
          const effectsRun = new Set()
          effects && effects.forEach(fn=>{
          	if(fn!==activeEffect){
          	   effectsRun.add(effectFn)
          	}
          })
      	  effectsRun.forEach(fn=>{
              fn.options.scheduler
                  ? fn.options.scheduler(fn)
              	  : fn()
          })
    }
    
    // 实现调度器
    const jobQueue = new Set()
    const p = promise.resolve()
    let isResolve = false
    const flushJob = () => {
        if(isResolve) return;
        isResolve = true
        p.then(()=>{
          jobQueue.forEach(job=>job())  
        }).finally(()=>{
            isResolve = false
        })
    }
    
    const options = {
             scheduler(fn){
            //调度器
                 jobQueue.add(fn)
                 flushJob()
            }
        }
    

此外就是一些竟态问题,lazy懒加载问题的处理,感兴趣的可以留言,我们一起研究探讨

image.png