从零构建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,需要的小伙伴可以自行参考~

相关推荐
青稞儿几秒前
面试题高频之token无感刷新(vue3+node.js)
vue.js·node.js
diygwcom7 分钟前
electron-updater实现electron全量版本更新
前端·javascript·electron
Hello-Mr.Wang23 分钟前
vue3中开发引导页的方法
开发语言·前端·javascript
程序员凡尘1 小时前
完美解决 Array 方法 (map/filter/reduce) 不按预期工作 的正确解决方法,亲测有效!!!
前端·javascript·vue.js
编程零零七4 小时前
Python数据分析工具(三):pymssql的用法
开发语言·前端·数据库·python·oracle·数据分析·pymssql
(⊙o⊙)~哦6 小时前
JavaScript substring() 方法
前端
无心使然云中漫步7 小时前
GIS OGC之WMTS地图服务,通过Capabilities XML描述文档,获取matrixIds,origin,计算resolutions
前端·javascript
Bug缔造者7 小时前
Element-ui el-table 全局表格排序
前端·javascript·vue.js
xnian_7 小时前
解决ruoyi-vue-pro-master框架引入报错,启动报错问题
前端·javascript·vue.js
罗政8 小时前
[附源码]超简洁个人博客网站搭建+SpringBoot+Vue前后端分离
vue.js·spring boot·后端