Composition API

1. setup

1.1 setup函数有两个参数:props,context

  • props:其实就是父组件传递过来的属性,但是在setup外面依然需要用props接收
  • context:里面包含三个属性:
    • attrs:父组件传递过来的没有被prop接收的属性
    • slots:父组件传递过来的插槽
    • emit:当组件内部需要发出事件的时候用(vue2里面是this.$emit,但是vue3中,setup不能访问this)

1.2 setup 函数的返回值

setup() {
let a = 1
return {
a
}
}

setup的返回值可以在模板 template 中使用,也就是可以代替data

但是直接这么返回变量,是没有响应式的

1.3 setup 不可以使用 this

  • this 并没有指向当前组件实例
  • 在 setup 被调用之前,data、computed、等都没有被解析
  • 所以无法在 setup 中获取this

2. reactive API

为setup中定义的数据提供响应式的特性

reactive API 传入的类型必须是对象或者数组

const state = reactive({
name: "why" // 这时候这个name就是响应式的
})

3. ref API

3.1 ref API 基本使用

可以传入基本数据类型,在开发中推荐使用 ref,便于代码的抽离,当然如果属性关系很紧密的时候,我们也可以用 reactive

ref 会返回一个可变的响应式对象

const message = ref("hello world")
  • 在 template 中引用 ref 的值时,Vue会自动帮我们进行解包,就是说我们不需要在模板中 xxx.value 来使用
  • 但是在 setup 内部,它依然是一个 ref 的引用,所以要使用 ref.value
3.2 ref API 的补充
  • toRefs 和 toRef
let info = reactive({
name: 'why',
age: 18
})
// 当我们想解构的时候
// let {name, age} = info // 不再是响应式的了

// 如果希望响应式的话
// 1. toRefs: 将 reactive 对象中所有属性都转成 ref
// let {name, age} = toRefs(info)

// 只希望单个响应式的话
// 2. toRef
let {name} = info // 不是响应式
let age = toRef(info, "age")

const change = () => {
age.value++
}
  • shallowRef:创建一个跟踪自身 .value 变化的 ref,但不会使其值也变成响应式的
  • triggerRef:手动触发和 shallowRef 相关联的副作用
let info = shallowRef({
name: "why"
})
const change = () => {
// info.value = "hello" 响应式
info.value.name = "james" // 不是响应式
// 手动触发和 shallowRef 相关联的副作用(变成响应式)
triggerRef(info)
}
  • customRef 自定义 ref

4. readonly

  • 当我们希望我们给其他组件传递数据时,希望其他组件只是使用我们的内容,但是不允许它们修改的时候,可以用readonly

  • 实际上,readonly 会返回原生对象的只读代理,也就是它依然是一个 Proxy,但是set方法被劫持

  • 开发中常见的readonly方法会传入三个类型的值

    • 普通对象
    • reactive 返回的对象
    • ref 的对象
  • readonly 使用时,readonly 返回的对象不允许修改,但是经过 readonly 处理的原来的对象是可以修改的

    const info = {
    name: "why"
    }
    const state = readonly(info)
    state.name = "aaa" // 不可以
    info.name = "aaa" // 可以

5. computed

  • 用法一:传入一个getter函数,computed的返回值是一个只读的ref对象(不能修改)

    const fullName = computed(() => firstName.value + "-" +lastName.value)

    const changeData = () => {
    firstName.value = "james" // 可修改
    fullName.value = "yuzi bing" // 不可修改
    }
  • 用法二:传入一个对象,对象包含 getter/setter,返回一个可读写的ref 对象

const fullName = computed({
get: () => firstName.value + "-" +lastName.value,
set(newValue) {
const names = newValue.split(' ')
firstName.value = names[0]
lastName.value = names[1]
}
})

const changeData = () => {
fullName.value = "yuzi bing" // 可修改
}

6. watchEffect

6.1 watchEffect 基本使用

  • watchEffect 传入的函数会被立即执行一次, 并在执行的过程中自动收集依赖(相当于你在这个函数使用了什么变量,它会自动收集到)
  • 只有收集的依赖发生变化时,watchEffect 传入的函数才会再次执行

下面案例中,name 的改变会被侦听到,而 age 不会被侦听

// watchEffect: 自动收集响应式的依赖
const name = ref("jenny")
const age = ref(18)

const changeName = () => name.value = "tony"
const changeAge = () => age.value = 20

watchEffect(() => {
console.log("name:", name.value);
})

6.2 watchEffect 停止侦听

如果在发生某些情况下,我们希望停止侦听,这个时候我们可以获取watchEffect的返回值函数,调用该函数即可

const age = ref(18)
// watchEffect 会返回一个函数,供我们停止侦听使用
const stop = watchEffect(() => {
console.log("age:", age.value);
})
const changeAge = () => {
age.value++
// 案例:age 到25的时候就停止侦听
if (age.value > 25) {
stop()
}
}

6.3 watchEffect 清除副作用

清除副作用?

比如我们需要在侦听器中执行网络请求,但是在网络请求还没完成之前,我们停止了侦听器或者修改了数据让侦听器侦听函数再次执行了,这时候我们应该清除上一次的副作用(数据改变了要重新发送请求或者说不需要发了)

const stop = watchEffect((onInvalidate) => {
const timer = setTimeout(() => {
console.log('网络请求成功~'); // 定时器模拟网络请求
}, 2000);

// 在传入的回调函数中执行一些清除工作
onInvalidate(() => {
clearTimeout(timer)
console.log('onInvalidate');
})

console.log("age:", age.value);
})

6.4 watchEffect 执行时机

  • 首先补充一下:在 setup 中如何属于 ref 或者元素或者组件?

    定义一个 ref 对象,绑定到元素或组件的ref属性上

<h2 ref="title">hello</h2> // 绑定到元素的ref属性

const title = ref(null) // 定义 ref 对象
  • watchEffect 执行时机

    如果我们希望在副作用函数中获取元素,我们会发现打印结果有两个

    watchEffect(() => {
    console.log(title.value);
    })
    image-20220329144156444
    • 这是因为 setup 函数在执行时就会立即执行传入的副作用函数,这个时候 DOM 并没有挂载,所以打印为 null
    • 当 DOM 挂载时,会给 title 的 ref 对象赋新的值,副作用函数会再次执行
  • 调整 watchEffect 的执行时机

    watchEffect(() => {
    console.log(title.value);
    }, {
    // flush: "pre" // 在元素挂载或更新之前执行
    flush: "post" // 元素挂载更新之后执行, 这时候只打印一次<h2></h2>
    })

7. watch

7.1 侦听单个数据源
  • 侦听一个 getter 函数
const info = reactive({
name: "jenny",
age: 18
})

// 1. watch侦听时,传入一个getter函数, 具体监听某个属性
watch(() => info.name, (newValue, oldValue) => {
console.log(newValue, oldValue); // Tom jenny
})

const changeData = () => {
info.name = "Tom"
}
  • 直接侦听一个可响应式的对象,reactive 或 ref (ref更常用)
// 传入一个可响应式对象: reactive对象/ref对象
const title = ref("hello")
const info = reactive({
name: "jenny",
age: 18
})

watch(info, (newValue, oldValue) => {
console.log(newValue, oldValue);
// Proxy {name: 'Tom', age: 18}
})
watch(title, (newValue, oldValue) => {
console.log(newValue, oldValue); // world hello
})
const changeData = () => {
info.name = "Tom"
title.value = "world"
}
7.2 侦听多个数据源

注:如果我们希望侦听一个数组或者对象,那么可以使用一个getter函数,并且对可响应对象进行解构

(不解构也行,不解构n,o就是一个Proxy对象)

const info = reactive({
name: "jenny",
age: 18
})
const title = ref("hello")

// 同时侦听多个数据源
watch([() => ({...info}), title], ([newInfo, newTitle], [oldInfo, oldTitle]) => {
console.log(newInfo, newTitle, oldInfo, oldTitle);
// {name: 'Tom', age: 18} 'world'
// {name: 'jenny', age: 18} 'hello'
})

const changeData = () => {
info.name = "Tom"
title.value = "world"
}
7.3 watch 的选项
const info = reactive({
name: "jenny",
hobby: {
title: 'haha'
}
})

watch(() => ({...info}), (newValue, oldValue) => {
console.log(newValue, oldValue);
}, {
deep: true, // 深度监听
immediate: true // 立即执行一次
})

const changeData = () => {
// info.name = "Tom"
info.hobby.title = "hehe" // 开启深度监听
}

8. 生命周期钩子

// 在挂载开始之前被调用:相关的 render 函数首次被调用。
onBeforeMount(() => {})
// 实例挂载完毕后调用
onMounted(() => {})

// 在数据发生改变后,DOM 被更新之前被调用。这里适合在现有 DOM 将要被更新之前访问它,比如移除手动添加的事件监听器。
onBeforeUpdate(() => {})
// 在数据更改导致的虚拟 DOM 重新渲染和更新完毕之后被调用。
onUpdated(() => {})

// 卸载组件实例前调用, 这个阶段,实例仍然是完全正常的
onBeforeUnmount(() => {})
// 卸载组件实例后调用,调用此钩子时,组件实例的所有指令都被解除绑定,所有事件侦听器都被移除,所有子组件实例被卸载。
onUnmounted(() => {}),

// 被 keep-alive 缓存的组件激活时调用。
onActivated(() => {})
// 被 keep-alive 缓存的组件失活时调用。
onDeactivated(()=>{})

9. Provide 和 Inject

父组件通过 provide 来提供数据(必须要在父组件中使用过子组件,建立联系才能提供数据)

provide(属性名,属性值)

const name = ref("jenny")
// 给后代组件提供属性或方法(为了不让子组件随意修改父组件的数据,可以使用readonly)
provide("name", readonly(name))

后代组件可以通过 Inject 来注入需要的属性和对应值

inject(要注入的属性名, 默认值) ,默认值就是如果父组件没有提供改数据的话就使用默认值

const name = inject("name")

10. composition API 练习

自定义 hooks

1. useTitle

改变页面标题

import { ref, watch } from 'vue'

export default function(title = "默认的title") {
const titleRef = ref(title)

watch(titleRef, (newValue) => {
document.title = newValue
}, {
immediate: true
})

return titleRef
}
2. useScrollPosition

监听页面滚动位置

import { ref } from 'vue'
export default function useScrollPosition() {
const scrollX = ref(0)
const scrollY = ref(0)

document.addEventListener("scroll", () => {
scrollX.value = window.scrollX
scrollY.value = window.scrollY
})

return {
scrollX,
scrollY
}
}
3. useMousePosition

监听鼠标位置

import { ref } from 'vue'

export default function useMousePosition() {
const mouseX = ref(0)
const mouseY = ref(0)

window.addEventListener('mousemove', (event) => {
mouseX.value = event.pageX
mouseY.value = event.pageY
})

return {
mouseX,
mouseY
}
}
4. useLocalStorage

使用 localStorage 存储和获取数据

export default function(key, value) {
const data = ref(value)
// 如果有传value,表示要存储值,否则是获取值
if (value) {
window.localStorage.setItem(key, JSON.stringify(value))
} else {
data.value = JSON.parse(window.localStorage.getItem(key))
}

watch(data, (newValue) => {
window.localStorage.setItem(key, JSON.stringify(newValue))
})

return data
}

使用:

// 在浏览器存取值
let data = useLocalStorage("name", "jenny")
const changeData = () => data.value = "hahaha"
5. useCounter
import { ref, computed } from 'vue';

export default function() {
const counter = ref(0);
const doubleCounter = computed(() => counter.value * 2);

const increment = () => counter.value++;
const decrement = () => counter.value--;

return {
counter,
doubleCounter,
increment,
decrement
}
}

11. 认识自定义指令

11.1 简单使用
  • 除了 v-for, v-show,等指令,Vue也允许我们自定义指令

  • 自定义指令分为两种

    • 自定义局部指令:组件中通过 directives 选项,只能在当前组件中使用
    • 自定义全局指令:app的 directive 方法,可以在任意组件中被使用
  • 简单的案例:当某个元素挂载完成后可以自定获取焦点

    • 默认方式的实现

      <template>
      <div>
      <input type="text" ref="input">
      </div>
      </template>
      <script>
      import {ref, onMounted} from 'vue'
      export default {
      setup() {
      const input = ref(null)

      onMounted(() => {
      input.value.focus()
      })
      return {
      input
      }
      }
      }
    • 自定义局部指令 v-focus

      <input type="text" ref="input" v-focus>

      export default {
      directives: {
      // 自定义属性的名称(这里不需要写 v-)
      focus: {
      mounted(el) {
      el.focus()
      }
      }
      },
      }
    • 自定义全局指令 v-focus (main.js中)

      app.directive("focus", {
      mounted(el) {
      el.focus()
      }
      })
11.2 指令的生命周期
  • 一个指令定义的对象,Vue提供了如下的几个钩子函数:
  • created:在绑定元素的 attribute 或事件监听器被应用之前调用;
  • beforeMount:当指令第一次绑定到元素并且在挂载父组件之前调用
  • mounted:在绑定元素的父组件被挂载后调用
  • beforeUpdate:在更新包含组件的 VNode 之前调用
  • updated:在包含组件的 VNode 及其子组件的 VNode 更新后调用
  • beforeUnmount:在卸载绑定元素的父组件之前调用
  • unmounted:当指令与元素解除绑定且父组件已卸载时,只调用一次

指令的生命周期可以拿到几个参数

app.directive("focus", {
// el: <input>
// bindings: 包含一些属性的对象
// vnode: 一个真实 DOM 元素
// preVnode: 上一个虚拟节点
mounted(el, bindings, vnode, preVnode) {
console.log("focus created", el, bindings, vnode, preVnode);
console.log(bindings.value); // 拿到传入的参数
console.log(bindings.modifiers); // 指令的修饰符
el.focus()
}
})
11.3 指令的参数和修饰符
  • 指令接受参数或者修饰符

    v-指令名:参数名.修饰符="具体值"
    <button v-why:info.aaa.bbb="{title: 'hello', name: 'me'}"></button>
11.4 自定义指令练习

自定义时间格式化的指令 v-format-time

import dayjs from "dayjs";

export default function(app) {
app.directive("format-time", {
created(el, bindings) {
// 默认格式
bindings.formatString = "YYYY-MM-DD HH:mm:ss"
// 如果有传入格式的参数,那么使用传入的格式
if (bindings.value) {
bindings.formatString = bindings.value
}
},
mounted(el, bindings) {
const textContent = el.textContent // 节点及其后代的文本内容
let timestamp = parseInt(textContent)
if (textContent.length === 10) {
timestamp = timestamp * 1000 // 转成毫秒
}
el.textContent = dayjs(timestamp).format(bindings.formatString)
},
})
}

12. nextTick

将回调推迟到下一个 DOM 更新周期之后执行。在更改了一些数据以等待 DOM 更新后立即使用它

案例:当message更新时,拿到它的高度

如果不放到 nextTick执行,拿到的就是未更新之前的数据,比如第一次点添加内容,输出0

<template>
<div>
<h2 class="title" ref="titleRef">{{message}}</h2>
<button @click="addContent">添加内容</button>
</div>
</template>

<script>
import { ref, nextTick } from 'vue'
export default {
setup() {
const message = ref("")
const titleRef = ref(null)

const addContent = () => {
message.value += "哈哈哈哈哈哈哈哈哈"
// console.log(titleRef.value.offsetHeight);
nextTick(() => {
console.log(titleRef.value.offsetHeight);
})
}
...
}
}
</script>

nextTick的回调函数会被加入到微任务队列之后,在微任务队列中,DOM更新完之后才轮到它执行

其他补充

1. render函数

1.1 认识 h 函数
  • 绝大多数情况下,我们的HTML都是用模板<template>创建的,如果在一些特殊的场景,真的需要JavaScript的完全编程能力,这个时候可以使用 渲染函数,它比模板更接近编译器

    • Vue在生成真实的 DOM 之前,会将我们的节点转换成 VNode(虚拟节点),而VNode组合在一起形成一棵树结构,就是虚拟DOM(VDOM)

    • 你想充分的利用JavaScript的编程能力,我们可以自己来编写 createVNode 函数,生成对应的 VNode

  • h()函数是一个用于创建 VNode 的函数

1.2 h 函数基本使用
  • h() 函数接收三个参数,(标签名,组件名…)(属性)(子节点,内容)
  • h 函数可以在两个地方使用,render 函数选项中或者 setup 函数选项中
// render 函数选项中
<script>
import { h } from 'vue'
export default {
render() {
return h("h2", {class: "title"}, "Hello Render")
},
}
</script>
setup() {
return () => h("h2", {class: "title"}, "Hello Render")
}
image-20220330152843095
1.3 h 函数实现计数器案例
import { h } from 'vue';
export default {
data() {
return {
counter: 0
}
},
render() {
return h("div", {class: "app"}, [
h("h2", null, `当前计数:${this.counter}`),
h("button", {onClick: () => this.counter++}, "+1"),
h("button", {onClick: () => this.counter--}, "-1")
])
},
}

2. jsx

  • 在项目中使用 jsx 需要添加对 jsx 的支持

    • 安装Babel支持Vue的jsx插件

      npm install @vue/babel-plugin-jsx -D

    • 在 babel.config.js 配置文件中配置插件

      module.exports = {
      plugins: [
      "@vue/babel-plugin-jsx"
      ]
      }
  • 基本使用:计数器案例

    export default {
    data() {
    return {
    counter: 0
    }
    },

    render() {
    const increment = () => this.counter++;
    const decrement = () => this.counter--;

    return (
    <div>
    <h2>当前计数: {this.counter}</h2>
    <button onClick={increment}>+1</button>
    <button onClick={decrement}>-1</button>
    </div>
    )
    }
    }

3. 认识 Teleport

(先了解一下)

在组件化开发中,我们封装一个组件A,在另外一个组件B中使用,p 那么组件A中template的元素,会被挂载到组件B中template的某个位置,最终形成一棵 DOM 树结构

但是某些情况下,我们希望组件不是挂载在这个组件树上的,可能是移动到Vue app之外的其他位置,这个时候就可以通过teleport完成

4. 认识 Vue 插件

通常我们向Vue全局添加一些功能时,会采用插件的模式,它有两种编写方式

  • 对象类型:一个对象,但是必须包含一个 install 的函数,该函数会在安装插件时执行

    export default {
    install(app) { //
    app.config.globalProperties.$name = "hillyee"
    }
    }
  • 函数类型:一个function,这个函数会在安装插件时自动执行

    // plugins_function.js
    export default function(app) {
    console.log(app);
    }
  • main.js

    app.use(pluginsFunction)
    app.use(pluginsObject)