VUE3对象和数组完整响应原理代码和详解

/*
*原理:当触发数据读取操作时,执行副作用函数并存储到桶中
*当设置数据操作时,再将副作用函数从桶中取出并执行
*/
//用一个全局变量activeEffect存储被注册过的副作用函数
let activeEffect
//const buket=new Set()
/*
*weakMap为弱引用,不影响垃圾回收机制工作,当用户代码对一个
*对象没有引用关系时,垃圾会收器会回收该对象,避免引起栈堆的溢出
*/
const bucket=new WeakMap()
const effectStack=[]
//定义一个宏任务队列
const jobQueue=new Set()
//定义一个Promose,将一个任务添加到微任务队列
const p=Promise.resolve()
//是否正在刷新队列
let isFlushing=false
//Symbol唯一,可以作为对象属性标识符使用
const ITERATE_KEY=Symbol()
//定义一个map实例,存储原始对象到代理对象的映射
const reactiveMap=new Map()
//代表是否追踪
let shouldTrack=true
//重写arr.includes,indexOf,lastIndexOf方法
const arrayInstumentations={}
;['includes','indexof','lastIndexof'].forEach(method=>{
    const originMethod=Array.prototype[method]
    arrayInstumentations[method]=function(...args){
        //先在代理对象中查找,this是代理对象,将结果保存到res中
        let res=originMethod.apply(this,args)
        //如果res不存在,说明this.raw拿到了原始数组,再去其中查找,并跟新res的值
        if(res===false){
            res=originMethod.apply(this.raw,args)
        }
        return res
    }
})

//重写push方法
;['push','pop','shift','unshift','splice'].forEach(method=>{
    const originMethod=Array.prototype[method]
    arrayInstumentations[method]=function(...args){
        //在调用原始方法之前,禁止追踪
        shouldTrack=false
        //调用原始防范
        let res=originMethod.apply(this,args)
        //调用原始方法之后,允许追踪
        shouldTrack=true
        return res
    }
})

/*
const data={
    foo:1,bar:2,
    get tep(){
        return this.foo
    }
}
*/
/*
const obj={}
const proto={bar:1}
const child=reactive(obj)
const parent=reactive(proto)
//使用parent作为child的原型
Object.setPrototypeOf(child,parent)

//此处打印true,因为代理对象可以通过raw属性读取原始数据
console.dir(child.raw===obj)
console.dir(parent.raw===proto)

effect(()=>{
    //会执行两次
    console.log(child.bar)
})
*/
/*
let arr=[1,2,3]
//const obj=readonly(arr)
const obj=reactive(arr)

//重新建立副作用函数
effect(
    ()=>{
        for(const key of obj){
            console.log(key)
            document.getElementById('test').innerHTML=key
        }
    }
)
setTimeout(()=>obj[3]=10,1000)
*/


//封装代理对象
//isShallow浅响应,isReadonly,浅只读
function createReactive(obj,isShallow=false,isReadonly=false){
    return new Proxy(obj,{
        //对原始数据的代理
        //拦截读取操作
        get(target,key,receiver){
            //代理对象可以通过raw属性访问原始数据
            if(key==='raw'){
                return target
            }
            /*
            *重写arr.includes方法
            *如果定义的目标是数组,并且key存在于arrayInstrumentations上
            *那么返回定义再arrayInstrumentations上的值
            *执行函数时,实际执行的是定义再arrayInstrumentations上的includes方法
            */
            if(Array.isArray(target) && arrayInstumentations.hasOwnProperty(key)){
                return Reflect.get(arrayInstumentations,key,receiver)
            }
            /*
            *因为数组的for of会读取symbol.iterator属性
            *为避免错误和性能开销,要避免副作用函数与之建立响应
            *如果key的类型是symbol则不进行追踪
            */
            if(!isReadonly && typeof key!=='symbol'){
                track(target,key)
            }
            //返回属性值
            //如果对象自身不存在该属性,会从对象原型寻找对应属性,并调用原型get方法得到最终结果
            const res=Reflect.get(target,key,receiver)
            if(isShallow){
                return res
            }
            //深响应
            /*
            *对于obj.foo.bar来说,当修改obj.foo.bar的值时,并不能触发响应
            *为了解决这个问题,需要递归地调用reactive函数,直到能返回深响应地数据
            */
            if(typeof res==='object'  && res!==null){
                /*
                *实现深只读
                *如果是只读对象,则调用readyonly对数据,返回只读对象
                */
                return isReadonly?readonly(res):reactive(res)
            }
            return res
        },
        //拦击in操作符读取属性值
        has(target,key){
            track(target,key)
            return Reflect.has(target,key)
        },
        //拦截for in 循环读取属性值
        ownKeys(target){
            //将副作用函数和ITERATE_KEY关联
            //如果操作的是数组,则用length作为key并建立响应
            track(target,Array.isArray(target)?'length':ITERATE_KEY)
            return Reflect.ownKeys(target)
        },
        //拦截设置操作
        set(target,key,newvalue,receiver){
            //数据是只读的,则打印错误并返回
            if(isReadonly){
                console.warn(`属性${key}是只读的`)
                return true
            }
            //获取旧值
            const oldval=target[key]
            
            //如果属性不存在,说明是添加新属性,否则是设置已有属性
            //Object.prototype.hasOwnProperty检查当前操作的属性是否已经存在对象身上
            /*
            *如果代理目标是数组,检测被设置的索引是否小于数组长度
            *如果是,为SET操作,否则ADD操作
            */
            const type=Array.isArray(target)
                ?Number(key){
            if(fn!==activeEffect){
                effectsToRun.add(fn)
            }
        })
    }
    /*
    *当直接设置了数组的length属性时,只需要对大于新length的元组进行操作即可
    *如果操作的是数组的length属性
    *那么取出大于新length的所有元素对应的副作用函数执行
    */
    if(Array.isArray(target) && key==='length'){
        depsMap.forEach((effects,key)=>{
            if(key>=newValue){
                effects.forEach(fn=>{
                    if(fn!==activeEffect){
                        effectsToRun.add(fn)
                    }
                })
            }
        })
    }
    
    //取得与INTERATE_KEY相关的副作用函数
    const interateEffects=depsMap.get(ITERATE_KEY)
    //避免自增导致无限循环
    //ECMA规范:再调用foreach遍历set集合时,如果一个值已经被访问过
    //但这个值被删除并重新添加到集合,如果遍历没有结束,那么这个值
    //又会重新被访问,解决办法是建立一个新的Set来遍历
    effects && effects.forEach(f=>{
        if(f!=effectsToRun){
            effectsToRun.add(f)
        }
    })
    //将ITERATE_KEY相关联的副作用函数6添加到effectsToRun
    //删除属性会导致ITERATE_KEY减少,所以需要重新触发
    if(type==='ADD' || type==='DELETE'){
        interateEffects && interateEffects.forEach(effect=>{
            if(effect!==activeEffect){
                effectsToRun.add(effect)
            }
        })
    }
    effectsToRun.forEach(fn=>{
        //如果副作用函数存在调度函数,那么执行调度函数,否则执行原函数
        if(fn.options.scheduler){
            fn.options.scheduler(fn)
        }else{
            fn()
        }
    })
}

//通过修改第二个参数来实现只读深浅,此处浅只读
function readonly(obj){
    return createReactive(obj,false,true)
}
//此处深只读
function shallowReadonly(obg){
    return createReactive(obj,true,true)
}



//当如下代码运行时,因为reactive是深响应,所以会返回false
//这是重复创建了代理对象的问题
//并且使用了map之后,includes(obj)还是false
//因为includes内部的this指向的是代理对象arr
const obj={}
const arr=reactive([obj])
console.log(arr.includes(arr[0]))  //false
console.log(arr.includes(obj))  //false

//深响应
function reactive(obj){
    //从map中查找之前创建的代理对象。
    const existionProxy=reactiveMap.get(obj)
    if(existionProxy) return existionProxy
    //创建新的代理对象
    const proxy=createReactive(obj)
    //将代理对象存储到Map中
    reactiveMap.set(obj,proxy)
    return proxy
}
//实现浅响应
function shallowReactive(obj){
    return createReactive(obj,true)
}

//options对象动态调度副作用函数的执行时机
function effect(fn,options={}){
    const effectFn=()=>{
        //例如effet(function effectFn(){document.body.inntext=obj.ok?obj.text:'not'})
        //清除工作
        cleanup(effectFn)
        //存储被注册过的副作用函数
        activeEffect=effectFn
        //嵌套的副作用函数
        //在调用副作用函数前将其压入栈中,首先压入的内层副作用函数
        effectStack.push(effectFn)
        let res=fn()
        //调用完之后,将其弹出栈,弹出内层的副作用函数
        effectStack.pop()
        activeEffect=effectStack[effectStack.length-1]
        //返回fn的结果
        return res
    }
    //存储与该副作用相关的依赖集合
    effectFn.deps=[]
    //将options挂在到副作用函数
    effectFn.options=options
    if(!options.lazy) effectFn()
    return effectFn
}

function cleanup(effectFn){
    //遍历副作用函数的deps数组
    for(let i=0;i{console.log(obj.foo)},
    scheduler(fn){
        //执行调度时,将其添加到微任务队列
        jobQueue.add(fn)
        //刷新队列
        flushJob()
    }
)
obj.foo++
obj.foo++
*最终输出
1
3
*微任务队列最终执行的只有一次,而此时obj.foo的值已经是3.
*/
function flushJob(){
    //如果正在刷新任务队列,什么都不做,否则isFlushing=true
    if(isFlushing) return
    isFlushing=true
    //将任务添加到微任务队列
    p.then(()=>{
        jobQueue.forEach(job=>job())
    }).finally(()=>{isFlushing=false})
}

/*
*计算属性与懒执行
*/

function computed(getter){
    let value
    //是否需要重新计算值,true代表需要计算
    let dirty=true
    //只有调用value的时候才会执行
    const effectFn=effect(getter,{
        //不执行
        lazy:true,
        //当值发生变化时,在跳读器中重新设置diarty。
        scheduler(){
            if(!dirty){
                dirty=true
                //当计算属性依赖的响应数据发生变化时,手动调用函数触发响应
                trigger(obj, 'value')
            }
        }
    })
    const obj={
        get value(){
            if(dirty){
                //执行副作用函数
                value=effectFn()
                //设置为false,下次访问时,直接使用原来的值
                dirty=false
            }
            //当读取value时,手动调用track函数进行追踪
            track(obj, 'value')
            //返回值为fn的值
            return value
        }
    }
    return obj
}

/*
*wach的实现原理
*当数据发生变化时,执行回调
*/
function watch(source,cb,options={}){
    let getter
    //如果source是函数,则执行函数,否则调用traverse函数递归地读取属性
    if(typeof source==='function'){
        getter=source
    }else{
        getter=()=>traverse(source)
    }
    //旧值与新值
    let oldValue,newValue
     let cleanup
    function onInvalidate(fn){
        cleanup=fn
    } 
    //对scheduler函数的封装
    const job=()=>{
        newValue=effectFn()
         if(cleanup){
            cleanup()
        }
        //返回旧值,新值,已经回调给用户使用
        cb(newValue,oldValue,onInvalidate)
        //已经触发了回调函数,所以这里重新赋值
        oldValue=newValue
    }
    
    //出发操作,建立回调
    const effectFn=effect(
        //调用函数递归地读取数据
        ()=>getter()
    ,{
        lazy:true,
        //调度函数
        scheduler:()=>{
            //创建微任务队列,再DOM加载完成后再执行
            if(options.flush==='post'){
                const p=Promise.resolve()
                p.then(job)
            }else{
                job()
            }
        }
    })
    if(options.immediate){
        job()
    }else{
        //调用副作用函数,拿到旧值
        oldValue=effectFn()
    }
}

function traverse(value,seen=new Set()){
    //如果数据是原始值或者已经被读取过了,则什么都不做
    if(typeof value!=='object' || value===null || seen.has(value)) return
    seen.add(value)
    //堆对象内部地属性,递归地读取数据
    for(const k in value){
        traverse(value[k],seen)
    }
    return value
}

/*
watch(()=>obj.foo,(newValue,oldValue)=>alert(oldValue+':'+newValue))
setTimeout(()=>obj.foo++,1000)

const sum=computed(()=>{
    document.getElementById('test').innerHTML=obj.tep
})

//重新建立副作用函数
effect(function effectFn(){
    sum.value
})
*/
VUE3对象和数组完整响应原理代码和详解_第1张图片

你可能感兴趣的:(javascript,vue.js,开发语言,前端)