Vue3学习(对比Vue2)

Vue3学习(对比Vue2)

前言

参考文档:vuejs官方中文文档

背景:最近换了新工作,要用Vue3进行开发项目。为了能顺利转正,趁闲余时间抓紧学习下。分享出来也希望能得到大佬们的指正。

什么是选项式API (Options API)和组合式API(Composition API)?

Vue2使用的是Options API,需要将内容固定写在特定位置或方法中。例如,响应式数据需要放在data中,方法需要写在methods中。

xml 复制代码
<script>
export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
    }
  },
}
</script>

Vue3主要使用Composition API,同时兼容Options API。它提供了更自由的使用方式,不再限制内容的位置。例如,响应式数据只需要调用ref方法,方法也不再需要写在methods中。

xml 复制代码
<script setup>
const count = ref(0)
function increment() {
  count.value++
}
</script>

模板语法的差异

1、Vue3支持绑定多个动态的值。比如:

arduino 复制代码
const objectOfAttrs = {
  id: 'container',
  class: 'wrapper'
}
// 使用方式威使用v-bind绑定对象
<div v-bind="objectOfAttrs"></div>

2、支持动态参数。写法如下(实践中有什么意义?)

xml 复制代码
<a v-on:[eventName]="doSomething"> ... </a>
​
<!-- 简写 -->
<a @[eventName]="doSomething">

3、v-modle修改了默认值。

Vue2的v-model是属性value和自定义事件input的语法糖。具体语法如下:

ini 复制代码
<input v-model="searchText" />
<input
  :value="searchText"
  @input="searchText = $event.target.value"
/>

Vue3v-model的默认属性改为modelValue和默认自定义事件改为update:modelValue

ini 复制代码
<CustomInput
  :modelValue="searchText"
  @update:modelValue="newValue => searchText = newValue"
/>

4、v-model支持修饰符,可以操作返回的参数 。例如:vanttabs组件v-model:avtive

ini 复制代码
<van-tabs v-model:active="active"></van-tabs>

什么是Hooks?

Hooks一词的英文含义是"钩子",在编程中是指用钩子将外部特性勾住。在Vue3中,比如ref的功能就是返回一个响应式的数据并将参数设为默认值。简单来说,Hooks就是一种加工数据的方法。

为什么要用Hooks?或者说Hooks对比之前的编码方式都有哪些优点和缺点?

  1. 相比之前的编码方式,使用Hooks可以使组件自定义常用方法更加便捷。在React 18之前,通常使用组件继承的方式编写常用自定义方法,而Vue2则使用mixin方式合并自定义方法,原生JavaScript中则常常将功能抽象为工具类的方法。
  2. 使用Hooks可以使代码更加整洁。通过将功能相同的代码集中在一块,我们可以更清晰地了解组件的逻辑和状态管理。
  3. Hooks采用函数式编程的方式,这使得在使用TypeScript等静态类型检查工具时更有利于代码的管理和类型检查。

而使用Hooks可能带来的一些潜在缺点是:

  1. 学习成本。对于面向对象编程熟悉的开发者,初步掌握函数式编程可能需要一定的时间和精力

setup 的使用方式有哪些?

在原生的JavaScript中,我们要实现独立的组件需要使用函数返回的方式执行代码。为了在引用组件时使数据独立,选择了data函数返回新对象的方式而不是直接使用data对象的方式。所以在不借助外部工具的情况下,setup的使用方式是调用setup方法并在该方法中返回模板中所需的变量。这样的设计与React调用render方法渲染模板类似。示例代码如下:

javascript 复制代码
export default {
  setup() {
    const count = ref(0)
    function increment() {
      // 在 JavaScript 中需要 .value
      count.value++
    }
    return {
      count,
      increment
    }
  }
}

可以看到,在官方模板中,调用Hooks是不需要显式引用的。Vue可以利用闭包原理,在外层定义ref等Hooks方法后,通过调用setup方法实现方法的引用。

由于Vue使用的是单文件组件 (SFC) ,可以对代码进行自定义操作,将部分繁杂的操作隐藏起来。使用方式是在<script>标签中添加setup标记。

xml 复制代码
<script setup>
// import { ref } from 'vue'
​
const count = ref(0)
​
function increment() {
  count.value++
}
</script>
  1. 你可以直接使用定义的数据,Vue3会返回当前函数作用域的闭包。
  2. 不需要引用官方的hook方法也可以使用,你可以理解为在代码块是在setup函数作用域中。

响应式

ref

ref()接收参数,并返回一个包裹了.value属性的ref对象。在JavaScript代码中,我们需要使用.value的形式读取数据,在模板中可以直接使用(Vue3会自动解构ref对象)。

底层原理与Vue2的响应式原理类似,都是通过Object.defineProperty来劫持对象的属性。不同的是,Vue2劫持的是data方法返回的对象中的每个属性,而ref只劫持了单个属性------value

对于对象或数组等深层嵌套数据类型,ref会自动递归劫持该数据。

reative

reative()接收一个普通对象作为参数,并返回一个对象的响应式代理对象。我们可以像使用普通对象一样使用该代理对象。

底层原理是通过Proxy来劫持对象所有属性的访问和修改。由于代理需要与地址关联,所以作为参数的类型必须是对象。在JavaScript中,简单数据类型存储在内存栈中,而复杂数据类型存储在内存堆中。只有存储在内存堆中的数据才具有内存地址,因此只有复杂数据类型才能被成功代理。

在JavaScript中,声明变量时,变量的键值关系(也就是变量名与对应的值)是存在内存栈中的。当我们对变量进行直接赋值或解构赋值时,会改变变量名的引用,从而导致响应式对象的关联关系丢失,进而导致响应式功能失效。比如:

ini 复制代码
let state = reactive({ count: 0 });
state = { count: 0 }; // 这里直接赋值,变量state将指向一个新的内存地址
state.count++; // 页面不响应,因为现在state与原先的响应式对象失去了关联

上面的代码对应的关系变化如下:

变化顺序 变量名 变量值 内存地址
1 state reactive({ count: 0 }) 1
2 state { count:0 } 2
3 state { count:1 } 2

为了确保响应式功能正常运作,正确的做法如下:

  1. 单个属性通过修改对象属性的方式。比如:state.count = 1
  2. 多个属性通过对象的合并方法。比如:Object.assign(count:2,name:'la')
  3. 对于数组、Map、Set等集合类型数据,也应该使用特定的方法来修改内部数据,以确保响应式功能的正常运作。

综上所述,由于reactive()在使用中有一些限制,Vue3官方文档推荐我们在声明响应式数据时,优先使用ref,它能更好地满足大多数的需求,并且使用起来更加方便和直观。

Dom更新时机

如果将数据修改和更新页面放在同一个事件队列中,将增加页面的渲染次数。为了更好的性能,我们的目标是多次修改仅渲染一次。

JavaScript的事件轮询机制是宏任务-微任务-页面渲染,在页面渲染完后会执行钩子函数nextTick来告知一次事件循环结束。

因此,实现方案就显而易见了:在第一次事件循环时执行代码修改虚拟DOM,在nextTick中对比虚拟DOM和真实DOM,并修改不同部分,在第二次事件循环中进行页面渲染。

Vue中也有钩子函数nextTick,表示修改状态后、页面更新前。根据上面的原理,我们知道只需要在页面渲染前回调nextTick方法就可以实现优化渲染。

生命周期

对比Vue2的生命周期,对卸载组件的回调函数名称进行了修改,由destory 改为unMount

在Vue3中加入了Hooks的用法,如果多次调用同一声明周期会有什么效果呢?尝试运行下面的代码:

scss 复制代码
onMounted(() => {
  console.log(4);
  setTimeout(() => {
    console.log(3)
  }, 0)
})
onMounted(() => {
  console.log(2)
})
onMounted(() => {
  console.log(1)
})
// 输出结果 4,2,1,3

从输出结果可以看出,声明多个声明周期会按照从上往下的顺序执行。

如果给你设计,应该如何实现这个功能?

  1. 在当前作用域创建一个先进先出的桶作为容器,保证从上往下的执行顺序。
  2. 在调用方法的位置将函数添加到桶中。
  3. 在需要的地方遍历并调用桶中的方法。

我们可以使用以下代码简单实现上述思路:

scss 复制代码
const onMountedBucket = []
function onMounted (callback) {
    onMounredBucket.push(callback)
}
nextTick(() => {
    onMountedBucket.forEach(callback => callback())
})

这种设计模式解决了什么问题?有什么优点和缺点

解决了将某一类方法集合在某一处执行的需求。它的优点是可以保证在不同的作用域下,代码仍能按照预期顺序执行,而不需要将这些代码写在同一个作用域内。

假设应用到性能优化当中,能否优化性能?比如我们要实现reactsetState方法,将多次修改数据操作集合成一次修改数据操作。然而,这种设计模式也带来了一个缺点,即数据更新不及时的问题。在reactsetState方法中,为了解决这个问题,引入了callback参数,允许开发者在数据更新后通过回调函数获取到更新后的state。我们尝试着实现这个方案:

  1. 初级目标是将多次修改数据操作集合成一次修改数据操作。操作的时机要求是一次代码执行完后,根据我们对事件循环的认识,选择微任务或开启新一轮事件循环的时机。从执行顺序的角度来看,微任务的方案会更加适合,因此我们优先考虑实现微任务的方案。
  2. 实现完初级目标后,我们将尝试加入回调函数来获取更新后的 state

下面就是我的实现方案了:

scss 复制代码
const state = {};
let provStateMap = {};// 为了方便数据合并操作。设计一个类似state的数据结构
​
function setState(provState) {
  Object.assign(provStateMap, provState);
  // 首次执行添加一次合并回调钩子
  if (Object.keys(provStateMap).length === 0) {
    Promise.resolve().then(() => {
      if (Object.keys(provStateMap).length > 0) {
        Object.assign(state, provStateMap);
        provStateMap = {}; // 执行完毕后清空数据,方便下次操作
      }
    });
  }
}
// 初级目标已经实现了,下面实现回调函数获取更新后的state
let callbakBucket = [] // 设计一个桶来装回调函数
function setStateWithCallback(provState,callback){
    setState(provState)
    // 首次执行添加一次合并回调钩子
    if(setStateBucket.length === 0){
        Promise.resolve().then(() => {
            if(callbakBucket.length > 0)
                callbakBucket.forEach(callback => callback())
                callbakBucket = []
        })
    }
}

当我们想要实现新一轮事件循环时,发现只需要将 Promise.resolve().then修改成setTimeout就可以实现了。

计算属性和侦听器

computed

ini 复制代码
const surname = 'li'
const lastName = 'hua'
const getName = computed(() => surName+lastName)

watch

javascript 复制代码
const count = ref(0)
watch(count,
      (newCount) =>{console.log(newCount)},
      {deep:true,immediate: true}
      ) 

watchEffect

自动读取函数中使用到的响应式数据并在数据更新时调用方法。

scss 复制代码
const count = ref(0)
watchEffect(() => {
    console.log(newCount)
})

底层实现中,watchwatchEffect 都是基于响应式原理实现的。其大致思路如下:

  1. 劫持响应式数据的访问,并将回调函数添加到监听队列中。
  2. 当响应式数据发生变化时,触发监听队列中的回调函数。

一种简化的实现思路如下:

ini 复制代码
const Bucket = [];
const addCallback = (callback) => { return callback; }
const reactiveObject = new Proxy({}, {
  get: (target) => {
    Bucket.push(addCallback());
  },
  set: (target) => {
    Bucket.forEach(callback => callback());
  }
});

watch 通过第一个参数找到对应的响应式数据,然后将第二个参数的回调函数添加到监听队列中。

watchEffect 则在初始化时执行一次函数,触发函数内部对响应式数据的访问,然后将回调函数添加到监听队列中。

computed 可以看作是高级版的 watchEffect。通过声明变量,执行函数,并调用相应属性的 get 方法,将回调函数添加到监听队列中。以上述代码为例进行解释:

ini 复制代码
const surname = 'li';
const lastName = 'hua';
const getName = computed(() => surName + lastName);
​
// 等同于
let name = "";
watchEffect(() => {
  name = surname + lastName;
});

computed 将一个计算属性的依赖关系和更新函数封装在一起,当依赖数据发生变化时,它会自动计算新值,并触发更新函数。这样可以更加高效地处理依赖关系和数据更新。

组件通信

props

在 Vue3 中,通过 defineProps() 方法返回组件的 props 对象,其中保存了父组件传递给子组件的属性。

defineProps 接受一个对象参数,用于定义哪些属性可以被读取。官方设计此方法的原因是为了在开发者使用错误的属性时,能够在浏览器中发出警告,并通过对参数的读取实现在模板中不需要写明 props 的效果。

withDefaults 提供了为 props 设置基于类型声明的默认值的能力。

具体使用方法如下:

typescript 复制代码
// 定义允许使用的属性列表,以字符串数组形式传递
const props = defineProps(['foo']);
​
// 定义允许使用的属性以及其数据类型,以对象形式传递
const props2 = defineProps({
  title1: String,
  title2: [String, Number],
  title3: {
      required: true,
      type: String,
      default: '标题'
  },
  title4: {
      type: Object,
      default: () => ({}),
      validator: (value) => { console.log(value) }
  }
});
​
// 使用 TypeScript 的情况
const props = defineProps<{
  title?: string;
  likes?: number;
}>();
​
// 使用 withDefaults 的情况
export interface Props {
  msg?: string;
  labels?: string[];
}
​
const props = withDefaults(defineProps<Props>(), {
  msg: 'hello',
  labels: () => ['one', 'two']
});

emit

通过 defineEmits() 方法返回 Vue3 中的 $emit 对象,其设计与 props 类似。使用方式如下:

typescript 复制代码
// 定义允许使用的自定义方法列表,以字符串数组形式传递
const emit = defineEmits(['inFocus', 'submit']);
emit('inFocus'); // 调用自定义方法
​
// 使用 TypeScript 的情况,e 表示事件名称,第二个参数是传给外部的值,void 代表方法没有返回值
const emit = defineEmits<{
  (e: 'change', id: number): void;
  (e: 'update', value: string): void;
}>();
相关推荐
Redstone Monstrosity16 分钟前
字节二面
前端·面试
东方翱翔23 分钟前
CSS的三种基本选择器
前端·css
Fan_web1 小时前
JavaScript高级——闭包应用-自定义js模块
开发语言·前端·javascript·css·html
yanglamei19621 小时前
基于GIKT深度知识追踪模型的习题推荐系统源代码+数据库+使用说明,后端采用flask,前端采用vue
前端·数据库·flask
千穹凌帝1 小时前
SpinalHDL之结构(二)
开发语言·前端·fpga开发
dot.Net安全矩阵1 小时前
.NET内网实战:通过命令行解密Web.config
前端·学习·安全·web安全·矩阵·.net
Hellc0071 小时前
MacOS升级ruby版本
前端·macos·ruby
前端西瓜哥1 小时前
贝塞尔曲线算法:求贝塞尔曲线和直线的交点
前端·算法
又写了一天BUG1 小时前
npm install安装缓慢及npm更换源
前端·npm·node.js
cc蒲公英2 小时前
Vue2+vue-office/excel 实现在线加载Excel文件预览
前端·vue.js·excel