作者:京东物流 乔盼盼
一、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介绍
快速,节省磁盘空间的包管理器,主要采用符号链接的方式管理模块
1.快速
2.高效利用磁盘空间
pnpm 内部使用基于内容寻址的文件系统来存储磁盘上所有的文件,这个文件系统出色的地方在于:
•不会重复安装同一个包。用 npm/yarn 的时候,如果 100 个项目都依赖 lodash,那么 lodash 很可能就被安装了 100 次,磁盘中就有 100 个地方写入了这部分代码。但在使用 pnpm 只会安装一次,磁盘中只有一个地方写入,后面再次使用都会直接使用hardlink(硬链接)
•即使一个包的不同版本,pnpm 也会极大程度地复用之前版本的代码。比如 lodash 有 100 个文件,更新版本之后多了一个文件,那么磁盘当中并不会重新写入 101 个文件,而是保留原来的 100 个文件的hardlink,仅仅写入那一个新增的文件。
1.支持Monorepo
pnpm 与 npm/yarn 一个很大的不同就是支持了 monorepo
1.安全性高
之前在使用 npm/yarn 的时候,由于 node_module 的扁平结构,如果 A 依赖 B, B 依赖 C,那么 A 当中是可以直接使用 C 的,但问题是 A 当中并没有声明 C 这个依赖。因此会出现这种非法访问的情况。但 pnpm自创了一套依赖管理方式,很好地解决了这个问题,保证了安全性
默认情况下,pnpm 则是通过使用符号链接的方式仅将项目的直接依赖项添加到node_modules的根目录下。

安装和初始化
•全局安装(node版本>16)
npm install pnpm -g
•初始化
pnpm init
配置workspace
根目录创建pnpm-workspace.yaml
1packages: 2 - 'packages/*'
将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:完整版本,包括运行时和编译器
1 +---------------------+ 2 | | 3 | @vue/compiler-sfc | 4 | | 5 +-----+--------+------+ 6 | | 7 v v 8 +---------------------+ +----------------------+ 9 | | | | 10 +------------>| @vue/compiler-dom +--->| @vue/compiler-core | 11 | | | | | 12 +----+----+ +---------------------+ +----------------------+ 13 | | 14 | vue | 15 | | 16 +----+----+ +---------------------+ +----------------------+ +-------------------+ 17 | | | | | | | 18 +------------>| @vue/runtime-dom +--->| @vue/runtime-core +--->| @vue/reactivity | 19 | | | | | | 20 +---------------------+ +----------------------+ +-------------------+
scripts
Vue3在开发环境使用esbuild打包,生产环境采用rollup打包
包的相互依赖
安装
把packages/shared安装到packages/reactivity
pnpm install @vue/shared@workspace --filter @vue/reactivity

使用
在reactivity/src/computed.ts中引入shared中相关方法
1import { isFunction, NOOP } from '@vue/shared' // ts引入会报错 2 3const onlyGetter = isFunction(getterOrOptions) 4 if (onlyGetter) { 5 ... 6 } else { 7 ... 8 } 9...
tips:@vue/shared引入会报错,需要在tsconfig.json中配置
1{ 2 "compilerOptions": { 3 "baseUrl": ".", 4 "paths": { 5 "@vue/compat": ["packages/vue-compat/src"], 6 "@vue/*": ["packages/*/src"], 7 "vue": ["packages/vue/src"] 8 } 9 }, 10}
5、打包
所有包的入口均为src/index.ts这样可以实现统一打包.
•reactivity/package.json
1{ 2 "name": "@vue/reactivity", 3 "version": "3.2.45", 4 "main": "index.js", 5 "module":"dist/reactivity.esm-bundler.js", 6 "unpkg": "dist/reactivity.global.js", 7 "buildOptions": { 8 "name": "VueReactivity", 9 "formats": [ 10 "esm-bundler", 11 "cjs", 12 "global" 13 ] 14 } 15}
•shared/package.json
1{ 2 "name": "@vue/shared", 3 "version": "3.2.45", 4 "main": "index.js", 5 "module": "dist/shared.esm-bundler.js", 6 "buildOptions": { 7 "formats": [ 8 "esm-bundler", 9 "cjs" 10 ] 11 } 12}
formats为自定义的打包格式,有esm-bundler在构建工具中使用的格式、esm-browser在浏览器中使用的格式、cjs在node中使用的格式、global立即执行函数的格式
开发环境esbuild打包

开发时 执行脚本, 参数为要打包的模块
<!---->1"scripts": { 2 "dev": "node scripts/dev.js reactivity -f global" 3}
1// Using esbuild for faster dev builds. 2// We are still using Rollup for production builds because it generates 3// smaller files w/ better tree-shaking. 4 5// @ts-check 6const { build } = require('esbuild') 7const nodePolyfills = require('@esbuild-plugins/node-modules-polyfill') 8const { resolve, relative } = require('path') 9const args = require('minimist')(process.argv.slice(2)) 10 11const target = args._[0] || 'vue' 12const format = args.f || 'global' 13const inlineDeps = args.i || args.inline 14const pkg = require(resolve(__dirname, `../packages/${target}/package.json`)) 15 16// resolve output 17const outputFormat = format.startsWith('global') 18 ? 'iife' 19 : format === 'cjs' 20 ? 'cjs' 21 : 'esm' 22 23const postfix = format.endsWith('-runtime') 24 ? `runtime.${format.replace(/-runtime$/, '')}` 25 : format 26 27const outfile = resolve( 28 __dirname, 29 `../packages/${target}/dist/${ 30 target === 'vue-compat' ? `vue` : target 31 }.${postfix}.js` 32) 33const relativeOutfile = relative(process.cwd(), outfile) 34 35// resolve externals 36// TODO this logic is largely duplicated from rollup.config.js 37let external = [] 38if (!inlineDeps) { 39 // cjs & esm-bundler: external all deps 40 if (format === 'cjs' || format.includes('esm-bundler')) { 41 external = [ 42 ...external, 43 ...Object.keys(pkg.dependencies || {}), 44 ...Object.keys(pkg.peerDependencies || {}), 45 // for @vue/compiler-sfc / server-renderer 46 'path', 47 'url', 48 'stream' 49 ] 50 } 51 52 if (target === 'compiler-sfc') { 53 const consolidateDeps = require.resolve('@vue/consolidate/package.json', { 54 paths: [resolve(__dirname, `../packages/${target}/`)] 55 }) 56 external = [ 57 ...external, 58 ...Object.keys(require(consolidateDeps).devDependencies), 59 'fs', 60 'vm', 61 'crypto', 62 'react-dom/server', 63 'teacup/lib/express', 64 'arc-templates/dist/es5', 65 'then-pug', 66 'then-jade' 67 ] 68 } 69} 70 71build({ 72 entryPoints: [resolve(__dirname, `../packages/${target}/src/index.ts`)], 73 outfile, 74 bundle: true, 75 external, 76 sourcemap: true, 77 format: outputFormat, 78 globalName: pkg.buildOptions?.name, 79 platform: format === 'cjs' ? 'node' : 'browser', 80 plugins: 81 format === 'cjs' || pkg.buildOptions?.enableNonBrowserBranches 82 ? [nodePolyfills.default()] 83 : undefined, 84 define: { 85 __COMMIT__: `"dev"`, 86 __VERSION__: `"${pkg.version}"`, 87 __DEV__: `true`, 88 __TEST__: `false`, 89 __BROWSER__: String( 90 format !== 'cjs' && !pkg.buildOptions?.enableNonBrowserBranches 91 ), 92 __GLOBAL__: String(format === 'global'), 93 __ESM_BUNDLER__: String(format.includes('esm-bundler')), 94 __ESM_BROWSER__: String(format.includes('esm-browser')), 95 __NODE_JS__: String(format === 'cjs'), 96 __SSR__: String(format === 'cjs' || format.includes('esm-bundler')), 97 __COMPAT__: String(target === 'vue-compat'), 98 __FEATURE_SUSPENSE__: `true`, 99 __FEATURE_OPTIONS_API__: `true`, 100 __FEATURE_PROD_DEVTOOLS__: `false` 101 }, 102 watch: { 103 onRebuild(error) { 104 if (!error) console.log(`rebuilt: ${relativeOutfile}`) 105 } 106 } 107}).then(() => { 108 console.log(`watching: ${relativeOutfile}`) 109})
生产环境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、基本使用
1const { effect, reactive } = VueReactivity 2// console.log(effect, reactive); 3const state = reactive({name: 'qpp', age:18, address: {city: '南京'}}) 4console.log(state.address); 5effect(()=>{ 6 console.log(state.name) 7})
4、reactive实现
1import { mutableHandlers } from'./baseHandlers'; 2// 代理相关逻辑import{ isObject }from'./util';// 工具方法 3export function reactive(target: object) { 4 // if trying to observe a readonly proxy, return the readonly version. 5 if (isReadonly(target)) { 6 return target 7 } 8 return createReactiveObject( 9 target, 10 false, 11 mutableHandlers, 12 mutableCollectionHandlers, 13 reactiveMap 14 ) 15} 16function createReactiveObject(target, baseHandler){ 17 if(!isObject(target)){ 18 return target; 19 } 20 ... 21 const observed =new Proxy(target, baseHandler); 22 return observed 23}
baseHandlers
1import { isObject, hasOwn, hasChanged } from"@vue/shared"; 2import { reactive } from"./reactive"; 3const get = createGetter(); 4const set = createSetter(); 5function createGetter(){ 6 return function get(target, key, receiver){ 7 // 对获取的值进行放射 8 const res = Reflect.get(target, key, receiver); 9 console.log('属性获取',key) 10 if(isObject(res)){// 如果获取的值是对象类型,则返回当前对象的代理对象 11 return reactive(res); 12 } 13 return res; 14 } 15} 16function createSetter(){ 17 return function set(target, key, value, receiver){ 18 const oldValue = target[key]; 19 const hadKey =hasOwn(target, key); 20 const result = Reflect.set(target, key, value, receiver); 21 if(!hadKey){ 22 console.log('属性新增',key,value) 23 }else if(hasChanged(value, oldValue)){ 24 console.log('属性值被修改',key,value) 25 } 26 return result; 27 } 28} 29export const mutableHandlers ={ 30 get,// 当获取属性时调用此方法 31 set// 当修改属性时调用此方法 32}
这里我只选了对最常用到的get和set方法的代码,还应该有
has、deleteProperty、ownKeys。这里为了快速掌握核心流程就先暂且跳过这些代码
5、effect实现
我们再来看effect的代码,默认effect会立即执行,当依赖的值发生变化时effect会重新执行
1export let activeEffect = undefined; 2// 依赖收集的原理是 借助js是单线程的特点, 默认调用effect的时候会去调用proxy的get,此时让属性记住 3// 依赖的effect,同理也让effect记住对应的属性 4// 靠的是数据结构 weakMap : {map:{key:new Set()}} 5// 稍后数据变化的时候 找到对应的map 通过属性出发set中effect 6function cleanEffect(effect) { 7 // 需要清理effect中存入属性中的set中的effect 8 // 每次执行前都需要将effect只对应属性的set集合都清理掉 9 // 属性中的set 依然存放effect 10 let deps = effect.deps 11 for (let i = 0; i < deps.length; i++) { 12 deps[i].delete(effect) 13 } 14 effect.deps.length = 0; 15 16} 17 18// 创建effect时可以传递参数,computed也是基于effect来实现的,只是增加了一些参数条件而已 19export function effect<T = any>( 20 fn: () => T, 21 options?: ReactiveEffectOptions 22){ 23 // 将用户传递的函数编程响应式的effect 24 const _effect = new ReactiveEffect(fn,options.scheduler); 25 // 更改runner中的this 26 _effect.run() 27 const runner = _effect.run.bind(_effect); 28 runner.effect = _effect; // 暴露effect的实例 29 return runner// 用户可以手动调用runner重新执行 30} 31export class ReactiveEffect { 32 public active = true; 33 public parent = null; 34 public deps = []; // effect中用了哪些属性,后续清理的时候要使用 35 constructor(public fn,public scheduler?) { } // 你传递的fn我会帮你放到this上 36 // effectScope 可以来实现让所有的effect停止 37 run() { 38 // 依赖收集 让熟悉和effect 产生关联 39 if (!this.active) { 40 return this.fn(); 41 } else { 42 try { 43 this.parent = activeEffect 44 activeEffect = this; 45 cleanEffect(this); // vue2 和 vue3中都是要清理的 46 return this.fn(); // 去proxy对象上取值, 取之的时候 我要让这个熟悉 和当前的effect函数关联起来,稍后数据变化了 ,可以重新执行effect函数 47 } finally { 48 // 取消当前正在运行的effect 49 activeEffect = this.parent; 50 this.parent = null; 51 } 52 } 53 } 54 stop() { 55 if (this.active) { 56 this.active = false; 57 cleanEffect(this); 58 } 59 } 60}
在effect方法调用时会对属性进行取值,此时可以进行依赖收集。
1effect(()=>{ 2 console.log(state.name) 3 // 执行用户传入的fn函数,会取到state.name,state.age... 会触发reactive中的getter 4 app.innerHTML = 'name:' + state.name + 'age:' + state.age + 'address' + state.address.city 5 6})
6、依赖收集
核心代码
1// 收集属性对应的effect 2export function track(target, type, key){}// 触发属性对应effect执行 3export function trigger(target, type, key){}
<!---->1function createGetter(){ 2 return function get(target, key, receiver){ 3 const res = Reflect.get(target, key, receiver); 4 // 取值时依赖收集 5 track(target, TrackOpTypes.GET, key); 6 if(isObject(res)){ 7 return reactive(res); 8 } 9 return res; 10 } 11}
1function createSetter(){ 2 return function set(target, key, value, receiver){ 3 const oldValue = target[key]; 4 const hadKey =hasOwn(target, key); 5 const result = Reflect.set(target, key, value, receiver); 6 if(!hadKey){ 7 // 设置值时触发更新 - ADD 8 trigger(target, TriggerOpTypes.ADD, key); 9 }else if(hasChanged(value, oldValue)){ 10 // 设置值时触发更新 - SET 11 trigger(target, TriggerOpTypes.SET, key, value, oldValue); 12 } 13 return result; 14 } 15}
track的实现
1const targetMap = new WeakMap(); 2export function track(target: object, type: TrackOpTypes, key: unknown){ 3 if (shouldTrack && activeEffect) { // 上下文 shouldTrack = true 4 let depsMap = targetMap.get(target); 5 if(!depsMap){// 如果没有map,增加map 6 targetMap.set(target,(depsMap =newMap())); 7 } 8 let dep = depsMap.get(key);// 取对应属性的依赖表 9 if(!dep){// 如果没有则构建set 10 depsMap.set(key,(dep =newSet())); 11 } 12 13 trackEffects(dep, eventInfo) 14 } 15} 16 17export function trackEffects( 18 dep: Dep, 19 debuggerEventExtraInfo?: DebuggerEventExtraInfo 20) { 21 //let shouldTrack = false 22 //if (effectTrackDepth <= maxMarkerBits) { 23 // if (!newTracked(dep)) { 24 // dep.n |= trackOpBit // set newly tracked 25 // shouldTrack = !wasTracked(dep) 26 //} 27 //} else { 28 // Full cleanup mode. 29 // shouldTrack = !dep.has(activeEffect!) 30 } 31 32 if (!dep.has(activeEffect!) { 33 dep.add(activeEffect!) 34 activeEffect!.deps.push(dep) 35 //if (__DEV__ && activeEffect!.onTrack) { 36 // activeEffect!.onTrack({ 37 // effect: activeEffect!, 38 // ...debuggerEventExtraInfo! 39 // }) 40 // } 41 } 42}
trigger实现
1export function trigger(target, type, key){ 2 const depsMap = targetMap.get(target); 3 if(!depsMap){ 4 return; 5 } 6 const run=(effects)=>{ 7 if(effects){ effects.forEach(effect=>effect()); } 8 } 9 // 有key 就找到对应的key的依赖执行 10 if(key !==void0){ 11 run(depsMap.get(key)); 12 } 13 // 数组新增属性 14 if(type == TriggerOpTypes.ADD){ 15 run(depsMap.get(isArray(target)?'length':''); 16 }}
依赖关系

