尤雨溪:Vue Function-based API RFC

window.removeEventListener(‘mousemove’, update)

})

return { x, y }

}

// 在组件中使用该函数

const Component = {

setup() {

const { x, y } = useMouse()

// 与其它函数配合使用

const { z } = useOtherLogic()

return { x, y, z }

},

template: <div>{{ x }} {{ y }} {{ z }}</div>

}

从以上例子中可以看到:

  • 暴露给模版的属性来源清晰(从函数返回);

  • 返回值可以被任意重命名,所以不存在命名空间冲突;

  • 没有创建额外的组件实例所带来的性能损耗。

文末附录中有与 React Hooks 的一些细节对比。

类型推导

3.0 的一个主要设计目标是增强对 TypeScript 的支持。原本我们期望通过 Class API 来达成这个目标,但是经过讨论和原型开发,我们认为 Class 并不是解决这个问题的正确路线,基于 Class 的 API 依然存在类型问题。

基于函数的 API 天然对类型推导很友好,因为 TS 对函数的参数、返回值和泛型的支持已经非常完备。更值得一提的是基于函数的 API 在使用 TS 或是原生 JS 时写出来的代码几乎是完全一样的。下文会提供新 API 类型推导的更多细节,此外文末附录中有关于 Class API 类型问题的更多细节。

打包尺寸

基于函数的 API 每一个函数都可以作为 named ES export 被单独引入,这使得它们对 tree-shaking 非常友好。没有被使用的 API 的相关代码可以在最终打包时被移除。同时,基于函数 API 所写的代码也有更好的压缩效率,因为所有的函数名和 setup 函数体内部的变量名都可以被压缩,但对象和 class 的属性/方法名却不可以。

设计细节

setup() 函数

我们将会引入一个新的组件选项,setup()。顾名思义,这个函数将会是我们 setup 我们组件逻辑的地方,它会在一个组件实例被创建时,初始化了 props 之后调用。setup() 会接收到初始的 props 作为参数:

const MyComponent = {

props: {

name: String

},

setup(props) {

console.log(props.name)

}

}

需要留意的是这里传进来的 props 对象是响应式的 —— 它可以被当作数据源去观测,当后续 props 发生变动时它也会被框架内部同步更新。但对于用户代码来说,它是不可修改的(会导致警告)。

在 setup 内部可以使用 this,但你大部分时候不会需要它。

组件状态

类似 data(),setup() 可以返回一个对象 —— 这个对象上的属性将会被暴露给模版的渲染上下文:

const MyComponent = {

props: {

name: String

},

setup(props) {

return {

msg: hello ${props.name}!

}

},

template: <div>{{ msg }}</div>

}

上面这个例子跟 data() 一摸一样:msg 可以在模版中被直接使用,它甚至可以被模版中的内联函数修改。但如果我们想要创建一个可以在 setup() 内部被管理的值,可以使用 value 函数:

import { value } from ‘vue’

const MyComponent = {

setup(props) {

const msg = value(‘hello’)

const appendName = () => {

msg.value = hello ${props.name}

}

return {

msg,

appendName

}

},

template: <div @click="appendName">{{ msg }}</div>

}

value() 返回的是一个 value wrapper (包装对象)。一个包装对象只有一个属性:.value ,该属性指向内部被包装的值。在上面的例子中,msg 包装的是一个字符串。包装对象的值可以被直接修改:

// 读取

console.log(msg.value) // ‘hello’

// 修改

msg.value = ‘bye’

为什么需要包装对象?

我们知道在 JavaScript 中,原始值类型如 string 和 number 是只有值,没有引用的。如果在一个函数中返回一个字符串变量,接收到这个字符串的代码只会获得一个值,是无法追踪原始变量后续的变化的。

因此,包装对象的意义就在于提供一个让我们能够在函数之间以引用的方式传递任意类型值的容器。这有点像 React Hooks 中的 useRef —— 但不同的是 Vue 的包装对象同时还是响应式的数据源。有了这样的容器,我们就可以在封装了逻辑的组合函数中将状态以引用的方式传回给组件。组件负责展示(追踪依赖),组合函数负责管理状态(触发更新):

setup() {

const valueA = useLogicA() // valueA 可能被 useLogicA() 内部的代码修改从而触发更新

const valueB = useLogicB()

return {

valueA,

valueB

}

}

包装对象也可以包装非原始值类型的数据,被包装的对象中嵌套的属性都会被响应式地追踪。用包装对象去包装对象或是数组并不是没有意义的:它让我们可以对整个对象的值进行替换 —— 比如用一个 filter 过的数组去替代原数组:

const numbers = value([1, 2, 3])

// 替代原数组,但引用不变

numbers.value = numbers.value.filter(n => n > 1)

如果你依然想创建一个没有包装的响应式对象,可以使用 stateAPI(和 2.x 的 Vue.observable()等同):

import { state } from ‘vue’

const object = state({

count: 0

})

object.count++

Value Unwrapping(包装对象的自动展开)

在上面的一个例子中你可能注意到了,虽然 setup()返回的 msg是一个包装对象,但在模版中我们直接用了 {{ msg }}这样的绑定,没有用 .value。这是因为当包装对象被暴露给模版渲染上下文,或是被嵌套在另一个响应式对象中的时候,它会被自动展开 (unwrap) 为内部的值。

比如一个包装对象的绑定可以直接被模版中的内联函数修改:

const MyComponent = {

setup() {

return {

count: value(0)

}

},

template: <button @click="count++">{{ count }}</button>

}

当一个包装对象被作为另一个响应式对象的属性引用的时候也会被自动展开:

const count = value(0)

const obj = state({

count

})

console.log(obj.count) // 0

obj.count++

console.log(obj.count) // 1

console.log(count.value) // 1

count.value++

console.log(obj.count) // 2

console.log(count.value) // 2const MyComponent = {

setup() {

return {

count: value(0)

}

},

template: <button @click="count++">{{ count }}</button>

}

以上这些关于包装对象的细节可能会让你觉得有些复杂,但实际使用中你只需要记住一个基本的规则:只有当你直接以变量的形式引用一个包装对象的时候才会需要用 .value 去取它内部的值 —— 在模版中你甚至不需要知道它们的存在。

Computed Value (计算值)

除了直接包装一个可变的值,我们也可以包装通过计算产生的值:

import { value, computed } from ‘vue’

const count = value(0)

const countPlusOne = computed(() => count.value + 1)

console.log(countPlusOne.value) // 1

count.value++

console.log(countPlusOne.value) // 2

计算值的行为跟计算属性 (computed property) 一样:只有当依赖变化的时候它才会被重新计算。

computed() 返回的是一个只读的包装对象,它可以和普通的包装对象一样在 setup() 中被返回 ,也一样会在渲染上下文中被自动展开。默认情况下,如果用户试图去修改一个只读包装对象,会触发警告。

双向计算值可以通过传给 computed 第二个参数作为 setter 来创建:

const count = value(0)

const writableComputed = computed(

// read

() => count.value + 1,

// write

val => {

count.value = val - 1

}

)

Watchers

watch() API 提供了基于观察状态的变化来执行副作用的能力。

watch() 接收的第一个参数被称作 “数据源”,它可以是:

  • 一个返回任意值的函数

  • 一个包装对象

  • 一个包含上述两种数据源的数组

第二个参数是回调函数。回调函数只有当数据源发生变动时才会被触发:

watch(

// getter

() => count.value + 1,

// callback

(value, oldValue) => {

console.log('count + 1 is: ', value)

}

)

// -> count + 1 is: 1

count.value++

// -> count + 1 is: 2

和 2.x 的 $watch 有所不同的是,watch() 的回调会在创建时就执行一次。这有点类似 2.x watcher 的 immediate: true 选项,但有一个重要的不同:默认情况下 watch() 的回调总是会在当前的 renderer flush 之后才被调用 —— 换句话说,watch()的回调在触发时,DOM 总是会在一个已经被更新过的状态下。 这个行为是可以通过选项来定制的。

在 2.x 的代码中,我们经常会遇到同一份逻辑需要在 mounted 和一个 watcher 的回调中执行(比如根据当前的 id 抓取数据),3.0 的 watch() 默认行为可以直接表达这样的需求。

观察 props

上面提到了 setup() 接收到的 props 对象是一个可观测的响应式对象:

const MyComponent = {

props: {

id: Number

},

setup(props) {

const data = value(null)

watch(() => props.id, async (id) => {

data.value = await fetchData(id)

})

return {

data

}

}

}

观察包装对象

watch()可以直接观察一个包装对象:

// double 是一个计算包装对象

const double = computed(() => count.value * 2)

watch(double, value => {

console.log('double the count is: ', value)

}) // -> double the count is: 0

count.value++ // -> double the count is: 2

观察多个数据源

watch() 也可以观察一个包含多个数据源的数组 - 这种情况下,任意一个数据源的变化都会触发回调,同时回调会接收到包含对应值的数组作为参数:

watch(

[valueA, () => valueB.value],

([a, b], [prevA, prevB]) => {

console.log(a is: ${a})

console.log(b is: ${b})

}

)

停止观察

watch() 返回一个停止观察的函数:

const stop = watch(…)

// stop watching

stop()

如果 watch() 是在一个组件的 setup() 或是生命周期函数中被调用的,那么该 watcher 会在当前组件被销毁时也一同被自动停止:

export default {

setup() {

// 组件销毁时也会被自动停止

watch(/* … */)

}

}

清理副作用

有时候当观察的数据源变化后,我们可能需要对之前所执行的副作用进行清理。举例来说,一个异步操作在完成之前数据就产生了变化,我们可能要撤销还在等待的前一个操作。为了处理这种情况,watcher 的回调会接收到的第三个参数是一个用来注册清理操作的函数。调用这个函数可以注册一个清理函数。清理函数会在下属情况下被调用:

  • 在回调被下一次调用前

  • 在 watcher 被停止前

watch(idValue, (id, oldId, onCleanup) => {

const token = performAsyncOperation(id)

onCleanup(() => {

// id 发生了变化,或是 watcher 即将被停止.

// 取消还未完成的异步操作。

token.cancel()

})

})

之所以要用传入的注册函数来注册清理函数,而不是像 React 的 useEffect 那样直接返回一个清理函数,是因为 watcher 回调的返回值在异步场景下有特殊作用。我们经常需要在 watcher 的回调中用 async function 来执行异步操作:

const data = value(null)

watch(getId, async (id) => {

data.value = await fetchData(id)

})

我们知道 async function 隐性地返回一个 Promise - 这样的情况下,我们是无法返回一个需要被立刻注册的清理函数的。除此之外,回调返回的 Promise 还会被 Vue 用于内部的异步错误处理。

Watcher 回调的调用时机

默认情况下,所有的 watcher 回调都会在当前的 renderer flush 之后被调用。这确保了在回调中 DOM 永远都已经被更新完毕。如果你想要让回调在 DOM 更新之前或是被同步触发,可以使用 flush 选项:

watch(

() => count.value + 1,

() => console.log(count changed),

{

flush: ‘post’, // default, fire after renderer flush

flush: ‘pre’, // fire right before renderer flush

flush: ‘sync’ // fire synchronously

}

)

全部的 watch 选项(TS 类型声明)

interface WatchOptions {

lazy?: boolean

deep?: boolean

flush?: ‘pre’ | ‘post’ | ‘sync’

onTrack?: (e: DebuggerEvent) => void

onTrigger?: (e: DebuggerEvent) => void

}

interface DebuggerEvent {

effect: ReactiveEffect

target: any

key: string | symbol | undefined

type: ‘set’ | ‘add’ | ‘delete’ | ‘clear’ | ‘get’ | ‘has’ | ‘iterate’

}

  • lazy与 2.x 的 immediate 正好相反

  • deep与 2.x 行为一致

  • onTrack 和 onTrigger 是两个用于 debug 的钩子,分别在 watcher 追踪到依赖和依赖发生变化的时候被调用,获得的参数是一个包含了依赖细节的 debugger event。

生命周期函数

所有现有的生命周期钩子都会有对应的 onXXX 函数(只能在 setup() 中使用):

import { onMounted, onUpdated, onUnmounted } from ‘vue’

const MyComponent = {

setup() {

onMounted(() => {

console.log(‘mounted!’)

})

onUpdated(() => {

console.log(‘updated!’)

})

// destroyed 调整为 unmounted

onUnmounted(() => {

console.log(‘unmounted!’)

})

}

}

依赖注入

import { provide, inject } from ‘vue’

const CountSymbol = Symbol()

const Ancestor = {

setup() {

// providing a value can make it reactive

const count = value(0)

provide({

})

}

}

const Descendent = {

setup() {

const count = inject(CountSymbol)

return {

count

}

}

}

如果注入的是一个包装对象,则该注入绑定会是响应式的(也就是说,如果 Ancestor 修改了 count,会触发 Descendent 的更新)。

类型推导

为了能够在 TypeScript 中提供正确的类型推导,我们需要通过一个函数来定义组件:

import { createComponent } from ‘vue’

const MyComponent = createComponent({

props: {

msg: String

},

setup(props) {

watch(() => props.msg, msg => { /* … */ })

return {

count: value(0)

}

},

render({ state, props }) {

// state typing inferred from value returned by setup()

console.log(state.count)

// props typing inferred from props declaration

console.log(props.msg)

// this exposes both state and props

console.log(this.count)

console.log(this.msg)

}

})

createComponent 从概念上来说和 2.x 的 Vue.extend 是一样的,但在 3.0 中它其实是单纯为了类型推导而存在的,内部实现是个 noop(直接返回参数本身)。它的返回类型可以用于 TSX 和 Vetur 的模版自动补全。如果你使用单文件组件,则 Vetur 可以自动隐式地帮你添加这个调用。

Required Props

Props 默认都是可选的,也就是说它们的类型都可能是 undefined。非可选的 props 需要声明 required: true :

import { createComponent } from ‘vue’

createComponent({

props: {

foo: {

type: String,

required: true

},

bar: {

type: String

}

} as const,

setup(props) {

props.foo // string

props.bar // string | undefined

}

})

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
组件,则 Vetur 可以自动隐式地帮你添加这个调用。

Required Props

Props 默认都是可选的,也就是说它们的类型都可能是 undefined。非可选的 props 需要声明 required: true :

import { createComponent } from ‘vue’

createComponent({

props: {

foo: {

type: String,

required: true

},

bar: {

type: String

}

} as const,

setup(props) {

props.foo // string

props.bar // string | undefined

}

})

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-5MLQRG3y-1715237804517)]

[外链图片转存中…(img-fc53NPcw-1715237804517)]

[外链图片转存中…(img-6pPUSSe1-1715237804518)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值