Vue3小小记录

1,073 阅读27分钟

为什么要用Vue3?

  1. 性能提升,运行速度是Vue2的1.5倍
  2. 体积更小,按需编译,体积比Vue2小的多
  3. 类型判断,支持TypeScript
  4. 高级给予,暴露了更底层的API和提供更先进的内置组件
  5. 组合API,更好的组织逻辑,封装逻辑,复用逻辑
  6. 最重要的一点,就是被生活所迫,公司需要鸭~ 一、在开发中,我们经常会碰到这种script里写个setup,什么意思???

setup函数和script+setup【语法糖】 - 理解

image.png

它是Vue3的一个新的语法糖(骚等再来解释,先来看看setup函数和script+setup的相同与不同点

#### 相同点:
1.(很多文章都说,setup是在beforeCreate和create生命周期之间运行)
其实不是这样的,实践出真知,上代码:

import { defineComponent } from 'vue'
 export default defineComponent ({
    beforeCreate() {
       console.log("beforeCreate已执行")
    },
    created() {
       console.log("create已执行")
    },
    setup() {
       console.log("setup已执行")
    }
 })
 控制台打印结果: 
      setup已执行  beforeCreate已执行  created已执行
 在得出结论之前,我们再来看看Vue2的生命周期函数:
      beforeCreate 和 created生命周期函数
 
 beforeCreate函数:此时,实例刚在内存中被创建出来,还没有
初始化好data和methods属性,使用data里的数据或调用methods
里的方法都是undefined(未定义)
                  
 created函数:实例已经在内存中被创建出来,此时的data和methods
已经被创建完成,但是template模板还没开始编译
 
 【嘿嘿】,顺带再介绍一下 beforeMount 和 mounted 叭 ~~~
 
 beforeMount函数:此时已经完成了模板的编译,但是还没有挂载到
页面上!
 
 mounted函数:将已经编译好的模板挂载到页面中指定的容器中显示!
 
 【来个小小的结论】:因为setup函数的执行在beforeCreated和
created之前,所以在setup里,我们是无法使用data里的(响应式)
变量和调用methods里的方法的

2.(相同点):setup里是没有this指向的,this是undefined,为什么呢???

在Vue3源码中(源码就先不上,先理解下意思),setup并没有去绑定this,为什么? 原因是 在Vue2中,我们可以在optionsAPI(Vue2使用的是optionsAPI,来定义组件内部的一些属性,如methods、data等等,【Vue3使用的是compositionAPI,也叫组合式API,vue3组合式API的入口就是setup函数】)中调用this来指向当前组件的实例(const me = new Person()),me就是Person的实例。但是在Vue3的setup中并不能这样做,因为setup的执行是位于组件创建成功之后(此时并没有解析到data、methods、computed等,正常情况来说,通过new Vue()创建vue实例之后,首先先进入的应该是beforeCreate生命周期,但是setup的执行是在beforeCreate之前的,所以此时this是undefined)

不同点

1.其实 script + setup 是 setup函数的语法糖,更方便,代码量更少。后者,在setup函数里定义的变量、方法等都需要暴露(return)出去,模板(template)里才能使用。在setup函数中引入组件,还需要注册才能使用;而前者,script+setup中声明的函数、变量以及import引入的内容、组件都能在模板中直接使用【在script+setup中,我们不必再声明export default和setup方法,这种写法会自动将所有的顶级绑定(包括变量、函数声明,以及import引入的内容)声明公开给模板(template)使用

setup函数就像这样

<script>
import { defineComponent, ref } from 'vue'
import HelloGirl from './HelloGirl.vue'

    export default defineComponent({
        compoents: {
           HelloGirl
        },
        setup () {
           const name = ref('张三')
           const changName = modifyName => {
             name.value = modifyName 
           }
           return {
              name,
              changeName
           }
        }
    })
</script>

ps:像ref、defineCompoennt这种不知道什么不知道什么意思的,
后续都会解释(不要着急,不要做着急的男人,拒绝做快男~)
>:如果这个变量、方法等定义了很多,是不是觉得很麻烦?
疯狂return,是不是比谈恋爱还要麻烦??

和上面同样的功能,用script+setup我再来写一遍(里面很多方法都没用到,现在先浅谈一下【毕竟就是语法差别,一个意思,跟随时尚,能少几行代码少几行,用最短的代码把功能写出来!用最短的时间把女py追到是我们写代码人的宗旨!

<script setup>
import { ref } from 'vue'
import HelloGirl from './HelloGirl.vue'
           const name = ref('张三')
           const changName = modifyName => {
             name.value = modifyName 
           }
    })
</script>

>代码不要太精简,代码不要太美丽,这就是女人,
哦不,script+setup的魅力吗?
>嗯,是的。

defineComponent

接下来,先出场的是defineComponent(这个讲完后,会按顺序将开发中常用api用最温柔的语气,最易懂的方法给你们一一道来,退后,我要开始装逼了……

Vue3中,新增了defineComponent,其实它并没有实现任何的逻辑,它做的只是将接收到的Object直接返回,它的存在就是让传入的对象获得对应的类型,它的存在就是完全为了服务TypeScript【就是定义类型的,js常量、变量、数组、函数参数等,不管什么类型的都可以赋值给它们,Ts就是规定变量类型。为了防止随意赋值这种现象,Ts光荣诞生了~ 解释-安排:这就好比没有法律之前,渣女无休止的伤害我们这些纯情少男而没有受到制裁,法律【TypeScript】来了之后,渣女们就开始收敛了不少,因为渣女们再渣我们【定义了类型再给我乱写】,控制台就会报错:会告诉渣女们,你错了!【please保护我】】而存在的。

关键源码:
export function defineComponent (options: unknown) {
  return isFunction (options) ? { setup: options } : options
}

options: unknown  ->  这就是TypeScript对参数类型的定义
平常我们没有用setup语法糖写的时候,都是这么写的:

// 注意我们传入的是个对象
export default defineComponent ({})  
再对比源码,它是在判断是不是函数,不是就直接返回原来我们传给
defineComponent的(在这里是{})

Ts默认情况下根本不认识.vue文件,为什么Vue-cli创建的项目,可以识别呢? 因为在shims-vue.d.ts文件中有声明

/* eslint-disable */
declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}

对比vue+js,我们可以发现vue+ts里多了个defineComponent的函数包裹,它有什么作用呢?

export default defineComponent ({
  setup () {
    return {}
  },
  components: {}
})
  • 【从js的角度来讲】,这个defineComponent函数,传入什么对象,就会返回什么对象,毫无意义
  • 【从ts的角度来讲】,借助defineComponent函数,做了很多的类型限制和类型推导,能让对象内的代码更加严谨

Vue3如果使用Ts,导出时候就要用defineComponent,这俩是配对的,为了类型的审查准确

image.png

实践是检验真理的唯一标准,我发现在使用setup函数的同时没有使用defineComponent,直接导出也不会报错)像这样:

export default {
  setup () {
    return {}
  }
}

问题:defineComponent这个API用起来和不用有什么区别呢???

  1. 这个API一般是在ts或tsx文件中使用的(index.ts、index.tsx),所以,当我们创建了一个这种类型的文件后,它是不知道我们要去写vue实例代码的,所以在这种情况下,我们需要defineComponent来帮我们做内部的一些options的提示。我们来看一个使用了defineComponent和没有使用defineComponent的例子:
image.png image.png image.png
  1. 给予正确的参数类型判断

拿setup函数来说,defineComponent可以为setup函数的props传参做出正确的类型判断

image.png image.png

本文主导 script + setup,抛弃‘通过defineComponent’定义组件在里面编写setup函数再return的方式ps:我并不是一个狠心的男人

说到这里,好像忘了提到,defineComponent里的name属性是什么意思?有什么用?

我是这么理解的

A页面(A.vue)
<script>
import { defineComponent } from 'vue'
export default defineComponent({
  name: 'AAA'
})
</script>

B页面
<template>
  // Vue3不再像Vue2(写个div,内容写在div里)只有一个根节点
  <A />
</template>
<script>
import { defineComponent } from 'vue'
import AAA from './A.vue'
export default defineComponent({
  name: 'BBB',
  components: {
    AAA
  }
})
</script>

name就是组件的名字

defineComponent没有定义name属性

问题来了,如果defineComponent里没有定义name属性呢?怎么搞???

直接安排上,上代码!

---- MyLove.vue ----
<template>
  <div> Where is my love ??? </div>
</template>

<script>
  import { defineComponent } from 'vue'
  
  export default defineComponent ({
    setup () {
      return {}
    }
  })
</script>

---- MyBaby.vue ----
<template>
   <MyLove />
   <xxx />
</template>

<script>
  import MyLove from './MyLove.vue'
  // import xxx from './MyLove.vue'
  import { defineComponent } from 'vue'
  
  export default defineComponent ({
    components: {
      MyLove,
      // xxx
    }
  })
</script>

  这个时候,MyBaby页面就会显示‘where is my love ???’, 
意思是: 我的爱情在哪里 ???

  -> 【已经测试过了】,MyLove组件并没有定义name属性,
我在MyBaby页面引入MyLove组件时,引入的名字是文件名
或任意名时,启动项目就都可以看到效果

  > 由此可以得出结论:name跟引入的名称没有任何关系,
我们平时中保持name和文件名一致,仅仅是为了可读性高
而已,【它的本质就是引入一个export default的模块,
名称引入者随意取,如同import vue from 'vue',也可
以是import anyone from 'vue',这两者都是一样的,
只是规范上不建议这么使用罢了

2.jpg

defineComponent定义name属性与不定义name属性有什么区别?

对keep-alive的理解

再来深究一下,定义组件名称,也就是name属性,写与不写,它的影响在哪里???

这个时候就要提一提keep-alive了
  >1.keep-alive是什么?在开发Vue项目时,大部分的组件是没有
必要进行多次渲染的,所以,Vue提供了一个内置组件keep-alive
来缓存这些组件(组件的内部状态),避免重复渲染。

  2.我们最常见的就是这个
<keep-alive>
  <router-view></router-view>
</keep-alive>

<router-view></router-view>是什么?干什么用的呢?
  >安排!router-view是渲染路由指定的组件的,什么意思?
就是说,如果有个路由,它的地址是/index,当我在地址栏
输入/index时,会出现/index对应的页面(组件),而这个
组件就会显示在router-view这个地方

  -> 这一段代码的意思就是,将所有匹配到的路由组件都
缓存起来【被keep-alive包裹着】,包括路由组件里面的
组件【如果/index对应的组件内还有子组件,那就一起
显示在router-view对应的位置上】

  有人就会问了,不是所有的页面都需要缓存把,可以选择
性的进行缓存吗???
  》可以的,代码:
<keep-alive>
  <component :is="switchComponent"></component>
</keep-alive>
  这个switchComponent可以是具体的组件名,也可以声明
在计算属性,最后返回一个组件名,:is="组件名",这个的
意思就是到底显示哪一个组件。被keep-alive包裹,意味着
要缓存哪一个组件~

也可以这么判断
<keep-alive>
  <component-a v-if="a === 1"> </component-a>
  <component-b v-else-if="a === 2"></component-b>
  <component-c v-else></component-c>
</keep-alive>
-> a = 1 缓存 component-a 组件,
   a = 2 缓存 component-b 组件,
   a = 其他 缓存 conponent-c 组件

------------------------------------------------
  官网上:在Vue2.1.0之前,如果你想要缓存你想要缓存的
路由,你可以这么干:
------------------------------------------------
<keep-alive>
  <router-view v-if="$route.meta.keepAlive">
      // 当页面打开,如果该页面(组件)是被缓存的,
就会替代router-view显示在这个位置
      // 怎么看该路由是不是被缓存了,就看路由里的
meta属性下的keepAlive是不是为true,为true,就是
被缓存了
  </router-view>
</keep-alive>

<router-view v-if="!$route.meta.keepAlive" />

// router 配置
new Router({
  routes: [
    {
      path: '/whereis',
      name: '在哪里',
      component: 'WhereIs',
      meta: {
        keepAlive: true // 需要被缓存
      }
    },
    {
      path: '/mylove',
      name: '我的爱情',
      component: 'MyLove',
      meta: {
        keepAlive: false // 不需要被缓存
      }
    }
  ]
})

---------------------------------------------------------
Vue2.1.0版本之后,Vue新增了两个属性-来配合keep-alive有条件的
缓存 路由/组件
----------------------------------------------------------
  上面我们是使用路由元信息的方式,缺点是:我们要多创建一个
router-view标签,并且每个路由都要配置一个元信息,这么做
确实是可以实现我们想要的效果,但是过于繁琐了点。

·新增属性:
   ·include:匹配到的 路由/组件 会被缓存
   ·exclude: 匹配到的 路由/组件 不会被缓存
   ·max:类型为数字,意思是:最多缓存多少组件实例;如果说最多
缓存10个,这时候第十一个需要缓存的组件来了,就默认销毁最长时间
没被使用到的组件

  include 和 exclude 支持三种方式来有条件的缓存路由:1.采用
逗号分隔的字符串形式、23必须用v-bind形式来使用,分别是正则
和数组的形式
-----defineComponent里加name和不加name的区别要出来了-----
上代码! 三种方式:
提前说明: 
         <1> 使用include/exclude的前提是:要在组件中定义
name属性,不加name属性,include/exclude都不生效
         <2> exclude的优先级比include的高,同时设置 ->
生效的是exclude。
         <3> ps: 如果你第一次进入某个页面<假如说是Tab页>
且这个页面是缓存页,(被keep-alive包裹或include中有声明),
初次进到这个页面,它执行的声明周期函数为created, mounted,
activated(在keep-alive组件被激活时调用),如果说这时在Tab页,
前进/后退到某个页面后,再次进到Tab页,这个Tab缓存页只会执行
activated函数,所以数据的处理需要分两种情况:
 [1]数据是否每次进入页面都要更新 ? 是的话 -> 在activated
里写
 [2]如果数据只需要初始化一次,就在created或mounted写
         
1.
   <keep-alive include="c1, c2">
     <component :is="switchComponent" />
   </keep-alive>
   这么写的意思是:如果c1, c2存在的话,被缓存的只
有c1、c2。而里面的switchComponent(组件(名))是
不会被缓存的(前提是switchComponent不是c1或c2);
用v-bind绑定is的这种切换组件的写法,一般是通过某种
判断去动态的改变switchComponent的值从而达到切换组
件的目的,而不是通过改变路由的方式去切换组件(ps:
通过改变路由改变组件的是router-view)

<扩展:> 如果是这样的:
  <keep-alive>
     <component :is="switchComponent" />
  </keep-alive>
  解释:switchComponent最后赋值完成后是哪个组件,
哪个组件就被缓存
  <keep-alive include="xxx">
    <router-view />
  </keep-alive>
  解释:只缓存name为xxx的组件,其他页面都不缓存,
router-view是缓存/非缓存页的入口
2.
  <keep-alive :include="/c1|c2/">
    <component :is="switchComponent" />
  </keep-alive>
3.
  <keep-alive :include="['c1', 'c2']">
    <component :is="switchComponent" />
  </keep-alive>

图解: image.png

image.png

下面来浅浅的介绍下Vue3使用Keep-alive实现组件缓存(Vue3的Composition API快要来了~)

新版官方推荐这么使用:
  <router-view v-slot="{ Component }">
    <keep-alive :include="includeList">
      <Component v-if="$route.meta.KeepAlive"
                 :is="Component" 
                 :key="$route.name" 
      />
    </keep-alive>
    <Component v-if="!$route.meta.KeepAlive" 
               :is="Component" 
               :key="$route.name" 
    />
  </router-view>
  注意:上述代码中有:key属性,通常多个页面会使用
到KeepAlive属性,为了保持唯一性,规定用key值来表
示和组件的对应关系。如果是多个页面,不加key属性是
会报错的。

> 被keep-alive包裹的组件,会多出两个生命周期函数
1. Vue2 - activated, Vue3 - onActivated,被包
裹组件被激活时触发
2. Vue2 - deactivated, Vue3 - onDeactivated,
从被包裹组件离开时触发(比如:A组件,切换到B组件,
A组件消失时执行)

Vue3生命周期

讲到这,就顺便来讲下Vue3的生命周期函数叭!

除了beforeCreate和created(它们被setup方法本身所取代),剩下的9个钩子函数我们都可以在setup函数中去访问/使用

  1. beforeCreate -> 被setup所取代
  2. created -> 被setup所取代
  3. beforeMount -> onBeforeMount 在组件(template模板)开始挂载到节点之前被调用:相关的render函数首次被调用
  4. mounted -> onMounted 组件挂载完成之后执行
  5. beforeUpdate -> onBeforeUpdate 组件数据更新前调用
  6. updated -> onUpdated 组件数据更新完成后调用
  7. beforeDestroy -> onBeforeUnmount 在卸载组件实例之前调用。在这个阶段,实例仍然是完全正常的。
  8. destroyed -> onUnmounted 卸载组件实例后被调用。调用了这个钩子函数,意味着组件实例里的所有指令全都被解除绑定,所有侦听器都被移除,所有子组件实例都被卸载。
  9. errorCaptured -> onErrorCaptured 当捕获一个来自子孙组件的错误信息时被调用。这个钩子函数接收三个参数:错误对象,发生错误的组件实例,包含错误来源信息的字符串。如果你想阻止这个错误继续向上传播,可以在这个钩子函数中return false。 补两个(上面写过,整整齐齐的再写一哈):
  10. onActivated 被keep-alive缓存的组件激活时调用
  11. onDeactivated 被keep-alive缓存的组件停用时调用

众望所归,Vue3 Composition API 来了

ref

  1. ref 是什么?先来上一个关于ref的代码
  // 首先通过ref声明的值是响应式的【当然也有破坏响应式
的办法】

  // ref 推荐定义基本数据类型,虽然ref的值也可以是对象,
但是并不推荐这么使用

  // 基本数据类型:undefined、null、Boolean、Number、
String

  // 我的个人猜测,如果定义一个ref的值为对象
const obj = ref({ love: 'iswhere' }) 

  // 取值就得这么取:obj.value.love 才能取到 'isWhere' 
太麻烦,还是reactive适合存对象(马上介绍)

  // const obj = reactive({ whereIs: 'mylove' }) 
取值:obj.love  ---> 方便,巴适得很

// 听听就好,纯属扯皮

<template>
   {{ name }}
</template>

<script setup>
  import { ref } from 'vue'
  const name = ref('张三')
  const getName = () => {
     console.log('name', name.value) // 张三
  }
  // 这就是script+setup语法糖的好处,不需要return出去,
导入进来的直接用
</script>

ref()实际就是把传入的值或者引用对象统一转换成Proxy对象。Proxy用于监听对象的变动,因为Proxy只支持引用对象,所以对于值对象,就会被转换成 { value: 值 } 再转换成Proxy对象,此时就可以监听到value值的变化,这也就解释了为什么用ref声明的值在取值时‘需要加上.value’

补充:目前Proxy的监听是单层的,new Proxy(obj)只会把obj转成Proxy对象,obj内部的引用对象并不会转换,所以obj.a的变动可以监听的到,但是obj.a.b的变动就无法监听到了。如果想要监听全部变动,只需要递归把对象内的引用对象全部转成Proxy对象就可以了

reactive

  1. reactive是什么?同样先来段小代码
// reactive与ref的用法相似,也是将数据变成响应式数据。不同的
是ref用于基本数据类型,而reactive是用于复杂数据类型
// 如对象和数组

// 需要注意的是,基本数据类型的数据传给reactive, reactive并
不会将它包装成proxy对象,(数据改变->页面没有/不会发生变化)

// reactive 接收的参数必须是 数组 或 对象

<template>
   {{ reactiveObj.myLove }}
</template>

<script setup>
  import { reactive } from 'vue'
  const myLove = {
    isComing: '???'
  }
  const reactiveObj = reactive(obj)
  const getObj = () => {
     // reactiveObj ???
     console.log('reactiveObj', reactiveObj.isComing)
  }
</script>

toRef、ref与toRef的区别

  1. toRef是什么?代码gogogo
  // toRef是将某个对象里的某个值转化为响应式数据。
  // toRef接收两个参数,第一个参数为对象本身,第二个参数为
对象中的属性名

<script setup>
    // 1. 导入toRef (Vue3都是按需导入,减小包的体积,不像
  Vue2,在main.js全局引入,啥都能用)
  import { toRef } from 'vue'
  
  ----注意:此时定义的是普通对象而非响应式对象(后面有点
  反转的样子)----
  const obj = { count: 1 }
  // 2. 将obj对象里的count属性的值转化为响应式数据
  const state = toRef(obj, 'count')
  // 3. 现在就可以将toRef包装过的数据对象提供给template
使用了!
</script>

  问题来了?表面上看toRef这个API好像没什么用,因为这个
功能貌似也可以用ref来实现(ref也是将数据变成响应式的嘛)
话不多说,看代码:

<script setup>
// 1. 导入ref
import { ref } from 'vue'

const obj = { count: 1 }
// 2. 将obj对象中count属性的值转化为响应式数据
const state = ref(obj.state)
</script>

猛的一看,好像还真没有什么区别。其实这两者(ref和toRef)
是有区别的,再看段代码比较一下!
看一段伪代码哈~~~

<template>
  <div>{{ state1 }}</div>
  <button @click="add1">ADD1</button>
  
  <div>{{ state2 }}</div>
  <button @click="add2">ADD2</button>
</template>

<script setup>
  import { ref, toRef } from 'vue'
   ---注意:此时定义的是普通对象而非响应式对象(后面
 有点反转的样子)---
  const obj = { count: 1 }
  const state1 = ref(obj.count)
  const state2 = toRef(obj, 'count')
  
  const add1 = () => {
    state1.value ++
    console.log('原始值1', obj)
    console.log('响应式数据对象1', state1)
  }
  
  const add2 = () => {
    state2.value ++
    console.log('原始值2', obj)
    console.log('响应式数据对象2', state2)
  }
</script>


  我们分别用ref和toRef将obj中的count转化为响应式,
并声明两个方法分别使count的值增加,每次增加后打印
一下原始值obj和被包装过的响应式数据对象,同时还要
看看视图的变化
1. 先来分析下通过ref将数据转化为响应式数据的情况:
     当对响应式数据的值进行+1操作后,视图改变了,
原始值(obj.count)并没有改变。响应式数据对象的值
改变了,这就说明了ref是对原数据的一个‘拷贝’,不会
影响到原始值,同时响应式数据对象值改变后会同步更新
视图!
2. 再来分析下toRef将数据转化为响应式数据的情况:
     在对响应式数据的值进行+1操作后,‘视图没有发
生改变’,且原始值变了,响应式数据对象的值也变了,
这说明toRef是对原数据的一个引用,会影响到原始值,
(重要的是:响应式数据对象值改变后不会更新视图!)

微微总结:ref和toRef的区别

ref本质是拷贝,修改响应式数据不会影响原始数据;toRef的本质是引用关系,修改响应式数据会影响原始数据。 ref数据发生改变,页面会自动更新;toRef-当数据发生改变时,界面不会自动更新(这个的前提是toRef的第一个对象参数是普通对象而不是响应式对象)。

(实践是检验真理的唯一标准,控制台是大哥,来试一下,揭下神秘的面纱

接下来出场的依然是toRef,这次附带个赠品 -> toRefs。上代码了,老板们~

toRef的深入理解

首先登场的是‘toRef’(要继续讲的是,如果我们传的是响应式对象呢?还会是上面的那种改变数据视图不发生
更新的情况吗?
>让我们带着疑惑的眼神看着这个花里胡哨的'toRef'

<template>
   <div>
     // 两个显示的都是123,5秒后依然是123
     <p>{{ obj1.count }} --- {{ toRef1 }}</p>
     // 都先显示1234,5秒后视图更新,一起变成778899
     <p>{{ obj2.count }} --- {{ toRef2 }}</p>
     // 从头到尾都是显示: 12345 --- 667788
     <p>{{ obj3.count }} --- {{ toRef3 }}</p>
   </div>
</template>

<script setup>
  import { toRef } from 'vue'
  // 1. 先定义一个普通对象
  const obj1 = { count: 123 }
  // 2. 为了测试,再定义一个响应式对象
  const obj2 = reactive({ count: 1234 })
  // 3. ref定义一个对象呢?只是不推荐,没说不行~
  const obj3 = ref({ count: 12345 })
  
  // <1> 先尝试第一个
     const toRef1 = toRef(obj1, 'count')
     // 很多人觉得这是5秒后就立马执行setTimeout里的
函数,其实不是的,这段代码的意思是:到了5秒后就具备了
执行这个函数的条件,但不一定就是5秒后执行【这方面可以
看看事件队列(EventLoop)相关的】
     setTimeout(() => {
       toRef1.value = 6666
       // 5s后 打印 666
       console.log('obj1.count:', obj1.count)
       // 5s后 打印 666
       console.log('toRef1.value:', toRef1.value)
     }, 5000)
     setTimeout(() => {
       obj1.count = 88888888
       // 8s后 打印 88888888 【这个打印没截图,已验证】
       console.log('obj1.count:', obj1.count)
       // 8s后 打印 88888888
       console.log('toRef1.value:', toRef1.value)
    }, 8000)
       现象:【后续会以图片形式截出打印结果】,从打印结果
来看,我们可以得出,当toRef的第一个参数传入的是普通对象时,
5秒后改变toRef1的值,它的原始对象里的count值和响应式的值
都发生了改变,变成了666。但是视图没有发生更新,依然是123。
8秒后,改变原始值,视图依然没改变。
   
  // <2> 尝试第二个
     const toRef2 = toRef(obj2, 'count')
     // 1234
     console.log('toRef2.value变化前', toRef2.value)
     setTimeout(() => {
       toRef2.value = 778899
       // 5s后 这个值 变成 778899
       console.log('obj2.count:', obj2.count)
       // 5s后 打印 778899
       console.log('toRef2.value:', toRef2.value)
     }, 5000)
     setTimeout(() => {
     // 初始展示 1234 -> 5秒后:778899 -> 7秒后:
1314520  【两个值obj2.count = 1314520展示都一样】
     }, 7000)
      现象:当我们传给toRef的第一个参数是reactive
对象时,很惊讶的发现,视图发生了改变。(当toRef的
第一个参数是普通对象或响应式对象时,两者都改变了原
始值,区别是:当toRef传入的是响应式对象时,改变它
的值视图会发生更新)
    
  // <3> 尝试第三个
     const toRef3 = toRef(obj3, 'count')
     toRef3.value = 667788
     console.log('先改变下值:', toRef3.value) // 667788
     setTimeout(() => {
       toRef3.value = 'bu hao se 难道 how are you 吗'
       // 3s后打印 'bu hao se 难道 how are you 吗'
       console.log('toRef3.value:', toRef3.value)
       // 3s 后打印出 12345(并未影响到原始值)
       console.log('obj3.value:', obj3.value)
     }, 3000)
      现象:当还没有执行到toRef3.value = 667788的时候,
toRef3.value的值是12345;当执行到toRef3.value = 667788
的时候, toRef3.value = 667788,因为是同步的,此时页面显示
的是667788,3秒后,改变响应式toRef3的值,发现toRef3的值
改变了,但原始值obj3.value并没有改变,且视图都没有更新,
都是 12345 --- 667788
</script>
  1. 截图1: image.png

  2. 截图2:

image.png

image.png

  1. 截图3:

image.png

对toRef的再次总结:

toRef关键源码:
  // 可以把一个对象的某个属性转化成ref类型
  export function toRef(target, key) {
    return new ObjectRefImpl(target, key)
  }
  class ObjectRefImpl {
    public __v_isRef = true;
    constructor(public target, public key) {}
    get value () {
      return this.target[this.key]
    }
    set value (newValue) {
      this.target[this.key] = newValue
    }
  }
  toRef相当于我们将对象中某一个key对应的值,转化为ref,
就是暴露出一个属性的代理出去,并不会做其他事情,所以这个
属性是否是响应式的,取决于代理的对象target是否是响应式的。

  >toRef相当于是引用数据,这种情况针对toRef第一个参数传的
是普通对象和reactive响应对象。它们俩修改复制过来的值都会
影响原数据,不同的是,第一个参数传的是普通对象的,数据改变,
视图不发生更新;而第一个参数传的是响应式对象的,数据改变,
视图发生更新
  >而toRef的第一个参数若传的是ref定义的响应式对象,情况就
有所不同,【源码解释也说了,这个属性是否是响应式的取决传的
对象是否是响应式的】,ref声明的对象确实是响应的,但是ref具
有一个特性:【ref相当于复制数据,修改复制过来的数据不会影
响原数据,***但是原数据变化时页面会发生变化***】,这也就
解释了第三个toRef案例中,为什么从始至终,template里的值
都没有发生改变

(啰嗦一句,我是一个啰嗦的痴情少男,因为我经常唱-痴心决对)
  > 什么是引用? 看看这个例子瞬间明白(爱情这杯酒谁喝都得醉阿)
  // 声明一个常量a,因为是引用对象,所以要开辟一个内存空间,
用一个栈内存保存这个内存空间的地址,所以这个常量a就是保存着
一个地址,指向这个内存空间const a = { b: 1 }

  // 常量a保存着内存地址,把地址赋值给常量c,常量c跟常量a
拥有相同的地址,指向同一片内存空间const c = a

  // 所以当c可以改变b,改了c,a会变;改了a,c会变c.b = 2
  
  // 这就是引用,a和c是相同的引用关系
  console.log(a.b) // 2

眼泪快要出来了,一个toRef讲这么多,被渣女伤害恐怕还没我伤心

toRefs

  1. toRefs是啥子?比toRef多了个s,是不是有联系?是的,各位老板
老板们,先来段关键性源码看看:
   // object 可能传递的是一个数组或对象
   export function toRefs (object) {
     const ret = isArray(object) ?
                 new Array(object.length) : {}
     
     for (let key in object) {
       ret[key] = toRef(object, key)
     }
     return ret
   }
   好家伙,把对象里的每一个属性都给toRef搞一遍,刺激刺激
   这个和toRef的结论是一致的,toRefs接收一个参数,参数是
对象/响应式对象/数组。参数是普通对象时,也是值都会变但是视
图不更新;而如果传的是响应式对象时,值也是都会变视图也都会更新;
=========================================================
官网上对 toRefs 的解释是这样的:【将’响应式对象’转换为普通对象,
其中结果对象的每个property都是指向原始对象相应property的ref】
  示例:
      const state = reactive({
        foo: 1,
        bar: 2
      })
      
      const stateAsRefs = toRefs(state)
      /*
         stateAsRefs 的类型:
         {
           foo: Ref<number>,
           bar: Ref<number>
         }
      */
      
      // ref 和原始 property 已经“链接”起来了
      state.foo ++
      console.log(stateAsRefs.foo.value) // 2
      
      stateAsRefs.foo.value ++
      console.log(state.foo) // 3
      
========================================================

应用场景:
【Parent-父组件】
<template>
  <Child :citys="beautifulCity" />
</template>

<script setup>
    import Child from './Child.vue'
    import { reactive } from 'vue'
    
    const beautifulCity = reactive({
      city1: '平潭',
      city2: '福州',
    })
</script>Child.vue - 子组件】
<template>
  {{ city1 }} -- {{ city2 }}
</template>

<script setup>
  // 父组件将值传给子组件,子组件用defineProps接收
(后面会讲到~)
  import { defineProps } from 'vue'

  const props = defineProps({
    citys: {
      type: Object, // 传过来的类型要是对象,否则
  控制台警告
      default: () => {}, // 默认为空对象
      required: false  // 不是必须要传的
    }
  })
  // 因为传过来的beautifulCity是响应式对象,如果
对响应式对象进行结构,会失去响应式,toRefs用的最
多的就是解构。
  const { city1, city2 } = toRefs(props.citys)
</script>

shallowReactive

  1. shallowReactive是什么?

相信英语四级没过的朋友们都知道,‘shallow’是‘浅、浅层’的意思。合起来难道shallowReactive就是浅层的reactive吗???那reactive呢?难道是深层的reactive吗??? 没错,你的猜想是对的!(这么聪明的人会没有女朋友?说出来都不敢相信T~T)

小看一段代码:毕竟空口无凭睡服不了大部分兄弟们

  其实reactive和shallowReactive用法都一样,这边以参数为
对象为例:
1. 其实将obj作为参数传递给reactive生成响应式对象时,若
obj的层级不止一层,那么reactive会将每一层都用Proxy包装
一次,我们来验证一下
<script setup>
  import { reactive } from 'vue'
  const obj = {
    a: 1,
    b: {
      c: 3,
      d: {
        e: 5
      }
    }
  }
  
  const state = reactive(obj)
  console.log('state:', state)
  console.log('state.b', state.b)
  console.log('state.b.d', state.b.d)
</script>

一个个都是被包装过的Proxy对象:

image.png

主角出场,shallowReactive 对 reactive 说:Baby,i 母 coming ~

【再来设想一下,如果一个对象的层级很深,如果每一层都用Proxy包装
的话,这对于性能来讲是很不友好的】
【这时候,shallowReactive来了~】
 上代码!(跟上面同样的代码,只是将reactive改成
shallowReactive):
 <script setup>
   import { shallowReactive } from 'vue'
      const obj = {
        a: 1,
        b: {
          c: 3,
          d: {
            e: 5
          }
        }
    }
  
  const state = shallowReactive(obj)
  console.log('state:', state)
  console.log('state.b', state.b)
  console.log('state.b.d', state.b.d)
</script>

image.png

结果非常明了,只有第一层被Proxy处理了,也就是说只有修改第一层的值时,才会响应式更新!你听的没错,这是一个用于优化性能的API!

shallowRef

  1. 都有shallowReactive了,难道会没有shallowRef???

有的

同样!先解释再来段代码:
  >真理:如果是通过shallowRef创建的数据。 那么Vue监听
的是.value 变化。 并不是第一层的数据的变化。因此如果要
更改shallowRef创建的数据。 应该xxx.value={}
  <template>
     <div>
       // 3s后视图更新,更新为:哈喽!!!
       <p>msg : {{ res1.msg }}
       // 3s后视图更新,更新为:333;再过2s后,值发生
  改变,变为:520,但视图没发生更新。
       <p>res1: {{ res1.obj.count }}</p>
       // 5s后值由 666 -> 999,视图发生更新
       <p>res2: {{ res2.obj.count }}</p>
     </div>
  </template>
  
  <script setup>
    import { shallowRef, ref } from 'vue'
    let sum = shallowRef(0)
    // 声明两个变量,区别是:一个是shallowRef, 一个是ref
    let res1 = shallowRef({
      msg: '哈喽',
      obj: {
        count: 666
      }
    })
    let res2 = ref({
      msg: '哈喽',
      obj: {
        count: 666
      }
    })
    setTimeout(() => {
     // 通过.value的方式可以改变obj里count的值
      res1.value = {
        msg: '哈喽!!!',
        obj: {
          count: 333
        }
    }, 3000)
    
    setTimeout(() => {
     // 但是如果通过其他方式,使用shallowRef却改变不了它的值,
  如:
     // 使用shallowRef,只监听.value的变化,可以理解为是ref
  的浅层监听
     // 值发生改变,视图没有更新,换句话说,就是没被监听到
      res1.value.obj = {
        count: 520
      }
     // 为什么这么说,因为通过ref的方式就可以改变count的值,
  这就是区别
      res2.value.obj = {
        count: 999
      }
    }, 5000)
  </script>

难道就没有一种方法可以把shallowRef监听不到的地方给监听到,让视图也更新??

triggerRef

  1. 于是,triggerRef 诞生了
依然是上代码,个人理解:这个API比较适合shallowRef。
参数:接收一个对象
作用:将ref生成的数据强制在页面更新ui

<template>
  <p>count: {{ res1.obj.count }}</p>
</template>

<script setup>
let res1 = shallowRef({
  msg: '哈喽',
  obj: {
    count: 666
  }
})

setTimeout(() => {
  // 因为shallowRef是浅层监听,只监听.value的,所以这么做,
只是值发生改变,页面却是没有更新
  res1.value.obj = {
    count: 999
  }
  // 为了解决这一现象,使用triggerRef就可以解决
  // 此时视图发生更新,有点深度监听的意思了~
  triggerRef(res1)
}, 3000)
</script>

toRaw

  1. toRaw是干嘛用的?
  作用:toRaw方法是用于获取ref或reactive对象的原始数据的
  场景:目前我碰到的场景是:由reactive声明的响应式对象或
数组,它是Proxy// ->  typeof obj  ->  object
const obj = reactive([1,2])
接口传参是如果需要原对象/数组,就拿toRaw转一下。

<template>
  <p>{{ state.name }}</p>
  <p>{{ state.age }}</p>
</template>

<script setup>
import { reactive, toRaw } from 'vue'

const obj = {
  name: 'lwy',
  age: 23
}

const arr = reactive([1, 2, 3])

const state = reactive(obj)
console.log('state1:', state)
console.log('arr:', arr)

setTimeout(() => {
  console.log('toRaw(state):', toRaw(state))
  console.log('toRaw(arr):', toRaw(arr))
  obj.age = 33
  console.log('state2:', state)
}, 2000)

</script>

可以对照着打印结果看下:

image.png

markRaw

  1. markRaw?
  1.markRaw方法可以将‘原始数据’,记住是‘原始数据’标记为非响应
式的,即使之后你用ref或reactive,依然无法实现数据响应,markRaw
接收一个参数,就是‘原始数据’,并返回被标记后的数据

  2.需要注意的是:传入markRaw的对象一定是普通对象才能使它失去
响应式;如果传入markRaw的对象是被reactive或ref包裹着的。那么,
即使用了markRaw还是具有响应式的,【别问我为什么知道,第一,
好男人啥都知道,第二,我试过hh~】

  3.还需要注意的是,如果是用ref声明的对象(为obj),传给
markRaw的时候, 需要加个.valuemarkRaw(obj.value)】
<template>
    <p>{{ state.name }}</p>
    <p>{{ state.age }}</p>
</template>

<script setup>
import {reactive, markRaw} from 'vue'
    const obj = {
        name: '想回家种地的少男',
        age: 23
    }
    // 通过markRaw标记原始数据obj, 使其数据更新不再
被追踪
    const raw = markRaw(obj)   
    // 试图用reactive包装raw, 使其变成响应式数据
    const state = reactive(raw) 
    
    setTimeout(() => {
      state.age = 90
      console.log(state);
    }, 3000)
</script>

watch

  1. 在开发中,常用的API还有watch,这不得来介绍一下???

Vue3新增了个监听的方法 -> watchEffect, watch 和 watchEffect 都是用来监听某项数据的变化从而执行指定的操作的,两者就是用法有点区别 watch: watch(source, cs, [options])

参数说明:

  • source:可以是表达式或者函数,用于指定监听的依赖对象
  • cb:依赖对象变化后执行的回调函数
  • options:可选参数,可以配置的属性有:1.immediate(立即执行-触发回调函数)2.deep(深度监听)
第一种情况:
   当监听ref类型时(伪代码):
   <script setup>
     import { ref, watch } from 'vue'
     const state = ref(0)
     watch(state, (newVal, oldVal) => {
       console.log('新值', newVal)
       console.log('改变前的值', oldVal)
     })
   </script>
   
第二种情况:
   当监听reactive类型时(伪代码):
   <script setup>
     import { reactive, watch } from 'vue'
     const state = reactive({
       love: '在哪里'
     })
     watch(() => state.love, (newVal, oldVal) => {
       console.log('新值', newVal)
       console.log('改变前的值', oldVal)
     })
   </script>
   
第三种情况:
   当监听多个值时(伪代码):
   <script setup>
     import { reactive, watch } from 'vue'
     const state = reactive({
       love: '在哪里',
       baby: 'where is my baby?'
     })
     watch([
             () => state.love, 
             () => state.baby
           ], 
       ([newLove, oldLove], 
       [newBaby, oldBaby]) => {
          console.log('newLove', newLove)
          console.log('oldLove', oldLove)
          console.log('newBaby', newBaby)
          console.log('oldBaby', oldBaby)
     })
   </script>
   
第四种情况:
   当需要立即执行回调函数和深度监听时(伪代码):
   watch(() => xxx, (newVal, oldValue) => {}, {
      // 可配置,默认值为false
      immediate: true, // 是否在初始化页面时就执行回调函数
 (执行第二个参数)
      deep: true // 多用于要监听的数据不在数据第一层时
   })
   
  补充:watch方法会返回一个stop方法,若要停止监听,可以
执行stop函数

watchEffect

  1. watchEffect怎么用?说实话我还没get到它好用的地方
先上段代码!:
   <script setup>
     import { ref, reactive, watchEffect } from 'vue'
     const num = ref(0)
     const obj = reactive({
       love: 'isComing?'
     })
     
     setInterval(() => {
       num.value ++
       obj.love += '?'
     }, 2000)
     /*
         watchEffect发生的现象:
           1.首先先打印一次num和love,之后每隔一秒打印一次。
到第二秒打印完后停止打印【因为执行stop()】
           2.从代码看出,watchEffect并没有像watch一样需要先
传入依赖【而是自动收集依赖,看内部用了什么响应式变量就监听它】
           3.在组件初始化时,会先执行一次来收集依赖,之后当
收集到的依赖中的数据发生变化时,就会再次执行回调函数
           4.watchEffect无法获取到变化前的值,只能取变化后
的值
     */
     const stop = watchEffect(() => {
       console.log('num.value:', num.value)
       console.log('obj.love:', obj.love)
     })
     
     setTimeout(() => {
       stop()
     }, 2000)
   </script>

computed 和 watchEffect的区别

看到这,有人就有疑问了,computed 和 watchEffect 都会自动收集依赖,并且在值更新的时候会触发回调,会初始化一次。那么,它们的区别在哪里?

  • computed 和 watchEffect触发初始化的时机是不一样的。如:如果computed的值没有被使用,是不会触发回调的,只有在该值被使用的时候才会触发回调。但是watchEffect是在setup的时候就会被初始化执行一次。我们可以根据不同的业务场景选择合适的处理方法~

computed

  1. computed 又称计算属性,怎么用?相信Vue2学过的人对这会很熟悉
既然都这么熟悉了,那就浅浅的说下把~
相信看完这段代码的人都理解了,嘿嘿!
<template>
  // where is my love, baby~~~
  <div>where is my {{ word }}</div>
</template>

<script setup>
  import { computed } from 'vue'
  
  const word = computed(() => {
    return 'love, baby~~~'
  })
</script>

Vue3组件通信

defineProps

  1. defineProps【主要是接收父组件传给子组件的参数,并对参数进行接收的配置】
先来段代码看看:
  父组件 Parent.vue
  <template>
    <Chlid :msg="msg1" :obj="obj1" />
  </template>
  
  <script setup>
    import Child from './Child.vue'
    import { ref, reactive } from 'vue'
    const msg1 = ref('这个就是爱情吗?')
    const obj1 = reactive({
      fruilt: 'apple'
    })
  </script>
  
  子组件 Child.vue
  <script setup>
    import { defineProps } from 'vue'
    const props = defineProps({
      // 在这里接收、定义 父组件传来的值
      msg: {
        // 配置传过来的msg的值,是什么类型的、默认值是什么、
    是否必须传这个msg参数
        type: String,
        default: '亲爱的,这不是爱情',
        required: true
      },
      // 只有一个参数,默认是规定类型
      obj: Array 
    })
  </script>

defineEmits

  1. defineEmits【子组件要传什么值给父组件,可以用这个】
同样的是,来看段代码理解理解:
  父组件 Parent.vue
  <template>
    // 有的人分不清@changeMsg="changeMsg"这种写法,就换下面这种区分开
    <Child @changeMsg="msgChange" />
  </template>
  <script setup>
    import Child from './Child.vue'
    // 第一个参数:receptValue,接收子组件传给父组件的值
    const msgChange = receptValue => {
      console.log('receptValue.msg:', receptValue.msg) // '这是子组件传给父组件的值'
    }
  </script>
  
  子组件 Child.vue
  <script setup>
    import { defineEmits } from 'vue'
    const emits = defineEmits(['changeMsg'])
    setTimeout(() => {
      // 两秒后,触发父组件里定义子组件的changeMsg方法,并把值传给它
      emits('changeMsg', { msg: '这是子组件传给父组件的值' }) // 第二个参数可以是任意值
【传给父组件的】
    }, 2000)
  </script>

defineExpose/ref

  1. defineExpose/ref【子组件暴露出属性或方法供父组件使用】
  依然是看代码来理解【不难看出我是个专一的男人】
    子组件 Child.vue
    <script setup>
      import { defineExpose } from 'vue'
      defineExpose({
        childAttr: '子组件的属性',
        childMethod () {
          console.log('这是子组件暴露给父组件用的方法')
        }
      })
    </script>
    
    父组件 Parent.vue
    <template>
      // 注意:ref="childComponent"
      <Child ref="childComponent" />
    </template>
    
    <script setup>
      import Child from './Child.vue'
      import { ref } from 'vue'
      // 注意:声明的常量要与ref里的保持一致才可以获取到子组件暴露出来的属性和方法
      const childComponent = ref(null)
      
      console.log(childComponent.value.childAttr) // 获取子组件对外暴露的属性
      console.log(childComponent.value.childMethod()) // 调用子组件对外暴露的方法
    </script>

provide/inject

  1. provide/inject
这次来微微的解释一下:
  provide: 我们可以为后代提供指定的数据
  inject: 在任何后代组件中,如果你想要接收provide提供给后代的数据,都可以用inject接收,且这个
后代不管嵌套多深,inject都可以拿的到

  // 父组件 Parent.vue
  <script setup>
    import { provide } from 'vue'
    provide("word", "伤过的心,就像玻璃碎片~")
  </script>
  
  // 子组件 Child.vue
  <script setup>
    import { inject } from 'vue'
    const word = inject('word')
    console.log('word', word) // 伤过的心,就像玻璃碎片~
  </script>

v-model

  1. vue3 -> v-model:可以支持多个数据双向绑定

vue2 -> v-model

父:
v-model 属性只能用一次,若需要向子组件传递多个数据,使用.sync
<Child v-model="age" />
相当于
<Child :value="age" @input:age="age=$event" />

子触发:
this.$emit('input', 值)

vue2 -> .sync

父:
<Child :age.sync = "nianling" :gender.sync = "xb" />
相当于
<Child 
  :age="nianling"
  @update:age="nianling=$event"
/>

子:
props: {
  age: { type: Number },
  gender: { type: String }
}

子触发:
this.$emit('@update:age', 2)

vue3 -> v-model取代.sync

  一、 
  父: <Child v-model:abc="efg" />
  相当于
  <Child :abc="efg" @update:abc="efg=$event" />
  
  子触发:this.$emit('update:abc', 值)
  
  二、
  父:<Child v-model="efg" />
  相当于
  <Child :modelValue="efg" @update:modelValue="efg=$event" />
  
  子触发:this.$emit('update:modelValue', 值)

Pinia(Vue2中的Vuex)

6.最后讲一个也是做Vue3项目肯定要用到的--Pinia(小菠萝)

image.png

跟Vuex很像,但是有些许不同,刚开始学Vue3的人,个人感觉更多的应该是先学会怎么用,在会用了之后再去看一些文章,收获就会不一样。至于为什么要从Vuex转变成小菠萝,一个是小菠萝压缩后的体积只有1.6kb,另一个是去除了mutations,目前只剩下state, getters和actions【支持同步和异步】;支持TypeScript。最重要的一点是公司要用小菠萝~

用法:
  1.定义一个小菠萝页面存数据 - store.js
     import { ref, defineStore } from 'pinia'
     // defineStore 第一个参数是id值,不能重复。就像v-for 里要定义一个key属性一样
     export const userStore = defineStore('userStore', () => {
       const userInfo = ref({
          userName: 'admin',
          password: 123456
       })
       
       function setUserInfo (data) {
         userInfo.value.userName = data.userName
         userInfo.value.password = data.password
       }
       
       return { userInfo, setUserInfo }
     })
   2. 要在哪里使用存在小菠萝的数据就引入
      // 导入地址随便写的 - 但是引入的名字一定要对应【userStore】
      import { userStore } from './store.js'
      
      // 解构完之后,就可以拿到store.js的数据,也可以设置里面的值,如userName和password
      const { userInfo, setUserInfo }  = userStore()
      console.log(userInfo.userName, userInfo.password) // admin 123456