本篇是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写的一个小项目,但愿你们能点一个小赞。