Vue3 design ideas and responsive source code analysis | JD Logistics Technology Team

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 + lernapnpm

Introduction to pnpm

A fast , disk space-saving package manager that mainly uses symbolic links to manage modules.

1. Fast
2. Efficiently utilize disk space

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一个新增的文件
3.Support Monorepo

A big difference between pnpm and npm/yarn is that it supports monorepo

4. High safety

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

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 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 .gettersetter
  • 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 pointingthisthis
  • 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





Author: JD Logistics Qiao Panpan

Source: JD Cloud Developer Community Ziyuanqishuo Tech Please indicate the source when reprinting

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.
{{o.name}}
{{m.name}}

Guess you like

Origin my.oschina.net/u/4090830/blog/10150674