后端小白的Vue3新特性学习笔记

Vue3

写在前面

本博文仅作为个人学习过程的记录,可能存在诸多错误,希望各位看官不吝赐教,支持错误所在,帮助小白成长!

一、Vite初体验

这里我们只是初次体验一下使用vite创建一个Vue3项目,听说这个玩意儿贼快!!

因为尤大说长期下,会推动vite作为Vue的主要项目构建工具!短期下vue-cli和vite并存!后续我们会对vite进行更深入的学习。vite官网:Home | Vite (vitejs.dev)

  1. npm初始化

    npm init @vitejs/app <project_name>
    

    project_name 为可选参数,可以在配置选项中进行设置,然后针对项目进行配置选择。

  2. 到项目目录下,依赖安装

    cd xxxx
    npm install
    
  3. 启动项目

    npm run dev
    

    说时迟那时快,啪的一下,很快啊,一下就启动起来了。嗯~~ 有bear来!

    image-20210530211413330

这是Vue3官方文档中贴出的Vue3中新特性一览:

image-20210530231856940

二、Composition(组合式) API入门

2.0、关于

什么是Composition API? 为什么使用?

推荐阅读博文:vue3 composition(组合式)API 是什么?我为什么要使用它?_LGD_Sunday的专栏-CSDN博客

官方给出了一段引言,反复阅读:

通过创建 Vue 组件,我们可以将界面中重复的部分连同其功能一起提取为可重用的代码段。仅此一项就可以使我们的应用在可维护性和灵活性方面走得更远。然而,我们的经验已经证明,光靠这一点可能是不够的,尤其是当你的应用变得非常大的时候——想想几百个组件。当处理这样的大型应用时,共享和重用代码变得尤为重要

目前我们使用SFC( single-file component,单页面组件)写法来管理组件,其初始目的就是将页面上重复的内容抽离出来形成模块进行管理和重用。可是当单个文件中的内容变得十分庞大,逻辑变得更加复杂时,有暴露出两个问题:

  1. 代码的可读性下降
  2. 存在大量重复逻辑

官方的例子中,给出了一个视图组件,这一个组件有好几项职责,这固然会使得代码的逻辑复杂度激增!并且一个视图组件中只有一套组件选项(datamethodscomputedwatch),这一套组件选项就要同时管理这么多功能逻辑,如果代码中每一种颜色就表示一个逻辑关注点时,那么在“超大”组件中代码就变成这样的五颜六色。这就是我们在Vue2中使用的选项式API

Vue 选项式 API: 按选项类型分组的代码

这种代码维护起来简直就是噩梦,一个逻辑关注点的相关代码分散在文件的各个角落,我们在进行维护的时候,总是在上下翻寻代码,那么有没有可以将这些逻辑关注点的相关代码整理成块的方式?!

image-20210531002154034

就像这样,即使代码逻辑变得复杂,开发者进行维护和优化的难度也大大降低!那么接下来就是Composition API大放异彩的时候了!选项式API 与 组合式API 的较量拉开序幕

2.1、setup

组合式API依然是作用于组件,所以我们需要一个位置来使用组合式API,它就是setup

setup是一个接受propscontext参数的函数,它在组件被创建之前被调用,一旦props被解析后,setup就成为组合式API的入口。

由于setup执行时机的问题,组件还未被创建,所以组件中其他选项是无法在setup函数中使用的!当然this也是无法使用的!只能使用props(即外部传给组件的数据)

当setup执行完成后,其返回值中的内容将暴露给组件的其他部分:计算属性、方法、甚至是生命周期钩子函数以及组件的template。

废话不多说我们先来一个简单的案例:

App.vue

<template>
  <h1>Example</h1>
  <h2>Counter: {{counter}}</h2>
  <button @click="incr">+1</button>
</template>

<script lang="ts">
import {defineComponent} from 'vue'

export default defineComponent({
  name: 'App',
  // ===================================
  setup() {
    return {
      counter: 0
    }
  },
  // ===================================
  methods: {
    incr() {
      this.counter ++
      console.log(this.counter)
    }
  }
})
</script>

我们使用setup函数返回一个counter,值为0,然后我们可以直接在template中引用这个counter!并且我们可以在methods中使用this.counter去访问和使用 setup返回的counter。

但是,会发现一个问题:虽然counter的数值在增加,但是页面并没有变化?!

ref

这是咋回事?!

因为setup()返回的counter,并不是响应式数据!

响应式:页面会响应数据的变化进行重新渲染,也就是我们的数据和页面是绑定的。

官网上有一张图,说明了这个问题的解决方案:

按引用传递与按值传递

我们出现问题的原因是:template中的counter和代码逻辑中的counter只是主副本关系,两者相互独立。那么解决办法就是:创建一个counter的引用!如何创建呢?请看下节

2.2、ref

ref是一个接收参数并将其包装成一个带value的property的对象(Ref)并返回的函数,这样我们就拿到了一个数据对象的引用(即一个响应式数据!)我们可以通过对象的value property对数据值进行修改。

那么我们修改一下上面的代码,让setup返回一个响应式数据:

<script lang="ts">
import {defineComponent, ref} from 'vue'

export default defineComponent({
  name: 'App',
  setup() {
    let counter = ref(0)
    return {
      counter
    }
  },
  methods: {
    incr() {
      this.counter ++
    }
  }
})
</script>

// 不过这里好像并没有使用.value来操作响应式数据的值(可能是因为我们包装的原数据就是number),但是效果实现了响应式!

let counter = ref(2021)
console.log(counter.value)
console.log(counter)


// 2021
// App.vue:23 RefImpl {_rawValue: 2021, _shallow: false, __v_isRef: true, _value: 2021}

来看看声明文件中的ref函数,以及其返回值类型Ref:

image-20210531134304552

总结:ref()为我们创建了一个响应式的引用,通过这个引用我们就可以获取到响应式数据,并进行操作!在组合API中我们会经常提到引用!

其实响应丢失的问题,使用ref一般只针对基本类型的数据(number、string)因为JavaScript中Number和String都是进行值传递的!。但是如果setup函数返回了一个对象,好像就没有这种顾虑,因为返回的本身就是一个对象的引用:

<template>
  <h1>Example</h1>
  <h2>Counter: {{counter}}</h2>
  <h4>{{person}}</h4>
  <button @click="incr"> +1 </button>
</template>

<script lang="ts">
import {defineComponent, ref} from 'vue'

export default defineComponent({
  name: 'App',
  setup() {
    let counter = ref(2021)
    return {
      counter,
      person: {
        name: 'sakura',
        age: 21
      }
    }
  },
  methods: {
    incr() {
      this.person.age ++
      this.counter ++
    }
  }
})
</script>
ref-object

2.3、生命周期钩子

为了使组合式API更加完整,还提供了生命周期钩子函数的创建,这都要归功于Vue中几个新导出的函数,组合式API中钩子的名字与选项式API相似,不过都以on作为前缀。(例如:选项式中的mounted,在组合式中为onMounted

setup() {
    let counter = ref(0)

    onMounted(() => {
        console.log("`mounted` hook was triggered!")
        counter.value = 2021
    })

    return {
        counter,
        person: {
            name: 'sakura',
            age: 21
        }
    }
},

好像和我们之前写钩子函数的方式不太一样:

mounted() {
    console.log('选项式API的钩子被触发!')
}
// 或者这样
mounted: function() {
    // .....
}

我们现在直接将钩子的回调函数作为参数传入onMounted(hook: () => any)

2.4、watch监视

在组合式API中,我们可以使用vue导出的watch()函数像在选项式API中那样对property的修改做出对应的响应操作。

watch函数接收三个参数:

  • 要监视数据的响应式引用或者getter方法
  • 一个回调函数
  • 可选参数

下面是一个简单的watch函数使用:

setup() {
    let counter = ref(0)

    onMounted(() => {
        console.log('`mounted` hook was triggered!')
        counter.value = 2021
    })
	
    // 侦听counter
    watch(counter,(newValue, oldValue) => {
        console.log(`counter was changed from ${oldValue} to ${newValue}`)
    })

    return {
        counter,
        person: {
            name: 'sakura',
            age: 21
        }
    }
},
image-20210601121610914

watch()函数不仅可以在Vue实例的setup函数中使用,还可以在vue组件外使用

import {defineComponent, onMounted, ref, watch,} from 'vue'

// vue实例外部使用
let num = ref(0);
watch(num, (newValue, oldValue) => {
  console.log('num was changed!!' + oldValue + '->' + newValue)
})

export default defineComponent({
  name: 'App',
  setup() {
    let counter = num

    onMounted(() => {
      console.log('`mounted` hook was triggered!')
      counter.value = 2021
    })

    watch(counter,(newValue, oldValue) => {
      console.log(`counter was changed from ${oldValue} to ${newValue}`)
    })

    return {
      counter,
      person: {
        name: 'sakura',
        age: 21
      }
    }
  },
  methods: {
    incr() {
      this.person.age ++
      this.counter ++
    }
  },
  mounted() {
    console.log('选项式API的钩子被触发!')
  }
})

现在我们点击按钮,就会改变counter的数值,而counter保存的是num的引用,所以num的数值随之变化,两个侦听的回调函数都会被调用!

后面我们会详细看watch的高阶使用:响应式计算和侦听 | Vue.js (vuejs.org)

2.5、独立的computed属性

与watch一样,你可以从vue导入computed函数,它可以让你在组件外部创建一个独立计算属性!

计算属性必须是以响应式数据为基础的!并且computed函数返回的也是一个响应式引用!

import {computed, defineComponent, ref,} from 'vue'

let year = ref(2021);

let age = computed(() => {
  return year.value - 2000
})

export default defineComponent({
  name: 'App',
  setup() {
    let counter = year

    return {
      counter,
      person: {
        name: 'sakura',
        age: age
      }
    }
  },
  methods: {
    incr() {
      this.counter ++
    }
  }
})

当页面上的按钮按下后,counter的数值变化带动其引用的响应式数据year的数值变化,year数值变化又触发了computed回调函数(即computed函数的第一个参数,一个getter)变化,然后得到新的age数值!

**值得注意的是:computed函数参数中getter函数返回的是一个只读的响应式引用!**所以如果我们要使用.value来获取计算属性的值!

2.6、小结

一路下来,你可能感觉到我们一直都在壮大我们的setup()函数,往里面不停的塞东西。这样做的好处就是在实例在被mount之前,我们就可以用setup函数来处理一些事情。

但是setup函数不断壮大也会带给维护很大麻烦,但是你反过来看我们上述讲到的refwatchcomputed都是可以独立在组件外部使用的!只需要从Vue中import即可!!

那么这样的话,我们的单个逻辑关注点完全可以抽出一个单独的js/ts文件文件来完成编写,配合使用ref、watch、computed函数。多个逻辑关注点,在组件代码中进行导入并在setup函数中进行组合即可!!(如果不太明白,可以看看官方文档的介绍案例!)


三、响应性

本章属于Vue的原理学习,如果我们想要理解组合式API的一些操作,那么学习理解Vue的响应性是比不可少的!非侵入性的响应性系统也是Vue最独特的特性之一!

3.1、什么是响应性

学习了这么久的Vue,对于响应性应该并不陌生。官网给出了一个经典的例子:Excel编程,当我们对某个表格进行了编程设置为多个单元格之和时,那么当单元格数值发生变化就会引起编程单元格的值变化:

响应性-excel

例如上图中A3单元格被编程为=SUM(A1:A2)即A1到A2单元格的求和,一旦求和范围单元格发生了变化,结果单元格随之变化!

可是在我们的JavaScript中,并没有这种效果:

image-20210602121316387

我们用JavaScript代码模拟了我们在Excel中的操作,但是结果并不相同!也就是说普通的JavaScript代码是无法做到响应性更新的

我们需要如何使用JavaScript完成这个任务呢?官方给出了一种解决思路:

  • 检测其中某一个值是否发生变化
  • 用跟踪 (track) 函数修改值
  • 用触发 (trigger) 函数更新为最新的值

3.2、Vue中如何实现(难)

官方对这部分做了详细的说明,我们可以一步步进行模拟。

首先为了实现响应性更新,我们需要将我们更新数据的动作包裹成一个函数:

const updateSum = () => {
    sum = a1 + a2
}

下一步就是当数据更新后,我们如何告知Vue调用这个函数?

Vue通过一个副作用(Effect)来跟踪当前正在运行的函数,副作用是一个函数包裹器,在函数调用前就启动了跟踪,并且副作用知道何时运行,能在需要时再次执行!

createEffect(() => {
    sum = a1 + a2
})

createEffect()就是辅助追踪和执行的!它可以这样实现:

// 维持一个执行副作用的栈
const runningEffects = []

// fn 即我们上面的updateSum函数
const createEffect = fn => {
  // 将传来的 fn 包裹在一个副作用函数中
  const effect = () => {
    runningEffects.push(effect)
    fn()
    runningEffects.pop()
  }

  // 立即自动执行副作用
  effect()
}

如果这部分不是特别明白的话,可以先放一放。
我们后面会学习一个Vue公开API中暴露的一个watchEffect函数,其行为与我们例子中的createEffect()函数效果类型!

最后一步就是Vue如何跟踪数据变化,这部分较为重要并且涉及的内容较多,单独作为一节说明:

3.2.1、Vue如何跟踪数据变化

官方给出了最核心的一句指南:

当把一个普通的 JavaScript 对象作为 data 选项传给应用或组件实例的时候,Vue 会使用带有 getter 和 setter 的处理程序遍历其所有 property 并将其转换为 Proxy

可以先读一下这句话,目前我们还有一些知识点尚未涉及到,所以读起来有些生涩!

首先我们要认识一下ES6中新引入的Proxy对象

Proxy(前置内容)

Proxy初步认识

学习过设计模式的小伙伴,应该不会对这个词陌生。我们一般称其为代理,与其相关的有一个经典且重要的模式:动态代理设计模式!

不扯远了,我们先来看看proxy在实际使用中有什么作用吧。

我们先来阅读一下:Proxy - JavaScript | MDN (mozilla.org)

image-20210602173536166

简述:Proxy使得你可以为一个对象创建其代理对象,并且这个代理对象可以拦截并重定义被代理对象的基本操作

构建参数:

  • target:想要代理的原始对象
  • handler:一个对象,定义了那些操作会被拦截以及会被如何重定义(我们一般称其为处理程序)

如果理解起来还是很绕,回到现实生活中,最常见的代理就是代理中介,一般他作为卖家和买家中间的隔层,常见的就是房屋拥有者找代理帮他买房子,此时代理商就充当proxy的角色,房屋拥有者代表target,既然房屋拥有者找了代理,那你就得听代理的话,代理怎么买房子、什么流程与房屋拥有者没有关系,target就等着拿钱就行了,房屋中介的买房手段就是handler!

image-20210602180758064

我们经常看到,房屋中介低价收入房源,然后高价卖出,中间赚差价。这就是handler的功劳了!

基础示例

知道这些后,我们来看一个使用示例:

案例中创建了一个简单的target,并且创建了一个handler但是没有指定任何属性:

const target = {
  message1: "Hello",
  message2: "World"
};

const handler1 = {};

const proxy1 = new Proxy(target, handler1);

正因为我们的proxy的handler是空对象,那么他们的行为就和target一模一样:

console.log(proxy1.message1); // Hello
console.log(proxy1.message2); // World

下面我们给handler加一些内容:

参数介绍中说了,handler是一个对象,定义了哪些操作会被拦截,以及拦截后的操作重定义。那我们应该怎么写呢?

实例中的代码:

const handler2 = {
  get(target, prop, receiver) {
    return "HaHa";
  }
};

并解释:handler提供了get()的方法实现,它会拦截所有的target属性访问操作!

我们更换handler后,再运行看看:

console.log(proxy.message1) // HaHa
console.log(proxy.message2) // HaHa

*通常我们将handler中编写的函数叫做 “陷阱(trap)”,因为它会捕获目标对象即 target的引用!*上面的handler2例子只是对目标对象的属性访问操作设置了trap.

同理我们可以尝试一下创建一个属性设置的trap:对应handler的set()方法

let handler3 = {
  set(target, prop, value) {
    // 使用Reflect完成属性的更改!
    Reflect.set(target, prop, value)
    console.log(`${prop} was set, new value is ${value}`)
  },
}

let proxy = new Proxy(target1, handler3)

proxy.message1 = 'Sakura' // output: message1 was set, new value is Sakura
console.log(proxy.message1) // Sakura

可能看到这串代码时,你可能会对set方法的参数产生一些好奇,但是应该不难看出各个参数代表什么!

下面我们就get()set()方法来说说他们各自的参数含义!

handler.get()/set()

handler.get():

参数

  • target: 目标对象。
  • property: 被获取的属性名。
  • receiver: Proxy或者继承Proxy的对象

返回值

​ handler.get可以返回任何值~

拦截以下操作

约束

  • 如果要访问的目标属性是不可写以及不可配置的,则返回的值必须与该目标属性的值相同
  • 如果要访问的目标属性没有配置访问方法,即get方法是undefined的,则返回值必须为undefined

违反以上约束,proxy会抛出TypeError

var obj = {};
Object.defineProperty(obj, "a", {
  configurable: false,
  enumerable: false,
  value: 10,
  writable: false
});

var p = new Proxy(obj, {
  get: function(target, prop) {
    return 20; // 应改为 return target[prop]即返回目标对象的属性值
  }
});

p.a; //因为obj中a属性被定义为不可配置、不可写,而proxy.get违反了约束,会抛出TypeError

handler.set()

参数

  • target:目标对象
  • property: 被设置的属性
  • value: 将要设置的属性值
  • receiver: 最初被调用的对象。通常是 proxy 本身,但 handler 的 set 方法也有可能在原型链上,或以其他方式被间接地调用(因此不一定是 proxy 本身)。

返回值

​ 应当返回一个布尔值。

  • 返回 true 代表属性设置成功。
  • 在严格模式下,如果 set() 方法返回 false,那么会抛出一个 TypeError异常。

拦截以下操作

  • 指定属性值:proxy[foo] = barproxy.foo = bar
  • 指定继承者的属性值:Object.create(proxy)[foo] = bar
  • Reflect.set()

约束

  • 若目标属性是一个不可写及不可配置的数据属性,则不能改变它的值
  • 如果目标属性没有配置存储方法,即 [[Set]] 属性的是 undefined,则不能设置它的值。
  • 在严格模式下,如果 set() 方法返回 false,那么也会抛出一个 TypeError异常。
var p = new Proxy(
  {}, // 代理一个空对象
  {
    set: function (target, prop, value, receiver) {
      target[prop] = value // 设置属性
      console.log('property set: ' + prop + ' = ' + value)
      return true
    },
  },
)

console.log('a' in p) // false

p.a = 10 // "property set: a = 10"
console.log('a' in p) // true
console.log(p.a) // 10

其他handler方法使用请参考网站:Proxy - JavaScript | MDN (mozilla.org)

实现数据跟踪

使用 Proxy 实现响应性的第一步就是跟踪一个 property 何时被读取

所以我们需要对handler.get()加上一些跟踪操作,官方说明时使用了track()函数,并传入了target、property。并说明此函数会检查当前在运行的副作用(Effect),并记录在target、property旁边,Vue就知道了property是该副作用的依赖项!

const handler = {
  get(target, property, receiver) {
    track(target, property) // *
    return Reflect.get(...arguments)
  }
}

第二步就是在property更新时,执行此副作用,完成数据更新

那么就要对handler.set()下文章了,官方使用了trigger()函数作为触发执行函数:

const handler = {
  get(target, property, receiver) {
    track(target, property)
    return Reflect.get(...arguments)
  },
  set(target, property, value, receiver) {
    trigger(target, property) // **
    return Reflect.set(...arguments)
  }
}

现在我们回头看3.1节中末尾给出的实现响应性的三个步骤,Vue都有了具体的实现方式:

  1. 在handler.get中使用track()对属性开始跟踪
  2. 使用handler.set捕捉到属性值修改操作
  3. 使用triggle()函数执行effect,完成依赖数据的更新

现在我们用一个简单的Vue例子来复习一下:

const app = new Vue({
    el: `#app`,
    data() {
        return {
            a1: 2,
            a2: 3,
        }
    },
    computed: {
        sum() {
            return this.a1 + this.a2
        },
    },
})

console.log(`${app.a1} + ${app.a2} = ${app.sum}`) // 2 + 3 = 5

app.a1 = 3
console.log(`${app.a1} + ${app.a2} = ${app.sum}`) // 3 + 3 = 6

data()方法返回的a1、a2被包装成一个响应式代理,并存储为this.$data,而this. d a t a . a 1 和 t h i s . data.a1和this. data.a1this.data.a2则是this.a1和this.a2的别名,因为是通过代理去取值的!

image-20210603162237369

然后我们将sum()方法包裹在一个副作用中,当我们视图访问this.sum时,会使用副作用来计算数值,而前面使用的响应式代理$data将会跟踪a1、a2并在副作用执行时进行取值计算。

以上这些操作在Vue2中都是底层帮我们实现,用户感觉不到代理的存在。在Vue3中这些操作你可以通过一个独立包进行使用,将data返回数据封装为响应式代理的函数叫做reactive(),这样我们就可以在不使用组件的情况下就将一个对象包裹在一个响应式代理中!

3.3、响应性基础

3.3.1、声明响应式状态

上面说过了,在Vue3中可以使用reactive()方法为JavaScript对象创建响应式状态。代码如下:

import { reactive } from 'vue'

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

并且这个转换是一个“深度转换”(即嵌套对象的property也会受到影响!)

响应式状态在Vue中最常见的用例就是,用于声明响应式数据,当数据发生变化时,页面也同步渲染!当组件中data()返回一个对象时,内部会使用reactive将对象转换为响应式,模板也会编译成使用响应式对象的render函数。

3.3.2、创建独立的响应式值

如果我有一个基本类型的数值,我想将其转换为响应式,应该怎么做?使用前面学习组合式API提到的ref函数。

import { ref } from 'vue'
const counter = ref(0)
const name = ref('sakura')

如果ref()传入的是一个对象,那么会使用reactive对数据进行处理并返回!

返回响应式对象,其响应式引用维护着其内部的值,使用valueproperty即可取出。

console.log(name.value) // sakura

关于ref的基础使用就这些,下面我们说一些ref的使用技巧

ref解包

当Ref对象作为setup()返回对象的property并且在模板(template)中使用时,会做浅层的解包操作。不需要使用.value来取值,只有嵌套的对象的property需要用.value取值 !

<template>
  <h1>Now is {{year}}</h1>
  <h2>username is {{user.name.value}}</h2>
</template>

<script lang="ts">
import {defineComponent, ref} from 'vue'

export default defineComponent({
  name: 'App',
  setup() {
    const year = ref(2021)
    const name = ref('sakura')
    return {
      year: year,
      user: {
        name: name,
      }
    }
  }
})
</script>

示例代码中,在模板中使用year时自动做了ref解包操作,并不需要我们手动写year.value访问值!但是如果是嵌套的对象property,就必须使用.value了(例如代码中的user.name

访问响应式对象

当ref作为响应式对象(即使用reactive()函数处理过后对象),在访问或者设置property时,为了使体验与普通property操作相似,会自动对Ref对象进行解包!(即不需要使用.value取ref对象的值!)

<template>
  <h1>Now is {{year}}</h1>
  <h2>username is {{user.name}}</h2>
</template>

<script lang="ts">
import {defineComponent, reactive, ref} from 'vue'

export default defineComponent({
  name: 'App',
  setup() {
    const year = ref(2021)
    const name = ref('sakura')
    return {
      year: year,
      user: reactive({
        name: name,
      })
    }
  }
})
</script>

如果你为property重新赋值了一个ref对象,那么旧的ref对象将会被覆盖。

ref自动拆包只适用于在**响应式对象(Object)**中访问作为property的ref对象!

如果是一个集合(例如Array、Map)还是需要使用.value进行取值!

<template>
  <ul>
    <li v-for="hobby in hobbies">{{hobby}}</li>
  </ul>
</template>

<script lang="ts">
import {defineComponent, onMounted, reactive, ref, Ref} from 'vue'

export default defineComponent({
  name: 'App',
  setup() {
    const hobbies = reactive([ref('tennis'), ref('code'), ref('game'),])
    return {
      hobbies: hobbies
    }
  },
})
</script>

hobbies是一个响应式集合,但是如果我们直接在模板中使用集合中的ref对象时,并不会自动解包:

<ul>
    <li v-for="hobby in hobbies">{{hobby}}</li>
</ul>
image-20210604174938068

所以还是只能使用.value的方式进行取值显示:

<ul>
    <li v-for="hobby in hobbies">{{hobby.value}}</li>
</ul>
image-20210604175107534

3.3.3、响应式状态解构

当我们需要使用一个大型的响应式状态的某些property时,我们首先想到的就是用ES6中提供的结构语法,来从对象中提取我们需要的property:

import { reactive } from 'vue'
const blog = reactive({
  url: 'http://www.xxxxxx.blog?id=xxx',
  title: '菜鸟学Vue',
  author: 'sakura',
  releaseDate: '2021-6-4',
  genres: ['前端', 'JavaScript', 'TypeScript'],
  views: 1
})
// 使用解构获取property
let {title, author} = blog

console.log(title + ' - ' + author) // 菜鸟学Vue - sakura

这样做法并没有任何不妥,只是结构出来的两个property失去了响应性!(即使用解构出来的property时,任何对操作都不会影响原来的对象中的property,因为两者关系是值复制!)

title = 'Vue今日不学何时学'
console.log(title) // Vue今日不学何时学
console.log(blog.title) // 菜鸟学Vue  原对象中的property值没有受到影响!

如果你想要使用解构,并且保证property的响应性需要使用toRefs,将源对象转换为一组ref,这些ref将保证与源对象的property的关联:

import { toRefs } from 'vue'
let {title, author} = toRefs(blog)

title.value = 'Vue今日不学何时学'
console.log(title.value) // Vue今日不学何时学
console.log(blog.title)  // Vue今日不学何时学

使用了toRefs后,解构出来的property都是ref对象,所以需要使用.value来访问或者修改!

3.3.4、readonly限制修改

当我们创建的一个响应式对象,并且需要提供给多方共享,但是我又不希望他们对我提供的对象进行修改,我们可以加上readonly进行限制,如果没有加,则表示我对其开放修改权限。

示例:有一份共享数据,只允许root用户能改,guest账户只能查看

import {reactive, readonly} from 'vue'

const data = reactive({
  id: 725872,
  name: 'Yuchen Guan',
  age: 21,
  birth: '2000-08-17',
  identity: 'student',
  job: 'no'
})

const root = data
const guest = readonly(data)

root.name = 'sakura' // 通过
console.log(data.name) // sakura

guest.name = 'gyc' // 报错,提示只读

3.4、响应计算与侦听

3.4.1、计算值

我们通常需要使用一个依赖于其他状态的转态,在Vue组件中我们使用计算属性来解决需求。在组合式API中我们使用computed方法来完成。computed方法默认接收一个getter函数作为参数,此getter函数将返回一个不可变响应式ref对象

let firstName = ref('Tony')
let lastName = ref('Stark')

let fullName = computed(() => { return firstName.value + ' ' + lastName.value})

当你试图对fullName进行修改时,会提示你是一个readonly属性!这是因为我们只为计算属性提供了getter方法。

如果你想增加写功能,为computed传参的时候,用一个对象包裹getter、setter方法即可!

let firstName = ref('Tony')
let lastName = ref('Stark')

let fullName = computed({
    // getter
    get:  () => { return firstName.value + ' ' + lastName.value},
    
    // setter
    set: (newValue) => {
        // 解构赋值
        [firstName.value, lastName.value] = newValue.split(' ')
    },
})

现在你就可以对计算属性值进行修改了!

3.4.2、watch

回顾一下我们之前使用选项式中侦听属性时,使用watch的写法:

watch: {
    property: function(newValue, oldValue) => {
        /* ... */
    },
    // 或者
    property2(newValue, oldValue) {
        /* ...  */
    }
}

Vue3中提供了独立的watch API,即不依赖组件选项,你可以直接通过从vue中import然后在任何地方使用:

watch(firstName, (newVal, oldVal) => {
    console.log(`fistName was turn from ${oldVal} to ${newVal}`);
})

但是他们默认都是惰性的!即只有在侦听的源发生变化时,才会触发执行侦听的回调函数(也即副作用

侦听单/多个属性

上面我们演示的是对ref属性进行侦听,属性值改变后就会触发!我们还可以侦听一个返回值的getter函数:

let name = reactive({
    firstName,
    lastName
})

watch(
    // 被侦听的getter
    () => name.firstName,
    
    (newVal, oldVal) => {
        console.log(`fistName was turn from ${oldVal} to ${newVal}`);
    }
)

name.firstName = "Philip" // fistName was turn from Tony to Philip

我们还可以同时侦听多个数据源:

watch([lastName, firstName], () => {
    console.log('fullName was updated!')
})

这里我们同时监视了lastName和firstName,那么在修改会触发的副作用的参数newValoldVal会将两者结合为一个整体!(例如原始值是Tony Stark,那么当你修改firstName为Philip后,newVal就是Philip,Stark【因为两者现在是一个整体!】)

侦听响应式属性

当我们侦听的是一个响应式对象时,我们需要有一份对象值副本,方便我们在回调函数中查看历史的对象值:

在比较数组的值时:

let numbers: number[] = reactive([1, 3, 5, 7, 9])

watch(
    () => [...numbers],
    (numbers, prevArray) => {
        console.log(prevArray, numbers)
    },
)
numbers[0] = 2

这里如果我们直接将数据源写成numbers,但是其本身是一个响应式对象,无论什么时候获取他都是指向同一个东西。所以我们应该使用一个getter函数:() => [...numbers],每次都将响应式对象的内容生成一份副本!然后在回调函数中才会起作用。

image-20210609203524077

当我监听的内容是一个结构化的对象时,如果你需要监听到对象内部的内容修改,你需要额外对watch配置:

// 一个响应式对象
let person = reactive({
    id: 11111,
    name: {
        firstName: 'Tony',
        lastName: 'Stark',
    },
    age: 31,
})

watch(
    () => person,
    () => {
        console.log('info was updated')
    },
)

person.name.firstName = 'Philip'

上述这种写法,当对象的属性更新后,控制台没有输出,即修改操作并没有被侦听到。【但是修改已经发生了!】

你有两种选择:

  1. 将侦听的数据源,直接写成person,而不是用一个getter函数。

    watch(
        person,
        () => {
            console.log('info was updated')
        },
    )
    
  2. 给watch方法增加一个参数:{deep: true} 【推荐】

    watch(
        () => person,
        () => {
            console.log('info was updated')
        },
        { deep: true },
    )
    

可是当你使用deep后,想要将修改前后的数据输出来看的时候出现了问题:

watch(
    () => person,
    (oldVal, newVal) => {
        console.log('info was updated')
        console.log(
            `old: ${oldVal.name.firstName}, new: ${newVal.name.firstName}`,
        )
    },
    { deep: true },
)

person.name.firstName = 'Philip' // old: Philip new: Philip

我们的历史数据是被覆盖了嘛?!其实问题还是出在对象副本值上,我们需要借助第三方工具来对对象属性做深拷贝,才能达到为对象属性的深度侦听!(例如lodash的cloneDeep)

  1. 首先我们安装lodash

    npm install --save lodash
    
  2. 在声明文件shims-vue.d.ts中加上lodash的声明

    declare module 'lodash'
    
  3. 然后在我们的组件文件中(*.vue)中导入lodash模块进行使用:

    import _ from 'lodash'
    
    watch(
        // 使用lodash的cloneDeep进行对象值深拷贝
        () => _.cloneDeep(person),
        (oldVal, newVal) => {
            console.log('info was updated')
            console.log(
                `old: ${oldVal.name.firstName}, new: ${newVal.name.firstName}`,
            )
        },
    )
    
    person.name.firstName = 'Philip' // old: Tony new: Philip
    

3.4.3、watchEffect

在学习watch时,我们了解到其的副作用 执行的惰性的!即只有当侦听的属性发送了变化才会执行对应的回调函数。那么我们现在要学习的watchEffect则是根据响应式状态自动执行和重新执行副作用!

用一个示例来演示一下watchEffect的使用:

<template>
  {{counter}}
  <button @click="counter++">counter +1</button>
</template>

<script lang="ts">
import {defineComponent, ref, watchEffect} from 'vue'

export default defineComponent({
  name: 'App',
  setup() {
    let counter = ref(0)
    watchEffect(() => {
      console.log("counter was updated!", "counter now is: " + counter.value);
    })

    return {
      counter: counter
    }
  },
})
</script>

在watchEffect的参数中我们传入了一个匿名函数,他被叫做Effect即我们常说的副作用。它会在系统启动时就执行一次!无论副作用中依赖的响应式转态是否发生了改变!【启动执行在watch中可以通过加上immedate: true选项实现!】

然后当副作用中依赖的响应式状态变化后,都会重新应用副作用!【例如代码中副作用依赖响应式状态counter,当counter发生变化时,副作用就会被执行!】

watchEffect-demo

停止侦听

watchEffect 在组件的 setup() 函数或生命周期钩子被调用时,侦听器会被链接到该组件的生命周期,并在组件卸载时自动停止。

当然也提供手动停止侦听的方式:调用watchEffect的返回值

setup() {
    let counter = ref(0)
    let stop = watchEffect(() => {
        console.log("counter was updated!", "counter now is: " + counter.value);
    })

    counter.value ++ // 0 -> 1 有Effect输出
    // 停止侦听
    stop()
    counter.value ++ // 1 -> 2 无输出
    console.log(counter.value) // 2
},

清除副作用

先说说清除副作用在实际开发中应用的案例:

应用现在有使用watchEffect实现的一个搜索功能,用户点击search按钮后,其副作用就会触发一次搜索请求。

如果现在用户点了五次search按钮,那么就会发送五次搜索请求。如果允许这样的操作出现就会带给服务器巨大的压力!

如果我们能在副作用中取消上一次还未执行完的副作用,这样就能始终保证只有一次请求处理!这就是清除副作用在实际场景中应用的案例!我们常称其为【按钮去抖动

我们用counter案例来重现一下问题:

<template>
  <h2>{{counter}}</h2>
  <br>
  <button @click="counter++">+1</button>
</template>

<script lang="ts">
import {defineComponent, ref, watchEffect} from 'vue'

export default defineComponent({
  name: 'App',
  setup() {
    let counter = ref(0)
    // 副作用是一个异步任务
    watchEffect(() => {
      setTimeout(() => {
        console.log("counter was updated!");
      }, 1000)
      console.log("counter now is", counter.value)
    })
    return {counter}
  },
})
</script>

可以发现watchEffect的副作用是一个异步任务,即有可能副作用还没执行完,我们的响应式状态已经发生了变化,那么还未执行完成的副作用就是无意义的,将其即使清理掉就是本小节要完成的任务!

你可以将+1按钮视为search按钮,副作用中的延时任务是我们的搜索数据请求过程。如果不使用清除副作用你将看到这样的效果:

watchEffect-cancelEffect

我们连续点击了四次按钮,虽然“搜索过程”没有跟上我们点击速度,但是每次搜索请求都发送到了服务器!

我们期望的效果则是:我们连续点击后,每次点击都将上一次未完成的“搜索任务”取消掉,那么最终只会有最后一次点击发出的搜索请求会被处理!

下面来看看如何实现:

副作用(Effect)函数接收一个onInvalidate函数作为入参,此函数用于注册清理失效的副作用时的回调

何时会调用这个回调函数:

  • 副作用被重新执行时
  • 侦听被停止后(如果在setup中使用watchEffect,组件生命周期结束,在组件被卸载时)
watchEffect((onInvalidate) => {
    /* ...  */
    // 注册清理失效的副作用的回调函数!
    onInvalidate(() => {
        console.log("old request was cancelled!");
        // ... 这里我们手动取消失效的Effect中的异步任务!
    })

})

onInvalidate只是一个注册清理失效副作用的回调函数的函数!具体如何取消副作用的中未执行完的异步任务,需要在回调函数中完成!

例如我们上面的案例:

watchEffect((onInvalidate) => {

    let request = setTimeout(() => {
        console.log("counter was updated!");
    }, 1000);

    // 清理副作用
    onInvalidate(() => {
        console.log("old request was cancelled!");
        // 取消timeout任务
        clearTimeout(request)
    })

    console.log("counter now is", counter.value)
})

在回调函数中我们使用clearTimeout来取消前面未完成的延时任务!然后实现的效果如图:

watchEffect-cancelEffect-02

最终你会发现,我们点击了多次按钮,但是只有最后一次的请求被响应!前面未执行完成的请求都在副作用被重新执行时利用我们在onInvalidate中的回调函数取消了!

副作用的刷新时机

由于目前还没有测试的场景,无法细致讲清楚其带来的变化。

默认情况下,我们的副作用将会在组件DOM更新(或者组件初始化前)执行,如果需要可以在watchEffect的第三个参数options中加上flush: 'post'(默认为pre),使得副作用在DOM更新后执行!【此外flush还支持sync

关于其具体的使用效果,在后面的模板引用中会进行介绍。

四、Composition API

现在我们继续深入学习组合式API,通过前面的入门学习各位应该都清楚为什么使用组合式API,以及其带来的便利。我们现在分点进行深入学习。

4.1、Setup

4.1.1、参数

先来看看setup函数的两个参数:

  • props: 组件接收的属性
  • context: 上下文对象

下面我们来看看一般如何使用这两个参数:

props

在vue2中我们和props很早就见面了,在父子组件通信中我们首次接触到props,使用props组件可以接收外部传入的数据,并应用在模板代码上!所以props默认就是响应式的,我们可以在setup函数中进行操作:

下面我们创建了一个组件:

BlogPanel.vue

<template>
<div class="blog">
  <h1>{{title}}</h1>
  作者: <span>{{author}}</span>
  <hr>
  点赞: <span>{{like}}</span>
</div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
  name: 'BlogPanel',
  props: {
    title: String,
    like: Number,
    author: String
  }
})

</script>

<style>
  .blog {
    border: rgb(110, 15, 15) 2px solid;
    border-radius: 5px;
    padding: 5px;
    display: inline-block;
  }
  span {
    color: rgb(10, 99, 233);
    font-weight: bold;
  }
</style>

我们定义了三个props:title、like、author,并且都在template中进行了使用。然后我们在App.vue中使用了这个组件,同时向组件的props进行传值:

App.vue

<template>
  <BlogPanel 
  :title="title"
  :like="like"
  :author="author"
  >
  </BlogPanel>
</template>

<script lang="ts">
import {defineComponent} from 'vue'
import BlogPanel from './components/BlogPanel.vue'

export default defineComponent({
  name: 'App',
  data() {
    return {
      title: '人菜就要多学习,颜丑就要多读书',
      like: 1,
      author: 'sakura'
    }
  },
  components: {
    BlogPanel
  }
})
</script>

目前显示效果:

image-20210604220557875

现在我们希望在BlogPanel中使用这些传入的属性值:我们可以直接通过setup的参数进行访问:

export default defineComponent({
  name: 'BlogPanel',
  props: {
    title: String,
    like: Number,
    author: String
  },
  setup(props, context) {
    console.log(`标题: ${props.title}, 作者: ${props.author}, 点赞数: ${props.like}`)
  }
})

如果使用了ES6解构从props中抽取个别property,抽取出来的property会失去响应性,我们应该使用toRefs对props进行解构!(参考3.3.3节响应式状态解构!)

如果props中某个属性是可选的,即我们取值时可能为空,
使用toRefs解构时,需要为属性指定一个默认值:例如toRefs(props.title, 'default value')

context

context是一个普通的JavaScript对象,意味着你可以对其使用ES6解构。context对象暴露了三个property:

  • attrs: 组件的非props属性
  • slots: 组件的插槽
  • emit(event: string, args: any[])【方法】: 用于组件发射事件

还是上面的例子:

BlogPanel.vue

<template>
<!--增加style属性(非props属性【attrs】)-->
<div class="blog" style="color: red">
  <h1>{{title}}</h1>
  作者: <span>{{author}}</span>
  <hr>
  点赞: <span>{{like}}</span>
  <hr>
  <!--创建一个插槽-->
  分类: <slot>预留插槽</slot>
</div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
  name: 'BlogPanel',
  props: {
    title: String,
    like: Number,
    author: String
  },
  setup(props, context) {
    console.log(`标题: ${props.title}, 作者: ${props.author}, 点赞数: ${props.like}`)
    console.log(context.attrs)
    console.log(context.slots)
    // 发出test事件
    context.emit('test', ['Hello World', 2022])
  }
})

</script>

<style>
  .blog {
    border: rgb(110, 15, 15) 2px solid;
    border-radius: 5px;
    padding: 5px;
    display: inline-block;
  }
  span {
    color: rgb(10, 99, 233);
    font-weight: bold;
  }
</style>

App.vue

<template>
  <BlogPanel 
  :title="title"
  :like="like"
  :author="author"
  @test = "output"
  >
  </BlogPanel>
</template>

<script lang="ts">
import {defineComponent} from 'vue'
import BlogPanel from './components/BlogPanel.vue'

export default defineComponent({
  name: 'App',
  data() {
    return {
      title: '人菜就要多学习,颜丑就要多读书',
      like: 1,
      author: 'sakura'
    }
  },
  methods: {
    output(args:any[]) {
      console.log(args)
    }
  },
  components: {
    BlogPanel
  }
})
</script>

菜鸟我现在还不清楚attrsslots两个property如何使用!官方说他们是有状态的对象,会随组件的更新而更新,所以我们不应该对他们进行解构,应该始终以attrs.xx / slots.xx来访问其属性!并且他们是非响应式的!

但是这个context.emit结合前面父子组件通信的事件发射,应该容易理解!其作用等同于:this.$emit

示例中在子组件setup函数内发射了一个test事件,然后在父组件中进行监听并输出其参数!@test="output"

4.1.2、访问组件的property

由于setup的运行时机【组件创建之前调用,props解析完成之后】

所以setup函数中只能通过函数访问到组件的propsattrsslotsemit()属性/方法

datamethodscomputed这些属性在组件为创建前都是无法访问的,故setup函数中无法使用!

并且由于此时组件还未创建,所以setup()无法使用this

4.1.3、结合template使用

除了props中从外部传入的属性在模板中可以进行使用,此外在setup中返回的属性/方法也可以直接在template中使用!

并且得益于refs在模板中使用时的浅层自动解包,使用起来和data几乎没有差别!

前面已经使用过很多次了,就不再提供示例了。

4.1.4、返回渲染函数

setup返回值可以返回一个渲染函数,会替换当前模板的渲染函数:

还记得我们在Vue学习模板编译时,使用runtime-only创建的项目中,渲染函数render:用到了一个函数h,这里我们也是使用此函数来生成的:

import { defineComponent, h } from 'vue'

export default defineComponent({
  name: 'BlogPanel',
  setup(props, context) {
    return () => h('div', ['Hello World'])
  }
})

这样组件的template的渲染函数会被覆盖掉:

image-20210605141930706

4.2、生命周期钩子函数

先来看一下选项式和组合式生命周期钩子函数命名对照:

选项式 APIHook inside setup
beforeCreateNot needed*
createdNot needed*
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
errorCapturedonErrorCaptured
renderTrackedonRenderTracked
renderTriggeredonRenderTriggered
activatedonActivated
deactivatedonDeactivated

基本上setup中的生命周期钩子函数命名只是在原来的基础上加上了on前缀,并使用小驼峰命名。

为什么createdbeforeCreate在setup中没有对应的钩子函数呢?

因为setup本身就是围绕created、beforeCreate进行的,【setup在组件创建前调用】。所以你可以将setup()就看做是一个created、beforeCreate的合体钩子函数,选项式中它们中间的代码在组合式API下都应该写在setup函数内!

除此以外,相较于Vue2.x声明周期的钩子的位置和命名也发生了微小的变化:

2.x中的beforeDestroydestroyed在3.0中被替换为beforeUnmountunmounted!

实例的生命周期

4.3、Provide & Inject

4.3.1、基本介绍

在Vue2.x的学习中,父子组件的传值都是使用props,但是如果是深层的父子组件进行传值,就只能沿着组件链向下传,这个传递的过程只要缺了一环就会导致最终子组件的显示错误!

image-20210605151027438

并且这样层层传递,如果数据出错不便于我们排查,我们需要沿着组件链依次检查!那么有没有一种办法使得组件提供属性,然后其所有的子组件可以按需取用?!

image-20210605151801275

这就是我们马上要学习的Provide & Inject

父组件Provide数据,子组件按需Inject数据。

props传递(传统)实现

开始之前,我们先用传统props传递实现:

从上到下的父级关系为:App -> BlogPanel -> Blog

App.vue:

<template>
  <BlogPanel :title="title">
  </BlogPanel>
</template>

<script lang="ts">
import {defineComponent} from 'vue'
import BlogPanel from './components/BlogPanel.vue'

export default defineComponent({
  name: 'App',
  data() {
    return { 
      title: 'Vue的学习之路'
    }
  },
  components: {
    BlogPanel
  }
})
</script>

BlogPanel.vue

<template>
  <div class="panel">
    <Blog :title="title" :preview="preview" :author="author"></Blog>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
import Blog from './Blog.vue'

export default defineComponent({
    name: "BlogPanel",
    props: {
      title: { 
        type: String,
        required: true
      }
    },
    data() {
      return {
        preview: '一个菜鸟的Vue学习记录罢了...',
        author: 'LiMing'
      }
    },
    components: { Blog },
})
</script>

<style>
.panel {
  min-width: 50%;
  max-width: 70%;
  background-color: rgb(235, 228, 221);
  border: #aaaaaa 5px dotted;
  margin: auto;
  padding: 15px;
  border-radius: 10px
}
</style>

Blog.vue

<template>
  <div class="blog">
    <h2 class="title">{{title}}</h2>
    摘要:<span class="preview">{{preview}}</span>
    <hr>
    作者:<span class="author">{{author}}</span>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
  name: 'Blog',
  props: {
    title: {
      type: String,
      required: true
    },
    preview: {
      type: String,
      default: '无法预览'
    },
    author: {
      type: String,
      default: 'unknown'
    }
  }
})
</script>

<style>
.blog {
  border: #333333 2px solid;
  border-radius: 5px;
  padding: 10px;
  margin: 25px
}
.title {
  color: rgb(0, 82, 136)
}
.preview {
  color: gray;
  font-size: 12px;
}
.author {
  color: rgb(51, 143, 51);
}
</style>

这里我们主要关注title属性从App传递到Blog组件的过程,中间经历了一次转发【BlogPanel使用props接收App传入的title,然后再传给Blog组件,Blog组件内再使用props接收,然后再模板中使用!】

这还只是经过了一次转发,如果两个组件的层级相差很深的话,就需要多次这种“无效”的转发,并且每次转发都不能出错!


使用Provide&Inject

现在我们来试试使用ProvideInject写法:

App.vue

<template>
  <BlogPanel>
  </BlogPanel>
</template>

<script lang="ts">
import {defineComponent} from 'vue'
import BlogPanel from './components/BlogPanel.vue'

export default defineComponent({
  name: 'App',
  // Provide 提供数据
  provide: {
    provideTitle: 'Vue的学习之路'
  },
  components: {
    BlogPanel
  }
})
</script>

因为不需要中间层转发,所以BlogPanel中没有任何相关操作,并且你可以将title的props和子组件传值操作直接去除!

Blog.vue

组件不需要通过props链完成传值,改为使用inject注入属性值,所以可以移除props中的title:

<template>
  <div class="blog">
    <h2 class="title">{{title}}</h2>
    摘要:<span class="preview">{{preview}}</span>
    <hr>
    作者:<span class="author">{{author}}</span>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
  name: 'Blog',
  // 使用inject 注入属性值,
  inject: ['provideTitle'],
  data() {
    return {
      // 将注入的值转变为数据进行使用
      title: this.provideTitle
    }
  },
  props: {
    preview: {
      type: String,
      default: '无法预览'
    },
    author: {
      type: String,
      default: 'unknown'
    }
  },
  
})
</script>
<style>
/*
    略
 */
</style>

这种父子传递数据的方式,更加扁平化,提供者与需求者直接交流,没有中间层。子组件定义好“接口”【需求】,父组件定义好数据【供给】。感觉和props差不多,只是范围更广泛,一样降低了父子组件的耦合度。

  • 父组件Provide时不必知道数据将提供给谁
  • 子组件Inject时不必知道数据的来自何处

如果provide提供的数据是来自实例的property时,需要将provide转化为函数:

import {defineComponent} from 'vue'
import BlogPanel from './components/BlogPanel.vue'

export default defineComponent({
  name: 'App',
  data() {
    return {
      title: 'Vue的学习之路'
    }
  },
  provide() {
    return { 
        provideTitle: this.title 
    }
  },
  components: {
    BlogPanel
  }
})

4.3.2、处理响应性

使用Provide和Inject绑定的属性,并不是响应性的。例如上面例子中,父组件App提供的provideTitle,与Blog组件中注入的provideTitle虽然是一一对应,但是App组件中对其的修改并没有带动Blog组件中数据变化。所以如果我们希望任何对provider中提供的数据操作都能被注入者响应,我们需要provider返回一个ref值或者reactive对象!

这里我们使用了computed函数,返回了一个计算属性。

provide() {
    return { 
        provideTitle: computed(() => { return this.title }) 
    }
},
provide响应性

但是为什么我们将this.title使用ref包裹就无法达到此效果呢?! 等待后面的详细学习进行解答

4.3.3、组合式API中使用

除了前面在选项式中可以使用,组合式API也不示弱!但是provide & inject都只能在setup函数内调用,并且都是以方法形式调用,需要从vue中显示导入provideinject

provide()需要传入两个参数分别对应property的key和value!(key的类型可以是string或者number或者一个InjectionKey【extends Symbol】)

export declare function provide<T>(key: InjectionKey<T> | string | number, value: T): void;

inject()需要传入一个property的key,还有一个可选参数:属性默认值。会如果provide的property中能找到对应的key,即将属性值返回,若没有返回配置的可选参数。你可以使用一个变量进行接收然后进行使用!

下面我们用组合式API的写法,来调整一下代码:

App.vue:

setup() {
    let title:string = 'Provide & Inject 代码笔记'
    // 提供(provide)属性
    provide('title', title)
    return { title: title }
},

Blog.vue

setup() {
    // 注入(Inject)属性
    let title = inject('title')
    return {
        title: title
    }
},

4.3.3.1、解决响应性

使用上述方式完成provide和inject后,绑定的属性依旧不是响应性。原因还是在于provide方提供的是一个非响应式的数据。所以我们修改调整provide时的数据,使用ref或者reactive进行包裹!

setup() {
    // ref包裹数据,返回响应式数据
    let title = ref('Provide & Inject 代码笔记')
    provide('title', title)
    return { title: title }
},

inject方不用做任何操作【所有被provide的属性数据操作相关的都由Provide方进行负责,inject不必知道过多的细节。】

provide响应性-组合式

4.3.3.2、响应式Property的修改

上面说过,使用响应式的provide/inject属性时,应当尽可能将对property的修改限制在provide组件的内部!【所有的数组操作由Provide方管理】

export default defineComponent({
  name: 'App',
  setup() {
    let title = ref('Provide & Inject 代码笔记')
    provide('title', title)
    let setProvideTitle = () => {
      title.value = 'Provide 提供的修改方法2'
    }

    return { 
      title: title ,
      setProvideTitle: setProvideTitle
    }
  },
  methods: {
    setTitle() {
      this.title = 'Provide 提供的修改方法'
    }
  },
  components: {
    BlogPanel
  }
})

例如上面代码中,我们分别在在setup和methods选项中设置了一个方法对provide的title进行修改。

当需要在inject组件中修改inject的属性时,也建议使用provide组件 提供一个修改方法然后由inject组件注入并使用!

App.vue

let setProvideTitle = () => {
    title.value = 'Provide 提供的修改方法2'
}
// provide 修改方法
provide('setProvideTitle', setProvideTitle)

Blog.vue

// 注入对应的修改方法
let setTitle = inject('setProvideTitle', () => {})
return {
    setTitle: setTitle
}

**此外不建议在Inject组件中对注入的属性直接修改!**虽然inject/provide组件中响应式的property的数据值是绑定的(即任何一边的修改都能影响到另外一边的数据!)但是也不希望这种修改操作对inject组件开放。

还是那句话:【被provide的响应式property的所有操作,都应该由provide组件一手操办!】

消费者(inject)使用生产者(provide)提供的方式[产品说明]对产品(property)进行操作!

如果你觉得只是规定不够强势,你可以在provide时使用readonly对property进行修饰,保证其对于inject组件是只读的!(关于readonly请查看3.3.4小节)

let title = ref('Provide & Inject 代码笔记')
// readonly修饰属性,表示inject组件对此属性为只读
provide('title', readonly(title))

当你想要在inject组件中视图修改注入的响应式组件时:

provide响应性-组合式-readonly

修改操作是被警告的,并且不会通过

4.4、模板引用

我们如果需要对模板中的元素或组件实例进行引用,就需要引用本节需要学习的知识: 模板引用

我们可以先在setup中创建并暴露一个ref对象,然后在模板中使用ref属性绑定我们暴露的ref对象:

<template>
  <div ref="element"></div>
</template>

<script lang="ts">
import {defineComponent, ref} from 'vue'

export default defineComponent({
  name: 'App',
  setup() {
    let element = ref('')
    return {element}
  },
})
</script>

// 待补充

4.5、自定义hook函数

使用hook函数,我们可以将组件中的复杂逻辑抽离成为一个功能函数,便于复用。其功能类似于Vue2中的mixin

4.5.1、演示案例

现在我们完成了一检测用户鼠标点击位置的功能,我们希望将其转变为hook函数,并在多个组件中复用!

以下是我们在组合式API中完成的功能实现:

<template>
  <h1>请点击屏幕</h1>
  <h2>点击位置坐标:({{x}}, {{y}})</h2>
</template>

<script lang="ts">
import {defineComponent, onBeforeUnmount, onMounted, ref} from 'vue'

export default defineComponent({
  name: 'App',
  setup() {
    let x = ref(0)
    let y = ref(0)

    // 点击事件处理器,将点击事件的pageX、pageY赋值给x,y
    const clickHandler = (event: MouseEvent) => {
      x.value = event.pageX
      y.value = event.pageY
    }

    // 挂载前,增加一个点击事件监听器
    onMounted(() => {
      window.addEventListener('click', clickHandler)
    })

    // 卸载组件前,移除监听器
    onBeforeUnmount(() => {
      window.removeEventListener('click', clickHandler)
    })

    // 返回获取的坐标x,y
    return {
      x,
      y
    }
  }
})
</script>

<style></style>

现在的问题就是功能复用性太差,我们需要将其抽离出来成为hook函数。请参照下面的做法:

首先创建一个ts文件,将功能代码使用函数形式包裹,然后export,按需引入需要的外部模块

import {onBeforeUnmount, onMounted, ref} from "vue";

export default function () {
  /**
   * 功能实现...略
   */

  // 返回获取的坐标x,y
  return {
    x,
    y
  }
}

然后回到vue组件代码中,从刚才的ts文件中引入我们export的功能函数,并使用。

<template>
  <h1>请点击屏幕</h1>
  <h2>点击位置坐标:({{x}}, {{y}})</h2>
</template>

<script lang="ts">
import {defineComponent} from 'vue'
// 引入功能函数
import useMousePosition from "./hooks/useMousePosition";

export default defineComponent({
  name: 'App',
  setup() {
    // 使用功能函数
    let {x, y}  =useMousePosition()
    return {
      x,
      y
    }
  }
})
</script>

<style></style>

这样我们的功能代码就可以在其他组件中进行重用了!当然这一切都得益于Vue3中大部分选项式中的组件选项都有了独立的API可以在组件外部单独使用!

4%E5%90%88%E5%BC%8F-readonly.gif" alt=“provide响应性-组合式-readonly” style=“zoom:80%;” />

修改操作是被警告的,并且不会通过

4.4、模板引用

我们如果需要对模板中的元素或组件实例进行引用,就需要引用本节需要学习的知识: 模板引用

我们可以先在setup中创建并暴露一个ref对象,然后在模板中使用ref属性绑定我们暴露的ref对象:

<template>
  <div ref="element"></div>
</template>

<script lang="ts">
import {defineComponent, ref} from 'vue'

export default defineComponent({
  name: 'App',
  setup() {
    let element = ref('')
    return {element}
  },
})
</script>

// 待补充

4.5、自定义hook函数

使用hook函数,我们可以将组件中的复杂逻辑抽离成为一个功能函数,便于复用。其功能类似于Vue2中的mixin

4.5.1、演示案例

现在我们完成了一检测用户鼠标点击位置的功能,我们希望将其转变为hook函数,并在多个组件中复用!

以下是我们在组合式API中完成的功能实现:

<template>
  <h1>请点击屏幕</h1>
  <h2>点击位置坐标:({{x}}, {{y}})</h2>
</template>

<script lang="ts">
import {defineComponent, onBeforeUnmount, onMounted, ref} from 'vue'

export default defineComponent({
  name: 'App',
  setup() {
    let x = ref(0)
    let y = ref(0)

    // 点击事件处理器,将点击事件的pageX、pageY赋值给x,y
    const clickHandler = (event: MouseEvent) => {
      x.value = event.pageX
      y.value = event.pageY
    }

    // 挂载前,增加一个点击事件监听器
    onMounted(() => {
      window.addEventListener('click', clickHandler)
    })

    // 卸载组件前,移除监听器
    onBeforeUnmount(() => {
      window.removeEventListener('click', clickHandler)
    })

    // 返回获取的坐标x,y
    return {
      x,
      y
    }
  }
})
</script>

<style></style>

现在的问题就是功能复用性太差,我们需要将其抽离出来成为hook函数。请参照下面的做法:

首先创建一个ts文件,将功能代码使用函数形式包裹,然后export,按需引入需要的外部模块

import {onBeforeUnmount, onMounted, ref} from "vue";

export default function () {
  /**
   * 功能实现...略
   */

  // 返回获取的坐标x,y
  return {
    x,
    y
  }
}

然后回到vue组件代码中,从刚才的ts文件中引入我们export的功能函数,并使用。

<template>
  <h1>请点击屏幕</h1>
  <h2>点击位置坐标:({{x}}, {{y}})</h2>
</template>

<script lang="ts">
import {defineComponent} from 'vue'
// 引入功能函数
import useMousePosition from "./hooks/useMousePosition";

export default defineComponent({
  name: 'App',
  setup() {
    // 使用功能函数
    let {x, y}  =useMousePosition()
    return {
      x,
      y
    }
  }
})
</script>

<style></style>

这样我们的功能代码就可以在其他组件中进行重用了!当然这一切都得益于Vue3中大部分选项式中的组件选项都有了独立的API可以在组件外部单独使用!

posted @ 2022-01-21 20:33  5akura  阅读(170)  评论(0编辑  收藏  举报