Vue3 学习笔记 —— Vue3 新特性

目录

一.配置环境、创建项目

二.响应式对象

2.1 ref()、computed()、setup()

2.2 reactive()、toRefs()、区分 ref 和 reactive

三.生命周期(Vue2 vs Vue3)

四.侦测函数 - watch

五.模块化开发 - hooks

5.1 useMouseTracker 鼠标追踪器

5.2 useURLLoader 抽离加载函数

5.3 【拓展】为加载函数添加泛型 

六.瞬间移动【模态框】- Teleport

6.1 Modal 组件 编写

6.2 Modal 组件 使用

七.异步请求 - Suspense

八.全局 API 修改


一.配置环境、创建项目

1.配置环境

  • 安装要注意的问题(指我自己的电脑):使用 vue3 时,需要保证 vue cli 版本在 4.5.0 以上

  • 卸载 1.0、2.0 旧版本脚手架:
  1. npm uninstall vue-cli -g
  2. yarn global remove vue-cli
  • 安装/卸载 3.0、4.0 新版脚手架:
  1. npm install/uninstall -g @vue/cli
  2. yarn global add/remove @vue/cli
  • 安装最新发布的脚手架(需要执行两个卸载命令,卸载"旧版"、"新版",安装最新版):
  1. npm install -g @vue/cli@v5.0.0-alpha.3
  2. yarn global add @vue/cli@v5.0.0-alpha.3

  • 注意:因为是 alpha 版本(未发布),所以一定要注意 命令不同;
  • 此时 VueCLI 官网还未更新相关命令,但 github 上已经更新相关内容,如下所示:

Vue3 学习笔记 —— Vue3 新特性_第1张图片

  • 相关网址:
  • https://github.com/vuejs/vue-cli/blob/dev/CHANGELOG.md
  • https://cli.vuejs.org/guide/installation.html

2.创建项目

  • 命令行创建项目之后的一些选择:
  • Vue3 学习笔记 —— Vue3 新特性_第2张图片
  1. Please pick a preset (创建 vue2/3 或者手动决定) - 选择 Manually select features
  2. Check the features needed for your project:创建的项目需要搭配的内容,比如路由、babel、ts、vuex、css预处理器、lint语法规范等
  3. Choose a version of Vue.js that you want to start the project with - 选择 3.x (Preview)
  4. Use class-style component syntax - 输入 n,回车
  5. Use Babel alongside TypeScript - 输入n,回车
  6. Pick a linter / formatter config - 直接回车
  7. Pick additional lint features - 直接回车
  8. Where do you prefer placing config for Babel, ESLint, etc.? - 直接回车
  9. Save this as a preset for future projects? - 输入n,回车

  • 关于 Eslint 插件:
  • 如果 Eslint 不生效,可以在根目录创建 .vscode 文件夹,在里面创建 settings.json 然后输入:
{
  "eslint.validate": [
    "typescript"
  ]
}

二.响应式对象

2.1 ref()、computed()、setup()

  • ref 函数,接受一个参数,返回一个 响应式对象
  • const count = ref(0):将 参数0 传入 ref()函数,使之变成响应式对象

  • const double = computed(() => {
        return count.value * 2
      })
  • computed() 计算属性,接受一个 回调函数
  • return count.value * 2:count 在上面被声明为了响应式对象,需要通过 .value 获取或改变 响应式对象实际值

  • setup() 位于: beforeCreate() 和 Created() 两个钩子函数之间执行
  • 也就是说,在 setup() 执行的时候,Created() 还没有执行,因此 setup() 不存在 data 和 methods,这些内容可以在 setup() 之外进行书写
  • setup() 中的 this 被设为 undefined,因此 setup() 不能使用 this
  • 在 setup() 中定义的变量和方法,需要通过 return 返回出去,供模板使用


import { computed, ref } from "vue"

setup() {
  // 将 参数0 传入 ref()函数,未来可以检测到改变,并作出响应
  const count = ref(0)
  // 计算属性 computed() 接受一个回调函数 () => {}
  const double = computed(() => {
    // 获取响应式对象值的方法:.value
    return count.value * 2
  })
  // setup() 中,不存在 data、method,因此直接通过 回调函数 () => {}声明方法
  const increase = () => {
    count.value++
  }
  // setup() 中的变量和方法需要 return 出去,才能被模板使用
  return {
    count,
    increase,
    double
  }
}

2.2 reactive()、toRefs()、区分 ref 和 reactive

  • reactive({}) 内部,接受普通数据,toRefs() 让 reactive({}) 对象变成响应式的
  • 定义复杂变量,需要在 setup() 之前,声明 interface 规范其 内部各个参数的 数据类型
  • 在 reactive({}) 中使用计算属性,比如下面的例子,直接 data1.xxxx 就行,无需添加 .value,因为 使用 toRefs() 之前, reactive({}) 中的数据并非是响应式的
import { ref, computed, reactive, toRefs } from 'vue'

// 为复杂变量 定义复杂参数类型
interface DataProps {
  count: number;
  double: number;
  increase: () => void;
}

setup() {
  const data1: DataProps  = reactive({
    count: 0,
    // 注意,这里直接改变 data 中变量的值,不需要.value,因为 data1 此时并非响应式
    double: computed(() => data1.count * 2)
    increase: () => { data1.count++},
  })

  // toRefs() 为普通数据注入响应式能力
  const refData = toRefs(data1)

  return {
    ...refData
    // 也可以直接写成 ...toRefs(data1)
  }
}

  • 使用 ref() 还是 reactive() 的场景:
  1. ref() 适合简单变量,取值改值需要通过 .value
  2. reactive() 适合复杂变量(对象、数组等),取值改值无需 .value,而是直接改(state.xxx = xx),要记得使用 toRefs() 保证 reactive 对象属性保持响应性
  3. 一个文件中,reactive 和 ref 选一种,toRef 和 toRefs 选一种,最好二选一,尽量不要混着用(混着用也没啥问题)

  • 个人推荐 reactive(),把数据挂载到 state 变量里,统一管理;return 用 toRefs() 解构出去,写法简洁;
  • 其实 React 或 小程序 都是有 state数据管理 这样的概念的
setup() {
    const state = reactive({
        firstIcon: require("@/assets/images/icons.png").default,
        cardData: [{
            id: 1,
            name: '啦啦啦',
        }];
        cardActive: '啦啦啦'
    });

    return {
        ...toRefs(state),
    }
}

三.生命周期(Vue2 vs Vue3)

  • 在 setup() 中使用的 hook 名称 和 原生命周期 的关系:
  • beforeCreate -> 不需要
  • created -> 不需要
  • 上面这俩其实被替换成 setup() 了,setup() 在他俩之间进行

  • beforeMount -> onBeforeMount
  • mounted -> onMounted(挂载)
  • beforeUpdate -> onBeforeUpdate
  • updated -> onUpdated(更新)
  • beforeUnmount -> onBeforeUnmount
  • unmounted -> onUnmounted(卸载)
  • errorCaptured -> onErrorCaptured(捕获错误)

  • renderTracked -> onRenderTracked(?)
  • renderTriggered -> onRenderTriggered(?)

四.侦测函数 - watch

  • watch(),接受两个参数:要监听的对象、接受新旧值的回调函数
  • watch(data, (newValue, oldValue) => { }) 
  • 此处 data 是 响应式对象(props.data、reactive、ref 等等)
  • 注意:此处的 data 是完整的响应式对象,就是说不能只是 reactive 里面的某一项

  • 要监听的对象 可以是 数组,里面包含多个要监听的值 
  • watch([greetings, data], (newValue, oldValue) => {})

  • 使用 getter 写法:() => data.count,可以侦测 reactive 中的某一项
  • watch([greetings, () => data.count], (newValue, oldValue) => {})
  • 此处 data.count,是 reactive 中的某一项,这么做是保证 reactive 被 toRefs() 后,再获取需要的部分,这样获取的就是响应式对象了
// watch 简单应用 
// 此处 data 是完整的响应式对象(比如整个reactive对象,整个ref)
watch(data, () => {
  document.title = 'updated ' + data.count
})

// watch 的两个参数,代表新的值和旧的值
watch(refData.count, (newValue, oldValue) => {
  console.log('old', oldValue)
  console.log('new', newValue)
  document.title = 'updated ' + data.count
})

// watch 多个值,返回的也是多个值的数组
watch([greetings, data], (newValue, oldValue) => {
  document.title = 'updated' + greetings.value + data.count
})

// 使用 getter 的写法 watch reactive 对象中的一项
watch([greetings, () => data.count], (newValue, oldValue) => {
  document.title = 'updated' + greetings.value + data.count
})

五.模块化开发 - hooks

5.1 useMouseTracker 鼠标追踪器

  • 先尝试一下,在组件内添加对应的逻辑
  • 声明两个响应式对象:
  • const x = ref(0) / const y = ref(0)
  • 声明一个鼠标事件,为之前的响应式对象进行赋值:
  • const updateMouse = (e: MouseEvent) => {
      x.value = e.pageX
      y.value = e.pageY
    }
  • 挂载的时候,为整个文档添加点击事件监听,调用鼠标事件:
  • onMounted(() => {
      document.addEventListener('click', updateMouse)
    })
  • 卸载的时候,为整个文档移除点击事件监听:
  • onUnmounted(() => {
      document.removeEventListener('click', updateMouse)
    })

  • 这段逻辑代码 跟 组件本身 其实没有任何关系,可以把它抽成一个方法 
  • 该方法会被写入 xxx.ts 文件中,放在 hooks 目录下
  • hooks目录 推荐放一些公共逻辑代码,导出方法供给组件使用
  • xxx.ts 文件中,是可以导入 vue 各种方法 / 生命周期 的
function useMouseTracker() {
  const x = ref(0)
  const y = ref(0)
  const updatePosition = (event: MouseEvent) => {
    x.value = event.clientX
    y.value = event.clientY 
  }
  onMounted(() => {
    document.addEventListener('click', updatePosition)
  })
  onUnmounted(() => {
    document.removeEventListener('click', updatePosition)
  })
  return { x, y }
}

export default useMouseTracker

  • vue3 这种实现方式的优点
  1. 可以清楚的知道 x、y 的来源、作用:他们来自 useMouseTracker 的返回,用来追踪鼠标位置
  2. 不同文件引入,避免了命名冲突
  3. 这段逻辑可以脱离组件存在,因为它和组件的实现没有关系
  • 建议:存放公共方法的文件最好以 usexxx.ts 这种格式命名

5.2 useURLLoader 抽离加载函数

  • 安装 axios:npm install axios --save
import { ref } from 'vue'
import axios from 'axios'

// 添加参数url,用于接受地址
const useURLLoader = (url: string) => { 
  const result = ref(null) // 真实结果
  const loading = ref(true) // 正在加载
  const loaded = ref(false) // 加载完成
  const error = ref(null) // 错误信息

// 发送异步请求,获取结果
  axios.get(url).then((rawData) => {
    loading.value = false // 获取到结果之后,更改 正在加载 的值为 false
    loaded.value = true // 更改 加载完成 的值为 true
    result.value = rawData.data // 更改 结果 的值为 rawData.data
  }).catch((e) => {
    error.value = e // 更改 错误 的值为 e
  })

  return { result, loading, error, loaded }
}

export default useURLLoader // 导出方法
  • 在组件中使用 useURLLoader:
  • 导入公共方法,传入参数,解构内容,把内容 return 给模板使用

Loading...

import useURLLoader from './hooks/useURLLoader' const { result, loading, loaded } = useURLLoader('xxxurl') return { result, loading, loaded, }

5.3 【拓展】为加载函数添加泛型 

  • 由于请求数据存在延迟,请求到数据前,结果为空,请求到数据后,结果不为空
  • 因此最终返回的结果 result 存在两种情况,需要 泛型 为它指定类型

  • ​​​​​​​const result = ref(null)
  • 解释:<> 内包含了两种情况(类型T | 空null),默认值为 null
function useURLLoader(url: string) {
  // 还没请求到的时候结果为 null 类型,请求到了结果为 T 类型
  const result = ref(null)

======= 上面是 hooks 目录下的公共方法 =======
===========================================
======= 下面是组件内调用公共方法      =======

// 定义泛型具体类型
interface CatResult {
  id: string;
  url: string;
  width: number;
}

// 使用方法 并 传入泛型
const { result, loading, loaded } = useURLLoader('xxxurl')

六.瞬间移动【模态框】- Teleport

  • vue2 存在的问题:模态框大多嵌套在很多层级之下,更改了会导致全局混乱

  • ​​​​​​​vue3 新添加了一个默认组件 Teleport
  • 它有一个 to 属性,代表要把这个组件渲染到哪个 dom元素中
  • 该组件最终的渲染,不再嵌套在 根元素之下,而是独立于根元素,和根元素并排

6.1 Modal 组件 编写

  • 通过 to="#modal" 把 Modal 组件 绑定在 #modal 这个 dom元素 上
  • 将会是同一层级
  • 通过 v-if="isOpen" 控制 Modal 是否显示
  • Modal 中,定义插槽,供用户自定义 Modal 内容
  • Modal 中,定义按钮,绑定 @click="buttonClick",Modal 组件通过 context.emit('close-modal') 发送事件,Modal 组件 监听 父组件 是否 触发该方法
  • 注意:setup() 中, context 参数可以获取到 emit,context.emit()




6.2 Modal 组件 使用

  • 父组件触发 Modal组件 发送/监听 的方法 @close-modal="XXX"
  • 父组件可以自定义 Modal 组件展示的内容

My Modal ! import Modal from './components/Modal.vue' const modalIsOpen = ref(false) const openModal = () => { modalIsOpen.value = true } const onModalClose = () => { modalIsOpen.value = false } return { modalIsOpen, openModal, onModalClose, }

七.异步请求 - Suspense

  • vue3 新添加了一个默认组件 Suspense,用于优雅的处理 异步请求
    
      
      
    
  • 当异步请求还未成功时,#fallback 中的内容会显示,用来放 loading 内容
  • 当异步请求成功时,#default 中的内容会显示,用来放 异步请求结果
  • Suspense 中可以添加 多个异步组件

  
  

八.全局 API 修改

  • Vue2 的全局配置:
import Vue from 'vue'
import App from './App.vue'

Vue.config.ignoredElements = [/^app-/]
Vue.use(/* ... */)
Vue.mixin(/* ... */)
Vue.component(/* ... */)
Vue.directive(/* ... */)

Vue.prototype.customProperty = () => {}

new Vue({
  render: h => h(App)
}).$mount('#app')
  • Vue2 这样写在一定程度上,修改了 Vue对象 的 全局状态
  1. 在单元测试中,全局配置易污染全局环境,用户需要在每次 case 之间,保存和恢复配置。有一些 api (vue use vue mixin)甚至没有方法恢复配置
  2. 不同的 APP 中,想共享一份有不同配置的 Vue对象,非常困难

  • ​​​​​​​ Vue3 的全局配置:
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)
// 此时 app 就是 App 的实例之一
// 现在设置任何配置,都是在不同的 app实例上的,就不会冲突

app.config.isCustomElement = tag => tag.startsWith('app-')
app.use(/* ... */)
app.mixin(/* ... */)
app.component(/* ... */)
app.directive(/* ... */)

app.config.globalProperties.customProperty = () => {}

// 配置结束以后,再用 mount() 方法,把 App 挂载到 固定的 DOM节点
app.mount(App, '#app')

你可能感兴趣的