一、Vue3 结构分析
1、Vue2 与 Vue3 的对比
对 TypeScript 支持不友好(所有属性都放在了 this 对象上,难以推倒组件的数据类型)
大量的 API 挂载在 Vue 对象的原型上,难以实现 TreeShaking。
架构层面对跨平台 dom 渲染开发支持不友好,vue3 允许自定义渲染器,扩展能力强。
CompositionAPI。受 ReactHook 启发
对虚拟 DOM 进行了重写、对模板的编译进行了优化操作...
2、Vue3 设计思想
Vue3.0 更注重模块上的拆分,在 2.0 中无法单独使用部分模块。需要引入完整的 Vuejs(例如只想使用使用响应式部分,但是需要引入完整的 Vuejs), Vue3 中的模块之间耦合度低,模块可以独立使用。拆分模块
Vue2 中很多方法挂载到了实例中导致没有使用也会被打包(还有很多组件也是一样)。通过构建工具 Tree-shaking 机制实现按需引入,减少用户打包后体积。重写 API
Vue3 允许自定义渲染器,扩展能力强。不会发生以前的事情,改写 Vue 源码改造渲染方式。扩展更方便
依然保留了 Vue2 的特点:
依旧是声明式框架,底层渲染逻辑不关心(命令式比较关注过程,可以控制怎么写最优?编写过程不同),如 for 和 reduce
采用虚拟 DOM
区分编译时和运行时
内部区分了编译时(模板?编程成 js 代码,一般在构建工具中使用)和运行时
简单来说,Vue3 框架更小,扩展更加方便
3、monorepo 管理项目
Monorepo 是管理项目代码的一个方式,指在一个项目仓库(repo)中管理多个模块/包(package)。也就是说是一种将多个 package 放在一个 repo 中的代码管理模式。Vue3 内部实现了一个模块的拆分, Vue3 源码采用 Monorepo 方式进行管理,将模块拆分到 package 目录中。
早期使用yarn workspace + lerna
来管理项目,后面是pnpm
pnpm 介绍
快速,节省磁盘空间的包管理器,主要采用符号链接的方式管理模块
快速
高效利用磁盘空间
pnpm 内部使用基于内容寻址
的文件系统来存储磁盘上所有的文件,这个文件系统出色的地方在于:
不会重复安装同一个包。用 npm/yarn 的时候,如果 100 个项目都依赖 lodash,那么 lodash 很可能就被安装了 100 次,磁盘中就有 100 个地方写入了这部分代码。但在使用 pnpm 只会安装一次,磁盘中只有一个地方写入,后面再次使用都会直接使用hardlink
(硬链接)
即使一个包的不同版本,pnpm 也会极大程度地复用之前版本的代码。比如 lodash 有 100 个文件,更新版本之后多了一个文件,那么磁盘当中并不会重新写入 101 个文件,而是保留原来的 100 个文件的hardlink
,仅仅写入那一个新增的文件
。
支持 Monorepo
pnpm 与 npm/yarn 一个很大的不同就是支持了 monorepo
安全性高
之前在使用 npm/yarn 的时候,由于 node_module 的扁平结构,如果 A 依赖 B, B 依赖 C,那么 A 当中是可以直接使用 C 的,但问题是 A 当中并没有声明 C 这个依赖。因此会出现这种非法访问的情况。但 pnpm 自创了一套依赖管理方式,很好地解决了这个问题,保证了安全性
默认情况下,pnpm 则是通过使用符号链接的方式仅将项目的直接依赖项添加到node_modules
的根目录下。
安装和初始化
配置 workspace
根目录创建 pnpm-workspace.yaml
将 packages 下所有的目录都作为包进行管理。这样我们的 Monorepo 就搭建好了。确实比 lerna + yarn workspace
更快捷
4、项目结构
packages
reactivity:响应式系统
runtime-core:与平台无关的运行时核心 (可以创建针对特定平台的运行时 - 自定义渲染器)
runtime-dom: 针对浏览器的运行时。包括 DOM API,属性,事件处理等
runtime-test:用于测试
server-renderer:用于服务器端渲染
compiler-core:与平台无关的编译器核心
compiler-dom: 针对浏览器的编译模块
compiler-ssr: 针对服务端渲染的编译模块
template-explorer:用于调试编译器输出的开发工具
shared:多个包之间共享的内容
vue:完整版本,包括运行时和编译器
+---------------------+
| |
| @vue/compiler-sfc |
| |
+-----+--------+------+
| |
v v
+---------------------+ +----------------------+
| | | |
+------------>| @vue/compiler-dom +--->| @vue/compiler-core |
| | | | |
+----+----+ +---------------------+ +----------------------+
| |
| vue |
| |
+----+----+ +---------------------+ +----------------------+ +-------------------+
| | | | | | |
+------------>| @vue/runtime-dom +--->| @vue/runtime-core +--->| @vue/reactivity |
| | | | | |
+---------------------+ +----------------------+ +-------------------+
复制代码
scripts
Vue3 在开发环境使用 esbuild 打包,生产环境采用 rollup 打包
包的相互依赖
安装
把 packages/shared 安装到 packages/reactivity
pnpm install @vue/shared@workspace --filter @vue/reactivity
复制代码
使用
在 reactivity/src/computed.ts 中引入 shared 中相关方法
import { isFunction, NOOP } from '@vue/shared' // ts引入会报错
const onlyGetter = isFunction(getterOrOptions)
if (onlyGetter) {
...
} else {
...
}
...
复制代码
tips:@vue/shared 引入会报错,需要在 tsconfig.json 中配置
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@vue/compat": ["packages/vue-compat/src"],
"@vue/*": ["packages/*/src"],
"vue": ["packages/vue/src"]
}
},
}
复制代码
5、打包
所有包的入口均为src/index.ts
这样可以实现统一打包.
• reactivity/package.json
{
"name": "@vue/reactivity",
"version": "3.2.45",
"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": "3.2.45",
"main": "index.js",
"module": "dist/shared.esm-bundler.js",
"buildOptions": {
"formats": [
"esm-bundler",
"cjs"
]
}
}
复制代码
formats
为自定义的打包格式,有 esm-bundler
在构建工具中使用的格式、 esm-browser
在浏览器中使用的格式、 cjs
在 node 中使用的格式、 global
立即执行函数的格式
开发环境esbuild
打包
开发时 执行脚本, 参数为要打包的模块
"scripts": {
"dev": "node scripts/dev.js reactivity -f global"
}
复制代码
// Using esbuild for faster dev builds.
// We are still using Rollup for production builds because it generates
// smaller files w/ better tree-shaking.
// @ts-check
const { build } = require('esbuild')
const nodePolyfills = require('@esbuild-plugins/node-modules-polyfill')
const { resolve, relative } = require('path')
const args = require('minimist')(process.argv.slice(2))
const target = args._[0] || 'vue'
const format = args.f || 'global'
const inlineDeps = args.i || args.inline
const pkg = require(resolve(__dirname, `../packages/${target}/package.json`))
// resolve output
const outputFormat = format.startsWith('global')
? 'iife'
: format === 'cjs'
? 'cjs'
: 'esm'
const postfix = format.endsWith('-runtime')
? `runtime.${format.replace(/-runtime$/, '')}`
: format
const outfile = resolve(
__dirname,
`../packages/${target}/dist/${
target === 'vue-compat' ? `vue` : target
}.${postfix}.js`
)
const relativeOutfile = relative(process.cwd(), outfile)
// resolve externals
// TODO this logic is largely duplicated from rollup.config.js
let external = []
if (!inlineDeps) {
// cjs & esm-bundler: external all deps
if (format === 'cjs' || format.includes('esm-bundler')) {
external = [
...external,
...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.peerDependencies || {}),
// for @vue/compiler-sfc / server-renderer
'path',
'url',
'stream'
]
}
if (target === 'compiler-sfc') {
const consolidateDeps = require.resolve('@vue/consolidate/package.json', {
paths: [resolve(__dirname, `../packages/${target}/`)]
})
external = [
...external,
...Object.keys(require(consolidateDeps).devDependencies),
'fs',
'vm',
'crypto',
'react-dom/server',
'teacup/lib/express',
'arc-templates/dist/es5',
'then-pug',
'then-jade'
]
}
}
build({
entryPoints: [resolve(__dirname, `../packages/${target}/src/index.ts`)],
outfile,
bundle: true,
external,
sourcemap: true,
format: outputFormat,
globalName: pkg.buildOptions?.name,
platform: format === 'cjs' ? 'node' : 'browser',
plugins:
format === 'cjs' || pkg.buildOptions?.enableNonBrowserBranches
? [nodePolyfills.default()]
: undefined,
define: {
__COMMIT__: `"dev"`,
__VERSION__: `"${pkg.version}"`,
__DEV__: `true`,
__TEST__: `false`,
__BROWSER__: String(
format !== 'cjs' && !pkg.buildOptions?.enableNonBrowserBranches
),
__GLOBAL__: String(format === 'global'),
__ESM_BUNDLER__: String(format.includes('esm-bundler')),
__ESM_BROWSER__: String(format.includes('esm-browser')),
__NODE_JS__: String(format === 'cjs'),
__SSR__: String(format === 'cjs' || format.includes('esm-bundler')),
__COMPAT__: String(target === 'vue-compat'),
__FEATURE_SUSPENSE__: `true`,
__FEATURE_OPTIONS_API__: `true`,
__FEATURE_PROD_DEVTOOLS__: `false`
},
watch: {
onRebuild(error) {
if (!error) console.log(`rebuilt: ${relativeOutfile}`)
}
}
}).then(() => {
console.log(`watching: ${relativeOutfile}`)
})
复制代码
生产环境rollup
打包
具体代码参考 rollup.config.mjs
build.js
二、Vue3 中 Reactivity 模块
1、vue3 对比 vue2 的响应式变化
在 Vue2 的时候使用 defineProperty 来进行数据的劫持, 需要对属性进行重写添加getter
及setter
性能差。
当新增属性和删除属性时无法监控变化。需要通过$set
、$delete
实现
数组不采用 defineProperty 来进行劫持 (浪费性能,对所有索引进行劫持会造成性能浪费)需要对数组单独进行处理
Vue3 中使用 Proxy 来实现响应式数据变化。从而解决了上述问题
2、CompositionAPI
在 Vue2 中采用的是 OptionsAPI, 用户提供的 data,props,methods,computed,watch 等属性 (用户编写复杂业务逻辑会出现反复横跳问题)
Vue2 中所有的属性都是通过this
访问,this
存在指向明确问题
Vue2 中很多未使用方法或属性依旧会被打包,并且所有全局 API 都在 Vue 对象上公开。Composition API 对 tree-shaking 更加友好,代码也更容易压缩。
组件逻辑共享问题, Vue2 采用 mixins 实现组件之间的逻辑共享; 但是会有数据来源不明确,命名冲突等问题。 Vue3 采用 CompositionAPI 提取公共逻辑非常方便
简单的组件仍然可以采用 OptionsAPI 进行编写,compositionAPI 在复杂的逻辑中有着明显的优势~。 reactivity
模块中就包含了很多我们经常使用到的 API
例如:computed、reactive、ref、effect 等
3、基本使用
const { effect, reactive } = VueReactivity
// console.log(effect, reactive);
const state = reactive({name: 'qpp', age:18, address: {city: '南京'}})
console.log(state.address);
effect(()=>{
console.log(state.name)
})
复制代码
4、reactive 实现
import { mutableHandlers } from'./baseHandlers';
// 代理相关逻辑import{ isObject }from'./util';// 工具方法
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
if (isReadonly(target)) {
return target
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
)
}
function createReactiveObject(target, baseHandler){
if(!isObject(target)){
return target;
}
...
const observed =new Proxy(target, baseHandler);
return observed
}
复制代码
baseHandlers
import { isObject, hasOwn, hasChanged } from"@vue/shared";
import { reactive } from"./reactive";
const get = createGetter();
const set = createSetter();
function createGetter(){
return function get(target, key, receiver){
// 对获取的值进行放射
const res = Reflect.get(target, key, receiver);
console.log('属性获取',key)
if(isObject(res)){// 如果获取的值是对象类型,则返回当前对象的代理对象
return reactive(res);
}
return res;
}
}
function createSetter(){
return function set(target, key, value, receiver){
const oldValue = target[key];
const hadKey =hasOwn(target, key);
const result = Reflect.set(target, key, value, receiver);
if(!hadKey){
console.log('属性新增',key,value)
}else if(hasChanged(value, oldValue)){
console.log('属性值被修改',key,value)
}
return result;
}
}
export const mutableHandlers ={
get,// 当获取属性时调用此方法
set// 当修改属性时调用此方法
}
复制代码
这里我只选了对最常用到的 get 和 set 方法的代码,还应该有 has
、 deleteProperty
、 ownKeys
。这里为了快速掌握核心流程就先暂且跳过这些代码
5、effect 实现
我们再来看 effect 的代码,默认 effect 会立即执行,当依赖的值发生变化时 effect 会重新执行
export let activeEffect = undefined;
// 依赖收集的原理是 借助js是单线程的特点, 默认调用effect的时候会去调用proxy的get,此时让属性记住
// 依赖的effect,同理也让effect记住对应的属性
// 靠的是数据结构 weakMap : {map:{key:new Set()}}
// 稍后数据变化的时候 找到对应的map 通过属性出发set中effect
function cleanEffect(effect) {
// 需要清理effect中存入属性中的set中的effect
// 每次执行前都需要将effect只对应属性的set集合都清理掉
// 属性中的set 依然存放effect
let deps = effect.deps
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
effect.deps.length = 0;
}
// 创建effect时可以传递参数,computed也是基于effect来实现的,只是增加了一些参数条件而已
export function effect<T = any>(
fn: () => T,
options?: ReactiveEffectOptions
){
// 将用户传递的函数编程响应式的effect
const _effect = new ReactiveEffect(fn,options.scheduler);
// 更改runner中的this
_effect.run()
const runner = _effect.run.bind(_effect);
runner.effect = _effect; // 暴露effect的实例
return runner// 用户可以手动调用runner重新执行
}
export class ReactiveEffect {
public active = true;
public parent = null;
public deps = []; // effect中用了哪些属性,后续清理的时候要使用
constructor(public fn,public scheduler?) { } // 你传递的fn我会帮你放到this上
// effectScope 可以来实现让所有的effect停止
run() {
// 依赖收集 让熟悉和effect 产生关联
if (!this.active) {
return this.fn();
} else {
try {
this.parent = activeEffect
activeEffect = this;
cleanEffect(this); // vue2 和 vue3中都是要清理的
return this.fn(); // 去proxy对象上取值, 取之的时候 我要让这个熟悉 和当前的effect函数关联起来,稍后数据变化了 ,可以重新执行effect函数
} finally {
// 取消当前正在运行的effect
activeEffect = this.parent;
this.parent = null;
}
}
}
stop() {
if (this.active) {
this.active = false;
cleanEffect(this);
}
}
}
复制代码
在 effect 方法调用时会对属性进行取值,此时可以进行依赖收集。
effect(()=>{
console.log(state.name)
// 执行用户传入的fn函数,会取到state.name,state.age... 会触发reactive中的getter
app.innerHTML = 'name:' + state.name + 'age:' + state.age + 'address' + state.address.city
})
复制代码
6、依赖收集
核心代码
// 收集属性对应的effect
export function track(target, type, key){}// 触发属性对应effect执行
export function trigger(target, type, key){}
复制代码
function createGetter(){
return function get(target, key, receiver){
const res = Reflect.get(target, key, receiver);
// 取值时依赖收集
track(target, TrackOpTypes.GET, key);
if(isObject(res)){
return reactive(res);
}
return res;
}
}
复制代码
function createSetter(){
return function set(target, key, value, receiver){
const oldValue = target[key];
const hadKey =hasOwn(target, key);
const result = Reflect.set(target, key, value, receiver);
if(!hadKey){
// 设置值时触发更新 - ADD
trigger(target, TriggerOpTypes.ADD, key);
}else if(hasChanged(value, oldValue)){
// 设置值时触发更新 - SET
trigger(target, TriggerOpTypes.SET, key, value, oldValue);
}
return result;
}
}
复制代码
track 的实现
const targetMap = new WeakMap();
export function track(target: object, type: TrackOpTypes, key: unknown){
if (shouldTrack && activeEffect) { // 上下文 shouldTrack = true
let depsMap = targetMap.get(target);
if(!depsMap){// 如果没有map,增加map
targetMap.set(target,(depsMap =newMap()));
}
let dep = depsMap.get(key);// 取对应属性的依赖表
if(!dep){// 如果没有则构建set
depsMap.set(key,(dep =newSet()));
}
trackEffects(dep, eventInfo)
}
}
export function trackEffects(
dep: Dep,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
//let shouldTrack = false
//if (effectTrackDepth <= maxMarkerBits) {
// if (!newTracked(dep)) {
// dep.n |= trackOpBit // set newly tracked
// shouldTrack = !wasTracked(dep)
//}
//} else {
// Full cleanup mode.
// shouldTrack = !dep.has(activeEffect!)
}
if (!dep.has(activeEffect!) {
dep.add(activeEffect!)
activeEffect!.deps.push(dep)
//if (__DEV__ && activeEffect!.onTrack) {
// activeEffect!.onTrack({
// effect: activeEffect!,
// ...debuggerEventExtraInfo!
// })
// }
}
}
复制代码
trigger 实现
export function trigger(target, type, key){
const depsMap = targetMap.get(target);
if(!depsMap){
return;
}
const run=(effects)=>{
if(effects){ effects.forEach(effect=>effect()); }
}
// 有key 就找到对应的key的依赖执行
if(key !==void0){
run(depsMap.get(key));
}
// 数组新增属性
if(type == TriggerOpTypes.ADD){
run(depsMap.get(isArray(target)?'length':'');
}}
复制代码
依赖关系
作者:京东物流 乔盼盼
来源:京东云开发者社区 自猿其说 Tech 转载请注明来源
评论