简单对比vue2.x与vue3.x响应式及新功能

简单对比vue2.x与vue3.x响应式

  对响应方式来讲:Vue3.x 将使用Proxy ,取代Vue2.x 版本的 Object.defineProperty

为何要将Object.defineProperty换掉呢?

  1、我刚上手Vue2.x的时候就经常遇到一个问题,数据更新了啊,为何页面不更新呢?

  2、什么时候用$set更新,什么时候用$forceUpdate强制更新,你是否也一度陷入困境?

后来的学习过程中开始接触源码,才知道一切的根源都是 Object.defineProperty

  在vue2.0中vm.items[indexOfItem] = newValue这种是无法检测的。事实上,Object.defineProperty 本身是可以监控到数组下标的变化的,只是在 Vue 的实现中,从性能 / 体验的性价比考虑,放弃了这个特性。

下面个例子 Object.defineProperty

     function defineReactive(data, key, value) {             Object.defineProperty(data, key, {                 get: () => {                     console.log('get',value)                     return value                 },                 set:(newValue)=>{                     console.log('set')                     value=newValue                 }             })         }         function observe(data){             Object.keys(data).forEach(function(key){                 defineReactive(data,key,data[key])             })         }         let arr=[1,2,3]         observe(arr)         arr[1]         arr[2]=9         arr[2]

简单对比vue2.x与vue3.x响应式及新功能

 可以看到,通过下标获取某个元素会触发 getter 方法, 设置某个值会触发 setter 方法。

Object.defineProperty 在数组中的表现和在对象中的表现是一致的,数组的索引就可以看做是对象中的 key

  1. 通过索引访问或设置对应元素的值时,可以触发 getter 和 setter 方法。

  2. 通过 push 或 unshift 会增加索引,对于新增加的属性,需要再手动初始化才能被 observe。

  3. 通过 pop 或 shift 删除元素,会删除并更新索引,也会触发 setter 和 getter 方法。

所以,Object.defineProperty是有监控数组下标变化的能力的,只是 Vue2.x 放弃了这个特性。

那么Object.defineProperty 和 Proxy 对比存在哪些优缺点呢?

1. Object.defineProperty 只能劫持对象的属性,而 Proxy 是直接代理对象。

  由于 Object.defineProperty 只能对属性进行劫持,需要遍历对象的每个属性,如果属性值也是对象,则需要深度遍历。而 Proxy 直接代理对象,不需要遍历操作。

2. Object.defineProperty 对新增属性需要手动进行 Observe。

  由于 Object.defineProperty 劫持的是对象的属性,所以新增属性时,需要重新遍历对象,对其新增属性再使用 Object.defineProperty 进行劫持。也正是因为这个原因,使用 Vue 给 data 中的数组或对象新增属性时,需要使用 vm.$set 才能保证新增的属性也是响应式的。

总 结

  1. Object.defineProperty 并非不能监控数组下标的变化,Vue2.x 中无法通过数组索引来实现响应式数据的自动更新是 Vue 本身的设计导致的,不是 defineProperty 的锅。

  2. Object.defineProperty 和 Proxy 本质差别是,defineProperty 只能对属性进行劫持,所以出现了需要递归遍历,新增属性需要手动 Observe 的问题。

  3. Proxy 作为新标准,浏览器厂商势必会对其进行持续优化,但它的兼容性也是块硬伤,并且目前还没有完整的 polyfill 方案。

自定义 Hooks

一个实现加减的例子, 这里可以将其封装成一个hook, 我们约定这些「自定义 Hook」以 use 作为前缀,和普通的函数加以区分。

useCount.js

     import { ref, Ref, computed } from "vue";
        export default function useCount(initValue = 1) {
            const count = ref(initValue);

            const increase = (delta) => {
                if (delta) {
                    count.value += delta;
                } else {
                    count.value += 1;
                }
            };
            const multiple = computed(() => count.value * 2)

            const decrease = (delta) => {
                if (delta) {
                    count.value -= delta;
                } else {
                    count.value -= 1;
                }
            };

            return {
                count,
                multiple,
                increase,
                decrease,
            };
        }

接下来看一下在组件中使用useCount这个 hook:

<template>
  <div>
    <p>count: {{ count }}</p>
    <p>倍数: {{ multiple }}</p>
    <div>
      <button @click="increase()">加1</button>
      <button @click="decrease()">减一</button>
    </div>
  </div>
</template>

<script>
import useCount from "../hooks/useCount";
export default {
  components: {},
  //con==context(attrs,emit,slots)
  setup(props, con) {
    const { count, multiple, increase, decrease } = useCount(10);
    return {
      count,
      multiple,
      increase,
      decrease,
    };
  },
};
</script>

<style lang='scss' scoped>
</style>

Vue 3 任意传送门——Teleport

  如果用过 React 的同学,可能对于 Portals 比较熟悉。React 的 Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案,我理解,Vue 3 中的 Teleport 跟这个其实是类似的。   在子组件Header中使用到Dialog组件,我们实际开发中经常会在类似的情形下使用到 Dialog ,此时Dialog就被渲染到一层层子组件内部,处理嵌套组件的定位、z-index和样式都变得困难。   Dialog从用户感知的层面,应该是一个独立的组件,从dom结构应该完全剥离Vue顶层组件挂载的DOM;同时还可以使用到Vue组件内的状态(data或者props)的值。简单来说就是,即希望继续在组件内部使用Dialog,又希望渲染的DOM结构不嵌套在组件的DOM中。   此时就需要Teleport上场,我们可以用<Teleport>包裹Dialog, 此时就建立了一个传送门,可以将Dialog渲染的内容传送到任何指定的地方。

Teleport的使用

我们希望Dialog渲染的dom和顶层组件是兄弟节点关系, 在index.html文件中定义一个供挂载的元素:

<div id="app"></div>
+ <div id="dialog"></div>
</body>

定义一个Dialog组件Dialog.vue, 留意 to 属性, 与上面的id选择器一致:

<template>
    <teleport to="#dialog">
        <div class="dialog">
            <div class="dialog_wrapper">
                <div class="dialog_header" v-if="title">
                    <slot name="header">
                        <span>{{title}}</span>
                    </slot>
                </div>
            </div>
            <div class="dialog_content">
                <slot></slot>
            </div>
            <div class="dialog_footer">
                <slot name="footer"></slot>
            </div>
        </div>
    </teleport>
</template>

最后在一个子组件Header.vue中使用Dialog组件,这里主要演示 Teleport的使用,不相关的代码就省略了。header组件

<div class="header">
    ...
    <navbar />
+    <Dialog v-if="dialogVisible"></Dialog>
</div>
...

可以看到,我们使用 teleport 组件,通过 to 属性,指定该组件渲染的位置与 <div id="app"></div> 同级,也就是在 body 下,但是 Dialog 的状态 dialogVisible 又是完全由内部 Vue 组件控制

Vue3 新特性 —— Suspense 异步组件

在vue2.0前后端交互获取数据时, 是一个异步过程,一般我们都会提供一个加载中的动画,当数据返回时配合v-if来控制数据显示。

<div>
    <div v-if="!loading">
        ...
    </div>
    <div v-if="loading">
        加载中...
    </div>
</div>

如果你使用过vue-async-manager这个插件来完成上面的需求, 你对Suspense可能不会陌生,Vue3.x感觉就是参考了vue-async-manager.

Suspense, 它提供两个template slot, 刚开始会渲染一个fallback状态下的内容, 直到到达某个条件后才会渲染default状态的正式内容, 通过使用Suspense组件进行展示异步渲染就更加的简单。如果使用 Suspense, 要返回一个promise 组件的使用:

 <Suspense>
        <template #default>
            <async-component></async-component>
        </template>
        <template #fallback>
            <div>
                Loading...
            </div>
        </template>
    </Suspense>

案例

/src/components/Async.vue

<template>
  <h1>{{ result }}</h1>
</template>
<script >
export default{
  setup() {
    return new Promise(resolve => {
      setTimeout(() => {
        return resolve({ result: "HI~Async" });
      }, 3000);
    });
  }
}
</script>

在 App.vue 中使用异步组件

注意:

  • 使用 <Suspense></Suspense> 包裹所有异步组件相关代码
  • <Suspense></Suspense><template #default></template> 插槽包裹异步组件
  • <Suspense></Suspense><template #fallback></template> 插槽包裹渲染异步组件之前的内容
/src/App.vue
<template>
  <div id="app">
    <Suspense>
      <template #default>
        <Async></Async>
      </template>
      <template #fallback>
        <h1>Loading...</h1>
      </template>
    </Suspense>
  </div>
</template>

<script lang="ts">
import Async from "./components/Async.vue";

export default {
  name: "App",
  components: {
    Async
  }
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

片段(Fragment)

在 Vue2.x 中, template中只允许有一个根节点: 但是在 Vue3.x 中,你可以直接写多个根节点, 是不是很爽:
<template>
    <span></span>
    <span></span>
</template>

更好的 Tree-Shaking

 Vue 团队希望能帮助开发者减小 Web 应用的体积。

什么是 TreeShaking 简单对比vue2.x与vue3.x响应式及新功能

TreeShaking 是一个术语,指的是在打包构建过程中移除没有被引用到的代码,这些代码可以成为 dead code。这个概念最早在基于 ES6 的打包工具 Rollup 中提出,后来被引入到 webpack 中。TreeShaking 比较依赖于 ES6 模块系统的静态结构特性,比如 import 和 export。


我们在vue去操作dom时往往会用到vue.nextTick或this.$nextTick

// vue2.x
import Vue from "vue"

Vue.nextTick(()=>{
    ...
})

  其实在单个的 Vue 实例中也可以使用这个方法:$nextTick,这不过是 Vue 2.x 版本中方便开发者的一种做法,这个函数的本质依然是 Vue.nextTick

  假如你没有用到 Vue.nextTick 这个方法,或者你更喜欢用 setTimeout 来代替,这样的话 Vue 中 nextTick的部分将会变成 dead code —— 徒增代码体积但从来不会被用到,这对于客户端渲染的 web app 来说是拖累性能的一大因素。

  虽然我们借助webpacktree-shaking,但是不管我们实际上是否使用Vue.nextTick(),最终都会进入我们的生产代码, 因为 Vue实例是作为单个对象导出的, 打包器无法查出代码使用了对象的哪些属性。

vue3.x写法

在 Vue 3 中,官方团队重构了所有全局 API 的组织方式,让所有的 API 都支持了 TreeShaking。所以,当开发者在 Vue 3 中使用全局 API 时,需要主动将其导入到目标文件中,比如上面的例子,需要改写成:

import { nextTick } from "vue"

nextTick(() =>{
    ...
})

受影响的 API

Vue.nextTick
Vue.observable (用 Vue.reactive 替换)
Vue.version
Vue.compile  (仅限完整版本时可用)
Vue.set     (仅在 2.x 兼容版本中可用)
Vue.delete   (与上同)

vue3与vue2相比的一些变动

slot 具名插槽语法

 

在Vue2.x中, 具名插槽的写法:

<!--  子组件中:-->
<slot name="title"></slot>


<!--  父组件中:-->
<template slot="title">
    <h1>歌曲:成都</h1>
<template>

 

在vue2.x slot上面绑定数据,可以使用作用域插槽

// 子组件 
<slot name="content" :data="data"></slot>
export default {
    data(){
        return{
            data:["1234","2234","3234"]
        }
    }
}
<!-- 父组件中使用 -->
<template slot="content" slot-scope="scoped">
    <div v-for="item in scoped.data">{{item}}</div>
<template>

在Vue2.x中具名插槽和作用域插槽分别使用slotslot-scope来实现。

在Vue3.0中将slotslot-scope进行了合并。

<!-- 父组件中使用 -->
 <template v-slot:content="scoped">
   <div v-for="item in scoped.data">{{item}}</div>
</template>

<!-- 也可以简写成: -->
<template #content="{data}">
    <div v-for="item in data">{{item}}</div>
</template>

自定义指令

 Vue 2 中实现一个自定义指令

// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
  // 当被绑定的元素插入到 DOM 中时……
  inserted: function (el) {
    // 聚焦元素
    el.focus()
  }
})

在Vue 2 中, 自定义指令通过以下几个可选钩子创建:

  • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
  • inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
  • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。
  • componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
  • unbind:只调用一次,指令与元素解绑时调用。

在Vue 3 中对自定义指令的 API进行了更加语义化的修改

简单对比vue2.x与vue3.x响应式及新功能

所以在Vue3 中, 可以这样来自定义指令:

<input v-focus />
const { createApp } from "vue"

const app = createApp({})
app.directive('focus', {
    mounted(el) {
        el.focus()
    }
})

v-model 升级

  • 变更:在自定义组件上使用v-model时, 属性以及事件的默认名称变了
  • 变更:v-bind.sync修饰符在 Vue 3 中又被去掉了, 合并到了v-model
  • 新增:同一组件可以同时设置多个 v-model
  • 新增:开发者可以自定义 v-model修饰符

异步组件

Vue3 中 使用 defineAsyncComponent 定义异步组件,配置选项 component 替换为 loader ,Loader 函数本身不再接收 resolve 和 reject 参数,且必须返回一个 Promise,用法如下:

<template>
  <!-- 异步组件的使用 -->
  <AsyncPage />
</tempate>

<script>
import { defineAsyncComponent } from "vue";

export default {
  components: {
    // 无配置项异步组件
    AsyncPage: defineAsyncComponent(() => import("./NextPage.vue")),
    
    // 有配置项异步组件
    AsyncPageWithOptions: defineAsyncComponent({
   loader: () => import(".NextPage.vue"),
   delay: 200, 
   timeout: 3000,
   errorComponent: () => import("./ErrorComponent.vue"),
   loadingComponent: () => import("./LoadingComponent.vue"),
 })
  },
}
</script>

 

上一篇:Vue2.X源码学习笔记(一)选项合并


下一篇:Vue2.x-踩坑记