坏蛋Dan
知乎@坏蛋Dan
发布时间:2024.6.21

前言

昨天我们刚配置完了调试环境,今天我们来开始学习源码。


createPinia

在开始分析之前,我们要回顾下怎么使用pinia,这样有助于我们分析源码。

vuex类似,我们都需要注册这个插件到vue中。

那么这个函数是在哪呢?

我们先找入口,包的入口都会在package.json中声明(注意这是packages/piniapackage.json,而不是根目录的package.json):

那么我们直接进入packages/pinia/index.ts中:

测试用例

这个我们就直接用官方自带的测试用例即可,因为基本所有测试用例都需要初始化pinia才能使用,所以随便找一个即可。

rootState.spec.ts文件中:

然后我们就直接打上断点开始调试旅程

源码

代码非常简短,我们来一步一步看下什么情况

  • vue-demivue官方提供的一个工具,用来方便兼容vue2vue3
  • effectScopevue内部组件实例收集响应式effects的方法(比如computedwatch/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的逻辑。

整理下:

  1. 收集响应式effect,如果当前pinia实例被释放,那么这些被收集的effectcomputed、watch/WtachEffect等)也会跟着释放掉
  2. 初始化state,并赋值给pinia.state
  3. use中收集插件,等待install时注入到pinia._p
  4. install过程中将自己注入到vue根组件实例中,这样所有子组件都可以访问到。

其它

provide/inject里的provide源码逻辑

虽然根组件实例的provide和一般组件实例的provide两者逻辑基本都是用一样,不过还是有些微区别。

我们来看下两者的区别:

根节点的packages\runtime-core\src\apiCreateApp.ts

很简单,就是将它赋值给provides,这个providesprovide/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学的)。

currentInstance

packages\runtime-core\src\component.ts

全局唯一的变量,然后我们看下set是在组件实例创建的什么时候,这一点影响我们分析inject是从自己拿的还是从父组件拿的(也就是说执行inject的时候,currentInstance指向父组件实例还是指向自己组件实例)。

在同个文件中的setupStatefulComponent函数中(实际执行setup函数的位置)

可以看到setCurrentInstance是在setup函数执行之前,那就意味着执行provide/inject的时候currentInstance都指向自己,这也符合我们前面的分析。

另外为啥要搞一个全局唯一变量指向当前创建的组件实例呢?

因为有些逻辑参数不合适传来传去,所以放全局省事,反正单线程不会有竞争的问题。

额外补充下响应式数据和组件实例是如何挂钩的,也是通过全局唯一变量的方式,组件实例创建时会有一个

组件实例收集和管理依赖以及触发自己更新的核心:ReactiveEffect,具体可以看往期文章,提到过挺多次了。

组件实例创建时会创建一个ReactiveEffect实例,然后通过全局唯一的方式和setup里的响应式数据挂钩(被收集),这样数据更新会触发ReactiveEffect.fnfn中组件实例将自己放入任务调度队列等待重新渲染。

currentRenderingInstance

currentInstance差不多,都是指向当前渲染的组件实例。

这里要补充下渲染逻辑,父组件render function渲染到子组件的时候,就会退出当前渲染,等待子组件实例渲染和初始化完毕。

从时间上来说,父组件先于子组件渲染,但是子组件早于父组件渲染完毕。

可以想象成一个单向栈渲染遇到子组件 -> 入栈 -> 子组件渲染完毕 -> 出栈 -> 组件继续渲染

这也就是为什么要有currentInstancecurrentRenderingInstance的原因,因为组件实例化和渲染这整个过程中不一定都是指向自己的,遇到子组件就会临时让给子组件实例。

扯远了,我们看下currentRenderingInstance的相关逻辑

packages\runtime-core\src\componentRenderUtils.ts(执行render function的时候,生产环境render是包含在setup里的,开发模式则是单独拎出来方便hmr

这里就是执行render function的地方,此时会切换currentRenderingInstance指向子组件实例,子组件实例渲染完毕才会返回给父组件实例。

注意,上面提到的创建实例的逻辑都是在patch执行过程中的。具体流程可以看往期文章。

ok到这就说完了这块。

plugin install

去到vue源码文件夹中:

packages\runtime-core\src\apiCreateApp.ts

这也就是为什么我们插件实现规范得写install的原因。


总结

额,今天有些偏题,不过补充点额外知识也是不错的~