likes
comments
collection
share

Pinia 原理解读 - 相关 methods 与 api 的解析

作者站长头像
站长
· 阅读数 15

一、Pinia 的方法、api 解析

前面两个源码解析的章节我们已经分析了 Pinia 是如何被注册引入到 Vue 项目当中以及模块化数据仓库的初始化和获取,接下来我们来解析下 Pinia 提供的方法、api的实现。

1.1 Pinia 的 api:

storeToRefs

storeToRefs的作用就是创建一个引用对象,包含 store 的所有 state、 getter 和 plugin 添加的 state 属性。 类似于Vue.js 的toRefs,但专门为 Pinia store 设计, 所以 method 和非响应式属性会被完全忽略。

  • 其实底层也是使用toReftoRefs就能实现的一个 api 方法。

使用例子:

import { mainStore } from '../store/index'
import { storeToRefs } from 'pinia'

const store = mainStore()

const { hello: myHello } = storeToRefs(store)
const { hello: hello2 } = store

// 响应式数据 - store.state 更新后会同步到 template 上
console.log('myHello', myHello)

// 非响应式数 - store.state 更新后不会同步到 template 上
const hello2 = 'hello2 world2'
console.log('myHello2', hello2) 

源码分析:

// vuejs:pinia/packages/pinia/src/storeToRefs.ts

export function storeToRefs<SS extends StoreGeneric>(
  store: SS
): ToRefs<
  StoreState<SS> & StoreGetters<SS> & PiniaCustomStateProperties<StoreState<SS>>
> {
  // 先通过 toRaw 方法获取原始的数据仓库对象
  store = toRaw(store)
  
  // 创建一个新的对象,存储属性
  const refs = {} as ToRefs<
    StoreState<SS> &
      StoreGetters<SS> &
      PiniaCustomStateProperties<StoreState<SS>>
  >
  
  // 遍历 store 对象
  for (const key in store) {
    const value = store[key]
    // 检测是否为 Ref 或者 Reactive 对象的响应式数据
    if (isRef(value) || isReactive(value)) {
      // 
      refs[key] = toRef(store, key)
    }
  }
  
  // 返回经过对属性关联性响应式 toRef 处理后的对象
  return refs
}

能够看到,其实具体的实现非常简单,主要就是三步:

  • 通过toRaw获取 store 的原始数据(这样子对原本 store 对象进行修改也不会触发 proxy 相关的响应式监听劫持处理,是性能优化的一种方法);
  • 创建一个新的对象 refs 去挂载响应式的数据(state、getter);
  • 遍历这个 store 数据仓库:
    • 使用isRefisReactive判断是否是响应式的数据;
    • 判断为响应式的属性数据时候使用toRef对 store 的单个属性的响应式转换获取并设置到 refs 对象的同名 key 属性上面;
  • 返回这个 refs 对象。

映射辅助函数

Pinia 在提供 hooks 写法的 api 同时 pinia 为了兼容 option api 其实也提供了类似于 Vuex map系列的映射辅助函数给到开发者使用(因为 Pinia 中没有 mutation 概念了因此是没有 mapMutation 这个 api 了)。虽然 pinia 为了兼容 option api 的形式也是提供了这些 map 的映射方法,但是在 Vue 3.x 的 composition api 下还是多使用 hooks 的形式,不推荐使用这种 option api 的使用。

接下来我们还是来简单的分析这些映射辅助函数的源码实现。

mapState / mapGetters

// vuejs:pinia/packages/pinia/src/mapHelpers.ts

export function mapState<
  Id extends string,
  S extends StateTree,
  G extends _GettersTree<S>,
  A,
  KeyMapper extends Record<
    string,
    keyof S | keyof G | ((store: Store<Id, S, G, A>) => any)
  >
>(
  useStore: StoreDefinition<Id, S, G, A>,
  keyMapper: KeyMapper
): _MapStateObjectReturn<Id, S, G, A, KeyMapper>

export function mapState<
  Id extends string,
  S extends StateTree,
  G extends _GettersTree<S>,
  A,
  Keys extends keyof S | keyof G
>(
  useStore: StoreDefinition<Id, S, G, A>,
  keys: readonly Keys[]
): _MapStateReturn<S, G, Keys>

export function mapState<
  Id extends string,
  S extends StateTree,
  G extends _GettersTree<S>,
  A
>(
  useStore: StoreDefinition<Id, S, G, A>,
  keysOrMapper: any
): _MapStateReturn<S, G> | _MapStateObjectReturn<Id, S, G, A> {
  return Array.isArray(keysOrMapper)
    ? keysOrMapper.reduce((reduced, key) => {
        reduced[key] = function (this: ComponentPublicInstance) {
          return useStore(this.$pinia)[key]
        } as () => any
        return reduced
      }, {} as _MapStateReturn<S, G>)
    : Object.keys(keysOrMapper).reduce((reduced, key: string) => {
        reduced[key] = function (this: ComponentPublicInstance) {
          const store = useStore(this.$pinia)
          const storeKey = keysOrMapper[key]

          return typeof storeKey === 'function'
            ? (storeKey as (store: Store<Id, S, G, A>) => any).call(this, store)
            : store[storeKey]
        }
        return reduced
      }, {} as _MapStateObjectReturn<Id, S, G, A>)
}

// mapGetters 与 mapState 同样逻辑
export const mapGetters = mapState

mapState通过函数重载来实现不同的调用传入参数的处理,这样子方便业务代码使用方的认知和使用负担,只需要记住一个mapState方法即可;

能够看到其实mapState的实现逻辑是非常简单,其实就是在 pinia 对象当中使用找到对应的模块数据仓库(如果参数当中传入对应的数据仓库则直接使用这个 store hooks),在这个数据仓库当中通过查找对应的要取出映射的 key 的对象属性 value,并且将这个值作为另外一个新对象相同 key 的 value,最后将这个承载了响应式 state 或者 getters 的新的对象返回。

我们从上面的源码当中能够看到这样一句代码mapGetters = mapState;也就是其实mapStatemapGetters是同样的逻辑实现,直接复用mapState的处理逻辑,甚至能够直接通过mapState方法直接来获取getters

mapActions

// vuejs:pinia/packages/pinia/src/mapHelpers.ts

export function mapActions<
  Id extends string,
  S extends StateTree,
  G extends _GettersTree<S>,
  A,
  KeyMapper extends Record<string, keyof A>
>(
  useStore: StoreDefinition<Id, S, G, A>,
  keyMapper: KeyMapper
): _MapActionsObjectReturn<A, KeyMapper>

export function mapActions<
  Id extends string,
  S extends StateTree,
  G extends _GettersTree<S>,
  A
>(
  useStore: StoreDefinition<Id, S, G, A>,
  keys: Array<keyof A>
): _MapActionsReturn<A>

export function mapActions<
  Id extends string,
  S extends StateTree,
  G extends _GettersTree<S>,
  A,
  KeyMapper extends Record<string, keyof A>
>(
  useStore: StoreDefinition<Id, S, G, A>,
  keysOrMapper: Array<keyof A> | KeyMapper
): _MapActionsReturn<A> | _MapActionsObjectReturn<A, KeyMapper> {
  return Array.isArray(keysOrMapper)
    ? keysOrMapper.reduce((reduced, key) => {
        reduced[key] = function (
          this: ComponentPublicInstance,
          ...args: any[]
        ) {
          return useStore(this.$pinia)[key](...args)
        }
        return reduced
      }, {} as _MapActionsReturn<A>)
    : Object.keys(keysOrMapper).reduce((reduced, key: keyof KeyMapper) => {
        reduced[key] = function (
          this: ComponentPublicInstance,
          ...args: any[]
        ) {
          return useStore(this.$pinia)[keysOrMapper[key]](...args)
        }
        return reduced
      }, {} as _MapActionsObjectReturn<A, KeyMapper>)
}

同样,mapActionsmapState一样是通过函数重载来实现不同的参数处理;甚至是处理逻辑也是基本上类似的思路,就是找到对应的数据仓库,然后找寻对应的 action 方法挂载到一个新的对象当中,最后返回这个对象。

1.2 Store 数据仓库的 api:

在上一篇的 “初始化流程” 的源码分析文章当中我们了解到,在模块化的 store 上面会在defineStore阶段createSetupStore方法里面对这个 store 对象挂载一系列的方法,因此接下来要探究的方法基本实现原理和逻辑都在那块地方上面。

$onAction

设置一个回调,当一个 action 即将被调用时,就会被调用。 回调接收一个对象, 其包含被调用 action 的所有相关信息:

  • store: 被调用的 store
  • name: action 的名称
  • args: 传递给 action 的参数

前置知识 - 订阅发布模式

在 Pinia 的源码底层当中,是将 订阅发布 相关的功能模块单独抽取封装出来了,是软件设计当中的 “解耦” 的一个很好的例子。

// vuejs:pinia/packages/pinia/src/subscriptions.ts

import { getCurrentScope, onScopeDispose } from 'vue-demi'
import { _Method } from './types'

export const noop = () => {}

// 添加订阅回调事件
export function addSubscription<T extends _Method>(
  subscriptions: T[],            // 订阅的回调列表
  callback: T,                   // 新增的订阅回调函数
  detached?: boolean,            // 标识 - 组件卸载后是否保留监听
  onCleanup: () => void = noop   // 清除订阅回调函数后执行的回调
) {
  subscriptions.push(callback) // 回调列表添加新的回调函数项

  // 内部定义的清除当前订阅回调函数的方法
  const removeSubscription = () => {
    const idx = subscriptions.indexOf(callback)
    if (idx > -1) {
      subscriptions.splice(idx, 1)
      onCleanup()
    }
  }

  // 根据 detached 参数以及当前组件作用域判断是否要调用 removeSubscription 在组件删除时候进行清除订阅的回调函数
  if (!detached && getCurrentScope()) {
    onScopeDispose(removeSubscription)
  }

  
  return removeSubscription // 返回清除订阅回调的函数
}

// 触发订阅(发布)
export function triggerSubscriptions<T extends _Method>(
  subscriptions: T[],
  ...args: Parameters<T>
) {
  // 遍历订阅列表,执行各个订阅函数
  subscriptions.slice().forEach((callback) => {
    callback(...args)
  })
}

看源码便知道这是一个很标准的一个订阅发布的实现:

  • 添加订阅 - 往订阅列表当中 push 增加订阅回调;
  • 触发订阅 - 遍历订阅回调事件列表,逐个执行回调事件;

$onAction 分析

import { mainStore } from '../store/index'

const store = mainStore()

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

  console.log({
    after, onError, args, name, store,
  });

  // 这将在 action 成功并完全运行后触发。它等待着任何返回的 promise
  after((result) => {
    console.log(
      `Finished "${name}" after ${
        Date.now() - startTime
      }ms.\nResult: ${result}.`
    )
  })

  // 如果 action 抛出或返回一个拒绝的 promise,这将触发
  onError((error) => {
    console.warn(
      `Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}.`
    )
  })

});

setInterval(() => {
  store.sayHello();
}, 1000);
// vuejs:pinia/packages/pinia/src/store.ts

function createSetupStore<
    Id extends string,
    SS extends Record<any, unknown>,
    S extends StateTree,
    G extends Record<string, _Method>,
    A extends _ActionsTree
    >(
    $id: Id,
    setup: () => SS,
    options:
        | DefineSetupStoreOptions<Id, S, G, A>
        | DefineStoreOptions<Id, S, G, A> = {},
    pinia: Pinia,
    hot?: boolean,
    isOptionsStore?: boolean
): Store<Id, S, G, A> {
  // ··· ···

	// 创建订阅函数列表数组对象
  let actionSubscriptions: StoreOnActionListener<Id, S, G, A>[] = markRaw([])

  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)) {
      // ··· ···
    } else if (typeof prop === 'function') { // 判断当前属性是函数,也就是 action 方法
      // 使用 wrapAction 包装这个 action 方法
      const actionValue = wrapAction(key, prop)

      // 使用经过 wrapAction 包装后的 action 方法替换掉原方法
      setupStore[key] = actionValue

      // ··· ···
    }
  }

  function wrapAction(name: string, action: _Method) {
    return function (this: any) {
      setActivePinia(pinia) // 设置激活的数据仓库为当前数据仓库

      const args = Array.from(arguments) // 获取调用 action 方法时候的参数

      // 定义相关回调函数数据列表
      const afterCallbackList: Array<(resolvedReturn: any) => any> = []
      const onErrorCallbackList: Array<(error: unknown) => unknown> = []

      // 定义 after 方法
      function after(callback: _ArrayType<typeof afterCallbackList>) {
        afterCallbackList.push(callback) // 将
      }

      // 定义 onError 方法
      function onError(callback: _ArrayType<typeof onErrorCallbackList>) {
        onErrorCallbackList.push(callback)
      }

      // 触发 $onAction 的订阅函数
      triggerSubscriptions(actionSubscriptions, { args, name, store, after, onError })

      let ret: any
      try {
        // 执行 action 方法
        ret = action.apply(this && this.$id === $id ? this : store, args)
      } catch (error) {
        // 执行 action 出异常或者 action 抛除 reject 时候触发 onError 的订阅发布 
        triggerSubscriptions(onErrorCallbackList, error)
        throw error
      }

      // 判断执行 action 方法是否返回的是一个 Promise
      if (ret instanceof Promise) {
        // 如果返回的是 Promise 则需要进行 then 判断运行结果
        return ret
            .then((value) => {
              // action 返回的 Promise 执行成功则进行对 $action 里对 after 的回调进行触发处理
              triggerSubscriptions(afterCallbackList, value)
              return value
            })
            .catch((error) => {
              // 执行出现异常时候触发 onError 的订阅发布 
              triggerSubscriptions(onErrorCallbackList, error)
              return Promise.reject(error)
            })
      }

      // 触发 after 的订阅函数
      triggerSubscriptions(afterCallbackList, ret)
      
      return ret
    }
  }

  const partialStore = {
    // ··· ···

    // 注册 $onAction 的订阅发布处理
    $onAction: addSubscription.bind(null, actionSubscriptions),
  } as _StoreWithState<Id, S, G, A>

  // ··· ···
}

从上面精简后的createSetupStore方法来看,$onAction就是基于前文提及到的订阅发布模式为基础来实现的。

首先在createSetupStore里面会创建一个 onAction方法的订阅发布回调事件数组,然后使用bind重新生成一个修改参数的‘addSubscription‘增加订阅的方法作为onAction 方法的订阅发布回调事件数组,然后使用 bind 重新生成一个修改参数的`addSubscription`增加订阅的方法作为 onAction方法的订阅发布回调事件数组,然后使用bind重新生成一个修改参数的addSubscription增加订阅的方法作为onAction,也就是 $onAction 调用时候其实就是调用addSubscription方法添加了一次订阅;

接着将 action 里面的方法都使用wrapAction方法包装了一次,wrapAction方法就是实现 action 方法调用时候触发 onAction回调的核心处理逻辑:在‘wrapAction‘方法其实主要是先执行完原action的方法,接在再对onAction 回调的核心处理逻辑:在`wrapAction`方法其实主要是先执行完原 action 的方法,接在再对 onAction回调的核心处理逻辑:在wrapAction方法其实主要是先执行完原action的方法,接在再对onAction 回调内的 after 与 onError 回调的处理;

  • 创建 after 与 onError 各自的订阅发布回调函数数组,并且封装对应添加订阅的方法并且该方法作为 $onAction 方法的参数,提供给外部调用添加 after 与 onError 的订阅回调;
  • 先执行完原 action 的方法(包括如果 action 返回的 Promise 也保证执行完);
  • 然后 action 方法没执行异常或者方法内部没返回 Promise.reject 则使用triggerSubscriptions触发 after 的发布继而正常执行 after 的订阅回调;
  • 如果执行其中有报错则因为 try catch 原因进入捕获报错逻辑,进而调用triggerSubscriptions触发 onError 的订阅发布,触发对应的错误订阅回调。

$subscribe

设置一个回调,当状态发生变化时被调用。它会返回一个用来移除此回调的函数。 请注意,当在组件内调用 store.$subscribe() 时,除非 detached 被设置为 true, 否则当组件被卸载时,它将被自动清理掉。

import { mainStore } from '../store/index'

const store = mainStore()

store.$subscribe(
  (option, state) => {
    let { events, storeId, type } = option;
    console.log(events, storeId, type, state);
  },
  { detached: false }
);
// vuejs:pinia/packages/pinia/src/store.ts

function createSetupStore<
    Id extends string,
    SS extends Record<any, unknown>,
    S extends StateTree,
    G extends Record<string, _Method>,
    A extends _ActionsTree
    >(
    $id: Id,
    setup: () => SS,
    options:
        | DefineSetupStoreOptions<Id, S, G, A>
        | DefineStoreOptions<Id, S, G, A> = {},
    pinia: Pinia,
    hot?: boolean,
    isOptionsStore?: boolean
): Store<Id, S, G, A> {
  // ··· ···

  let subscriptions: SubscriptionCallback<S>[] = markRaw([]) // 存储 state 的发布订阅回调事件列表

  const partialStore = {
    // ··· ···
    
    $subscribe(callback, options = {}) {
      // 新增一个 state 监听回调
      const removeSubscription = addSubscription(
          subscriptions,
          callback,
          options.detached,
          () => stopWatcher() // 传入停止 watch 监听的函数
      )

      const stopWatcher = scope.run(() => // 通过 effectScope 来捕获内部创建可清除处理的响应式副作用
          // 使用 watch 来对当前数据仓库的 state 对象属性进行监听
          watch(
              () => pinia.state.value[$id] as UnwrapRef<S>,
              (state) => {
                if (options.flush === 'sync' ? isSyncListening : isListening) {
                  // 在触发到 state 对象内部属性进行修改时候执行 callback
                  callback(
                      {
                        storeId: $id,
                        type: MutationType.direct,
                        events: debuggerEvents as DebuggerEvent,
                      },
                      state
                  )
                }
              },
              assign({}, $subscribeOptions, options)
          )
      )!

      return removeSubscription
    }
    
  } as _StoreWithState<Id, S, G, A>

  // ··· ···
}

$subscribe这个 api 是基于 Vue.js 的watch以及前文提及的订阅发布模式来实现的。

实现 state 的监听的逻辑不难理解,就是创建了一个监听 state 的回调函数数组列表subscriptions,然后使用订阅发布的addSubscription添加监听执行 callback;然后利用 Vue.js 的watchapi 对当前数据仓库 store 的 state 属性进行监听,当修改触发 watch 回调时候调用参数的 callback 回调。

这时候有没有发现为啥创建了两种不同类型的监听呢,watch 监听的回调是有着标志 isSyncListening 与 isListening 判断是否进行回调 callback 的执行,并且通过订阅发布的addSubscription创建的订阅好像没有相关的发布触发回调呢。先别着急,接下来的$patch这个修改 state 值的 api 就会牵涉到这块订阅发布的回调逻辑了。

$patch

将一个 state 补丁应用于当前状态,其实就是改 state 的值。允许传递嵌套值。

useCounter.$patch({ counter: 2 });

useCounter.$patch((state) => {
    state.counter = 2;
});
// vuejs:pinia/packages/pinia/src/store.ts

let activeListener: Symbol | undefined
function $patch(stateMutation: (state: UnwrapRef<S>) => void): void
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

  if (typeof partialStateOrMutator === 'function') { // 参数是函数情况
    partialStateOrMutator(pinia.state.value[$id] as UnwrapRef<S>) // 调用 $patch 的参数函数,传入当前 state 作为参数,函数内直接对该 state 对象的属性进行修改
    
    subscriptionMutation = {
      type: MutationType.patchFunction,
      storeId: $id,
      events: debuggerEvents as DebuggerEvent[],
    }
  } else { // 参数是对象情况
    mergeReactiveObjects(pinia.state.value[$id], partialStateOrMutator) // 调用合并响应式的对象的方法合并当前数据仓库的 state 与新的对象
    
    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 中 watch 设置的回调,需要手动调用触发订阅发布
  triggerSubscriptions(
    subscriptions, // state 的订阅发布回调函数列表
    subscriptionMutation,
    pinia.state.value[$id] as UnwrapRef<S>
  )
}

$patch的逻辑需要结合着前面的$subscribe方法的实现才能较好的理解。

  • 首先是设置 isListening 与 isSyncListening 这两个前面控制$subscribe内 watch 监听回调 callback 的标识为 false,让其先不触发 state 修改后 watch 监听的回调(这是因为$patch能够批量同时对 state 多个属性进行更新,会多次触发 watch 的监听回调,因此需要先在更新 state 前进行对监听回调的禁用);
  • 然后根据$patch参数的形式是函数还是对象分别处理逻辑(函数直接执行、对象则进行对象属性合并)来进行对 state 的更新,更新后将前面的 isListening 与 isSyncListening 重新设置为 true;
  • 因为前面更新 state 时候禁用了 watch 的监听回调,因此我们需要手动去调用$triggerSubscriptions触发订阅发布事件,这时候就是回收前面$subscribe方法里面提及到的subscriptions订阅回调函数列表的使用。

$reset

通过建立一个新的状态对象,将 store 重设为初始状态。

const userStore = useUserStore();

userStore.$patch(state => {
  state.name = 'xxx'
});

userStore.$reset();
// vuejs:pinia/packages/pinia/src/store.ts

function $reset() {
  const newState = state ? state() : {}
  this.$patch(($state) => {
    assign($state, newState)
  })
}

简单粗暴的实现方式,就是重新调用 state 方法进行获取 setup 响应式的 state与getters,然后使用$patch修改设置当前数据仓库 store 对象的$state属性。

$dispose

停止 store 的相关作用域,并从 store 注册表中删除它。 插件可以覆盖此方法来清理已添加的任何副作用函数。

const userStore = useUserStore();
userStore.$dispose();
// vuejs:pinia/packages/pinia/src/store.ts

function $dispose() {
  scope.stop()
  subscriptions = []
  actionSubscriptions = []
  pinia._s.delete($id)
}

在之前的初始化数据仓库的文章当中我们谈及到,其实模块数据仓库 store 其实是在createSetupStore方法里面初始化创建的对象,其中数据仓库的 state、getters 都是存储在一个使用effectScope(至于 effectScope 这个 Vue.js 的高阶响应式 api 这里就不展开讲了)进行创建的一个响应式对象 scope 上面,因此这里的销毁数据仓库的操作就变得比较简单了,只要清除响应式相关数据、清除相关的监听等就可以了。

  • 调用 scope: EffectScope 的stop方法,对内部所创建的响应式副作用一次性清除掉;
  • 清空当前数据仓库的监听列表;
  • 最后从 pinia 对象当中删除对该数据仓库的指向引用即可。


二、收获与感想

通过对 Pinia 源码阅读,最大的收获还是更深层次的对 pinia 这个全局数据仓库管理库的一个实现原理的认知与学习,学会了一些可能在官方文档上也比较少记载描述的 api 的用法或者是平常当中较少使用的方法,像是初始化数据 state 仓库那样三种不同的方式;以及一些 api 的巧妙使用方法或者用处;甚至是一些 Vue.js 日常当中可能比较少使用到的 api 以及像 effectScope、makeRaw、toRaw 等能够提升代码运行性能的 api 的应用场景等。

当然在阅读源码的这段时间过程当中收获的不仅仅是代码知识相关的,还有额外的一些收获,像是这段时间的沉下心来专心的去完成并且一点一滴的不断地在进行阅读、思考、做笔记;自从毕业了之后投身到工作当中,这几年好像仿佛好久没有试着真正沉静下来,放空其他思想去认真仔细的参详、研究一样事物了。后续也会保持着继续深入对 Vue.js 周边的生态环境进行深入的研究,像 Vue.js Code、Vue-Router、Vuex 等相关库的源码、实现原理进行阅读分析。


参考资料

相关的系列文章

相关的参考资料