从零构建Vue(一)——响应式原理

前置准备

初始化项目:

bash 复制代码
pnpm init

tsc --init

我们使用vite来构建项目:

bash 复制代码
pnpm i vite

修改 package.json:

json 复制代码
  "scripts": {
    "dev":"vite"
  },

vite的入口文件是根目录下的index.html,构建所需文件如下图:

然后在index.html中引入main.ts即可.

响应式原理

响应式原理是 Vue 的核心,在 Vue2 中,响应式原理是通过Object.defineProperty去实现的,但这个方法存在一些局限性:

  1. 不能劫持对象新增的属性(提供了$set解决)
  2. 监听不到数组的变化(重写了数组的7个方法)
  3. 存在性能问题,需要递归所有属性,监听大量对象时开销较大

为了解决上述缺陷,Vue3使用了Proxy去替代Object.defineProperty实现响应式更新。

reactive

reactive 是响应式核心API,具体实现思路有三步:

  1. 通过 proxy 劫持 get 和 set
  2. 在 get 中收集依赖,添加副作用函数
  3. 在 set 中触发依赖更新,执行相关副作用函数

effect(副作用函数)

effect 接收一个函数,它的作用是收集当前的副作用函数,并根据传入的配置项决定是否在初始化时调用。

ts 复制代码
//配置项
interface Options {
  scheduler?: Function; //控制副作用函数的执行时机
  lazy?: boolean; //是否在初始化时执行
}
ts 复制代码
let activeEffect: Function;
export const effect = (fn: Function, options?: Options) => {
  const _effect = () => {
    activeEffect = _effect;
    return fn();
  };
  _effect.options = options;
  if (options && options.lazy) {
    return _effect;
  } else {
    _effect();
    return _effect;
  }
};

track(依赖收集)

有了副作用函数,我们就要构建监听的对象的和副作用函数之间的依赖映射关系,在vue3中使用了WeakMap去实现,相比与Map它有以下好处:

  1. 键只能是对象,不会造成内存泄漏
  2. 在频繁添加、删除键值对的场景下,WeakMap 的性能要优于普通的 Map
  3. WeakMap 的键是弱引用,不会干扰垃圾回收机制,如果该对象需要回收,WeakMap 中的键会自动消失。
  4. WeakMap 的数据只能由键对象访问,外部无法直接遍历,保证了数据的私有性。

假设我们传入的对象为:{name:"theshy",age:"24"},监听的属性为name,那么我们要构建的数据结构如下图:

ts 复制代码
const targetMap = new WeakMap();

export const track = (target: object, key: string | symbol) => {
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    depsMap = new Map();
    //设置key为传入的对象,value为Map
    targetMap.set(target, depsMap);
  }
  let deps: Set<Function> = depsMap.get(key);
  if (!deps) {
    deps = new Set();
    //设置key为监听的属性,value为依赖集
    depsMap.set(key, deps);
  }
  //收集副作用函数`
  deps.add(activeEffect);
};

trigger(依赖更新)

最后是依赖更新,我们只需根据targetMap去除副作用函数的集合,然后依次执行副作用函数即可(如果有调度函数就传给对应的调度函数,然后在调度函数内决定执行时机)

ts 复制代码
export const trigger = (target: object, key: string | symbol, value: any) => {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  const deps: Set<any> = depsMap.get(key);
  if (!deps) return;
  deps.forEach((effect) => {
    if (effect?.options?.scheduler) {
      effect.options.scheduler(effect);
    } else {
      effect?.();
    }
  });
};

reactive

实现reactive所需函数我们都已实现,接下来使用proxy进行劫持即可,需要注意的是,对于多层嵌套的对象,我们需要进行递归处理:

ts 复制代码
import { track, trigger } from "./effect";
const reactive = <T extends object>(target: T): T => {
  return new Proxy(target, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver);
      track(target, key);
      if (typeof res === "object" && res !== null) {
        return reactive(res);
      }
      return res;
    },
    set(target, key, value, receiver) {
      const res = Reflect.set(target, key, value, receiver);
      trigger(target, key, value);
      return res;
    },
  });
};

export default reactive;

接下来让我们测试一下:

ts 复制代码
//main.ts
import { effect } from "./reactive/effect";
import reactive from "./reactive/reactive";

const person = reactive({ name: "TheyShy", age: 18 });

effect(() => {
  document.querySelector("#app")!.innerHTML = person.name + person.age;
});
document.querySelector("#btn")!.addEventListener("click", () => {
  person.age++;
});
html 复制代码
//index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app"></div>
    <hr />
    <button id="btn">年龄+1</button>
    <script type="module" src="./main.ts"></script>
  </body>
</html>

效果如下,已经实现了响应式更新:

computed

computed实现的核心思路就是设置一个缓存值,依赖值不发生变化时走缓存,否则就重新获取值,我们可以设置一个变量dirty去控制是否走缓存,通过scheduler配置项在每次依赖项更新时重置dirty

ts 复制代码
import { effect } from "./effect";
export const computed = <T>(fn: () => T) => {
  let dirty = true;
  let _cacheValue: T;
  const _value = effect(fn, {
    lazy: false,
    scheduler: () => {
      dirty = true;
    },
  });
  class ComputedRefImpl {
    get value() {
    //缓存值不变就走缓存 否则重新获取值
      if (dirty) {
        _cacheValue = _value();
        dirty = false;
      }
      return _cacheValue;
    }
  }
  return new ComputedRefImpl();
};

测试一下:

ts 复制代码
//main.ts
import { computed } from "./reactive/computed";
import { effect } from "./reactive/effect";
import reactive from "./reactive/reactive";

const person = reactive({
  name: "TheyShy",
  age: 18,
});

const nameAndAge = computed(() => {
  return person.name + person.age;
});

effect(() => {
  person.age; //需要触发一下依赖收集track
  document.querySelector("#app")!.innerHTML = nameAndAge.value;
});

document.querySelector("#btn")!.addEventListener("click", () => {
  person.age++;
});

效果与之前一致:

watch

首先我们需要定义配置项:

ts 复制代码
interface Options {
  immediate?: boolean; //创建时立即触发回调
  flush?: "sync" | "post"; //控制组件更新前/后触发回调
}

然后我们需要一个工具函数,帮助处理直接传入对象的形式:

ts 复制代码
const traverse = (target: any, seen = new Set()) => {
  if (typeof target !== "object" || target === null || seen.has(target)) {
    return;
  }
  seen.add(target);
  //如果是嵌套对象则递归调用处理
  for (const key in target) {
    traverse(target[key], seen);
  }
  return target;
};

实现watch函数主要就是newValueoldValue的处理,我们先调用一次effect保存旧值,再通过job函数完成新旧值的替换,需要注意的是,如果immediate为true时,直接调用了job函数,故旧值为undefined

针对flush:post的情况,因为dom更新是异步的,我们只需将job添加到微任务队列即可

ts 复制代码
import { effect } from "./effect";

const watch = (source: any, callback: Function, options?: Options) => {
  let getter: Function;
  if (typeof source === "function") {
    getter = source;
  } else {
    getter = () => traverse(source);
  }
  let newValue, oldValue;
  //dom更新是异步的,故将回调放入微任务队列中
  const flushPostCallbacks = () => {
    Promise.resolve().then(job);
  };
  const job = () => {
    newValue = effectFn();
    callback(newValue, oldValue);
    oldValue = newValue;
  };
  const effectFn = effect(getter, {
    lazy: true,
    scheduler: options && options.flush === "post" ? flushPostCallbacks : job,
  });

  if (options && options.immediate) {
    options?.flush === "post" ? flushPostCallbacks() : job();
  } else {
    oldValue = effectFn(); //默认值
  }
};

export default watch;

测试一下:

ts 复制代码
console.log("a");
watch(
  () => person.age,
  (newValue: any, oldValue: any) => {
    console.log(newValue, oldValue);
  },
  {
    immediate: true,
    flush: "post",
  }
);
console.log("b");

结果符合预期,immediateflush均正常

其它

至于其它响应式相关函数如:

  • ref
  • toRef
  • toRefs
  • toRaw
  • shallowReactive

等API的实现比较简单,这里就不再详细列出了,具体代码可以已上传github,需要的小伙伴可以自行参考~

相关推荐
brief of gali几秒前
记录一个奇怪的前端布局现象
前端
计算机毕设指导64 分钟前
基于 SpringBoot 的作业管理系统【附源码】
java·vue.js·spring boot·后端·mysql·spring·intellij-idea
Json_181790144801 小时前
电商拍立淘按图搜索API接口系列,文档说明参考
前端·数据库
风尚云网1 小时前
风尚云网前端学习:一个简易前端新手友好的HTML5页面布局与样式设计
前端·css·学习·html·html5·风尚云网
木子02041 小时前
前端VUE项目启动方式
前端·javascript·vue.js
GISer_Jing1 小时前
React核心功能详解(一)
前端·react.js·前端框架
捂月1 小时前
Spring Boot 深度解析:快速构建高效、现代化的 Web 应用程序
前端·spring boot·后端
深度混淆2 小时前
实用功能,觊觎(Edge)浏览器的内置截(长)图功能
前端·edge
Smartdaili China2 小时前
如何在 Microsoft Edge 中设置代理: 快速而简单的方法
前端·爬虫·安全·microsoft·edge·社交·动态住宅代理
秦老师Q2 小时前
「Chromeg谷歌浏览器/Edge浏览器」篡改猴Tempermongkey插件的安装与使用
前端·chrome·edge