05-Vue3语法变化

7/24/2022

# 一、Vue3语法变化

# 1,在Vue2中,v-for 和 ref 同时使用,这会自动收集 $refs。当存在嵌套的v-for时,这种行为会变得不明确且效率低下。在Vue3中,v-for 和 ref 同时使用,这不再自动收集$refs。我们可以手动封装收集 ref 对象的方法,将其绑定在 ref 属性上。

// Demo.vue

<template>
  <h1>ref</h1>
  <!-- ref属性和v-for一起使用,需要手动封装方法来收集这些DOM -->
  <div v-for="i in 5" :key="i" v-text="i" :ref="collect"></div>

  <!-- ref属性使用单一节点上,需要声明一个ref对象来接收DOM -->
  <h1 ref="hello">你好</h1>
</template>

<script setup>
import { onMounted, ref } from "vue";
const hello = ref();
const arr = [];
const collect = (ref) => {
  if (ref) arr.push(ref);
};

console.log("arr:", arr);
console.log("hello:", hello);

onMounted(() => {
  arr[2].style.color = "red";
  hello.value.style.color = "blue";
});
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 2,在Vue3中,使用 defineAsyncComponent 可以异步地加载组件。需要注意的是,这种异步组件是不能用在Vue-Router的路由懒加载中。

// Demo.vue

<template>
  <h1>Demo组件</h1>
</template>

<script setup>
</script>
1
2
3
4
5
6
7
8
// App.vue

<template>
  <AsyncChild />
</template>

<script setup>
import { defineAsyncComponent } from "vue";
// 异步加载组件
const AsyncChild = defineAsyncComponent({
  loader: () => import("./components/Demo.vue"),
  delay: 200,
  timeout: 3000,
});
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 3,Vue3.0中的 $attrs,包含了父组件传递过来的所有属性,包括 class 和 style 。在Vue2中,$attrs 是接到不到 class 和 style 的。在 setup 组件中,使用 useAttrs() 访问;在非 setup组件中,使用 this.$attrs /setupCtx.attrs 来访问。

// Demo.vue

<template>
  <!-- 注:当父组件有传递class和style时,这里只能使用单一根节点 -->
  <div>
    <h1>$attrs</h1>
  </div>
</template>

<script setup>
import { useAttrs } from "vue";

const attrs = useAttrs(); // 父组件传递过来的自定义属性们
console.log("attrs:", attrs);
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// App.vue

<template>
  <Demo a="1" :b="2" :c="[110]" class="box" style="color: red" />
</template>

<script setup>
import Demo from "./components/Demo.vue";
</script>
1
2
3
4
5
6
7
8
9

# 4,Vue3中,移除了 $children 属性,要想访问子组件只能使用 ref 来实现了。在Vue2中,我们使用 $children 可以方便地访问到子组件,在组件树中“肆意”穿梭。

# 5,Vue3中,使用 app.directive() 来定义全局指令,并且定义指令时的钩子函数们也发生了若干变化。

import { createApp } from 'vue'
import './style.css'
import App from './App.vue'

let app = createApp(App)

// 全局指令
app.directive('upper', {
    // v3中新增的
    created() { },
    // 相当于v2中的 bind()
    beforeMount() {
    },
    // 相当于v2中的 inserted()
    mounted(el, binding, vnode, prevVnode) {
        console.log("----");
        console.log(binding);
        console.log(el);
        el.innerHTML = binding.value.toUpperCase();
    },
    // v3中新增的
    // beforeUpdate() { },
    // 相当于v2中的 update()+componentUpdated()
    // updated() { },
    // v3中新增的
    // beforeUnmount() { },
    // 相当于v2中的 unbind()
    // unmounted() { }
})

app.mount('#app')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// Demo.vue

<template>
  <div>
    <h1 v-upper="msg"></h1>
  </div>
</template>

<script setup>
import { ref } from "vue";
let msg = ref("hello malu");
</script>
1
2
3
4
5
6
7
8
9
10
11
12

# 6,data 选项,只支持工厂函数的写法,不再支持对象的写法了。在Vue2中,创建 new Vue({ data }) 时,是可以写成对象语法的。

// Demo.vue

<script>
  import { createApp } from 'vue'

  createApp({
    data() {
      return {
        msg: 'Hello World'
      }
    }
  }).mount('#app')
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13

# 7,Vue3中新增了 emits 选项。在非script setup写法中,使用 emits选项 接收父组件传递过来的自定义,使用 ctx.emit() 来触发事件。在script setup中,使用 defineEmits 来接收自定义事件,使用 defineProps 来接收自定义属性。

在组件通信的案例中演示过了

# 8,Vue3中 移除了 $on / $off / $once 这三个事件 API,只保留了 $emit 。

  • 也就是说在vue3中不能使用事件总线

# 9,Vue3中,移除了全局过滤器(Vue.filter)、移除了局部过滤器 filters选项。取而代之,你可以封装自定义函数或使用 computed 计算属性来处理数据。

  • Vue3中没有过滤器了

# 10,Vue3 现在正式支持了多根节点的组件,也就是片段,类似 React 中的 Fragment。使用片段的好处是,当我们要在 template 中添加多个节点时,没必要在外层套一个 div 了,套一层 div 这会导致多了一层 DOM结构。可见,片段 可以减少没有必要的 DOM 嵌套。

// Demo.vue

<template>
  <header>...</header>
  <main>...</main>
  <footer>...</footer>
</template>
1
2
3
4
5
6
7

# 11,函数式组件的变化:在Vue2中,要使用 functional 选项来支持函数式组件的封装。在Vue3中,函数式组件可以直接用普通函数进行创建。如果你在 vite 环境中安装了 @vitejs/plugin-vue-jsx 插件来支持 JSX语法,那么定义函数式组件就更加方便了。

  • 后面讲React时,会讲解什么是JSX和函数式组件

# 12,Vue2中的Vue构造函数,在Vue3中已经不能再使用了。所以Vue构造函数上的静态方法、静态属性,比如 Vue.use/Vue.mixin/Vue.prototype 等都不能使用了。在Vue3中新增了一套实例方法来代替,比如 app.use()等。

// Demo.vue

import { createApp } from 'vue'
import router from './router'
import store from './store'
import App from './App.vue'

const app = createApp(App)

// 相当于 v2中的 Vue.prototype
app.config.globalProperties.$http = ''
// 等价于 v2中的 Vue.use
app.use(router) // 注册路由系统
app.use(store)  // 注册状态管理
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 13,在Vue3中,使用 getCurrentInstance 访问内部组件实例,进而可以获取到 app.config 上的全局数据,比如 $route、$router、$store 和自定义数据等。这个 API 只能在 setup 或 生命周期钩子 中调用。

  • 之前讲组件式API时演示过了

# 14,我们已经知道,使用 provide 和 inject 这两个组合 API 可以组件树中传递数据。除此之外,我们还可以应用级别的 app.provide() 来注入全局数据。在编写插件时使用 app.provide() 尤其有用,可以替代app.config.globalProperties。

// main.tx
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'

let app = createApp(App)

// 注册全局数据
app.provide('$url', 'http://localhost:8888')

app.mount('#app')
1
2
3
4
5
6
7
8
9
10
11
// Demo.vue
<template>
  <div>
    <h1>app.provide()注入数据</h1>
  </div>
</template>

<script setup>
import { inject } from "vue";
const $url = inject("$url");
console.log("$url:", $url);
</script>
1
2
3
4
5
6
7
8
9
10
11
12

# 15,在Vue2中,Vue.nextTick() / this.$nextTick 不能支持 Webpack 的 Tree-Shaking 功能的。在 Vue3 中的 nextTick ,考虑到了对 Tree-Shaking 的支持。

nextTick介绍

  • Vue在观察到数据变化时,并不是直接更新DOM,而是开启一个队列,并且缓存同一轮事件循环中的所有数据改变。在缓冲时会除去重复的操作, 等到下一轮事件循环时,才开始更新。
  • $nextTick的作用:就是用来告知DOM什么时候更新完,当DOM更新完毕后,nextTick方法里面的回调就会执行

需求: 有一个div,默认用 v-if 将它隐藏,点击一个按钮后,改变 v-if 的值,让它显示出来,同时拿到这个div的文本内容。如果v-if的值是 false,直接去获取div内容是获取不到的,因为此时div还没有被创建出来,那么应该在点击按钮后,改变v-if的值为 true,div才会被创建,此时再去获取。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.7.10/dist/vue.js"></script>
</head>

<body>

    <div id="app">
        <div id="div" v-if="showDiv">这是一段文本</div>
        <button @click="getText">获取div内容</button>
    </div>

    <script>
        let vm = new Vue({
            el: "#app",
            data: {
                showDiv: false
            },
            methods: {
                // 如果像注释的代码,不加nextTick这个方法,就会报错,因为DOM更新是在下一次事件循环,才更新,所以此时获取不到div元素。
                // getText: function () {
                //   this.showDiv = true;
                //   var text = document.getElementById('div').innerHTML;
                //   console.log(text);
                // }
                getText: function() {
                    this.showDiv = true;
                    this.$nextTick(function() {
                        var text = document.getElementById('div').innerHTML;
                        console.log(text);
                    });
                }
            }
        });
    </script>

</body>

</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

应用场景

  • mounted 获取ajax数据,后根据页面渲染数据的样式(文字占宽)来修改布局。
  • 几乎所有更新数据后操作dom的操作,都需要用到异步更新队列

Vue3.x中使用

// Demo.vue

<template>
  <h1>Vue3中nextTick</h1>
  <div id="div" v-if="showDiv">这是一段文本</div>
  <button @click="getText">获取div内容</button>
</template>

<script setup>
import { ref, nextTick } from "vue";
let showDiv = ref(false);

let getText = () => {
  showDiv.value = true;
  nextTick(() => {
    console.log(document.getElementById("div").innerHTML);
  });
};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 16,Vue3中,对于 v-if/v-else/v-else-if的各分支项,无须再手动绑定 key了, Vue3会自动生成唯一的key。因此,在使用过渡动画transition对多个节点进行显示隐藏时,也无须手动加 key了。

// Demo.vue

<template>
  <!-- 穿梭框,把内部元素插入to属性所对应的DOM节点中去 -->
  <teleport to="head">
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/animate.css@4.1.1/animate.min.css"
    />
  </teleport>
  <div>
    <transition
      enter-active-class="animate__animated animate__bounce"
      leave-active-class="animate__animated animate__backOutDown"
      mode="out-in"
    >
      <!-- <transition name="ml" mode="out-in"> -->
      <h1 v-if="bol">白天</h1>
      <h1 v-else>黑夜</h1>
    </transition>
  </div>
  <button @click="toggle">切换</button>

  <teleport to="body">
    <h1>你好</h1>
  </teleport>
</template>

<script setup>
import { ref } from "vue";
const bol = ref(true);

const toggle = () => {
  bol.value = !bol.value;
};
</script>

<style>
.ml-enter-from {
  opacity: 0;
  color: red;
}
.ml-enter-active {
  transition: all ease 2s;
}
.ml-enter-to {
  opacity: 1;
  color: black;
}

.ml-leave-from {
  opacity: 1;
  color: black;
}
.ml-leave-active {
  transition: all ease 2s;
}
.ml-leave-to {
  opacity: 0;
  color: blue;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

# 17,在Vue2中,使用 Vue.config.keyCodes 可以修改键盘码,这在Vue3 中已经淘汰了。

  • 基本上没有项目会去修改键盘码

# 18,Vue3中,$listeners 被移除了。因此我们无法再使用 $listeners 来访问、调用父组件给的自定义事件了。

  • Vue2中的$listeners地址:https://www.jb51.net/article/240305.htm

# 19,在Vue2中,根组件挂载 DOM时,可以使用 el 选项、也可以使用 $mount()。但,在 Vue3中只能使用 $mount() 来挂载了。并且,在 Vue 3中,被渲染的应用会作为子元素插入到 div id='app' 中,进而替换掉它的innerHTML。

# 20,在Vue2中,使用 propsData 选项,可以实现在 new Vue() 时向根组件传递 props 数据。在Vue3中,propsData 选项 被淘汰了。替代方案是:使用createApp的第二个参数,在 app实例创建时向根组件传入 props数据。

// main.ts
import { createApp } from 'vue'
import App from './App.vue'
// 使用第二参数,向App传递自定义属性
const app = createApp(App, { name:'vue3' })
app.mount('#app') // 挂载
1
2
3
4
5
6
// App.vue
<script setup>
  import { defineProps } from 'vue'
  // 接收 createApp() 传递过来的自定义属性
  const props = defineProps({
    name: { type: String, default: '' }
  })
  console.log('app props', props);
</script>
1
2
3
4
5
6
7
8
9

# 21,在Vue2中,组件有一个 render 选项(它本质上是一个渲染函数,这个渲染函数的形参是 h 函数),h 函数相当于 React 中的 createElement()。在Vue3中,render 函数选项发生了变化:它的形参不再是 h 函数了。h 函数变成了一个全局 API,须导入后才能使用。

虚拟DOM的渲染过程

认识h函数 Vue推荐在绝大数情况下使用模板来创建你的HTML,然后一些特殊的场景,你真的需要JavaScript的完全编程的能力,这个时候你可以使用 渲染函数 ,它比模板更接近编译器;

  • Vue在生成真实的DOM之前,会将我们的节点转换成VNode,而VNode组合在一起形成一颗树结构,就是虚 拟DOM(VDOM)
  • 事实上,我们之前编写的 template 中的HTML 最终也是使用渲染函数生成对应的VNode
  • 那么,如果你想充分的利用JavaScript的编程能力,我们可以自己来编写 createVNode 函数,生成对应的VNode
  • h() 函数是一个用于创建 vnode 的一个函数
  • 其实更准备的命名是 createVNode() 函数,但是为了简便在Vue将之简化为 h() 函数

代码如下:

// Demo.vue
<template>
    <renderButton></renderButton>
</template>

<script setup>
    // h是用来创建虚拟节点
    import {
        h
    } from "vue";

    let renderButton = {
        // render叫渲染函数
        render() {
            return h("h2", {
                class: "title"
            }, "Hello Vue");
        },
    };
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Demo.vue

<template>
  <renderButton></renderButton>
</template>

<script setup>
// h是用来创建虚拟节点
import { h, ref } from "vue";
let counter = ref(0);

let renderButton = {
  // render叫渲染函数
  render() {
    return h("div", { class: "app" }, [
      h("h2", null, `计数器:${counter.value}`),
      h(
        "button",
        {
          onClick: () => counter.value++,
        },
        "加1"
      ),
      h(
        "button",
        {
          onClick: () => counter.value--,
        },
        "减1"
      ),
    ]);
  },
};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# 22,Vue3中新增了实验性的内置组件suspense,它类似 React. Suspense 一样,用于给异步组件加载时,指定 Loading指示器。需要注意的是,这个新特征尚未正式发布,其 API 可能随时会发生变动。

// Demo.vue

<template>
  <suspense>
    <!-- 用name='default'默认插槽加载异步组件 -->
    <AsyncChild />
    <!-- 异步加载成功前的loading 交互效果 -->
    <template #fallback>
      <div> Loading... </div>
    </template>
  </suspense>
</template>

<script setup>
  import { defineAsyncComponent } from 'vue'
  const AsyncChild = defineAsyncComponent({
    loader: ()=>import('./components/Child.vue'),
    delay: 200,
    timeout: 3000
  });
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 23,Vue3中,过渡动画transition发生了一系列变化。之前的 v-enter 变成了现在的 v-enter-from , 之前的 v-leave 变成了现在的 v-leave-from 。

// Demo.vue

<template>
  <transition name="fade">
    <h1 v-if="bol">但使龙城飞将在,不教胡马度阴山!</h1>
  </transition>
  <button @click="bol = !bol">切换</button>
</template>

<script setup>
import { ref } from "vue";
const bol = ref(true);
</script>

<style scoped>
.fade-enter-from {
  opacity: 0;
  color: red;
}
.fade-enter-active {
  transition: all 1s ease;
}
.fade-enter-to {
  opacity: 1;
  color: black;
}

.fade-leave-from {
  opacity: 1;
  color: black;
}
.fade-leave-active {
  transition: all 1.5s ease;
}
.fade-leave-to {
  opacity: 0;
  color: blue;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

# 24,在Vue3中,v-on的.native修饰符已被移除。

# 25,同一节点上使用 v-for 和 v-if ,在Vue2中不推荐这么用,且v-for优先级更高。在Vue3中,这种写法是允许的,但 v-if 的优秀级更高。

# 26,在Vue2中,静态属性和动态属性同时使用时,不确定最终哪个起作用。在Vue3中,这是可以确定的,当动态属性使用 :title 方式绑定时,谁在前面谁起作用;当动态属性使用 v-bind='object'方式绑定时,谁在后面谁起作用。

// Demo.vue

<template>
  <!-- 这种写法,同时绑定静态和动态属性时,谁在前面谁生效! -->
  <div id='red' :id='("blue")'>不负当下</div>
  <div :title='("hello")' title='world'>不畏未来</div>
  <hr>
  <!-- 这种写法,同时绑定静态和动态属性时,谁在后面谁生效! -->
  <div id='red' v-bind='{id:"blue"}'>不负当下</div>
  <div v-bind='{title:"hello"}' title='world'>不畏未来</div>
</template> 
1
2
3
4
5
6
7
8
9
10
11

# 27,当使用watch选项侦听数组时,只有在数组被替换时才会触发回调。换句话说,在数组被改变时侦听回调将不再被触发。要想在数组被改变时触发侦听回调,必须指定deep选项。

// Demo.vue

<template>
  <div v-for="t in list" v-text="t.task"></div>
  <button @click.once="addTask">添加任务</button>
</template>

<script setup>
import { reactive, watch } from "vue";
const list = reactive([
  { id: 1, task: "读书", value: "book" },
  { id: 2, task: "跑步", value: "running" },
]);
const addTask = () => {
  list.push({ id: 3, task: "学习", value: "study" });
};
// 当无法监听一个引用类型的变量时
// 添加第三个选项参数 { deep:true }
watch(
  () => list[0].task,
  () => {
    console.log("list changed", list);
  },
  { deep: true }
);
setTimeout(() => {
  list[0].task += "666";
}, 2000);
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 28,在Vue2中接收 props时,如果 prop的默认值是工厂函数,那么在这个工厂函数里是有 this的。在Vue3中,生成 prop 默认值的工厂函数不再能访问this了。

// Demo01.vue
<template>
  <h1>Demo01组件</h1>
</template>

<script setup>
import { defineProps, inject } from "vue";
const props = defineProps({
  a: { type: Number, default: 0 },
  b: { type: Boolean, default: false },
  // 如果自定义属性是引用数据类型,default建议使用如下写法(工厂函数)
  c: {
    type: Array,
    default() {
      return inject("list", []);
    },
  },
  d: {
    type: Object,
    default() {
      return inject("info", { name: "码路", age: 10 });
    },
  },
});
console.log("props:", props);
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Demo.vue

<template>
  <h1>Demo组件</h1>
  <Demo01 :a="1" b :c="list" />
</template>

<script setup>
import Demo01 from "./Demo01.vue";
import { reactive, provide } from "vue";
const list = reactive([1, 2, 3]);
const info = reactive({ a: 1, b: 2 });

provide("list", [4, 5, 6]);
provide("info", { c: 3, d: 4 });
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 29,Vue3中,新增了teleport组件,这相当于 ReactDOM.createPortal(),它的作用是把指定的元素或组件渲染到任意父级作用域的其它DOM节点上。上面第 16个知识点中,用到了teleport加载 animate.css 样式表,这算是一种应用场景。除此之外,teleport还常用于封装 Modal 弹框组件。

  • 自行搜索,使用teleport封装 Modal

# 30,在Vue3中,移除了 model 选项,移除了 v-bind 指令的 .sync 修饰符。在Vue2中,v-model 等价于 :value + @input ;在Vue3中,v-model 等价于 :modelValue + @update:modelValue 。在Vue3中,同一个组件上可以同时使用多个 v-model。在Vue3中,还可以自定义 v-model 的修饰符。

v-model在组件上的简单使用:

// Demo.vue

<template>
  <h1>页面</h1>
  <hr />

  <!-- v-model = :modelValue + @update:modelValue -->
  <!-- v-model:gender = :gender + @update:gender -->
  <!-- v-model:xxx = :xxx + @update:xxx -->

  <!-- <Demo01 :modelValue="lang" @update:modelValue="lang = $event" /> -->
  <Demo01 v-model="lang" />
</template>

<script setup>
import Demo01 from "./Demo01.vue";
import { ref } from "vue";
const lang = ref("zh");
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Demo01.vue
<template>
  <div>
    <div class="lang">
      <span
        v-for="item in langs"
        :key="item.id"
        v-text="item.label"
        :class="{ on: modelValue === item.value }"
        @click="langChange(item)"
      >
      </span>
    </div>
  </div>
</template>

<script setup>
import { reactive, defineProps, defineEmits, computed } from "vue";
const langs = reactive([
  { id: 1, value: "zh", label: "中文" },
  { id: 2, value: "en", label: "英文" },
  { id: 3, value: "fr", label: "法语" },
]);
const props = defineProps({
  modelValue: { type: String, default: "zh" },
  modelModifiers: { default: () => ({}) },
});
const emit = defineEmits(["update:modelValue"]);

const langChange = (item) => {
  // 如果sort为真,先处理排序,再回传给父组件
  // 如果trim为真,把字符串首尾空字符删除,再回传父组件
  emit("update:modelValue", item.value);
};
</script>

<style>
.lang span {
  cursor: pointer;
  padding: 20px;
}
.lang span.on {
  color: red;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

多个v-model的使用,如下:

// Demo.vue

<template>
  <h1>v-model在组件上的使用</h1>
  <hr />

  <!-- v-model = :modelValue + @update:modelValue -->
  <!-- v-model:gender = :gender + @update:gender -->
  <!-- v-model:xxx = :xxx + @update:xxx -->

  <!-- <Demo01 :modelValue="lang" @update:modelValue="lang = $event" /> -->
  <Demo01 v-model="lang" v-model:gender="gender" />
</template>

<script setup>
import Demo01 from "./Demo01.vue";
import { ref } from "vue";
const lang = ref("zh");
const gender = ref("unknow");
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Demo01.vue

<template>
  <div>
    <div class="lang">
      <span
        v-for="item in langs"
        :key="item.id"
        v-text="item.label"
        :class="{ on: modelValue === item.value }"
        @click="langChange(item)"
      >
      </span>
    </div>
    <div>
      <!-- 对H5的单选按钮组来讲, v-model = :checked + @change -->
      <!-- 对H5的文本表单来讲,v-model = :value + @input -->
      <!-- 对H5的下拉框来讲,v-model = :value + @change -->
      <input
        type="radio"
        value="man"
        :checked="gender === 'man'"
        @change="emit('update:gender', $event.target.value)"
      /><input type="radio" value="woman" v-model="g" /><input type="radio" value="unknow" v-model="g" />保密
    </div>
  </div>
</template>

<script setup>
import { reactive, defineProps, defineEmits, computed } from "vue";
const langs = reactive([
  { id: 1, value: "zh", label: "中文" },
  { id: 2, value: "en", label: "英文" },
  { id: 3, value: "fr", label: "法语" },
]);
const props = defineProps({
  modelValue: { type: String, default: "zh" },
  modelModifiers: { default: () => ({}) },

  gender: { type: String, default: "man" },
  genderModifiers: { default: () => ({}) },
});
const emit = defineEmits(["update:modelValue", "update:gender"]);
const g = computed({
  get() {
    return props.gender;
  },
  set(val) {
    emit("update:gender", val);
  },
});
const langChange = (item) => {
  emit("update:modelValue", item.value);
};
</script>

<style>
.lang span {
  cursor: pointer;
  padding: 20px;
}
.lang span.on {
  color: red;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

自定义修饰符,如下:

// Demo.vue

<template>
  <h1>页面</h1>
  <hr />

  <!-- v-model = :modelValue + @update:modelValue -->
  <!-- v-model:gender = :gender + @update:gender -->
  <!-- v-model:xxx = :xxx + @update:xxx -->

  <!-- <Demo01 :modelValue="lang" @update:modelValue="lang = $event" /> -->
  <Demo01 v-model.trim.sort="lang" />
</template>

<script setup>
import Demo01 from "./Demo01.vue";
import { ref } from "vue";
const lang = ref("zh");
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Demo01.vue

<template>
  <div>
    <div class="lang">
      <span
        v-for="item in langs"
        :key="item.id"
        v-text="item.label"
        :class="{ on: modelValue === item.value }"
        @click="langChange(item)"
      >
      </span>
    </div>
  </div>
</template>

<script setup>
import { reactive, defineProps, defineEmits, computed } from "vue";
const langs = reactive([
  { id: 1, value: "zh", label: "中文" },
  { id: 2, value: "en", label: "英文" },
  { id: 3, value: "fr", label: "法语" },
]);
const props = defineProps({
  modelValue: { type: String, default: "zh" },
  modelModifiers: { default: () => ({}) },
});
const emit = defineEmits(["update:modelValue"]);

const langChange = (item) => {
  const { sort, trim } = props.modelModifiers;
  console.log(sort, trim);
  // 如果sort为真,先处理排序,再回传给父组件
  // 如果trim为真,把字符串首尾空字符删除,再回传父组件
  emit("update:modelValue", item.value);
};
</script>

<style>
.lang span {
  cursor: pointer;
  padding: 20px;
}
.lang span.on {
  color: red;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

自行研究如下案例:

// GoodFilter.vue

<template>
  <span>请选择商家(多选):</span>
  <span v-for="s in shopArr">
    <input
      type="checkbox"
      :value="s.value"
      :checked="shop.includes(s.value)"
      @change="shopChange"
    />
    <span v-text="s.label"></span> </span
  ><br />

  <span>请选择价格(单选):</span>
  <span v-for="p in priceArr">
    <input
      type="radio"
      :value="p.value"
      :checked="p.value === price"
      @change="priceChange"
    />
    <span v-text="p.label"></span>
  </span>
</template>

<script setup>
import { reactive, defineProps, defineEmits, toRefs } from "vue";

const props = defineProps({
  shop: { type: Array, default: [] },
  // 接收v-model:shop的自定义修饰符
  shopModifiers: { default: () => ({}) },
  price: { type: Number, default: 500 },
  // 接收v-model:price的自定义修饰符
  priceModifiers: { default: () => ({}) },
});
const { shop, price } = toRefs(props);
// 接收v-model的自定义事件
const emit = defineEmits(["update:shop", "update:price"]);

const shopArr = reactive([
  { id: 1, label: "华为", value: "huawei" },
  { id: 2, label: "小米", value: "mi" },
  { id: 3, label: "魅族", value: "meizu" },
  { id: 4, label: "三星", value: "samsung" },
]);
const priceArr = reactive([
  { id: 1, label: "1000以下", value: 500 },
  { id: 2, label: "1000~2000", value: 1500 },
  { id: 3, label: "2000~3000", value: 2500 },
  { id: 4, label: "3000以上", value: 3500 },
]);

// 多选框
const shopChange = (ev) => {
  const { checked, value } = ev.target;
  // 使用v-model:shop的自定义修饰符
  const { sort } = props.shopModifiers;
  let newShop = checked
    ? [...shop.value, value]
    : shop.value.filter((e) => e !== value);
  if (sort) newShop = newShop.sort();
  emit("update:shop", newShop);
};
// 单选框
const priceChange = (ev) => {
  emit("update:price", Number(ev.target.value));
};
</script>

<style scoped>
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// App.vue

<template>
  <GoodFilter
    v-model:shop.sort='shop'
    v-model:price='price'
  />
</template>

<script setup>
  import { ref, reactive, watch } from 'vue'
  import GoodFilter from './components/GoodFilter.vue'
  const shop = ref([])
  const price = ref(500)

  watch([shop, price], ()=>{
    console.log('changed', shop.value, price.value)
  });
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 31,新增了一个指令v-memo

// Demo.vue

<template>
  <h1>v-memo指令</h1>

  <!-- 只有当foo这个变量变化时,v-memo所包裹的视图结构才更新 -->
  <div v-memo="[foo]">
    <h1 v-text="num"></h1>
    <button @click="num++">自增</button>
  </div>
  <hr />

  <h1 v-text="foo"></h1>
  <button @click="foo--">自减</button>
</template>

<script setup>
import { ref } from "vue";
const num = ref(1);
const foo = ref(1000);
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 32,样式书写的必变

// Demo.vue

<template >
  <h1 class="box">样式的变化</h1>
  <button @click="change">改变</button>
  <hr />

  <h1 :class="zz">测试</h1>
  <button @click="changeZZ">改变</button>
</template>

<script setup>
import { ref, useCssModule } from "vue";
const style = useCssModule();
const mlstyle = useCssModule("ml");

const cc = ref("red");
const ff = ref(20);
const zz = ref(mlstyle.r1);

const change = () => {
  cc.value = "blue";
};
const changeZZ = () => {
  zz.value = mlstyle.r2;
};
</script>

<style>
/* 在style标签中,只能使用v-bind */
.box {
  color: v-bind(cc);
}
.r1 {
  color: orange;
}
</style>

<style scoped module='ml'>
.r1 {
  color: red;
}
.r2 {
  color: blue;
}
.r3 {
  color: green;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

# 二,总结Vue3.x变化

# 1,移除了哪些?

  • $children移除了(注意$parent还在)
  • new Vue({ data:{} }) 这个写法淘汰了,如果用到data一定要使用工厂函数。
  • 移除了 $on / $off / $once (注意$emit还在)
  • 在V3中,事件总线这种通信这种基于订阅发布模式的通信方案也不能用了。
  • 移除了全局过滤器(Vue.filter)、移除了局部过滤器 filters选项。
  • 在V3中,Vue这个构造函数不能用了,所以像V2中的那个全局API都不能了,所以Vue的原型链也不能用了。
  • 在V3中,不再支持修改键盘码了。
  • 移除了$listeners (注意$attrs还在)
  • 移除了 el选项、model选项、propsData选项
  • v-on的.native修饰符已被移除。

# 2,新增了哪些?

  • 在V3中,增加了 emits选项、defineEmits,用于在子组件中接收自定义事件。
  • 在组件中,使用 getCurrentInstance 可以访问 app根实例。
  • 新增了 suspense,用于给异步组件添加交互效果。
  • 新增了 teleport的to属性,用于把包裹的视图结构“穿梭”到父级的DOM节点中去。
  • 新增了样式的玩法,第一个玩法是在style中可以使用v-bind来做动态样式;在style标签上module='default'实现样式模块化,在组合中使用useCssModule访问样式模块。
  • 新增了一个指令 v-memo,用法:在div上v-memo='[a, b]',有且仅有当a或b变化时,其包裹的视图结构才会更新。
  • 在V3中,新增了 useSlots,用于在子组件中访问插槽实例。相当于V2中的this.$slots。

# 3,变化了哪些?

  • 在V2中,v-for和ref一起作用,可以自动收集refs对象,在this.$refs上访问。在V3中,不能自动收集了,需要自己封装收集方法
  • 在V2中,ref属性作用在HTML标签或组件上,可以在this.$refs上访问DOM或组件实例;在V3中,ref属性配合ref这个组合API为完成对DOM或组件实例的方法,div ref='box',这个box是一个ref对象,使用box.value访问对应的DOM。
  • 在V3中,使用 defineAsyncComponent 定义更加友好的异步组件。
  • 在V2中,使用this.$attrs访问父组件传递过来的属性们(不包括class和style);在V3中,使用useAttrs/context.attrs/$attrs访问父组件传递过来的自定义属性们(包含class和style)。
  • 在V3中,使用 app.directive({}/fn) 自定义指令,注意在指令的选项钩了发生了若干变化。
  • 在V3中,template视图模块支持多节点。(当在自定义组件上使用class/style时、在自定义组件上使用多个v-model时,都报了多节点的警告,这时建议将其改成单一根节点)
  • 在V2中,provide/inject是没有响应式的。在V3中,provide用选项写法时,必须配合computed才能实现响应式;provide用组合写法时,默认就有响应式。
  • 在V2中,Vue.nextTick() / this.$nextTick 不能支持 Webpack 的 Tree-Shaking 功能的。在 V3 中的 nextTick ,考虑到了对 Tree-Shaking 的支持。
  • V3中,对于 v-if/v-else/v-else-if的各分支项,无须再手动绑定 key了, V3会自动生成唯一的key。比如transition对多节点执行动画时,无须再加key了。
  • 在V3中,render选项的函数参数不再是h函数了,如果要使用h函数,得从vue导入。
  • transition过渡动画的变化:v-enter-from/v-enter-active/v-enter-to和V2不同了;对多节点执行显示与隐藏时,不需要再加key了。当我们封装自定义组件时,如果transition是这个组件的根节点,要使用props传值来触发动画。
  • 同一节点上使用 v-for 和 v-if ,在V2中不推荐这么用,且v-for优先级更高。在V3中,这种写法是允许的,但 v-if 的优秀级更高。
  • watch这个组合API:允许同时监听多个变量的变化,允许停止监听。const stop = watch([a, b], callback)
  • 在V3中,使用defineProps接收自定义属性时,如果这个属性是对象类型,其default: () => { return },并且在这个default工厂函数中可以使用inject。
  • v-model有三个变化,在自定义组件上v-model = :modelValue + @update:modelValue, v-model:name = :name + @update:name;在自定义组件上,可以同时使用多个v-model,在People 组件上使用v-model='age' v-model:name='name';还支持自定义修饰符,注意在子组件中必须使用defineProps来分别接收自定义修饰符。
Last Updated: 5/17/2023, 11:37:25 AM