Composition API
Options API的弊端
在Vue2中,我们编写组件的方式是Options API:
- Options API的一大特点就是在对应的属性中编写对应的功能模块;
- 比如
data定义数据、methods中定义方法、computed中定义计算属性、watch中监听属性改变,也包括生命周期钩子;
但是这种代码有一个很大的弊端:
- 当我们实现某一个功能时,这个功能对应的代码逻辑会被拆分到各个属性中;
- 当我们组件变得更大、更复杂时,逻辑关注点的列表就会增长,那么同一个功能的逻辑就会被拆分的很分散;
- 尤其对于那些一开始没有编写这些组件的人来说,这个组件的代码是难以阅读和理解的(阅读组件的其他人);
下面我们来看一个非常大的组件,其中的逻辑功能按照颜色进行了划分:
- 这种碎片化的代码使用理解和维护这个复杂的组件变得异常困难,并且隐藏了潜在的逻辑问题;
- 并且当我们处理单个逻辑关注点时,需要不断的跳到相应的代码块中;
如果我们能将同一个逻辑关注点相关的代码收集在一起会更好。
这就是Composition API想要做的事情,以及可以帮助我们完成的事情。
认识Composition API
那么既然知道Composition API想要帮助我们做什么事情,接下来看一下到底是怎么做呢?
- 为了开始使用Composition API,我们需要有一个可以实际使用它(编写代码)的地方;
- 在Vue组件中,这个位置就是 setup 函数;
setup其实就是组件的另外一个选项:
- 只不过这个选项强大到我们可以用它来替代之前所编写的大部分其他选项;
- 比如methods、computed、watch、data、生命周期等等;
setup函数
setup函数的参数
我们先来研究一个setup函数的参数,它主要有两个参数:
- 第一个参数:
props - 第二个参数:
context
props非常好理解,它其实就是父组件传递过来的属性会被放到props对象中,我们在setup中如果需要使用,那么就可以直接通过props参数获取:
- 对于定义props的类型,我们还是和之前的规则是一样的,在props选项中定义;
- 并且在template中依然是可以正常去使用props中的属性,比如message;
- 如果我们在setup函数中想要使用props,那么不可以通过 this 去获取;
因为props有直接作为参数传递到setup函数中,所以我们可以直接通过参数来使用即可;
- 另外一个参数是
context,我们也称之为是一个SetupContext,它里面包含三个属性: attrs:所有的非prop的attribute; -slots:父组件传递过来的插槽(这个在以渲染函数返回时会有作用,后面会讲到);emit:当我们组件内部需要发出事件时会用到emit(因为我们不能访问this,所以不可以通过 this.$emit发出事件);
app.vue
Home.vue
打印结果
setup函数的返回值
setup既然是一个函数,那么它也可以有返回值,它的返回值用来做什么呢?
- setup的返回值可以在模板
template中被使用; - 也就是说我们可以通过setup的返回值来替代data选项;
甚至是我们可以返回一个执行函数来代替在methods中定义的方法:
home.vue 此时引用counter产生了闭包
执行结果
但是,如果我们将 counter 在 increment 或者 decrement进行操作时,是否可以实现界面的响应式呢?
- 答案是不可以;
- 这是因为对于一个定义的变量来说,默认情况下,Vue并不会跟踪它的变化,来引起界面的响应式操作;
setup不可以使用this
- this并没有指向当前组件实例;
- 在setup被调用之前,data、computed、methods等都没有被解析;
- 所以无法在setup中获取this;
最新官方文档中
Reactive API
如果想为在setup中定义的数据提供响应式的特性,那么我们可以使用reactive的函数:
那么这是什么原因呢?为什么就可以变成响应式的呢?
- 这是因为当我们使用
reactive函数处理我们的数据之后,数据再次被使用时就会进行依赖收集; - 当数据发生改变时,所有收集到的依赖都是进行对应的
响应式操作(比如更新界面); - 事实上,我们编写的data选项,也是在内部交给了
reactive函数将其编程响应式对象的;
Ref API
reactive API对传入的类型是有限制的,它要求我们必须传入的是一个对象或者数组类型: 如果我们传入一个基本数据类型(String、Number、Boolean)会报一个警告;
这个时候Vue3给我们提供了另外一个API:ref API
- ref 会返回一个可变的响应式对象,该对象作为一个 响应式的引用 维护着它内部的值,这就是ref名称的来源;
- 它内部的值是在ref的 value 属性中被维护的;
这里有两个注意事项:
- 在模板中引入ref的值时,Vue会自动帮助我们进行解包操作,所以我们并不需要在模板中通过 ref.value 的方式来使用;
- 但是在 setup 函数内部,它依然是一个 ref引用, 所以对其进行操作时,我们依然需要使用 ref.value的方式;
这里的counter不用写成counter是因为vue自动帮我们解包了
在setup里被ref包裹的100返回的counter是一个对象,并且此时vue不会帮我们解包,所以在increment函数里需要写counter.value++
打印的结果为
- 模板中的解包是
浅深层的解包(在最新的vue3中,不再是浅层解包) - 如果我们将ref放到一个reactive的属性当中,那么在模板中使用时,它会自动解包:
readonly
我们通过reactive或者ref可以获取到一个响应式的对象,但是某些情况下,我们传入给其他地方(组件)的这个响应式对象希望在另外一个地方(组件)被使用,但是不能被修改,这个时候如何防止这种情况的出现呢?
- Vue3为我们提供了readonly的方法;
- readonly会返回原生对象的只读代理(也就是它依然是一个Proxy,这是一个proxy的set方法被劫持,并且不能对其进行修改);
readonly():接受一个对象 (不论是响应式还是普通的) 或是一个 ref,返回一个原值的只读代理。
只读代理是深层的:对任何嵌套属性的访问都将是只读的。它的 ref 解包行为与 reactive() 相同,但解包得到的值是只读的。
要避免深层级的转换行为,请使用 shallowReadonly() 作替代。
toRefs
如果我们使用ES6的解构语法,对reactive返回的对象进行解构获取值,那么之后无论是修改结构后的变量,还是修改reactive返回的state对象,数据都不再是响应式的:
是因为ES6的解构赋值相当于给reactive返回的对象里的属性赋予一个新的变量,展示在页面中不再是响应式的。
那么有没有办法让我们解构出来的属性是响应式的呢?
- Vue为我们提供了一个toRefs的函数,可以将reactive返回的对象中的属性都转成ref;
- 那么我们再次进行结构出来的 name 和 age 本身都是 ref的;
注意toRefs后的age是一个ref对象,在increment函数中需要写成age.value++
这种做法相当于已经在state.name和ref.value之间建立了 链接,任何一个修改都会引起另外一个变化;
toRefs 在调用时只会为源对象上可以枚举的属性创建 ref。如果要为可能还不存在的属性创建 ref,请改用 toRef。
toRef
如果我们只希望转换一个reactive对象中的属性为ref, 那么可以使用toRef的方法:
注意:来接收
ref时不要写成{age},并且toRef只能接收reactive对象,不能接收普通对象
customRef
创建一个自定义的ref,并对其依赖项跟踪和更新触发进行显示控制:
- 它需要一个工厂函数,该函数接受 track 和 trigger 函数作为参数;
- 并且应该返回一个带有 get 和 set 的对象;
这里我们使用一个的案例:对双向绑定的属性进行debounce(节流)的操作;
computed
计算属性computed:当我们的某些属性是依赖其他状态时,我们可以使用计算属性来处理
- 在前面的Options API中,我们是使用computed选项来完成的;
- 在Composition API中,我们可以在 setup 函数中使用 computed 方法来编写一个计算属性;
如何使用computed呢?
- 方式一:接收一个getter函数,并为 getter 函数返回的值,返回一个不变的 ref 对象;
- 方式二:接收一个具有 get 和 set 的对象,返回一个可变的(可读写)ref 对象;
watchEffect
在Composition API中,我们可以使用watchEffect和watch来完成响应式数据的侦听;
- watchEffect用于自动收集响应式数据的依赖;
- watch需要手动指定侦听的数据源;
当侦听到某些响应式数据变化时,我们希望执行某些操作,这个时候可以使用 watchEffect。
我们来看一个案例:
- 首先,watchEffect传入的函数会被立即执行一次,并且在执行的过程中会收集依赖;
- 其次,只有收集的依赖发生变化时,watchEffect传入的函数才会再次执行;
页面打印
watchEffect的停止侦听
如果在发生某些情况下,我们希望停止侦听,这个时候我们可以获取watchEffect的返回值函数,调用该函数即可。
比如在上面的案例中,我们age达到25的时候就停止侦听:
获取一个stop()函数在需要的时候调用即可
可以看到停止了侦听
watchEffect清除副作用
什么是清除副作用呢?
- 比如在开发中我们需要在侦听函数中执行网络请求,但是在网络请求还没有达到的时候,我们停止了侦听器,或者侦听器侦听函数被再次执行了。
- 那么上一次的网络请求应该被取消掉,这个时候我们就可以清除上一次的副作用;
在我们给watchEffect传入的函数被回调时,其实可以获取到一个参数:onInvalidate
- 当副作用即将重新执行 或者 侦听器被停止 时会执行该函数传入的回调函数;
- 我们可以在传入的回调函数中,执行一些清楚工作;
setup中使用ref
在setup中如何使用ref或者元素或者组件?
其实非常简单,我们只需要定义一个ref对象,绑定到元素或者组件的ref属性上即可;
watchEffect的执行时机
默认情况下,组件的更新会在副作用函数执行之前:
如果我们希望在副作用函数中获取到元素,是否可行呢?
我们会发现打印结果打印了两次:
- 这是因为setup函数在执行时就会立即执行传入的副作用函数,这个时候DOM并没有挂载,所以打印为null;
- 而当DOM挂载时,会给title的ref对象赋值新的值,副作用函数会再次执行,打印出来对应的元素;
如果我们希望在第一次的时候就打印出来对应的元素呢?
- 这个时候我们需要改变副作用函数的执行时机;
- 它的默认值是pre,它会在元素 挂载 或者 更新 之前执行;
- 所以我们会先打印出来一个空的,当依赖的title发生改变时,就会再次执行一次,打印出元素;
我们可以设置副作用函数的执行时机:
默认情况下,侦听器将在组件渲染之前执行。设置 flush: 'post' 将会使侦听器延迟到组件渲染之后再执行。详见回调的触发时机。
Watch
watch的API完全等同于组件watch选项的Property:
- watch需要侦听特定的数据源,并在回调函数中执行副作用;
- 默认情况下它是惰性的,只有当被侦听的源发生变化时才会执行回调;
与watchEffect的比较,watch允许我们:
- 懒执行副作用(第一次不会直接执行);
- 更具体的说明当哪些状态发生变化时,触发侦听器的执行;
- 访问侦听状态变化前后的值;
侦听单个数据源
watch侦听函数的数据源有两种类型:
- 一个getter函数:但是该getter函数必须引用可响应式的对象(比如reactive或者ref);
- 直接写入一个可响应式的对象,reactive或者ref(比较常用的是ref);
类型一:获取一个getter函数
类型二(1):获取reactive对象,newValue和oldValue也是reactive对象
我们可以使用一个getter函数,对可响应对象进行解构:
此时newValue和oldValue都是一个普通函数
类型二(2):获取ref对象,newValue和oldValue都是value本身
侦听多个数据源
侦听器还可以使用数组同时侦听多个源:
此时newValue和oldValue会是一个数组
侦听响应式对象
如果我们希望侦听一个数组或者对象,那么可以使用一个getter函数,并且对可响应对象进行解构:
watch的选项
第三个可选的参数是一个对象,支持以下这些选项:
immediate:在侦听器创建时立即触发回调。第一次调用时旧值是undefined。deep:如果源是对象,强制深度遍历,以便在深层级变更时触发回调。参考深层侦听器。flush:调整回调函数的刷新时机。参考回调的刷新时机及watchEffect()。onTrack / onTrigger:调试侦听器的依赖。参考调试侦听器。
watch 默认是浅层的:被侦听的属性,仅在被赋新值时,才会触发回调函数——而嵌套属性的变化不会触发。如果想侦听所有嵌套的变更,你需要深层侦听器:
生命周期钩子
使用方式注意:
所有罗列在本页的 API 都应该在组件的 setup() 阶段被同步调用。相关细节请看指南 - 生命周期钩子。
组合式 API:依赖注入
Provide函数
我们可以通过 provide来提供数据:
- 可以通过 provide 方法来定义每个 Property;
- pprovide可以传入两个参数:name:提供的属性名称;value:提供的属性值;
App.vue
Inject函数
在 后代组件 中可以通过 inject 来注入需要的属性和对应的值:
- 可以通过 inject 来注入需要的内容;
- inject可以传入两个参数:要 inject 的 property 的 name和 默认值;
数据的响应式
在上面的例子中,我们通过provide传递的是一个普通的属性,而这会导致它不具备响应性
为了增加 provide 值和 inject 值之间的响应性,我们可以在 provide 值时使用 ref 和 reactive。
在App组件中,用provide传递一个ref对象,并且用readonly使传过去的属性只读,可以避免传过去的数据在子组件中被修改
在home组件中,homeAdd无法操作数据
组合式函数
在 Vue 应用的概念中,“组合式函数”(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。
我们直接来看例子吧! 我们可以将关于counter的代码抽离出来封装成一个函数,这会使得我们需要的功能在任何地方都能实现复用,同时,它更加优雅
App.vue
useCounter.js
useTitle.js
useScroll.js
渲染函数 & JSX
h()函数
Vue推荐在绝大数情况下使用模板来创建你的HTML,然后一些特殊的场景,你真的需要JavaScript的完全编程的能力,这个时候你可以使用 渲染函数 ,它比模板更接近编译器;
前面我们讲解过VNode和VDOM的改变:
- Vue在生成真实的DOM之前,会将我们的节点转换成VNode,而VNode组合在一起形成一颗树结构,就是虚拟DOM(VDOM);
- 事实上,我们之前编写的 template 中的HTML 最终也是使用渲染函数生成对应的VNode;
- 那么,如果你想充分的利用JavaScript的编程能力,我们可以自己来编写 createVNode 函数,生成对应的VNode;
那么我们应该怎么来做呢?使用 h()函数:
- h() 函数是一个用于创建 vnode 的一个函数;
- 其实更准备的命名是 createVNode() 函数,但是为了简便在Vue将之简化为 h() 函数;
h()函数 如何使用呢?它接受三个参数:
注意事项:
- 如果没有props,那么通常可以将children作为第二个参数传入;
- 如果会产生歧义,可以将null作为第二个参数传入,将children作为第三个参数传入;
// 除了类型必填以外,其他的参数都是可选的
h('div')
h('div', { id: 'foo' })
// attribute 和 property 都能在 prop 中书写
// Vue 会自动将它们分配到正确的位置
h('div', { class: 'bar', innerHTML: 'hello' })
// props modifiers such as .prop and .attr can be added
// with '.' and `^' prefixes respectively
h('div', { '.name': 'some-name', '^width': '100' })
// 类与样式可以像在模板中一样
// 用数组或对象的形式书写
h('div', { class: [foo, { bar }], style: { color: 'red' } })
// 事件监听器应以 onXxx 的形式书写
h('div', { onClick: () => {} })
// children 可以是一个字符串
h('div', { id: 'foo' }, 'hello')
// 没有 props 时可以省略不写
h('div', 'hello')
h('div', [h('span', 'hello')])
// children 数组可以同时包含 vnodes 与字符串
h('div', ['hello', h('span', 'hello')])
h函数可以在两个地方使用:
render函数选项中;setup函数选项中(setup本身需要是一个函数类型,函数再返回h函数创建的VNode);
h函数计数器案例
JSX
create-vue 和 Vue CLI 都有预置的 JSX 语法支持。如果你想手动配置 JSX,请参阅 @vue/babel-plugin-jsx 文档获取更多细节。
JSX 是 JavaScript 的一个类似 XML 的扩展,有了它,我们可以用以下的方式来书写代码:
const vnode = <div>hello</div>
在 JSX 表达式中,使用大括号来嵌入动态值:
const vnode = <div id={dynamicId}>hello, {userName}</div>
jsx计数器案例
自定义指令
在Vue的模板语法中我们学习过各种各样的指令:v-show、v-for、v-model等等,除了使用这些指令之外,Vue也允许我们来自定义自己的指令。
注意:在Vue中,代码的复用和抽象主要还是通过组件; 通常在某些情况下,你需要对DOM元素进行底层操作,这个时候就会用到自定义指令;
自定义指令分为两种:
- 自定义局部指令:组件中通过 directives 选项,只能在当前组件中使用;
- 自定义全局指令:app的 directive 方法,可以在任意组件中被使用;
实现方式
比如我们来做一个非常简单的案例:当某个元素挂载完成后可以自定获取焦点
- 实现方式一:如果我们使用默认的实现方式;
- 实现方式二:自定义一个 v-focus 的局部指令;
- 实现方式三:自定义一个 v-focus 的全局指令;
实现方式一:聚焦的默认实现
实现方式二:局部自定义指令
实现方式二:自定义一个 v-focus 的局部指令
- 这个自定义指令实现非常简单,我们只需要在组件选项中使用 directives 即可;
- 它是一个对象,在对象中编写我们自定义指令的名称(注意:这里不需要加v-);
- 自定义指令有一个生命周期,是在组件挂载后调用的 mounted,我们可以在其中完成操作;
实现方式三:自定义全局指令
自定义一个全局的v-focus指令可以让我们在任何地方直接使用
指令钩子
一个指令定义的对象,Vue提供了如下的几个钩子函数:
- created:在绑定元素的 attribute 或事件监听器被应用之前调用;
- beforeMount:当指令第一次绑定到元素并且在挂载父组件之前调用;
- mounted:在绑定元素的父组件被挂载后调用;
- beforeUpdate:在更新包含组件的 VNode 之前调用;
- updated:在包含组件的 VNode 及其子组件的 VNode 更新后调用;
- beforeUnmount:在卸载绑定元素的父组件之前调用;
- unmounted:当指令与元素解除绑定且父组件已卸载时,只调用一次;
指令的参数和修饰符
如果我们指令需要接受一些参数或者修饰符应该如何操作呢?
- info是参数的名称;
- aaa-bbb是修饰符的名称;
- 后面是传入的具体的值;
在我们的生命周期中,我们可以通过 bindings 获取到对应的内容:
自定义指令练习
自定义指令案例:时间戳的显示需求:
- 在开发中,大多数情况下从服务器获取到的都是时间戳;
- 我们需要将时间戳转换成具体格式化的时间来展示;
- 在Vue2中我们可以通过过滤器来完成;
- 在Vue3中我们可以通过
计算属性(computed)或者 自定义一个方法(methods)来完成; - 其实我们还可以通过一个自定义的指令来完成;
我们来实现一个可以自动对时间格式化的指令v-format-time:
- 这里我封装了一个函数,在首页中我们只需要调用这个函数并且传入app即可;
Vue插件
通常我们向Vue全局添加一些功能时,会采用插件的模式,它有两种编写方式:
- 对象类型:一个对象,但是必须包含一个 install 的函数,该函数会在安装插件时执行;
- 函数类型:一个function,这个函数会在安装插件时自动执行;
插件可以完成的功能没有限制,比如下面的几种都是可以的:
- 添加全局方法或者 property,通过把它们添加到 config.globalProperties 上实现;
- 添加全局资源:指令/过滤器/过渡等;
- 通过全局 mixin 来添加一些组件选项;
- 一个库,提供自己的 API,同时提供上面提到的一个或多个功能;
插件的编写方式
对象类型的写法
函数类型的写法