vue3.0中的数据监听、响应式原理简析

实现可数据可响应的方式

通过可响应对象,实现对数据的侦测,从而告知外界数据变化。

  1. 单一的访问器getter和setter
  2. Object.defineProperty()--实现了vue2.0响应式原理
  3. Proxy--实现3.0版本的原理
vue2.0响应式原理的弊端
  1. 需要对 Object 和 Array 两种类型采用不同的处理方式。

  2. 为了感知 Array 的变化,对 Array 原型上几个改变数组自身的内容的方法做了拦截,虽然实现了对数组的可响应,但同样存在一些问题。vue2.0的数组方法拦截

  3. defineProperty 通过递归实现 getter/setter 也存在一定的性能问题。

vue3.0主要使用的语法
  1. proxy
  2. reflect
  3. WeakMap

1.reflect

Reflect对象与Proxy对象一样,也是拥有get、set方法。

  • Reflect对象的设计目的:
  1. 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上
  2. 修改某些Object方法的返回结果,让其变得更合理
  3. 让Object操作都变成函数行为
  4. Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。

2.关于proxy

Proxy 可以代理数组,并且 API 提供了get 、 set。

let data = { foo: 'foo' }
let p = new Proxy(data, {
  get(target, key, receiver) {
    return target[key]
  },
  set(target, key, value, receiver) {
    console.log(receiver)// receiver是proxy对象本身
    console.log('set value')
    target[key] = value // target就是data对象
  }
})
p.foo = 123 
//Proxy{}
//{foo: "foo"} "set value"
// 此时data被修改,p是代理data的代理对象
let data = [1,2,3]
let p = new Proxy(data, {
  get(target, key, receiver) {
    return target[key]
  },
  set(target, key, value, receiver) {
    console.log('set value')
    target[key] = value
  }
})
p.push(4) // Uncaught TypeError: 'set' on proxy: trap returned falsish for property '3'
  • 产生问题:set也需要返回值
    需要修改为
let data = [1,2,3]
let p = new Proxy(data, {
  get(target, key, receiver) {
    return target[key]
  },
  set(target, key, value, receiver) {
    console.log('set value')
    target[key] = value
    //++
    return true
    //++
  }
})

p.push(4)

// set value // 打印2次

打印两次是因为处理数组时候push不但修改数组的项还修改了length

let data = [1,2,3]
let p = new Proxy(data, {
  get(target, key, receiver) {
    console.log('get value:', key)
    return target[key]
  },
  set(target, key, value, receiver) {
    console.log('set value:', key, value)
    target[key] = value
    return true
  }
})

p.push(1)

// get value: push
// get value: length
// set value: 3 1
// set value: length 4
  • 产生问题:多次触发set和get就会多次触发view的更新,此时只需要触发一次set更新
    可以用类似于 debounce 的操作处理多次执行的问题(vue3.0有更好的方式)
function reactive(data, cb) {
  let timer = null
  return new Proxy(data, {
    get(target, key, receiver) {
      return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
      clearTimeout(timer)
      timer = setTimeout(() => {
        cb && cb()
      }, 0);
      return Reflect.set(target, key, value, receiver)
    }
  })
}

let ary = [1, 2]
let p = reactive(ary, () => {
  console.log('trigger')
})
p.push(3)

// trigger

但是vue3.0提供了更好的方式处理执行重复的问题,利用私有属性
(以下没有完全解决重复问题,对于数组只是避免了length对数组更新的重复渲染,像splice,unshift这种多次修改索引的方法还会触发多次)

function trigger(){
    console.log('触发视图更新')
}
function isObject(target){
    return typeof target==='object'&&target!==null;
}
function reactive(target){
    if(!isObject(target)){
        return target;
    }
    const handler={
        set(target,key,value,receiver){
            console.log('setValue',value)
            //如果触发的是私有属性的话,触发视图的更新
            if(!target.hasOwnProperty(key)){//添加新属性
                trigger();
            }else if(value!==target[key]){//set的值变化,更新属性
                trigger();
            }
            return Reflect.set(target,key,value,receiver)
        },
        get(target,key,receiver){
            const res=Reflect.get(target,key,receiver);
            console.log('getValue',res)
            return res;

        },
        deleteProperty(target,key){
            return Reflect.deleteProperty(target,key);
        }
    }
    let observer=new Proxy(target,handler);
    return observer;
}
let obj=[1,2,3];
let p=reactive(obj)
p.unshift(1);

监听深层的对象

let obj={
  a:[1,2,3],
  b:{c:4}
};
let p=reactive(obj)
p.b.c = 2

// getValue: {c:4} 不触发set
  • 产生问题:proxy 只能代理一层;
    因为没有触发 set 的输出,反而是触发了 get ,因为 set 的过程中访问了 bar 这个属性。
    深度数据监听,可以用 递归 处理问题,把对象的每一层都进行代理(同vue2.0),但是优于2.0,3.0的处理方式是只有使用数据的当前属性的时候才会去代理,2.0则是全部代理,3.0的处理方式节约的大量不必要的工作。
function trigger(){
    console.log('触发视图更新')
}
function isObject(target){
    return typeof target==='object'&&target!==null;
}
function hasOwn(){
    
}
function reactive(target){
    if(!isObject(target)){
        return target;
    }
    const handler={
        set(target,key,value,receiver){
            //如果触发的是私有属性的话,触发视图的更新
            if(!target.hasOwnProperty(key)){//添加新属性
                trigger();
            }else if(value!==target[key]){//set的值变化,更新属性
                trigger();
            }
            return Reflect.set(target,key,value,receiver)
        },
        get(target,key,receiver){
            const res=Reflect.get(target,key,receiver);
            if(isObject(res)){
                return reactive(res)
            }
            return res;
        },
        deleteProperty(target,key){
            return Reflect.deleteProperty(target,key);
        }
    }
    let observer=new Proxy(target,handler);
    return observer;
}
let obj={
    a:[1,2,3],
    b:{c:1}
}
let p=reactive(obj)
p.b.c=4
// 触发视图更新

有些数据并非需要侦测,我们需要对数据侦测做更细的控制,全部监听浪费一部分的性能,vue3.0有更好的解决办法,weakMap。

3.weakMap对象,实现完整的reactive数据监听

WeakMap对象也是键值对的集合。它的键必须是对象类型,值可以是任意类型。它的键被弱保持,也就是说,当其键所指对象没有其他地方引用的时候,它会被GC回收掉。WeakMap提供的接口与Map相同。

  • 我认为使用WeakMap主要是因为
  1. WeakMap的键是对象类型,保持弱引用。
  2. 列表是否存在取决于垃圾回收器的状态,性能比object更高
//++ ----------
const toProxy= new WeakMap()
const toRaw = new WeakMap()
// ++ ----------
function trigger(){
    console.log('触发视图更新')
}
function isObject(target){
    return typeof target==='object'&&target!==null;
}
function reactive(target){
    if(!isObject(target)){
        return target;
    }
// ++ ----------
    //如果代理表中存在target了就说明,target已经被代理过了,就返回代理过的对象
    if(toProxy.get(target)){
        console.log('target已经被代理过了,是一个obj对象')
        return toProxy.get(target)
    }

    if(toRaw.has(target)){//如果这个对象被代理过了,就把对象原封不动的返回
        console.log('target已经被代理过了,是一个proxy对象')
        return target
    }
// ++ ----------
    const handler={
        set(target,key,value,receiver){
            const oldValue = target[key];
            //如果触发的是私有属性的话,触发视图的更新
            if(!target.hasOwnProperty(key)){//添加新属性,用hasOwnProperty是因为可以屏蔽一些无用的属性更新,像数组的length等
                trigger();
            }else if(value!==oldValue){//set的值变化,更新属性
                trigger();
            }
            return Reflect.set(target,key,value,receiver)
        },
        get(target,key,receiver){
            const res=Reflect.get(target,key,receiver);
            if(isObject(res)){
                return reactive(res)
            }
            return res;
        },
        deleteProperty(target,key){
            return Reflect.deleteProperty(target,key);
        }
    }
    let observer=new Proxy(target,handler);
// ++ ----------
    toProxy.set(target,observer);//记录原对象代理过的结果
    toRaw.set(observer,target);
// ++ ----------
    return observer;
}
let obj={
    a:[1,2,3],
    b:{c:1}
}
let obj2={
    a:[1,2,3],
    b:{c:1}
}
let p=reactive(obj)
p=reactive(p);// p是被代理过的对象

let n=reactive(obj2);
let n1=reactive(obj2);//多次代理一个对象obj1=

以上实现了一个简易的数据监听

4. 依赖收集(发布订阅),实现响应式

更新视图主要依赖于effect函数。

let obj=reactive({name:'zf'});
effect(()=>{ // effect会执行两次,默认先执行一次 之后依赖的数据变化了 会再次执行。
  console.log(obj.name)//会调用get方法
});
obj.name='jw';//代理的属性值被修改

effect的作用是首先会执行一次传入的函数,之后如果代理的属性有变化,会继续触发传入effect的函数。
所以此时需要把传入effect的函数包装成一个响应式的函数。

//栈 先进后出
let activeEffectStacks=[]; // 
//响应式  副作用
function effect(fn){
  // 需要把fn这个函数包装成响应式的函数,在把这个函数默认先执行一次
  let effect = createReactiveEffect(fn);
  effect()//默认执行一次
}
function createReactiveEffect(fn){
  let effect=function(){//这个就是创建的响应式的effect
      return run(effect,fn) // 运行 1.让fn执行 2.把这个effect存入到栈中
  }
  return effect;// 返回这个包装好的函数
}
function run (){//运行fn并把effect存起来
  try{//try包裹防止fn内报错
    activeEffectStacks.push(effect);
    fn();//利用了js是单线程的。
    //fn触发,里边读取了proxy的属性,所以会进入到reactive的get中。
  }finally{
    activeEffectStacks.pop();//用完释放
  }
 
}

reactive的get中收集依赖

const toProxy= new WeakMap()
const toRaw = new WeakMap()
function trigger(){
    console.log('触发视图更新')
}
function isObject(target){
    return typeof target==='object'&&target!==null;
}
function reactive(target){
    if(!isObject(target)){
        return target;
    }
    //如果代理表中存在target了就说明,target已经被代理过了,就返回代理过的对象
    if(toProxy.get(target)){
        console.log('target已经被代理过了,是一个obj对象')
        return toProxy.get(target)
    }

    if(toRaw.has(target)){//如果这个对象被代理过了,就把对象原封不动的返回
        console.log('target已经被代理过了,是一个proxy对象')
        return target
    }
    const handler={
        set(target,key,value,receiver){
            // const oldValue = target[key];
            const res = Reflect.set(target, key, value, receiver)
            //如果触发的是私有属性的话,触发视图的更新
            //if(!target.hasOwnProperty(key)){//添加新属性,用hasOwnProperty是因为可以屏蔽一些无用的属性更新,像数组的length等(经测试会引起bug)
//++++++++++++++++++++++++++++
            //    trigger(target,'add',key);
//++++++++++++++++++++++++++++
            //}else if(value!==oldValue){//set的值变化,更新属性
//++++++++++++++++++++++++++++
            //    trigger(target,'set',key);
//++++++++++++++++++++++++++++
            //}
            if (target.hasOwnProperty(key)) {
              trigger(target, '', key);
            }
            return res
        },
        get(target,key,receiver){
            const res=Reflect.get(target,key,receiver);
//++++++++++++++++++++++++++++++++++++++++++++++++
            // 收集依赖 订阅 ,把当前的key和effect对应起来
            track(target,key) // 如果target上的这个key变化了重新让数组中的effect执行即可,所以需要建立target中的key于effect的关联(关联的结构如下图所示)
//++++++++++++++++++++++++++++++++++++++++++++++++            
            if(isObject(res)){
                return reactive(res)
            }
            return res;
        },
        deleteProperty(target,key){
            return Reflect.deleteProperty(target,key);
        }
    }
    let observer=new Proxy(target,handler);
    toProxy.set(target,observer);//记录原对象代理过的结果
    toRaw.set(observer,target);
    return observer;
}

track用于对应target中的key与effect的关联


vue3.0中的数据监听、响应式原理简析_第1张图片
图片来源于jw
let targetMap=new WeakMap();

function track(target,key){//如果这个target中的key变化了 就执行数组里activeEffectStacks的方法。
  let effect=activeEffectStacks[activeEffectStacks.length-1];
  if(effect){//有对应的关系 才创建关联
    let depsMap=targetMap.get(target);
    if(!depsMap){
       targetsMap.set(target,depsMap=new Map())
    }
    let deps=depsMap.get(key);
    if(!deps){
        depsMap.set(key,deps=new Set());
    }
    if(!deps.has(effect)){
      deps.add(effect);
    }
  }
  // 以上是动态创建依赖关系。
}

trigger 触发key对应的effect,更新视图

function trigger(target,type,key){
  let depsMap=targetMap.get(target);
  if(depsMap){
    let deps=depsMap.get(key);
    if(deps){//将当前key对应的effect依此执行
     deps.forEach(effect=>effect())
    }
  }
}

完整vue3.0响应式代码

const toProxy = new WeakMap()
const toRaw = new WeakMap()
function isObject(target) {
  return typeof target === 'object' && target !== null;
}
function reactive(target) {
  if (!isObject(target)) {
    return target;
  }
  // 如果代理表中存在target了就说明,target已经被代理过了,就返回代理过的对象
  if (toProxy.get(target)) {
    console.log('target已经被代理过了,是一个obj对象')
    return toProxy.get(target)
  }
  if (toRaw.has(target)) { // 如果这个对象被代理过了,就把对象原封不动的返回
    console.log('target已经被代理过了,是一个proxy对象')
    return target
  }
  const handler = {
    set(target, key, value, receiver) {
      // const oldValue = target[key];
      const res = Reflect.set(target, key, value, receiver)
      // 如果触发的是私有属性的话,触发视图的更新

      // if (!target.hasOwnProperty(key)) { // 添加新属性,用hasOwnProperty是因为可以屏蔽一些无用的属性更新,像数组的length等(经测试会引起bug)
      //   trigger(target, 'add', key);
      // } else if (value !== oldValue) { // set的值变化,更新属性
      //   console.log(target, key, oldValue, value, 24)
      //  trigger(target, 'set', key);
      // }
      if (target.hasOwnProperty(key)) {
        trigger(target, '', key);
      }
      return res
    },
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver);
      // 收集依赖 订阅 ,把当前的key和effect对应起来
      track(target, key) // 如果target上的这个key变化了重新让数组中的effect执行即可,所以需要建立target中的key于effect的关联
      if (isObject(res)) {
        return reactive(res)
      }
      return res;
    },
  }
  const observer = new Proxy(target, handler);
  toProxy.set(target, observer);// 记录原对象代理过的结果
  toRaw.set(observer, target);
  return observer;
}

// 栈 先进后出
const activeEffectStacks = []; //
// 响应式  副作用
function effect(fn) {
  // 需要把fn这个函数包装成响应式的函数,在把这个函数默认先执行一次
  const effect = createReactiveEffect(fn);
  effect()// 默认执行一次
}
function createReactiveEffect(fn) {
  const effect = function () { // 这个就是创建的响应式的effect
    return run(effect, fn) // 运行 1.让fn执行 2.把这个effect存入到栈中
  }
  return effect;// 返回这个包装好的函数
}
function run(effect, fn) { // 运行fn并把effect存起来
  try { // try包裹防止fn内报错
    activeEffectStacks.push(effect);
    fn();// 利用了js是单线程的。
    // fn触发,里边读取了proxy的属性,所以会进入到reactive的get中。
  } finally {
    activeEffectStacks.pop();// 用完释放
  }
}

const targetMap = new WeakMap();
function track(target, key) { // 如果这个target中的key变化了 就执行数组里activeEffectStacks的方法。
  console.log(activeEffectStacks, target, '-----------track')
  const effect = activeEffectStacks[activeEffectStacks.length - 1];
  if (effect) { // 有对应的关系 才创建关联
    let depsMap = targetMap.get(target);
    if (!depsMap) {
      targetMap.set(target, depsMap = new Map())
    }
    let deps = depsMap.get(key);
    if (!deps) {
      depsMap.set(key, deps = new Set());
    }
    if (!deps.has(effect)) {
      deps.add(effect);
    }
  }
}
// 以上是动态创建依赖关系。
function trigger(target, type, key) {
  const depsMap = targetMap.get(target);
  console.log(target, depsMap, '--------trigger')
  if (depsMap) {
    const deps = depsMap.get(key);
    if (deps) { // 将当前key对应的effect依此执行
      console.log(deps.size)
      deps.forEach(effect => effect())
    }
  }
}

let obj={name:'haha'};
let observer= reactive(obj);
effect(()=>{
  console.log(observer.name);
})
observer.name='xixi';
observer.name='xixi';
//执行两次haha和xixi

(以下不会再次触发更新的情况)

//1.对象只监听到了外层
const obj = { name: { n: 'haha' } };
const observer = reactive(obj);
effect(() => {
  console.log(observer.name, '-------------108');
})
observer.name.n = 'xixi';
//必须这样监听
const obj = { name: { n: 'haha' } };
const observer = reactive(obj);
effect(() => {
  console.log(observer.name.n);
})
observer.name.n = 'xixi';

//2.对象内层的数组操作问题
const obj1 = { a: [1, 2, 3] };
const oarr = reactive(obj1);
effect(() => {
    oarr.a.forEach((item)=>{
        console.log(item,'更新视图');
    })
})
oarr.a[0]=2;
oarr.a.push(4);

原因是:

set(target, key, value, receiver) {
      // const oldValue = target[key];
      const res = Reflect.set(target, key, value, receiver)

      // 添加的新属性或数组的项会导致更新失效
      //   if (!target.hasOwnProperty(key)) { 
      //     // trigger(target, 'add', key);
      //   } else if (value !== oldValue) {
      //     console.log(target, 'key:', key, oldValue, value)
      //     trigger(target, 'set', key);
      //     console.log(value, oldValue)
      //   }
      //以上处理方式需要改为 
      //如果数组方法过滤掉length等属性会出问题
      if (target.hasOwnProperty(key)) {
        trigger(target, '', key);
      }
      return res
    },

备上html测试方式

+

你可能感兴趣的:(vue3.0中的数据监听、响应式原理简析)