昨天我们刚配置完了调试环境,今天我们来开始学习源码。
在开始分析之前,我们要回顾下怎么使用pinia
,这样有助于我们分析源码。
和vuex
类似,我们都需要注册这个插件到vue
中。
那么这个函数是在哪呢?
我们先找入口,包的入口都会在package.json
中声明(注意这是packages/pinia
的package.json
,而不是根目录的package.json
):
那么我们直接进入packages/pinia/index.ts
中:
这个我们就直接用官方自带的测试用例即可,因为基本所有测试用例都需要初始化pinia
才能使用,所以随便找一个即可。
rootState.spec.ts
文件中:
然后我们就直接打上断点开始调试旅程
代码非常简短,我们来一步一步看下什么情况
vue-demi
:vue
官方提供的一个工具,用来方便兼容vue2
和vue3
。effectScope
:vue
内部组件实例收集响应式effects
的方法(比如computed
和watch/watchEffect
),往期文章中貌似有介绍过,组件实例释放掉之后它收集的数据也会被dispose
掉,这里也用上,是用来搭配disposePinia
的: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
:activePinia
中,声明当前激活的pinia实例
是它。后面我们还会遇到这个点的。{ use(plugin) {} }
:这个则是pinia
自己注册依赖的方法,比如:_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
和一般组件实例的provide
两者逻辑基本都是用一样,不过还是有些微区别。
我们来看下两者的区别:
根节点的packages\runtime-core\src\apiCreateApp.ts
:
很简单,就是将它赋值给provides
,这个provides
是provide/inject
的核心,祖组件数据传递给孙组件就是通过provides
。
然后我们再看下一般的packages\runtime-core\src\apiInject.ts
:
两者唯一的差别在于provide
从哪来,一般组件实例的是从父组件中继承
来的,而根组件因为是孤儿
,所以它需要调整为从外部注入。
为什么这里说的是继承
呢?
因为组件实例创建的时候默认就是从父组件中继承的:
packages\runtime-core\src\component.ts
(组件实例初始化)
可以看到直接就从父组件那里继承了下来。
如果组件实例自己在setup
函数中执行provide([xxxx])
,那么就会触发上面的逻辑,将新的数据插入到provides
中(注意,只有这个组件实例的子孙组件实例才能访问的到)
至于组件实例是从什么时候
合并父组件provide
的,那自然就是执行provide
函数的时候,换句话说就是执行setup
函数的时候。
然后我们看下inject
:
可以看到就是从当前的实例provides
中去拿而已。
执行的时机同理于provide
函数(都是在setup执行时)。
这里有俩关键变量:currentInstance、currentRenderingInstance
我这里补充下相关的逻辑,这俩代表着当前的组件实例
是谁和当前正在渲染的组件实例
是谁。
注意,他俩都是全局唯一的变量
,同一时间只能有一个(组件实例“所有权”,学rust学的)。
packages\runtime-core\src\component.ts
全局唯一的变量,然后我们看下set
是在组件实例创建的什么时候,这一点影响我们分析inject
是从自己拿的还是从父组件拿的(也就是说执行inject
的时候,currentInstance
指向父组件实例还是指向自己组件实例)。
在同个文件中的setupStatefulComponent
函数中(实际执行setup
函数的位置)
可以看到setCurrentInstance
是在setup
函数执行之前,那就意味着执行provide/inject
的时候currentInstance
都指向自己,这也符合我们前面的分析。
另外为啥要搞一个全局唯一变量指向当前创建的组件实例呢?
因为有些逻辑参数不合适传来传去,所以放全局省事,反正单线程不会有竞争
的问题。
额外补充下响应式数据和组件实例是如何挂钩的,也是通过全局唯一变量的方式,组件实例创建时会有一个
组件实例收集和管理依赖以及触发自己更新的核心:ReactiveEffect
,具体可以看往期文章,提到过挺多次了。
组件实例创建时会创建一个ReactiveEffect
实例,然后通过全局唯一的方式和setup
里的响应式数据挂钩(被收集),这样数据更新会触发ReactiveEffect.fn
,fn
中组件实例将自己放入任务调度队列
等待重新渲染。
和currentInstance
差不多,都是指向当前渲染的组件实例。
这里要补充下渲染逻辑,父组件render function
渲染到子组件的时候,就会退出当前渲染,等待子组件实例渲染和初始化完毕。
从时间上来说,父组件先于子组件渲染,但是子组件早于父组件渲染完毕。
可以想象成一个单向栈
,渲染遇到子组件 -> 入栈 -> 子组件渲染完毕 -> 出栈 -> 组件继续渲染。
这也就是为什么要有currentInstance
和currentRenderingInstance
的原因,因为组件实例化和渲染这整个过程中不一定都是指向自己的,遇到子组件就会临时让给子组件实例。
扯远了,我们看下currentRenderingInstance
的相关逻辑
packages\runtime-core\src\componentRenderUtils.ts
(执行render function
的时候,生产环境render
是包含在setup
里的,开发模式则是单独拎出来方便hmr
)
这里就是执行render function
的地方,此时会切换currentRenderingInstance
指向子组件实例,子组件实例渲染完毕才会返回给父组件实例。
注意,上面提到的创建实例的逻辑都是在patch
执行过程中的。具体流程可以看往期文章。
ok到这就说完了这块。
去到vue
源码文件夹中:
packages\runtime-core\src\apiCreateApp.ts
这也就是为什么我们插件实现规范得写install
的原因。
额,今天有些偏题,不过补充点额外知识也是不错的~