万字长文总结下 Vue3

vue3出来也有一段时间了,笔者目前在公司已经正式用 vue3 开发了两个项目,并且都已顺利上线了一段时间,现在就结合我这段时间的使用感受以及我学习vue3的一些理解,对vue3做个简单的总结。

Vue3

更快

按照官方的说法,vue3 比 vue2 性能方面提升了 1.2 ~ 2倍,性能方面的提升包括了以下优化

diff算法

vue3 对 diff 算法做了优化,加入了 PatchFlags 来动态的标记需要更新的部分,PatchFlags可以具体标记元素哪些部分可能会有更新,具体如下:

export const enum PatchFlags {
  TEXT = 1,  // 动态文字内容
  CLASS = 1 << 1, OB10 // 动态 class
  STYLE = 1 << 2, OB100 // 动态样式
  PROPS = 1 << 3, OB1000 // 动态 props
  FULL_PROPS = 1 << 4, OB10000 // 有动态的key,也就是说props对象的key不是确定的
  HYDRATE_EVENTS = 1 << 5, OB100000 // 合并事件
  STABLE_FRAGMENT = 1 << 6, OB1000000 // children 顺序确定的 fragment
  KEYED_FRAGMENT = 1 << 7, OB10000000 // children中有带有key的节点的fragment
  UNKEYED_FRAGMENT = 1 << 8, OB100000000 // 没有key的children的fragment
  NEED_PATCH = 1 << 9, OB1000000000 // 只有非props需要patch的,比如`ref`
  DYNAMIC_SLOTS = 1 << 10, OB10000000000 // 动态的插槽
  // 以下是特殊的flag,不会在优化中被用到,是内置的特殊flag
  // 表示他是静态节点,他的内容永远不会改变,对于hydrate的过程中,不会需要再对其子节点进行diff
  HOISTED = -1,
  // 用来表示一个节点的diff应该结束
  BAIL = -2,
}
复制代码

之前 vue2 在做 diff 时会对比全部的节点,然后再对有变化的节点进行更新

而 vue3 会先对元素做标记,标记需要更新的部分(比如一个元素只有class是动态的,其他都是写死静态的,那么就标记 class 的 PatchFlags),diff 时只对比需要更新的部分,不会对比全部,从而优化了性能。

事件缓存

vue3 还做了事件监听缓存(cacheHandlers),也就是说如果元素绑定的事件是不会改变的,事件会被缓存下来,下次创建渲染元素时,直接使用缓存的事件

以下是在 vue3 的编译网站( vue-next-template-explorer.netlify.app/#/ )尝试的代码

<button @click="clickFun">按钮</button>
复制代码
import { openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("button", {
    onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.clickFun && _ctx.clickFun(...args)))
  }, "按钮"))
}
复制代码

静态提升

vue3 会判断出哪些元素是静态的,不要更新的,比如下面的 .title元素,这些元素会被 vue 提升,只创建一次,后续更新不会再重新创建,只是直接复用,节省性能。而 vue2 中无论元素需不需要重新创建,每次渲染时都会重新创建一遍。

<template>
  <div>
    <div class='title'>静态数据</div>
    <div>
        {{ data }}
    </div>	
  </div>
</template>
复制代码

当然 vue3 的性能优化不止这些,这里只是简单说了几个有代表性的,而且 vue3 还在继续升级,优化

更小

vue3 采用按需编译的方式,全局和内部 API 已经被重构为支持 tree-shaking。

过去 vue2 使用的 Api 很多都是挂载在单个单个 Vue 对象的全局上,需要使用时通过 Vue.或者this.直接使用,但是有些时候,比如一些小的项目,我们可能用不到 vue 中的很多其他api,那么这些没使用的 api 照理是不应该打包进我们最终的项目中去的,当时 vue2 是会一起打包进去的。而到了 vue3 ,我们可以通过导入的方式,表明我们具体需要使用哪些功能,比如

import { nextTick } from 'vue'
复制代码

通过这种方式,其他未被使用的 api 会在最终打包中被 tree-shaking 清除,以达到更小的打包体积。

typescript的支持

在 vue2 中我们想使用 typescript 需要借助额外的包 vue-class-component 装饰器,而 vue3 是由typescript编写的,天然支持 typescript ,如果要让 TypeScript 正确推断 Vue 组件选项中的类型,需要使用 defineComponent 全局方法定义组件。

import { defineComponent } from 'vue'

const Component = defineComponent({
  // 已启用类型推断
})
复制代码

生命周期

vue2 和 vue3 的生命周期图示

life.jpg

通过比对上面的图片可以发现两者存在以下区别

我们从上往下看

创建实例的区别

vue2和vue3在创建实例的方面有了很大的变化,我们先看下面创建实例的代码

  • vue2创建实例

    import Vue from 'vue'
    import App from './App.vue'
    import router from './router'
    import store from './store'
    
    new Vue({
      router,
      store,
      render: (h) => h(App),
    }).$mount('#app')
    复制代码
  • vue3创建实例

    import { createApp } from "vue";
    import App from "./App.vue";
    import router from "./router";
    import store from "./store";
    
    createApp(App).use(ElementPlus).use(store).use(router).mount("#app");
    复制代码

通过代码可以发现:vue2是通过 new 来创建Vue实例,Vue是一个构造函数,通过Vue构造的对象会同享所有的全局配置

import GlobalButton from './components/GlobalButton.vue'
Vue.compoent('global-button', GlobalButton)

const appA = new Vue().$mount('#app-A')
const appB = new Vue().$mount('#app-B')
复制代码

如以上代码,global-button 组件已经全局注册在Vue上,appA和appB都可以使用这个组件,而我们无法做到例如只在appA中全局注册global-button 组件,所以从某种意义上讲,vue2并没有所谓应用的这种概念。

而在vue3中

import { createApp } from "vue";
import appA from "./appA.vue";
import appB from "./appB.vue";
import GlobalButton from './components/GlobalButton.vue'

const appA = createApp(appA)
appA.component('global-button', GlobalButton)
appA.mount("#app-A");

const appB = createApp(appB).mount("#app-B");
复制代码

createApp这个工厂函数会返回提供应用上下文的应用实例,允许链式,注意:mount要放在最后,因为mount不会返回应用本身,其返回的是根组件实例。

该实例是独立的,与其他实例互不干扰,不受其他实例影响。

Has ’el‘ option?的区别

从图中可以发现,vue2 的图 有一个 Has ’el‘ option? 的棱形,vue2在这些完beforeCreate 和 created 后判断有没有挂载在 el 元素上(也就是$mount(el)),有则继续执行,没有则等到有了后才继续执行,而在 vue3 中,可以看到 el 在一开始的方块里(app.mount(el))就有了,可以理解为 vue3 是在一切准备好再开始的。

el处理的区别

走到 “ Has ‘template’ option? ” 在 NO 的分支时:

vue2 是 Compile el's outerHTML as template。而vue3 是 Compile el's innerHTML as template

这里可以发现一个是 outerHTML ,一个是 innerHTML,二者区别可以看以下例子,可以发现,outerHTML包括了元素本身的标签

<div id="app">
   <span>文本</span>
</div>

<script>
	let app = document.querySelector('#app')
	console.log(app.outerHTML) // <div id="app"><span>文本</span></div>
	console.log(app.innerHTML) // <span>文本</span>
</script>
复制代码

所以在生命周期的图中继续往下我们可以发现

vue2 是Create vm.$el and replace "el" with it 的方式也就是替换的方式

vue3 是Create app.$el and append it to el的方式也就是插入的方式

对比vuecli创建的vue2和vue3的项目中,我们可以发现 App.vue 文件vue2中有 id为app的div元素,而vue3则没有

// vue2 App.vue 文件
<template>
  <div id="app">
    <router-view />
  </div>
</template>

<script>
export default {
}
</script>


// vue3 App.vue 文件
<template>
  <router-view />
</template>

<script>
import { defineComponent } from "vue";
export default defineComponent({
});
</script>
复制代码

组件销毁生命周期的区别

从图中我们可以发现 beforeDestroy 被改名为了 beforeUnmount;destroyed 被改名为 unmounted

组合式API

vue3的重大更新之一,与vue2的以data、methods、watch等构成的选项式 api 不同,组合式API可以以更自由,更接近原生的书写方式提高代码的灵活性、重用性,在大型项目中,这一点显得尤为重要。

看以下vue2 和 vue3 的简单例子

// vue2
<template>
  <div>
    <div class="">
      {{ fruit }}
    </div>
    <el-button @click="changeFruit">修改fruit值</el-button>
    <div class="">
      {{ animal }}
    </div>
    <el-button @click="changeAnimal">修改animal值</el-button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      fruit: 'apple', // 逻辑1
      animal: 'monkey', // 逻辑2
    }
  },
  methods: {
    changeFruit() {
      this.fruit = 'banner'
    },  // 逻辑1
    changeAnimal() {
      this.animal = 'tiger'
    },  // 逻辑2
  },
}
</script>
复制代码
// vue3
<template>
  <div>
    <div class="">
      {{ fruit }}
    </div>
    <el-button @click="changeFruit">修改fruit值</el-button>
    <div class="">
      {{ animal }}
    </div>
    <el-button @click="changeAnimal">修改animal值</el-button>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from "vue";

export default defineComponent({
  setup() {
    let fruit = ref<string>("apple"); // 逻辑1
    function changeFruit() {
      fruit.value = "banner";
    } // 逻辑1

    // ------------------------------------------
    let animal = ref<string>("monkey"); // 逻辑2
    function changeAnimal() {
      animal.value = "tiger";
    } // 逻辑2

    return {
      fruit,
      changeFruit,
      animal,
      changeAnimal,
    };
  },
});
</script>
复制代码
// 也可以这样写
<script lang="ts">
import { defineComponent, ref } from "vue";
function fruitSetup(){
	let fruit = ref<string>("apple"); // 逻辑1
    function changeFruit() {
      fruit.value = "banner";
    } // 逻辑1
    return {
        fruit,
        changeFruit
    }
} 
    
function animalSetup(){
	let animal = ref<string>("monkey"); // 逻辑2
    function changeAnimal() {
      animal.value = "tiger";
    } // 逻辑2
    return {
        animal,
        changeAnimal
    }
}     
export default defineComponent({
  setup() {
    return {
      ...fruitSetup(),
      ...animalSetup(),  
    };
  },
});
</script>
复制代码

可以发现,组合式 API 可以摆脱data,methods等选项的拘束,将相同逻辑代码写在一块,便于后续维护和迭代。上面的例子只是非常简单的示例,在日常开发中,特别是大型项目中,一个大的组件可能有非常多的逻辑关注点,这时,选项式API会加大代码的阅读难度,使得维护变得困难,逻辑点的分离也隐藏了潜在的逻辑问题,增加了隐藏风险。组合式 API 的诞生解决了以上的问题。

参考下图,按颜色划分的逻辑关注点

setup.PNG

setup

setup是组件的选项,是组合式 API 的入口函数,在beforeCreate钩子之前(组件被创建之前),props被解析之后执行。

在setup函数中访问 this 为 undefined。也就是说不需要再像 vue2 一样写很多的 this 。

那么为什么 vue2 需要 this,因为受到作用域的限制,而组合式API 可以摆脱这种限制,让我们可以以更加灵活,自由的方式将组件需要的各种变量,函数等等组合到需要的组件中去,也进一步提高了代码的复用性。

<template>
  <div @click="changeDataText">
    {{ dataText }}
  </div>
  <div @click="changeDataInfo">
    {{ dataInfo }}
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
import { commonSetup, commonSetupSecond } from '@/common/commonSetup.ts'
export default defineComponent({
  setup(){
    const { dataText, changeDataText } = commonSetup()
    const { dataInfo, changeDataInfo } = commonSetupSecond()
    return {
      dataText, 
      changeDataText,
      dataInfo, 
      changeDataInfo
    }
  },
})
</script>
复制代码

说到vue代码的复用性,我们自然而然就会想到 Mixin ,这个让我们又爱又恨的东西,首先 Mixin 确实很方便,在vue2 中我们可以用它将很多相同功能的代码写在一起供多个组件去使用,但是真实的情况是一个组件往往不会用到其使用的 Mixin 中的全部功能,但是为了图方便,有些时候大家可能会选择直接导入 Mixin 简单粗暴,但这也为我们组件引入了很多不需要的功能,甚至一个组件引入多个 Mixin,造成 Mixin 滥用,再者,Mixin的引用往往是隐式导入,比如在组件中看到一个函数的调用,但却不知道这个函数是从哪里来的,等等 Mixin 的问题造成后续接手项目的人一头雾水,增加了项目的维护成本。

而组合式API 完全可以取代 mixin 的作用,我们可以将公用部分抽离在一个函数,也就是 hooks,在各个组件中引入我们需要的变量,函数等等,如上面的代码所示,当然vue3中也支持Mixin,但官方文档已经表示不推荐使用,vue3的项目,Mixin应该能不用就坚决不用,用组合式 API 不香吗。

setup函数的参数

setup函数有两个参数

  • props

    与使用选项式 API 时的 this.$props 类似, props 对象仅包含显性声明的 prop,并且,所有声明了的 prop,不管父组件是否向其传递了,都将出现在 props 对象中。其中未被传入的可选的 prop 的值会是 undefined。props是响应式的,但是不能解构使用,否则会失去响应式。

  • context :

    context是一个上下文对象,其包含了attrs、slots、emit、expose四个属性,

    • attrs 等同于 我们熟悉的 $attrs

    • slots 等同于 $slots

    • emit 等同于 $emit

    • expose 是 Vue 3.2 版本 中新增的一个函数,expose 只能被调用一次,可以通过该函数指定组件向外暴漏的 property,让组件外通过 ref$parent$root等获取的该组件实例可以使用指定的property

      <template>
        <div></div>
      </template>
      
      <script lang="ts">
      import { defineComponent } from 'vue'
      export default defineComponent({
        name: 'ExposeComponent',
        setup(props, { expose }){
          function exposeExample(){
            console.log('外部方法')
          }
          function internalExample(){
            console.log('内部方法')
          }
          // 暴漏exposeExample给外部访问
          expose({
            exposeExample
          })
          // 组件内部可以访问的
          return {
            internalExample
          }
        },
      })
      </script>
      复制代码

      这样组件外部就访问不了 internalExample 方法了

组合式API生命周期钩子

生命周期钩子大概有以下几种

  • onBeforeMount

  • onMounted

  • onBeforeUpdate

  • onUpdated

  • onBeforeUnmount

  • onUnmounted

  • onActivated

  • onDeactivated

    .....

组合式API的生命周期钩子和选项式 API 的生命周期选项类似,只是在前面加了”on“单词,对应的触发逻辑是一样的

组合式API的生命周期钩子不需要 beforeCreate 和 created ,也就是说没有 onBeforeCreate 和 onCreated ,因为setup 执行在这两者之前,需要在beforeCreate 和 created 中执行的情况直接写在 setup() 中执行即可。

import { onMounted, onUnmounted } from 'vue'
const MyComponent = {
  setup() {
    onMounted(() => {
      console.log('mounted')
    })
    onUnmounted(() => {
      console.log('unmounted')
    })
  }
}
复制代码

响应式API

vue的特点之一就是其双向绑定的特性,方便。

vue2 中是使用 Object.defineProperty 来对数据进行劫持,实现双向绑定

let definePropertyObj = {};

Object.defineProperty(definePropertyObj, 'prop', {
  configurable: false,
  set: (v) => {
    definePropertyObj._v = v
  },
  get: () => {
    return definePropertyObj._v
  }
});
复制代码

可以发现,使用 Object.defineProperty 来对数据进行劫持,由于 Vue 在实例初始化期间执行 getter/setter 转换过程,因此一开始 vue 就需要拿到数据的 property ,以便 Vue 对其进行转换并使其具有响应式

<template>
  <div>
    <div id='name'>{{ obj.name }}</div>
    <div class="footer">
      <button @click="changeName">修改name</button>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      obj: {},
    }
  },
  methods: {
    changeName() {
      this.obj.name = 'name值'
    },
  },
}
</script>
复制代码

如上面的例子,点击按钮之后,“#name”的元素内容并不会发生改变,除非一开始data中的 obj 有 name 属性

所以,vue2 无法检测到 property 的添加或删除,vue2也提供了 vue.$setvue.$delete 的 Api 来解决以上的问题

而vue3 基于ES2015 Proxy对引用类型实现了"深层"的响应式转换

let proxyObj = {};
let proxy = new Proxy(proxyObj, {
  set: (obj, prop, value) => { 
    proxyObj[prop] = value; 
  },
  get: (obj, prop) => { 
    return obj[prop] 
  },
  deleteProperty: () => {}
});
复制代码

以此,vue3 可以实现监听对象属性的新增和删除,那么 vue3 是否放弃了Object.defineProperty 全部改用 proxy 呢,当然不是,往下看

vue3 中提供了各种响应式 API 来转换我们的变量。

reactive

import { reactive } from 'vue'
const state = reactive({value: 'vue'})
console.log(state)
复制代码

通过打印我们可以发现

v5.PNG

返回一个 Proxy 对象

结合上面讲到的proxy的例子,我们可以发现 proxy 返回原始对象的响应式对象,reactive 基于proxy 返回对象的响应式副本,于是,上面中的 obj 的增删改都是响应式的,reactive的参数只能是引用类型。

ref

接受一个内部值并返回一个响应式且可变的 ref 对象。

const value = ref('vue')
console.log(value)
复制代码

通过打印我们可以发现

v4.png

返回一个 RefImpl 对象,我们就简称它为 ref 对象吧

ref 对象暴漏出有且仅有一个 value属性 ,指向该内部值。

import { ref } from 'vue'
const str = ref('vue2')
console.log(str.value) // vue2
str.value = 'vue3'
console.log(str.value) // vue3
复制代码

当 ref 对象在setup中返回并在模板中访问时,它会自动浅层次解包内部值,也就是不需要 .value 访问, 只有访问嵌套的 ref 时需要在模板中添加 .value,看下官网的例子

<template>
  <div>
    <span>{{ count }}</span>
    <button @click="count ++">Increment count</button>
    <button @click="nested.count.value ++">Nested Increment count</button>
  </div>
</template>

<script>
  import { ref } from 'vue'
  export default {
    setup() {
      const count = ref(0)
      return {
        count,

        nested: {
          count
        }
      }
    }
  }
</script>
复制代码

如果 ref 的参数是对象,则 vue 内部会自动转为使用 reactive 转化

const state = ref({
  data: 'vue'
})
console.log(state)
console.log(state.value)
复制代码

通过依次打印我们可以发现 state.value 的值是一个 Proxy 对象

v6.PNG

ref 对比 reactive

从上面我们知道 reactive 是返回一个 Proxy 对象,使用 Proxy 进行数据劫持

而 ref 包裹的值如果是基本类型时(复杂类型自动转为 reactive),使用 Object.defineProperty 进行数据劫持

那么 Proxy 和 Object.defineProperty 性能的比较怎么样呢

通过看大佬的案例 thecodebarbarian.com/thoughts-on…

const Benchmark = require('benchmark');
let suite = new Benchmark.Suite

let obj = {};
let definePropertyObj = {};
let proxyObj = {};

Object.defineProperty(definePropertyObj, 'prop', {
  configurable: false,
  set: v => definePropertyObj._v = v
});

let proxy = new Proxy(proxyObj, {
  set: (obj, prop, value) => { proxyObj[prop] = value; }
});

suite.add('normal', function() {
  obj.prop = 'gd';
})
  .add('definePropertyObj', function() {
    definePropertyObj.prop = 'gd'
  })
  .add('proxyObj', function() {
    proxy.prop = 'gd'
})
  .on('cycle', function(event) {
      console.log(String(event.target));
  })
  .on('complete', function() {
      console.log('Fastest is ' + this.filter('fastest').map('name'));
  })
  .run({ 'async': true });
复制代码
/*
normal x 839,613,391 ops/sec ±1.93% (90 runs sampled)
definePropertyObj x 846,333,142 ops/sec ±0.78% (91 runs sampled)
proxyObj x 926,137 ops/sec ±23.25% (75 runs sampled)
Fastest is definePropertyObj
*/
复制代码

可以发现,Object.defineProperty 的性能是远高于 proxy 的,Vue3 用 proxy 也是为了监听对象属性的新增和删除。所以在实际使用中,建议优先使用ref,在vue3.2 中,官方对 ref 做了优化,读取速度提高约 260%,写入速度提高约 50%。

插一句,在对象的属性还是对象的情况,vue2 在对对象做响应式转换时,是通过递归对象使用Object.defineProperty将子对象变成也响应式。而 vue3 则是做了惰性处理,只有在该对象属性被访问时才执行操作将其转为响应式。这也是 vue3 做的一个性能优化处理。

isRef

检查值是否为一个 ref 对象。

import { ref, isRef } from 'vue'
const str = 'vue2'
const refStr = ref('vue3')
console.log(isRef(str)) // false
console.log(isRef(refStr)) // true
复制代码

unref

如果参数是一个 ref 对象,则返回内部值(value值),否则返回参数本身。

import { ref, unref } from 'vue'
const str = 'vue2'
const refStr = ref('vue3')
console.log(unref(str)) // 'vue2'
console.log(unref(refStr)) // 'vue3'
复制代码

toRef

为响应式对象的某个属性(property)新建一个 ref 对象,ref 可以被传递,它会保持对其源 property 的响应式连接,什么意思呢,可以看下面的例子

const states = reactive({
  num: 0,
});

const numRef = toRef(states, "num");
console.log(states.num); // 0
console.log(numRef.value, isRef(numRef)); // 0 true

numRef.value++;
console.log(numRef.value); // 1
console.log(states.num); // 1

states.num++;
console.log(numRef.value); // 2
console.log(states.num); // 2
复制代码

如果 toRef 对普通对象使用呢

const obj = {
  str: 'vue'
}
const strRef = toRef(obj, 'str')
console.log(strRef.value) // vue
function changeStrRef(){
  strRef.value = 'vue3'
  console.log(obj) // { str: "vue3" }
  console.log(strRef.value) // vue3
}
function changeObjStr(){
  obj.str = 'vue2'
  console.log(obj) // { str: "vue2" }
  console.log(strRef.value) // vue2
}
复制代码

可见,普通对象也可以创建,可是页面并不会跟着响应

v2.gif

toRefs

顾名思义,上面的 toRef 的复数版,也就是为响应式对象的所有属性(property)新建一个 ref 对象

const states = reactive({
  num: 0,
  figure: 1,
});
const { num, figure } = toRefs(states); // 对 toRefs 返回的值进行结构并不会影响其响应性
console.log(states.num); // 0
console.log(states.figure); // 1
console.log(num.value); // 0
console.log(figure.value); // 1
states.num++;
console.log(num.value); // 1
figure.value++;
console.log(states.figure); // 2
复制代码

结合 reactive 和 toRefs 可以便利地在template中引用响应式数据

<template>
  <div>
    <div>姓名:{{ name }}</div>
    <div>性别:{{ sex }}</div>
    <div>年龄:{{ age }}</div>
  </div>
</template>
<script lang="ts">
import { defineComponent, reactive } from "vue";

export default defineComponent({
  name: "responsive",
  setup() {
    const state = reactive({
    	name: 'Paul',
    	sex: '男',
    	age: 18
  	})

  	return {
    	...toRefs(state)
  	}
  },
});
</script>
复制代码

这里可能有人有疑问,setup里直接

return {
   ...state
}
复制代码

不行吗?

当然不行,上面的写法相当于

return {
   name: state.name,
   sex: state.sex,
   age: state.age,
}
复制代码

相当于暴漏出去简单的字符串和数值,这样就没有响应式了,也就是state里的属性改变时,视图数据不会跟着改变。

readonly

readonly 可以返回一个对象的只读代理,并且是深层的只读(与 const 的区别)。

readonly 是对象,可以是响应式对象或普通对象,也可以是 ref 对象。

<script lang="ts">
import { defineComponent, readonly, onMounted } from "vue";
export default defineComponent({
  name: "readonly",
  setup() {
    const readonlyObj = readonly({
        value: 'apple'
    });

    onMounted(() => {
      readonlyObj.value = 'banner';
      console.log(readonlyObj.value); // apple
    });
  },
});
</script>
复制代码

以上代码会报下图的错误

v1.PNG

isProxy、isReactive、isReadonly

  • isProxy 并不是判断是否new Proxy对象,而是检查对象是否是由 reactive 或 readonly 创建的 proxy,返回boolean值

    const state = reactive({
      num: 0
    })
    const readonlyObj = readonly({
      num: 0
    });
    const refObj = ref({
      num: 0
    })
    const numRef = ref(0)
    let proxyObj = Object.create(null);
    let newProxy = new Proxy(proxyObj, {});
    console.log(isProxy(state)) // true
    console.log(isProxy(readonlyObj)) // true
    console.log(isProxy(numRef)) // false
    console.log(isProxy(refObj)) // false
    console.log(isProxy(refObj.value)) // true  // 这里可以验证 vue3 中ref参数是对象时,自动调用reactive
    console.log(isProxy(newProxy)) // false
    复制代码
  • isReactive

    检查对象是否是由 reactive创建的响应式代理。

    const state = reactive({
      num: 0
    })
    const readonlyObj = readonly({
      num: 0
    });
    const readonlyState = reactive(state)
    const refObj = ref({
      num: 0
    })
    console.log(isReactive(state)) // true
    console.log(isReactive(readonlyObj)) // false
    console.log(isReactive(refObj)) // false
    console.log(isReactive(refObj.value)) // true
    console.log(isReactive(readonlyState)) // true
    复制代码

    上面代码可以发现,如果 readonly 的参数是 reactive 创建的返回对象,则也可以通过 isReactive 检查

  • isReadonly

    检查对象是否是由 readonly创建

    const readonlyObj = readonly({
      num: 0
    });
    console.log(isReadonly(readonlyObj)) // true
    复制代码

toRaw

返回 reactive 或 readonly 代理的原始对象。我们都知道,Proxy 是根据原始对象创建的 proxy 代理对象,reactive 和 readonly是基于Proxy实现的,toRaw 可以返回其原始对象

const obj = {}
const reactiveObj = reactive(obj)
console.log(toRaw(reactiveObj) === obj) // true
复制代码

Computed

computed 默认接收一个 getter 函数,返回一个不可修改的响应式 ref 对象。

computed的参数也可以是一个包括 get 和 set 函数的对象,返回一个可以修改的ref 对象。

注意,computed 返回的是 ref 对象,那么在setup中访问修改计算属性的值时,需要加上 .value。

<template>
  <div>
    <div>
      {{ num }}
    </div>
    <div>
      {{ result }}
    </div>
    <div>
      {{ setResult }}
    </div>
    <el-button @click="addNum">num增加1</el-button>
    <el-button @click="setNum">设置setResult</el-button>
  </div>
</template>

<script lang="ts">
import { computed, defineComponent, ref } from "vue";

export default defineComponent({
  name: "computed",
  setup() {
    let num = ref<number>(0);

    const result = computed(()=>num.value+1)

    const setResult = computed({
      get: () => num.value+2,
      set: (val) => num.value = num.value - val
    })

    function setNum (){
      setResult.value = 1
    }

    function addNum(){
      num.value++
    }

    return {
      num,
      result,
      addNum,
      setResult,
      setNum,
    };
  },
});
</script>
复制代码

v7.gif

Watch

和之前的 this.$watch 等效

import { watch } from vue
复制代码

watch 可以接收三个参数,

第一个参数是监听源,监视源可以是一个getter函数、一个ref、一个响应对象或这些类型的数组,第二个参数是包含监听源新旧两个参数的回调函数,第三个参数是options,也就是我们熟悉的 deep 和 immediate 参数

单个数据源

<script lang="ts">
import { defineComponent, ref, watch } from "vue";
export default defineComponent({
  name: "watch",
  setup() {
    let num = ref<number>(0);

    watch(num, (newVal, oldVal) => {
        console.log('监听到修改(watch: num)', newVal, oldVal)
      }
    )

    watch(()=>num.value, (newVal, oldVal) => {
        console.log('监听到修改(watch: ()=>num.value)', newVal, oldVal)
      }
    )

    num.value++ // 监听到修改(watch: num) 1 0
      			// 监听到修改(watch: ()=>num.value) 1 0
    return {
      num,
    };
  },
});
</script>
复制代码

上面代码执行 num++ 后可以发现,打印的值其实是一样的

watch监听reactive

<script lang="ts">
import { defineComponent, ref, watch } from "vue";
export default defineComponent({
  name: "watch",
  setup() {
    const state = reactive({
      name: "杰克",
    });

    watch(
      state,
      (newValues, prevValues) => {
        console.log(newValues, prevValues);
        console.log(newValues.name, prevValues.name);
      }
    );

    state.name = "麦克";
  },
});
</script>
复制代码

按照上面的例子,如果直接监听 state ,我们打印发现 newValues, prevValues 其实是一样的,

v9.PNG 这时候我们可以将上面代码修改一下

watch(
   () => state.name,
   (newValues, prevValues) => {
      console.log(newValues, prevValues);
   }
);
复制代码

v10.PNG

这时候可以发现,我们要的效果就出来了。

多数据源

watch也可以通过传递数组来实现监听多个数据

let surnames = ref("");
let name = ref("");

watch([surnames, name], (newValues, prevValues) => {
    console.log(newValues, prevValues);
});
 
surnames.value = "Michael"; // logs: ["Michael", ""]  ["", ""]
name.value = "Jackson"; // logs: ["Michael", "Jackson"]  ["Michael", ""]
复制代码

watchEffect

vue3 新增的函数,官方对它的解释是:立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。

先看个示例

<template>
  <div>
    <el-button @click="changeAppleAmount">苹果加一</el-button>
    <el-button @click="changeBannerAmount">香蕉加一</el-button>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, watchEffect } from "vue";
export default defineComponent({
  name: "watchEffect",
  setup() {
    let appleAmount = ref(0);
    let bannerAmount = ref(0);

    watchEffect(() =>
      console.log(
        `苹果一共${appleAmount.value}个,香蕉一共${bannerAmount.value}个`
      )
    );

    function changeAppleAmount() {
      appleAmount.value++;
    }

    function changeBannerAmount() {
      bannerAmount.value++;
    }

    return {
      changeAppleAmount,
      changeBannerAmount,
    };
  },
});
</script>
复制代码

v8.gif

可以发现 watchEffect 里的函数一开始就会执行一遍,然后,watchEffect 里的函数涉及的值发生变化时,watchEffect就会再次执行。

那么这个函数有点像是 夹在 Computed 和 Watch 中间的产物,它既不像 Computed 一样需要 return 某个值,也不像 Watch 一样需要 显式的写出监听源,同时他也无法拿到变化的值的前后旧值和新值。

watch 和 watchEffect 停止监听

watch 和 watchEffect 会在所在组件卸载时停止监听,但是我们可以通过调用其返回值来手动停止监听

<template>
  <el-card>
    <el-button @click="quantity++">数量加一</el-button>
    <el-button @click="stopWatchEffect">停止watchEffect</el-button>
    <el-button @click="stopWatch">停止watch</el-button>
  </el-card>
</template>

<script lang="ts">
import { defineComponent, ref, watchEffect, watch } from "vue";

export default defineComponent({
  name: "watchEffect",
  setup() {
    const quantity = ref(0);

    const watchEffectStop = watchEffect(() =>
      console.log(`watchEffect: ${quantity.value}`)
    );

  	const watchStop = watch(quantity, (newValue, oldValue) =>
    	console.log(`watch: ${newValue}`)
  	);

    function stopWatchEffect() {
      watchEffectStop(); // 停止watchEffect的监听
    }

    function stopWatch() {
      watchStop(); // 停止watch的监听
    }

    return {
      quantity,
      stopWatchEffect,
      stopWatch,
    };
  },
});
</script>
复制代码

v10.gif

模板引用(ref)

在 选项式API 中我们可以通过 ref 来获取指定的元素

<template>
  <div class="test" ref="test"></div>
</template>

<script>
export default {
  mounted() {
    let test = this.$refs.test
    console.log(test)
  },
}
</script>
复制代码

在组合式 API 中使用 ref,可以看一个小例子

<template>
  <el-form
    ref="ruleFormRef"
    :model="ruleForm"
    :rules="rules"
    label-width="120px"
    class="demo-ruleForm"
  >
    <el-form-item label="名称" prop="name">
      <el-input v-model="ruleForm.name"></el-input>
    </el-form-item>
    <el-form-item>
      <el-button @click="submitForm()">Create</el-button>
    </el-form-item>
  </el-form>
</template>

<script lang="ts">
import { defineComponent, ref } from "vue";
import { ElForm } from "element-plus";
export default defineComponent({
  setup() {
    const ruleFormRef = ref<InstanceType<typeof ElForm>>();

    const rules = {
      name: [{ required: true }],
    };

    const ruleForm = ref({
      name: "",
    });

    function submitForm() {
      ruleFormRef.value?.validate((valid) => {
        if (valid) {
          alert("submit!");
        } else {
          console.log("error submit!!");
          return false;
        }
      });
    }

    return {
      ruleForm,
      ruleFormRef,
      rules,
      submitForm,
    };
  },
});
</script>
复制代码

这里我们在setup中返回的ruleFormRef暴漏出去,并将其应用在div的 ref 属性值中,注意,这里的 ref 中前面不能加冒号,因为如果这里加了冒号,ref 的值就是空 ,不加就是字符串的 “ruleFormRef”。在 vue3 的虚拟DOM补丁算法中,如果 VNode 的 ref 键对应于渲染上下文中的 ref,则vue3会将该 VNode 的相应元素或组件实例分配给该 ref 的值,这是在虚拟 DOM 挂载/打补丁过程中执行的,因此模板引用只会在初始渲染之后获得赋值。

作为模板使用的 ref 的行为与任何其他 ref 一样:它们是响应式的,可以传递到 (或从中返回) 复合函数中。

route router

在组合式 api 中使用route router时,可以使用 useRoute, useRouter

下面代码 useRoute() 和 useRouter() 的 route 和 router 使用上基本和之前的 r o u t e route router 一致

<script lang="ts">
    import { defineComponent, watch } from 'vue'
	import { useRoute, useRouter } from 'vue-router'
	export default defineComponent({
		setup() {
    		const route = useRoute()
    		const router = useRouter()
            watch(
  				() => route.path,
  				() => {
                    ...
  				},
  				{ immediate: true }
			)    
            function goPath() {
      			router.push(path)
    		}
    	}
    })
</script>	
复制代码

vuex

在组合式 api 中使用 store 时,也可以使用 useStore,useStore() 返回的 store 使用上和vue2 的 $store 一致

<script lang="ts">
    import { defineComponent } from 'vue'
    import { useStore } from 'vuex'
    export default defineComponent({
         setup() {
    			const store = useStore()
    			const companyInfo = computed(() => store.state.user.companyInfo)
    			return {
      				companyInfo,
    			}
  		},
    })
</script>    
复制代码

指令

自定义指令

vue2的自定义指令钩子函数

  • bind:只调用一次,指令第一次绑定到元素时调用。
  • inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
  • update:所在组件 VNode 更新时调用。
  • componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
  • unbind:只调用一次,指令与元素解绑时调用。

vue3 更新了自定义指令的钩子函数

  • created:在绑定元素的 attribute 或事件监听器被应用之前调用。
  • beforeMount:当指令第一次绑定到元素并且在挂载父组件之前调用。(同vue2的bind)
  • mounted:在绑定元素的父组件被挂载后调用。(同vue2的inserted)
  • beforeUpdate:在更新包含组件的 VNode 之前调用。
  • updated:在包含组件的 VNode 及其子组件的 VNode更新后调用。(同vue2的componentUpdated)
  • beforeUnmount:在卸载绑定元素的父组件之前调用。
  • unmounted:当指令与元素解除绑定且父组件已卸载时,只调用一次。(同vue2的unbind)

可以发现, vue3 在 vue2 的基础上扩展了更多的钩子,丰富了指令的应用场景,在命名上也更加让人容易理解。

vue3 的钩子函数的参数还是和以前一样(即 elbindingvnodeprevVnode)

其他更新

多根节点的组件的支持

vue2中组件的 template 中一定需要一个根节点组件,而到了 vue3 组件可以包含多个根节点,但是我们需要定义 attribute 应该分布在哪个根节点组件,不然比如我们引用该组件,在组件标签上定义 class 就不知道这个class 是定义在哪个根节点上了

<template>
  <p class='header'>...</p>
  <div v-bind="$attrs">...</div>
  <div>...</div>
</template>
复制代码

注意:但是vue指令和 attribute 不同,不会通过 $attrs 被传入元素中,当指令被用与多根节点的组件时,指令会被忽略,且vue会抛出警告

Teleport

类似 React 的 Portal,是一个标签,可以自由控制元素渲染在哪个父节点下

<template>
  <div>
    <div>组件内</div>
    <teleport to='body'>
      <div class="teleport">嵌入body</div>
    </teleport>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
})
</script>
复制代码

v3.png

定义自定义事件 emits

vue的自定义事件我们已经很熟悉了,在vue3中自定义事件需要在emits选项中定义,如果不定义,虽然也可以执行,不过vue会发出警告,vue希望我们定义所有发出的事件,以便更好地记录组件应该如何工作。

而且如果emits定义了原生事件,将使用组件中的事件替代原生事件。

在 vue2 中,我们可以通过 .native 修饰符将原生事件绑定到子组件的根元素上,而 vue3 已经移除了v-on.native修饰符,我们可以在 emits 选项上不定义对应的原生事件,则原生事件默认就绑定到组件根元素中

// 父组件
<template>
  <children @focus="parentsEvent" />
</template>

<script lang="ts">
import { defineComponent } from "vue";
import children from "./components/children.vue";
export default defineComponent({
  components: { children },
  setup() {
    function parentsEvent() {
      console.log("子元素聚焦");
    }
    return {
      parentsEvent,
    };
  },
});
</script>

// 子组件
<template>
  <input type="text" />
</template>

<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
  name: "children",
  emits: [], // 这里不定义 emits: ["focus"],那么子组件的 input 元素聚焦时就会触发父组件的 parentsEvent 事件
});
</script>
复制代码

移除过滤器 filter

其实 filter 完全可以用 methods 或是 computed 代替

移除 .sync

正常情况下,我们不能修改接收到的prop值,在 vue2 中,我们可以使用 .sync 修饰符来对一个prop进行”双向绑定“

// 父组件
<template>
  <div>
    <myDialog :dialogVisible.sync="visible" />
    <el-button @click="showDialog">打开弹窗</el-button>
  </div>
</template>

<script>
import myDialog from './components/myDialog.vue'
export default {
  components: { myDialog },
  data() {
    return {
      visible: false,
    }
  },
  methods: {
    showDialog() {
      this.visible = true
    },
  },
}
</script>

// 子组件
<template>
  <el-dialog
    title="提示"
    :visible.sync="dialogVisible"
    width="30%"
    :before-close="close"
  >
      <el-button type="primary" @click="close">关闭</el-button>
  </el-dialog>
</template>

<script>
export default {
  props: {
    dialogVisible: {
      type: Boolean,
      default: false,
    },
  },
  methods: {
    close() {
      this.$emit('update:dialogVisible', false)
    },
  },
}
</script>
复制代码

而在 vue3 中 .sync 已被移除,因为vue3 更改了组件上 v-model 的用法,已经不再需要 .sync 了

<MyComponent v-model="componentVisible" />
// 相当于
<MyComponent :modelValue="componentVisible" @update:modelValue="componentVisible = $event"/>
复制代码

v-model 也可以加参数

<MyComponent v-model:visible="componentVisible" />
// 相当于
<MyComponent :componentVisible="componentVisible" @update:componentVisible="componentVisible = $event"/>
复制代码

示例:

// 父组件
<template>
  <div>
    <MyDialog v-model:dialogVisible="visible" />
    <el-button @click="open">打开弹窗</el-button>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from "vue";
import MyDialog from "./components/myDialog.vue";
export default defineComponent({
  components: { MyDialog },
  setup() {
    const visible = ref(false);
    function open() {
      visible.value = true;
    }
    return {
      visible,
      open,
    };
  },
});
</script>

// 子弹窗组件
<template>
  <el-dialog
    :model-value="dialogVisible"
    title="Tips"
    width="30%"
    :before-close="close"
  >
    <span>This is a message</span>
    <template #footer>
      <span class="dialog-footer">
        <el-button @click="close">取消</el-button>
      </span>
    </template>
  </el-dialog>
</template>

<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
  name: "MyDialog",
  props: {
    dialogVisible: {
      type: Boolean,
      default: false,
    },
  },
  emits: ["update:dialogVisible"],
  setup(props, { emit }) {
    function close() {
      emit("update:dialogVisible", false);
    }
    return {
      close,
    };
  },
});
</script>
复制代码

同时,一个组件上也可以有多个 v-model

<MyComponent v-model:visible="componentVisible"  v-model:userId="dataUserId" />
复制代码

移除了 $listeners

$listeners已经在 Vue 3中被移除,事件监听器已经和 $attrs 合并

// 父组件
<template>
  <children @parentsEvent="parentsEvent" :count="count" />
</template>

<script lang="ts">
import { defineComponent, ref } from "vue";
import children from "./components/children.vue";
export default defineComponent({
  name: "listenersAttrs",
  components: { children },
  setup() {
    const count = ref(0);
    function parentsEvent(value: string) {
      console.log(value);
    }
    return {
      count,
      parentsEvent,
    };
  },
});
</script>

// 子组件
<template>
  <div></div>
</template>

<script lang="ts">
import { defineComponent, onMounted } from "vue";
export default defineComponent({
  name: "children",
  setup(props, { attrs }) {
    onMounted(() => {
      console.log(attrs);
    });
  },
});
</script>
复制代码

打印结果如下

v12.PNG

移除了 $on、$off 和 $once

vue2 中我们可以 new Vue() 一个实例,再通过在这个实例上进行 $on、$off 、 $once 的操作来实现跨组件通信,也就是所谓的 vue bus,但是目前 new Vue() 已经没有了,$on、$off 和 $once也被移除了,毕竟,bus 太多的话,项目看上去就太乱了,还是用 vuex 或其他代替吧。

vue3.2

<script setup>

<script setup> 中我们可以不必声明export default 和 setup 方法, 这种写法可以将所有<script setup> 声明的顶层的绑定,包括变量,函数,以及import导入的内容暴漏出去,在模板中直接使用,看下示例

<template>
  <div>
      {{ data }}
      <MyComponents/>
  </div>
</template>

<script lang="ts" setup>
import { ref } from 'vue'
import MyComponents from './components/MyComponents.vue';
const data = 'vue3.2'
</script>

复制代码

defineProps 、 defineEmits

在<script setup>中由于没有了 setup函数,也没有props,emits选项,那么在这种写法中使用props和emits进行组件通信就需要借助defineProps 和 defineEmits。

defineProps 和 defineEmits只有在<script setup>中才能使用,且不需要从“vue”或者其他地方导入,直接就可以用。

defineProps 接收与 props选项相同的值,defineEmits 也接收 emits选项相同的值。

// 父组件
<template>
  <div>
      <MyComponents :propsCount='propsCount' @changePropsCount='changePropsCount'/>
  </div>
</template>

<script lang="ts" setup>
import { ref } from 'vue'
import MyComponents from './components/MyComponents.vue';
const propsCount = ref(1)
function changePropsCount(){
    propsCount.value++
}
</script>

// 子组件
<template>
  <div>
    <p>{{ propsCount }}</p>
    <el-button @click="emit('changePropsCount')">增加</el-button>
  </div>
</template>

<script lang="ts" setup>
import { ref } from "vue";
const props = defineProps({
  propsCount: Number,
});
const emit = defineEmits(['changePropsCount'])
</script>
复制代码

defineExpose

与常规的使用 setup 的写法一样,如果组件需要暴漏出去属性给外部使用,setup函数的第二个参数有个 expose 的方法属性

而在<script setup>中可以使用 defineExpose,使用方法与 expose 一致

<script setup>
import { ref } from 'vue'
const outData = ref(1)

defineExpose({
  outData
})
</script>
复制代码

useSlots useAttrs

同样,常规的 setup函数写法,setup函数的第二个参数有 attrs 和 slots 属性

而在<script setup>中可以使用 useSlots useAttrs

<script setup>
import { useSlots, useAttrs } from 'vue'
const slots = useSlots()
const attrs = useAttrs()
</script>
// slots 和 attrs的使用与之前一致
复制代码

与普通<script>搭配使用

也许你会发现<script setup>的写法好像没法定义组件名name

我们可以将其与普通<script>一起使用

<script>
import { defineComponent } from 'vue'
export default defineComponent({
  name: 'ComponentName',
})
</script>

<script setup>
	...
</script>
复制代码

v-bind

一个让人眼前一新的功能,单文件的 style 标签中可以通过 v-bind 使用当前组件的变量,如下

<template>
  <div class="box"></div>
</template>

<script lang="ts">
import { defineComponent, ref } from "vue";
export default defineComponent({
  name: "css-v-bind",
  setup() {
    let width = ref("100px");
    let height = ref("100px");
    let background = ref("#00cd96");

    return {
      width,
      height,
      background,
    };
  },
});
</script>

<style scoped>
.box {
  width: v-bind(width);
  height: v-bind(height);
  background: v-bind(background);
}
</style>
复制代码

v-memo

v-memo指令,一个可以避免无效的重复渲染的指令,该指令接收一个数组作为依赖值(数组里可以有多个依赖值),根据数组里的值判断是否需要更新渲染。

<template>
  <div>
    <div v-memo="[divMemoValue]">
      {{ innerData }}
    </div>
    <button @click="changeDivData">修改div数据</button>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from "vue";
export default defineComponent({
  setup() {
    const divMemoValue = ref(1);
    const innerData = ref("盒子数据1");
    function changeDivData() {
      innerData.value = "盒子数据2";
    }
    return {
      divMemoValue,
      innerData,
      changeDivData,
    };
  },
});
</script>
复制代码

上面的例子可以发现,点击按钮修改 innerData 数据,视图并不会发生改变,因为 v-memo里的 divMemoValue 没有发生修改,所以不会触发重新渲染dom。

根据这个原理,其实我们可以结合我们实际开发的项目情况,来合适的减少重新渲染,提高性能。

如果 v-memo的值是一个空数组,则对应的元素或组件将只渲染一次,不会更新,和 v-once 功能一样

这一点,特别是体现在和 v-for 的结合使用中

和 v-for 结合使用

看下面的例子

<template>
  <div>
    <div
      v-for="item in list"
      :key="item.order"
      v-memo="[item.order === activeOrder]"
    >
      {{ item.order === activeOrder ? "选中了" : "没选中" }}
      <button @click="selectItem(item.order)">选择</button>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from "vue";

export default defineComponent({
  setup() {
    const list = ref([
      {
        order: 1,
        label: "数据1",
      },
      {
        order: 2,
        label: "数据2",
      },
      {
        order: 3,
        label: "数据3",
      },
      {
        order: 4,
        label: "数据4",
      },
    ]);

    const activeOrder = ref(1);

    function selectItem(order: number) {
      activeOrder.value = order;
    }

    return {
      activeOrder,
      list,
      selectItem,
    };
  },
});
</script>
复制代码

上面例子中,当我们更新了选中的列表数据时,只有原先选中的和新选中的两条数据的“item.order === activeOrder”条件会变化,其他两条数据的“item.order === activeOrder”还是一样是false,所以跳过了其他两条数据的更新,优化了性能。

当然,这只是个简单的例子,实际开发中,可能数据有成百上千条,每条数据渲染的dom可能更复杂,实际优化的性能会特别可观。

猜你喜欢

转载自juejin.im/post/7041859244391399437