Vue2.x To Vue3.0

背景

​ 最近項目組重構一個大型項目,爲了引領時尚潮流,作公司最靚的仔。項目前端組採用Vue3.0進行重構。javascript

​ 固然,他們沒有強制要求使用Vue3.0,不習慣的依然採用Vue2.x的寫法慢慢過渡。我我的喜歡追求新知識,因此我就一步到位了。Vue3.0的升級必然有許多亮點,以前也有大體瞭解:函數式API,Typescript支持等。最喜歡這種工做,能夠在工做嘗試和學習新的東西。今後開始Vue3.0之旅。css

​ 初看composition-api ,個人表情以下:前端

​ 這不就是ReactHook嗎? 也關注了一下網上同行的評價,許多React的開發者對此次的升級表示不屑,這不就是抄襲嗎?因爲Vue入門簡單,擁有豐富的UI庫,龐大的使用人羣,良好的生態系統,成爲了當下火熱的前端框架。現在Vue的star數162K,React的star數爲147K。Vue「抄襲」ReactHook也能理解,既然有巨人的肩膀,爲什麼Vue不站上去呢。咱們不能作一個隨波逐流的人,獨立思考了一下,引用最近比較火的一句話:「存在即合理」。我的以爲Vue這次升級的主要是爲了解決:vue

  1. Typescript是大趨勢,TypeScript安利指南Vue2.xTypescript支持度不高;
  2. 代碼複用:Vue2.x能夠經過mixins(相似多繼承)和extends(相似單繼承)來實現,存在命名衝突,隱藏複用API等缺點;Vue3.0採用函數編程便能很好的解決代碼複用問題。

1 主要升級了什麼?

下面介紹的是對於咱們開發者比較容易感知的一些優化,至於重寫虛擬節點,提升運行時效率等優化暫時尚未深刻研究。java

1.1 雙向綁定

vue2.x雙向綁定

​ 衆所周知,Vue2.x雙向綁定經過Object. defineproperty重定義data中的屬性的getset方法,從而劫持了datagetset操做。Vue2.x雙向綁定存在的弊端有:react

  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的全方位增強版。git

Object.defineProperty能作的Proxy能作github

Proxy有多達13種攔截方法,不限於apply、ownKeys、deleteProperty、has等等是Object.defineProperty不具有的。Object.defineProperty不能作的Proxy還能作。面試

Proxy做爲新標準,獲得了各大瀏覽器廠商的大力支持,性能持續優化。惟一的不足就是兼容性的問題,並且沒法經過polyfill解決。vuex

更多詳細內容見:面試官: 實現雙向綁定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>
    複製代碼

都看到這裏了,點個贊,關注再走唄。

相關文章
相關標籤/搜索