1. Vue3 structural analysis
1. Comparison between Vue2 and Vue3
- Unfriendly to TypeScript support (all properties are placed on this object, making it difficult to override the data type of the component)
- A large number of APIs are mounted on the prototype of the Vue object, making it difficult to implement TreeShaking.
- The architectural level is not friendly to cross-platform DOM rendering development support. Vue3 allows custom renderers and has strong scalability.
- CompositionAPI. Inspired by ReactHook
- The virtual DOM has been rewritten and the compilation of templates has been optimized...
2. Vue3 design ideas
- Vue3.0 pays more attention to the splitting of modules. In 2.0, some modules cannot be used independently. It is necessary to introduce complete Vuejs (for example, you only want to use the responsive part, but you need to introduce complete Vuejs). The coupling between modules in Vue3 is low, and the modules can be used independently. split module
- Many methods in Vue2 are mounted into instances and will be packaged if not used (the same goes for many components). Through the tree-shaking mechanism of the construction tool, on-demand introduction is implemented to reduce the volume of user packaging. Rewrite API
- Vue3 allows custom renderers and has strong scalability. What happened before will not happen, the Vue source code will be rewritten and the rendering method will be transformed. Expansion is more convenient
Still retains the features of Vue2:
It is still a declarative framework and does not care about the underlying rendering logic (the imperative style pays more attention to the process and can control how to write optimally? The writing process is different), such as for and reduce
Using virtual DOM
Distinguish between compile time and run time
Internally, there is a distinction between compile time (template? programmed into js code, generally used in build tools) and runtime
Simply put, the Vue3 framework is smaller and more convenient to expand.
3. monorepo management project
Monorepo is a way to manage project code, which refers to managing multiple modules/packages in a project repository (repo). In other words, it is a code management model that puts multiple packages in one repo. Vue3 implements the splitting of a module internally. The Vue3 source code is managed using the Monorepo method and the modules are split into the package directory.
- One warehouse can maintain multiple modules, so there is no need to search for warehouses everywhere.
- Convenient for version management and dependency management, references and calls between modules are very convenient
- Each package can be published independently
Used early to manage projects, lateryarn workspace + lerna
pnpm
Introduction to pnpm
A fast , disk space-saving package manager that mainly uses symbolic links to manage modules.
The file system used internally by pnpm to store all files on the disk. The great thing about this file system is that:基于内容寻址
- The same package will not be installed repeatedly. When using npm/yarn, if 100 projects all depend on lodash, then lodash is likely to be installed 100 times, and this part of the code is written in 100 places on the disk. But when using pnpm, it will only be installed once. There is only one place on the disk to write, and it will be used directly (hard link) when used again.
hardlink
- Even if there are different versions of a package, pnpm will reuse code from previous versions to a great extent. For example, lodash has 100 files, and after the updated version, one more file is added, then the 101 files will not be rewritten to the disk, but the original 100 files will be retained, and only those will be written .
hardlink
一个新增的文件
A big difference between pnpm and npm/yarn is that it supports monorepo
When using npm/yarn before, due to the flat structure of node_module, if A depends on B, and B depends on C, then A can directly use C, but the problem is that A does not declare the dependency of C. Therefore, this kind of illegal access occurs. But pnpm has created its own dependency management method, which solves this problem well and ensures security.
By default, pnpm only adds the project's direct dependencies to the root directory by using symbolic links.node_modules
Installation and initialization
- Global installation (node version >16)
npm install pnpm -g
- initialization
pnpm init
Configure workspace
Create pnpm-workspace.yaml in the root directory
packages:
- 'packages/*'
All directories under packages are managed as packages. In this way, our Monorepo is set up. Indeed
faster than
lerna + yarn workspace
4. Project structure
packages
- reactivity: responsive system
- runtime-core: Platform-independent runtime core (can create platform-specific runtimes - custom renderers)
- runtime-dom: Runtime for browsers. Including DOM API, properties, event handling, etc.
- runtime-test: used for testing
- server-renderer: used for server-side rendering
- compiler-core: Platform-independent compiler core
- compiler-dom: Compilation module for browsers
- compiler-ssr: Compilation module for server-side rendering
- template-explorer: Development tool for debugging compiler output
- shared: content shared between multiple packages
- vue: full version, including runtime and compiler
+---------------------+
| |
| @vue/compiler-sfc |
| |
+-----+--------+------+
| |
v v
+---------------------+ +----------------------+
| | | |
+------------>| @vue/compiler-dom +--->| @vue/compiler-core |
| | | | |
+----+----+ +---------------------+ +----------------------+
| |
| vue |
| |
+----+----+ +---------------------+ +----------------------+ +-------------------+
| | | | | | |
+------------>| @vue/runtime-dom +--->| @vue/runtime-core +--->| @vue/reactivity |
| | | | | |
+---------------------+ +----------------------+ +-------------------+
scripts
Vue3 is packaged using esbuild in the development environment and rollup in the production environment.
Package interdependencies
Install
Install packages/shared to packages/reactivity
pnpm install @vue/shared@workspace --filter @vue/reactivity
use
Introduce related methods in shared in reactivity/src/computed.ts
import { isFunction, NOOP } from '@vue/shared' // ts引入会报错
const onlyGetter = isFunction(getterOrOptions)
if (onlyGetter) {
...
} else {
...
}
...
Tips: The introduction of @vue/shared will report an error and needs to be configured in tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@vue/compat": ["packages/vue-compat/src"],
"@vue/*": ["packages/*/src"],
"vue": ["packages/vue/src"]
}
},
}
5. Packing
The entrances of all packages are so that unified packaging can be achieved.src/index.ts
{
"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"
]
}
}
{
"name": "@vue/shared",
"version": "3.2.45",
"main": "index.js",
"module": "dist/shared.esm-bundler.js",
"buildOptions": {
"formats": [
"esm-bundler",
"cjs"
]
}
}
formats
It is a customized packaging format, including the format used in the build tool, the format used in the browser, the format used in node, and the format for immediate function execution.esm-bundler
esm-browser
cjs
global
Development environment packagingesbuild
Execute the script during development, the parameter is the module to be packaged
"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}`)
})
Production environment packagingrollup
Please refer to rollup.config.mjs for specific code.
build.js
2. Reactivity module in Vue3
1. Responsive changes in vue3 compared to vue2
- Using defineProperty to hijack data in Vue2 requires rewriting and adding properties and results in poor performance .
getter
setter
- Changes cannot be monitored when attributes are added and deleted. need to be passed and realized
$set
$delete
- Arrays do not use defineProperty to hijack (a waste of performance, hijacking all indexes will cause a waste of performance), the array needs to be processed separately
Proxy is used in Vue3 to implement responsive data changes. thus solving the above problems
2、CompositionAPI
- OptionsAPI is used in Vue2, and the data, props, methods, computed, watch and other attributes provided by the user (users writing complex business logic will have repeated horizontal jump problems)
- All properties in Vue2 are accessed through , and there is a problem of clear pointing
this
this
- Many unused methods or properties in Vue2 will still be packaged, and all global APIs are exposed on Vue objects. The Composition API is more friendly to tree-shaking, and the code is easier to compress.
- Component logic sharing issues, Vue2 uses mixins to achieve logic sharing between components; however, there will be problems such as unclear data sources and naming conflicts. Vue3 uses CompositionAPI to extract public logic very conveniently
Simple components can still be written using OptionsAPI, and compositionAPI has obvious advantages in complex logic~. The module contains many things we often use, such as computed, reactive, ref, effect, etc.reactivity
API
3. Basic use
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 implementation
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// 当修改属性时调用此方法
}
Here I only selected the code for the most commonly used get and set methods. There should also be , , and . In order to quickly grasp the core process, we will skip these codes for the time being.has
deleteProperty
ownKeys
5. Effect implementation
Let's look at the effect code again. By default, the effect will be executed immediately. When the dependent value changes, the effect will be re-executed.
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);
}
}
}
When the effect method is called, the attribute will be valued, and dependencies can be collected at this time.
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. Dependency collection
core code
// 收集属性对应的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 implementation
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 implementation
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':'');
}}
Dependencies
OpenAI opens ChatGPT Voice Vite 5 for free to all users. It is officially released . Operator's magic operation: disconnecting the network in the background, deactivating broadband accounts, forcing users to change optical modems. Microsoft open source Terminal Chat programmers tampered with ETC balances and embezzled more than 2.6 million yuan a year. Used by the father of Redis Pure C language code implements the Telegram Bot framework. If you are an open source project maintainer, how far can you endure this kind of reply? Microsoft Copilot Web AI will be officially launched on December 1, supporting Chinese OpenAI. Former CEO and President Sam Altman & Greg Brockman joined Microsoft. Broadcom announced the successful acquisition of VMware.Author: JD Logistics Qiao Panpan
Source: JD Cloud Developer Community Ziyuanqishuo Tech Please indicate the source when reprinting