Vue3 の設計アイデアとレスポンシブなソース コード分析 | JD Logistics Technology Team

1. Vue3の構造解析

1. Vue2とVue3の比較

  • TypeScript サポートに不向き (すべてのプロパティがこのオブジェクトに配置されるため、コンポーネントのデータ型をオーバーライドすることが困難になります)
  • Vue オブジェクトのプロトタイプには多数の API が実装されており、TreeShaking の実装が困難です。
  • アーキテクチャ レベルは、クロスプラットフォームの DOM レンダリング開発サポートには適していませんが、Vue3 ではカスタム レンダラが可能で、強力な拡張性を備えています。
  • 構成API。ReactHook からインスピレーションを得た
  • 仮想 DOM が書き換えられ、テンプレートのコンパイルが最適化されました...

2. Vue3 の設計アイデア

  • Vue3.0 ではモジュールの分割がより重視されており、2.0 では一部のモジュールを単独で使用することができません。完全な Vuejs を導入する必要があります (例: レスポンシブ部分だけを使いたいが、完全な Vuejs を導入する必要がある) Vue3 はモジュール間の結合が低く、モジュールを独立して使用できます。分割モジュール
  • Vue2 の多くのメソッドはインスタンスにマウントされ、使用されない場合はパッケージ化されます (多くのコンポーネントについても同様です)。構築ツールのツリーシェイキング機構により、オンデマンド導入が実現され、ユーザーのパッケージング量を削減します。APIを書き換える
  • Vue3 はカスタム レンダラーを使用でき、強力な拡張性を備えています。以前に起こったことは起こらず、Vue のソース コードが書き換えられ、レンダリング メソッドが変換されます。拡張するとさらに便利になります

Vue2 の機能も引き続き保持しています。

これは依然として宣言型フレームワークであり、基礎となるレンダリング ロジックを気にしません (命令型スタイルはプロセスにさらに注意を払い、最適な記述方法を制御できますか? 記述プロセスは異なります)。

仮想 DOM の使用

コンパイル時と実行時を区別する

内部的には、コンパイル時 (テンプレート? js コードにプログラムされ、通常はビルド ツールで使用されます) と実行時の間に区別があります。

簡単に言うと、Vue3 フレームワークはより小さく、拡張しやすくなっています。

3. モノレポ管理プロジェクト

Monorepo はプロジェクト コードを管理する方法であり、プロジェクト リポジトリ (リポジトリ) 内の複数のモジュール/パッケージを管理することを指します。つまり、複数のパッケージを 1 つのリポジトリに配置するコード管理モデルです。Vue3 は内部でモジュールの分割を実装しており、Vue3 のソースコードは Monorepo メソッドを使用して管理され、モジュールはパッケージ ディレクトリに分割されます。

  • 1 つのウェアハウスで複数のモジュールを維持できるため、あちこちのウェアハウスを探す必要はありません。
  • バージョン管理や依存関係管理に便利、モジュール間の参照や呼び出しが非常に便利
  • 各パッケージは独立して公開できます

初期はプロジェクト管理に使用され、その後はyarn workspace + lernapnpm

pnpm の概要

主にシンボリック リンクを使用してモジュールを管理する、高速でディスク領域を節約するパッケージ マネージャー

1. 速い
2. ディスク容量を効率的に利用する

ディスク上のすべてのファイルを保存するためにpnpm によって内部的に使用されるファイル システム。このファイル システムの優れた点は次のとおりです。基于内容寻址

  • 同じパッケージが繰り返しインストールされることはありません。npm/yarn を使用する場合、100 のプロジェクトがすべて lodash に依存している場合、lodash は 100 回インストールされる可能性があり、コードのこの部分はディスク上の 100 個の場所に書き込まれます。ただし、pnpm を使用する場合、インストールは 1 回だけであり、ディスク上に書き込む場所は 1 つだけであり、再度使用するときは直接(ハード リンク)使用されますhardlink
  • パッケージのバージョンが異なる場合でも、pnpm は以前のバージョンのコードを大幅に再利用します。たとえば、lodash には 100 個のファイルがあり、更新バージョンの後、さらに 1 つのファイルが追加されます。その後、101 個のファイルはディスクに書き換えられませんが、元の 100 個のファイルは保持され、それらのみが書き込まれますhardlink一个新增的文件
3. モノレポのサポート

pnpm と npm/yarn の大きな違いは、monorepo をサポートしていることです。

4. 高い安全性

以前に npm/yarn を使用した場合、node_module のフラット構造により、A が B に依存し、B が C に依存する場合、A は C を直接使用できますが、問題は、A が C の依存関係を宣言していないことです。そのため、このような不正アクセスが発生してしまうのです。しかし、pnpm は、この問題をうまく解決し、セキュリティを確保する独自の依存関係管理メソッドを作成しました。

デフォルトでは、pnpm はシンボリック リンクを使用してプロジェクトの直接の依存関係をルート ディレクトリに追加するだけです。node_modules



インストールと初期化

  • グローバル インストール (ノード バージョン >16)
npm install pnpm -g

  • 初期化
pnpm init

ワークスペースの構成

ルート ディレクトリに pnpm-workspace.yaml を作成します

packages:
  - 'packages/*'

パッケージ配下のディレクトリはすべてパッケージとして管理されます。このようにして、Monorepo が設定されました。確かに それよりも速い lerna + yarn workspace

4. プロジェクトの構造

パッケージ

  • 反応性: 反応性システム
  • runtime-core: プラットフォームに依存しないランタイム コア (プラットフォーム固有のランタイム - カスタム レンダラーを作成可能)
  • runtime-dom: ブラウザのランタイム。DOM API、プロパティ、イベント処理などが含まれます。
  • runtime-test: テストに使用されます
  • サーバーレンダラー: サーバー側のレンダリングに使用されます。
  • コンパイラコア: プラットフォームに依存しないコンパイラコア
  • Compiler-dom: ブラウザ用のコンパイル モジュール
  • Compiler-ssr: サーバーサイドレンダリング用のコンパイルモジュール
  • template-explorer: コンパイラ出力をデバッグするための開発ツール
  • 共有: 複数のパッケージ間で共有されるコンテンツ
  • vue: ランタイムとコンパイラを含むフルバージョン
                                    +---------------------+
                                    |                     |
                                    |  @vue/compiler-sfc  |
                                    |                     |
                                    +-----+--------+------+
                                          |        |
                                          v        v
                      +---------------------+    +----------------------+
                      |                     |    |                      |
        +------------>|  @vue/compiler-dom  +--->|  @vue/compiler-core  |
        |             |                     |    |                      |
   +----+----+        +---------------------+    +----------------------+
   |         |
   |   vue   |
   |         |
   +----+----+        +---------------------+    +----------------------+    +-------------------+
        |             |                     |    |                      |    |                   |
        +------------>|  @vue/runtime-dom   +--->|  @vue/runtime-core   +--->|  @vue/reactivity  |
                      |                     |    |                      |    |                   |
                      +---------------------+    +----------------------+    +-------------------+

スクリプト

Vue3 は、開発環境では esbuild を使用してパッケージ化され、運用環境ではロールアップが使用されます。

パッケージの相互依存性

インストール

パッケージのインストール/パッケージ/反応性への共有

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 {
    ...
  }
...

ヒント: @vue/shared を導入するとエラーが報告されるため、tsconfig.json で構成する必要があります。

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@vue/compat": ["packages/vue-compat/src"],
      "@vue/*": ["packages/*/src"],
      "vue": ["packages/vue/src"]
    }
  },
}

5.梱包

すべてのパッケージの入口は統一されたパッケージを実現するためにあります。src/index.ts

反応性/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"
    ]
  }
}

共有/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 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 を参照してください。

ビルド.js

2. Vue3 の反応性モジュール

1. vue2 と比較した vue3 のレスポンシブな変更点

  • Vue2 で defineProperty を使用してデータをハイジャックするには、プロパティの書き換えと追加が必要となりパフォーマンスが低下しますgettersetter
  • 属性の追加および削除時の変更は監視できません。通過して実現する必要がある$set$delete
  • 配列はハイジャックにdefinePropertyを使用しません(パフォーマンスの無駄です。すべてのインデックスをハイジャックするとパフォーマンスの無駄が発生します)。配列は個別に処理する必要があります。
Vue3 ではプロキシを使用して、応答性の高いデータ変更を実装します。したがって、上記の問題を解決します

2、構成API

  • Vue2 では OptionsAPI が使用され、データ、プロップ、メソッド、計算済み、監視、その他の属性はユーザーによって提供されます (複雑なビジネス ロジックを作成するユーザーは水平ジャンプの問題を繰り返し発生します)。
  • Vue2 のすべてのプロパティは を通じてアクセスされ明確なポインティングの問題があります。thisthis
  • Vue2 の未使用のメソッドやプロパティの多くは引き続きパッケージ化され、すべてのグローバル API は Vue オブジェクトで公開されます。Comboposition API はツリーシェイキングに適しており、コードの圧縮が容易です。
  • コンポーネントのロジック共有の問題。Vue2 はミックスインを使用してコンポーネント間のロジックの共有を実現しますが、不明瞭なデータ ソースや名前の競合などの問題が発生します。Vue3 は、CompositionAPI を使用してパブリック ロジックを非常に便利に抽出します。
単純なコンポーネントは依然として OptionsAPI を使用して作成でき、compositionAPI は複雑なロジックにおいて明らかな利点を持っています~。 このモジュールには、computed、reactive、ref、effect など、よく使用するものが多数含まれています。 reactivity API

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. リアクティブな実装

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
}

ベースハンドラー

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.エフェクトの実装

エフェクトのコードをもう一度見てみましょう。デフォルトではエフェクトはすぐに実行されますが、依存する値が変更されるとエフェクトが再実行されます。

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(()=>{
    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;
    }
}

実装を追跡する

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!
    //  })
   // }
  }
}

トリガーの実装

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':'');
    }}

依存関係





著者: JD Logistics Qiao Panpan

出典:JD Cloud Developer Community Ziyuanqishuo Tech 転載の際は出典を明記してください

OpenAI が ChatGPT Voice Vite 5 をすべてのユーザーに無料で公開、正式にリリース オペレーターの魔法の操作: バックグラウンドでネットワークを切断、ブロードバンド アカウントを非アクティブ化、ユーザーに光モデムの変更を強制 Microsoft オープン ソースの ターミナル チャット プログラマーが ETC 残高を改ざんし、年間 260 万元以上を横領 Redis の父が使用する Pure C 言語コードは、Telegram Bot フレームワークを実装しています あなたがオープンソース プロジェクトのメンテナである場合、この種の返答にどこまで耐えることができますか? Microsoft Copilot Web AI は 12 月 1 日に正式にリリースされ、中国の OpenAI をサポートします 元 CEO 兼社長の Sam Altman 氏と Greg Brockman 氏が Microsoft に加わりました Broadcom は VMware の買収に成功したと発表しました
{{名前}}
{{名前}}

おすすめ

転載: my.oschina.net/u/4090830/blog/10150674