pinia源码分析【4】- Pinia Methods

3,627 阅读4分钟

专栏导航

分析pinia源码之前必须知道的API

Pinia源码分析【1】- 源码分析环境搭建

Pinia源码分析【2】- createPinia

pinia源码分析【3】- defineStore

pinia源码分析【4】- Pinia Methods

前言

本系列文章参考源码pinia V2.0.14

源码分析仓库:github.com/vkcyan/mini…

​ 上一章我们对store的核心流程完成了分析,从而了解了一个store从定义到被使用的实现逻辑,但是store相关的方法,我们还未进行分析,本章我们就重点分析分析store自带的Methods

$onAction

使用示例

订阅当前store所有action操作,每当action被执行的时候,便会触发该方法

onMounted(() => {
  useCounter1.$onAction((option) => {
    let { after, onError, args, name, store } = option;
  });

  setInterval(() => {
    useCounter1.counter++;
    // useCounter1.increment();
  }, 1000);
});

源码分析

订阅

$Action声明的地方,我们可以看到一段这样的函数

第一个参数传null,则不改变this指向,并且在后续的调用依旧是该this。

const partialStore = {
  $onAction: addSubscription.bind(null, actionSubscriptions), // action事件注册函数
}

​ 也就是说,当我们使用store.$Action的时候实际上触发的是addSubscription函数,并将我们$Action中的回调函数传入createSetupStore中的actionSubscriptions中,也就是订阅了我们的callback

​ 运行store.$Action后得到了addSubscription方法的返回值removeSubscription方法,让我们可以执行其返回值,达到取消订阅的目的。

export function addSubscription<T extends _Method>(
  subscriptions: T[], // createSetupStore中的actionSubscriptions
  callback: T, // 我们传入的callback
  detached?: boolean, // 如果为true,则该$Action在页面销毁之后依旧有效
  onCleanup: () => void = noop
) {
  // 使用$Action的时候就会触发本函数
  subscriptions.push(callback)

  const removeSubscription = () => {
    const idx = subscriptions.indexOf(callback)
    if (idx > -1) {
      subscriptions.splice(idx, 1)
      onCleanup()
    }
  }

  if (!detached && getCurrentInstance()) {
    // 如果detached参数不存在,则在当前页面卸载的时候,去除该订阅事件
    onUnmounted(removeSubscription)
  }
  return removeSubscription
}

触发订阅

​ 在useStore中对action进行处理的逻辑中,存在这样的一段代码,这段代码中的hot在正常使用的业务场景下都是undefined,所以会走后面的逻辑。

const actionValue =  wrapAction(key, prop) // hot为undefined的情况下

image-20220720184122095

​ 所有的action在初始化阶段都会被wrapAction方法拦截,也就代表我们执行action的时候,实际上执行的是wrapAction函数,那就让我们就看看,在wrapAction中究竟发生了什么

/**
* 包装一个action来处理订阅
*
* @param name - store的名称
* @param action - 需要被包装的action
* @returns a wrapped action to handle subscriptions
*/
function wrapAction(name: string, action: _Method) {
    return function (this: any) {
        setActivePinia(pinia);
        // 获取当前action的参数
        const args = Array.from(arguments);

        const afterCallbackList: Array<(resolvedReturn: any) => any> = [];
        const onErrorCallbackList: Array<(error: unknown) => unknown> = [];
        // 声明after方法
        function after(callback: _ArrayType<typeof afterCallbackList>) {
            // 将after的call放入list中
            afterCallbackList.push(callback);
        }
        // 声明error方法
        function onError(callback: _ArrayType<typeof onErrorCallbackList>) {
            onErrorCallbackList.push(callback);
        }

        // @ts-expect-error
        // 触发actionSubscriptions中订阅的store.$Action的全部回调函数,并将参数传入
        // 此时store.$Action的callback已经执行,但是after onError的回调函数尚未执行
        triggerSubscriptions(actionSubscriptions, {
            args,
            name,
            store,
            after,
            onError,
        });

        let ret: any; // ret为action的返回值
        try {
            ret = action.apply(this && this.$id === $id ? this : store, args);
            // handle sync errors
        } catch (error) {
            // 如果action执行出错,则直接执行错误回调,终止函数
            triggerSubscriptions(onErrorCallbackList, error);
            throw error;
        }
        // 如果ret是promise,则当前结果未知,会通过上方的try catch,但是会在action结尾增加then catch进行结果捕捉
        if (ret instanceof Promise) {
            return ret
                .then((value) => {
                triggerSubscriptions(afterCallbackList, value);
                return value;
            })
                .catch((error) => {
                triggerSubscriptions(onErrorCallbackList, error);
                return Promise.reject(error);
            });
        }

        // allow the afterCallback to override the return value
        // 如果try catch 通过,并且当前action不是Promise,则逻辑进行到此处,触发所有 触发真正的after函数,并将当前action的返回值传入其中,至此完成对action触发的监听。
        triggerSubscriptions(afterCallbackList, ret);
        return ret;
    };
}

​ 之前在$Action中的回调函数在此处发挥了作用,每当一个action触发的都会遍历之前订阅的所有$Action的回调函数,其内部执行action方法,action执行正常在触发aftercallback,执行异常则触发onErrorcallback

小结

image-20220721143852948

本质上来说$Action就是一个订阅发布模式。

$Action 订阅者

store.action 发布者

actionSubscriptions - 事件注册中心

triggerSubscriptions - 调度中心

​ 通过订阅者($Action)把对发布者(action)的订阅注册到事件注册中心(actionSubscriptions)中,当发布者(action)触发时,通知调度中心(triggerSubscriptions),**调度中心(triggerSubscriptions)**触发事件注册中心中的所有订阅。

$subscribe

使用示例

​ 订阅当前store中的state的变化,state发生任意更改都会触发其回调函数,他还会返回一个用来删除的回调函数

let abc = useCounter1.$subscribe(
    (option, state) => {
        // 通过store.num = xxxx修改,type为direct
        // 通过store.$patch({ num: 'xxx' })修改,type为directpatchObject
        // 通过store.$patch((state) => num.name='xxx')修改,type为patchFunction

        // storeId为当前store的id
        // events 当前改动说明
        let { events, storeId, type } = option;
        console.log(events, storeId, type, state);
    },
    { detached: false }
);

源码分析

当我们使用$subscribe并传入callback的时候,首先会将当前的callback加入注册中心中

const removeSubscription = addSubscription(
    subscriptions, // 事件注册中心
    callback, // $subscribe传入的callback
    options.detached, // 页面卸载的时候是否取消监听
    () => stopWatcher() // 执行stopWatcher实际上执行的是scope.run返回的watch,而执行watch的返回函数,也就是停止当前watch
);

​ 前三个参数经过对$Action的分析后已经比较熟悉,这里我们重点说明一下第四个参数

stopWatcher是当前store中的effectScope,我们将对当前statewatch放入scope中,以便于销毁store的时候统一处理。

const stopWatcher = scope.run(() =>
    watch(
        () => pinia.state.value[$id] as UnwrapRef<S>, // 监听state的变化
        (state) => {
            // 在不使用$patch的情况下,则两个参数都为true,callback一定会执行
            if (options.flush === "sync" ? isSyncListening : isListening) {
                callback(
                    {
                        storeId: $id, // 
                        type: MutationType.direct,
                        events: debuggerEvents as DebuggerEvent,
                    },
                    state
                );
            }
        },
        assign({}, $subscribeOptions, options)
    )  
)

小结

image-20220722170139137

$subscribe主要依赖vue3watch进行实现,在subscriptions中注册callback,但是注册的callback不通过triggerSubscriptions进行触发,仅仅作为保存,watch的触发函数中通过闭包触发$subscribe中的callback,达到store中任意值发生变化的时候都执行callback的目的

​ 在addSubscription的返回值removeSubscription中,不仅会在subscriptions(注册中心)删除订阅,同时也会执行() => stopWatcher(),停止watch监听。达到完全停止监听的目的。

$patch

使用示例

直接更新当前state,可以通过传入对象callback两种方式进行state更新,允许传递嵌套值

// 对象
useCounter1.$patch({ counter: 2 });
// function
useCounter1.$patch((state) => {
    state.counter = 2;
});

源码分析

$patch的主体逻辑不算很复杂,针对不同的参数类型进行分别处理,其中partialStateOrMutator是传入的方法,我们将当前store传入其中,通过其callback直接完成state的修改,而传入类型为object的时候,则通过mergeReactiveObjects进行处理。

function $patch(stateMutation: (state: UnwrapRef<S>) => void): void; // Fun传参
function $patch(partialState: _DeepPartial<UnwrapRef<S>>): void; // 对象传参
function $patch(
partialStateOrMutator:
 | _DeepPartial<UnwrapRef<S>>
 | ((state: UnwrapRef<S>) => void)
): void {
    let subscriptionMutation: SubscriptionCallbackMutation<S>;
    isListening = isSyncListening = false;
    // reset the debugger events since patches are sync
    /* istanbul ignore else */
    if (__DEV__) {
        debuggerEvents = [];
    }
    // 如果参数是方法,走以下处理逻辑
    if (typeof partialStateOrMutator === "function") {
        partialStateOrMutator(pinia.state.value[$id] as UnwrapRef<S>);
        subscriptionMutation = {
            type: MutationType.patchFunction,
            storeId: $id,
            events: debuggerEvents as DebuggerEvent[],
        };
    } else {
    // 如果参数是对象,走以下处理逻辑
        mergeReactiveObjects(pinia.state.value[$id], partialStateOrMutator);
        subscriptionMutation = {
            type: MutationType.patchObject,
            payload: partialStateOrMutator,
            storeId: $id,
            events: debuggerEvents as DebuggerEvent[],
        };
    }
    const myListenerId = (activeListener = Symbol());
    nextTick().then(() => {
        if (activeListener === myListenerId) {
            isListening = true;
        }
    });
    isSyncListening = true;
    // 在上方逻辑中,我们将isListening isSyncListening 重置为false,不会触发$subscribe中的callback,所以需要手动进行订阅发布
    triggerSubscriptions(
        subscriptions,
        subscriptionMutation,
        pinia.state.value[$id] as UnwrapRef<S>
    );
}


// $patch传入参数为Object的处理逻辑
function mergeReactiveObjects<T extends StateTree>(
  target: T,
  patchToApply: _DeepPartial<T>
): T {
  // no need to go through symbols because they cannot be serialized anyway
  for (const key in patchToApply) {
    if (!patchToApply.hasOwnProperty(key)) continue;
    const subPatch = patchToApply[key];
    const targetValue = target[key];
    if (
      isPlainObject(targetValue) &&
      isPlainObject(subPatch) &&
      target.hasOwnProperty(key) &&
      !isRef(subPatch) &&
      !isReactive(subPatch)
    ) {
      // 如果被修改的值 修改前修改后都是object类型并且不是Function类型、并且不是ref 不是isReactive,则递归mergeReactiveObjects达到修改嵌套object的目的
      target[key] = mergeReactiveObjects(targetValue, subPatch);
    } else {
      // @ts-expect-error: subPatch is a valid value
      // 如果是简单类型 则直接进行state的修改,这里的target为pinia.state.value[$id]
      // 按我们的示例来实际分析:pinia.state.value[$id].counter = 2
      target[key] = subPatch;
    }
  }
  return target;
}

​ 完成对mergeReactiveObjects的分析后,$patch的核心逻辑就全部结束了,但是还有一点我们没完成,就是通过$patch修改的state$subscribe是否可以监听到。

patch触发patch触发subscribe

​ 在$patch执行的中,我们会修改当前store中的state$subscribe中的watchflush='sync'的情况下可以立刻监听到,但是也无法执行callback,因为$patch函数最开始的地方将isListening,isSyncListening置为false

​ 在对值完成修改后,我们将isSyncListening置为true,并且手动订阅$subscribecallback,达到通过$patch修改state也能被$subscribe监听到的目的。

小结

$patch的源码相对来说比较简单,但是关于触发$subscribe的部分代码逻辑比较复杂,尤其是当$subscribe option设置中的flush为sync的时候,修改state立刻就会触发$subscribewatch,虽然最终呈现出来的结果是一致的,但是内部对不同情况的兼容没有看起来那么简单。

image-20220723162542035

$dispose

调用该方法后将会注销当前store

scope中存储当前store中的相关反应,当前statewatchref,等等effect都通过scope.run创建,就是为了方便统一处理,这里调用scope.stop()所有的effect便被全部注销了。

  function $dispose() {
    scope.stop();
    subscriptions = []; // $subscribe注册中心
    actionSubscriptions = []; // $Action的注册中心
    pinia._s.delete($id); // 删除effectMap结构
  }

$reset

调用该方法可以将当前state重置为初始化的状态

但是有点需要注意,如果defineStore通过setup类型声明,则无法调用该函数

const $reset = __DEV__
	? () => {
        throw new Error(
            `🍍: Store "${$id}" is built using the setup syntax and does not implement $reset().`
        );
	}
	: noop; // noop为空函数

如果通过option类型进行声明,则会重写$reset方法

store.$reset = function $reset() {
    // state通过闭包机制获得最初state定义的状态
    const newState = state ? state() : {};
    // 通过$patch完成对state中数据的更新
    this.$patch(($state) => {
        assign($state, newState);
    });
};

总结

​ 至此,我们就完成了对pinia所有方法的源码解读,而pinia源码解读系列文章也将告一段落,我们从pinia的初始化到了解如何实现state,getters的响应式,最后完成对pinia metnods的全部解读,也算是完全了解了其核心实现,最后我们将会实现一个mini版的pinia,仅仅保留核心实现,降低阅读门槛,让大多数人可以轻松了解pinia的核心实现原理~