vue3响应式系统的实现(二)

前面回顾

在vue3响应式系统实现的一,我们通过解决在实现过程中遇到的一些问题实现了一个相对比较完善的vue3响应式系统,本章节我们通过上节实现的响应式系统来完成vue3相关的api进行实现 为方便大家进行回顾,如下代码实现了第一小节

javascript 复制代码
 let activeEffect = null;

// activeEffect执行栈
const activeEffectStack = [];
function effect(fn){
     const innerEffect = () => {
         cleanup(innerEffect);
         activeEffect = innerEffect;
    
         activeEffectStack.push(activeEffect);
         fn();
      
         activeEffectStack.pop();
         activeEffect =  activeEffectStack[activeEffectStack.length - 1];
  
     }
 effect.deps = [];
 innerEffect()
}

// 清除依赖收集
function cleanup(effectFn){
  const deps = effectFn.deps;
  for (let i = 0; i < deps.length; i++) {
    const depSet = deps[i];
    depset.delete(effectFn);
  }
  deps.length = 0;
}

const bucketMap = new WeakMap() // 用来区分不同对象的副作用函数



function track(target, key){
      if (!activeEffect) {
        return
      }
      
      let depsMap = bucketMap.get(target);
      if (!depsMap) {
        depsMap = new Map();
        bucketMap.set(target, depsMap);
      }
      
      let effects = depsMap.get(key);
      if (!effects) {
      
        effects = new Set();
        depsMap.set(key, effects);
        
      }

      effects.add(activeEffect);
      activeEffect.deps.push(effects);
};

function trigger(target, key, value){
      const depsMap = bucketMap.get(target)
      if (!depsMap) {
        return;
      }

     const effects = depsMap.get(key);
     const effectsToRun = new Set(effects); 
     effectsToRun.forEach(effect => {
     
       //新增 自己调用自己时进行拦截
       if (activeEffect !== effect){
         effect();
       }
     });
}

  const obj = new Proxy(data, {
    get(target, key, receiver) {
    track(target, key)
      return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
      const result = Reflect.set(target, key, value, receiver);
      trigger(target, key, value);
      return result;
    }
    })

调度执行

什么是调度执行,所谓的调度执行指的是triger函数动作触发副作用执行重新执行时,有权力去决定副作用函数执行的时机,次数以及方式

我们有一个需求我们需要知道知道trigger函数什么时候执行完毕,针对上述需求我们的代码如图所示

javascript 复制代码
 const obj ={ name: 'hello react' };
 effect(() => console.log(obj.name));
 
 obj.name = 'hello vue';
 
 console.log('trigger执行结束了')

现在我们需求有变,需要在触发trigger之前进行打印,有没有什么办法不需要改动代码就能实现呢,这里我们可以利用函数式编程的思想给effect函数增加scheduler函数

javascript 复制代码
 function effect(fn, scheduler) {
   const innerEffect = () => {
         cleanup(innerEffect);
         activeEffect = innerEffect;
    
         activeEffectStack.push(activeEffect);
         fn();
      
         activeEffectStack.pop();
         activeEffect =  activeEffectStack[activeEffectStack.length - 1];
  
     }
 effect.deps = [];
 effect.sceduler = scheduler // 新增代码
 innerEffect()
 }

我们只需要判断是不是传递来scheduler,将对副作用的触发控制权交给用户的输入来决定

javascript 复制代码
function trigger(target, key, value){
      const depsMap = bucketMap.get(target)
      if (!depsMap) {
        return;
      }

     const effects = depsMap.get(key);
     const effectsToRun = new Set(effects); 
     effectsToRun.forEach(effect => {
     
       //新增 自己调用自己时进行拦截
       if (activeEffect !== effect){
         if(effect.scheduler) {
          effect.scheduler(effect) // 将副作用的执行权进行移交
         } else {
           effect()
         }
       }
     });
}

如上代码我们就实现了用户对副作用函数的控制权

我们再来看一个需求,有时候其实副作用的中间状态我们并关心,我们更多关系的是最终状态

javascript 复制代码
const obj ={ count: 0 };
 effect(() => console.log(obj.count));
 
 obj.count++;
 obj.count++;
 

我们来分析上面的代码我们发现最终会打印 0,1,2,此时我们需要实现打印的效果是0,3也就是中间状态我们不需要关心我们应该怎么去解决呢,其实解决办法也很简单我们可以利用js函数异步的特点来进行解决如下代码所示

javascript 复制代码
const jobQueue = new Set();
let isFlushing = false; // 是否要刷新队列
function flushJob() {
 if (isFlushing) {
   return
 }
 isFlushing = true;
 Promise.resove().then(() => {
  jobQueue.forEach(job => job())
 }).catch((e) {
   throw new Error(e);
  }).finally(() => isFlushing = false)
}

effect(() => {
console.log(obj.count)
}, (fn) => {
 jobQueue.add(fn);
 flushJob();
})

从上面代码也可以看出上述这段代码有点在vue中批量更新的那点味道了,多次修改响应式数据但只会触发一次更新

计算属性computed和lazy

懒执行的副作用函数

我们前面通过给副作用函数指定了schedule函数实现了用户对effect副作用函数的控制权,本节我们还需要增加其他参数,因此将上述代码进行改造如下所示

javascript 复制代码
function effect(fn, options = {}) {
   const innerEffect = () => {
         cleanup(innerEffect);
         activeEffect = innerEffect;
    
         activeEffectStack.push(activeEffect);
         fn();
      
         activeEffectStack.pop();
         activeEffect =  activeEffectStack[activeEffectStack.length - 1];
  
     }
 effect.deps = [];
 effect.options = options // 改造部分
 innerEffect()
 }
 
 function trigger(target, key, value){
      const depsMap = bucketMap.get(target)
      if (!depsMap) {
        return;
      }

     const effects = depsMap.get(key);
     const effectsToRun = new Set(effects); 
     effectsToRun.forEach(effect => {
     
       if (activeEffect !== effect){
         if(effect.options.scheduler) { // 改造部分
          effect.options.scheduler(effect) // 改造部分
         } else {
           effect()
         }
       }
     });
}

在开始实现计算属性之前,我们来看一下关于懒执行的effect,什么是懒执行呢,顾名思义就是我们不让副作用函数立即执行,举个例子如下代码所示

javascript 复制代码
const obj ={ count: 0 };
const effectFn =  effect(() => console.log(obj.count));
 obj.count++;
 
 effectFn() // 只有当外界运行effectFn函数才会执行副作用函数,否则一直不会执行

为了实现上述代码,我们需要额外增加一个参数用来控制是否返回一个函数

javascript 复制代码
function effect(fn, options = {}) {
   const innerEffect = () => {
         cleanup(innerEffect);
         activeEffect = innerEffect;
    
         activeEffectStack.push(activeEffect);
         
      
         activeEffectStack.pop();
         activeEffect =  activeEffectStack[activeEffectStack.length - 1];
  
     }
 effect.deps = [];
 effect.options = options // 改造部分
 if (options.lazy) {
   return innerEffect()
  } else {
    innerEffect()
  }
 }

computed的实现

数据的懒计算

通过上述操作我们就实现了只有外界进行调用effectFn来会执行副作用函数,接下来根据这个特性我们来实现一下computed

javascript 复制代码
 function computed(getter) {
  const effectFn = effect(getter,{lazy: true})
  const obj = {
    get value(){
     return effectFn();
    }
  }
 }

计算属性缓存的实现

我们现在了computed的懒计算功能,还有一个计算属性可以进行值的缓存,为了解决以上问题,我们需要进行数据检查

javascript 复制代码
 function computed(getter) {
  let dirty = true // 是否脏数据
  let value
  const effectFn = effect(getter,{lazy: true})
  const obj = {
     get value(){
      if(dirty) {
      value = effectFn();
      dirty = false
     }
     return value;
    }
  }
  return obj;
 }

我们虽然实现了计算属性的缓存,我们发现下述这段代码运行其实不符合我们的预期

javascript 复制代码
 const obj = {
   a: 1,
   b: 2
  }
  
  computed(() => {
   return obj.a + obj.b;
  })
  
  obj.a++;

我们发现当obj.a发生变换其实是符合我们的预期,我们希望拿到的值是4,但是执行输出的确是3,那么我们应该如何去解决呢,解决方法也比较简单,每次执行调度器的时候我们将 drity赋值为true

javascript 复制代码
 function computed(getter) {
  let dirty = true // 是否脏数据
  let value
  const effectFn = effect(getter,{lazy: true, scheduler(){ dirty = true }})
  const obj = {
     get value(){
      if(dirty) {
      value = effectFn();
      dirty = false
     }
     return value;
    }
  }
  return obj;
 }

这样只要数据发生改变就会执行schedler函数,从而将dirty赋值为true从而实现了从而重新运行副作用函数 这个时候我们的computed已经接近完美了但是还是有一个问题,就是在effect副作用函数进行读取computed的值时当computed值发生改变并不会触发副作用函数的执行,因此我们需要进行手动进行依赖的追踪

javascript 复制代码
function computed(getter) {
 let dirty = true // 是否脏数据
 let value
 const effectFn = effect(getter,{lazy: true, scheduler(){ 
  dirty = true;
  trigger(obj, 'value', value); // 手动进行依赖触发
 }})
 const obj = {
    get value(){
     if(dirty) {
     value = effectFn();
     dirty = false
    }
    track(obj, 'value'); // 手动进行依赖收集
    return value;
   }
 }
 return obj;

最终代码

javascript 复制代码
function computed(getter) {
  let dirty = true // 是否脏数据
  let value
  const effectFn = effect(getter,{lazy: true, scheduler(){ 
   dirty = true;
   trigger(obj, 'value', value); // 手动进行依赖触发
  }})
  const obj = {
     get value(){
      if(dirty) {
      value = effectFn();
      dirty = false
     }
     track(obj, 'value'); // 手动进行依赖收集
     return value;
    }
  }
  return obj;

watch的实现原理

什么是wathc呢,其实本质就是观测一个响应式数据的改变,当数据发生改变时通知并执行相应的副作用函数, 本质上watch的实现也是利用了effect函数进行实现

javascript 复制代码
 function watch(source, cb){
  effect(() => source.count, { scheduler(){ cb() } });
 }

我们再来思考一下我们应该如何去实现对值读取呢,其实也可以利用函数编程的思想

javascript 复制代码
function watch(source, cb,) {
  effect(() => {
    return traverse(source);
  }, { 
    scheduler() {
      cb()
    }
  })
}

function traverse(value, seen = new Set()) {
  if (typeof value !== 'object' || value === null || !seen.has(value)) {
    seen.add(value)
  } else {
    for (const key in value) {
      // 触发trigger
      traverse(value[key], seen);
    }
  }
  return value
}

我们再次利用函数式编程的思想让其更加灵活

javascript 复制代码
function watch(source, cb,) {
  if (typeof source === 'function') {
    getter = source()
  } else {
    getter = () => traverse(source)
  }
  effect(() => {
    getter()
  }, { 
    scheduler() {
      cb()
    }
  })
}

接下来我们只要实现如何将旧值和新值进行组装基本上就ok了

javascript 复制代码
function watch(source, cb,) {
  if (typeof source === 'function') {
    getter = source()
  } else {
    getter = () => traverse(source)
  }

  let newValue, oldValue;
  const effectFn = effect(() => getter(), {  scheduler() { 
    newValue = effectFn()
    cb(newValue, oldValue);
    oldValue = newValue;
  }, lazy: true });

  oldValue = effectFn();

}

立即执行

我们来看一下立即执行的回调函数,默认情况下,一个watch的回调只会在响应式数据发生变化才执行

javascript 复制代码
watch(obj,() =>{ console.log('值发生改变') }, { immediate: true })

为了实现以上功能我们需要在我们的watch函数里面增加immediate参数

javascript 复制代码
function watch(source, cb, options) {
  if (typeof source === 'function') {
    getter = source()
  } else {
    getter = () => traverse(source)
  }

  let newValue, oldValue;

  const job = () => {
    newValue = effectFn()
    cb(newValue, oldValue);
    oldValue = newValue;
  }

  const effectFn = effect(() => getter(), { scheduler: job, lazy: true });

  if ( options && options.immediate) {
    job()
  } else {
    oldValue = effectFn();
  }
}

回调执行时机

在vue中我们可以通过参数来控制调度函数的执行时机,因此我们的watch也需要实现这个功能,其实实现;逻辑也相对比较简单可以借助于上述的jobQueue来进行实现,至于vue中的pre设涉及到组件的更新机制暂时没办法实现

javascript 复制代码
function watch(source, cb, options) {
  if (typeof source === 'function') {
    getter = source()
  } else {
    getter = () => traverse(source)
  }

  let newValue, oldValue;

  const job = () => {
    newValue = effectFn()
    cb(newValue, oldValue);
    oldValue = newValue;
  }

  const effectFn = effect(() => getter(), { scheduler: () => {
    if (options && options.flush === 'post') {
      const p = Promise.resolve()
      p.then(job).catch(() => {});
    } else {
      job()
    }
  }, lazy: true });

  if ( options && options.immediate) {
    job()
  } else {
    oldValue = effectFn();
  }
}

过期的副作用

使用过react的都知道effect函数可以返回一个回调函数用于副作用函数的处理,在react effect中我们经常遇到请求竞态的问题,为了在reat中解决请求竞态的问题我们通常会设置一个变量来进行解决

javascript 复制代码
 const [expired, setExpired] = useState<boolen>(false);
 
 useffect(() => {
  if (exprired === false) {
   // 网络请求
   expried = true
   } 
   return () =>{
    exprired = false
   }
 })

上述是react进行解决请求竞态的问题,那么在vue中我们应该如何去解决呢,其实我们可以参考react的解决思路也是通过标识符来进行解决

javascript 复制代码
let finallyData = null;

watch( obj, async() => {
  const data = await fetch('xxx');
  finallyData = data;
});

watch(obj, async(newVal, oldVal) => {
  let expired = false;
  onInvalidate(() => {
    expired = true;
  });

  const data = await fetch('xxx');
  if (!expired) {
    finallyData = data;
    onInvalidate();
  }
});

watch最终代码

javascript 复制代码
function watch(source, cb, options) {
  if (typeof source === 'function') {
    getter = source()
  } else {
    getter = () => traverse(source)
  }

  let newValue, oldValue, cleanup;

  function onInvalidate(fn) {
    cleanup = fn
  }

  const job = () => {
    newValue = effectFn();
    if (cleanup) {
      cleanup();
    };
    
    cb(newValue, oldValue, onInvalidate);
    oldValue = newValue;
  }

  const effectFn = effect(() => getter(), { scheduler: () => {
    if (options && options.flush === 'post') {
      const p = Promise.resolve()
      p.then(job).catch(() => {});
    } else {
      job()
    }
  }, lazy: true });

  if ( options && options.immediate) {
    job()
  } else {
    oldValue = effectFn();
  }
}

总结

  • 完善effect函数实现用户对副作用函数执行时机的控制
  • watch 和 computed 其实都是依赖于副作用effect的函数的实现
  • 实现computed的最终实现
  • 实现wathc的最终实现
  • 通过分析react的请求竞态问题解决vue的请求竞态问题
相关推荐
你挚爱的强哥2 小时前
✅✅✅【Vue.js】sd.js基于jQuery Ajax最新原生完整版for凯哥API版本
javascript·vue.js·jquery
susu10830189113 小时前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
天天进步20155 小时前
Vue+Springboot用Websocket实现协同编辑
vue.js·spring boot·websocket
疯狂的沙粒6 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员6 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
想自律的露西西★7 小时前
用el-scrollbar实现滚动条,拖动滚动条可以滚动,但是通过鼠标滑轮却无效
前端·javascript·css·vue.js·elementui·前端框架·html5
白墨阳7 小时前
vue3:瀑布流
前端·javascript·vue.js
程序媛-徐师姐9 小时前
Java 基于SpringBoot+vue框架的老年医疗保健网站
java·vue.js·spring boot·老年医疗保健·老年 医疗保健
余道各努力,千里自同风9 小时前
前端 vue 如何区分开发环境
前端·javascript·vue.js
PandaCave9 小时前
vue工程运行、构建、引用环境参数学习记录
javascript·vue.js·学习