看完上一章 初入茅庐
以后,相信你们已经对vue-next(Vue 3.0)有所了解了。本章带你掌握 vue-next
函数式的API,了解这些的话,不管是对于源码的阅读,仍是当正式版发布时开始学习,应该都会有起到必定的辅助做用。javascript
直接拷贝下面代码,去运行看效果吧。推荐使用高版本的chrome浏览器,记得打开F12调试工具哦!html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script src="https://s1.zhuanstatic.com/common/js/vue-next-3.0.0-alpha.0.js"></script>
<div id="app"></div>
<script> const { ref, reactive, createApp, watch, effect } = Vue function useMouse() { const x = ref(0) const y = ref(0) const update = e => { x.value = e.pageX y.value = e.pageY } Vue.onMounted(() => { window.addEventListener('mousemove', update) }) Vue.onUnmounted(() => { window.removeEventListener('mousemove', update) }) return { x, y } } const App = { props: { age: Number }, // Composition API 使用的入口 setup(props, context){ console.log('props.age', props.age) // 定义响应数据 const state = reactive({name:'zhuanzhuan'}); // 使用公共逻辑 const {x,y} = useMouse(); Vue.onMounted(()=>{ console.log('当组挂载完成') }); Vue.onUpdated(()=>{ console.log('数据发生更新') }); Vue.onUnmounted(()=>{ console.log('组件将要卸载') }) function changeName(){ state.name = '转转'; } // 建立监视,并获得 中止函数 const stop = watch(() => console.log(`watch state.name:`, state.name)) // 调用中止函数,清除对应的监视 // stop() // 观察包装对象 watch(() => state.name, (value, oldValue) => console.log(`watch state.name value:${value} oldValue:${oldValue}`)) effect(() => { console.log(`effect 触发了! 名字是:${state.name},年龄:${props.age}`) }) // 返回上下文,能够在模板中使用 return { // state: Vue.toRefs(state), // 也能够这样写,将 state 上的每一个属性,都转化为 ref 形式的响应式数据 state, x, y, changeName, } }, template:`<button @click="changeName">名字是:{{state.name}} 鼠标x: {{x}} 鼠标: {{y}}</button>` } createApp().mount(App, '#app', {age: 123}); </script>
</body>
</html>
复制代码
组件 API 设计所面对的核心问题之一就是如何组织逻辑,以及如何在多个组件之间抽取和复用逻辑。基于 Vue 2.x 目前的 API 有一些常见的逻辑复用模式,但都或多或少存在一些问题。这些模式包括:vue
网络上关于这些模式的介绍不少,这里就再也不赘述细节。整体来讲,以上这些模式存在如下问题:java
模版中的数据来源不清晰。举例来讲,当一个组件中使用了多个 mixin
的时候,光看模版会很难分清一个属性究竟是来自哪个 mixin
。HOC
也有相似的问题。react
命名空间冲突。由不一样开发者开发的 mixin
没法保证不会正好用到同样的属性或是方法名。HOC
在注入的 props
中也存在相似问题。git
性能。HOC
和 Renderless Components
都须要额外的组件实例嵌套来封装逻辑,致使无谓的性能开销。github
从以上useMouse
例子中能够看到:chrome
vue-next
的一个主要设计目标是加强对 TypeScript
的支持。本来指望经过 Class API
来达成这个目标,可是通过讨论和原型开发,认为 Class
并非解决这个问题的正确路线,基于 Class
的 API
依然存在类型问题。api
基于函数的 API
自然对类型推导很友好,由于 TS
对函数的参数、返回值和泛型的支持已经很是完备。更值得一提的是基于函数的 API
在使用 TS
或是原生 JS
时写出来的代码几乎是彻底同样的。数组
咱们将会引入一个新的组件选项,setup()
。顾名思义,这个函数将会是咱们 setup
咱们组件逻辑的地方,它会在一个组件实例被建立时,初始化了 props 以后调用。它为咱们使用 vue-next
的 Composition API
新特性提供了统一的入口。
setup
函数会在 beforeCreate
以后、created
以前执行。
声明 state
主要有如下几种类型。
基础类型能够经过 ref
这个api
来声明,以下:
const App = {
setup(props, context){
const msg = ref('hello')
function appendName(){
msg.value = `hello ${props.name}`
}
return {appendName, msg}
},
template:`<div @click="appendName">{{ msg }}</div>`
}
复制代码
咱们知道在 JavaScript
中,原始值类型如 string
和 number
是只有值,没有引用的。若是在一个函数中返回一个字符串变量,接收到这个字符串的代码只会得到一个值,是没法追踪原始变量后续的变化的。
所以,包装对象的意义就在于提供一个让咱们可以在函数之间以引用的方式传递任意类型值的容器。这有点像 React Hooks
中的 useRef
—— 但不一样的是 Vue
的包装对象同时仍是响应式的数据源。有了这样的容器,咱们就能够在封装了逻辑的组合函数中将状态以引用的方式传回给组件。组件负责展现(追踪依赖),组合函数负责管理状态(触发更新):
setup(props, context){
// x,y 可能被 useMouse() 内部的代码修改从而触发更新
const {x,y} = useMouse();
return { x, y }
}
复制代码
包装对象也能够包装非原始值类型的数据,被包装的对象中嵌套的属性都会被响应式地追踪。用包装对象去包装对象或是数组并非没有意义的:它让咱们能够对整个对象的值进行替换 —— 好比用一个 filter
过的数组去替代原数组:
const numbers = ref([1, 2, 3])
// 替代原数组,但引用不变
numbers.value = numbers.value.filter(n => n > 1)
复制代码
这里补充一下,在 基础类型 第一个例子中你可能注意到了,虽然 setup()
返回的 msg
是一个包装对象,但在模版中咱们直接用了 {{ msg }}
这样的绑定,没有用 .value
。这是由于当包装对象被暴露给模版渲染上下文,或是被嵌套在另外一个响应式对象中的时候,它会被自动展开 (unwrap)
为内部的值。
引用类型除了可使用 ref
来声明,也能够直接使用 reactive
,以下:
const App = {
setup(props, context){
const state = reactive({name:'zhuanzhuan'});
function changeName(){
state.name = '转转';
}
return {state, changeName, msg}
},
template:`<button @click="changeName">名字是:{{state.name}}</button>`
}
复制代码
props
中定义当前组件容许外界传递过来的参数名称:props: {
age: Number
}
复制代码
setup
函数的第一个形参,接收 props
数据:setup(props) {
console.log('props.age', props.age)
watch(() => props.age, (value, oldValue) => console.log(`watch props.age value:${value} oldValue:${oldValue}`))
}
复制代码
除此以外,还能够直接经过 watch
方法来观察某个 prop
的变更,这是为何呢?答案很是简单,就是 props
自己在源码中,也是一个被 reactive
包裹后的对象,所以它具备响应性,因此在watch
方法中的回调函数会自动收集依赖,以后当 age
变更时,会自动调用这些回调逻辑。
setup
函数的第二个形参是一个上下文对象,这个上下文对象中包含了一些有用的属性,这些属性在 vue 2.x
中须要经过 this
才能访问到,那我想经过 this
像在 vue2
中访问一些内置属性,怎么办?好比 attrs
或者 emit
。咱们能够经过 setup 的第二个参数,在 vue-next
中,它们的访问方式以下:
const MyComponent = {
setup(props, context) {
context.attrs
context.slots
context.parent
context.root
context.emit
context.refs
}
}
复制代码
注意:==在 setup()
函数中没法访问到 this
==
reactive()
函数接收一个普通对象,返回一个响应式的数据对象。
等价于 vue 2.x
中的 Vue.observable()
函数,vue 3.x
中提供了 reactive()
函数,用来建立响应式的数据对象,基本代码示例以下:
// 建立响应式数据对象,获得的 state 相似于 vue 2.x 中 data() 返回的响应式对象
const state = reactive({name:'zhuanzhuan'});
复制代码
const { reactive } = Vue
复制代码
setup()
函数中调用 reactive()
函数,建立响应式数据对象:const { reactive } = Vue
setup(props, context){
const state = reactive({name:'zhuanzhuan'});
return state
}
复制代码
template
中访问响应式数据:template:`<button>名字是:{{name}} </button>`
复制代码
Value Unwrapping(包装对象的自动展开)
ref()
函数用来根据给定的值建立一个响应式的数据对象,ref()
函数调用的返回值是一个对象,这个对象上只包含一个 .value
属性。
const { ref } = Vue
// 建立响应式数据对象 age,初始值为 3
const age = ref(3)
// 若是要访问 ref() 建立出来的响应式数据对象的值,必须经过 .value 属性才能够
console.log(age.value) // 输出 3
// 让 age 的值 +1
age.value++
// 再次打印 age 的值
console.log(age.value) // 输出 4
复制代码
setup()
中建立响应式数据:setup() {
const age = ref(3)
return {
age,
name: ref('zhuanzhuan')
}
}
复制代码
template
中访问响应式数据:template:`<p>名字是:{{name}},年龄是{{age}}</p>`
复制代码
当把 ref()
建立出来的响应式数据对象,挂载到 reactive()
上时,会自动把响应式数据对象展开为原始的值,不需经过 .value
就能够直接被访问。
换句话说就是当一个包装对象被做为另外一个响应式对象的属性引用的时候也会被自动展开例如:
const age = ref(3)
const state = reactive({
age
})
console.log(state.age) // 输出 3
state.age++ // 此处不须要经过 .value 就能直接访问原始值
console.log(age) // 输出 4
复制代码
以上这些关于包装对象的细节可能会让你以为有些复杂,但实际使用中你只须要记住一个基本的规则:只有当你直接以变量的形式引用一个包装对象的时候才会须要用 .value 去取它内部的值 —— 在模版中你甚至不须要知道它们的存在。
==注意:新的 ref 会覆盖旧的 ref,示例代码以下:==
// 建立 ref 并挂载到 reactive 中
const c1 = ref(0)
const state = reactive({
c1
})
// 再次建立 ref,命名为 c2
const c2 = ref(9)
// 将 旧 ref c1 替换为 新 ref c2
state.c1 = c2
state.c1++
console.log(state.c1) // 输出 10
console.log(c2.value) // 输出 10
console.log(c1.value) // 输出 0
复制代码
isRef()
用来判断某个值是否为 ref()
建立出来的对象;应用场景:当须要展开某个可能为 ref()
建立出来的值的时候,例如:
const { isRef } = Vue
const unwrapped = isRef(foo) ? foo.value : foo
复制代码
const { toRefs } = Vue
setup() {
// 定义响应式数据对象
const state = reactive({
age: 3
})
// 定义页面上可用的事件处理函数
const increment = () => {
state.age++
}
// 在 setup 中返回一个对象供页面使用
// 这个对象中能够包含响应式的数据,也能够包含事件处理函数
return {
// 将 state 上的每一个属性,都转化为 ref 形式的响应式数据
...toRefs(state),
// 自增的事件处理函数
increment
}
}
复制代码
页面上能够直接访问 setup() 中 return 出来的响应式数据:
template:` <div> <p>当前的age值为:{{age}}</p> <button @click="increment">+1</button> </div> `
复制代码
computed()
用来建立计算属性,computed()
函数的返回值是一个 ref
的实例。使用 computed
以前须要按需导入:
const { computed } = Vue
复制代码
const { computed } = Vue
// 建立一个 ref 响应式数据
const count = ref(1)
// 根据 count 的值,建立一个响应式的计算属性 plusOne
// 它会根据依赖的 ref 自动计算并返回一个新的 ref
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 输出 2
plusOne.value++ // error
复制代码
在调用 computed()
函数期间,传入一个包含 get
和 set
函数的对象,能够获得一个可读可写的计算属性,示例代码以下:
const { computed } = Vue
// 建立一个 ref 响应式数据
const count = ref(1)
// 建立一个 computed 计算属性
const plusOne = computed({
// 取值函数
get: () => count.value + 1,
// 赋值函数
set: val => { count.value = val - 1 }
})
// 为计算属性赋值的操做,会触发 set 函数
plusOne.value = 9
// 触发 set 函数后,count 的值会被更新
console.log(count.value) // 输出 8
复制代码
watch()
函数用来监视某些数据项的变化,从而触发某些特定的操做,使用以前须要按需导入:
const { watch } = Vue
复制代码
const { watch } = Vue
const count = ref(0)
// 定义 watch,只要 count 值变化,就会触发 watch 回调
// watch 会在建立时会自动调用一次
watch(() => console.log(count.value))
// 输出 0
setTimeout(() => {
count.value++
// 输出 1
}, 1000)
复制代码
监视 reactive
类型的数据源:
const { watch, reactive } = Vue
const state = reactive({name:'zhuanzhuan'});
watch(() => state.name, (value, oldValue) => { /* ... */ })
复制代码
const { watch, ref } = Vue
// 定义数据源
const count = ref(0)
// 指定要监视的数据源
watch(count, (value, oldValue) => { /* ... */ })
复制代码
监视 reactive 类型的数据源:
const { reactive, watch, ref } = Vue
onst state = reactive({ age: 3, name: 'zhuanzhuan' })
watch(
[() => state.age, () => state.name], // Object.values(toRefs(state)),
([age, name], [prevCount, prevName]) => {
console.log(age) // 新的 age 值
console.log(name) // 新的 name 值
console.log('------------')
console.log(prevCount) // 旧的 age 值
console.log(prevName) // 新的 name 值
},
{
lazy: true // 在 watch 被建立的时候,不执行回调函数中的代码
}
)
setTimeout(() => {
state.age++
state.name = '转转'
}, 1000)
复制代码
在 setup()
函数内建立的 watch
监视,会在当前组件被销毁的时候自动中止。若是想要明确地中止某个监视,能够调用 watch()
函数的返回值便可,语法以下
// 建立监视,并获得 中止函数
const stop = watch(() => { /* ... */ })
// 调用中止函数,清除对应的监视
stop()
复制代码
有时候,当被 watch
监视的值发生变化时,或 watch
自己被 stop
以后,咱们指望可以清除那些无效的异步任务,此时,watch
回调函数中提供了一个 cleanup registrator function
来执行清除的工做。这个清除函数会在以下状况下被调用:
Template 中的代码示例以下:
/* template 中的代码 */
<input type="text" v-model="keywords" />
复制代码
Script 中的代码示例以下:
// 定义响应式数据 keywords
const keywords = ref('')
// 异步任务:打印用户输入的关键词
const asyncPrint = val => {
// 延时 1 秒后打印
return setTimeout(() => {
console.log(val)
}, 1000)
}
// 定义 watch 监听
watch(
keywords,
(keywords, prevKeywords, onCleanup) => {
// 执行异步任务,并获得关闭异步任务的 timerId
const timerId = asyncPrint(keywords)
// keywords 发生了变化,或是 watcher 即将被中止.
// 取消还未完成的异步操做。
// 若是 watch 监听被重复执行了,则会先清除上次未完成的异步任务
onCleanup(() => clearTimeout(timerId))
},
// watch 刚被建立的时候不执行
{ lazy: true }
)
// 把 template 中须要的数据 return 出去
return {
keywords
}
复制代码
之因此要用传入的注册函数来注册清理函数,而不是像 React
的 useEffect
那样直接返回一个清理函数,是由于watcher
回调的返回值在异步场景下有特殊做用。咱们常常须要在 watcher
的回调中用 async function
来执行异步操做:
const data = ref(null)
watch(getId, async (id) => {
data.value = await fetchData(id)
})
复制代码
咱们知道 async function
隐性地返回一个 Promise
- 这样的状况下,咱们是没法返回一个须要被马上注册的清理函数的。除此以外,回调返回的 Promise
还会被 Vue
用于内部的异步错误处理。
默认状况下,全部的 watch
回调都会在当前的 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
}
)
复制代码
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'
}
复制代码
全部现有的生命周期钩子都会有对应的 onXXX 函数(只能在 setup() 中使用):
const { onMounted, onUpdated, onUnmounted } = Vue
const MyComponent = {
setup() {
onMounted(() => {
console.log('mounted!')
})
onUpdated(() => {
console.log('updated!')
})
// destroyed 调整为 unmounted
onUnmounted(() => {
console.log('unmounted!')
})
}
}
复制代码
下面的列表,是 vue 2.x
的生命周期函数与新版 Composition API
之间的映射关系:
beforeCreate
-> setup()
created
-> setup()
beforeMount
-> onBeforeMount
mounted
-> onMounted
beforeUpdate
-> onBeforeUpdate
updated
-> onUpdated
beforeDestroy
-> onBeforeUnmount
destroyed
-> onUnmounted
errorCaptured
-> onErrorCaptured
provide()
和 inject()
能够实现嵌套组件之间的数据传递。这两个函数只能在 setup()
函数中使用。父级组件中使用 provide()
函数向下传递数据;子级组件中使用 inject()
获取上层传递过来的数据。
App.vue
根组件:
<template>
<div id="app">
<h1>App 根组件</h1>
<hr />
<LevelOne />
</div>
</template>
<script> import LevelOne from './components/LevelOne' // 1. 按需导入 provide import { provide } from '@vue/composition-api' export default { name: 'app', setup() { // 2. App 根组件做为父级组件,经过 provide 函数向子级组件共享数据(不限层级) // provide('要共享的数据名称', 被共享的数据) provide('globalColor', 'red') }, components: { LevelOne } } </script>
复制代码
LevelOne.vue
组件:
<template>
<div>
<!-- 4. 经过属性绑定,为标签设置字体颜色 -->
<h3 :style="{color: themeColor}">Level One</h3>
<hr />
<LevelTwo />
</div>
</template>
<script> import LevelTwo from './LevelTwo' // 1. 按需导入 inject import { inject } from '@vue/composition-api' export default { setup() { // 2. 调用 inject 函数时,经过指定的数据名称,获取到父级共享的数据 const themeColor = inject('globalColor') // 3. 把接收到的共享数据 return 给 Template 使用 return { themeColor } }, components: { LevelTwo } } </script>
复制代码
LevelTwo.vue
组件:
<template>
<div>
<!-- 4. 经过属性绑定,为标签设置字体颜色 -->
<h5 :style="{color: themeColor}">Level Two</h5>
</div>
</template>
<script> // 1. 按需导入 inject import { inject } from '@vue/composition-api' export default { setup() { // 2. 调用 inject 函数时,经过指定的数据名称,获取到父级共享的数据 const themeColor = inject('globalColor') // 3. 把接收到的共享数据 return 给 Template 使用 return { themeColor } } } </script>
复制代码
以下代码实现了点按钮切换主题颜色的功能,主要修改了 App.vue
组件中的代码,LevelOne.vue
和 LevelTwo.vue
中的代码不受任何改变:
<template>
<div id="app">
<h1>App 根组件</h1>
<!-- 点击 App.vue 中的按钮,切换子组件中文字的颜色 -->
<button @click="themeColor='red'">红色</button>
<button @click="themeColor='blue'">蓝色</button>
<button @click="themeColor='orange'">橘黄色</button>
<hr />
<LevelOne />
</div>
</template>
<script> import LevelOne from './components/LevelOne' import { provide, ref } from '@vue/composition-api' export default { name: 'app', setup() { // 定义 ref 响应式数据 const themeColor = ref('red') // 把 ref 数据经过 provide 提供的子组件使用 provide('globalColor', themeColor) // setup 中 return 数据供当前组件的 Template 使用 return { themeColor } }, components: { LevelOne } } </script>
复制代码
经过 ref()
还能够引用页面上的元素或组件。
示例代码以下:
<template>
<div>
<h3 ref="h3Ref">TemplateRefOne</h3>
</div>
</template>
<script> import { ref, onMounted } from '@vue/composition-api' export default { setup() { // 建立一个 DOM 引用 const h3Ref = ref(null) // 在 DOM 首次加载完毕以后,才能获取到元素的引用 onMounted(() => { // 为 dom 元素设置字体颜色 // h3Ref.value 是原生DOM对象 h3Ref.value.style.color = 'red' }) // 把建立的引用 return 出去 return { h3Ref } } } </script>
复制代码
TemplateRefOne.vue
中的示例代码以下:
<template>
<div>
<h3>TemplateRefOne</h3>
<!-- 4. 点击按钮展现子组件的 count 值 -->
<button @click="showNumber">获取TemplateRefTwo中的count值</button>
<hr />
<!-- 3. 为组件添加 ref 引用 -->
<TemplateRefTwo ref="comRef" />
</div>
</template>
<script> import { ref } from '@vue/composition-api' import TemplateRefTwo from './TemplateRefTwo' export default { setup() { // 1. 建立一个组件的 ref 引用 const comRef = ref(null) // 5. 展现子组件中 count 的值 const showNumber = () => { console.log(comRef.value.count) } // 2. 把建立的引用 return 出去 return { comRef, showNumber } }, components: { TemplateRefTwo } } </script>
复制代码
TemplateRefTwo.vue
中的示例代码:
<template>
<div>
<h5>TemplateRefTwo --- {{count}}</h5>
<!-- 3. 点击按钮,让 count 值自增 +1 -->
<button @click="count+=1">+1</button>
</div>
</template>
<script> import { ref } from '@vue/composition-api' export default { setup() { // 1. 定义响应式的数据 const count = ref(0) // 2. 把响应式数据 return 给 Template 使用 return { count } } } </script>
复制代码
这个函数不是必须的,除非你想要完美结合 TypeScript
提供的类型推断来进行项目的开发。
这个函数仅仅提供了类型推断,方便在结合 TypeScript
书写代码时,能为 setup()
中的 props
提供完整的类型推断。
import { createComponent } from 'vue'
export default createComponent({
props: {
foo: String
},
setup(props) {
props.foo // <- type: string
}
}
复制代码
以上就是 vue-next(Vue 3.0) API,相信你们已经能够灵活运用了吧。
那么你们必定很好奇 vue-next
响应式的原理,下一章vue-next(Vue 3.0)之 炉火纯青
带你解密。