为什么要看源码
我理解有以下6点:
- 学习编码技巧
- 学习设计思想
- 提升阅读代码速度
- 工作中得心应手,减少心智负担,提升效率
- 对技术的使用边界更清晰,在项目前评估技术方案有理有据
- 团队成员在使用过程中出现问题,帮助队友,更重要的提升自己,升职加薪(专业奋斗共赢,企业文化传销骨干)
上面除了搞钱都是废话,既然吃这碗饭了,能多赚点就多赚点不丢人
准备工作
了解vue3,js,ts
pinia是什么
pinia使用方法
开始
vue3使用pinia
js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
app.mount('#app')
createPinia()做了什么
packages/src/createPinia.ts
js
export function createPinia() {
const scope = effectScope(true)
const state = scope.run(() => ref({}))
let _p = []
let toBeInstalled = []
const pinia = markRaw({
install(app){ ... },
use(plugin) { ... },
_p,
_a: null,
_e: scope,
_s: new Map(),
state,
})
}
以上有2个方法需要提前了解effectScope,markRaw
effectScope
effectScope的作用
创建一个 effect 作用域,可以捕获其中所创建的响应式副作用 (即计算属性和侦听器),这样捕获到的副作用可以一起处理
ts
effectScope(detached: boolean)
detached: 是否创建一个分离的效果
effectScope会返回一个对象 对象有两个属性方法run
、stop
html
<script setup lang="ts">
import { effectScope, ref, computed, watch } from 'vue'
const scoped = effectScope(true)
const aaa = ref(0)
scoped.run(() => {
console.log('run')
const bbb = computed(() => aaa.value * 2)
watch(bbb, () => {
console.log('bbb变了:', bbb.value)
})
})
function aaaClick() {
console.log('aaaClick')
aaa.value += 1
}
function stop() {
console.log('stop')
scoped.stop()
}
</script>
<template>
<button @click="aaaClick">给aaa+1</button>
<button @click="stop">停止</button>
</template>
我们把示例代码启动一下,可以看到界面并且打印了run说明scoped.run(fn)会立即执行fn
这个回掉函数
当我点击2次给aaa+1
的按钮,可以看到控制台打印了bbb变了: 2
和bbb变了: 4
当我再去点击停止,然后连点3次发现bbb变了不会去打印了
这样我们就了解了effectScope
回到源码我们就知道state是个ref({})
,其实就是创建一个局部的响应对象
markRaw
markRaw的作用
将一个对象标记为不可被转为代理。返回该对象本身
html
<script setup lang="ts">
import { markRaw, reactive, isReactive } from 'vue'
const aaa = {
a: 1,
b: 2
}
const bbb = markRaw(aaa)
const ccc = reactive(bbb)
console.log(ccc)
const ddd = reactive({
a: 1,
b: 2
})
console.log(ddd)
console.log(isReactive(ddd))
console.log(isReactive(ccc))
</script>
可以明显看到打印ccc是没有proxy
代理的,ddd有proxy
这样我们就了解了markRaw
我们再回到createPinia,就理解createPinia其实就是生成pinia对象并返回
app.use(pinia)
其实就是调用pinia.install(app)
js
install(app) {
setActivePinia(pinia)
pinia._a = app
app.provide(piniaSymbol, pinia)
app.config.globalProperties.$pinia = pinia
toBeInstalled.forEach((plugin) => _p.push(plugin))
toBeInstalled = []
}
piniaSymbol其实就是Symbol()
,app.provide(piniaSymbol, pinia)给整个应用层面提供pinia依赖
,这样我们在全局就可以拿到pinia对象
app.config.globalProperties.$pinia = pinia
,在组件实例添加$pinia数学即pinia对象
toBeInstalled循环把plugin放到_p数组,然后清空toBeInstalled
defineStore做了什么
packages/src/store.ts
项目中使用示例
js
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})
html
<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'
const { count, increment } = useCounterStore()
</script>
<template>
<div>{{ count }}</div>
<button @click="increment">点击</button>
</template>
js
export function defineStore(idOrOptions, setup, setupOptions) {
let id = idOrOptions
let options = setupOptions
function useStore(pinia){...}
useStore.$id = id
return useStore
}
defineStore其实就是返回useStore函数,useCounterStore == useStore
useStore
js
function useStore(pinia, hot) {
createSetupStore(id, setup, options, pinia)
// pinia._s就是map对象
const store = pinia._s.get(id)
return store
}
createSetupStore
js
function createSetupStore($id, setup, options, pinia) {
let scope
const initialState = pinia.state.value[$id]
if(!initialState) {
pinia.state.value[$id] = {}
}
function $patch(partialStateOrMutator){...}
function $dispose() {...}
function wrapAction(name, action) {...}
const partialStore = {
_p: pinia,
$id,
$patch,
$dispose
}
const store = reactive(partialStore)
pinia._s.set($id, store)
// setupStore就是{ count, doubleCount, increment }
const setupStore = pinia._e.run(() => {
scope = effectScope()
return (() => scope.run(setup))()
})
for (const key in setupStore) {
const prop = setupStore[key]
if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) {
pinia.state.value[$id][key] = prop
} else if (typeof prop === 'function') {
const actionValue = wrapAction(key, prop)
// 覆盖increment, actionValue其实就是() => increment.apply(null, ...args)
// wrapAction把原来increment做一层前置处理
setupStore[key] = actionValue
}
}
// 合并store, setupStore对象
assign(store, setupStore)
// toRaw返回代理对象的原始对象,reactive({ a: 1, b: 2}), toRaw一下返回 {a:1, b:2}
assign(toRaw(store), setupStore)
return store
}
其实createSetupStore就是生成store响应对象,里面有一些方法和{ count, doubleCount, increment }
, pinia._s.set($id, store)放入到map中
回到useStore返回就是这个store reactive对象,所以执行useCounterStore()
返回的就是store reactive对象
加餐
分析了defineStore的源码,大家有没有发现文章使用useCounterStore()
的问题
html
<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'
const { count, increment } = useCounterStore()
</script>
<template>
<div>{{ count }}</div>
<button @click="increment">点击</button>
</template>
就是无论点击执行increment多少次,count在页面中始终没变,我们分析了源码,这个问题就很好解释了, 看下面的代码
html
<script setup lang="ts">
import { ref, reactive, watch } from 'vue'
const aaa = ref(0)
const bbb = reactive({
ccc: aaa
})
const { ccc } = bbb
watch(aaa, (newVal) => {
console.log('aaa:', newVal)
})
function onClick() {
aaa.value += 1
console.log(aaa.value)
}
</script>
<template>
<div>
{{ aaa }}
</div>
<div>
{{ ccc }}
</div>
<button @click="onClick">点击</button>
</template>
我点击按钮3次,看输出的结果
ccc一直没有变还是0, 问题就在于useCounterStore()返回的是reactive对象,这个问题官方也有解释,reactive对象对解构不友好官方解释
总结
我们学习了createpinia
是做pinia插件的准备工作返回一个pinia对象,同时学习了effectScope
是给响应式副作用(即计算属性和侦听器)打包成一个作用域一起处理,还学习了markRaw
让对象返回一个新的对象并且不能被reactive
并且学习了defineStore本质是返回一个reactive对象,对象里面有一些源码内置的方法和属性同时合并了我们自己传入的回掉函数返回的属性和方法