当我们谈论vue3时,我们在谈论什么?

213 阅读14分钟

Vue2与Vue3的主要区别?

组合式API和选项式API的区别

Vue2 是 选项式API(Option API),一个逻辑会散乱在文件不同位置(data、props、computed、watch、生命周期函数等),导致代码的可读性变差,需要上下来回跳转文件位置。Vue3 组合式API(Composition API)则很好地解决了这个问题,可将同一逻辑的内容写到一起。

image.png 除了增强了代码的可读性、内聚性,组合式API 还提供了较为完美的逻辑复用性方案

组合API功能抽离,封装:

image.png

Vue2中 Mixins存在的问题

mixins的缺点是Composition API背后的主要推动因素之一

  • 命名冲突(在跨多个组件和mixin处理命名属性时,编写代码变得越来越困难。一旦第三方mixin作为带有自己命名属性的npm包被添加进来,就会特别困难,因为它们可能会导致冲突)

image.png

  • 隐式依赖(mixin也可以使用假定在组件中定义的数据属性,如果我们以后想重构一个组件,改变了mixin需要的变量的名称,运行时会出现错误,现在想象一个有很多mixin的组件。我们可以重构本地数据属性吗?或者它会破坏mixin吗?我们得手动搜索才能知道。)

Composition API的主要思想是,我们将它们定义为从新的 setup 函数返回的JavaScript变量,而不是将组件的功能(例如state、method、computed等)定义为对象属性。

ps:通过ES6的对象的解构,可以解决命名冲突的问题

假设我们有两个 mixin:useMixin1useMixin2,它们都包含一个名为 data 的属性。我们将在组件中使用这两个 mixin,并通过重命名变量来避免冲突。

首先,让我们定义 mixin:

javascriptCopy Code
// mixin1.js
export function useMixin1() {
  return {
    data: {
      message: 'Mixin 1 data'
    }
  };
}

// mixin2.js
export function useMixin2() {
  return {
    data: {
      message: 'Mixin 2 data'
    }
  };
}

然后,在组件中使用这两个 mixin 并解决属性名冲突:

javascriptCopy Code
import { useMixin1 } from './mixin1';
import { useMixin2 } from './mixin2';

export default {
  setup() {
    const { data: dataFromMixin1 } = useMixin1();
    const { data: dataFromMixin2 } = useMixin2();

    console.log(dataFromMixin1.message); // 输出:Mixin 1 data
    console.log(dataFromMixin2.message); // 输出:Mixin 2 data

    return {
      dataFromMixin1,
      dataFromMixin2
    };
  }
};

在这个例子中,我们通过重命名变量 data,即 { data: dataFromMixin1 }{ data: dataFromMixin2 },分别从两个 mixin 中解构出 data 属性,避免了可能的命名冲突。这样我们就可以在组合式函数中轻松地处理来自不同 mixin 的属性名相同的情况,确保代码的可读性和稳定性。

Composition API 解决 mixin 的缺陷

1、命名冲突 - 解决了

  我们之前已经了解了mixin如何使用与消费者组件中的名称相同的属性,或者甚至更隐蔽地使用了消费者组件使用的其他mixin中的属性。

  这不是Composition API的问题,因为我们需要显式命名任何状态或从合成函数返回的方法。

export default {
  setup () {
    const { someVar1, someMethod1 } = useCompFunction1();
    const { someVar2, someMethod2 } = useCompFunction2();
    return {
      someVar1,
      someMethod1,
      someVar2,
      someMethod2
    }
  }
}

  当命名冲突时就会很清楚的知晓。

2、隐式依赖 - 解决了!

  前面还看到mixin如何使用在消费组件上定义的 data 属性,这可能会使代码变得脆弱,并且很难进行推理。

  合成函数(Composition Function)还可以调用消费组件中定义的局部变量。不过,不同之处在于,现在必须将此变量显式传递给合成函数

import useCompFunction from "./useCompFunction";
export default {
  setup () {
    // 某个局部值的合成函数需要用到
    const myLocalVal = ref(0);
 
    // 它必须作为参数显式地传递
    const { ... } = useCompFunction(myLocalVal);
  }
}

从一个例子看出Composition API的好处:

vue2中是实现dicts字典的通用配置是通过插件,混入等方式,非常繁琐

image.png 反观vue3就十分简洁,高效

image.png

image.png

image.png

在Composition API中,因为我们需要显式命名任何状态或从合成函数返回的方法,以及显式传递给合成函数参数,所以有效避免了mixins的问题

image.png

Vue2和Vue3生命周期的区别

image.png

Vue2和Vue3挂载的区别

image.png

区别

Vue2的组件系统设计中,所有Vue实例是共享一个Vue构造函数对象的,包括全局指令/全局组件,无法做到相互隔离。

也就是说我们整个项目中,只有一个根Vue实例,其他的单文件组件创建的 Vue实例都会成为它的子实例。

而Vue3通过createApp方法可以返回一个提供应用上下文的应用实例。不同实例注册的组件无法在不同的实例下使用。

Vue2和Vue3声明全局变量的区别

clipboard.png

watch监听的区别

vue2中watch的使用

默认写法:

watch: {
    msg(newValue, oldValue) {
        console.log(oldValue)
        console.log(newValue)
    }
}

immediate和deep都是false

扩展:

watch中的immediate、handler和deep属性

1、immediate属性

使用watch时有一个特点,就是当值第一次绑定时,不会执行监听函数,只有值发生改变时才会执行。如果我们需要在最初绑定值的时候也执行函数,则就需要用到immediate属性。

2、handler属性

watch中需要具体执行的方法

3、deep属性

当需要监听一个对象的改变时,普通的watch方法无法监听到对象内部属性的改变,此时就需要deep属性对对象进行深度监听,数组字符串一般不需要

data() {
    return {
        obj: {
            'name': "王",
            'age': 18
        },
    }
},
watch: {
    obj: {
        // 执行方法
        handler(newValue,oldVal) {
             console.log(oldValue)
             console.log(newValue)
        },
        deep: true, // 深度监听
        immediate: true  // 第一次改变就执行
    }
 
   // 如果只需要监听对象中的一个属性值,则可以做以下优化:使用字符串的形式监听对象属性:
    'obj.name': {
        // 执行方法
        handler(newValue,oldVal) {
             console.log(oldValue)
             console.log(newValue)
        },
        deep: true, // 深度监听
        immediate: true  // 第一次改变就执行
    }
}

与 Vue2 中的 watch 不同,Vue3的 watch 需要在 setup 里使用,在使用之前,还需要先导入该组件 Vue3 可以多次使用 watch 方法,通过多个watch 方法来监听多个对象。而 Vue2 则是把所有的要监控的对象放在 watch 里面。

[watch(source, callback, [options])](interm)

//监听基础类型
const num = ref(9)
watch(num,(newValue,oldValue)=>{

})

//监听对象中的某个属性
watch(()=>demo.name,(newValue,oldValue)=>{

})

//监听整个对象的方法
watch(()=>demo,(newValue,oldValue)=>{

},{deep:true})

如上代码,监听 demo 对象的 name 属性,那么只有当 demo 对象的 name 属性发生变更时,才会触发 watch 方法,其他属性变更不会触发 watch 方法。注意,此时的第一个参数是一个箭头函数

Vue3有一个新的方法 watchEffect 监听会立即执行

区别

1:执行时机:watchEffect是立即执行的,在页面加载时会主动执行一次,来收集依赖;而watch是惰性地执行副作用,它不会立即执行,但可以配置 immediate,使其主动触发

watchEffect(() => { 
    console.log(test.value) 
}) 
watch (test.value,(val,oldval) => { 
    console.log(val) 
},{ immediate: true })

2:参数不同:watchEffect只需要传递一个回调函数,不需要传递侦听的数据,它会在页面加载时主动执行一次,来收集依赖;而watch至少要有两个参数(第三个参数是配置项),第一个参数是侦听的数据,第二个参数是回调函数

3:结果不同:watchEffect获取不到更改前的值;而watch可以同时获取更改前和更改后的值

watch和computed的区别:juejin.cn/post/700410…

Proxy API 替代 defineProperty API

响应式优化。

a. defineProperty API 的局限性最大原因是它只能针对单例属性做监听。 Vue2.x 中的响应式实现正是基于 defineProperty 中的descriptor,对 data 中的属性做了 遍历 + 递归,为每个属性设置了 getter、setter。 这也就是为什么 Vue 只能对 data 中预定义过的属性做出响应的原因,在 Vue 中使用 下标的方式直接修改属性的值或者添加一个预先不存在的对象属性是无法做到 setter 监 听的,这是defineProperty 的局限性。

b. Proxy API 的监听是针对一个对象的,那么对这个对象的所有操作会进入监听操作,这就 完全可以代理所有属性,将会带来很大的性能提升和更优的代码。 Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先 通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

c. 响应式是惰性的 在 Vue.js 2.x 中,对于一个深层属性嵌套的对象,要劫持它内部深层次的变化,就需要递 归遍历这个对象,执行 Object.defineProperty 把每一层对象数据都变成响应式的,这无疑 会有很大的性能消耗。 在 Vue.js 3.0 中,使用 Proxy API 并不能监听到对象内部深层次的属性变化,因此它的处 理方式是在 getter 中去递归响应式,这样的好处是真正访问到的内部属性才会变成响应 式,简单的可以说是按需实现响应式,减少性能消耗。

TypeScript 支持

Vue3 由TS重写,相对于 Vue2 有更好地TypeScript支持。

  • Vue2 Option API中 option 是个简单对象,而TS是一种类型系统,面向对象的语法,不是特别匹配。
  • Vue2 需要vue-class-component强化vue原生组件,也需要vue-property-decorator增加更多结合Vue特性的装饰器,写法比较繁琐。

v-if和 v-for优先级

  • 在 vue 2.x 中,在一个元素上同时使用 v-if 和 v-for 时,v-for 会优先作用。(每次渲染都会先循环再进行条件判断)
  • 在 vue 3.x 中,v-if 总是优先于 v-for 生效。

避免v-for和v-if同时使用的原因

  • 在Vue2中,v-for的优先级是高于v-if的,如果作用在同一元素上,输出的渲染函数中可以看除会先执行循环再判断条件,哪怕只渲染列表中一小部分元素,也得在每次重渲染的时候遍历整个列表,这会造成性能的浪费
  • 而在Vue3中,v-if的优先级时高于v-for的,因此v-if执行时要调用的变量可能还不存在,会导致报错。

避免v-for和v-if同时使用的原因

插槽上的使用区别

插槽本质上是为了动态生成HTML。 如果只是数据是动态的,用普通的数据绑定就可以, 如果不仅数据是动态的,HTML也是动态的,数据绑定就不行了,因为HTML语法会混在JS语法里面,导致语法错误。 用插槽其实就是解决这个问题。

插槽分为默认插槽,具名插槽和作用域插槽

一个组件中只能有一个“默认插槽”

默认插槽简写为#default

2.6.0中,为具名插槽和作用域插槽引入了一个新的统一的语法,即v-slot。它取代了slot和slot-scope这两个目前已被废弃但未被移除且仍在文档中的attribute中。但是将会在vue 3 中,被废弃的这两个,不会被支持即无效。

v-slot具名插槽用法

<child>子组件:

<div>
    <slot name='header' />
</div>

父组件:

<child>
    <template v-slot:header>
        <div>header-text</div>
    </template>
</child>

也可以使用#简写方式

<child>
    <template #header>
        <div>header-text</div>
    </template>
</child>

v-slot作用域插槽用法

child子组件:

<div>
    <slot name='footer' :haha="haha" :data="data">
    </slot>
</div>
<script>
export default {
  data () {
    return {
      data: ["小明""小李"],
      haha: '这是一个作用域插槽'
    }
  }
}
</script>

父组件:

<child>
   <template v-slot:footer="row">
      <div>{{row.data}}</div>
      <div>{{row.haha}}</div>
   </template>
</child>

使用#简写方式:

<child>
   <template #footer="row">
      <div>{{row.data}}</div>
      <div>{{row.haha}}</div>
   </template>
</child>

官网中有一句特别强调的话:父组件模板的所有东西都会在父级作用域内编译;子组件模板的所有东西都会在子级作用域内编译。简单的说,就是父组件中不能直接用子组件中定义的data数据。而slot-scope的出现就是解决了这样的问题。

clipboard.png

组件模板唯一顶层元素的限制

Vue3的优化主要体现在哪些方面?

blog.csdn.net/weixin_4433…

vue3的组件,组合式函数,指令,插件

四种都是为了逻辑复用

组件是主要的构建模块,而组合式函数则侧重于有状态的逻辑。另一方面,自定义指令主要是为了重用涉及普通元素的底层 DOM 访问的逻辑

如果说组件 (Component) 是用来构成你的 App 的业务模块,它的目标是 App.vue

那么插件 (Plugin) 是用来增强你的技术栈的功能模块,它的目标是 Vue 本身

简单来说,插件就是指对Vue的功能的增强或补充,是渐进式框架的理论基础(vue-router,vuex都是插件) 详细区别见 juejin.cn/post/718877…

vue3 几个常见问题?

reactive VS ref

刚开始看文档时,大家往往会去拿这两个去对比,总结一下:

reactive API: 可以把一个对象数据变成响应式(等同于 2.x 中的 Vue.observable()),Composition API 更推荐用户主动定义响应式式数据,而非内部的黑盒处理

ref: 针对数组 or 对象

Ref的本质是通过Reactive创建的,Ref(10)=>Reactive({value:10});

Ref在模板调用可以直接省略value,在方法中改变变量的值需要修改value的值,才能修改成功

另外注意一下toRefs: 针对组合函数返回响应式对对象时使用 toRefs, 本质上是帮我们做了一层getter和setter处理,解构就可以得到响应式的数据

ref和reative到底有什么区别? vue3利用proxy实现响应式,而proxy不能代理基础类型,vue3就只能给它包装成一个对象再进行代理,所以大家可以看到基础类型变成响应式读取值的时候需要.value啦

注意reactive封装的响应式对象,不要通过解构的方式return,这是不具有响应式的。可以通过 toRefs 处理,然后再解构返回,这样才具有响应式

Reactive的本质是将每一层的数都解析成proxy对象,Reactive 的响应式默认都是递归的,改变某一层的值都会递归的调用一遍,重新渲染dom。

如何正确使用 reactive ?

了解响应式

1.ref 定义数据(包括对象)时,都会变成 RefImpl(Ref 引用对象) 类的实例,无论是修改还是重新赋值都会调用 setter,都会经过 reactive 方法处理为响应式对象。

2.但是 reactive 定义数据(必须是对象),是直接调用 reactive 方法处理成响应式对象。如果重新赋值,就会丢失原来响应式对象的引用地址,变成一个新的引用地址,这个新的引用地址指向的对象是没有经过 reactive 方法处理的,所以是一个普通对象,而不是响应式对象。解构同理。

image.png

image.png

defineComponent 的作用

defineComponent 是 Vue 3 推出的一个全新 API ,可用于对 TypeScript 代码的类型推导,帮助开发者简化掉很多编码过程中的类型声明。

比如,原本需要这样才可以使用 setup 函数:

	import { Slots } from 'vue'
	// 声明 `props` 和 `return` 的数据类型
	interface Data {
	  [key: string]: unknown
	}
	// 声明 `context` 的类型
	interface SetupContext {
	  attrs: Data
	  slots: Slots
	  emit: (event: string, ...args: unknown[]) => void
	}
	// 使用的时候入参要加上声明, `return` 也要加上声明
	export default {
	  setup(props: Data, context: SetupContext): Data {
	    // ...
	    return {
	      // ...
	    }
	  },
	}

每个组件都这样进行类型声明,会非常繁琐,如果使用了 defineComponent ,就可以省略这些类型声明:

	import { defineComponent } from 'vue'
	// 使用 `defineComponent` 包裹组件的内部逻辑
	export default defineComponent({
	  setup(props, context) {
	    // ...
	    return {
	      // ...
	    }
	  },
	})

代码量瞬间大幅度减少,只要是 Vue 本身的 API , defineComponent 都可以自动推导其类型,这样开发者在编写组件的过程中,只需要维护自己定义的数据类型就可以了,可专注于业务。

defineExpose 的作用

在vue3的setup语法糖的情况下,如果想让子组件的属性和方法能通过父组件的模板引用的方式获取去获取到,切记需要使用defineExpose这个方法!!!因为使用 <script setup> 的组件是默认关闭的——即通过模板引用或者 $parent 链获取到的组件的公开实例,不会暴露任何在 <script setup> 中声明的绑定。 需要通过defineExpose编译器宏来显式指定在 <script setup> 组件中要暴露出去的属性:

<script setup>
import { ref } from 'vue'

const a = 1
const b = ref(2)

defineExpose({
  a,
  b
})
</script>

当父组件通过模板引用的方式获取到当前组件的实例,获取到的实例会像这样 { a: number, b: number } (ref 会和在普通实例中一样被自动解包)