Vue2.x对比Composition API写法

24,968 阅读6分钟

背景

​ 最近项目组重构一个大型项目,为了引领时尚潮流,做公司最靓的仔。项目前端组采用Vue3.0进行重构。

​ 当然,他们没有强制要求使用Vue3.0,不习惯的依然采用Vue2.x的写法慢慢过渡。我个人喜欢追求新知识,所以我就一步到位了。Vue3.0的升级必然有许多亮点,之前也有大致了解:函数式API,Typescript支持等。最喜欢这种工作,可以在工作尝试和学习新的东西。从此开始Vue3.0之旅。

​ 初看composition-api ,我的表情如下:

​ 这不就是ReactHook吗? 也关注了一下网上同行的评价,许多React的开发者对这次的升级表示不屑,这不就是抄袭吗?由于Vue入门简单,拥有丰富的UI库,庞大的使用人群,良好的生态系统,成为了当下火热的前端框架。如今Vue的star数162K,React的star数为147K。Vue“抄袭”ReactHook也能理解,既然有巨人的肩膀,为何Vue不站上去呢。我们不能做一个随波逐流的人,独立思考了一下,引用最近比较火的一句话:“存在即合理”。个人觉得Vue此次升级的主要是为了解决:

  1. Typescript是大趋势,TypeScript安利指南Vue2.xTypescript支持度不高;
  2. 代码复用:Vue2.x可以通过mixins(类似多继承)和extends(类似单继承)来实现,存在命名冲突,隐藏复用API等缺点;Vue3.0采用函数编程便能很好的解决代码复用问题。

1 主要升级了什么?

下面介绍的是对于我们开发者比较容易感知的一些优化,至于重写虚拟节点,提高运行时效率等优化暂时还没有深入研究。

1.1 双向绑定

vue2.x双向绑定

​ 众所周知,Vue2.x双向绑定通过Object. defineproperty重定义data中的属性的getset方法,从而劫持了datagetset操作。Vue2.x双向绑定存在的弊端有:

  1. 实例创建后添加的属性监听不到,因为数据劫持是在实例初始化过程中执行,具体是在beforeCreatecreated生命周期间完成。可以通过$set来解决后期添加监听属性的问题。
  2. defineproperty ()无法监听数组变化,当直接用index设置数组项是不会被检测到的。如:this.showData[1] = {a:1}。当然也能用$set解决。官方文档指出,通过下面八种方法操作数组,Vue能检测到数据变化,分别为:push()、pop()、shift()、unshift()、splice()、sort()、reverse()

vue3.0双向绑定

Vue3.0采用Proxy和Reflect实现双向绑定, 它在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。我们可以这样认为,ProxyObject.defineProperty的全方位加强版。

Object.defineProperty能做的Proxy能做

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

Proxy作为新标准,得到了各大浏览器厂商的大力支持,性能持续优化。唯一的不足就是兼容性的问题,而且无法通过polyfill解决。

更多详细内容见:面试官: 实现双向绑定Proxy比defineproperty优劣如何?

顺便感慨一下:掘金个个都是人才,说话又好听,噢哟超喜欢在里面!像在外面开厢一样,high到那种感觉,飞起来那种感觉 。不像某CSDN,帖子抄来抄去,还占据了大量的搜索资源。

1.2 函数式API

​ 函数式API主要是为了解决代码复用以及对Typescript的友善支持。这里主要介绍代码复用的升级。废话不多说,直接撸代码。下面介绍的场景相对简单,应该也比较好理解,是我们平时开发过程中常用的搜索组件。

代码结构如下:

初始化效果:

vue2.x代码复用

<template>
  <div class="vue2">
    <el-input type="text" @change="onSearch" v-model="searchValue" />
    <div v-for="name in names" v-show="name.isFixSearch" :key="name.id">
      {{ name.value }}
    </div>
  </div>
</template>
<script>
 // vue2.vue
import searchMixin from "./searchMixin";
export default {
  mixins: [searchMixin],
  data() {
    return {
      names: [
        { id: 1, isFixSearch: true, value: "name1" },
        { id: 2, isFixSearch: true, value: "name2" },
        { id: 3, isFixSearch: true, value: "name3" },
        { id: 4, isFixSearch: true, value: "name4" },
        { id: 5, isFixSearch: true, value: "name5" },
      ],
    };
  },
};
</script>
<style lang="less">
.vue2 {
}
</style>
// searchMixin.js
export default {
  data() {
    return {
      searchValue: ''
    }
  },
  /**
   * 命名固定,外面另外命名不容易
   * 应该可以通过 searchMixin.methods.onNewNameSearch =  searchMixin.methods.onSearch
   * 来进行重命名。但是data里面的应该就重命名不了了。
   */
  methods: {
    onSearch() { 
      this.names
          .forEach(name => 
             name.isFixSearch = name.value.includes(this.searchValue)
           )
    }
  }
}

效果如下:

缺点:

  1. 命名容易冲突,容易覆盖引入mixin的页面属性;
  2. 问题追踪难度大,这个缺点还是由于名称冲突导致的。mixin多的话容易出现不容易定位的问题。当然也可以通过namespace来解决。

vue3.0代码复用


<template>
  <div class="vue3">
    <el-input type="text" @change="onSearch" v-model="searchValue" />
    <div v-for="name in names" v-show="name.isFixSearch" :key="name.id">
      {{ name.value }}
    </div>
  </div>
</template>
<script>
 // vue3
import useSearch from "./useSearch";
export default {
  setup() {
    const originNames = [
      { id: 1, isFixSearch: true, value: "name1" },
      { id: 2, isFixSearch: true, value: "name2" },
      { id: 3, isFixSearch: true, value: "name3" },
      { id: 4, isFixSearch: true, value: "name4" },
      { id: 5, isFixSearch: true, value: "name5" },
    ];
    // 				可以很容易重命名
    const { onSearch, data: names, searchValue } = useSearch(originNames);
    return {
      onSearch,
      names,
      searchValue,
    };
  },
};
</script>
<style lang="less">
.vue3 {
}
</style>
// useSearch
import {
  reactive,
  toRefs
} from "@vue/composition-api";
export default function useSearch(names) {
  const state = reactive({
    data: names,
    searchValue: ''
  })
  const onSearch = () => {
    state.data.forEach(name => 
      name.isFixSearch = name.value.includes(state.searchValue)
    )
  }

  return {
    ...toRefs(state),
    onSearch
  }
}

效果如下:

缺点:暂无

1.3 Typescript支持

vue2.x Typescript支持

一开始的vue2.x是不支持Typescript的,耐不住Typescript的火热,就出现了.vueclass写法,通过vue-class-component强行支持Typescript。且看下面代码。

<template>
  <div>
    <input v-model="hello" />
    <p>hello: {{ hello }}</p>
    <p>computed: {{ computedMsg }}</p>
    <button @click="greet({a:1})">Hello TS</button>
  </div>
</template>

<script lang="ts">
import Vue from "vue";
import Component, { mixins } from "vue-class-component";

@Component
class MixinsDemo extends Vue {
  typescript = "Typescript";
}
// 这里便可以使用Typescript的类型检验了
function testTs(val: string) {
  console.log("testTs");
}
// 编译失败
testTs('1')

@Component
export default class TS extends mixins(MixinsDemo) {
  // 初始化数据
  hello = "Hello";
  // 声明周期钩子
  mounted() {}
  // 计算属性
  get computedMsg() {
    return `computed ${this.hello} ${this.typescript}`;
  }
  // template 传参校类型验不了
  greet(val: string) {
    alert(`greeting ${this.hello} ${this.typescript}-${val}`);
  }
}
</script>

缺点:

  1. Vue2.x语法外还要重新学习一套语法
  2. 代码复用的问题没有得到根本的解决

效果如下:

Vue2.x1引入Typescript意犹未尽的可以查看:在 Vue 中使用 TypeScript 的一些思考(实践)

Vue3.0 Typescript支持

由于Vue3.0采用函数式API开发,能很方便的引入Typescript,这里就不赘述了。

2 新写法和旧写法对比

​ 上面啰嗦了那么多“废话”,下面就开启Vue3.0之旅。首先简单介绍一下Vue3.0入口API setup(props,ctx)的两个参数:

  1. props:template传递的参数,不像vue2.x可以通过this.propsA访问到template传递的参数,这里要通过props.propsA进行访问
  2. ctx:上下文,setup里面this不再指向vue实例,ctx有几个属性:slots, root, parent, refs, attrs, listeners, isServer, ssrContext, emit,其中root指向`vue实例,其他详细介绍可见Vue Composition API

下面内容是Vue2.x常用的场景写法映射到Vue3.0,希望在你日常开发过程中有所帮助。代码目录结构如下:

页面效果如下:

2.1 双向绑定

Vue2.x双向绑定

export default {
  data() {
    return {
      plusValue: 1,
      stateValue: 1,
    };
  },
  created(){
    // 单向绑定
    this.singleValue = 2
  },
  methods: {
    onClickSingle() {
      this.singleValue++;
      console.log(this.singleValue);
    },
    onPlus() {
      this.plusValue++;
    },
    onPlueState() {
      this.stateValue++;
    },
  },
};

Vue3.0双向绑定

​ 双向绑定个人更喜欢通过reactive统一包裹,访问的时候可以通过state.stateValue进行访问和赋值,通过ref生成的双向绑定数据需要通过plusValue.value的形式进行访问和赋值。而且可以通过...toRefs(state)一次性解构为双向绑定的属性。

import { reactive, ref, toRefs } from "@vue/composition-api";

export default {
  setup() {
    // 单向绑定
    let singleValue = 2;
    // 单个双向绑定
    const plusValue = ref(1);
    // 对象包裹双向绑定
    const state = reactive({
      stateValue: 1,
    });

    const methods = {
      onClickSingle() {
        singleValue++;
        console.log(singleValue);
      },
      onPlus() {
        plusValue.value++;
      },
      onPlueState() {
        state.stateValue++;
      },
    };
    return {
      ...toRefs(state),
      plusValue,
      singleValue,
      ...methods,
    };
  },
};

2.2 computed

Vue2.x computed

computed: {
    plusValueAndStateValue() {
      return this.plusValue + this.stateValue;
    },
 },

Vue3.0 computed

import { computed } from "@vue/composition-api";
// 计算属性
const plusValueAndStateValue = computed(
    () => plusValue.value + state.stateValue
);

2.3 watch

Vue2.x watch

watch: {
    plusValueAndStateValue(val) {
      console.log("vue2 watch plusValueAndStateValue change", val);
    },
  },

Vue3.0 watch

import { watch } from "@vue/composition-api";
watch(plusValueAndStateValue, (val) => {
      console.log("vue3 watch plusValueAndStateValue change", val);
});

2.4 eventBus

Vue2.x eventBus

Vue2.x可以通过App.vue实例来跨组件广播事件,传递数据。

onClickSingle() {
      this.singleValue++;
	  // 广播事件
      this.$root.$emit("vue2 eventBus", { a: 1 });
      console.log(this.singleValue);
},

另一个存活的vue实例,接受事件

created() {
    this.$root.$on("vue2 eventBus", (data) => {
      console.log(data);
      debugger;
    });
  },

当然也可以通过监听vuex中的属性值来实现eventBus。参看状态机Vuex的奇淫巧技-多弹框、多事件统一控制

Vue3.0 eventBus

发送事件(原理和vue2.x一样)

onClickSingle() {
    singleValue++;
    ctx.root.$root.$emit("vue3 eventBus", { a: 3 });
    console.log(singleValue);
}

接受事件

ctx.root.$root.$on("vue3 eventBus", (data) => {
    console.log(data);
    debugger;
});

当然也可以通过Vue3.0vuex代替方案中监听注入属性来实现eventBus,见下面2.6

2.5 生命周期

Vue3.0中不再存在beforeCreatecreatedcomposition-api暴露其他生命周期的API,都是以on开头的API。下面就mounted写法进行举例,其他生命周期类比。

import { 
    onMounted,
    onBeforeMount,
    onBeforeUnmount,
    onBeforeUpdate,
    onDeactivated,
    onUnmounted,
    onUpdated,
} from "@vue/composition-api";
export default {
  setup(props, ctx) {
    onMounted(()=>{
        console.log('mounted')
    })
  },
};

2.6 vuex

Vue2.x vuex的写法可参看状态机Vuex的奇淫巧技-多弹框、多事件统一控制。由于Vue3.0不能直接访问到this,不能方便的调用this.$commit;也不方便通过mapStatecomputed注入属性。composition-api提供了两个API:provide、inject。让Vue更加React,通过这两个API可以替代vuex进行状态管理。

代码结构如下:

场景:统一控制弹框显示隐藏,不用在vue实例中设置isDialogShow和修改值,不用在弹框关闭的时候修改$parent中的isDialogShow

  1. store/BooleanFlag.js

    import {
      provide,
      inject,
      reactive,
    } from '@vue/composition-api'
    
    const bfSymbol = Symbol('BooleanFlag')
    
    export const useBooleanFlagProvider = () => {
      // 统一控制弹框显示隐藏
      const BooleanFlag = reactive({
        isDialogShow: false,
        isDialog2Show: false,
        isDialog3Show: false,
      })
      const setBooleanFlag = (keys) => {
        keys.forEach(key => {
          if (Object.keys(BooleanFlag).includes(key)) {
            BooleanFlag[key] = !BooleanFlag[key]
          }
        })
      }
      provide(bfSymbol, {
        BooleanFlag,
        setBooleanFlag,
      })
    }
    
    export const useBooleanFlagInject = () => {
      return inject(bfSymbol);
    };
    
  2. store/index.js

    // vue3 vuex 替代方案
    import {
      useBooleanFlagProvider,
      useBooleanFlagInject
    } from './BooleanFlag'
    
    export {
      useBooleanFlagInject
    }
    
    export const useProvider = () => {
      useBooleanFlagProvider()
    }
    
  3. init/initVueComposition.js

    import VueCompositionApi from '@vue/composition-api'
    import {
      useProvider
    } from '@/store'
    
    export default function initVueComposition(Vue) {
      Vue.use(VueCompositionApi)
    
      return function setup() {
        useProvider()
        return {}
      }
    }
    
  4. init/index.js

    import initElement from './initElement'
    import initAppConst from './initAppConst'
    import initI18n from './initI18n'
    import initAPI from './initAPI'
    import initRouter from './initRouter'
    import initVueComposition from './initVueComposition'
    
    // 往Vue原型上追加内容,简化开发调用,原型上追加内容是不会影响性能的,因为原型在内存中只存在一份
    export default function initVue(Vue) {
      initElement(Vue)
      initAppConst(Vue)
      const i18n = initI18n(Vue)
      const router = initRouter(Vue)
      initAPI(Vue)
      const setup = initVueComposition(Vue)
    
      return {
        i18n,
        router,
        setup
      }
    }
    
  5. main.ts

    import Vue from 'vue'
    import App from './App.vue'
    
    import initVue from './init'
    
    import 'static/css/base.css'
    import 'static/css/index.css'
    
    const init = initVue(Vue)
    
    new Vue({
        ...init,
        render: h => h(App),
    }).$mount('#app')
    
  6. vue文件注入

    <template>
      <div>
        <div>
          <span>vuex:</span>
          <span>{{ "isDialogShow  :  " + BooleanFlag.isDialogShow }}</span>
          <el-button @click="onDialogShow">onDialogShow</el-button>
        </div>
      </div>
    </template>
    <script>
    import { useBooleanFlagInject } from "@/store";
    
    export default {
      setup(props, ctx) {
        const { BooleanFlag, setBooleanFlag } = useBooleanFlagInject();
        const methods = {
          onDialogShow() {
            setBooleanFlag(["isDialogShow"]);
          },
        };
        return {
          BooleanFlag,
        };
      },
    };
    </script>
    

都看到这里了,点个赞,关注再走呗。