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 (