vue3响应式实现原理(2)

简介: vue3响应式实现原理(2)

theme: fancy

highlight: a11y-light

执行调度

当触发trigger副作用函数重新执行时,能够决定副作用函数执行的时机、次数等。如何处理:给effect再添加一个参数,提前说明他是一个对象,因为以后还会包含其他选项。判断注册副作用函数时是否存在调度器,如果存在,则直接调用调度器函数,并把当前要注册副作用函数作为参数传递过去,由用户自己控制如何执行;否则直接执行副作用函数。

现在要改变下面执行的顺序,把123放在中间执行即23之前执行。我们可以借用调度器来完成。

effect(
  () => {
   
    console.log(proxyData.age);
  },
);
proxyData.age++;
console.log('123')
//22
//23
//123
function effect(fn, options) {
   
  const effectFn = () => {
   
    activeEffect = effectFn;
    effectStack.push(activeEffect);
    fn();
    activeEffect = "";
  };
  effectFn.deps = [];
  effectFn.options = options;//新增
  effectFn();
}

  set(target, key, newVal) {
   
    target[key] = newVal;
    let effects = bucket?.get(target)?.get(key);
    const effectsToRun = new Set(effects);
    effects &&
      effectsToRun.forEach((effectFn) => {
   
        let deps = effectFn.deps;
        deps.forEach((item) => {
   
          item.delete(effectFn);
        });
        effectFn.deps.length = 0;
        if (activeEffect !== effectFn) {
   
          effectFn.options.scheduler ? effectFn.options.scheduler(effectFn) : effectFn();//新增
        }
      });
  },

  //接下来只需给effect函数添加第二个参数
  effect(
  () => {
   
    console.log(proxyData.age);
  },
  {
   
      scheduler(fn) {
   
          setTimeout(fn)
    },
  }
);

接下来看下面这种情况 会打印两次age,但是这两次操作相同,我们只关心结果,忽略过程。这个功能有点类似于在 Vue.js 中连续多次修改响应式数据但只会触发一次更新,实际上 Vue.js 内部实现了一个更加完善的调度器

effect(() => {
   
  console.log(proxyData.age);
});
proxyData.age++
proxyData.age++

首先我们需要一个微任务队列,还需要一个任务队列(Set),以及一个标识变量。思路是按照上面的情况会同步执行两次scheduler,在scheduler中将函数添加到任务队列,因为Set的去重效果,任务队列里只会添加一个函数,再执行微任务队列,我们还需要一个标识位用来判断微任务是否执行完,因为两次同步执行scheduler会执行两次微任务队列。

let jobQueue = new Set();
let p = Promise.resolve();
let flag = false;
function flushJob() {
   
  if (flag) return;//微任务队列未完成直接退出
  flag = true;
  p.then(() => {
   
    jobQueue.forEach((job) => {
   
      job();//执行副作用,因为set去重只会有一条打印信息,即最新的信息
    });
  }).finally(() => {
   
    flag = false;//修改标识代表微任务队列完成
  });
}


effect(
  () => {
   
    console.log(proxyData.age);
  },
  {
   
    scheduler(fn) {
   
      jobQueue.add(fn);//添加副作用
      flushJob();//刷新微任务队列
    },
  }
);

计算属性

计算属性是一个返回值,先看vue3计算属性使用方法

const fullName = computed(() => {
    return name.value + ' Doe'; });

要实现计算属性,首先要知道只有要使用计算属性的值时才需要触发副作用函数,即我们需要将副作用存储起来。

function effect(fn, options = {
   }) {
   
  const effectFn = () => {
   
    activeEffect = effectFn;
    effectStack.push(activeEffect);
    fn();
    activeEffect = "";
    return fn();//新增
  };
  effectFn.deps = [];
  effectFn.options = options;
  if (options.lazy) {
   //新增
    return effectFn;
    //return effectFn();不要直接返回一个值,这样后面不会触发副作用
  } else {
   
    effectFn();
  }
}


function computed(getter) {
   //新增
    // 把 getter 作为副作用函数,创建一个 lazy 的 effect
    const effectFn = effect(getter, {
    lazy: true })
    const obj = {
   
      get value() {
   
        return effectFn()
      }
    }
    return obj
  }
const sumRes = computed(() => {
   
  console.log("changed");
  return proxyData.age;
});
console.log(sumRes.value);
console.log(sumRes.value);

在上面打印两次sumRes.value,会触发两次副作用函数执行,显然这是没有必要的,所以我们需要把值缓存下来

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;
}

sumRes 是一个计算属性,并且在另一个 effect 的副作用函数中读取了 sumRes.value 的值。如果此时修改 obj.foo 的值,我们期望副作用函数重新执行

const sumRes = computed(() => {
   
  return proxyData.age;
});
effect(() => {
   
  console.log(sumRes.value);
});
proxyData.age++;
proxyData.age++;

解决方法:当读取计算属性的值时,我们可以手动调用 track 函数进行追踪;当计算属性依赖的响应式数据发生变化时,我们可以手动调用 trigger 函数触发响应:

function computed(getter) {
   
  let dirty = true;
  let value;
  const effectFn = effect(getter, {
   
    lazy: true,
    scheduler() {
   
      dirty = true;
      trigger(obj, "value");//新增
    },
  });
  const obj = {
   
    get value() {
   
      track(obj, "value");//新增
      if (dirty) {
   
        value = effectFn();
        dirty = false;
      }

      return value;
    },
  };
  return obj;
}

综上计算属性是由effect加上其中的lazy参数,调度函数组合实现的

相关文章
|
21天前
|
JavaScript 前端开发 算法
vue渲染页面的原理
vue渲染页面的原理
96 56
|
8天前
|
JavaScript 前端开发 UED
vue2和vue3的响应式原理有何不同?
大家好,我是V哥。本文详细对比了Vue 2与Vue 3的响应式原理:Vue 2基于`Object.defineProperty()`,适合小型项目但存在性能瓶颈;Vue 3采用`Proxy`,大幅优化初始化、更新性能及内存占用,更高效稳定。此外,我建议前端开发者关注鸿蒙趋势,2025年将是国产化替代关键期,推荐《鸿蒙 HarmonyOS 开发之路》卷1助你入行。老项目用Vue 2?不妨升级到Vue 3,提升用户体验!关注V哥爱编程,全栈开发轻松上手。
|
1月前
|
移动开发 JavaScript API
Vue Router 核心原理
Vue Router 是 Vue.js 的官方路由管理器,用于实现单页面应用(SPA)的路由功能。其核心原理包括路由配置、监听浏览器事件和组件渲染等。通过定义路径与组件的映射关系,Vue Router 将用户访问的路径与对应的组件关联,支持哈希和历史模式监听 URL 变化,确保页面导航时正确渲染组件。
|
4月前
|
JavaScript 前端开发 开发者
Vue是如何劫持响应式对象的
Vue是如何劫持响应式对象的
78 18
|
4月前
|
JavaScript 前端开发 API
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
143 17
|
10月前
|
JavaScript API
【vue实战项目】通用管理系统:api封装、404页
【vue实战项目】通用管理系统:api封装、404页
102 3
|
10月前
|
人工智能 JavaScript 前端开发
毕设项目-基于Springboot和Vue实现蛋糕商城系统(三)
毕设项目-基于Springboot和Vue实现蛋糕商城系统
115 0
|
10月前
|
JavaScript Java 关系型数据库
毕设项目-基于Springboot和Vue实现蛋糕商城系统(一)
毕设项目-基于Springboot和Vue实现蛋糕商城系统
258 0
|
10月前
|
JavaScript 前端开发 API
Vue3+Vite+TypeScript常用项目模块详解
现在无论gitee还是github,越来越多的前端开源项目采用Vue3+Vite+TypeScript+Pinia+Elementplus+axios+Sass(css预编译语言等),其中还有各种项目配置比如eslint 校验代码工具配置等等,而我们想要进行前端项目的二次开发,就必须了解会使用这些东西,所以作者写了这篇文章进行简单的介绍。
196 0
Vue3+Vite+TypeScript常用项目模块详解
|
10月前
|
设计模式 JavaScript
探索 Vue Mixin 的世界:如何轻松复用代码并提高项目性能(上)
探索 Vue Mixin 的世界:如何轻松复用代码并提高项目性能(上)
探索 Vue Mixin 的世界:如何轻松复用代码并提高项目性能(上)

相关实验场景

更多