Vue3設(shè)計(jì)思想及響應(yīng)式源碼解析
一、Vue3結(jié)構(gòu)分析
1、Vue2與Vue3的對比
- 對TypeScript支持不友好(所有屬性都放在了this對象上,難以推倒組件的數(shù)據(jù)類型)
- 大量的API掛載在Vue對象的原型上,難以實(shí)現(xiàn)TreeShaking。
- 架構(gòu)層面對跨平臺dom渲染開發(fā)支持不友好,vue3允許自定義渲染器,擴(kuò)展能力強(qiáng)。
- CompositionAPI。受ReactHook啟發(fā)
- 對虛擬DOM進(jìn)行了重寫、對模板的編譯進(jìn)行了優(yōu)化操作...
2、Vue3設(shè)計(jì)思想
- Vue3.0更注重模塊上的拆分,在2.0中無法單獨(dú)使用部分模塊。需要引入完整的Vuejs(例如只想使用使用響應(yīng)式部分,但是需要引入完整的Vuejs), Vue3中的模塊之間耦合度低,模塊可以獨(dú)立使用。拆分模塊
- Vue2中很多方法掛載到了實(shí)例中導(dǎo)致沒有使用也會被打包(還有很多組件也是一樣)。通過構(gòu)建工具Tree-shaking機(jī)制實(shí)現(xiàn)按需引入,減少用戶打包后體積。重寫API
- Vue3允許自定義渲染器,擴(kuò)展能力強(qiáng)。不會發(fā)生以前的事情,改寫Vue源碼改造渲染方式。擴(kuò)展更方便
依然保留了Vue2的特點(diǎn):
依舊是聲明式框架,底層渲染邏輯不關(guān)心(命令式比較關(guān)注過程,可以控制怎么寫最優(yōu)?編寫過程不同),如for和reduce
采用虛擬DOM
區(qū)分編譯時和運(yùn)行時
內(nèi)部區(qū)分了編譯時(模板?編程成js代碼,一般在構(gòu)建工具中使用)和運(yùn)行時
簡單來說,Vue3 框架更小,擴(kuò)展更加方便
3、monorepo管理項(xiàng)目
Monorepo 是管理項(xiàng)目代碼的一個方式,指在一個項(xiàng)目倉庫(repo)中管理多個模塊/包(package)。也就是說是一種將多個package放在一個repo中的代碼管理模式。Vue3內(nèi)部實(shí)現(xiàn)了一個模塊的拆分, Vue3源碼采用 Monorepo 方式進(jìn)行管理,將模塊拆分到package目錄中。
- 一個倉庫可維護(hù)多個模塊,不用到處找倉庫
- 方便版本管理和依賴管理,模塊之間的引用,調(diào)用都非常方便
- 每個包可以獨(dú)立發(fā)布
早期使用yarn workspace + lerna
來管理項(xiàng)目,后面是pnpm
pnpm介紹
快速,節(jié)省磁盤空間的包管理器,主要采用符號鏈接的方式管理模塊
- 快速
- 高效利用磁盤空間
pnpm 內(nèi)部使用基于內(nèi)容尋址
的文件系統(tǒng)來存儲磁盤上所有的文件,這個文件系統(tǒng)出色的地方在于:
- 不會重復(fù)安裝同一個包。用 npm/yarn 的時候,如果 100 個項(xiàng)目都依賴 lodash,那么 lodash 很可能就被安裝了 100 次,磁盤中就有 100 個地方寫入了這部分代碼。但在使用 pnpm 只會安裝一次,磁盤中只有一個地方寫入,后面再次使用都會直接使用
hardlink
(硬鏈接) - 即使一個包的不同版本,pnpm 也會極大程度地復(fù)用之前版本的代碼。比如 lodash 有 100 個文件,更新版本之后多了一個文件,那么磁盤當(dāng)中并不會重新寫入 101 個文件,而是保留原來的 100 個文件的
hardlink
,僅僅寫入那一個新增的文件
。
- 支持Monorepo
pnpm 與 npm/yarn 一個很大的不同就是支持了 monorepo
- 安全性高
之前在使用 npm/yarn 的時候,由于 node_module 的扁平結(jié)構(gòu),如果 A 依賴 B, B 依賴 C,那么 A 當(dāng)中是可以直接使用 C 的,但問題是 A 當(dāng)中并沒有聲明 C 這個依賴。因此會出現(xiàn)這種非法訪問的情況。但 pnpm自創(chuàng)了一套依賴管理方式,很好地解決了這個問題,保證了安全性
默認(rèn)情況下,pnpm 則是通過使用符號鏈接的方式僅將項(xiàng)目的直接依賴項(xiàng)添加到node_modules
的根目錄下。
安裝和初始化
- 全局安裝(node版本>16)
npm install pnpm -g
- 初始化
pnpm init
配置workspace
根目錄創(chuàng)建pnpm-workspace.yaml
packages: - 'packages/*'
將packages下所有的目錄都作為包進(jìn)行管理。這樣我們的Monorepo就搭建好了。確實(shí)比 lerna + yarn workspace
更快捷
4、項(xiàng)目結(jié)構(gòu)
packages
- reactivity:響應(yīng)式系統(tǒng)
- runtime-core:與平臺無關(guān)的運(yùn)行時核心 (可以創(chuàng)建針對特定平臺的運(yùn)行時 - 自定義渲染器)
- runtime-dom: 針對瀏覽器的運(yùn)行時。包括DOM API,屬性,事件處理等
- runtime-test:用于測試
- server-renderer:用于服務(wù)器端渲染
- compiler-core:與平臺無關(guān)的編譯器核心
- compiler-dom: 針對瀏覽器的編譯模塊
- compiler-ssr: 針對服務(wù)端渲染的編譯模塊
- template-explorer:用于調(diào)試編譯器輸出的開發(fā)工具
- shared:多個包之間共享的內(nèi)容
- vue:完整版本,包括運(yùn)行時和編譯器
+---------------------+ | | | @vue/compiler-sfc | | | +-----+--------+------+ | | v v +---------------------+ +----------------------+ | | | | +------------>| @vue/compiler-dom +--->| @vue/compiler-core | | | | | | +----+----+ +---------------------+ +----------------------+ | | | vue | | | +----+----+ +---------------------+ +----------------------+ +-------------------+ | | | | | | | +------------>| @vue/runtime-dom +--->| @vue/runtime-core +--->| @vue/reactivity | | | | | | | +---------------------+ +----------------------+ +-------------------+
scripts
Vue3在開發(fā)環(huán)境使用esbuild打包,生產(chǎn)環(huán)境采用rollup打包
包的相互依賴
安裝
把packages/shared安裝到packages/reactivity
pnpm install @vue/shared@workspace --filter @vue/reactivity
使用
在reactivity/src/computed.ts中引入shared中相關(guān)方法
import { isFunction, NOOP } from '@vue/shared' // ts引入會報(bào)錯 const onlyGetter = isFunction(getterOrOptions) if (onlyGetter) { ... } else { ... } ...
tips:@vue/shared引入會報(bào)錯,需要在tsconfig.json中配置
{ "compilerOptions": { "baseUrl": ".", "paths": { "@vue/compat": ["packages/vue-compat/src"], "@vue/*": ["packages/*/src"], "vue": ["packages/vue/src"] } }, }
5、打包
所有包的入口均為src/index.ts
這樣可以實(shí)現(xiàn)統(tǒng)一打包.
• 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
在構(gòu)建工具中使用的格式、 esm-browser
在瀏覽器中使用的格式、 cjs
在node中使用的格式、 global
立即執(zhí)行函數(shù)的格式
開發(fā)環(huán)境esbuild
打包
開發(fā)時 執(zhí)行腳本, 參數(shù)為要打包的模塊
"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}`) })
生產(chǎn)環(huán)境rollup
打包
具體代碼參考rollup.config.mjs
build.js
二、Vue3中Reactivity模塊
1、vue3對比vue2的響應(yīng)式變化
- 在Vue2的時候使用defineProperty來進(jìn)行數(shù)據(jù)的劫持, 需要對屬性進(jìn)行重寫添加
getter
及setter
性能差。 - 當(dāng)新增屬性和刪除屬性時無法監(jiān)控變化。需要通過
$set
、$delete
實(shí)現(xiàn) - 數(shù)組不采用defineProperty來進(jìn)行劫持 (浪費(fèi)性能,對所有索引進(jìn)行劫持會造成性能浪費(fèi))需要對數(shù)組單獨(dú)進(jìn)行處理
Vue3中使用Proxy來實(shí)現(xiàn)響應(yīng)式數(shù)據(jù)變化。從而解決了上述問題
2、CompositionAPI
- 在Vue2中采用的是OptionsAPI, 用戶提供的data,props,methods,computed,watch等屬性 (用戶編寫復(fù)雜業(yè)務(wù)邏輯會出現(xiàn)反復(fù)橫跳問題)
- Vue2中所有的屬性都是通過
this
訪問,this
存在指向明確問題 - Vue2中很多未使用方法或?qū)傩砸琅f會被打包,并且所有全局API都在Vue對象上公開。Composition API對 tree-shaking 更加友好,代碼也更容易壓縮。
- 組件邏輯共享問題, Vue2 采用mixins 實(shí)現(xiàn)組件之間的邏輯共享; 但是會有數(shù)據(jù)來源不明確,命名沖突等問題。 Vue3采用CompositionAPI 提取公共邏輯非常方便
簡單的組件仍然可以采用OptionsAPI進(jìn)行編寫,compositionAPI在復(fù)雜的邏輯中有著明顯的優(yōu)勢~。 reactivity
模塊中就包含了很多我們經(jīng)常使用到的 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實(shí)現(xiàn)
import { mutableHandlers } from'./baseHandlers'; // 代理相關(guān)邏輯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){ // 對獲取的值進(jìn)行放射 const res = Reflect.get(target, key, receiver); console.log('屬性獲取',key) if(isObject(res)){// 如果獲取的值是對象類型,則返回當(dāng)前對象的代理對象 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,// 當(dāng)獲取屬性時調(diào)用此方法 set// 當(dāng)修改屬性時調(diào)用此方法 }
這里我只選了對最常用到的get和set方法的代碼,還應(yīng)該有 has
、 deleteProperty
、 ownKeys
。這里為了快速掌握核心流程就先暫且跳過這些代碼
5、effect實(shí)現(xiàn)
我們再來看effect的代碼,默認(rèn)effect會立即執(zhí)行,當(dāng)依賴的值發(fā)生變化時effect會重新執(zhí)行
export let activeEffect = undefined; // 依賴收集的原理是 借助js是單線程的特點(diǎn), 默認(rèn)調(diào)用effect的時候會去調(diào)用proxy的get,此時讓屬性記住 // 依賴的effect,同理也讓effect記住對應(yīng)的屬性 // 靠的是數(shù)據(jù)結(jié)構(gòu) weakMap : {map:{key:new Set()}} // 稍后數(shù)據(jù)變化的時候 找到對應(yīng)的map 通過屬性出發(fā)set中effect function cleanEffect(effect) { // 需要清理effect中存入屬性中的set中的effect // 每次執(zhí)行前都需要將effect只對應(yīng)屬性的set集合都清理掉 // 屬性中的set 依然存放effect let deps = effect.deps for (let i = 0; i < deps.length; i++) { deps[i].delete(effect) } effect.deps.length = 0; } // 創(chuàng)建effect時可以傳遞參數(shù),computed也是基于effect來實(shí)現(xiàn)的,只是增加了一些參數(shù)條件而已 export function effect<T = any>( fn: () => T, options?: ReactiveEffectOptions ){ // 將用戶傳遞的函數(shù)編程響應(yīng)式的effect const _effect = new ReactiveEffect(fn,options.scheduler); // 更改runner中的this _effect.run() const runner = _effect.run.bind(_effect); runner.effect = _effect; // 暴露effect的實(shí)例 return runner// 用戶可以手動調(diào)用runner重新執(zhí)行 } export class ReactiveEffect { public active = true; public parent = null; public deps = []; // effect中用了哪些屬性,后續(xù)清理的時候要使用 constructor(public fn,public scheduler?) { } // 你傳遞的fn我會幫你放到this上 // effectScope 可以來實(shí)現(xiàn)讓所有的effect停止 run() { // 依賴收集 讓熟悉和effect 產(chǎn)生關(guān)聯(lián) if (!this.active) { return this.fn(); } else { try { this.parent = activeEffect activeEffect = this; cleanEffect(this); // vue2 和 vue3中都是要清理的 return this.fn(); // 去proxy對象上取值, 取之的時候 我要讓這個熟悉 和當(dāng)前的effect函數(shù)關(guān)聯(lián)起來,稍后數(shù)據(jù)變化了 ,可以重新執(zhí)行effect函數(shù) } finally { // 取消當(dāng)前正在運(yùn)行的effect activeEffect = this.parent; this.parent = null; } } } stop() { if (this.active) { this.active = false; cleanEffect(this); } } }
在effect方法調(diào)用時會對屬性進(jìn)行取值,此時可以進(jìn)行依賴收集。
effect(()=>{ console.log(state.name) // 執(zhí)行用戶傳入的fn函數(shù),會取到state.name,state.age... 會觸發(fā)reactive中的getter app.innerHTML = 'name:' + state.name + 'age:' + state.age + 'address' + state.address.city })
6、依賴收集
核心代碼
// 收集屬性對應(yīng)的effect export function track(target, type, key){}// 觸發(fā)屬性對應(yīng)effect執(zhí)行 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){ // 設(shè)置值時觸發(fā)更新 - ADD trigger(target, TriggerOpTypes.ADD, key); }else if(hasChanged(value, oldValue)){ // 設(shè)置值時觸發(fā)更新 - SET trigger(target, TriggerOpTypes.SET, key, value, oldValue); } return result; } }
track的實(shí)現(xiàn)
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);// 取對應(yīng)屬性的依賴表 if(!dep){// 如果沒有則構(gòu)建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實(shí)現(xiàn)
export function trigger(target, type, key){ const depsMap = targetMap.get(target); if(!depsMap){ return; } const run=(effects)=>{ if(effects){ effects.forEach(effect=>effect()); } } // 有key 就找到對應(yīng)的key的依賴執(zhí)行 if(key !==void0){ run(depsMap.get(key)); } // 數(shù)組新增屬性 if(type == TriggerOpTypes.ADD){ run(depsMap.get(isArray(target)?'length':''); }}
依賴關(guān)系
以上就是Vue3設(shè)計(jì)思想及響應(yīng)式源碼解析的詳細(xì)內(nèi)容,更多關(guān)于Vue3響應(yīng)式設(shè)計(jì)思想的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Props傳參v-for后TS報(bào)錯對象類型是unknow的解決方案
這篇文章主要介紹了Props傳參v-for后TS報(bào)錯對象類型是unknow的解決方案,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2024-03-03關(guān)于Vite不能使用require問題的解決方法
在vue2中我們通常會在模板中通過三目運(yùn)算符和require來實(shí)現(xiàn)動態(tài)圖片,下面這篇文章主要給大家介紹了關(guān)于Vite不能使用require問題的解決方法,需要的朋友可以參考下2022-10-10VUE和Antv G6實(shí)現(xiàn)在線拓?fù)鋱D編輯操作
這篇文章主要介紹了VUE和Antv G6實(shí)現(xiàn)在線拓?fù)鋱D編輯操作,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-10-10淺談vue中document.getElementById()拿到的是原值的問題
這篇文章主要介紹了淺談vue中document.getElementById()拿到的是原值的問題,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-07-07Vue純前端使用exceljs導(dǎo)出excel文件的完整圖文教程
這篇文章將一步一步為大家詳細(xì)介紹一下exceljs插件中的使用,以及如何使用exceljs導(dǎo)出excel文件,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2025-03-03vue實(shí)現(xiàn)input輸入模糊查詢的三種方式
本文主要介紹了vue實(shí)現(xiàn)input輸入模糊查詢的三種方式嗎,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-08-08vue.js如何處理數(shù)組對象中某個字段是否變?yōu)閮蓚€字段
這篇文章主要介紹了vue.js如何處理數(shù)組對象中某個字段是否變?yōu)閮蓚€字段方式,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2025-03-03