当前位置: 首页 > news >正文

手写Vue3源码

Vue3核心源码

B站视频地址:https://www.bilibili.com/video/BV1nW4y147Pd?p=2&vd_source=36bacfbaa95ea7a433650dab3f7fa0ae

Monorepo介绍

Monorepo 是管理项目代码的一种方式,只在一个仓库中管理多个模块/包

  • 一个仓库可以维护多个模块,不用到处找仓库
  • 方便版本管理和依赖管理,模块之间的引用和调用都非常方便

缺点:仓库的体积变大,打包和安装依赖都会变慢

例如Vue3源码、element-plus 等都已经采用这种方式来管理项目

了解更多可以查看这个文档:https://www.modb.pro/db/626876

Vue3项目结构

                            +---------------------+|                     ||  @vue/compiler-sfc  ||                     |+-----+--------+------+|        |v        v+---------------------+    +----------------------+|                     |    |                      |+-------->|  @vue/compiler-dom  +--->|  @vue/compiler-core  ||         |                     |    |                      |
+----+----+    +---------------------+    +----------------------+
|         |
|   vue   |
|         |
+----+----+   +---------------------+    +----------------------+    +-------------------+|         |                     |    |                      |    |                   |+-------->|  @vue/runtime-dom   +--->|  @vue/runtime-core   +--->|  @vue/reactivity  ||                     |    |                      |    |                   |+---------------------+    +----------------------+    +-------------------+
  • reactivity: 响应式系统
  • runtime-core:与平台无关的运行时核心 (可以创建针对特定平台的运行时 - 自定义渲染器)
  • runtime-dom: 针对浏览器的运行时。包括DOM API,属性,事件处理等
  • runtime-test:用于测试
  • server-renderer:用于服务器端渲染
  • compiler-core:与平台无关的编译器核心
  • compiler-dom: 针对浏览器的编译模块
  • compiler-ssr: 针对服务端渲染的编译模块
  • compiler-sfc: 针对单文件解析
  • size-check:用来测试代码体积
  • template-explorer:用于调试编译器输出的开发工具
  • shared:多个包之间共享的内容
  • vue:完整版本,包括运行时和编译器

Vue3构建流程搭建

vue3采用 monorepo 的方式,目前只有 yarn 支持,所以我们需要使用 yarn 来构建项目

初始化

找一个空文件夹执行命令

yarn init -y

然后修改生成的 package.json 文件

{"private":"true","workspaces":["packages/*"],"name": "zf-vue3","version": "1.0.0","main": "index.js","license": "MIT","devDependencies": {"@rollup/plugin-json": "^4.1.0","@rollup/plugin-node-resolve": "^13.0.0","execa": "^5.1.1","rollup": "^2.52.3","rollup-plugin-typescript2": "^0.30.0","typescript": "^4.3.4"}
}
  • private 表示这是一个私有的

  • workspaces 声明工作区间,将来我们的包都在 packages 这个文件夹下面

  • 修改了一下 name

然后执行安装命令

yarn install
依赖
typescript支持typescript
rollup打包工具
rollup-plugin-typescript2rollup 和 ts的 桥梁
@rollup/plugin-node-resolve解析node第三方模块
@rollup/plugin-json支持引入json
execa开启子进程方便执行命令

声明子文件

reactivity

新建 packages\reactivity\src\index.ts

const Reactivity = {}
export {Reactivity
}

然后在 packages\reactivity 位置执行 yarn init -y 初始化 package.json 文件

接着修改 package.json 文件

packages/reactivity/package.json

{"name": "@vue/reactivity", // 设置包的名称"version": "1.0.0","main": "index.js", // 在node环境中使用时会找 main 属性对应的地址"module": "dist/reactivity.esm-bundler.js", // es6模式下 import @vue/reactivity --> 使用的是这个地址"buildOptions":{ // 自定义打包配置"name": "VueReactivity", // 全局名称配置,通过 script 标签引入时,全局就会有 VueReactivity 这个名字"formats": ["esm-bundler","cjs","global"] // 打包模式,支持三种模式,node、es6、全局模块},"license": "MIT"
}
shared

新建 packages\shared\src\index.ts

const Shared = {}
export {Shared
}

然后在 packages\shared 位置执行 yarn init -y 初始化 package.json 文件

接着修改 package.json 文件

packages/shared/package.json

{"name": "@vue/shared","version": "1.0.0","main": "index.js","module": "dist/shared.esm-bundler.js","buildOptions":{"name": "VueShared","formats": ["esm-bundler","cjs"]},"license": "MIT"
}

编译脚本、rollup、ts配置

package.json 添加脚本配置

"scripts": {"dev":"node scripts/dev.js","build":"node scripts/build.js"
},

scripts/dev.js

这个文件只做某个包的打包,给 rollup 传入TARGET环境变量,生成 rollup 配置

const execa = require('execa');
const target = "reactivity";build(target)async function build(target) {// 执行rollup命令,并传入参数,其中TARGET:${target}表示目标环境// stdio: 'inherit' 将子进程的日志输出到主进程await execa('rollup', ['-cw', `--environment`, `TARGET:${target}`], { stdio: 'inherit' });
}

scripts/build.js

这个文件用来打包 packages 文件夹下所有的包

const fs = require('fs');
const execa = require('execa'); // 开启子进程打包// 过滤packages目录下的文件,只要保留文件夹
const targets = fs.readdirSync("packages").filter(f=>{// fs.statSync(`packages/reactivity`).isDirectory() 如果是文件则返回trueif(!fs.statSync(`packages/${f}`).isDirectory()){return false}else{return true}
})// 对不同的包进行依次打包
async function build(target){await execa("rollup",["-c",`--environment`,`TARGET:${target}`],{stdio: "inherit"})
}// 遍历所有的包,调用build方法
function runParallel(targets,buildFn){const result = []targets.forEach(item=>{result.push(buildFn(item))})return Promise.all(result)
}runParallel(targets,build).then(()=>{console.log("打包完成")
})

rollup.config.js

import path from "path";
import json from "@rollup/plugin-json";
import resolvePlugin from "@rollup/plugin-node-resolve";
import ts from "rollup-plugin-typescript2";// 找到packages
const packagesDir = path.resolve(__dirname, "packages");// 根据环境变量中的TARGET,找到模块中对应的package.json
const packageDir = path.resolve(packagesDir, process.env.TARGET);// 取到每个模块下面的package.json
const resolve = (p) => path.resolve(packageDir, p);
const pkg = require(resolve("package.json"));// 对打包类型做一个映射表,根据每个包的package.json中配置formats来格式化需要打包的内容
const outputConfig = {"esm-bundler": {file: resolve(`dist/${process.env.TARGET}.esm-bundler.js`),format: "es",},cjs: {file: resolve(`dist/${process.env.TARGET}.cjs.js`),format: "cjs",},global: {file: resolve(`dist/${process.env.TARGET}.global.js`),format: "iife",},
};const options = pkg.buildOptions;// 生成rollup配置
function createConfig(format, output) {// 声明全局名称output.name = options.name;// 生成sourcemapoutput.sourcemap = true;return {input: resolve(`src/index.ts`),output,plugins: [json(), ts({tsconfig:path.resolve(__dirname, "tsconfig.json"),}),resolvePlugin(),],};
}// 遍历formats生成不同的打包文件
export default options.formats.map((format) => createConfig(format, outputConfig[format]));

tsconfig.json

在根目录执行 npx tsc --init 先初始化一个 tsconfig.json 文件,然后添加如下内容

{"compilerOptions": {"target": "ESNEXT","module": "ESNEXT","strict": false,"esModuleInterop": true,"skipLibCheck": true,"forceConsistentCasingInFileNames": true,"moduleResolution": "node","baseUrl": ".","paths": {"@vue/*": ["packages/*/src"]}}
}

至此我们就可以来尝试打包一下,输入命令

yarn build

image-20240108172437702

对比打包后的文件内容可以发现两个包下面生成的打包文件是不一样的

image-20240108172539256

包之间的互相引用

当我们执行 yarn install 后会自动的再 node_modules 中生成软连接

image-20240108172840053

后面的箭头表示这是一个软连接。然后我们在代码里使用如下方式引入时

import { Shared } from "@vue/shared"const Reactivity = {}
export {Reactivity
}

@vue/shared 指向的就是这个包所在的真实文件地址

reactive Api实现

四个核心的API

import { reactive,shallowReactive,readonly,shallowReadonly } from "vue"
  • reactive:深层次的响应对象
  • shallowReactive:浅层响应对象,只会吧对象的第一层变成响应式的
  • readonly:对象是只读的
  • shallowReadonly:浅层对象只读

开启sourceMap

tsconfig.json 文件中的 sourceMap 打开并且设置成true,方便我们调试源码

{"compilerOptions": {"target": "ESNEXT","module": "ESNEXT","strict": false,"esModuleInterop": true,"skipLibCheck": true,"forceConsistentCasingInFileNames": true,"moduleResolution": "node","baseUrl": ".","paths": {"@vue/*": ["packages/*/src"]},"sourceMap": true}
}

目录结构

├── example
│   └── 1.reactivity-api.html  // 测试文件
├── package.json
├── packages
│   ├── reactivity
│   │   ├── package.json
│   │   └── src
│   │       ├── baseHandlers.ts
│   │       ├── index.ts  // reactive核心方法文件,只做导出操作
│   │       └── reactive.ts
│   └── shared
│       ├── package.json
│       └── src
│           └── index.ts // 公共功能
├── rollup.config.js
├── scripts
│   ├── build.js
│   └── dev.js
└── tsconfig.json

shared/src/index.ts

这个文件专门放置一些公共的方法

// 判断一个数据是否是一个对象
export function isObject (value){return typeof value === 'object' && value !== null;
}// 合并两个对象
export function mergeObjects<T extends object, U extends object>(obj1: T, obj2: U): T & U {return Object.assign(obj1, obj2);
}

reactivity/src/index.ts

// 导出四个核心方法
export {reactive,shallowReactive,readonly,shallowReadonly,
} from "./reactive";

reactivity/src/reactive.ts

import { isObject } from "@vue/shared";
import {mutableHandlers,readonlyHandlers,shallowReactiveHandlers,shallowReadonlyHandlers,
} from "./baseHandlers";export function reactive(target) {return createReactiveObject(target, false, mutableHandlers);
}export function shallowReactive(target) {return createReactiveObject(target, false, shallowReactiveHandlers);
}export function readonly(target) {return createReactiveObject(target, true, readonlyHandlers);
}export function shallowReadonly(target) {return createReactiveObject(target, true, shallowReadonlyHandlers);
}// 创建两个映射map,存放已经代理过的数据
// WeakMap 可以自动垃圾回收,不用担心内存泄漏问题。并且key只能是对象类型
const reactiveMap = new WeakMap();
const readlonlyMap = new WeakMap();/*** 创建代理方法* @param target 需要被代理的对象 * @param isReadonly 是否是只读的* @param baseHandlers get set 方法* @returns*/
export function createReactiveObject(target, isReadonly, baseHandlers) {// 判断这个数据是否是一个对象if (!isObject(target)) return target;// 判断这个对象是否被代理过,如果已经被代理过,则从map中获取数据const proxyMap = isReadonly ? readlonlyMap : reactiveMap;const existProxy = proxyMap.get(target);if (existProxy) return existProxy;// 创建代理对象const proxy = new Proxy(target, baseHandlers);proxyMap.set(proxy, target);// 最后返回被代理的对象return proxy;
}

reactivity/src/baseHandlers.ts

import { isObject, mergeObjects } from "@vue/shared";
import { reactive, readonly } from "./reactive";// 抽离出共用的set方法
const readonlyObj = {set: (target, key) => {console.warn(`key:${key} set 失败,因为这个对象是只读的`);},
}; // 拦截get的方法,柯里化
function createGetter(isReadonly = false, isShallow = false) {/*** 这个内部的方法参数来自于读取某个对象是,proxy传递进来的* target: 当前对象本身* key: 当前读取的key* receiver: 当前的代理对象*/return function (target, key, receiver) {// 这里使用Reflect.get来获取对象,相当于 target[key]// 后续Object上的方法,会被迁移到Reflect上去,Reflect.getProptotypeof()// target[key] = value 的方式可能会设置失败,但是并不会返回报错,也没有返回标识来表示是否成功// Reflect 方法具备返回值,所以这里要使用 Reflect 来取值和set值const res = Reflect.get(target, key, receiver);if (!isReadonly) {// 进行依赖收集,只有不是只读的对象才会进行依赖收集}// 如果是shallow则只返回拿到的值,不进行深层次代理if (isShallow) return res;// 如果拿到的返回值仍然是一个对象,则进行递归响应式处理// 这里和vue2不同,vue2是上来就进行对象的递归响应式处理// vue3则是懒代理,只有到读取到这个对象的某个属性时,才会对这个对象进行响应式处理if (isObject(res)) {return isReadonly ? readonly(res) : reactive(res);}return res;};
}// 拦截set的方法,柯里化
function createSetter(isShallow = false) {return function (target, key, value, receiver) {const res = Reflect.set(target, key, value, receiver);return res};
}// 生成不同的get方法
const get = createGetter();
const shallowGet = createGetter(false, true);
const readonlyGet = createGetter(true);
const shallowReadonlyGet = createGetter(true, true);// 生成不同的set方法
const set = createSetter();
const shallowSet = createSetter(true);export const mutableHandlers = {get,set,
};export const shallowReactiveHandlers = {get: shallowGet,set: shallowSet,
};// 合并两个对象,共用一个set方法
export const readonlyHandlers = mergeObjects({get: readonlyGet,},readonlyObj
);
// 合并两个对象,共用一个set方法
export const shallowReadonlyHandlers = mergeObjects({get: shallowReadonlyGet,},readonlyObj
);

测试reactive Api

example/1.reactive.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><script src="../node_modules/@vue/reactivity/dist/reactivity.global.js"></script><script>let { reactive, shallowReactive, readonly, shallowReadonly } =VueReactivity;let state = shallowReadonly({ name:"李四",age:{n:18}});state.name = 20console.log(state.age);</script></body>
</html>

Effect依赖收集

effect.ts

packages/reactivity/src/effect.ts

export function effect(fn: Function, options: any = {}) {const effect = createReactiveEffect(fn, options);if (!options.lazy) {effect();}return effect;
}let uid = 0;
let activeEffect; // 当前正在操作的effect
const effectStack = []; // 使用一个栈来存储effect
// 创建一个响应式的effect,根据不同的属性创建不同的effect方法
function createReactiveEffect(fn, options) {const effect = function reactiveEffect() {try {activeEffect = effect; // 当前正在操作的effectfn();effectStack.push(effect);} finally {effectStack.pop();activeEffect = effectStack[effectStack.length - 1];}};effect.id = uid++; // 制作一个effect标识,用于区分effecteffect._isEffect = true; // 用于标识这个是响应式effecteffect.row = fn; // 保留effect对应的原函数effect.options = options; // 保留effect的属性return effect;
}// 依赖收集方法,在 effect 中读取属性时就会触发 get 方法
// 在get方法中将对象本身,以及当前的key传递过来
// 然后和effect进行对应关联
const targetMap = new WeakMap()
export function track(target, type, key) {if(!activeEffect) return// 使用如下一个结构来进行属性和effect关联// new WeakMap( target, new Map( key, [effect1,effect2] ))let depTarget = targetMap.get(target)if(!depTarget){targetMap.set(target, depTarget = new Map())}// 拿到的是一个set,存放的是属性对应的多个effectlet depMap = depTarget.get(key)if(!depMap){depTarget.set(key, depMap = new Set())}depMap.add(activeEffect)
}

baseHandlers.ts

packages/reactivity/src/baseHandlers.ts

修改 createGetter 方法,在get时调用 effect.js 中的 track 方法,传入 target,type,key 进行响应式依赖收集

+ import { track } from "./effect";
+ import { TrackOpTypes } from "./operators";// 拦截get的方法,柯里化
function createGetter(isReadonly = false, isShallow = false) {/*** 这个内部的方法参数来自于读取某个对象是,proxy传递进来的* target: 当前对象本身* key: 当前读取的key* receiver: 当前的代理对象*/return function (target, key, receiver) {// 这里使用Reflect.get来获取对象,相当于 target[key]// 后续Object上的方法,会被迁移到Reflect上去,Reflect.getProptotypeof()// target[key] = value 的方式可能会设置失败,但是并不会返回报错,也没有返回标识来表示是否成功// Reflect 方法具备返回值,所以这里要使用 Reflect 来取值和set值const res = Reflect.get(target, key, receiver);if (!isReadonly) {// 进行依赖收集,只有不是只读的对象才会进行依赖收集
+      track(target,TrackOpTypes.GET,key)}// 如果是shallow则只返回拿到的值,不进行深层次代理if (isShallow) return res;// 如果拿到的返回值仍然是一个对象,则进行递归响应式处理// 这里和vue2不同,vue2是上来就进行对象的递归响应式处理// vue3则是懒代理,只有到读取到这个对象的某个属性时,才会对这个对象进行响应式处理if (isObject(res)) {return isReadonly ? readonly(res) : reactive(res);}return res;};
}

operators.ts

packages/reactivity/src/operators.ts

定义一个枚举,用于区分场景

// 设置一个枚举
export const enum TrackOpTypes {GET
} 

测试effect

example/2.effect.html

<div id="app"></div>
<script src="../node_modules/@vue/reactivity/dist/reactivity.global.js"></script>
<script>let { effect,reactive } = VueReactivitylet state = reactive({name:'张三',age:18})effect(()=>{app.innerText = `${state.name} + ${state.age}`})
</script>

最终生成的 targetMap 结构

image-20240109171018102

触发更新

修改createSetter方法

修改 /reactivity/src/baseHandlers.ts 文件中的 createSetter 方法

import {hasChange,hasOwn,isArray,isIntegerKey,isObject,mergeObjects,
} from "@vue/shared";
import { reactive, readonly } from "./reactive";
import { track, trigger } from "./effect";
import { TrackOpTypes, TriggerOpTypes } from "./operators";// 拦截set的方法,柯里化
function createSetter(isShallow = false) {return function (target, key, value, receiver) {// 获取旧值const oldValue = Reflect.get(target, key, receiver);// set数据时的时候更新这个key所收集的effect// 先判断target是否是一个数组并且key是否是数字字符串// 如果是则判断修改的key是否是在数组下标内// Number(key) < target.length 表示当前修改的下标在原有数组内,因为此时的target还没有发生修改// 否则的话再去判断当前修改的key是否在target身上let hadKey =isArray(target) && isIntegerKey(key)? Number(key) < target.length: hasOwn(target, key);// 修改值,Reflect.set返回的是一个布尔值,true表示修改成功const res = Reflect.set(target, key, value, receiver);if (!hadKey) {// 新增trigger(target, TriggerOpTypes.ADD, key, value);} else {// 判断两个值不相等的情况if (hasChange(oldValue, value)) {// 修改trigger(target, TriggerOpTypes.SET, key, value, oldValue);}}return res;};
}

reactivity/src/operators.ts

operators 文件中增加了一个枚举类,用于区分时新增一个属性还是修改一个属性

// set时用于区分时新增一个属性还是修改一个属性
export const enum TriggerOpTypes {ADD,SET,
}

shared/src/index.ts

公共方法增加了一些方法

// 判断一个数据是否是一个对象
export function isObject(value) {return typeof value === "object" && value !== null;
}// 合并两个对象
export function mergeObjects<T extends object, U extends object>(obj1: T,obj2: U
): T & U {return Object.assign(obj1, obj2);
}// 判断数据是否是数组
export function isArray<T>(value: unknown): value is T[] {return Array.isArray(value);
}// 判断数据是否是函数
export function isFunction(value: unknown): value is Function {return typeof value === "function";
}// 判断数据是否是数字
export function isNumber(value: unknown): value is number {return typeof value === "number";
}// 判断数据是否是字符串
export function isString(value: unknown): value is string {return typeof value === "string";
}// 判断一个字符串是否是数字字符串
export function isIntegerKey(key) {return parseInt(key) + "" === key;
}// 判断对象中是否有某个属性
export function hasOwn(obj, key) {return Object.prototype.hasOwnProperty.call(obj, key);
}// 判断两个数据是否相等
export function hasChange(oldValue, newValue) {return oldValue !== newValue;
}// 判断数据是否是Symbol类型
export function isSymbol(value) {return typeof value === "symbol";
}

reactivity/src/effect.ts

effect 中去增加一个 tagger 方法,此方法会根据传递进来的key和target,找到对应的 dep 进行更新,也就是会重新触发这个属性对应的 effect,从而实现页面更新

import { isArray, isIntegerKey, isSymbol } from "@vue/shared";
import { TriggerOpTypes } from "./operators";// 更新依赖的方法
export function trigger(target, type, key?, newValue?, oldValue?) {// 得到当前对象对应的maplet depMap = targetMap.get(target);// 判断这个target是否收集过依赖if (!depMap) return;// 将所有的effect放在一个数组中,最终一起执行let effects = [];const add = (effectsToAdd) => {if (effectsToAdd) {effectsToAdd.forEach((effect) => {effects.push(effect);});}};// 判断当前操作的是否是数组// 并且修改的key是legth,则需要更新数组所收集到的effectif (key === "length" && Array.isArray(target)) {console.log(depMap, "depMapdepMap");// Map类型数据进行forEach遍历是,第一个是键值对的值,第二个是键depMap.forEach((deps, key) => {// key > newValue// 例如我effect中使用了 state.arr[2],则收集到的依赖就会有一个key是2// 如果我更新时 state.arr.length = 1,则也要更新这个数组target所收集的依赖effectif (!isSymbol(key) && (key === "length" || key > newValue)) {add(deps);}});} else {// 这里可能是对象if (key !== undefined) {add(depMap.get(key));}// 如果是这种情况: state.arr[100] = 1// 这种情况表示更新的是数组的某个索引,此时key就是100// 但是100并不在原有数组的属性上,所以type是ADD// 去更新这个数组对应的length属性对应的effectswitch (type) {case TriggerOpTypes.ADD:if (isArray(target) && isIntegerKey(key)) {add(depMap.get("length"));}}}effects.forEach((effect) => effect());
}

测试触发更新

<div id="app"></div>
<script src="../node_modules/@vue/reactivity/dist/reactivity.global.js"></script>
<script>let { effect, reactive } = VueReactivity;let state = reactive({ name: "张三", age: 18, arr: [1, 2, 3] });effect(() => {app.innerHTML = `${state.arr} + ${state.arr.length}`;});setTimeout(() => {state.arr.length = 100;}, 1000);
</script>

image-20240109231817514

ref Api实现

四个API

ref 有关的API共有四个

  • ref:将一个普通数据类型转成响应式的,如果穿过来的是一个对象,则会调用 reactive 进行响应式处理
  • shallowRef:浅层的ref
  • toRef:用法 let refKey =toRef(object,key) 将某个对象中的某一个属性变成响应式的,此时修改 refKey.value = xxx 相当于修改的就是 object.key = xxx
  • toRefs:用法 let {key1,key2} = toRefs(object) 将某个对象中的所有属性变成响应式的,在对象解构时可以用这个方法,否则解构出来的属性会丢失响应式

reactivity/src/ref.ts

下面是源码实现

import { hasChange, isArray, isObject } from "@vue/shared";
import { track, trigger } from "./effect";
import { TrackOpTypes, TriggerOpTypes } from "./operators";
import { reactive } from "./reactive";export function ref(value) {return createRef(value);
}export function shallowRef(value) {return createRef(value, true);
}function convert(value) {return isObject(value) ? reactive(value) : value;
}class RefImpl {public _value: any;// 产生的实例会被添加一个__v_isRef标记,表示这是一个refpublic __v_isRef = true;// 给参数前面加上一个public关键字// 会自动的帮我们实现:this.rowValue = rowValue; this.shallow = shallowconstructor(public rowValue, public shallow) {// 如果是shallowRef 则直接返回,否则判断是否是对象,如果是对象,则递归进行深层响应式处理this._value = shallow ? rowValue : convert(rowValue);}// es6中的属性访问器,转成es5会转成 Object.definePropertyget value() {track(this, TrackOpTypes.GET, "value");return this._value;}set value(newValue) {if (hasChange(this._value, newValue)) {this.rowValue = newValue;this._value = newValue;trigger(this, TriggerOpTypes.SET, "value", newValue);}}
}export function createRef(value, shallow = false) {return new RefImpl(value, shallow);
}// toRef返回的实例不再需要收集依赖,如果传过来的target已经被响应式了,则生成的ref还是响应式的
// 在set value时还是会走响应式中的set方法进行依赖更新,所以这里不需要依赖收集
class ObjectRefImpl {// 产生的实例会被添加一个__v_isRef标记,表示这是一个refpublic __v_isRef = true;constructor(public target, public key) {}get value(){return this.target[this.key];}set value(newValue){this.target[this.key] = newValue;}
}// toRef实现
export function toRef(target, key) {return new ObjectRefImpl(target, key);
}// toRefs实现
export function toRefs(object) {const res = isArray(object) ? new Array(object.length) : {}// 遍历每一个属性。让每一个属性都变成reffor (let key in object) {res[key] = toRef(object, key);}return res
}

reactivity/src/index.ts

在 index 文件导入四个ref的四个方法

export {ref,shallowRef,toRef,toRefs} from "./ref"

测试ref

<div id="app"></div>
<div id="app1" style="width: 100px;height: 100px;"></div><script src="../node_modules/@vue/reactivity/dist/reactivity.global.js"></script><script>let { ref,effect,reactive,toRef,toRefs } = VueReactivity;// ref测试let color = ref("red")effect(()=>{app1.style.backgroundColor = color.value})setTimeout(()=>{color.value = "blue"},1000)// toRefs测试let state = reactive({name:"王五",age:12})let {name,age} = toRefs(state)effect(()=>{app.innerHTML = `${name.value} - ${age.value}`})setTimeout(()=>{name.value = "张三123"},1000)
</script>

image-20240110140402875

相关文章:

手写Vue3源码

Vue3核心源码 B站视频地址&#xff1a;https://www.bilibili.com/video/BV1nW4y147Pd?p2&vd_source36bacfbaa95ea7a433650dab3f7fa0ae Monorepo介绍 Monorepo 是管理项目代码的一种方式&#xff0c;只在一个仓库中管理多个模块/包 一个仓库可以维护多个模块&#xff0c;…...

如何无需重复输入FTP信息来安装WordPress主题和插件

WordPress作为一个广受欢迎的内容管理系统&#xff0c;提供了丰富的主题和插件来扩展网站的功能和外观。然而&#xff0c;许多用户在安装这些主题和插件时&#xff0c;经常遇到需要重复输入FTP信息的麻烦。幸运的是&#xff0c;有几种方法可以解决这个问题&#xff0c;让安装过…...

开发安全之:JSON Injection

Overview 在 XXX.php 的第 X 行中&#xff0c;responsemsg() 方法将未经验证的输入写入 JSON。攻击者可以利用此调用将任意元素或属性注入 JSON 实体。 Details JSON injection 会在以下情况中出现&#xff1a; 1. 数据从一个不可信赖的数据源进入程序。 2. 将数据写入到 …...

各种Linux版本安装Docker

文章目录 一、Ubuntu 20.04.61. 网卡和DNS配置2. Docker安装 二、CentOS Linux 7.91. 网卡和DNS配置2. Docker安装 三、Alibaba Cloud Linux 31. DNS配置2. repo说明3. Docker安装 四、验证是否安装成功 一、Ubuntu 20.04.6 1. 网卡和DNS配置 /etc/netplan 找到 *.yaml 文件 …...

git中合并分支时出现了代码冲突怎么办

目录 第一章、Git代码冲突介绍1.1&#xff09;什么是Git代码冲突①git merge命令介绍②代码冲突原因 1.2&#xff09;提示代码冲突的两种情况①本地不同分支的文件有差异时&#xff1a;②本地仓库和git远程仓库的文件有差异时&#xff1a; 1.3&#xff09;解决合并时的代码冲突…...

什么是防火墙?

目录 什么是防火墙&#xff0c;为什么需要防火墙&#xff1f;防火墙与交换机、路由器对比防火墙和路由器实现安全控制的区别防火墙的发展史1989年至1994年1995年至2004年2005年至今 什么是防火墙&#xff0c;为什么需要防火墙&#xff1f; “防火墙”一词起源于建筑领域&#x…...

tui.calender日历创建、删除、编辑事件、自定义样式

全是坑&#x1f573;&#xff01;全是坑&#x1f573;&#xff01;全是坑&#x1f573;&#xff01;能不用就不用&#xff01; 官方文档&#xff1a;https://github.com/nhn/tui.calendar/blob/main/docs/en/apis/calendar.md 实例的一些方法&#xff0c;比如创建、删除、修改、…...

OpenHarmonyOS-gn与Ninja

GN语法及在鸿蒙的使用 [gnninja学习 0x01]gn和ninja是什么 ohos_sdk/doc/subsys-build-gn-coding-style-and-best-practice.md GN 语言与操作 一、gn简介 gn是generate ninja的缩写&#xff0c;它是一个元编译系统&#xff08;meta-build system&#xff09;,是ninja的前端&am…...

Docker部署Traefik结合内网穿透远程访问Dashboard界面

文章目录 前言1. Docker 部署 Trfɪk2. 本地访问traefik测试3. Linux 安装cpolar4. 配置Traefik公网访问地址5. 公网远程访问Traefik6. 固定Traefik公网地址 前言 Trfɪk 是一个云原生的新型的 HTTP 反向代理、负载均衡软件&#xff0c;能轻易的部署微服务。它支持多种后端 (D…...

2024年甘肃省职业院校技能大赛信息安全管理与评估 样题二 理论题

竞赛需要完成三个阶段的任务&#xff0c;分别完成三个模块&#xff0c;总分共计 1000分。三个模块内容和分值分别是&#xff1a; 1.第一阶段&#xff1a;模块一 网络平台搭建与设备安全防护&#xff08;180 分钟&#xff0c;300 分&#xff09;。 2.第二阶段&#xff1a;模块二…...

从代码到项目管理:程序员的职业跃迁与PMP认证之路

哈喽&#xff0c;我是eleven,软件工程专业毕业&#xff0c;工作六年多的时间从事过测试岗、研发岗、项目经理岗。一路走来一直按照自己的职业规划向前发展&#xff0c;每一步都成功转型&#xff0c;目前已顺利拿到PMP项目管理资格认证。希望能通过自己的经验给大家带来些许帮助…...

空间形状对结构加法产物的影响

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 1 有2个点被固定在一个5*5的平面内&#xff0c;在这个平面内还有2个点在随机的运动。最终这4个点是如何分布的&#xff1f; 1 - - - 5 - - 1 9 - - 1 - 13 - - - 1 1 1 - 1 …...

构建高效外卖系统:技术实践与代码示例

外卖系统在现代社会中扮演着重要的角色&#xff0c;为用户提供了便捷的用餐解决方案。在这篇文章中&#xff0c;我们将探讨构建高效外卖系统的技术实践&#xff0c;同时提供一些基础的代码示例&#xff0c;帮助开发者更好地理解和应用这些技术。 1. 技术栈选择 构建外卖系统…...

HCIP-BGP选路实验

一.实验拓扑图 二.详细配置 R1 interface GigabitEthernet0/0/0 ip address 12.1.1.1 255.255.255.0interface LoopBack0 ip address 1.1.1.1 255.255.255.0interface LoopBack1 ip address 10.1.1.1 255.255.255.0bgp 1 router-id 1.1.1.1 peer 12.1.1.2 as-number 2ipv4-fa…...

线性表--顺序表

目录 1.什么是顺序表 2.动态顺序表实现 2.1动态顺序表结构体 2.2初始化 2.3打印验证函数 2.4判断是否扩容&#xff0c;按需扩容 2.5头插/尾插 2.6头删/尾删 2.7指定位置插入数据/指定位置删除数据 3.动态顺序表代码 1.什么是顺序表 线性表是n个具有相同特性的数据元素的…...

前端面试题:节流和防抖

节流和防抖都是通过降低事件执行的频率而达到节省资源的效果 节流 一段时间只执行一次,多少秒之后获取验证码、resize 事件和scroll 事件等 类似王者荣耀中的传送,一段时间内只能传送一次,具体实现如下: function throttle(fn, delay) {let lastTime = 0;return functi…...

网络工程师学习笔记——交换机路由器 数据传输

交换机和路由器是数据通信最核心&#xff0c;也是所有网工最熟悉的设备。今天学习&#xff1a;交换机%路由器数据传输过程。 目录 一、交换机 1、交换机原理 2、交换机数据传输过程 3、交换机基本原理配置命令 二、路由器 1、路由器原理 2、路由器数据传输过程 3、静态…...

【论文笔记】A Survey on 3D Gaussian Splatting

原文链接&#xff1a;https://arxiv.org/abs/2401.03890 1. 引言 NeRF在计算效率和可控性上具有局限性&#xff0c;这导致了3D高斯溅射&#xff08;3D GS&#xff09;的出现&#xff0c;重新定义了场景表达和渲染。 3D GS通过引入新的场景表达技术&#xff0c;用大量的3D高斯…...

项目实战————苍穹外卖(DAY11)

苍穹外卖-day11 课程内容 Apache ECharts 营业额统计 用户统计 订单统计 销量排名Top10 功能实现&#xff1a;数据统计 数据统计效果图&#xff1a; 1. Apache ECharts 1.1 介绍 Apache ECharts 是一款基于 Javascript 的数据可视化图表库&#xff0c;提供直观&#x…...

非常好用的Mac清理工具CleanMyMac X 4.14.7 如何取消您对CleanMyMac X的年度订购

CleanMyMac X 4.14.7是Mac平台上的一款非常著名同时非常好用的Mac清理工具。全方位扫描您的Mac系统&#xff0c;让垃圾无处藏身&#xff0c;您只需要轻松单击2次鼠标左键即可清理数G的垃圾&#xff0c;就这么简单。瞬间提升您Mac速度。 CleanMyMac X 4.14.7下载地址&#xff1a…...

【51单片机系列】proteus仿真单片机的串口通信

本文参考&#xff1a;https://zhuanlan.zhihu.com/p/425809292。 在proteus之外使用串口软件和单片机通信。通过在proteus设计一个单片机接收PC发送的数据&#xff0c;并将接收的数据发送出去&#xff0c;利用软件【Configure Virtual Serial Port Driver】创建一对虚拟串口&am…...

【Qt】对象树与坐标系

需要云服务器等云产品来学习Linux的同学可以移步/-->腾讯云<--/-->阿里云<--/-->华为云<--/官网&#xff0c;轻量型云服务器低至112元/年&#xff0c;新用户首次下单享超低折扣。 目录 一、Qt Creator快捷键 二、对象树 1、对象树的析构 2、自定义类的编写…...

【设计模式】腾讯二面:自动贩卖机/音频播放器使用了什么设计模式?

状态模式是什么&#xff1f; 状态模式&#xff0c;也被称作状态对象模式&#xff0c;是一种行为设计模式。 当一个对象的内在状态改变时&#xff0c;允许改变其行为&#xff0c;这个对象看起来像是改变了其类。 它让对象在其内部状态改变时改变自己的行为。外部调用者无需了…...

转换操作符转换类型:普通函数指针(普通函数、类的静态函数)、类的成员函数指针

一、转换操作符的定义 转换操作符是一种特殊的类成员函数 &#xff0c;它定义将类类型值转变为其他类型值的转换&#xff0c;转换操作符在类定义体内声明&#xff0c;在保留字operator之后跟着转换的目标类型&#xff0c;转换函数采用如下通用形式&#xff1a; operator type(…...

易控智驾高精度地图开发工程师校招一面、二面面经

本文介绍2024届秋招中&#xff0c;北京易控智驾科技有限公司的高精度地图开发工程师岗位的2场面试基本情况、提问问题等。 12月投递了北京易控智驾科技有限公司的高精度地图开发工程师岗位&#xff0c;所在部门暂不清楚。目前完成了一面、二面流程&#xff0c;在这里记录一下2场…...

用VSCode玩STM32的烧录工具 CooCox Cortex Flash Programmer

一、下载软件 经热心兄弟推荐的版本&#xff0c;不知道有没有版权&#xff0c;如有版权问题&#xff0c;请通知删除。 CSDN - 0积分下载&#xff1a;https://download.csdn.net/download/qq_49053936/88744187 二、生成bin文件 插件不同&#xff0c;方法有所不同&#xff0c;各…...

Pycharm无法刷新远程解释器的框架: Can‘t get remote credentials for deployment server

在Pycharm上部署项目到远程服务器&#xff0c;有时候需要启动SSH会话&#xff0c;启动的时候发现没反应&#xff0c;且事件日志显示&#xff1a;无法刷新远程解释器的框架: Can’t get remote credentials for deployment server 观察pycharm界面最下边&#xff0c;发现“无默…...

c++设计模式之单例模式

介绍 一个类无论创建多少对象&#xff0c;都只能得到一个实例 A* p1new A(); A* p2new A(); A* p3new A(); 如上述代码中&#xff0c;我们通过new运算符创建出了类A的三个对象实例&#xff0c;而我们现在要做的是&#xff0c;如何设计类A&#xff0c;使得上述代码运行之后永远…...

Git学习笔记(第5章):Git团队协作机制

目录 5.1 团队内协作 5.2 跨团队协作 Git进行版本控制都是在本地库操作的。若想使用Git进行团队协作&#xff0c;就必须借助代码托管中心。 5.1 团队内协作 问题引入&#xff1a;成员1&#xff08;大佬&#xff09;利用Git在宿主机上初始化本地库&#xff0c;完成代码的整体…...

Python 面向对象绘图(Matplotlib篇-16)

Python 面向对象绘图(Matplotlib篇-16)         🍹博主 侯小啾 感谢您的支持与信赖。☀️ 🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ�…...