Vue3 特点以及优势-源码解剖

Vue3 特点以及优势-Vue3.4源码解剖

Vue3 特点以及优势

1.声明式框架

命令式和声明式区别

  • 早在 JQ 的时代编写的代码都是命令式的,命令式框架重要特点就是关注过程
  • 声明式框架更加关注结果。命令式的代码封装到了 Vuejs 中,过程靠 vuejs 来实现

声明式代码更加简单,不需要关注实现,按照要求填代码就可以

- 命令式编程:
let numbers = [1,2,3,4,5]
let total = 0
for(let i = 0; i < numbers.length; i++) {
   
  total += numbers[i] - 关注了过程
}
console.log(total)

- 声明式编程:
let total2 = numbers.reduce(function (memo,current) {
   
  return memo + current
},0)
console.log(total2)

2.采用虚拟 DOM

传统更新页面,拼接一个完整的字符串 innerHTML 全部重新渲染,添加虚拟 DOM 后,可以比较新旧虚拟节点,找到变化在进行更新。虚拟 DOM 就是一个对象,用来描述真实 DOM 的

const vnode = {
   
  __v_isVNode: true,
  __v_skip: true,
  type,
  props,
  key: props && normalizeKey(props),
  ref: props && normalizeRef(props),
  children,
  component: null,
  el: null,
  patchFlag,
  dynamicProps,
  dynamicChildren: null,
  appContext: null,
};

3.区分编译时和运行时

  • 我们需要有一个虚拟 DOM,调用渲染方法将虚拟 DOM 渲染成真实 DOM (缺点就是虚拟 DOM 编写麻烦)
  • 专门写个编译时可以将模板编译成虚拟 DOM (在构建的时候进行编译性能更高,不需要再运行的时候进行编译,而且 vue3
    在编译中做了很多优化)

4.Vue3 设计思想

  • Vue3.0 注重模块上的拆分 Vue3 中的模块之间耦合度低,模块可以独立使用。 拆分模块
  • 通过构建工具 Tree-shaking 机制实现按需引入,减少用户打包后体积。 组合式 API
  • Vue3 允许自定义渲染器,扩展能力强。 扩展更方便
  • 使用 RFC 来确保改动和设计都是经过 Vuejs 核心团队探讨并得到确认的。也让用户可以了解每一个功能采用或废弃的前因后果。 采用RFC

Vue3整体架构

Monorepo 管理项目

Monorepo 是管理项目代码的一个方式,指在一个项目仓库(repo)中管理多个模块/包(package)。 Vue3 源码采用 monorepo 方式进行管理,将模块拆分到 package 目录中。作为一个个包来管理,这样职责划分更加明确。

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

1.Vue3 项目结构

在这里插入图片描述
在这里插入图片描述

2.Vue3 采用 Typescript

复杂的框架项目开发,使用类型语言非常有利于代码的维护,在编码期间就可以帮我们做类型检查,避免错误。所以 TS 已经是主流框架的标配~

Vue2 早期采用 Flow 来进行类型检测 (Vue2 中对 TS 支持并不友好), Vue3 源码采用 Typescript 来进行重写。同时 Vue2.7 也采用 TS 进行重写。TS 能对代码提供良好的类型检查,同时也支持复杂的类型推导。

搭建 Monorepo 环境

Vue3 中使用pnpm workspace来实现monorepo (pnpm是快速、节省磁盘空间的包管理器。主要采用符号链接的方式管理模块)

1.全局安装 pnpm

npm install pnpm -g # 全局安装pnpm

pnpm init  # 初始化配置文件

2.创建.npmrc 文件

shamefully-hoist = true

这里您可以尝试一下安装Vue3, pnpm install vue此时默认情况下vue3中依赖的模块不会被提升到node_modules下。 添加羞耻的提升可以将 Vue3,所依赖的模块提升到node_modules中

3.配置 workspace

新建 pnpm-workspace.yaml

packages:
  - "packages/*"

将 packages 下所有的目录都作为包进行管理。这样我们的 Monorepo 就搭建好了。确实比lerna + yarn workspace更快捷

4.环境搭建

打包项目 Vue3 采用 rollup 进行打包代码,安装打包所需要的依赖
在这里插入图片描述

pnpm install typescript minimist esbuild -D -w

5.初始化 TS

pnpm tsc --init

先添加些常用的ts-config配置,后续需要其他的在继续增加

{
   
  "compilerOptions": {
   
    "outDir": "dist", // 输出的目录
    "sourceMap": true, // 采用sourcemap
    "target": "es2016", // 目标语法
    "module": "esnext", // 模块格式
    "moduleResolution": "node", // 模块解析方式
    "strict": false, // 严格模式
    "resolveJsonModule": true, // 解析json模块
    "esModuleInterop": true, // 允许通过es6语法引入commonjs模块
    "jsx": "preserve", // jsx 不转义
    "lib": ["esnext", "dom"] // 支持的类库 esnext及dom
  }
}

6.创建模块

packages目录下新建两个 package

  • reactivity 响应式模块
  • shared 共享模块

所有包的入口均为src/index.ts 这样可以实现统一打包

  • reactivity/package.json
{
   
  "name": "@vue/reactivity",
  "version": "1.0.0",
  "main": "index.js",
  "module": "dist/reactivity.esm-bundler.js",
  "unpkg": "dist/reactivity.global.js",
  "buildOptions": {
   
    "name": "VueReactivity",
    "formats": ["esm-bundler", "cjs", "global"]
  }
}

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

formats 为自定义的打包格式

  • lobal 立即执行函数的格式,会暴露全局对象
  • esm-browser 在浏览器中使用的格式,内联所有的依赖项。
  • esm-bundler 在构建工具中使用的格式,不提供.prod 格式,在构建应用程序时会被构建工具一起进行打包压缩。
  • cjs 在 node 中使用的格式,服务端渲染。
pnpm install @vue/shared --workspace --filter @vue/reactivity

配置ts引用关系

"baseUrl": ".",
"paths": {
   
    "@vue/*": ["packages/*/src"]
}

7.开发环境esbuild打包

在这里插入图片描述
创建开发时执行脚本, 参数为要打包的模块
解析用户参数

"scripts": {
   
    "dev": "node scripts/dev.js reactivity -f esm"
}

import esbuild from "esbuild"; // 打包工具
import minimist from "minimist"; // 命令行参数解析
import {
    resolve, dirname } from "path";
import {
    fileURLToPath } from "url";
import {
    createRequire } from "module";
const require = createRequire(import.meta.url); // 可以在es6中使用require语法
const args = minimist(process.argv.slice(2)); // 解析打包格式和打包模块
const format = args.f || "iife";
const target = args._[0] || "reactivity";

// __dirname在es6模块中不存在需要自行解析
const __dirname = dirname(fileURLToPath(import.meta.url));

const pkg = require(`../packages/${
     target}/package.json`);

esbuild
  .context({
   
    entryPoints: [resolve(__dirname, `../packages/${
     target}/src/index.ts`)],
    outfile: resolve(
      // 输出的文件
      __dirname,
      `../packages/${
     target}/dist/${
     target}.js`
    ),
    bundle: true, // 全部打包
    sourcemap: true, // sourcemap源码映射
    format, // 打包格式 esm , cjs, iife
    globalName: pkg.buildOptions?.name, // 全局名配置
    platform: "browser", // 平台
  })
  .then((ctx) => {
   
    console.log("watching~~~");
    return ctx.watch(); // 监控文件变化
  });

Vue3 响应式数据核心

Vue3 中使用 Proxy 来实现响应式数据变化。

CompositionAPI
简单的组件仍然可以采用 OptionsAPI 进行编写(但是在 Vue3 中基本不在使用),compositionAPI 在复杂的逻辑中有着明显的优势~

  • CompositionAPI 在用户编写复杂业务逻辑不会出现反复横跳问题
  • CompositionAPI 不存在this指向不明确问题
  • Composition API 对 tree-shaking 更加友好,代码也更容易压缩。
  • CompositionAPI 提取公共逻辑非常方便

reactivity模块中就包含了很多我们经常使用到的API 例如:computed、reactive、ref、effect 等

Reactivity 模块基本使用

安装响应式模块

pnpm install @vue/reactivity -w

<div id="app"></div>
<script type="module">
  import {
   
    reactive,
    effect,
  } from "/node_modules/@vue/reactivity/dist/reactivity.esm-browser.js";
  const state = reactive({
    name: "jw", age: 30 });
  effect(() => {
   
    // 副作用函数 默认执行一次,响应式数据变化后再次执行
    app.innerHTML = state.name + "今年" + state.age + "岁了";
  });
  setTimeout(() => {
   
    state.age++;
  }, 1000);
</script>

reactive方法会将对象变成 proxy 对象, effect中使用reactive对象时会进行依赖收集,稍后属性变化时会重新执行effect函数~。

1.编写 reactive 函数

import {
    isObject } from "@vue/shared";
function createReactiveObject(target: object, isReadonly: boolean) {
   
  if (!isObject(target)) {
   
    return target;
  }
}
// 常用的就是reactive方法
export function reactive(target: object) {
   
  return createReactiveObject(target, false);
}
// 后面的方法,不是重点我们先不进行实现...
/*
export function shallowReactive(target: object) {
    return createReactiveObject(target, false)
}
export function readonly(target: object) {
    return createReactiveObject(target, true)
}
export function shallowReadonly(target: object) {
    return createReactiveObject(target, true)
}
*/

export function isObject(value: unknown) : value is Record<any,any> {
   
    return typeof value === 'object' && value !== null
}

由此可知这些方法接受的参数必须是一个对象类型。否则没有任何效果

const reactiveMap = new WeakMap(); // 缓存列表
const mutableHandlers: ProxyHandler<object> = {
   
  get(target, key, receiver) {
   
    // 等会谁来取值就做依赖收集
    const res = Reflect.get(target, key, receiver);
    return res;
  },
  set(target, key, value, receiver) {
   
    // 等会赋值的时候可以重新触发effect执行
    const result = Reflect.set(target, key, value, receiver);
    return result;
  },
};
function createReactiveObject(target: object, isReadonly: boolean) {
   
  if (!isObject(target)) {
   
    return target;
  }
  const exisitingProxy = reactiveMap.get(target); // 如果已经代理过则直接返回代理后的对象
  if (exisitingProxy) {
   
    return exisitingProxy;
  }
  const proxy = new Proxy(target, mutableHandlers); // 对对象进行代理
  reactiveMap.set(target, proxy);
  return proxy;
}

这里必须要使用 Reflect 进行操作,保证 this 指向永远指向代理对象

let person = {
   
  name: "jw",
  get aliasName() {
   
    return "**" + this.name + "**";
  },
};
let p = new Proxy(person, {
   
  get(target, key, receiver) {
   
    console.log(key);
    // return Reflect.get(target,key,receiver)
    return target[key];
  },
});
// 取aliasName时,我希望可以收集aliasName属性和name属性
p.aliasName;
// 这里的问题出自于 target[key] ,target指代的是原对象并不是代理对象

将对象使用 proxy 进行代理,如果对象已经被代理过,再次重复代理则返回上次代理结果。 那么,如果将一个代理对象传入呢?

const enum ReactiveFlags {
   
    IS_REACTIVE = '__v_isReactive'
}
const mutableHandlers: ProxyHandler<object> = {
   
    get(target, key, receiver) {
   
        if(key === ReactiveFlags.IS_REACTIVE){
    // 在get中增加标识,当获取IS_REACTIVE时返回true
            return true;
        }
    }
}
function createReactiveObject(target: object, isReadonly: boolean) {
   
    if(target[ReactiveFlags.IS_REACTIVE]){
    // 在创建响应式对象时先进行取值,看是否已经是响应式对象
        return target
    }
}

这样我们防止重复代理就做好了~~~
这里我们为了代码方便维护,我们将mutableHandlers抽离出去到baseHandlers.ts中。

2.编写 effect 函数

export let activeEffect = undefined; // 当前正在执行的effect

class ReactiveEffect {
   
  active = true;
  deps = []; // 收集effect中使用到的属性
  constructor(public fn, public scheduler) {
   }

  run() {
   
    if (
  • 21
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值