Vue3最小响应式核心原理解析

85 阅读10分钟

1 抛出问题

首先,看如下代码:

const obj = {
    text: 'hello, siri',
}
function effect() {
    document.body.innerHTML = obj.text;
};
effect();

effect函数在执行时,页面上的内容变为hello,siri,但是当重新设置obj.text = 'hello,xiaoai'的时候,页面上的内容并不会发生改变,而实际上我们所希望的是当obj.text的值发生改变的时候,对应的effect函数被自动的执行,从而页面内容发生改变

2 实现数据最基本响应式

首先我们可以先分析一下,为什么数据没有发生改变,因为obj是一个普通的对象,所以当重新设置里面的属性发生变化的时候,并不会有任何其他的反应。因此需要做的就是将obj对象变为响应式的数据。

首先,可以发现:

effect函数被执行的时候,触发了obj.text这个字段的读取操作。

在重新设置obj.text = 'hello,xiaoai'的时候,触发了obj.text这个字段的设置操作。

那么假设我们可以去拦截obj这个对象的读取操作和设置操作时,当重新去设置obj这个对象里面的某个字段时,进行一个拦截,在这里触发effect函数的执行操作,实现对象的最基本的响应式。

为了可以针对性的针对某个字段实现一些特定的拦截操作,则在针对这个字段在读取的时候,去讲这些特定操作给存储起来,这样在触发设置操作的时候,则可以将这些特定的操作给拿出来进行一个执行。

如下图所示:

image.png

在ES2015后,可以通过代理对象Proxy进行一个实现,实现对对象属性的读取和设置操作的拦截,代码如下所示:

// 创建一个桶 用于存储特定的某些函数
const bucket = new Set();
const data = {
    text: 'hello, siri',
}
const obj = new Proxy(data, {
    get(target, key) {
      	// 加入桶中
        bucket.add(effect);
        return target[key];
    },
    set(target, key, newVal) {
        // 如果更新的值和原来的值相同 则不做任何操作
        if(newVal === target[key]) {
            return ;
        }
        // 不同 则重新进行赋值
        target[key] = newVal;
        // 同时将桶中存储的函数拿出来,进行执行。
        bucket.forEach(fn =>fn());
        return true;
    }
});
function effect() {
    document.body.innerText = obj.text;
};
effect();
setTimeout(() => {
    obj.text = 'hello, xiaoai';
}, 1000);

 代码解释如下:

首先创建了一个用于存储特定函数的桶bucket,定义了一个对象data,并给其设置了其代理对象,并分别设置get和set的拦截函数,当读取属性值的时候,则将特定函数给存储到桶中,返回属性值,重新设置属性值的时,进行更新的操作,并将桶中的函数逐一拿出来进行执行,实现了对象的响应式。

在执行代码时,首先执行了effect函数,在函数中进行了数据的读取操作: document.body.innerText = obj.text,从而触发get函数的执行,将effect函数给存储到bucket桶中。设置定时器,在定时器中重新设置了obj.text的值: obj.text = 'hello, xiaoai',则触发了set函数,更新属性值,并重新执行effect函数。 效果展示如下链接: stackblitz.com/edit/js-whu…

3 明确特定属性和特定函数之间的联系

修复-特定属性值和特定函数之间的明确联系

但是在这里会存在一个问题,如下代码:

const effect = () => {
  document.body.innerText = obj.text;
  console.log('1');
};
setTimeout(() => {
    obj.newText = 'say hi';
}, 1000);

没有针对特定的属性值进行一个拦截操作,也就是说设置obj的newText属性的时候,特定的函数也会被执行,导致effect函数被执行两次,输出两个1。  而实际上,在这里不应该执行effect函数,该函数和obj.text关联,不与其他属性关联。当重新设置一个新的属性值时,effect函数不应该被执行,

本质上: 没有将特定的字段和特定的函数之间建立一个强联系。

解决方法:重新设置桶的结构,将特定字段和特定函数之间建立一个明确的联系。

// 创建一个桶 用于存储特定的某些函数--修改代码
const bucket = new WeakMap();
const data = {
    text: 'hello, siri',
}
// 修改代码
const obj = new Proxy(data, {
    get(target, key) {
        if(effect) {
            let depsMap = bucket.get(target);
            if(!depsMap) {
                bucket.set(target, (depsMap = new Map()));
            }
          // 取出所有和特定属性值相关的特定函数
            let deps = depsMap.get(key);
            if(!deps) {
                depsMap.set(key, deps = new Set());
            }
            deps.add(effect);  
        }
      return target[key];
    },
    set(target, key, newVal) {
        // 如果更新的值和原来的值相同 则不做任何操作
        if(newVal === target[key]) {
            return ;
        }
        // 不同 则重新进行赋值
        target[key] = newVal;
        // 同时将桶中存储的函数拿出来,进行执行。
        const depsMap = bucket.get(target);
        if(!depsMap) {
            return;
        }
        const deps = depsMap.get(key);
        deps.forEach(fn => fn());
        return true;
    }
});
function effect() {
    document.body.innerText = obj.text;
};
effect();
setTimeout(() => {
    obj.text = 'hello, xiaoai';
}, 1000);

 如下图示意图所示:

image.png

bucket中,由target---> Map构成,其中target是对象,Map是一个Map实例,Map中由key-->Set构成,key表示的是对象中的属性值,Set中包含了一些特定的函数。

4 实现深层次对象的响应式

在上面代码中,是针对对象中一层的属性进行数据的监听,如果对于深层次的对象,无法进行监听,如下代码所示:

// 创建一个桶 用于存储特定的某些函数
const bucket = new WeakMap();
const data = {
  text: 'hello siri',
  a: 'hello, siri',
  b: {
    c: 1,
    d: {
      e: '12',
    },
  },
};
const data = {
  a: 'hello, siri',
  b: {
    c: 1,
    d: {
      e: '12',
    },
  },
};
const effect = () => {
  document.body.innerText = obj.b.c;
  console.log('1');
};
effect();
setTimeout(() => {
  obj.b.c = 3; 
  obj.fn = () => {};
}, 1000);

这时候当修改obj.b.c的值的时候,页面上数据不会发生改变。 解决方法: 递归嵌套属性

// 创建一个桶 用于存储特定的某些函数
const bucket = new WeakMap();
function bindReactive(target) {
  if (typeof target !== 'object' || target == null) {
    // 不是对象或数组,则直接返回
    return target;
  }
  // 因为Proxy原生支持数组,所以这里不需要自己实现
  // if (Array.isArray(target)) {
  //    target.__proto__ = newPrototype
  // }
  // 传给Proxy的handler
  const handler = {
    get(target, key) {
      const reflect = Reflect.get(target, key);
      if (effect) {
        let depsMap = bucket.get(target);
        if (!depsMap) {
          bucket.set(target, (depsMap = new Map()));
        }
        let deps = depsMap.get(key);
        if (!deps) {
          depsMap.set(key, (deps = new Set()));
        }
        deps.add(effect);
      }
      // 当我们获取对象属性时,Proxy只会递归到获取的层级,不会继续递归子层级
      return bindReactive(reflect);
    },
    set(target, key, val) {
      // 重复的数据,不处理
      if (val === target[key]) {
        return true;
      }
      const success = Reflect.set(target, key, val);
      update();
      // 设置成功与否
      const depsMap = bucket.get(target);
      if (!depsMap) {
        return;
      }
      const deps = depsMap.get(key);
      deps && deps.forEach((fn) => fn());
      return success;
    },
    deleteProperty(target, key) {
      const success = Reflect.deleteProperty(target, key);
      // 删除成功与否
      update();
      // 不同 则重新进行赋值
      return success;
    },
  };
  // 生成proxy对象
  const proxy = new Proxy(target, handler);
  return proxy;
}
function update() {
  console.log('999');
}
const effect = () => {
  document.body.innerText = obj.b.c;
  console.log('1');
};
const data = {
  text: 'hello, siri',
  b: {
    c: 1,
    d: {
      e: '12',
    },
  },
};
const obj = bindReactive(data);
effect();
setTimeout(() => {
  obj.b.c = 3;
}, 1000);

5 优化

5.1 代码优化

5.1.1 函数的优化:

在上面代码中,effect函数是一个具体名字的函数,可以进行一个优化,将该函数转化为一个无论是任何名字或是匿名函数,都可以被收集到桶中。

// 创建一个桶 用于存储特定的某些函数
const bucket = new WeakMap();
function bindReactive(target) {
  if (typeof target !== 'object' || target == null) {
    // 不是对象或数组,则直接返回
    return target;
  }
  // 因为Proxy原生支持数组,所以这里不需要自己实现
  // if (Array.isArray(target)) {
  //    target.__proto__ = newPrototype
  // }
  // 传给Proxy的handler
  const handler = {
    get(target, key) {
      const reflect = Reflect.get(target, key);
      if (activeEffect) {
        let depsMap = bucket.get(target);
        if (!depsMap) {
          bucket.set(target, (depsMap = new Map()));
        }
        let deps = depsMap.get(key);
        if (!deps) {
          depsMap.set(key, (deps = new Set()));
        }
        deps.add(activeEffect);
      }
      // 当我们获取对象属性时,Proxy只会递归到获取的层级,不会继续递归子层级
      return bindReactive(reflect);
    },
    set(target, key, val) {
      // 重复的数据,不处理
      if (val === target[key]) {
        return true;
      }
      const success = Reflect.set(target, key, val);
      update();
      // 设置成功与否
      const depsMap = bucket.get(target);
      if (!depsMap) {
        return;
      }
      const deps = depsMap.get(key);
      deps && deps.forEach((fn) => fn());
      return success;
    },
    deleteProperty(target, key) {
      const success = Reflect.deleteProperty(target, key);
      // 删除成功与否
      update();
      // 不同 则重新进行赋值
      return success;
    },
  };
  // 生成proxy对象
  const proxy = new Proxy(target, handler);
  return proxy;
}
function update() {
  console.log('999');
}

const data = {
  a: 'hello, siri',
  b: {
    c: 1,
    d: {
      e: '12',
    },
  },
};
const obj = bindReactive(data);
setTimeout(() => {
  obj.b.c = 3;
}, 1000);

// ---修改
let activeEffect;
function effect(fn) {
  // 进行赋值
  activeEffect = fn;
  // 执行函数
  fn();
}
// 修改 这时候 可以传递任意函数 -这里传递了一个匿名函数
effect(() => {
  document.body.innerText = obj.b.c;
  console.log('1');
});
setTimeout(() => {
  obj.b.c = 3;
}, 1000);

 5.1.2 逻辑抽取

为了可以使得代码逻辑清晰,将特定函数给收集到桶中该逻辑从get函数中给抽取出来, 抽取为: collectFn函数 ,将重新设置属性值触发特定函数的执行逻辑从set函数中抽离出来,抽取为: trigger函数

// 创建一个桶 用于存储特定的某些函数
const bucket = new WeakMap();
function bindReactive(target) {
  if (typeof target !== 'object' || target == null) {
    // 不是对象或数组,则直接返回
    return target;
  }
  // 因为Proxy原生支持数组,所以这里不需要自己实现
  // if (Array.isArray(target)) {
  //    target.__proto__ = newPrototype
  // }
  // 传给Proxy的handler
  const handler = {
    get(target, key) {
      const reflect = Reflect.get(target, key);
      collectFn(target, key);
      // 当我们获取对象属性时,Proxy只会递归到获取的层级,不会继续递归子层级
      return bindReactive(reflect);
    },
    set(target, key, val) {
      // 重复的数据,不处理
      if (val === target[key]) {
        return true;
      }
      const success = Reflect.set(target, key, val);
      update();
      // 设置成功与否
      trigger(target, key);
      return success;
    },
    deleteProperty(target, key) {
      const success = Reflect.deleteProperty(target, key);
      // 删除成功与否
      update();
      // 不同 则重新进行赋值
      return success;
    },
  };
  // 生成proxy对象
  const proxy = new Proxy(target, handler);
  return proxy;
}
// 抽取逻辑--修改
function collectFn(target, key) {
  if (activeEffect) {
    let depsMap = bucket.get(target);
    if (!depsMap) {
      bucket.set(target, (depsMap = new Map()));
    }
    let deps = depsMap.get(key);
    if (!deps) {
      depsMap.set(key, (deps = new Set()));
    }
    deps.add(activeEffect);
  }
}
// 抽取逻辑--修改
function trigger(target, key) {
  // 同时将桶中存储的函数拿出来,进行执行。
  const depsMap = bucket.get(target);
  if (!depsMap) return;
  const deps = depsMap.get(key);
  if (deps) {
    deps.forEach((fn) => fn());
  }
}
function update() {
  console.log('999');
}

const data = {
  text: 'hello, siri',
  b: {
    c: 1,
    d: {
      e: '12',
    },
  },
};
const obj = bindReactive(data);
setTimeout(() => {
  obj.b.c = 3;
}, 1000);

let activeEffect;
function effect(fn) {
  console.log(typeof fn);
  // 进行赋值
  activeEffect = fn;
  // 执行函数
  fn();
}
// 修改 这时候 可以传递任意函数 -这里传递了一个匿名函数
effect(() => {
  document.body.innerText = obj.b.c;
  console.log('1');
});
setTimeout(() => {
  obj.b.c = 3;
}, 1000);

5.2 性能优化

5.2.1 修复-去除不必要的更新

如下案例所示:

const data = {
 isTrue: true,
  text: 'hello, siri',
  b: {
    c: 1,
    d: {
      e: '12',
    },
  },
};
effect(() => {
    document.body.innerHTML = obj.isTrue ? obj.text : 'hello, zoom';
    console.log(1);
})
setTimeout(() => {
    obj.isTrue = false;
}, 1000);
setTimeout(() => {
    obj.text = 'hello xiaoai';
}, 2000);

 分析:

当第一次执行effect函数的时候,该函数会分别被isTrue、text字段都给收集,如下图所示:

image.png

当第一个定时器一秒后执行,将obj.isTrue 设置为 false,则会触发effect函数的执行。

当第二个定时器两秒后执行,将obj.text 设置为'hello xiaoai',也会触发effect函数执行,而这时因为obj.isTrue的值是false,所以实际上页面不会有任何变化,也就是说当obj.isTrue设置为false的时候,这时候obj.text的值的变化就不应该去触发副作用函数的执行,导致不必要的消耗。

解决思路:

每次effect函数执行的时候,由于它的执行一定会触发特定属性的一个读取操作,因此我们可以在该函数执行前进行一个清空操作,也就是先将其和所有有关联的属性集合中进行删除操作,当执行的时候由于进行读取操作,因此会重新收集。

因此我们必须要明确有哪些属性对应的集合中收集了特定的函数,代码如下:

// 抽取逻辑--修改
function collectFn(target, key) {
  if (activeEffect) {
    let depsMap = bucket.get(target);
    if (!depsMap) {
      bucket.set(target, (depsMap = new Map()));
    }
    let deps = depsMap.get(key);
    if (!deps) {
      depsMap.set(key, (deps = new Set()));
    }
    deps.add(activeEffect);
    // 修改 --- 将deps给存储到 activeEffect.deps数组中 这样就可以知道哪些属性和函数关联
    activeEffect.deps.push(deps);
  }
}
// 修改 重新定义 effect函数
function effect(fn) {
  const effectFn = () => {
    activeEffect = effectFn;
    fn();
  };
  effectFn.deps = [];
  effectFn();
}

则通过这个数组,可以了解所有与这个特定函数相关的集合,在执行特定函数时可获取相关的所有关联的属性集合,则可以进行一个清除,代码如下:

function effect(fn) {
  const effectFn = () => {
    cleanUp(effectFn);
    activeEffect = effectFn;
    fn();
  };
  effectFn.deps = [];
  effectFn();
}
function cleanUp(effectFn) {
  for (let i = 0; i < effectFn.deps.length; i++) {
    const dep = effectFn.deps[i];
    dep.delete(effectFn);
  }
  effectFn.deps.length = 0;
}

总代码

// 创建一个桶 用于存储特定的某些函数
const bucket = new WeakMap();
function bindReactive(target) {
  if (typeof target !== 'object' || target == null) {
    // 不是对象或数组,则直接返回
    return target;
  }
  // 因为Proxy原生支持数组,所以这里不需要自己实现
  // if (Array.isArray(target)) {
  //    target.__proto__ = newPrototype
  // }
  // 传给Proxy的handler
  const handler = {
    get(target, key) {
      const reflect = Reflect.get(target, key);
      collectFn(target, key);
      // 当我们获取对象属性时,Proxy只会递归到获取的层级,不会继续递归子层级
      return bindReactive(reflect);
    },
    set(target, key, val) {
      // 重复的数据,不处理
      if (val === target[key]) {
        return true;
      }
      const success = Reflect.set(target, key, val);
      update();
      // 设置成功与否
      trigger(target, key);
      return success;
    },
    deleteProperty(target, key) {
      const success = Reflect.deleteProperty(target, key);
      // 删除成功与否
      update();
      // 不同 则重新进行赋值
      return success;
    },
  };
  // 生成proxy对象
  const proxy = new Proxy(target, handler);
  return proxy;
}
// 抽取逻辑--修改
function collectFn(target, key) {
  if (activeEffect) {
    let depsMap = bucket.get(target);
    if (!depsMap) {
      bucket.set(target, (depsMap = new Map()));
    }
    let deps = depsMap.get(key);
    if (!deps) {
      depsMap.set(key, (deps = new Set()));
    }
    deps.add(activeEffect);
    // 将deps给存储到 activeEffect.deps数组中 这样就可以知道哪些数据和当前的函数关联
    activeEffect.deps.push(deps);
  }
}
// 抽取逻辑--修改
function trigger(target, key) {
  // 同时将桶中存储的函数拿出来,进行执行。
  const depsMap = bucket.get(target);
  if (!depsMap) return;
  const deps = depsMap.get(key);
  const effectsFn = new Set(deps);
  if (effectsFn) {
    effectsFn.forEach((fn) => fn());
  }
}
function update() {
  console.log('999');
}
function cleanUp(effectFn) {
  for (let i = 0; i < effectFn.deps.length; i++) {
    const dep = effectFn.deps[i];
    dep.delete(effectFn);
  }
  effectFn.deps.length = 0;
}
const data = {
  isTrue: true,
  a: 'hello, siri',
  b: {
    c: 1,
    d: {
      e: '12',
    },
  },
};
const obj = bindReactive(data);
setTimeout(() => {
  obj.b.c = 3;
}, 1000);

let activeEffect;
function effect(fn) {
  const effectFn = () => {
    cleanUp(effectFn);
    activeEffect = effectFn;
    fn();
  };
  effectFn.deps = [];
  effectFn();
}
// 修改 这时候 可以传递任意函数 -这里传递了一个匿名函数
effect(() => {
  document.body.innerHTML = obj.isTrue ? obj.text : 'hello, zoom';
  console.log('1');
});
setTimeout(() => {
  obj.isTrue = false;
}, 1000);
setTimeout(() => {
  obj.text = 'hello xiaoai';
}, 2000);