阅读vue 3.0 pre-alpra源码可以看到和之前的vue2.x可以说完全不是一个代码编写风格,不仅仅是用ts全面重写,还有借鉴react hooks思路演变而来的全新的逻辑复用方案:composition-api,下面一一阐述相关变化以及用法上的不同之处。原文地址
Composition API 设计细节
1. setup()函数
作者引入了一个新的组件选项,替代了原先的两个生命周期(beforeCreate, created),初始化过程见下:
setup 本身的调用时机,从目前的源码来看,介于 beforeCreate 和 created 这两个生命周期之间,很显然此时vue实例还未创建,我们无法在这里使用 this 指向当前组件实例。所以作者
setup 是在组件实例被创建时, 初始化了 props 之后调用,处于 created 前。同时setup接受两个参数:
props
:和vue2.x一样,指的是外部下发的数据
context
:上下文对象,打印出来里面有 attrs,emit,listeners,parent,refs,root,slots, 这些都是vue2.x里能通过this访问的全局属性,在3.0里采用了函数组合式编程的思想,可以完全代替之前this调用的方式。
setup()和之前的data()一样可以return一个对象(render context),这个对象上的属性将会被暴露给模版的渲染上下文。
与vue2.x不同的是如果我们要在setup内部创建被管理的的值,必须用ref或者reactive函数,使我们创建的值成为响应式,这两个函数的本质都是使用proxy(不熟悉的同学可以去学习es6 proxy)。
至于为什么会有两个函数来实现,后面再单独说明。
例子如下:(注:本文代码片段均是在vue2.x+@vue/composition-api下的语法,正式vue3.0发布可能会有细微调整)
<template>
<div class="hooks">
<button @click="count++">{{ count }}</button>
</div>
</template>
<script lang="ts">
import { ref, Ref, createComponent, onMounted, onBeforeMount, computed, watch } from '@vue/composition-api'
export default createComponent({
setup (props) {
const count: Ref<number> = ref(0)
onMounted(()=>{
})
onBeforeMount(() => {
});
return {
count
}
}
})
</script>
2. ref 和 reactive
从官方的RFC文档了解到,ref常用于基本类型,reactive用于引用类型。why?
让我们从源码来看
export function ref(raw?: unknown) {
if (isRef(raw)) {
return raw
}
// 转化数据
raw = convert(raw)
const r = {
_isRef: true,
get value() {
// track的代码在effect中,猜测此处就是监听函数收集依赖的方法。
track(r, OperationTypes.GET, 'value')
return raw
},
set value(newVal) {
// 将设置的值,转化为响应式数据,赋值给raw
raw = convert(newVal)
// trigger,猜测此处就是触发监听函数执行的方法
trigger(
r,
OperationTypes.SET,
'value',
__DEV__ ? { newValue: newVal } : void 0
)
}
}
return r
}
我们看到,这里也定义了get/set,却没有任何Proxy相关的操作。但ref的入参是对象时,用到了reactive做转化。但是对于基本数据类型,函数传递或者对象解构时,会丢失原始数据的引用,换言之,我们没法让基本数据类型,或者解构后的变量(如果它的值也是基本数据类型的话),成为响应式的数据。例如:
// 我们是永远没办法让`a`或`x`这样的基本数据成为响应式的数据的,Proxy也无法劫持基本数据。
const a = 1;
const { x: 1 } = { x: 1 }
但是有时候,我们确实就是想一个数字、一个字符串是响应式的,或者就是想利用解构的写法。那么这个时候ref函数就派上用场了,通过创建一个对象,也即是源码中的Ref数据,然后将原始数据保存在Ref的属性value当中,再将它的引用返回给使用者。既然是我们自己创造出来的对象,也就没必要使用Proxy再做代理了,直接劫持这个value的get/set即可,这就是ref函数与Ref类型的由来。
同时ref还提供了isRef和toRefs两个函数,前者用于我们判断是否为Ref类型,后者能将reactive对象的每个属性都转化为Ref对象,这点再我们解构reactive的时候特别方便。
再说回reactive,话不多说,看码:
// 函数类型声明,接受一个对象,返回不会深度嵌套的Ref数据
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
// 函数实现
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
// 如果传递的是一个只读响应式数据,则直接返回,这里其实可以直接用isReadonly
if (readonlyToRaw.has(target)) {
return target
}
// target is explicitly marked as readonly by user
// 如果是被用户标记的只读数据,那通过readonly函数去封装
if (readonlyValues.has(target)) {
return readonly(target)
}
// 到这一步的target,可以保证为非只读数据
// 通过该方法,创建响应式对象数据
return createReactiveObject(
target, // 原始数据
rawToReactive, // 原始数据 -> 响应式数据映射
reactiveToRaw, // 响应式数据 -> 原始数据映射
mutableHandlers, // 可变数据的代理劫持方法
mutableCollectionHandlers // 可变集合数据的代理劫持方法
)
}
// 函数声明+实现,接受一个对象,返回一个只读的响应式数据。
export function readonly<T extends object>(
target: T
): Readonly<UnwrapNestedRefs<T>> {
// value is a mutable observable, retrieve its original and return
// a readonly version.
// 如果本身是响应式数据,获取其原始数据,并将target入参赋值为原始数据
if (reactiveToRaw.has(target)) {
target = reactiveToRaw.get(target)
}
// 创建响应式数据
return createReactiveObject(
target,
rawToReadonly,
readonlyToRaw,
readonlyHandlers,
readonlyCollectionHandlers
)
}
两个方法代码其实很简单,主要逻辑都封装到了createReactiveObject
,两个方法的主要作用是:
透传给createReactiveObject相应地的代理数据与响应式数据的双向映射 map。 reactive会做readonly的相关校验,反之readonly方法也是。 继续看createReactiveObject:
function createReactiveObject(
target: any,
toProxy: WeakMap<any, any>,
toRaw: WeakMap<any, any>,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
// 不是一个对象,直接返回原始数据,在开发环境下会打警告
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// 通过原始数据 -> 响应数据的映射,获取响应数据
let observed = toProxy.get(target)
// target already has corresponding Proxy
// 如果原始数据已经是响应式数据,则直接返回此响应数据
if (observed !== void 0) {
return observed
}
// target is already a Proxy
// 如果原始数据本身就是个响应数据了,直接返回自身
if (toRaw.has(target)) {
return target
}
// only a whitelist of value types can be observed.
// 如果是不可观察的对象,则直接返回原对象
if (!canObserve(target)) {
return target
}
// 集合数据与(对象/数组) 两种数据的代理处理方式不同。
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlerstargetMap
// 声明一个代理对象,也即是响应式数据
observed = new Proxy(target, handlers)
// 设置好原始数据与响应式数据的双向映射
toProxy.set(target, observed)
toRaw.set(observed, target)
// 在这里用到了targetMap,但是它的value值存放什么暂时不知道
if (!targetMap.has(target)) {
targetMap.set(target, new Map())
}
return observed
}
上文我们说过reactive用于引用类型,在createReactiveObject函数第一行就体现了,我们传入基本类型会在开发环境告警。上面有一些辅助函数,这里就不一一列出,大家自行去阅读源码。
reactive
文件其实非常通俗易懂,看完以后,我们心中只有 2 个问题:
baseHandlers,collectionHandlers的具体实现以及为什么要区分? targetMap到底是啥? 当然我们知道handlers肯定是做依赖收集跟响应触发的。和上面ref函数里的track和trigger类似。大家可以自行学习这两个方法
在baseHandlers中
对于原始对象数据,会通过 Proxy 劫持,返回新的响应式数据(代理数据)。 对于代理数据的任何读写操作,都会通过Refelct反射到原始对象上。 在这个过程中,对于读操作,会执行收集依赖的逻辑。对于写操作,会触发监听函数的逻辑 关于collectionHandlers,是针对由于Set|Map等集合数据的底层设计问题,Proxy无法直接劫持set或直接反射行为而做的单独处理,这块大家可以自行研究。
至此,我们基本掌握了ref和reactive的基本内部实现以及用法。
3. watch、computed
既然3.0提倡函数式组合编程,可想而至,这两个我们常用的api也是函数,与2.x不同的是这两个函数得在setup里执行,同样意味着我们无法在里面使用this。先看一下官方使用示例:
watch
// eg.1
// watch Ref数据
const count = ref(0)
watch(() => console.log(count.value))
// eg.2
// watch reactive state 以及 获取它之前的状态值
// watching a getter
const state = reactive({ count: 0 })
watch(() => state.count, (count, prevCount) => { /* ... */ })
// eg.3
// watch多个Ref数据, 注意:以数组的方式,callback支持两个参数,第一个数组是当前value,第二个是pre-value
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
/* ... */
})
//eg.4
// watch reactive 下多个state
watch([()=>state.fooRef, ()=>state.barRef], ([foo, bar], [prevFoo, prevBar]) => {
/* ... */
})
//eg.5
const stop = watch(() => { /* ... */ })
// later
stop()
需要注意的是,watch会返回一个StopHandle函数,让我们在需要的时候关闭监听函数。同时watch还支持第三个参数options,官方解释是说,为了向下兼容vue2的watch,提供了lazy属性。大家都知道vue2里的watch在初始化后不会立即执行,而是一个惰性执行,只在第一次监听value改变的时候才执行,但是很多时候我们需要它立即执行,这一点在vue中就会需要我们自己来用额外的state来实现。在vue3中默认就是实例化的时候就会执行,如果需要惰性执行,我们可以在option里加上lazy: true。
computed
computed 和 vue2没有什么变化,我们看一下声明类型:
// read-only
// 参数为function的时候会返回一个Readonly只读类型,无法在外部赋值
function computed<T>(getter: () => T): Readonly<Ref<Readonly<T>>>
// writable
function computed<T>(options: {
get: () => T,
set: (value: T) => void
}): Ref<T>
4. 生命周期
废弃了beforeCreate和created,同setup代替,其他声明周期在setup
函数中执行,注意一点的是,不在像2.x中那样覆写声明周期函数,而是在setup里面调用生命周期函数执行,传参为回调函数,在回调里执行我们需要的操作。
新增了两个调试钩子函数:onRenderTracked
,onRenderTriggered
,作用是能让我们在触发onTrack
和onTrigger
的时候debug。
点击查看更多原文地址
官方资料
-
官方Repo:vue-next
-
RFC:function-api
-
尤雨溪:Vue Function-based API RFC:介绍了vue function api的原理和用法