vue2和vue3的区别

150 阅读10分钟

一.数据的双向绑定

Vue2.0使用Object.defineProperty

原理:整体思路是数据劫持+观察者模式 对象内部通过 defineReactive 方法,使用 Object.defineProperty 将属性进行劫持(只会劫持已经存在的属性),数组则是通过重写数组方法来实现。当页面使用对应属性时,每个属性都拥有自己的 dep 属性,存放他所依赖的 watcher(依赖收集),当属性变化后会通知自己对应的 watcher 去更新(派发更新)。

Vue 3.0使用ES6的新特性porxy

原理:Proxy在ES2015规范中被正式发布,它在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写,我们可以这样认为,Proxy是Object.defineProperty的全方位加强版,具体的文档可以查看此处;

总结:Proxy有多达13种拦截方法,不限于apply、ownKeys、deleteProperty、has等等是Object.defineProperty不具备的。

Proxy返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改。

Proxy作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利。

当然,Proxy的劣势就是兼容性问题,而且无法用polyfill磨平

二.创建vue2和vue3项目的文件发生的变化

  • main.js中

8e6ce31ab604f3062fed9272ee51a83a.png

  • vue2.0中是直接创建了一个vue实例
  • vue3.0中按需导出了一个createApp
  • vue3.0中的app单文件不再强制要求必须有根元素 也就是说 在vue2.0中必须要有一个根元素,在vue3中没这个要求,下面是vue3.0的写法:
<template>
  <img alt="Vue logo" src="./assets/logo.png">
  <HelloWorld msg="Welcome to Your Vue.js App"/>
</template>

三.最具有颠覆意义的响应-组合 API

vue2.0 option API (配置式API)

vue3.0 composition 组合式API

这也是我们学习和转向 vue3 最有意义的一部分内容,可以这么说,如果没有掌握这一 部分内容,那么即使我们底层使用了vue3,也不过是写 vue2。就像我们拥有了一台铲车, 却还在坚持靠人力把重物装卸到车叉上,只把铲车当运输工具一样。

setup 函数

组件创建前执行的初始化函数,默认参数包含 props 和 context。context 可以理解为 组件实例挂载之前的上下文(虽然这么理解,但不是实例本身,这点要清楚),所以实例上 的 attrs,slots,emit 可以在 context 上访问到。 在setup中我们可以决定哪些数据用响应式哪些不用响应式 比较常用的是:ref reactive toRef toRefs下面分别来介绍一下:

1.ref

  • 常用于包装基本类型值为响应式对象,ref声明的变量,我们可以在 html代码中直接使用变量,但在 js 代码中,我们需要通过 .value来访问,因为 ref定义的变量返回的是一个响应式的 ref 对象,对象需要通过 .的形式来访问,官网详情见[此处]www.javascriptc.com/vue3js/api/… ref举例 :
  <div class="demo">
    <h2>姓名: {{name}}</h2>
    <h2>年龄: {{age}}</h2>
    <h3>岗位: {{job.type}}</h3>
    <h3>工龄: {{job.workingAge}}</h3>
    <button @click="updateInfo()">更新</button>
  </div>
</template>
 
<script>
import { defineComponent, ref } from 'vue'
 
export default defineComponent({
  setup () {
// 以下方式定义的是普通数据,非响应式数据
    // let name = '张三';
    // let age = 18;
    // let job = {
    //   type: 'web前端',
    //   workingAge: 8
    // }
    // function  updateInfo() {
    //   name = '李四';
    //   age = 20;
    //   console.log(name, age);
    // }
 
// 基本类型:ref生成一个引用对象,通过get/set做数据劫持,Ref(reference引用)Impl(implement实现)
    let name = ref('张三'); 
    let age = ref(18);
    console.log('name', name, age)
 
    // 对象类型,内部'求助'了vue3中的一个新函数reactive函数
    let job = ref({
      type: 'web前端',
      workingAge: 8
    })
    function  updateInfo() {
      // 注:模板里面不需要使用.value,模板解析时遇到 ref对象会自动读取.value
      console.log(name, age);
      name.value = '李四';
      age.value = 20;
      job.value.type = 'JAVA';
      job.value.workingAge = 10;
    }
    
    return {
      name,
      age,
      updateInfo,
      job
    }
  }
})
</script>
  • ref一般用来声明基本数据类型,当然也可以声明一个对象,但需要通过 .value.xxx来访问,比较繁琐,并且它将被 reactive函数处理为深层的响应式对象;

  • ref也可以用于获取 DOM,具体用法如下:

3271675925441_.pic.jpg

2.reactive

用于创建响应式的对象(只能是对象),取值不用加.value 官网链接[www.javascriptc.com/vue3js/api/…] 具体细节如下:

  <div class="demo">
    <h2>姓名: {{name}}</h2>
    <h3>岗位: {{job.type}}</h3>
    <h3 v-show="job.workingAge">工龄: {{job.workingAge}}</h3>
    <h3 v-show="job.age">年龄:{{job.age}}</h3>
    <button @click="updateInfo()">更新</button>
  </div>
</template>
 
<script>
import { defineComponent, ref, reactive } from 'vue'
 
export default defineComponent({
  setup () {
    let name = ref('张三'); 
    let job = reactive({
      type: 'web前端',
      workingAge: 8
    })
    console.log('job', job)
    function  updateInfo() {
      name.value = '李四';
      job.type = 'JAVA';
      delete job.workingAge;// 删除工龄
      job.age = 18; // 增加年龄
      
    }
    // 返回一个对象(常用)
    return {
      name,
      job,
      updateInfo
    }
  }
})
</script>

3.toRef和toRefs toRef将对象中的某一个属性变成响应式,toRefs将对象中的所有属性转化为响应式

  <div class="demo">
  <div>{{person.name}}</div>
  <div>{{name}}</div>
  <div>{{sex}}</div>
  <div>{{age}}</div>
  </div>
</template>
 
<script>
import { defineComponent, ref, reactive,toRef, toRefs } from 'vue'
 
export default defineComponent({
    setup () {
    let person = reactive({
      name: '张三',
      sex: '男',
      job: {
        j1: {
          age: 18
        }
      }
    })
 
    // 下面是几种方式的不同点描述
    return {
      person, // 直接return出去,模板中使用不能直接使用name 需要 person.name,比较麻烦
      name1: person.name, // 解构后模板中直接使用name,但是属性不再具有响应性
      name2: toRef(person, 'name'), //将对象中的某一个值name属性变为响应式可以通过toRef来转变成ref对象
      ...toRefs(person), // 如果想让person中的属性都变为响应式可以使用toRefs批量转换成ref对象
      ...toRefs(person.job.j1)
    }
  }
})
</script>

总结:

reactive对比ref
从定义数据角度对比:
  • ref用来定义基本类型数据

  • reactive用来定义对象(或数组)类型数据

  • 备注:ref也可以用来定义对象(或数组)类型数据,它内部会自动通过reactive转为代理对象;

从原理角度对比:
  • ref通过Object.defineProperty()的get与set来实现响应式(数据劫持)

  • reactive通过使用Proxy来实现响应式(数据劫持),并通过Reflect操作源对象内部的数据。

从使用角度对比:
  • ref定义的数据:js操作数据需要.value,模板中读取时不需要.value

  • reactive定义的数据,操作与读取均不需要.value

toRef, toRefs:

作用: 创建一个ref对象,其value值指向另一个对象中的某个属性

语法: const name = toRef(person, 'name')

应用: 要将响应式对象中的某个属性单独提供给外部使用时

扩展: toRefs与toRef功能一致,但可以批量创建多个ref对象,语法: toRefs(person)

  • readonly 防止更改响应式对象
  • isRef 是用来检测ref类型的,如果是返回的是true,否则返回false .
  • isReactive 可以判断对象是否为响应式的。是用来检测reactive类型的,如果是返回的是true,否者返回false
  • isProxy 区分是哪 种方法创建的代理对象.
  • isReadonly 判断对象是否为 readonly(不可被修改) 创建。
  • shallowXXX 根据名称 可知,只将对象自身的浅层属性进行转换,深层属性保持不变。

setup语法糖简介

直接在script标签中添加setup属性就可以直接使用setup语法糖了。
使用setup语法糖后,不用写setup函数;组件只需要引入不需要注册;属性和方法也不需要再返回,可以直接在template模板中使用

       <template>
	<my-component @click="func" :numb="numb"></my-component>
       </template>
	<script lang="ts" setup>
		import {ref} from 'vue';
		import myComponent from '@/component/myComponent.vue';
		//此时注册的变量或方法可以直接在template中使用而不需要导出
		const numb = ref(0);
		let func = ()=>{
			numb.value++;
		}
	</script>

setup语法糖中新增的api

defineProps:子组件接收父组件中传来的props
defineEmits:子组件调用父组件中的方法
defineExpose:子组件暴露属性,可以在父组件中拿到

  • defineProps

父组件代码

        <template>
            <my-component @click="func" :numb="numb"></my-component>
	</template>
	<script lang="ts" setup>
		import {ref} from 'vue';
		import myComponent from '@/components/myComponent.vue';
		const numb = ref(0);
		let func = ()=>{
			numb.value++;
		}
	</script>

子组件代码

        <template>
            <div>{{numb}}</div>
	</template>
	<script lang="ts" setup>
		import {defineProps} from 'vue';
		defineProps({
			numb:{
				type:Number,
				default:NaN
			}
		})
	</script>
  • defineEmits

子组件代码

	<template>
		<div>{{numb}}</div>
		<button @click="onClickButton">数值加1</button>
	</template>
	<script lang="ts" setup>
		import {defineProps,defineEmits} from 'vue';
		defineProps({
			numb:{
				type:Number,
				default:NaN
			}
		})
		const emit = defineEmits(['addNumb']);
		const onClickButton = ()=>{
			//emit(父组件中的自定义方法,参数一,参数二,...)
			emit("addNumb");
		}
	</script>

父组件代码

    	<template>
		<my-component @addNumb="func" :numb="numb"></my-component>
	</template>
	<script lang="ts" setup>
		import {ref} from 'vue';
		import myComponent from '@/components/myComponent.vue';
		const numb = ref(0);
		let func = ()=>{
			numb.value++;
		}
	</script>

  • defineExpose

子组件代码

    	<template>
		<div>子组件中的值{{numb}}</div>
		<button @click="onClickButton">数值加1</button>
	</template>
	<script lang="ts" setup>
		import {ref,defineExpose} from 'vue';
		let numb = ref(0);
		function onClickButton(){
			numb.value++;	
		}
		//暴露出子组件中的属性
		defineExpose({
			numb 
		})
	</script>

父组件代码

    	<template>
		<my-comp ref="myComponent"></my-comp>
		<button @click="onClickButton">获取子组件中暴露的值</button>
	</template>
	<script lang="ts" setup>
		import {ref} from 'vue';
		import myComp from '@/components/myComponent.vue';
		//注册ref,获取组件
		const myComponent = ref();
		function onClickButton(){
			//在组件的value属性中获取暴露的值
			console.log(myComponent.value.numb)  //0
		}
		//注意:在生命周期中使用或事件中使用都可以获取到值,
		//但在setup中立即使用为undefined
		console.log(myComponent.value.numb)  //undefined
		const init = ()=>{
			console.log(myComponent.value.numb)  //undefined
		}
		init()
		onMounted(()=>{
			console.log(myComponent.value.numb)  //0
		})
	</script>

四.生命周期钩子

基于 setup 方法使用的生命周期钩子同样有对应更新,setup执行的顺序在beforeCreate之前

beforeCreate -> setup()
created -> setup()
beforeMount -> onBeforeMount
mounted -> onMounted
beforeUpdate -> onBeforeUpdate
updated -> onUpdated
beforeUnmount -> onBeforeUnmount
unmounted -> onUnmounted 原来的destroyed errorCaptured -> onErrorCaptured // 错误上报钩子,仅做了解
renderTracked -> onRenderTracked // { key, target, type } 仅做了解 renderTriggered -> onRenderTriggered // { key, target, type } 仅做了解

五. 侦听器 watch

侦听器主要是用来监听页面数据或者是路由的变化,来执行相应的操作,在 vue3里面呢,也有侦听器的用法,功能基本一样,换汤不换药的东西。 侦听器是常用的 Vue API 之一,它用于监听一个数据并在数据变动时做一些自定义逻辑,下面将列举侦听器在 Vue 中的使用方式。

  • watch 侦听器使用。 watch API 使用,至少需要指定两个参数: source 和 callback,其中 callback 被明确指定只能为函数,所以不同使用方式的差别其实只在 source 。
  <div>
    <h1>watch 侦听器</h1>
    <input v-model="num" />
    <br>
    <br>
    <button type="primary" @click="num++">num + 1</button>
  </div>
</template>
<script>
  import { watch, ref } from 'vue'
  export default {
    setup() {
      const num = ref(1)
      watch(num, (newVal, oldVal) => {
        console.log("新值:", newVal, "   旧值:", oldVal)
      })
      return { num, }
    }
  }
</script>
<style scoped>
</style>

上面的代码是页面上有一个数字,点击按钮一下,数字加一,然后侦听器侦听数字的变化,打印出侦听的最新值和老值。

image.png

OK。上边的案例就是 vue3 侦听器的简单案例,侦听器和计算属性一样,可以创建多个侦听器,这个是没有问题的,案例就不写了,和上一节讲的声明多个计算属性是一致的。如果有不明白的可以看一下我的上一篇博客。

上边我们说过这么一句话,watch API 至少需要指定两个参数: source 和 callback。通过上边的案例我们看到了, 确实是两个,source 是监听的数据,callback 是监听回调,那为啥说是至少呢?

对的,因为他还有第三个参数 —— 配置对象。

在 vue2 里面,我们打开页面就像让侦听器立即执行,而不是在第一次数据改变的时候才开始执行,这时候有一个参数叫 immediate ,设置了这个参数,创建第一次就执行,所以说呢,vue3 同样可以使用。

上面的案例刷新执行的时候发现,在点击按钮之前,也就是 num 创建的时候,侦听器是没有执行的,所以说呢,加上 immediate 参数,就可以让侦听器立即执行操作。

  <div>
    <h1>watch 侦听器</h1>
    <input v-model="num" />
    <br>
    <br>
    <button type="primary" @click="num++">num + 1</button>
  </div>
</template>
<script>
  import { watch, ref } from 'vue'
  export default {
    setup() {
      const num = ref(1)
      watch(num, (newVal, oldVal) => {
        console.log("新值:", newVal, "   旧值:", oldVal)
      }, {
        immediate: true
      })
      return { num, }
    }
  }
</script>
<style scoped>
</style>

我们看到,刷新完页面,还没有点击按钮让 num 加一的,控制台就有数据打印了,就是因为我们加了 immediate 为 true,让侦听器立即执行。控制台输出最新的值也就是我们初始化的值1,老的值没有,所以输出了 undefined。

image.png

  • 侦听器监听 reactive 下面来说说用来侦听对象的变化。
  <div>
    <h1>watch 侦听器</h1>
    <input v-model="num.age" />
    <br>
    <br>
    <button type="primary" @click="num.age++">num + 1</button>
  </div>
</template>
<script>
  import { watch, ref, reactive } from 'vue'
  export default {
    setup() {
      const num = reactive({
        name: '我是柠檬.',
        age: 10
      })
      watch(num, (newVal, oldVal) => {
        console.log(newVal, oldVal)
      })
      return { num }
    }
  }
</script>
<style scoped>
</style>

比如说上面代码,我们侦听 num 这个对象的变化。 image.png 看效果我们发下,在监听整个 reactive 响应式对象的时候,确实当里面的属性值发生改变了之后可以被侦听器检测到,但是 newVal 和 oldVal 的值都是新的,默认是10,点击之后,新值是 11 很正常,但是老值不应该是 10 吗?为什么这里老值和新值一样也是 11 呢?

这个不需要疑问哈,如果监听整个 reactive 数据的话,只能回调到最新的值,获取不到老的值。

那问题来喽,我就修改 age 属性,我就要获取 age 老的值怎么办?其实我们只需要监听 num 下面的 age 就可以了,先看下面的代码哈。

  <div>
    <h1>watch 侦听器</h1>
    <input v-model="num.age" />
    <br>
    <br>
    <button type="primary" @click="num.age++">num + 1</button>
  </div>
</template>
<script>
  import { watch, ref, reactive } from 'vue'
  export default {
    setup() {
      const num = reactive({
        name: '我是柠檬.',
        age: 10
      })
      watch(num.age, (newVal, oldVal) => {
        console.log(newVal, oldVal)
      })
      return { num }
    }
  }
</script>
<style scoped>
</style>

监听对象直接是 num.age, 监听年龄属性值,看一下效果。 image.png 刷新结果我们可以看到哈,我们啥都没干,侦听器直接报了一个警告给我们,啥意思呢,其实不能直接这样监听。

当我们需要监听某个对象属性的时候,我们不能直接对象点属性的方式进行监听,需要传入一个 getter 方法,也就是箭头函数进行监听,下面的代码是正确方式哈。

  <div>
    <h1>watch 侦听器</h1>
    <input v-model="num.age" />
    <br>
    <br>
    <button type="primary" @click="num.age++">num + 1</button>
  </div>
</template>
<script>
  import { watch, ref, reactive } from 'vue'
  export default {
    setup() {
      const num = reactive({
        name: '我是柠檬.',
        age: 10
      })
      watch(() => num.age, (newVal, oldVal) => { console.log(newVal, oldVal) }) return { num }
    }
  }
</script>
<style scoped>
</style>

保存刷新,我们发现,侦听器已经不报错了,而且我们点击按钮让 age 加一的时候,可以顺利的监听到 age 的变化,并且回调出最新值和上一次的值。 image.png 通过箭头函数,我们就可以实现对象属性的监听。

很多人说,vue2 在监听对象的时候需要对侦听器设置深度侦听,为什么 vue3 这个不需要呢?因为他监听响应式对象,默认就是深度监听。但是,如果监听的是深度嵌套对象或数组中的 property 变化时,仍然需要 deep 选项设置为 true。

看下面的案例,我们监听深层嵌套的 time 属性值。其实我觉得没大必要,不使用箭头函数其实可以。但是还是写一下吧。

  <div>
    <h1>watch 侦听器</h1>
    <input v-model="num.todo.time" />
    <br>
    <br>
    <button type="primary" @click="num.todo.time ++">num.todo.time + 1</button>
  </div>
</template>
<script>
  import { watch, ref, reactive, computed, } from 'vue'
  export default {
    setup() {
      const num = reactive({
        name: '我是柠檬.',
        age: 10,
        todo: {
          name: '弹吉他',
          time: 1
        }
      })
      watch(() => num, (newVal, oldVal) => {
        console.log(newVal.todo.time, oldVal.todo.time)
      })
      return { num }
    }
  }
</script>
<style scoped>
</style>

image.png 这个时候就可以加上 deep 深度监听。

  <div>
    <h1>watch 侦听器</h1>
    <input v-model="num.todo.time" />
    <br>
    <br>
    <button type="primary" @click="num.todo.time ++">num.todo.time + 1</button>
  </div>
</template>
<script>
  import { watch, ref, reactive, computed, } from 'vue'
  export default {
    setup() {
      const num = reactive({
        name: '我是𝒆𝒅.',
        age: 10,
        todo: {
          name: '弹吉他',
          time: 1
        }
      })
      watch(() => num, (newVal, oldVal) => {
        console.log(newVal.todo.time, oldVal.todo.time)
      }, { deep: true })
      return { num }
    }
  }
</script>
<style scoped>
</style>

就可以打印出来了,这种场景也不常用

image.png

  • 监听多个参数执行各自逻辑
      // 第一个
      watch(num, (newVal, oldVal) => {
        console.log(newVal, oldVal)
      })

      // 第二个
      watch(()=> boy.age, (newVal, oldVal) => {
        console.log(newVal, oldVal)
      })

  • 监听多个参数执行相同逻辑
  <div>
    <h1>watch 侦听器</h1>
    <input v-model="num.name" />
    <input v-model="num.age" />
    <br>
    <br>
    <button @click="num.todo.time ++">num.todo.time + 1</button>
  </div>
</template>
<script>
  import { watch, ref, reactive, computed, } from 'vue'
  export default {
    setup() {
      const num = reactive({
        name: '我是柠檬.',
        age: 10,
        todo: {
          name: '写代码',
          time: 1
        }
      })

      watch([() => num.name, () => num.age], (newVal, oldVal) => {
        console.log(newVal, oldVal)
      })
      return { num }
    }
  }
</script>
<style scoped>
</style>

image.png 如果我们不想让他返回数组可以这样改一下,可以了解一下

watch([() => num.name, () => num.age], ([newName, newAge], [oldName, oldAge]) => {
        console.log(newName, newAge, oldName, oldAge)
      })
      return { num }
    }

image.png