Vue3 reactive原理(一)-代理对象及数组

Proxy 只能拦截对一个对象的基本操作(例如读取、设置属性值),而无法拦截复合操作(例如,obj.fun(),由两个基本操作组成,1)get到fun这个属性,2)函数调用)。

1 代理对象

|--------------------------------|-----------------|--------------------|
| 操作 | 跟踪方法 | 触发方法 |
| 访问属性,obj.name | get(traget,p) | set、delete |
| 判断对象或原型上是否存在给定的key, key in obj | has(target,p) | delete,set |
| 使用for...in遍历对象 | ownkeys(target) | delete,set(添加新属性时) |

表 普通对象所有可能的读取操作

ownkeys方法没有具体的属性,这时我们指定一个唯一符号:ITERATE_KEY。来将它与副作用函数关联。

const ITERATE_KEY = Symbol("ITERATE_KEY")
return new Proxy(obj,{
    // 省略其他代码
    set(target, p, newValue, receiver) {
      const type = Object.prototype.hasOwnProperty.call(target,p) ? 'SET' : 'ADD'
      const res = Reflect.set(target,p,newValue,receiver)
      trigger(target,p,type,newValue)
      return res
    },
    deleteProperty(target, p) {
      const res = Reflect.deleteProperty(target,p)
      trigger(target,p,'DELETE')
      return res  
    },
    has(target, p) {
      track(target,p)
      return Reflect.has(target,p)
    },
    ownKeys(target) {
      track(target,ITERATE_KEY)
      return Reflect.ownKeys(target)
    }
})
function trigger(target,p,type,newValue) {
    const map = effectMap.get(target)
    if (map) {
        const addSet = new Set()
        // 省略其他代码
        if (type === 'ADD') {
            const tempSet = map.get(ITERATE_KEY)
            tempSet && tempSet.forEach(fn => {
                if (activeEffect !== fn) addSet.add(fn)
            })
        }
        addSet.forEach(fn => fn())
    }
}

1.1 合理触发

为了提供性能及用户交互,应当满足下列条件时,才触发响应:

  1. 值发生改变时。

  2. 初始值和新值不都是NaN。

  3. 响应对象的原型也说响应对象时,访问原型上的属性时,只触发一次。

    return new Proxy(obj,{
    get(target, p, receiver) {
    if (p === 'raw') return target
    // 省略其他代码
    },
    set(target, p, newValue, receiver) {
    // 省略其他代码
    if (receiver.raw === target) {
    if (oldVal !== newValue && (newValue === newValue || oldVal === oldVal)) {
    trigger(target,p,type)
    }
    }
    },
    deleteProperty(target, p) {
    const hadKey = Object.prototype.hasOwnProperty.call(target,p)
    const res = Reflect.deleteProperty(target,p)
    if (hadKey && res) {
    trigger(target,p,'DELETE')
    }
    },
    })

1.2 深响应与浅响应

目前的reactive函数创建的响应对象是浅响应,即对象的首层才具有响应性。如果对象的某个属性值是个对象,那么该对象不具备响应性。而深响应是指无论多少层,对象都具有响应性。

function createReactive(obj,isShallow = false) {
    if (obj.raw) return obj
    return new Proxy(obj,{
        get(target, p, receiver) {
            // 省略其他代码
            const val = Reflect.get(target,p,receiver)
            if (isShallow) return val
            return typeof val === 'object' && val != null ? createReactive(val) : val
        },
        // 省略其他代码
    })
}

1.2.1 深只读和浅只读

某些数据要是只读的(例如props),当用户尝试修改只读数据时,会收到一条警告信息。浅只读是指,对象的首层不可写,但是其他层可写(对象的一个属性值也是对象时,那么这个属性值对象里的属性是可写的)。

function createReactive(obj,isShallow = false,isReadonly) {
    if (obj.raw) return obj
    return new Proxy(obj,{
        get(target, p, receiver) {
            // 省略其他代码
            const val = Reflect.get(target,p,receiver)
            if (isShallow) return val
            return typeof val === 'object' && val != null ? (isReadonly ? readonly(val) :  createReactive(val)) : val
        },
        set(target, p, newValue, receiver) {
            if (isReadonly) {
                console.error('该属性不可写')
                return
            }
            // 省略其他代码
        },
        deleteProperty(target, p) {
            if (isReadonly) {
                console.error('该属性不可写')
                return
            }
            // 省略其他代码
        },
        // 省略其他代码
    })
}

function readonly(obj,isShallow = false) {
    return createReactive(obj,isShallow,true)
}

2 代理数组

数组也是一种对象(但属于异质对象,与常规对象相比,它们的部分内部方法和常规对象的不同)。因此用于代理普通对象的大部份代码可以继续使用。

2.1 索引与length

  1. 通过索引设置元素值时,可能会影响到length属性,即当设置索引值大等于数组长度时,length属性会发生改变。

  2. 设置length属性,可能会影响到索引值。当length设置为更小值时,索引大等于length的部分元素全部会被删除。

    new Proxy(obj,{
    set(target, p, newValue, receiver) {
    // 省略其他代码
    const type = Array.isArray(target) ? (Number(p) < target.length ? 'SET' : 'ADD') :
    (Object.prototype.hasOwnProperty.call(target,p) ? 'SET' : 'ADD')
    // 省略其他代码
    },
    // 省略其他代码
    })

    function trigger(target,p,type,newValue) {
    const map = effectMap.get(target)
    if (map) {
    // 省略其他代码
    if (type === 'ADD' && Array.isArray(target)) {
    const tempSet = map.get("length")
    tempSet && tempSet.forEach(fn => {
    if (activeEffect !== fn) addSet.add(fn)
    })
    }

         if (p === 'length' && Array.isArray(target)) {
             map.forEach((set,key) => {
                 if (key >= newValue) {
                     set.forEach(fn => {
                         if (activeEffect !== fn) addSet.add(fn)
                     })
                 }
             })
         }
         addSet.forEach(fn => fn())
     }
    

    }

2.2 遍历数组

1)for...in,和普通对象一样,内部会调用ownKeys方法,但不同的是,其触发条件是length的改变。

new Proxy(obj,{
    ownKeys(target) {
       track(target,Array.isArray(target) ? 'length' : ITERATE_KEY)
       return Reflect.ownKeys(target)
    }
    // 省略其他代码
})

2)for...of,索引值的设置及length的改变,都会触发该迭代,所以几乎不要添加额外的代码,就能让for...for迭代具有响应性。

从性能上及为了避免发生意外的错误,我们不应该使副作用函数与symbol值之间建立响应联系。

2.3 数组的查找方法

数组的方法内部其实都依赖了对象的基本语义。因此大多数情况下,我们不需要做特殊处理即可让这些方法按预期工作。但某些场景,执行结果会不符合我们预期:

const proxyObj = reactive(['hi','js',{name: 'js'}])

console.log(proxyObj.includes('hi'),proxyObj.includes(proxyObj[2])) // true、false

这里,查找基本类型数据时,结果是正确的。但是查找对象时,结果错误。这是因为reactive函数会为属性中的对象也创建响应对象,而且每次都会创建新的响应对象。而且,这里还有两个问题:

  1. 将响应体对象赋值给另一个响应体时,reactive不应该为其再创建响应体了。

  2. 无论查找对象,还是其响应体对象,返回的结果应该一致(从业务的角度看,它们属于同一对象)。

    const reactiveMap = new WeakMap()

    new Proxy(obj,{
    get(target, p, receiver) {
    if (p === 'raw') return target
    // 省略其他代码
    if (Array.isArray(target) && arrayInstrumentation.hasOwnProperty(p)) {
    return Reflect.get(arrayInstrumentation,p,receiver)
    }
    // 省略其他代码
    },
    })

    function reactive(obj) {
    const originalProxy = reactiveMap.get(obj)
    if (originalProxy) return originalProxy;
    const proxyObj = createReactive(obj)
    reactiveMap.set(obj,proxyObj)
    return proxyObj
    }

    const arrayInstrumentation = {};
    ['includes','indexOf','lastIndexOf'].forEach(method => {
    const originalMethod = Array.prototype[method]
    arrayInstrumentation[method] = function (...args) {
    let res = originalMethod.apply(this,args)
    if (res === false || res === -1) {
    res = originalMethod.apply(this.raw,args)
    }
    return res
    }
    });

2.4 隐式修改数组长度的原型方法

push/pop/shift/unshift/splice这些方法会隐式地修改数组长度。例如push方法,在往数组中添加元素时,既会读取length,也会设置数组的length。这会导致两个独立的副作用函数互相影响:

effect(() => {
    proxyObj.push(1)
})
effect(() => {
    proxyObj.push(2)
})

运行上面这段代码,会得到栈溢出的错误。因为副作用函数1执行时,会修改及读取length,会触发副作用函数2执行,而副作用2也会修改和读取length。这样就好造成循环调用。

解决方法是:"屏蔽"这些方法对length属性的读取。

['push','pop','shift','unshift','splice'].forEach(method => {
    const originalMethod = Array.prototype[method]
    arrayInstrumentation[method] = function (...args) {
        shouldTrack = false
        const res = originalMethod.apply(this,args)
        shouldTrack = true
        return res
    }
})

function track(target,p) {
    if (activeEffect && shouldTrack) {
        // 跟踪代码
    }
}
相关推荐
旧林843几秒前
第八章 利用CSS制作导航菜单
前端·css
yngsqq12 分钟前
c#使用高版本8.0步骤
java·前端·c#
Myli_ing1 小时前
考研倒计时-配色+1
前端·javascript·考研
余道各努力,千里自同风1 小时前
前端 vue 如何区分开发环境
前端·javascript·vue.js
PandaCave1 小时前
vue工程运行、构建、引用环境参数学习记录
javascript·vue.js·学习
软件小伟1 小时前
Vue3+element-plus 实现中英文切换(Vue-i18n组件的使用)
前端·javascript·vue.js
醉の虾1 小时前
Vue3 使用v-for 渲染列表数据后更新
前端·javascript·vue.js
张小小大智慧1 小时前
TypeScript 的发展与基本语法
前端·javascript·typescript
hummhumm2 小时前
第 22 章 - Go语言 测试与基准测试
java·大数据·开发语言·前端·python·golang·log4j
asleep7012 小时前
第8章利用CSS制作导航菜单
前端·css