本篇是vue3從入門到實戰中篇,主要講一些vue3中的簡單原理,若是你還未接觸過vue3,能夠觀看個人vue3從入門到實戰上篇:juejin.cn/post/686968…html
因爲筆者只是學習了前端11個月的小白,對vue3不少原理並不瞭解,如dom-diff,虛擬dom,模板編譯等,這些知識筆者大多隻是知其然不知其因此然,本篇主要寫一些vue3的簡單原理,如computed,reactive,watchEffect,vuex等,而後稍微簡單分析一下vite。筆者能力有限,也許寫的不會太好,不過本文會盡量的講的詳細,會將代碼上傳自github,喜歡的小夥伴能夠去github上自取,而後在本地進行調試和學習。前端
vuex-ts 超簡易源碼 github.com/1131446340a…vue
vue3-響應式 超簡易源碼 但願各位讀者大大賞一個小贊。 github.com/1131446340a…node
這些過於簡單,只是爲了讓你們在以後的閱讀能瞭解數據類型,所以這些只寫一下代碼而不作過多解釋 #、# util.ts 寫了兩個函數判斷是不是對象和函數react
export const isObject = (target:any) => !!(target && typeof target === 'object')
export const isFunction =(target:any)=>(typeof target ==='function')
複製代碼
import { DefineProperty } from './../interface';
export type strNumSym = string | number | symbol
export type isFunOrObject = Function | undefined | null
export type effectTypeGet = 'get'
export type effectTypeSet = 'set' | 'add'
export type computedOptiobs = DefineProperty | Function
export type _Function = <T extends object>() =>T
複製代碼
import { DefineProperty } from './../interface';
export type strNumSym = string | number | symbol
export type isFunOrObject = Function | undefined | null
export type effectTypeGet = 'get'
export type effectTypeSet = 'set' | 'add'
export type computedOptiobs = DefineProperty | Function
export type _Function = <T extends object>() =>T
複製代碼
說明:要求瞭解es6 Proxy,Reflectwebpack
import { isObject } from "./util"
import { handle } from './basehandles'
export const reactive = <T extends object>(target: T) => {
if (isObject(target)) {
return new Proxy(target, handle)
}
return target
}
複製代碼
這段代碼超級簡單,只是定義了個泛型函數,其做用就是如過參數是對象,則對其使用Proxy代理,不然直接返回。git
你們能夠看到,若是是對象,使用了handle對其進行處理,handle是在basehandles.ts中引入的,先讓咱們來看一下它的代碼es6
import { strNumSym } from './types/index';
import { reactive } from './reactive'
import { isObject } from './util';
function get<T extends object>(target: T, key: strNumSym, receiver: T) {
let res = Reflect.get(target, key, receiver)
return isObject(target[key]) ? reactive(target[key]) : res;
}
function set<T>(target: any, key: strNumSym, value: T, receiver: object) {
let hasKey = Object.prototype.hasOwnProperty.call(target, key)
let oldval = target[key]
let res = Reflect.set(target, key, value, receiver)
if (!hasKey) {
// do something...
}
else if (value !== oldval) {
// do something....
}
return res
}
export const handle = {
get, set
}
複製代碼
沒錯,爲了簡單,我這裏都handle只是一個對象,只有get參數和set參數。 get函數相對簡單,若是進行取值操做,觸發get函數,若是target[key] 是對象則進行深度代理,不然直接返回target[key]。github
set函數也比較簡單,若是進行了改值操做,則觸發set函數。將target[key]改成 新值。web
不過改值有添加屬性和修改屬性值兩種,對這兩種分別作一個判斷,作一些其餘操做便可。
值得注意對是,對於數組而言,若是push不但會增長一個屬性還會修改數組的length屬性。對於這兩個分別作了什麼操做稍後再分析。
咱們先簡單分析一下代碼,就會發現只有在讀值的時候纔會進行遞歸操做使數據變成響應式,而不是一上來就深度遞歸使全部數據進行響應式。
Effect 有點長,所以打算分三部分寫完
首先是第一部分
import { strNumSym, effectTypeSet, effectTypeGet } from './types';
import { Effect, EffectOptions }from './interface'
export const effect = (fn: Function, options:EffectOptions= {
lazy: false
}) => {
let effect = createEffect(fn,options)
if (!options.lazy) {
effect()
}
return effect
}
複製代碼
先看effect函數,接受兩個參數,第一個是回調函數,第二個是options函數,關於options有那些類型能夠去看一EffectOptions接口。目前只傳了一個lazy屬性。 若是你們知道watchEffect方法的話,就會知道watchEffect中的回調函數在項目中啓動的時候就會當即執行一次。所以,若是options.lazy爲false的話,則調用一下回調函數,可是咱們還要作一些其餘的操做,所以咱們來看一下 createEffect 函數
let uid = 0
let activeEffect: Effect
let effectStack: Effect[] = []
function createEffect(fn: Function, options:EffectOptions = {}) {
let effect: Effect = function effectReactive() {
if (!effectStack.includes(effect)) {
try {
effectStack.push(effect)
activeEffect = effect
return fn()
} finally {
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
}
}
effect.id = uid++
effect.options = options
effect.deps=[]
return effect
}
複製代碼
這段代碼uid是一個id標誌,activeEffect見名知意,暫時尚未用到,主要是爲了未來進行依賴收集,咱們暫時不用去管它。最核心的代碼就是建立了一個effectStack隊列,其主要是爲了確認當前活躍Effect。而後執行咱們傳進入的回調函數,而後刪除隊列中的最後一個並修改活躍的activeEffect。 好了,咱們目前作的僅僅只是讓傳入的effect回調在執行時就調用函數,它的另一個功能就是在改值時從新執行一遍函數,那麼咱們怎麼作怎麼作?
那就要對其進行依賴收集和觸發依賴了 首先看track收集依賴
let targetMap: WeakMap<object, Map<strNumSym, Set<Effect>>> = new WeakMap()
export const track = <T extends object>(target: T, type: effectTypeGet, key: strNumSym) => {
if (activeEffect === undefined) return
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
}
複製代碼
咱們先看一下effect的用法
const state = reactive({
a:3
})
effect(()=>{
console.log(state.a)
})
state.a = 4;
複製代碼
執行上面這段代碼會打印3和4
咱們能夠看到咱們讀了state.a。那麼就會觸發get函數,所以咱們在get函數中添加一行代碼,就是執行track函數
function get<T extends object>(target: T, key: strNumSym, receiver: T) {
let res = Reflect.get(target, key, receiver)
track(target, 'get', key)
return isObject(target[key]) ? reactive(target[key]) : res;
}
複製代碼
track函數很簡單,無非就是收集依賴到targetMap。咱們看一下上面代碼執行後targetMap的樣子。 targetMap是一個weakMap,鍵名是target,在上例中即{a:3},值是一個map數據結構。map數據結構的鍵名是key值,也就是a,值爲一個set數據結構,其中每一項是Effect
如今咱們能夠看到執行完effect後,會根據effect函數中讀的屬性建立一個收集到的依賴targetMap
那麼下面就是觸發依賴了
咱們看過vue2原理的都知道,在set函數中觸發依賴。咱們能夠看到,我在set函數中寫的是do something,如今咱們把do something 改爲觸發依賴
if (!hasKey) {
trigger(target, 'add', key, res)
}
else if (value !== oldval) {
trigger(target, 'set', key, res)
}
複製代碼
在修改值的適合也就是我上面寫的state.a 會觸發set函數,調用trigger 函數,咱們來看一下trigger函數
export const trigger = <T extends object>(target: T,
type: effectTypeSet, key: strNumSym, value?: any) => {
let depsMap = targetMap.get(target)
if (!depsMap) return
const run = (effects: Set<Effect>) => {
effects.forEach(effect => {
effect()
}
}
if (type === 'add') {
run(depsMap.get(Array.isArray(target) ? 'length' : ''))
}
run(depsMap.get(key))
}
複製代碼
trigger函數有四個參數,分別是代理的對象,類型,代理對象的屬性,代理對象對應鍵的值
let depsMap = targetMap.get(target)
複製代碼
targetMap是收集到的依賴,這步操做很簡單,咱們如今拿到一個map數據depsMap,key是代理的屬性,value是effect Set集合 。
run方法接受一個Effect 集合,如今咱們取depsMap的key值對應對value就是一個Effect集合,注意的是,咱們在數組中,收集依賴的是length屬性。如今咱們對set集合中對Effect遍歷執行便可,這樣一從新修改值,咱們的回調函數就會再執行一遍。
export function computed(options: computedOptiobs) {
let get: Function
let set: <T extends object>(key?: T) => any
if (typeof options === 'function') {
get = options
set = () => { }
}
else {
get = options.get
set = options.set
}
let computed: GetValue
let value: any
let dirty = true
let runner = effect(get, {
lazy: true,
computed: true,
scheduler() {
if (!dirty) {
dirty = true
}
trigger(computed, 'set', 'value')
}
})
return computed = {
get value() {
if (dirty) {
value = runner()
dirty = false
track(computed, 'get', 'value')
}
return value
},
set value(val) {
set(val)
}
}
}
複製代碼
咱們你們都知道computed能夠傳一個函數也能夠傳一個對象,因此咱們先對參數進行一下判斷是函數仍是對象。你們都知道computed能夠對值進行緩存,因此咱們定義一個dirty屬性用來判斷需不須要緩存。在vue3中,咱們使用計算屬性返回一個對象,對象的value屬性是計算後的結果,和ref同樣。 set函數是用戶自定義的,咱們很少作管理。 當咱們執行以下代碼
let y = computed(()=>{
return state.a+1
})
複製代碼
當咱們執行到state.a時,就會觸發get函數,若是dirty爲false,直接返回value便可。 不然咱們讓value = runner(),同時讓dirty爲false,除此以外,咱們的computed也應該是響應式的,所以咱們也要對computed進行依賴追蹤。
runner 其實就是effect函數的返回值,和一開始相比,咱們的effect函數只是多傳入了幾個參數,也就是computed和scheduler函數,computed只是一個是不是computed的標記。
如今咱們返回去看 effect()作了什麼
export const effect = (fn: Function, options: EffectOptions = {
lazy: false
}) => {
let effect = createEffect(fn, options)
if (!options.lazy) {
effect()
}
return effect
}
複製代碼
因爲咱們的lazy爲true,能夠看到咱們只幹了一件事,就是執行createEffect函數並將其結果返回。
那麼如今就很簡單了,因此咱們的runner等於這段代碼
let effect: Effect = function effectReactive() {
if (!effectStack.includes(effect)) {
try {
effectStack.push(effect)
activeEffect = effect
return fn()
} finally {
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
}
}
複製代碼
如今執行runner 咱們發現返回的是什麼??就是fn(),也就是執行
()=>{
return state.a+1
}
複製代碼
咱們都知道,一旦咱們從新修改state.a的值,dirty要從新變成true,同時會觸發trigger函數, 所以咱們對trigger函數作一個簡單的修改。
let ComputedRunner: Set<Effect> = new Set()
let effectRunner: Set<Effect> = new Set()
const run = (effects: Set<Effect>) => {
if (effects) {
effects.forEach(effect => {
if (effect.options.computed) {
ComputedRunner.add(effect)
} else {
effectRunner.add(effect)
}
})
}
}
if (type === 'add') {
run(depsMap.get(Array.isArray(target) ? 'length' : ''))
}
run(depsMap.get(key))
effectRunner.forEach(effect => {
if (effect.options.scheduler) {
effect.options.scheduler()
}
})
effectRunner.forEach(effect => {
effect()
})
複製代碼
很簡單,咱們將computed和普通effect進行分組,而後再分別遍歷執行便可。注意的是,computed咱們執行的是scheduler函數。
schedule函數很簡單,咱們假設咱們修改了state.a =4 。如今修改了值,咱們就不能繼續取緩存中的值,因此先讓dirty 變成true。注意的是,咱們也能夠在effect函數中讀取computed的值,computed變了,effect也要從新執行,因此咱們還要對computed進行一次依賴觸發。
首先咱們先看一下Vuex4.0怎麼使用
import Vuex from '../vuex'
export default Vuex.createStore({
state: {
},
getters: {
},
mutations: {
},
actions: {
},
modules: {
a: {}
})
複製代碼
和以往不同的是,在4.0中咱們是使用vuex.createStore方法建立一個倉庫
而後在使用的地方調用vuex.useStore方法使用便可
const {state,commit,dispatch,getters,actions} = Vuex.useStore()
複製代碼
而後其餘使用方法和vue2基本一致,在vuex4.0中,是基於provide和inject實現的,我實現了其最基本功能,加上ts接口差很少100多行代碼。
class Store {
install = (app: App) => {
let _this = this
app.provide('store', _this)
}
}
const createStore = <T extends StoreOpts>(opts: T) => {
return new Store(opts)
}
const useStore = (): Store => {
return inject('store') as Store
}
}
複製代碼
首先createStore至關簡單,就是new 了一下Store。
useStore也至關簡單,就是注入了一下store,咱們在install方法Provide('store',_this)),源碼中是使用Symbol代替字符串,這裏爲了簡單使用字符串。
咱們先看一下接口
interface _ObjectFun {
[key: string]: ((...params: any[]) => any)[]
}
interface _ObjectGetters {
[key: string]: (getter: object) => any
}
interface StoreOpts {
getters?: { [key: string]: Function }
state?: { [key: string]: any }
mutations?: { [key: string]: Function }
actions?: { [key: string]: Function }
modules?: { [key: string]: StoreOpts }
}
interface ModulesRoot {
[key: string]: Modules
}
interface Modules {
_raw: StoreOpts,
state: object,
_children: ModulesRoot
}
複製代碼
如今咱們來一步步看Store類作了什麼操做
class Store {
getters: _ObjectGetters
state: object
mutations: _ObjectFun
actions: _ObjectFun
modules: collectionModules
constructor(opts: StoreOpts) {
if (opts === void 0) opts = {}
this.getters = Object.create(null)
this.mutations = Object.create(null)
this.actions = Object.create(null)
this.modules = new collectionModules(opts)
this.state = reactive(opts.state)
installModules(this, this.state, [], this.modules._root)
}
commit = (type: string, ...params: any[]) => {
}
dispatch = (type: string, ...params: any[]) => {
}
install = (app: App) => {
let _this = this
app.provide('store', _this)
}
}
複製代碼
Store類首先先初始化state,getters等,注意state使用reactive包裹一下使其成爲響應式,
咱們都知道,使用vuex中modules下的a倉庫中的b數據不是 store.modules.a.state.b 而是store.state.a.b這樣使用,所以咱們對modules要作一下其餘操做。
咱們來看一下 collectionModules類
class collectionModules {
_root: Modules
constructor(opts: StoreOpts) {
this._root = {
_raw: {},
state: {},
_children: {}
}
this.register([], opts)
}
register(path: string[], rootModules: StoreOpts) {
let newModules = {
_raw: rootModules,
state: rootModules.state || Object.create(null),
_children: Object.create(null)
}
if (path.length === 0) {
this._root = newModules
} else {
let parent = path.slice(0, -1).reduce((root: Modules, current: string): Modules => {
return root._children[current]
}, this._root)
parent._children[path[path.length - 1]] = newModules
}
if (rootModules.modules) {
Object.keys(rootModules.modules).forEach((moduleName: string) => {
this.register(path.concat(moduleName), rootModules.modules[moduleName])
})
}
}
}
複製代碼
collectionModules 類最重要的是調用register函數,path和rootModules。
path其實就是一個保留父子關係的數組。如path爲['a','b','c']則表明模塊a下有模塊b,模塊b下有模塊c若是未空數組,則代碼沒有模塊。rootModules就是當前模塊小的一個小倉庫。
newModules有三個屬性,_raw是當前小倉庫,state是當前倉庫的state,children是一個對象,鍵名爲模塊名,鍵值爲小倉庫。
咱們慢慢分析,若是咱們的store沒有模塊,那麼register函數只作了一件事,那就是初始化_root屬性。毫無疑問,咱們接下來就是將全部模塊遞歸插入到_children屬性中。
Object.keys(rootModules.modules).forEach((moduleName: string) => {
this.register(path.concat(moduleName), rootModules.modules[moduleName])
})
複製代碼
遍歷對象中的模塊,遞歸調用register()函數,構建path數組父子關係並將模塊中的小倉庫傳入進去。 咱們如今來看else部分,咱們有了path保存了父子關係,那麼咱們能夠很簡單的照到其父親模塊名。 而後將小倉庫做爲鍵值,倉庫名做爲鍵名加入到父modules下的_children對象中便可。
如今咱們只是收集好了模塊之間的關係。
咱們都知道對於getters,不論是那個模塊下的getters,咱們只要使用getters.xxx。而不是getters.moduleName.xxx。當咱們dispath或者commit一個方法,全部模塊下的同名方法都會執行。咱們如今來看最後一個核心方法installModules
咱們在construction中調用
installModules(this, this.state, [], this.modules._root)
複製代碼
前面兩個參數很好理解,就是store,和其state。而且在後面的遞歸調用中這兩個參數一直是同一個,也就是說,是整個Store和最外層的那個state。第三個參數是path和收集模塊中的path一個做用, this.modules._root就是咱們收集到的沒一個模塊,在日後的遞歸調用中是咱們收集到的_children中的一項。先看代碼。
const installModules = (store: Store, state: object, path: string[], rootModules: Modules) => {
if (path.length > 0) {
let parent = path.slice(0, -1).reduce((state, current): object => {
return state[current]
}, state)
parent[path[path.length - 1]] = rootModules.state
}
let { getters, mutations, actions } = rootModules._raw
getters && (Object.keys(getters).forEach((getter: string) => {
Object.defineProperty(store.getters, getter, {
get() {
return getters[getter](rootModules.state)
}
})
}))
mutations && (Object.keys(mutations).forEach(mutation => {
let arr = store.mutations[mutation] || (store.mutations[mutation] = [])
arr.push((...params: any[]) => { mutations[mutation](rootModules.state, ...params) })
}))
actions && (Object.keys(actions).forEach(action => {
let arr = store.actions[action] || (store.actions[action] = [])
arr.push((...params: any[]) => { actions[action](store, ...params) })
}))
if (rootModules._children) {
Object.keys(rootModules._children).forEach(moduleName => {
installModules(store, state, path.concat(moduleName), rootModules._children[moduleName])
})
}
}
複製代碼
若是沒有子模塊,state不須要作任何操做。 getters模塊也相對簡單,就是遍歷getters將其餘getters中的數據劫持到最外層到getters上。
mutations 和 actions幾乎同樣,就是將全部模塊中的同名函數放到一個隊列中,因此咱們的store.mutations和store.actions的每一項都轉化爲一個數組。數組的每一項是一個函數。
咱們先把這個放在一邊,咱們來看有modules的狀況,將子模塊的state合併到最外層到state上。 也很簡單,咱們找到父模塊的state給其加一個鍵名爲模塊名,鍵值爲模塊的state便可。 如今,咱們調用Vuex.commit('actionName')和vuex.dispatch('mutationname')便可。
如今咱們來補充完這兩個函數
commit = (type: string, ...params: any[]) => {
this.mutations[type].forEach(callback => {
callback(...params)
})
}
dispatch = (type: string, ...params: any[]) => {
this.actions[type].forEach(callback => {
callback(...params)
})
}
複製代碼
咱們剛剛說了,咱們中的隊列每一項都是一個方法,如今咱們直接遍歷隊列進行調用函數便可。
每個函數都是咱們本身寫的mutations和actions。注意的是mutation中的函數第一個參數是當前模塊的state,而actions中的函數第一個參數是store。
如今咱們就完成了一個簡單的vuex。完整的源代碼你們能夠去github上自取
你們都知道,vue3可使用腳手架和vite兩種方式建立,說真的,vite的構建速度和腳手架使用webpack構建速度不是一個量級的。在我剛剛使用vue3時,vite還不支持less等預處理語言,不過如今vite對less基本開箱即用,只要安裝less和less-loader便可,不用再進行其餘配置。vite簡單來看就是使用koa2搭建的一個服務器。
首先咱們看index.html
<script type="module" src="/src/main.js"></script>
複製代碼
咱們發現script 中type屬性等於 module。使用es6模塊,天生按需加載。
咱們都知道,module模塊,只支持./ ../ /開頭的引入方式
可是咱們 import { createApp ,provide} from 'vue'
並非這三種之一的開頭啊,那麼在vite是如何加載的呢?
如今咱們打開瀏覽器的network 面板,咱們發現咱們的請求變成了 @modules/vue.js
,這時候咱們就恍然大悟了,咱們在咱們請求的模塊上加上/@modules便可,這個時候他就是/開頭的了。咱們於沒有./ / ../的模塊自動加上 /@modules便可,而後咱們再根據必定的映射關係找到對應的模塊。
好比咱們請求的是 'vue' ,這個時候咱們就會轉變成去請求 '@modules/vue' 而後 有一個對象對應的鍵名'vue' 對應的鍵值是 './node_modules/@vue/compiler-dom/dist/compiler-dom.esm-bundler.js'
如今咱們就能夠很快樂的和之前同樣請求非/ ./ ../ 開頭的文件了,可是咱們的.vue 文件又如何請求呢??? 咱們首先看一下network中請求的app.vue變成了什麼
import HelloWorld2 from "/src/components/HelloWorld.vue";
import store from "/src/vuex/index.ts";
const __script = {
name: "App",
components: {
HelloWorld: HelloWorld2
},
setup() {
const {useStore} = store;
console.log(useStore());
const {state, commit, dispatch, getters, actions} = useStore();
const change = () => {
commit("increment", 4);
};
return {
state,
getters,
change
};
}
};
import { render as __render } from "/src/App.vue?type=template"
__script.render = __render
__script.__hmrId = "/src/App.vue"
__script.__file = "/Users/huanglihao/learn/vuex4.0/vuex/src/App.vue"
export default __script
複製代碼
主要作了兩個操做:1 將script標籤中的內容放入__script變量中並導出。2:將請求內容加上type === 'template'
將.vue 文件改寫成上面那種很簡單,只要作適當的正則和字符串就能寫出來。
對於 模版咱們作以下操做
if (ctx.query.type === 'template') {
ctx.type = "js";
let content = descriptor.template.content
const { code } = compileTemplate({ source: content })
ctx.body = code
}
複製代碼
調用vue3中自帶的compileTemplate函數將template轉化爲虛擬dom便可
固然除了這些,還有不少關鍵的代碼,好比經過建立webSocket服務進行熱更新操做。 其主要原理監聽整個文件夾是否有內容發生改變,而後記錄發生改變的文件,若是有文件發生的話則經過websocket進行事實通訊後調用locatio.reload方法進行頁面更新。
結語:vue3中還有不少還須要琢磨的東西,好比vue-router,createApp函數,深刻研究vite原理等。特別是vue3 中的vue-router感受和vue2中區別有點大,筆者嘗試用一天時間去寫一個簡單版的,竟然寫出了bug,簡直菜哭了。emmm,而後後面就幹其餘事情去了,而後就沒有再嘗試了。之後有時間會再嘗試一下的。下一篇會簡單介紹一下拿vue3寫的一個小項目,但願你們能點一個小贊。