组合API

组合API

组合(Composition)API是在vue3中引入的,它是一组附加的、基于函数的API,允许灵活地组合组件逻辑。

组合API没有引入新的概念,更多的是将vue的核心功能(如创建和观察响应状态)公开为独立的函数,以此来代替vue2中的表达组件逻辑的选项。

为什么要引入组合API

使用vue构建中小型应用程序是很容易的,但用vue构建大型项目,这些项目由一个多名开发人员的团队在很长一段时间内迭代和维护,在一些项目中遇到了vue2所要求的编程模型的限制,遇到的问题可以归纳为以下两类:

  • 随着时间推移,复杂组件的代码越来越难理解,尤其是当开发人员在阅读不是自己编写的代码时。根本原因是vue2的现有API强制按选项组织代码,但在某些情况下,按逻辑关注点组织代码更有意义。
  • 缺乏在多个组件之间提取和重用逻辑的干净且无成本的机制。

vue3新增的组合API为用户组织组件代码提供了更大的灵活性。现在,可以将代码编写为函数,每个函数处理一个特定的功能,而不再需要按选项组织代码了。组合API还使在组件之间甚至外部组件之间提取和重用逻辑变得更加简单。

此外,由于组合API是一套基于函数的API,因此能够更好地与TypeScript集成,使用组合API编写的代码可以享受完整的类型推断。

组合API也可以与现有的基于选项的API一起使用,不过组合API是在选项(data、computed、methods)之前解析,因此在组合API中是无法访问这些选项定义的属性的。

setup()函数

setup函数是一个新的组件选项,它作为在组件内部使用组合API的入口点。setup函数在初始的prop解析之后,组件实例创建之前被调用。对于组件的生命周期钩子,setup函数在beforeCreate钩子之前调用。

如果setup函数返回一个对象,该对象上的属性将被合并到组件模板的渲染上下文中。

setup(){
    //为目标对象创建一个响应式对象
    const state = Vue.reactive({count:0});
    function increment(){
        state.count++;
    }
    //返回一个对象,该对象上的属性可以在模板中使用
    return {
        state,
        increment
    }
}

setup函数返回的对象有两个属性,一个是响应式对象(即为原始对象创建的代理对象),另一个是函数。在DOM模板中,可以直接使用这两个属性:

<button @click="increment">count值:{{ state.count }}</button>

需要注意的是,当和现有的基于选项的API一起使用时,从setup函数返回的属性在选项中可以通过this访问。

setup函数可以接受两个可选的参数,第一个参数是已解析的props,通过该参数可以访问在props选项中定义的prop:

app.component('PostItem',{
    props:['postTitle'],
    setup(props){
        console.log(props.postTitle)
    }
})

setup函数接受的props对象是响应式的,也就是说,在组件外部传入新的prop值时,props对象会更新,可以调用watchEffect或watch方法监听该对象并对更改做出响应:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
	</head>
	<body>
        <div id="app">
        	<post-item :post-title="title"></post-item>
        </div>
	
	      <script src="https://unpkg.com/vue@next"></script>
        <script>
            const app = Vue.createApp({
                data(){
                    return {
                        title: 'Java无难事'
                    }
                }
            });
            app.component('PostItem', {
                //声明props
                props: ['postTitle'],
                setup(props){
                    Vue.watchEffect(() => {
                        console.log(props.postTitle);
                    })
                },
                template: '<h3>{{ postTitle }}</h3>'
            });
        	const vm = app.mount('#app');
        </script>
	</body>
</html>

不要去解构props对象,否则会失去响应性:

app.component('PostItem', {
    //声明props
    props: ['postTitle'],
    setup(postTitle){
        Vue.watchEffect(() => {
            console.log(props.postTitle);   //不再是响应式的
        })
    },
    template: '<h3>{{ postTitle }}</h3>'
});

同时也要注意,不要试图去修改props对象,否则将得到一个警告。

setup()函数接受的第二个可选参数是一个context对象,该对象是一个普通js对象,公开了3个组件属性:

const MyComponent = {
    set(props,context) {
        //属性(非响应式对象)
        console.log(context.attrs)
        //插槽(非响应式对象)
        console.log(context.slots)
        //发出的事件(方法)
        console.log(context.emit)
    }
}

context对象是一个普通JavaScript对象,非响应式,可以使用ES6的对象解构语法对context进行解构。

attrs和slots是有状态的对象,当组件本身被更新时,他们也总是被更新。但attrs和slots对象本身并不是响应式的,所以不应该对他们进行解构,并始终将属性引用为attrs.x或slots.x:

const MyComponent = {
    setup(props,{ attrs }) {
        //在稍后阶段可能被调用的函数
        function onClick() {
            console.log(attrs.foo)  //保证是最新的引用
        }
    }
}

当setup函数和选项API一起使用时,在setup函数内部不要使用this。因为setup是在选项被解析之前调用的。也就是说,在setup函数内不能访问data、computed、methods组件选项。

响应式API

reactive()方法和watchEffect()方法

  1. reactive()方法

对一个JavaScript对象创建响应式状态。在HTML页面中可以编写如下代码:

<script src="https://unpkg.com/vue@next"></script>
<script>
	//响应式状态
    const state = Vue.reactive({
        count:0
    })
</script>

在单文件组件中,可以编写如下代码:

import { reactive } from 'vue'

//响应式状态
const state = reactive({
	count:0
})

reactive()方法相当于vue2中的vue.observable()方法。

  1. watchEffect()方法

上述代码返回的state是一个响应式对象,我们可以在渲染期间使用它。由于依赖关系的跟踪,当state对象发生变化时,视图会自动更新。在DOM中渲染内容被认为是一个副作用,要应用和自动重新应用基于响应式的state对象,可以使用watchEffect API。

<!DOCTYPE html>
<html>
    <head>
		<meta charset="UTF-8">
		<title></title>
	</head>
	<body>
		<script src="https://unpkg.com/vue@next"></script>
		<script>
		    const {reactive, watchEffect} = Vue;
		    const state = reactive({
               count: 0
            })
            
            watchEffect(() => {
               document.body.innerHTML = `count is ${state.count}`
            })
		</script>
	</body>
</html>

watchEffect()方法接收一个函数对象作为参数,他会立即运行该函数,同时响应式地跟踪其依赖项,并在依赖项发生更改时重新运行该函数。watchEffect方法类似于vue2中的watch选项,但是它不需要分离监听的数据源和副作用回调。组合API还提供了一个watch方法,其行为与vue2的watch选项完全相同。

  1. 解构响应性状态

当要使用一个较大的响应式对象的一些属性时,可能会考虑使用ES6的对象解构语法获得想要的属性:

<!DOCTYPE html>
<html>
    <head>
		<meta charset="UTF-8">
		<title></title>
	</head>
	<body>
        <div id="app">
            <p>作者:{{author}}</p>
            <p>书名:{{title}}</p>
        </div>
		<script src="https://unpkg.com/vue@next"></script>
        <script>
            const {reactive, toRef} = Vue;
            const app = Vue.createApp({
                setup(){
                    const book = reactive({
                        author: '孙鑫',
                        year: '2020',
                        title: 'Java无难事',
                        description: '让Java的学习再无难事',
                        price: '188'
                    })
                    let { author, title } = book;
                    return {
                        author,
                        title
                    }
                }
            })
            const vm = app.mount('#app');
        </script>
	</body>
</html>

但是,通过这种解构,author和title的响应性会丢失。遇到这种情况,需要将响应式对象转换为一组ref,这些ref将保留到源对象的响应式连接。这个转换是通过调用toRefs()方法完成的,该方法将响应式对象转换为普通对象,其中结果对象上的每个属性都是指向原始对象中相应属性的ref。

修改上述代码,调用toRefs方法对book对象进行转换。代码如下:

<script>
            const {reactive, toRef} = Vue;
            const app = Vue.createApp({
                setup(){
                    const book = reactive({
                        author: '孙鑫',
                        year: '2020',
                        title: 'Java无难事',
                        description: '让Java的学习再无难事',
                        price: '188'
                    })
                    let { author, title } = toRefs(book);
                    // title.value = 'VC++深入详解'  // title现在是一个ref,需要使用.value
                    // console.log(book.title)      // 'VC++深入详解'
                    
                    //const author = toRef(book, 'author');
                    //const title = toRef(book, 'title');
                    return {
                        author,
                        title
                    }
                }
            })
            const vm = app.mount('#app');
        </script>

vue3还有一个toRefs()方法,该方法是为响应式源对象的某个属性创建ref,然后可以传递这个ref,并保持对其源属性的响应式连接。如上面代码分别为book对象的author、title属性创建ref对象。当把一个prop的ref传递给组合函数时,toRef方法就很有用了:

export default {
    setup(props) {
        useSomeFeature(toRef(props,'foo'))
    }
}

ref

reactive()方法为一个JavaScript对象创建响应式代理,如果需要对一个原始值(如字符串)创建响应式代理对象,一种方式是将该原始值作为某个对象的属性,调用reactive方法为该对象创建响应式代理对象,另一种方式就是使用Vue给出的另一个方法ref,该方法接收一个原始值,返回一个响应式和可变的ref对象,返回的对象只有一个value属性指向内部值。

<!DOCTYPE html>
<html>
    <head>
		<meta charset="UTF-8">
		<title></title>
	</head>
	<body>
		<script src="https://unpkg.com/vue@next"></script>
		<script>
		    const {ref, watchEffect} = Vue;
		    const state = ref(0)
            
            watchEffect(() => {
               document.body.innerHTML = `count is ${state.value}`
            })
		</script>
	</body>
</html>

此时取值需要访问state对象的value属性。当ref作为渲染上下文的属性返回(从setup返回的对象)并在模板中访问时,它将自动展开为内部值,不需要在模板中添加.value:

<!DOCTYPE html>
<html>
    <head>
		<meta charset="UTF-8">
		<title></title>
	</head>
	<body>
        <div id="app">
            <span>{{ count }}</span>
            <button @click="count ++">Increment count</button>
        </div>
		<script src="https://unpkg.com/vue@next"></script>
        <script>
            const {ref} = Vue;
            const app = Vue.createApp({
                setup(){
                    const count = ref(0);
                    return {
                        count
                    }
                }
            })
            app.mount('#app')
        </script>
	</body>
</html>

当ref作为响应式对象的属性被访问或更改时,它会自动展开为内部值,其行为类似于普通属性:

const count = ref(0)
const state = reactive({
    count
})
console.log(state.count)    //0
state.count = 1
console.log(count.value)    //1

如果一个新的ref被赋值给一个链接到现有ref的属性,它将替换旧的ref:

const otherCount = ref(2)

state.count = otherCount
console.log(state.count)    //2
console.log(count.value)    //1

ref展开仅在嵌套在响应式对象内时发生,当从数组或本地集合类型(如Map)中访问ref时,不会执行展开操作。

const books = reactive([ref('zzd')])
//需要添加.value
console.log(books[0].value)

const map = reactive(new Map([['count',ref(0)]]))
//需要添加.value
console.log(map.get('count').value)

readonly()

有时希望跟踪响应对象(ref或reactive)的变化,但还希望阻止从应用程序的某个位置对其进行修改。例如,当我们有一个提供的响应式对象时,想要防止它在注入的地方发生更改,为此,可以为原始对象创建一个只读代理:

import {reavtive,readonly} from 'vue'
const original = reactive({count:0})
const copy = readonly(original)
//改变original将触发依赖copy的观察者
original.count++
//修改copy将失败并导致警告
copy.count++  //报错

computed

computed()方法与computed选项作用一样,用于创建依赖于其他状态的计算属性。该方法接收一个getter函数,并为getter返回的值返回一个不可变的响应式ref对象:

const count = ref(1)
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value)  //2
plusOne.value++  //error

我们使用组合API反转字符串:

<html>
	<head>
		<meta charset="UTF-8">
		<title>计算属性</title>
	</head>
	<body>
        <div id="app">
            <p>原始字符串: {{ message }}</p>
            <p>计算后的反转字符串: {{ reversedMessage }}</p>
        </div>
	
		<script src="https://unpkg.com/vue@next"></script>
        <script>
            const {ref, computed} = Vue;
        	const vm = Vue.createApp({
        	    setup(){
        	        const message = ref('Hello,Java无难事!');
        	        const reversedMessage = computed(() =>
        	            message.value.split('').reverse().join('')
        	        );
        	        return {
        	            message,
        	            reversedMessage
        	        }
        	    }
        	}).mount('#app');
        </script>
	</body>
</html>

与computed选项一样,computed()方法也可以接受一个带有get()和set()函数的对象来创建一个可写的ref对象:

const count = ref(1)
const plusOne = computed({
    get:() => count.value + 1,
    set:val => {
        count.value = val - 1
    }
})

plusOne.value = 1
console.log(count.value)   // 0

用组合API实现人名输入框:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<title>计算属性的getter和setter</title>
	</head>
	<body>
        <div id="app">
          <p>First name: <input type="text" v-model="firstName"></p>
          <p>Last name: <input type="text" v-model="lastName"></p>
          <p>{{ fullName }}</p>
        </div>
	
		<script src="https://unpkg.com/vue@next"></script>
        <script>
            const {ref, computed} = Vue;
        	const vm = Vue.createApp({
        	    setup(){
        	        const firstName = ref('Smith');
        	        const lastName = ref('Will');
        	        const fullName = computed({
        	            get: () => firstName.value + ' ' + lastName.value,
        	            set: val => {
        	                let names = val.split(' ')
                	        firstName.value = names[0]
                	        lastName.value = names[names.length - 1]
        	            }
        	        });
        	        return {
        	            firstName,
        	            lastName,
        	            fullName
        	        }
        	    }
            }).mount('#app');
        </script>
	</body>
</html>

watch

watch()方法等同于vue2的this.$watch()方法,以及相应的watch选项。watch()方法需要监听特定的数据源,并在单独的回调函数中应用副作用。默认情况下,它也是惰性的,即只有当被监听的数据源发生变化时,才会调用回调函数。

与watchEffect()方法相比,watch方法有以下功能:

  • 惰性地执行副作用;
  • 更具体地说明什么状态应该触发监听器重新运行;
  • 访问被监听状态的前一个值和当前值。

watch()方法与watchEffect()方法共享行为,包括手动停止、副作用失效(将onInvalidate作为第3个参数传递给回调)、刷新时间和调试。

监听的数据源可以是返回值的getter函数,也可以是直接的ref对象。

例如:

const state = reactive({ count:0 })
//监听返回值的getter函数
watch(
	() => state.count,
    (count,prevCount) => {
        ...
    }
)
const count = ref(0)
//直接监听一个ref对象
watch(count,(count,prevCount) => {...})

生命周期钩子

在组合API中,生命周期钩子通过调用onXxx()函数显式地进行注册。这些生命周期钩子注册函数只能在setup()期间同步使用,因为它们依赖内部全局状态定位当前活动实例[即其setup()正在被调用的组件实例]。在没有当前活动实例的情况下调用它们将导致错误。组件实例上下文也是在生命周期钩子的同步执行期间设置的,因此在生命周期钩子内同步创建的监听器和计算属性也会在组件卸载时被自动删除。

前面介绍的生命周期选项与组合API之间的对应关系如下:

  • beforeCreate和created没有对应的onXxx函数,取而代之的是setup函数
  • beforeMount-----onBeforeMount
  • mounted-----onMounted
  • beforeUpdate-----onBeforeUpdate
  • updated-----onUpdated
  • beforeUnmounted-----onBeforeUnmount
  • unmounted-----onUnmounted
  • activated-----onActivated
  • deactivated-----onDeactivated
  • errorCaptured-----onErrorCaptured
  • renderTracked-----onRenderTracked
  • renderTriggered-----onRenderTriggered

下面是一个在单文件组件内使用组合API注册生命周期钩子的示例:

import {onMounted,onUpdated} from 'vue'
const MyComponent = {
	setup(){
        onMounted(() => {
            console.log('mounted!')
        })
        onUpdated(() => {
            console.log('updated!')
        })
	}
}

依赖注入

前面介绍过provide和inject选项,组合API给出了相应的provide()和inject()方法,以支持依赖注入。这两个方法只能在setup()期间使用当前活动实例来调用。

<html>
	<head>
		<meta charset="UTF-8">
		<title></title>
	</head>
	<body>
        <div id="app">
            <!--
        	<parent></parent>
        	-->
        </div>
		
		<script src="https://unpkg.com/vue@next"></script>
		<script>
		    const {provide, inject, ref, onMounted} = Vue;
		    
            const msgKey = Symbol();
			const helloKey = Symbol();
			const app = Vue.createApp({
			    setup(){
			        const msg = ref('Java无难事');
			        const sayHello = function (name){
			  		    console.log("Hello, " + name);
			  	    }
			  	    // provide方法需要指定一个Symbol类型的key,
			        provide(msgKey, msg);
			        provide(helloKey, sayHello);
			        return {
			            msg
			        }
			    },
				template: '<child/>'
			})
			/*
			app.component('parent', {
			    setup(){
			        const msg = ref('Java无难事');
			        const sayHello = function (name){
			  		    console.log("Hello, " + name);
			  	    }
			  	    // provide方法需要指定一个Symbol类型的key,
			        provide(msgKey, msg);
			        provide(helloKey, sayHello);
			        return {
			            msg
			        }
			    },
				template: '<child/>'
			})
			*/
			
			app.component('child', {
			    setup(){
			        // inject方法接受一个可选的默认值作为第二个参数,
			        // 如果没有提供默认值,并且在provide上下文中未找到该属性,则inject返回undefined。
			        const message = inject(msgKey, ref('VC++深入详解'));
			        const hello = inject(helloKey);
			        onMounted(() => hello('zhangsan'));
			        
			        return{
			            message
			        }
			    },
				// 当自身的数据属性来访问
				template: '<p>{{message}}</p>'
			})
			const vm = app.mount('#app')
		</script>
	</body>
</html>

现在如果修改parent组件的msg属性的值,则会引起child组件中注入的message属性的更改。

逻辑提取和重用

当涉及跨组件提取和重用逻辑时,组合API非常灵活。组合函数并不依赖于this,而只依赖于它的参数和全局导入的Vue API。只需将组件逻辑导出为函数,就可以重用组件逻辑的任何部分,甚至可以通过导出组件的整个setup函数实现扩展功能。

我们看一个跟踪鼠标的例子,由于跟踪鼠标的功能会在多个组件中用到,所以将它的实现逻辑提取出来,封装为一个函数并导出:

import {ref,onMounted,onUnmounted} from 'vue'

export function useMousePosition() {
    const x = ref(0)
    const y = ref(0)
    function update(e) {
        x.value = e.pageX
        v.value = e.pageY
    }
    onMounted(() => {
        window.addEventListener('mousemove',update)
    })
    onUnMounted(() => {
        window.removeEventListener('mousemove',update)
    })
    return {x,y}
}

接下来在组件中使用useMousePosition函数:

import {useMousePosition} from './mouse'
export default {
    setup(){
        const {x,y} = useMousePosition()
        //...
        return {x,y}
    }
}

与其他重用组件逻辑的方式相比,组合API有以下好处:

  • 暴露给模板的属性有明确的来源,因为他们是从组合函数返回的值。
  • 组合函数返回的值可以任意命名,这样就不会发生命名空间冲突。
  • 不需要为逻辑重用而创建不必要的组件实例
posted @ 2021-08-28 21:57  KKKyrie  阅读(210)  评论(0编辑  收藏  举报