前言#
昨天我们刚配置完了调试环境,今天我们来开始学习源码。
createPinia#
在开始分析之前,我们要回顾下怎么使用pinia,这样有助于我们分析源码。
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')和vuex类似,我们都需要注册这个插件到vue中。
那么这个函数是在哪呢?
我们先找入口,包的入口都会在package.json中声明(注意这是packages/pinia的package.json,而不是根目录的package.json):
"main": "index.js",
"module": "dist/pinia.mjs",那么我们直接进入packages/pinia/index.ts中:
export { createPinia, disposePinia } from './createPinia'测试用例#
这个我们就直接用官方自带的测试用例即可,因为基本所有测试用例都需要初始化pinia才能使用,所以随便找一个即可。
rootState.spec.ts文件中:
it('works with no stores', () => {
expect(createPinia().state.value).toEqual({})
})然后我们就直接打上断点开始调试旅程
源码#
import { Pinia, PiniaPlugin, setActivePinia, piniaSymbol } from './rootStore'
import { ref, App, markRaw, effectScope, isVue2, Ref } from 'vue-demi'
import { registerPiniaDevtools, devtoolsPlugin } from './devtools'
import { IS_CLIENT } from './env'
import { StateTree, StoreGeneric } from './types'
/**
* Creates a Pinia instance to be used by the application
*/
export function createPinia(): Pinia {
const scope = effectScope(true)
// NOTE: here we could check the window object for a state and directly set it
// if there is anything like it with Vue 3 SSR
const state = scope.run<Ref<Record<string, StateTree>>>(() =>
ref<Record<string, StateTree>>({})
)!
let _p: Pinia['_p'] = []
// plugins added before calling app.use(pinia)
let toBeInstalled: PiniaPlugin[] = []
const pinia: Pinia = markRaw({
install(app: App) {
// this allows calling useStore() outside of a component setup after
// installing pinia's plugin
setActivePinia(pinia)
if (!isVue2) {
pinia._a = app
app.provide(piniaSymbol, pinia)
app.config.globalProperties.$pinia = pinia
/* istanbul ignore else */
if (__USE_DEVTOOLS__ && IS_CLIENT) {
registerPiniaDevtools(app, pinia)
}
toBeInstalled.forEach((plugin) => _p.push(plugin))
toBeInstalled = []
}
},
use(plugin) {
if (!this._a && !isVue2) {
toBeInstalled.push(plugin)
} else {
_p.push(plugin)
}
return this
},
_p,
// it's actually undefined here
// @ts-expect-error
_a: null,
_e: scope,
_s: new Map<string, StoreGeneric>(),
state,
})
// pinia devtools rely on dev only features so they cannot be forced unless
// the dev build of Vue is used. Avoid old browsers like IE11.
if (__USE_DEVTOOLS__ && typeof Proxy !== 'undefined') {
pinia.use(devtoolsPlugin)
}
return pinia
}代码非常简短,我们来一步一步看下什么情况
vue-demi:vue官方提供的一个工具,用来方便兼容vue2和vue3。effectScope:vue内部组件实例收集响应式effects的方法(比如computed和watch/watchEffect),往期文章中貌似有介绍过,组件实例释放掉之后它收集的数据也会被dispose掉,这里也用上,是用来搭配disposePinia的:
export function disposePinia(pinia: Pinia) {
pinia._e.stop()
pinia._s.clear()
pinia._p.splice(0)
pinia.state.value = {}
// @ts-expect-error: non valid
pinia._a = null
}-
scope.run:这里就是收集effect的方法。这里传入一个空对象暂时不清楚是用于做什么,可能是初始化,又或者看注释NOTE: here we could check the window object for a state and directly set it if there is anything like it with Vue 3 SSR
貌似是用来
check环境是否正常的。后面如果有其它解释再回来补充 -
_p: PiniaPlugin[],在pinia注册到vue之前先注册它自己的插件 -
markRaw:标记这个数据不要被代理,《我为自己带盐》,什么远古烂梗 -
{ install(app) {} }:这是vue插件编写的格式:Plugins | Vue.js (vuejs.org)。 -
setActivePinia:export const setActivePinia: _SetActivePinia = (pinia) => (activePinia = pinia)将自己放置到全局唯一变量
activePinia中,声明当前激活的pinia实例是它。后面我们还会遇到这个点的。 -
{ use(plugin) {} }:这个则是pinia自己注册依赖的方法,比如:import { createPinia } from 'pinia' // add a property named `secret` to every store that is created // after this plugin is installed this could be in a different file function SecretPiniaPlugin() { return { secret: 'the cake is a lie' } } const pinia = createPinia() // give the plugin to pinia pinia.use(SecretPiniaPlugin) // in another file const store = useStore() store.secret // 'the cake is a lie' -
_a:指向vue根实例,也就是install传入的app。 -
app.provide,它和我们平时使用的provide/inject是不一样的(基础逻辑一样),它是createAppApi.provide,换句话说就是这货是根节点自己的provide。关于provide/inject源码逻辑以及根组件provide和子组件实例provide差别这一部分我给放到其它里了,不影响当前分析节奏。 -
app.config.globalProperties.$pinia = pinia:方便访问。 -
toBeInstalled.forEach((plugin) => _p.push(plugin)):注意,此时才正式将收集的插件注入到pinia中,毕竟不能偷跑。
最后return pinia。
这就是createPinia的逻辑。
整理下:
- 收集响应式
effect,如果当前pinia实例被释放,那么这些被收集的effect(computed、watch/WtachEffect等)也会跟着释放掉 - 初始化
state,并赋值给pinia.state。 use中收集插件,等待install时注入到pinia._p中install过程中将自己注入到vue根组件实例中,这样所有子组件都可以访问到。
其它#
provide/inject里的provide源码逻辑#
虽然根组件实例的provide和一般组件实例的provide两者逻辑基本都是用一样,不过还是有些微区别。
我们来看下两者的区别:
根节点的packages\runtime-core\src\apiCreateApp.ts:
{
// ...
provide(key, value) {
if (__DEV__ && (key as string | symbol) in context.provides) {
warn(
`App already provides property with key "${String(key)}". ` +
`It will be overwritten with the new value.`
)
}
context.provides[key as string | symbol] = value
return app
}
// ...
}很简单,就是将它赋值给provides,这个provides是provide/inject的核心,祖组件数据传递给孙组件就是通过provides。
然后我们再看下一般的packages\runtime-core\src\apiInject.ts:
export function provide<T>(key: InjectionKey<T> | string | number, value: T) {
if (!currentInstance) {
if (__DEV__) {
warn(`provide() can only be used inside setup().`)
}
} else {
let provides = currentInstance.provides
const parentProvides =
currentInstance.parent && currentInstance.parent.provides
if (parentProvides === provides) {
provides = currentInstance.provides = Object.create(parentProvides)
}
// TS doesn't allow symbol as index type
provides[key as string] = value
}
}两者唯一的差别在于provide从哪来,一般组件实例的是从父组件中继承来的,而根组件因为是孤儿,所以它需要调整为从外部注入。
为什么这里说的是继承呢?
因为组件实例创建的时候默认就是从父组件中继承的:
packages\runtime-core\src\component.ts(组件实例初始化)
const instance: ComponentInternalInstance = {
// ...
provides: parent ? parent.provides : Object.create(appContext.provides),
// ...可以看到直接就从父组件那里继承了下来。
如果组件实例自己在setup函数中执行provide([xxxx]),那么就会触发上面的逻辑,将新的数据插入到provides中(注意,只有这个组件实例的子孙组件实例才能访问的到)
至于组件实例是从什么时候合并父组件provide的,那自然就是执行provide函数的时候,换句话说就是执行setup函数的时候。
然后我们看下inject:
export function inject<T>(key: InjectionKey<T> | string): T | undefined
export function inject<T>(
key: InjectionKey<T> | string,
defaultValue: T,
treatDefaultAsFactory?: false
): T
export function inject<T>(
key: InjectionKey<T> | string,
defaultValue: T | (() => T),
treatDefaultAsFactory: true
): T
export function inject(
key: InjectionKey<any> | string,
defaultValue?: unknown,
treatDefaultAsFactory = false
) {
// fallback to `currentRenderingInstance` so that this can be called in
// a functional component
const instance = currentInstance || currentRenderingInstance
if (instance) {
// #2400
// to support `app.use` plugins,
// fallback to appContext's `provides` if the instance is at root
const provides =
instance.parent == null
? instance.vnode.appContext && instance.vnode.appContext.provides
: instance.parent.provides
if (provides && (key as string | symbol) in provides) {
// TS doesn't allow symbol as index type
return provides[key as string]
} else if (arguments.length > 1) {
return treatDefaultAsFactory && isFunction(defaultValue)
? defaultValue.call(instance.proxy)
: defaultValue
} else if (__DEV__) {
warn(`injection "${String(key)}" not found.`)
}
} else if (__DEV__) {
warn(`inject() can only be used inside setup() or functional components.`)
}
}可以看到就是从当前的实例provides中去拿而已。
执行的时机同理于provide函数(都是在setup执行时)。
这里有俩关键变量:currentInstance、currentRenderingInstance
我这里补充下相关的逻辑,这俩代表着当前的组件实例是谁和当前正在渲染的组件实例是谁。
注意,他俩都是全局唯一的变量,同一时间只能有一个(组件实例“所有权”,学rust学的)。
currentInstance#
packages\runtime-core\src\component.ts
export let currentInstance: ComponentInternalInstance | null = null
export const getCurrentInstance: () => ComponentInternalInstance | null = () =>
currentInstance || currentRenderingInstance
export const setCurrentInstance = (instance: ComponentInternalInstance) => {
currentInstance = instance
instance.scope.on()
}
export const unsetCurrentInstance = () => {
currentInstance && currentInstance.scope.off()
currentInstance = null
}全局唯一的变量,然后我们看下set是在组件实例创建的什么时候,这一点影响我们分析inject是从自己拿的还是从父组件拿的(也就是说执行inject的时候,currentInstance指向父组件实例还是指向自己组件实例)。
在同个文件中的setupStatefulComponent函数中(实际执行setup函数的位置)
// ...
const { setup } = Component
if (setup) {
const setupContext = (instance.setupContext =
setup.length >= 1 ? createSetupContext(instance) : null)
setCurrentInstance(instance)
pauseTracking()
const setupResult = callWithErrorHandling(
setup,
instance,
ErrorCodes.SETUP_FUNCTION,
[__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
)
resetTracking()
unsetCurrentInstance()
// ...可以看到setCurrentInstance是在setup函数执行之前,那就意味着执行provide/inject的时候currentInstance都指向自己,这也符合我们前面的分析。
另外为啥要搞一个全局唯一变量指向当前创建的组件实例呢?
因为有些逻辑参数不合适传来传去,所以放全局省事,反正单线程不会有竞争的问题。
额外补充下响应式数据和组件实例是如何挂钩的,也是通过全局唯一变量的方式,组件实例创建时会有一个
export let activeEffect: ReactiveEffect | undefined组件实例收集和管理依赖以及触发自己更新的核心:ReactiveEffect,具体可以看往期文章,提到过挺多次了。
组件实例创建时会创建一个ReactiveEffect实例,然后通过全局唯一的方式和setup里的响应式数据挂钩(被收集),这样数据更新会触发ReactiveEffect.fn,fn中组件实例将自己放入任务调度队列等待重新渲染。
currentRenderingInstance#
和currentInstance差不多,都是指向当前渲染的组件实例。
这里要补充下渲染逻辑,父组件render function渲染到子组件的时候,就会退出当前渲染,等待子组件实例渲染和初始化完毕。
从时间上来说,父组件先于子组件渲染,但是子组件早于父组件渲染完毕。
可以想象成一个单向栈,渲染遇到子组件 -> 入栈 -> 子组件渲染完毕 -> 出栈 -> 组件继续渲染。
这也就是为什么要有currentInstance和currentRenderingInstance的原因,因为组件实例化和渲染这整个过程中不一定都是指向自己的,遇到子组件就会临时让给子组件实例。
扯远了,我们看下currentRenderingInstance的相关逻辑
packages\runtime-core\src\componentRenderUtils.ts(执行render function的时候,生产环境render是包含在setup里的,开发模式则是单独拎出来方便hmr)
function renderComponentRoot (... ctx = currentRenderingInstance) {
const prev = setCurrentRenderingInstance(instance)
// ...
result = normalizeVNode(
render!.call(
proxyToUse,
proxyToUse!,
renderCache,
props,
setupState,
data,
ctx
)
)
// ...
setCurrentRenderingInstance(prev)
return result
}这里就是执行render function的地方,此时会切换currentRenderingInstance指向子组件实例,子组件实例渲染完毕才会返回给父组件实例。
注意,上面提到的创建实例的逻辑都是在patch执行过程中的。具体流程可以看往期文章。
ok到这就说完了这块。
plugin install#
去到vue源码文件夹中:
packages\runtime-core\src\apiCreateApp.ts
const app = (context.app = {
// ...
use(plugin: Plugin, ...options: any[]) {
if (installedPlugins.has(plugin)) {
__DEV__ && warn(`Plugin has already been applied to target app.`)
} else if (plugin && isFunction(plugin.install)) {
installedPlugins.add(plugin)
plugin.install(app, ...options)
} else if (isFunction(plugin)) {
installedPlugins.add(plugin)
plugin(app, ...options)
} else if (__DEV__) {
warn(
`A plugin must either be a function or an object with an "install" ` +
`function.`
)
}
return app
},
// ...
})
这也就是为什么我们插件实现规范得写install的原因。
总结#
额,今天有些偏题,不过补充点额外知识也是不错的~
