likes
comments
collection
share

初探Vue3响应式系统

作者站长头像
站长
· 阅读数 64

😵‍💫如何去实现一个简易版的响应式系统呢,这是最近萦绕在我脑海中的疑问,于是就有了这一篇;主要是明确了数据结构和副作用函数遗留问题

基础背景

  • 假设我们在一个副作用函数中读取了某个对象的属性,我们希望,当数值变化的时候,副作用函数能够重新执行
//意思就是,我这里的obj.text是可能别其他改变的,那么它数值变化的时候
//希望能够重新执行effect()函数
const obj = { text: '初探响应式' }
function effect() {
  document.body.innerText = obj.text
}

基础实现

  • 因为需要拦截到数据的读取,设置操作,所以我们需要使用Vue3也使用到的Proxy
    • effect函数去读取obj.text的时候将effect收集起来
    • obj.text设置的时候将effect拿出来执行
const list = new Set(); //用来存储effect函数
const obj = { text: '初探响应式' } //设置目标对象
//proxy为代理对象,代理obj
const proxy = new Proxy(obj, {
  get(target, key ) {
    //将effect函数收集起来
    list.add(effect);
    return target[key]
  },
  set(target, key, value, receiver) {
    target[key] = value;
    //将effect拿出来执行
    list.forEach(fn => fn())
    return true
  }
})
  • 测试一下:这里注意你要使用的对象一定是代理对象,这里是proxy,只有使用触发拦截
function effect() {
  document.body.innerText = proxy.text
}
effect();
setTimeout(()=>{
  proxy.text = '已经过了一秒,我要修改了'
},1000)
初探Vue3响应式系统

再走一步

  • 我们首先要明确数据结构
  • 首先按照我们上面的结构来说,无论是设置对象哪个属性,我们都会将所有副作用函数拿出来执行一遍,这显然不是我们要的效果,即我们需要明确 副作用函数与被操作的目标字段的联系
  • 也就是说全局中
    • 可能有多个被代理的对象
    • 一个对象里面可以有多个属性
    • 每个属性又可以对应多个副作用函数,副作用函数我们期望是不重复的,所以用Set存储
  • 数据结构图,obj表示被代理的目标对象,key表示对象的键,effect表示副作用函数 初探Vue3响应式系统
const list = new Map()
const obj = { name :'123'}
const proxy = new Proxy(obj, {
  get(target, key) {
    //如果没有副作用事件的话直接返回即可
    if(!effect) return target[key];
    //从list中获取desMap
    let desMap = list.get(target);
    //如果不存在的话则初始化,同时将其赋值desMap
    if(!desMap) list.set(target,(desMap = new Map()));
    //从desMap取出deps,它是一个Set类型:因为我们不希望存储重复的事件
    let deps = desMap.get(key);
    //如果不存在的话则初始化,同时将其赋值deps
    if(!deps)  desMap.set(key, (deps = new Set()) );
    //向deps存储副作用函数
    deps.add(effect);
    return target[key]
  },
  set(target, key, value) {
    target[key] = value;
    const desMap = list.get(target);
    if(!desMap) return;
    //从desMap中取中存放副作用事件的Set结构
    const effects = desMap.get(key);
    //如果存在的话就挨个执行
    effects && effects.forEach(fn => fn())
  } 
})

再走一步优化

  • 书中谈及可以用WeakMap替代Map 去存储键为被代理的目标对象,值为Map结构
  • 也就是说改成如图: 初探Vue3响应式系统
  • 原因就是说:如果使用Map的话,那么即使用户侧的代码对被代理的这个目标对象没有任何引用,这个目标对象也不会被回收,最终就可能导致内存的溢出

逻辑封装

  • get拦截函数中副作用收集的逻辑封装到 track 函数中
function track(target,key) {
      //如果没有副作用事件的话直接返回即可
      if(!effect) return target[key];
      //从list中获取desMap
      let desMap = list.get(target);
      //如果不存在的话则初始化,同时将其赋值desMap
      if(!desMap) list.set(target,(desMap = new Map()));
      //从desMap取出deps集合,它是一个Set类型:因为我们不希望存储重复的事件
      let deps = desMap.get(key);
      //如果不存在的话则初始化,同时将其赋值deps
      if(!deps)  desMap.set(key, (deps = new Set()) );
      //向deps集合存储副作用函数
      deps.add(effect);
}
  • set拦截函数中副作用执行的逻辑封装到trigger函数中
function trigger(target,key) {
  const desMap = list.get(target);
  if(!desMap) return;
  //从desMap中取中存放副作用事件的Set结构
  const effects = desMap.get(key);
  //如果存在的话就挨个执行
  effects && effects.forEach(fn => fn())
}
  • 总体代码
const list = new Map()
const obj = { name :'123'}
const proxy = new Proxy(obj, {
  get(target, key) {
    track(target, key);
    return target[key]
  },
  set(target, key, value) {
    target[key] = value;
   trigger(target,key);
  } 
})

测试

  • 当你要测试的时候,你会发现,并不是函数都叫做effect呀,那么这个时候我们就得再进行处理
  • 也就是说,提供一个用来注册副作用函数的机制
//用一个全局变量存储被注册的副作用函数
let activeEffect;
function effect(fn) {
  //注册
  activeEffect = fn;
  //执行
  fn();
}

既然修改了这里那么上面track函数中设计effect的也要换成activeEffect喔

  • 测试:对data进行代理,设置两个函数分别获取proxy.text proxy.text1
const data = { ok: true, text: 'hello world', text1: '国庆快乐'};
function test() {
  document.querySelector('.first').innerHTML = proxy.text;
}
function test1 () {
  document.querySelector('.second').innerHTML = proxy.text1;
}
effect(test);
effect(test1);
console.log(list);

初探Vue3响应式系统

  • 打印出来的东西跟我们之前分析的数据结构就一样啦

分支切换

先看问题

const data = { ok: true, text: 'hello world', text1: '国庆快乐'};
function question() {
  document.body.innerHTML = proxy.ok? proxy.text: 'not';
}
effect(question);
  • 按照我们的代码来说,副作用函数question会分别被proxy.okproxy.text收集,因为既获取了proxy.ok的值,又获取了proxy.text的值
data
    |___ok
          |__question
    |___text
          |__question      
  • 当我们将obj.ok的值修改为false之后,按照理想情况,副作用函数question不应该再被字段proxt.text所对应的依赖集合收集
//理想情况
data
    |___ok
          |__question     
  • 所以说这个时候就产生了遗留的副作用函数,当我们改变proxy.text的值的时候还会去调用该函数

解决思路

  • 每次副函数执行的时候,先把它从所有与之关联的依赖集合中删除
  • 当副作用执行完毕的时候,它会重新建立联系,那么自然就不会包含遗留的副作用函数了
  • 要实现这个操作,首先我们需要知道哪些集合有包含它
  • effectFn添加了deps属性,用来存储所有包含当前副作用函数的依赖集合
//用一个全局变量存储被注册的副作用函数
let activeEffect;
function effect(fn) {
  const effectFn = () => {
    //当effectFn执行的时候,把它设置为当前激活的副作用函数
    //这样的话又可以成功执行传入函数,又可以加上deps属性
    activeEffect = effectFn;
    fn();
  }
  effectFn.deps = [];
  effectFn(); //在这里执行
}
  • 有数组存储数据的结构后应该就去寻找要去哪里收集它
  • 找到track函数,deps不就刚好是副作用的集合吗,so
//添加
activeEffect.deps.push(deps);
  • 收集完就到了刚刚提到删除步骤了
    • 在调用之前执行,所以我们加上cleanup函数负责清除工作
    function effect(fn) {
      const effectFn = () => {
        cleanup(effectFn);
        //当effectFn执行的时候,把它设置为当前激活的副作用函数
        activeEffect = effectFn;
        fn();
      }
      effectFn.deps = [];
      effectFn();
    }
    
    • cleanup函数
    function cleanup(effectFn) {
      for(let i = 0; i<effectFn.deps.length;i++) {
        const deps = effectFn.deps[i];
        //将effectFn从依赖集合移除
        deps.delete(effectFn);
      }
      //重置数组
      effectFn.deps.length = 0;
    }
    
  • 你以为完了?其实没有,这里这个时候已经可以避免副作用函数的遗留问题了,但是目前的实现还是会导致无限循环执行,问题就出在trriger函数里面
  • 当副作用函数执行的时候,会执行cleanup,将当前的剔除,但是执行又会让他重新被收集,所以遍历一直在继续
  • 那么就构造另外一个集合去遍历它
  // effects && effects.forEach(fn => fn()) ----这一句不要啦
  //新增
  const effectsToRun = new Set(effects);
  effectsToRun.forEach(effectFn=> effectFn());
  • 注:这篇是基于个人学习 《Vue.js设计与实现》 这本书过程的一种梳理和整理输出
  • 今天就先到这里,over over
转载自:https://juejin.cn/post/7149189717366898724
评论
请登录