#完整原文地址见简书 #更多完整Vue笔记目录敬请见《前端 Web 笔记 汇总目录(Updating)》
#本文内容提要
- Composition API 的作用
- setup函数
- 例程,打印查看setup内容
- 非响应引用的案例
ref()
概念、原理 与 实战reactive()
概念、原理 与 实战- 使用
readonly
限制对象的访问权限- 使用
toRefs()
对reactive
对象进一步封装- 多个属性进行解构
- 多个属性 配合toRefs() 进行解构
- toRefs()无法处理 undefined的键值
- 使用toRef()应对上述问题
- 关于setup函数的三个参数【attrs、slots、emit】
- 回顾 没有 CompositionAPI时,emit的用法
- 使用setup的 context.emit 替代 this.$emit
- 使用Composition API开发 todoList
- 完善toDoList案例
- 优化上例的逻辑结构!
- setup的 computed 计算属性
- 当然以上是computed 的默认用法,实际上它可以接收一个对象
- 将上例的处理值换成 Object类型,再例
- setup 中的 watch 监听
- setup 中的 watch 监听:监听Object类型
- setup 中的 watch 监听:监听Object类型的 多个属性
- setup 中的 watchEffect监听 以及 与 watch 的异同比较
- 两者都可以用以下的方式,在一个设定的时延之后,停止监听
- 为 watch 配置 immediate属性,可使具备同watchEffect的 即时性
- setup 中的 生命周期
- setup中的provide、inject用法
- 配合上ref实现 响应特性 以及 readonly实现 单向数据流
- setup结合ref指令
####Composition API 的作用 **使得相同的、相关的功能代码 可以比较 完整地聚合起来, 提高可维护性、可读性,提高开发效率;
规避 同一个功能的代码, 却散落在 组件定义中的`data、methods、computed、directives、template、mixin`等各处 的问题;**
####setup函数 >**--- Composition API 所有代码编写之前, 都要 建立在setup函数 之上;
--- 在created 组件实例 被完全初始化之前 回调; (所以注意在`setup`函数中, 使用与`this`相关的调用是没有用的)
--- setup函数中的内容, 可以在 该组件的 模板`template` 中直接使用; (如下例程)** ``` Hello World! heheheheheheda ``` 运行效果:
####例程,打印查看setup内容 ``` ``` 运行效果:
####由于调用时序的关系,setup中 无法调用this等相关 如变量、methods中 等 其他内容,但是其他内容 却可以调用 setup函数!!【setup生时众为生,众生时setup已生】 ``` ``` 
####非响应引用的案例 **如下,这样没有使用`ref`/`reactive`的写法,是不会有响应的:** ``` Hello World! heheheheheheda ``` 如下,运行之后,两秒延时之后,DOM文本展示并不会自动改成`zhao`,而是一直展示初始化的`guan`:
####`ref()`概念、原理 与 实战 >**使用`ref`可以 用于处理 `基础类型的数据`,赋能`响应式`;
原理:通过 proxy 将 `数据` 封装成 类似 `proxy({value: '【变量值】'})`这样的一个`响应式引用`, 当`数据`变化时,就会 触发`template`等相关UI的更新
【赋予 非data中定义的变量 以`响应式`的能力 —— 原先,我们是借助Vue的`data函数`,完成`响应式变量`的定义的; 有了`ref`之后,我们可以不借助`data`中的定义, 而直接在`普通的函数`中对`js变量`做`proxy`封装, 就可以对 `普通的js引用` 赋能`响应式`了】;** ``` Hello World! heheheheheheda ``` **运行效果: 两秒后自动变化:**
####`reactive()`概念、原理 与 实战 >**使用`reactive()`用于处理 `非基础类型的数据(如Object、Array)`,赋能`响应式`; 原理类似`ref()`,只是处理的数据格式不同而已;**
如下,普通的Object类型
是没有响应式
的效果的:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello World! heheheheheheda</title>
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="heheApp"></div>
</body>
<script>
const app = Vue.createApp({
template: `
<div>{{nameObj.name}}</div>
`,
setup(props, context) {
// const { ref } = Vue;
const nameObj = { name: 'guan'};
setTimeout(() => {
nameObj.name = 'zhao';
}, 2000);
return { nameObj }
}
});
const vm = app.mount('#heheApp');
</script>
</html>
运行效果,两秒后无反应:
使用reactive()
处理Object类型
后,具备响应式
的能力:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello World! heheheheheheda</title>
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="heheApp"></div>
</body>
<script>
const app = Vue.createApp({
template: `
<div>{{nameObj.name}}</div>
`,
setup(props, context) {
const { reactive } = Vue;
const nameObj = reactive({ name: 'guan'});
setTimeout(() => {
nameObj.name = 'zhao';
}, 2000);
return { nameObj }
}
});
const vm = app.mount('#heheApp');
</script>
</html>
运行效果:
两秒后自动变化:
使用reactive()
处理Array类型
数据
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello World! heheheheheheda</title>
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="heheApp"></div>
</body>
<script>
const app = Vue.createApp({
template: `
<div>{{nameObj[0]}}</div>
`,
setup(props, context) {
const { reactive } = Vue;
const nameObj = reactive([123, 99, 567]);
setTimeout(() => {
nameObj[0] = 666;
}, 2000);
return { nameObj }
}
});
const vm = app.mount('#heheApp');
</script>
</html>
运行效果:
两秒后自动变化:
####使用`readonly`限制对象的访问权限 **使用`readonly()`封装对象,可以限制对象为`只读权限`;** ``` ``` **运行两秒之后会有相应的报错:**
####错误的解构案例 **如下的解构是行不通的, `const { name } = nameObj;`只能拿到`nameObj`的值,拿不到`proxy`对象;** ``` ```
####注意解构技巧 **赋值时 加上`{}`,会 直取 其 赋值右侧 Object的值;** ``` ``` 
**直接赋值,便是直接赋值一份引用;** ``` ``` 
####使用`toRefs()`对`reactive `对象进一步封装 >**--- `toRefs() expects a reactive object`; 首先,`toRefs()`需要接受一个`reactive `对象; 即,传给`toRefs()`之前,需要先用`reactive()`进行封装;
>--- 使用`toRefs()`对`reactive `封装的对象 进一步封装, >便可以顺利解构;
>--- 原理:`toRefs()`将类似`proxy({name:'guan'})`的结构, 转化成类似`{ name: proxy( {value:'guan'}) }`的结构, 这里可以看到`name`键的值,其实就类似于`ref`的处理结果;
然后使用`const { name } `对`{ name: proxy( {value:'guan'}) }`进行解构赋值, 左侧`name `变量 拿到的就是`proxy( {value:'guan'}) `这一部分的值, 所以放入DOM节点展示时候, 直接使用`name`即可;
>--- !!!注意: 类似`reactive()`的处理结果, 即`proxy({name:'guan'})`的结构, 放入DOM节点展示时候,需要使用`nameObj.name`的格式; 而类似`ref()`的处理结果, 即`proxy( {value:'guan'})`的结构, 放入DOM节点展示时候,直接使用`nameObj`的格式即可;** ``` ``` **运行两秒后,自动更新UI:**
####多个属性进行解构 >**注意多个属性解构时的写法 以及 return时的写法;** ``` ``` **运行结果(当然不会自动更新):**
####多个属性 配合toRefs() 进行解构 ``` ``` **运行结果:两秒后自动刷新:**
####toRefs()无法处理 undefined的键值 **如果意图解构的键, 不存在于`toRefs()`封装的对象中, 使用时会报错:** ``` ``` 运行结果:
####使用toRef()应对上述问题 `toRef(data, key)`会尝试从`data`中读取`key`对应的键值, 如果读得到,就直接取值, 如果读不到,会赋值undefined,后续可以为之赋实值: ``` ``` **运行两秒后自动更新:**
####关于setup函数的三个参数 >**setup函数的context参数中的三个属性,即`attrs, slots, emit`; 获取方法(解构`context参数`):** >``` >setup(props, context) { > const { attrs, slots, emit } = context; > return { }; > } >``` #####attrs >**-- 是一个`Proxy`对象; -- 子组件的`none-props属性`都存进`attrs`中;**
如下,父组件调用子组件,传递myfield
属性,
子组件没有用props
接收,则myfield
作为none-props属性
被 子组件承接,
这时候会存进setup
函数的attrs
中:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello World! heheheheheheda</title>
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="heheApp"></div>
</body>
<script>
const app = Vue.createApp({
template: `
<child myfield='heheda' />
`
});
app.component('child', {
template:`
<div>child</div>
`,
setup(props, context) {
const { attrs, slots, emit } = context;
console.log(attrs);
return { };
}
})
const vm = app.mount('#heheApp');
</script>
</html>
运行效果:可以看到attrs
也是一个Proxy
对象
(attrs.myfield)
打印取值:
<script>
const app = Vue.createApp({
template: `
<child myfield='heheda' />
`
});
app.component('child', {
template:`
<div>child</div>
`,
setup(props, context) {
const { attrs, slots, emit } = context;
console.log(attrs.myfield);
return { };
}
})
const vm = app.mount('#heheApp');
</script>
#####slots >**-- 是一个`Proxy`对象; -- 其中有一个 以为`default`为键的函数, 这个函数会以 `虚拟DOM`的形式, 返回 传给 子组件的`slot插槽` 的`组件`;**
**打印slots属性:** ``` ``` 
打印slots属性中的 default函数, 可见其返回的是虚拟DOM:
<script>
const app = Vue.createApp({
template: `
<child>hehehe</child>
`
});
app.component('child', {
template:`
<div>child</div>
<div><slot/></div>
`,
setup(props, context) {
const { attrs, slots, emit } = context;
console.log(slots.default());
return { };
}
})
const vm = app.mount('#heheApp');
</script>
#####使用setup中 context参数的 slots属性中的 default方法所返回的 虚拟DOM,通过render函数的形式,在setup函数返回,可以覆盖`template`的内容,渲染UI ``` ``` 
####补充:不使用 Vue3 的这个compositionAPI,子组件只能这样去获取`slots`内容 **通过在其他函数中,使用`this.$slots`的方式调用到`slots`内容** ``` ``` **运行结果如下,可以看到跟`setup`函数的`context.slots`是一样的:** 
#####回顾 没有 CompositionAPI时,emit的用法 ``` ``` 
#####使用setup的 context.emit 替代 this.$emit ``` ``` **运行,点击文本:**
####使用Composition API开发 todoList #####调测input框事件 **setup中, --- `const inputValue = ref('6666');`定义到一个`proxy`对象, 可以将此对象 用于setup外的 template中, 完成`双向绑定`中的一环——`数据字段映射到 UI`!!!;
--- `handleInputValueChange`定义一个方法; 运行时,将对应`触发组件`的 `内容`, 赋值给 `inputValue`, 完成`双向绑定`中的另一环——`UI 映射到数据`!!!; template中, `:value="inputValue"`使得对象的内容 初始化显示`inputValue`的内容;
--- ` @input="handleInputValueChange"`使得输入框被用户输入新的内容时, 会调用对应的方法,这里调用:** ``` ``` 运行效果:
####完善toDoList案例 **--- setup 中定义 数组list,以及 handleSubmit处理函数, 并都在return中返回;
--- template中, button添加click事件回调; li 标签,使用v-for,完成列表渲染:** ``` Hello World! heheheheheheda ``` 运行效果:
####优化上例的逻辑结构! >**如下,将 setup()中, >--- 与 list 相关的定义和操作函数, >封装到 `listHandleAction`中,然后return;
>--- 与 inputValue 相关的定义和操作函数, >封装到 `inputHandleAction`中,然后return;
>--- 最后 `setup()` 调用以上两个`业务模块封装函数`, >解构`返回的内容`,进行`中转调用`;
>【分模块 聚合`业务逻辑`,只留一个`核心方法`进行`统筹调度`,有`MVP`那味了】 >【这样设计,业务分明,方便定位问题,可读性、可维护性高!! 】** ``` ``` 运行效果:
####setup的 computed 计算属性 ``` ``` 运行结果:
####当然以上是computed 的默认用法,实际上它可以接收一个对象 **这个对象包含两个函数属性, 第一个是`get`函数,其内容即取值时候返回的内容,同默认用法; 第二个是`set`函数,当试图修改`computed`变量的值时,就会回调这个方法, 接收的参数,即`试图修改的值`:**
如下,试图在3秒后修改computed
变量countAddFive
的值,
这时回调set方法:
<script>
const app = Vue.createApp({
setup() {
const { ref, computed } = Vue;
const count = ref(0);
const handleClick = () => {
count.value += 1;
}
const countAddFive = computed({
get: () => {
return count.value + 5;
},
set: (param) => {
count.value = param -5;
}
})
setTimeout(() => {
countAddFive.value = 1000;
}, 2000);
return { count, countAddFive, handleClick }
},
template: `
<div>
<div>
<span @click="handleClick">{{count}}</span> --- {{countAddFive}}
</div>
</div>
`
});
const vm = app.mount('#heheApp');
</script>
运行3s后:
####将上例的处理值换成 Object类型,再例 ``` ``` 运行效果同上例;
####setup 中的 watch 监听 **如下, ---`watch`一个参数为要监听的引用, 第二个参数为函数类型,当监听的引用发生变化时会回调, 其有两个参数,一个是当前(变化后的)值,一个是变化前的值;
--- `input`组件中,`v-model`完成`双向绑定`!!!
--- `input`输入内容时,触发 `双向绑定`的特性, 内容映射到`name`引用上, 由`ref`的`响应特性`,`name`的内容又映射到`{{name}}`这`DOM节点`上:** ``` Hello World! heheheheheheda ``` 运行效果:
####setup 中的 watch 监听:监听Object类型 #####注意setup的watch的 可监听数据类型  **所以,这里主要是 ---将watch的 第一个参数改成 函数; ---使用toRefs,简化传递过程; (不然template要接收和处理的就是 nameObject.name了,而不是这里的name)** ``` ``` 运行效果同上例;
####setup 中的 watch 监听:监听Object类型的 多个属性 **注意watch的参数写法, 一参写成,以`函数类型`为`元素`的`数组`; 二参,参数列表写成两个数组, 第一个为current值数组,第二个为prev值数组;** ``` Hello World! heheheheheheda ``` 运行,先在Name输入框输入,后再EnglishName框输入,效果:
#### setup 中的 watchEffect监听 以及 与 watch 的异同比较 >**函数中,使得纯函数 变成 非纯函数的 异步处理等部分逻辑块, 称之为`effect块`;**
---
watch
是惰性
的,只有watch
监听的字段发生变化时,watch
的处理逻辑才会被回调; ---watchEffect
是即时性
的,也就是除了watch
的回调特性
,watchEffect
的处理逻辑
还会在页面渲染完成时
立马先执行一次, 即watchEffect
监听的字段未曾改变,watchEffect
就已经执行了一次; (实例可以见下例运行效果)
watch
需要写明监听字段
,watchEffect
不需要,直接写处理逻辑
即可,底层封装
会!自动监听!
所写处理逻辑
中用到的!所有字段!
;
如下例子中,watchEffect
的处理逻辑——console.log(nameObj.name, nameObj.englishName);
, 仅一行代码, 完成对nameObj.name
和nameObj.englishName
两个字段的监听, 并完成了处理逻辑;
watch
可以直接从参数列表
中获取到之前(变化前)的值
和当前(变化后)的值
,watchEffect
不行,处理逻辑中拿到的直接就是当前(变化后)的值
;
- 两者都可以用以下的方式,在一个设定的时延之后,停止监听
- 为
watch
配置 immediate属性,可使具备同watchEffect
的 即时性
<script>
const app = Vue.createApp({
setup() {
const { reactive, watch, watchEffect, toRefs } = Vue;
const nameObj = reactive({name: 'heheda', englishName:"lueluelue"});
watchEffect(() => {
console.log(nameObj.name, nameObj.englishName);
})
const { name, englishName } = toRefs(nameObj);
return { name, englishName }
},
template: `
<div>
<div>
Name:<input v-model="name">
</div>
<div>
Name is {{name}}
</div>
<div>
EnglishName:<input v-model="englishName">
</div>
<div>
EnglishName is {{englishName}}
</div>
</div>
`
});
const vm = app.mount('#heheApp');
</script>
跟紧console.log(nameObj.name, nameObj.englishName);
,
先在Name输入框输入123,后再EnglishName框输入456,运行效果:
注意第一行打印,第一行是页面渲染完成时立马执行, 用户未曾输入内容,
watchEffect
监听的字段未曾改变,watchEffect
就已经执行了一次,体现watchEffect
的即时性
!!!
#####两者都可以用以下的方式,在一个设定的时延之后,停止监听 >**将`watch / watchEffect`的`函数返回值` 赋给一个字段(如下`stopWatch / stopWatchEffect`); 接着在`watch / watchEffect`的`处理逻辑`中, 编写类似`setTimeout`的异步函数, 并在其中调用 与 刚刚定义的`字段` 同名的 函数(如下`stopWatch() / stopWatchEffect()`), 即可停止`对应的监听`;** ``` ``` **运行,可以看到, 前3s(我们设定的时延)两个输入框是可以响应监听的, 但是3s之后,无论怎么输入内容,console也不会打印log, 因为这时两个监听效果已经取消了:**
#####为 `watch` 配置 immediate属性,可使具备同`watchEffect`的 即时性 **如下,使用`{ immediate: true}`为 `watch` 配置 immediate属性, 可使具备同`watchEffect`的 即时性:** ``` ``` **运行效果如下,`undefined`是因第一次执行时, 监听的变量还没有变化, 所以就没有`先前值(prevValue)`的说法,只有`当前值(currentValue)`:**
####setup 中的 生命周期 **--- Vue3.0提供了一些对应生命周期的,可以写在`setup`函数中的回调方法; (具体请看 下方例程)
--- `setup`函数的`执行时间点` 在于`beforeCreate`和`Created`之间, 所以`CompositionAPI`里边是没有类似`onBeforeCreate`和`onCreated`的方法的, 要写在这两个周期中的逻辑, 直接写在`setup`中即可;
下面是两个Vue3.0引入的新钩子: --- onRenderTracked 渲染跟踪, 跟踪 收集响应式依赖的时机, 每次准备开始渲染时(onBeforeMount后,onMounted前)回调;
--- onRenderTriggered 渲染触发, 每次`触发`页面重新渲染时回调, 回调后,下一轮的 onBeforeMount紧跟其后;** ``` ``` **运行效果:**
####setup中的provide、inject用法 **--- 父组件拿出`provide`,提供键值对; --- 子组件拿出`inject`,一参为接收键,二参为默认值;** ``` Hello World! heheheheheheda ``` 运行结果:
####配合上`ref`实现 响应特性 以及 `readonly`实现 单向数据流 **--- setup中, 借`provide`传输的 数据字段 配合上`ref`, 使之具备`响应`的特性; --- 父组件提供 更改数据的接口, 在使用`provide`传递 数据字段时,加上 `readonly`包裹, 使得子组件 需要更改 父组件传递过来的数据字段 时, 无法直接 修改字段, 需调用 父组件的接口方法 更改, 按 单向数据流 规范编程;** ``` ``` 运行效果:点击:
####setup结合ref指令 >**前面笔记[《Vue3 | Mixin、自定义指令、Teleport传送门、Render函数、插件 详解 及 案例分析》](https://www.jianshu.com/p/dc7652457d2a)有写到普通场景的ref指定;**
--- setup中的ref是前面说的生成响应式字段的意思;
--- template中的ref是获取对应的dom节点;
--- 而当 template中的ref
指定的字段名,
跟setup中的ref生成的响应式字段名一样的时候,两者就会关联起来;
如下,
template中和setup中的字段heheda
关联起来,
在setup的onMounted中,使用这个DOM节点字段,
打印DOM代码:
<script>
const app = Vue.createApp({
setup() {
const { ref, onMounted } = Vue;
const heheda = ref(null);
onMounted(() => {
console.log(heheda);
console.log(heheda.value);
})
return { heheda }
},
template: `
<div>
<div ref="heheda">lueluelue</div>
</div>
`
});
const vm = app.mount('#heheApp');
</script>
运行结果:
####
####
####
####
####