一、Vue3結(jié)構(gòu)分析
1、Vue2與Vue3的對(duì)比
- 對(duì)TypeScript支持不友好(所有屬性都放在了this對(duì)象上,難以推倒組件的數(shù)據(jù)類型)
- 大量的API掛載在Vue對(duì)象的原型上,難以實(shí)現(xiàn)TreeShaking。
- 架構(gòu)層面對(duì)跨平臺(tái)dom渲染開發(fā)支持不友好,vue3允許自定義渲染器,擴(kuò)展能力強(qiáng)。
- CompositionAPI。受ReactHook啟發(fā)
- 對(duì)虛擬DOM進(jìn)行了重寫、對(duì)模板的編譯進(jìn)行了優(yōu)化操作...
2、Vue3設(shè)計(jì)思想
- Vue3.0更注重模塊上的拆分,在2.0中無(wú)法單獨(dú)使用部分模塊。需要引入完整的Vuejs(例如只想使用使用響應(yīng)式部分,但是需要引入完整的Vuejs), Vue3中的模塊之間耦合度低,模塊可以獨(dú)立使用。拆分模塊
- Vue2中很多方法掛載到了實(shí)例中導(dǎo)致沒有使用也會(huì)被打包(還有很多組件也是一樣)。通過(guò)構(gòu)建工具Tree-shaking機(jī)制實(shí)現(xiàn)按需引入,減少用戶打包后體積。重寫API
- Vue3允許自定義渲染器,擴(kuò)展能力強(qiáng)。不會(huì)發(fā)生以前的事情,改寫Vue源碼改造渲染方式。擴(kuò)展更方便
依然保留了Vue2的特點(diǎn):
依舊是聲明式框架,底層渲染邏輯不關(guān)心(命令式比較關(guān)注過(guò)程,可以控制怎么寫最優(yōu)?編寫過(guò)程不同),如for和reduce
采用虛擬DOM
區(qū)分編譯時(shí)和運(yùn)行時(shí)
內(nèi)部區(qū)分了編譯時(shí)(模板?編程成js代碼,一般在構(gòu)建工具中使用)和運(yùn)行時(shí)
簡(jiǎn)單來(lái)說(shuō),Vue3 框架更小,擴(kuò)展更加方便
3、monorepo管理項(xiàng)目
Monorepo 是管理項(xiàng)目代碼的一個(gè)方式,指在一個(gè)項(xiàng)目倉(cāng)庫(kù)(repo)中管理多個(gè)模塊/包(package)。也就是說(shuō)是一種將多個(gè)package放在一個(gè)repo中的代碼管理模式。Vue3內(nèi)部實(shí)現(xiàn)了一個(gè)模塊的拆分, Vue3源碼采用 Monorepo 方式進(jìn)行管理,將模塊拆分到package目錄中。
- 一個(gè)倉(cāng)庫(kù)可維護(hù)多個(gè)模塊,不用到處找倉(cāng)庫(kù)
- 方便版本管理和依賴管理,模塊之間的引用,調(diào)用都非常方便
- 每個(gè)包可以獨(dú)立發(fā)布
早期使用yarn workspace + lerna
來(lái)管理項(xiàng)目,后面是pnpm
pnpm介紹
快速,節(jié)省磁盤空間的包管理器,主要采用符號(hào)鏈接的方式管理模塊
-
快速
-
高效利用磁盤空間
pnpm 內(nèi)部使用基于內(nèi)容尋址
的文件系統(tǒng)來(lái)存儲(chǔ)磁盤上所有的文件,這個(gè)文件系統(tǒng)出色的地方在于:
- 不會(huì)重復(fù)安裝同一個(gè)包。用 npm/yarn 的時(shí)候,如果 100 個(gè)項(xiàng)目都依賴 lodash,那么 lodash 很可能就被安裝了 100 次,磁盤中就有 100 個(gè)地方寫入了這部分代碼。但在使用 pnpm 只會(huì)安裝一次,磁盤中只有一個(gè)地方寫入,后面再次使用都會(huì)直接使用
hardlink
(硬鏈接) - 即使一個(gè)包的不同版本,pnpm 也會(huì)極大程度地復(fù)用之前版本的代碼。比如 lodash 有 100 個(gè)文件,更新版本之后多了一個(gè)文件,那么磁盤當(dāng)中并不會(huì)重新寫入 101 個(gè)文件,而是保留原來(lái)的 100 個(gè)文件的
hardlink
,僅僅寫入那一個(gè)新增的文件
。
- 支持Monorepo
pnpm 與 npm/yarn 一個(gè)很大的不同就是支持了 monorepo
- 安全性高
之前在使用 npm/yarn 的時(shí)候,由于 node_module 的扁平結(jié)構(gòu),如果 A 依賴 B, B 依賴 C,那么 A 當(dāng)中是可以直接使用 C 的,但問(wèn)題是 A 當(dāng)中并沒有聲明 C 這個(gè)依賴。因此會(huì)出現(xiàn)這種非法訪問(wèn)的情況。但 pnpm自創(chuàng)了一套依賴管理方式,很好地解決了這個(gè)問(wèn)題,保證了安全性
默認(rèn)情況下,pnpm 則是通過(guò)使用符號(hào)鏈接的方式僅將項(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:與平臺(tái)無(wú)關(guān)的運(yùn)行時(shí)核心 (可以創(chuàng)建針對(duì)特定平臺(tái)的運(yùn)行時(shí) - 自定義渲染器)
- runtime-dom: 針對(duì)瀏覽器的運(yùn)行時(shí)。包括DOM API,屬性,事件處理等
- runtime-test:用于測(cè)試
- server-renderer:用于服務(wù)器端渲染
- compiler-core:與平臺(tái)無(wú)關(guān)的編譯器核心
- compiler-dom: 針對(duì)瀏覽器的編譯模塊
- compiler-ssr: 針對(duì)服務(wù)端渲染的編譯模塊
- template-explorer:用于調(diào)試編譯器輸出的開發(fā)工具
- shared:多個(gè)包之間共享的內(nèi)容
- vue:完整版本,包括運(yùn)行時(shí)和編譯器
+---------------------+
| |
| @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引入會(huì)報(bào)錯(cuò)
const onlyGetter = isFunction(getterOrOptions)
if (onlyGetter) {
...
} else {
...
}
...
tips:@vue/shared引入會(huì)報(bào)錯(cuò),需要在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ā)時(shí) 執(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對(duì)比vue2的響應(yīng)式變化
- 在Vue2的時(shí)候使用defineProperty來(lái)進(jìn)行數(shù)據(jù)的劫持, 需要對(duì)屬性進(jìn)行重寫添加
getter
及setter
性能差。 - 當(dāng)新增屬性和刪除屬性時(shí)無(wú)法監(jiān)控變化。需要通過(guò)
$set
、$delete
實(shí)現(xiàn) - 數(shù)組不采用defineProperty來(lái)進(jìn)行劫持 (浪費(fèi)性能,對(duì)所有索引進(jìn)行劫持會(huì)造成性能浪費(fèi))需要對(duì)數(shù)組單獨(dú)進(jìn)行處理
Vue3中使用Proxy來(lái)實(shí)現(xiàn)響應(yīng)式數(shù)據(jù)變化。從而解決了上述問(wèn)題
2、CompositionAPI
- 在Vue2中采用的是OptionsAPI, 用戶提供的data,props,methods,computed,watch等屬性 (用戶編寫復(fù)雜業(yè)務(wù)邏輯會(huì)出現(xiàn)反復(fù)橫跳問(wèn)題)
- Vue2中所有的屬性都是通過(guò)
this
訪問(wèn),this
存在指向明確問(wèn)題 - Vue2中很多未使用方法或?qū)傩砸琅f會(huì)被打包,并且所有全局API都在Vue對(duì)象上公開。Composition API對(duì) tree-shaking 更加友好,代碼也更容易壓縮。
- 組件邏輯共享問(wèn)題, Vue2 采用mixins 實(shí)現(xiàn)組件之間的邏輯共享; 但是會(huì)有數(shù)據(jù)來(lái)源不明確,命名沖突等問(wèn)題。 Vue3采用CompositionAPI 提取公共邏輯非常方便
簡(jiǎn)單的組件仍然可以采用OptionsAPI進(jìn)行編寫,compositionAPI在復(fù)雜的邏輯中有著明顯的優(yōu)勢(shì)~。
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){
// 對(duì)獲取的值進(jìn)行放射
const res = Reflect.get(target, key, receiver);
console.log('屬性獲取',key)
if(isObject(res)){// 如果獲取的值是對(duì)象類型,則返回當(dāng)前對(duì)象的代理對(duì)象
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)獲取屬性時(shí)調(diào)用此方法
set// 當(dāng)修改屬性時(shí)調(diào)用此方法
}
這里我只選了對(duì)最常用到的get和set方法的代碼,還應(yīng)該有
has
、deleteProperty
、ownKeys
。這里為了快速掌握核心流程就先暫且跳過(guò)這些代碼
5、effect實(shí)現(xiàn)
我們?cè)賮?lái)看effect的代碼,默認(rèn)effect會(huì)立即執(zhí)行,當(dāng)依賴的值發(fā)生變化時(shí)effect會(huì)重新執(zhí)行
export let activeEffect = undefined;
// 依賴收集的原理是 借助js是單線程的特點(diǎn), 默認(rèn)調(diào)用effect的時(shí)候會(huì)去調(diào)用proxy的get,此時(shí)讓屬性記住
// 依賴的effect,同理也讓effect記住對(duì)應(yīng)的屬性
// 靠的是數(shù)據(jù)結(jié)構(gòu) weakMap : {map:{key:new Set()}}
// 稍后數(shù)據(jù)變化的時(shí)候 找到對(duì)應(yīng)的map 通過(guò)屬性出發(fā)set中effect
function cleanEffect(effect) {
// 需要清理effect中存入屬性中的set中的effect
// 每次執(zhí)行前都需要將effect只對(duì)應(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í)可以傳遞參數(shù),computed也是基于effect來(lái)實(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// 用戶可以手動(dòng)調(diào)用runner重新執(zhí)行
}
export class ReactiveEffect {
public active = true;
public parent = null;
public deps = []; // effect中用了哪些屬性,后續(xù)清理的時(shí)候要使用
constructor(public fn,public scheduler?) { } // 你傳遞的fn我會(huì)幫你放到this上
// effectScope 可以來(lái)實(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對(duì)象上取值, 取之的時(shí)候 我要讓這個(gè)熟悉 和當(dāng)前的effect函數(shù)關(guān)聯(lián)起來(lái),稍后數(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)用時(shí)會(huì)對(duì)屬性進(jìn)行取值,此時(shí)可以進(jìn)行依賴收集。
effect(()=>{
console.log(state.name)
// 執(zhí)行用戶傳入的fn函數(shù),會(huì)取到state.name,state.age... 會(huì)觸發(fā)reactive中的getter
app.innerHTML = 'name:' + state.name + 'age:' + state.age + 'address' + state.address.city
})
6、依賴收集
核心代碼
// 收集屬性對(duì)應(yīng)的effect
export function track(target, type, key){}// 觸發(fā)屬性對(duì)應(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);
// 取值時(shí)依賴收集
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è)置值時(shí)觸發(fā)更新 - ADD
trigger(target, TriggerOpTypes.ADD, key);
}else if(hasChanged(value, oldValue)){
// 設(shè)置值時(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);// 取對(duì)應(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 就找到對(duì)應(yīng)的key的依賴執(zhí)行
if(key !==void0){
run(depsMap.get(key));
}
// 數(shù)組新增屬性
if(type == TriggerOpTypes.ADD){
run(depsMap.get(isArray(target)?'length':'');
}}
依賴關(guān)系
作者:京東物流?喬盼盼文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-746947.html
來(lái)源:京東云開發(fā)者社區(qū) 自猿其說(shuō)Tech 轉(zhuǎn)載請(qǐng)注明來(lái)源文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-746947.html
到了這里,關(guān)于Vue3設(shè)計(jì)思想及響應(yīng)式源碼剖析的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!