手写Vue3源码

news2024/11/16 11:55:03

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() 如果是文件则返回true
    if(!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;
  // 生成sourcemap
  output.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 = 20
      console.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; // 当前正在操作的effect
      fn();
      effectStack.push(effect);
    } finally {
      effectStack.pop();
      activeEffect = effectStack[effectStack.length - 1];
    }
  };
  effect.id = uid++; // 制作一个effect标识,用于区分effect
  effect._isEffect = true; // 用于标识这个是响应式effect
  effect.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,存放的是属性对应的多个effect
    let 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 } = VueReactivity
    let 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?) {
  // 得到当前对象对应的map
  let depMap = targetMap.get(target);

  // 判断这个target是否收集过依赖
  if (!depMap) return;

  // 将所有的effect放在一个数组中,最终一起执行
  let effects = [];

  const add = (effectsToAdd) => {
    if (effectsToAdd) {
      effectsToAdd.forEach((effect) => {
        effects.push(effect);
      });
    }
  };

  // 判断当前操作的是否是数组
  // 并且修改的key是legth,则需要更新数组所收集到的effect
  if (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所收集的依赖effect
      if (!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属性对应的effect
    switch (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标记,表示这是一个ref
  public __v_isRef = true;
  // 给参数前面加上一个public关键字
  // 会自动的帮我们实现:this.rowValue = rowValue; this.shallow = shallow
  constructor(public rowValue, public shallow) {
    // 如果是shallowRef 则直接返回,否则判断是否是对象,如果是对象,则递归进行深层响应式处理
    this._value = shallow ? rowValue : convert(rowValue);
  }

  // es6中的属性访问器,转成es5会转成 Object.defineProperty
  get 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标记,表示这是一个ref
  public __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) : {}
  // 遍历每一个属性。让每一个属性都变成ref
  for (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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1397130.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

书生·浦语大模型实战营第四次课堂笔记

先来看看参考作业 哈哈到这才想起来写笔记 倒回去看发现要求将不要葱姜蒜换成自己的名字和昵称&#xff01; 好好好我就是不配玩&#xff08;换成管理员也不行&#xff01;&#xff09; 诶怎么能进这个环境&#xff1f;要进双系统ubuntu&#xff1f; 现在看视频发现原来是…

Vcruntime140_1.dll丢失的错误提示怎么解决,关于Vcruntime140_1.dll文件

在使用电脑时你是否遇到过关于Vcruntime140_1.dll文件丢失的错误提示&#xff0c;出现这样的提示时是不是不知所措&#xff0c;今天就给大家讲解Vcruntime140_1.dll文件的一些相关介绍&#xff0c;希望能够帮助大家解决Vcruntime140_1.dll丢失的难题。 一.Vcruntime140_1.dll文…

档案数字化如何选择合适的扫描仪

选择合适的扫描仪是进行档案数字化的关键步骤。以下是一些选择合适扫描仪的要点&#xff1a; 1. 扫描速度&#xff1a;选择具有合适的扫描速度的扫描仪&#xff0c;以便能够快速处理大量的文件。 2. 扫描分辨率&#xff1a;扫描分辨率决定了扫描后图像的清晰度。对于大多数文档…

代码随想录二刷 | 二叉树 | 二叉搜索树的最近公共祖先

代码随想录二刷 &#xff5c; 二叉树 &#xff5c; 二叉搜索树的最近公共祖先 题目描述解题思路代码实现 题目描述 235.二叉搜索树的最近公共祖先 给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。 百度百科中最近公共祖先的定义为&#xff1a;“对于有根树 T 的…

Qt简单使用与初识

&#x1f307;个人主页&#xff1a;平凡的小苏 &#x1f4da;学习格言&#xff1a;命运给你一个低的起点&#xff0c;是想看你精彩的翻盘&#xff0c;而不是让你自甘堕落&#xff0c;脚下的路虽然难走&#xff0c;但我还能走&#xff0c;比起向阳而生&#xff0c;我更想尝试逆风…

vector迭代器的失效

1.vector的底层 vector的底层就是由三个指针构成的 iterator _start 这个指针指向vector数据起始位置。 iterator _finish这个指针指向vector数据的结束位置。 iterator _end_of_shorage这个指针指向vector容量的位置。 2.迭代器失效的种类 2.1扩容引发的迭代器失效 例&…

深入Android S (12.0) 探索Framework之输入子系统InputReader的流程

Framework层之输入系统 第一篇 深入Android S (12.0) 探索Framework之输入系统IMS的构成与启动 第二篇 深入Android S (12.0) 探索Framework之输入子系统InputReader的流程 文章目录 Framework层之输入系统前言一、基础知识1、输入子系统2、INotify 与 Epoll2.1、INotify 机制…

redisson的延时队列机制简述

概述 业务中经常会遇到一些延迟执行的需求&#xff1b;通常想到的都是rabbitmq或者rocketmq的延迟消息&#xff1b; 但是系统中不一定集成了mq&#xff0c;但为了控制分布式下的并发&#xff0c;一般redis都是有集成的&#xff1b; redis的key过期监听那个时间不准确&#xff…

初识React,基础(1), 安装react,jsx文件,类组件和函数组件,css样式

第一部分:初识react react: 用于构建用户界面的 JavaScript 库全局安装,win r, 命令: npm install create-react-app -g3. 创建一个react应用, 这里我在vscode 里面创建, 创建之后,运行 create-react-app my-appcd my-app npm start 第二部分: redact 组件定义以及使用 rea…

视频增强修复Topaz Video AI

Topaz Video AI是一款强大的视频增强软件&#xff0c;利用人工智能技术对数千个视频进行训练&#xff0c;结合多个输入视频的帧信息来提高素材的分辨率。该软件可将视频的分辨率提高到最高8K&#xff0c;并保持真实的细节和运动一致性。同时&#xff0c;它还能自动修复视频中的…

NFS网络共享存储服务技术攻略

目录 一.NFS 1.定义 2.特点 3.原理 二.服务端NFS配置文件 1.主配置文件 2.文件格式 3.相关命令 三.实验&#xff1a;NFS共享存储服务配置 1.服务端安装nfs-utils和rpcbind软件包 2.服务端新建共享目录给权限 3.服务端修改配置文件/etc/exports 4.服务端关闭防火墙…

【C++干货铺】C++11常用新特性 | 列表初始化 | STL中的变化

个人主页点击直达&#xff1a;小白不是程序媛 C系列专栏&#xff1a;C干货铺 代码仓库&#xff1a;Gitee 目录 C11简介 列表初始化 std::initializer_list std::initializer_list使用场景 decltype关键字 STL中的一些变化 新容器 array forward_list 容器中的一些新…

【Python_PySide6学习笔记(三十一)】基于PySide6实现自定义串口设备连接界面类:可实现串口连接断开、定时发送等功能

基于PySide6实现自定义串口设备连接界面类:可实现串口连接关闭、定时发送等功能 基于PySide6实现自定义串口设备连接界面类:可实现串口连接关闭、定时发送等功能前言一、界面布局二、串口相关功能实现三、完整代码四、调用方法五、实现效果基于PySide6实现自定义串口设备连接…

ChatGPT提示词保姆级教程

现在越来越多提示词教程&#xff0c;本文列个清单&#xff0c;方便以后整理&#xff0c;不定期更新&#xff0c;欢迎关注留言&#xff01; 后续更新欢迎关注 提示词&#xff08;prompt&#xff09;出来后&#xff0c;被称为一个新的岗位诞生&#xff0c;面向提示词工程师。 …

将vue项目打包成桌面客户端实现点击桌面图标直接进入项目

1.下载NW.js 下载地址&#xff1a;NW.js官网 下载完后zip解压 2.文件夹下新建index.html index内容如下&#xff1a; <!DOCTYPE html> <html> <head> </head> <body> <script language"javascript" type"text/javascript&q…

《游戏-01_2D-开发》

首先利用安装好的Unity Hub创建一个unity 2D&#xff08;URP渲染管线&#xff09;项目 选择个人喜欢的操作格局&#xff08;这里采用2 by 3&#xff09; 在Project项目管理中将双栏改为单栏模式&#xff08;个人喜好&#xff09; 找到首选项&#xff08;Preferences&#xff09…

K8s知识点总结_part1

Kubernetes 是什么 为用户提供一个具有普遍意义的容器编排工具。 它着重解决的问题是&#xff1a;大规模集群中的各种运行任务之间的关系处理&#xff0c;这些关系的处理&#xff0c;是作业编排和管理系统最困难的地方。 其能力有&#xff1a; • 基于容器的应用部署、维护和…

C#编程-使用反射检索元数据

使用反射检索元数据 术语反射通常用来指镜像。如果您站在镜子面前,镜子会反射出您的所有物理属性,如:您的身高、肤色和身体结构。在C#中也一样,反射被用于反射程序有关的所有信息。C#程序可以利用反射获得类在运行时的信息。 反射在运行时获取类型信息的过程中被使用。提…

Python自动化报告的输出用例详解

1、设计简单的用例 2、设计用例 以TestBaiduLinks.py命名 # coding:utf-8from selenium import webdriverimport unittestclass BaiduLinks(unittest.TestCase):def setUp(self):base_url https://www.baidu.comself.driver webdriver.Chrome()self.driver.implicitly_wait(…

监控系统——Zabbix

目录 Zabbix概述 Zabbix 监控原理 Zabbix 与 Prometheus的区别 Zabbix 6.0 新特性 Zabbix 6.0 功能组件 Zabbix Server 数据库 Web 界面 Zabbix Agent Zabbix Proxy Java Gateway Zabbix 6.0 部署 部署 zabbix 服务端 添加 zabbix 客户端主机 自定义监控内容…