vue3 從入門到實戰(中)

前言

本篇是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')
複製代碼

type/index.ts 文件定義了一些類型

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
複製代碼

interface.ts

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
複製代碼

reactive簡單原理

reactive.ts文件

說明:要求瞭解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屬性。對於這兩個分別作了什麼操做稍後再分析。

咱們先簡單分析一下代碼,就會發現只有在讀值的時候纔會進行遞歸操做使數據變成響應式,而不是一上來就深度遞歸使全部數據進行響應式。

Effcet簡單原理。

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遍歷執行便可,這樣一從新修改值,咱們的回調函數就會再執行一遍。

computed簡單原理

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 簡易源碼

首先咱們先看一下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上自取

vite 簡要分析。

你們都知道,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寫的一個小項目,但願你們能點一個小贊。

相關文章
相關標籤/搜索