vue3新特性

快速開始

  • 通過 CDN:<script src="https://unpkg.com/vue@next"></script>
  • 通過 Codepen 的瀏覽器 playground
  • 腳手架 Vite
npm init vite-app hello-vue3 # OR yarn create vite-app hello-vue3
npm install -g @vue/cli # OR yarn global add @vue/cli
vue create hello-vue3
# select vue 3 preset

vite

Vite,一個基于瀏覽器原生 ES imports 的開發服務器。利用瀏覽器去解析 imports,在服務器端按需編譯返回,完全跳過了打包這個概念,服務器隨起隨用。同時不僅有 Vue 文件支持,還搞定了熱更新,而且熱更新的速度不會隨著模塊增多而變慢。針對生產環境則可以把同一份代碼用 rollup 打包。雖然現在還比較粗糙,但這個方向我覺得是有潛力的,做得好可以徹底解決改一行代碼等半天熱更新的問題。
https://juejin.cn/post/6928175048163491848

簡陋的實現 vite http://www.lxweimin.com/p/88cad1a63faf

new Vue()與createApp() 創建實例

  • vue2
    import Vue from 'vue'
    import App from './App.vue'
    new Vue({
        render: h => h(App),
    }).$mount('#app')
  • vue3
    import { createApp } from 'vue';
    import App from './App.vue'

    createApp(App).mount('#app')

一個新的全局 API:createApp
調用 createApp 返回一個應用實例,這是 Vue 3 中的新概念
應用實例暴露當前全局 API 的子集,經驗法則是,任何全局改變 Vue 行為的 API 現在都會移動到應用實例上,以下是當前全局 API 及其相應實例 API 的表

2.x 全局 API 3.x 實例 API (app)
Vue.config app.config
Vue.config.productionTip removed
Vue.config.ignoredElements app.config.isCustomElement
Vue.component app.component
Vue.directive app.directive
Vue.mixin app.mixin
Vue.use app.use

Fragment

vue3不會再像vue2一樣需要手動添加一個根結點
vue2中 如果你創建一個Vue組件,那么它只能有一個根節點。 這意味著不能創建這樣的組件:

<template>
  <div>Hello</div>
  <div>World</div>
</template>

原因是代表任何Vue組件的Vue實例需要綁定到一個單一的DOM元素中。唯一可以創建一個具有多個DOM節點的組件的方法就是創建一個沒有底層Vue實例的功能組件。 結果發現React社區也遇到了同樣的問題。他們想出的解決方案是一個名為 Fragment 的虛擬元素。它看起來差不多是這樣的:

class Columns extends React.Component {
  render() {
    return (
      <React.Fragment>
        <td>Hello</td>
        <td>World</td>
      </React.Fragment>
    );
  }
}

盡管Fragment看起來像一個普通的DOM元素,但它是虛擬的,根本不會在DOM樹中呈現。這樣我們可以將組件功能綁定到一個單一的元素中,而不需要創建一個多余的DOM節點。 目前你可以在Vue 2中使用vue-fragments庫來使用Fragments,而在Vue 3中,你將會在開箱即用!

Teleport

Vue 鼓勵我們通過將 UI 和相關行為封裝到組件中來構建 UI。我們可以將它們嵌套在另一個內部,以構建一個組成應用程序 UI 的樹。

然而,有時組件模板的一部分邏輯上屬于該組件,而從技術角度來看,最好將模板的這一部分移動到 DOM 中 Vue app 之外的其他位置。

一個常見的場景是創建一個包含全屏模式的組件。在大多數情況下,你希望模態的邏輯存在于組件中,但是模態的定位很快就很難通過 CSS 來解決,或者需要更改組件組合。
比如下面:
html

<body>
  <div style="position: relative;">
    <h3>Tooltips with Vue 3 Teleport</h3>
    <div>
      <modal-button></modal-button>
    </div>
  </div>
</body>

modal-button

app.component('modal-button', {
  template: `
    <button @click="modalOpen = true">
        Open full screen modal! (With teleport!)
    </button>

    <teleport to="body">
      <div v-if="modalOpen" class="modal">
        <div>
          I'm a teleported modal! 
          (My parent is "body")
          <button @click="modalOpen = false">
            Close
          </button>
        </div>
      </div>
    </teleport>
  `,
  data() {
    return { 
      modalOpen: false
    }
  }
})

Teleport 提供了一種干凈的方法,允許我們控制在 DOM 中哪個父節點下呈現 HTML,而不必求助于全局狀態或將其拆分為兩個組件。
因此,一旦我們單擊按鈕打開模式,Vue 將正確地將模態內容渲染為 body 標簽的子級。(解決了fixed彈窗中 有用出現 全屏的彈窗這種問題)

v-model 雙向綁定

綁定響應式的props屬性寫法

父
<text-document
  v-bind:title="title"
  v-on:update:title="title = $event"
></text-document>

子
this.$emit('update:title', newTitle)

vue2 使用.sync修飾符來簡化父組件的綁定
<text-document v-bind:title.sync="doc.title"></text-document>
vue3中則將.sync封裝到了v-model之中
<text-document v-model:title="doc.title" />

多個 v-model 綁定

<user-name
  v-model:first-name="firstName"
  v-model:last-name="lastName"
></user-name>


const app = Vue.createApp({})

app.component('user-name', {
  props: {
    firstName: String,
    lastName: String
  },
  template: `
    <input 
      type="text"
      :value="firstName"
      @input="$emit('update:firstName', $event.target.value)">

    <input
      type="text"
      :value="lastName"
      @input="$emit('update:lastName', $event.target.value)">
  `
})

響應式

vue2
Object.defineProperty(obj, prop, descriptor)
vue3
new Proxy(target, handler);

組合式API 與 選項式

Vue3 是向下兼容 Vue2 API 的,但是 Vue3 中提供了一種全新的 Composition API

一個大型組件,有很多關注點,比如一個功能要實現a, 會在data、computed、methods、watch加入代碼。 后面增加功能b, 又會在data、computed、methods、watch中加入代碼。這樣一直加下去。 就很有很多邏輯關注點;

為了修改功能a,我們可能要從data ,跳轉到methods,然后又跳轉到其他methods,computed等,這樣增加了理解和維護難度。

如果我們能夠將與同一個邏輯關注點相關的代碼配置在一起會更好。而這正是組合式 API 使我們能夠做到的。

組合式 API 基礎

既然我們知道了為什么,我們就可以知道怎么做。為了開始使用組合式 API,我們首先需要一個可以實際使用它的地方。在 Vue 組件中,我們將此位置稱為 setup

我們可以在外面建了多個邏輯 a.js b.js c.js 里面實現不同的邏輯(data,methods, watch, computed,mounted, updated, provide, inject等都在各自的js總完成), 然后再setup 中引入對應js執行就行。

  name: 'App',
  components: {
    HelloWorld
  },
  setup(props, context){
    console.log('props', props);
    console.log('context', context);
  }
}

執行時機

  setup(props, context) {
    console.log('setup');
  },
  beforeCreate(){
    console.log('beforeCreate');
  },
  created(){
    console.log('created');
  }
image.png

可以看出 是在beforeCreate之前執行。執行 setup 時,組件實例尚未被創建,所以不能使用this訪問實例。(methods這些也就不能使用了)

setup 參數

  1. props
  2. context

Props
setup 函數中的第一個參數是 props。正如在一個標準組件中所期望的那樣,setup 函數中的 props 是響應式的,當傳入新的 prop 時,它將被更新。

但是,因為 props 是響應式的,你不能使用 ES6 解構,因為它會消除 prop 的響應性。

上下文
傳遞給 setup 函數的第二個參數是 context。context 是一個普通的 JavaScript 對象,它暴露三個組件的 property:


export default {
  setup(props, context) {
    // Attribute (非響應式對象) 沒用props傳遞的屬性 
    console.log(context.attrs)

    // 插槽 (非響應式對象)
    console.log(context.slots)

    // 觸發事件 (方法)
    console.log(context.emit)
    // 暴露
    console.log(context.expose)
  }
}

setup 生命周期鉤子

選項式 API Hook inside setup
beforeCreate Not needed*
created Not needed*
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount onBeforeUnmount
unmounted onUnmounted
errorCaptured onErrorCaptured
renderTracked onRenderTracked
renderTriggered onRenderTriggered

這些函數接受一個回調函數,當鉤子被組件調用時將會被執行:

// MyBook.vue

export default {
  setup() {
    // mounted
    onMounted(() => {
      console.log('Component is mounted!')
    })
  }
}

響應性基礎

聲明響應式狀態
要為 JavaScript 對象創建響應式狀態,可以使用 reactive 方法:

import { reactive } from 'vue'

// 響應式狀態
const state = reactive({
  count: 0
})
return{state}

創建獨立的響應式值作為 refs

import { ref } from 'vue'

const count = ref(0)
console.log(count.value) // 0

count.value++
console.log(count.value) // 1
return  { count }

template中 不用 使用value
<div>{{count}}</div>

訪問響應式對象

const count = ref(0)
const state = reactive({
  count
})

console.log(state.count) // 0

state.count = 1
console.log(count.value) // 1

return {state, count }

toRef
可以用來為源響應式對象上的 property 性創建一個 ref。然后可以將 ref 傳遞出去,從而保持對其源 property 的響應式連接

const state = reactive({
  foo: 1,
  bar: 2
})

const fooRef = toRef(state, 'foo')

fooRef.value++
console.log(state.foo) // 2

state.foo++
console.log(fooRef.value) // 3

響應式狀態解構 toRefs
將響應式對象轉換為普通對象,其中結果對象的每個 property 都是指向原始對象相應 property 的ref

const state = reactive({
  foo: 1,
  bar: 2
})

const stateAsRefs = toRefs(state)

// ref 和 原始property “鏈接”
state.foo++
console.log(stateAsRefs.foo.value) // 2

stateAsRefs.foo.value++
console.log(state.foo) // 3

computed
使用 getter 函數,并為從 getter 返回的值返回一個不變的響應式 ref 對象。

const count = ref(1)
const plusOne = computed(() => count.value + 1)

console.log(plusOne.value) // 2

plusOne.value++ // error

watchEffect
為了根據響應式狀態自動應用和重新應用副作用,我們可以使用 watchEffect 方法。它立即執行傳入的一個函數,同時響應式追蹤其依賴,并在其依賴變更時重新運行該函數

const count = ref(0)

watchEffect(() => console.log(count.value))
// -> logs 0

setTimeout(() => {
  count.value++
  // -> logs 1
}, 100)

watchEffect停止監聽
watchEffect 在組件的 setup() 函數或生命周期鉤子被調用時,偵聽器會被鏈接到該組件的生命周期,并在組件卸載時自動停止。

const stop = watchEffect(() => {
  /* ... */
})

// later
stop()

watchEffect 清除副作用
有時副作用函數會執行一些異步的副作用,這些響應需要在其失效時清除 (即完成之前狀態已改變了) 。所以偵聽副作用傳入的函數可以接收一個 onInvalidate 函數作入參,用來注冊清理失效時的回調。當以下情況發生時,這個失效回調會被觸發:

  • 副作用即將重新執行時
  • 偵聽器被停止 (如果在 setup() 或生命周期鉤子函數中使用了 watchEffect,則在組件卸載時)

比如: 假設我們現在用一個用戶ID去查詢用戶的詳情信息,然后我們監聽了這個用戶ID, 當用戶ID 改變的時候我們就會去發起一次請求,但是如果在請求數據的過程中,我們的用戶ID發生了多次變化,那么我們就會發起多次請求,而最后一次返回的數據將會覆蓋掉我們之前返回的所有用戶詳情。這不僅會導致資源浪費, watchEffect 我們就可以做到

<template>
  <div> count:{{count}}</div>
  <div @click="fn">click</div>
</template>

<script>
import { watchEffect, ref } from 'vue'
export default {
  setup() {
    const count = ref(2)
    watchEffect((onInvalidate) => {
      console.log(count.value, '副作用1111')

      const token = setTimeout(() => {
        console.log(count.value, '副作用22222')
        // 發送請求
      }, 4000)

      onInvalidate(() => {
        // 4 秒之內改變  清除副作用
        // token 是 上一下 watchEffect 中 返回的token
        clearTimeout(token)
      })
    })
    function fn() {
      count.value++
    }
    return {
      fn,
      count,
    }
  },
}
</script>
用戶 點擊4秒內 再次點擊 會取消上一次的 請求 ,如果4秒內 沒有點擊,再發送請求

副作用刷新時機
Vue 的響應性系統會緩存副作用函數,并異步地刷新它們,這樣可以避免同一個“tick” 中多個狀態改變導致的不必要的重復調用。在核心的具體實現中,組件的 update 函數也是一個被偵聽的副作用。當一個用戶定義的副作用函數進入隊列時,默認情況下,會在所有的組件 update 前執行;

flush: post; 在組件 update之后執行
// 在組件更新后觸發,這樣你就可以訪問更新的 DOM。
// 注意:這也將推遲副作用的初始運行,直到組件的首次渲染完成。

    const count = ref(0)

    watchEffect(
      () => {
        console.log('watchEffect', count.value)
      },
      {
        // flush: 'pre',
        // flush: 'post',
        // flush: 'sync',
      }
    )
    onBeforeUpdate(() => {
      console.log('組件更新')
    })

    setTimeout(() => {
      count.value++
    }, 1000)
    return {
      count,
    }
  },

默認 pre ; watchEffect 比 組件更新 先打印
post: watchEffect 比 組件更新 后打印
sync 強制效果始終同步觸發, 然而這時低效的,很少需要

watch
watch API 完全等同于組件偵聽器 property。watch 需要偵聽特定的數據源,并在回調函數中執行副作用。默認情況下,它也是惰性的,即只有當被偵聽的源發生變化時才執行回調

watchEffect 比較,watch 允許我們:

  • 懶執行副作用;
  • 更具體地說明什么狀態應該觸發偵聽器重新運行;
  • 訪問偵聽狀態變化前后的值
    // 直接偵聽ref
    const count = ref(0)
    watch(count, (count, prevCount) => {
      console.log('watch1', count, prevCount)
    })

    setTimeout(() => {
      count.value = 22
    }, 2000)

    // 直接監聽 getter
    const state = reactive({ count: 0 })
    watch(
      () => state.count,
      (count, prevCount) => {
        console.log('watch2', count, prevCount)
      }
    )
     setTimeout(() => {
      state.count = 33
    }, 2000)

    // 監聽多個數據源
    const firstName = ref('')
    const lastName = ref('')

    watch([firstName, lastName], (newValues, prevValues) => {
      console.log(newValues, prevValues)
    })

    firstName.value = 'John' // logs: ["John",""] ["", ""]
    lastName.value = 'Smith' // logs: ["John", "Smith"] ["John", ""]

    // 監聽響應式對象
    const numbers = reactive([1, 2, 3, 4])

    watch(
      () => [...numbers],
      (numbers, prevNumbers) => {
        console.log(numbers, prevNumbers)
      }
    )

    numbers.push(5) // logs: [1,2,3,4,5] [1,2,3,4]


    // immediate deep 
    const state = reactive({ count: 0, obj: {name: 'zs'} })
    watch(
      () => state,
      (count, prevCount) => {
        console.log('watch2', state.obj.name)
      },
      {
        immediate: true, // 初始 立即執行
        deep: true //深 監聽  沒有 deep 不會答應 watch2
      }
    )
    setTimeout(() => {
      state.obj.name = 'ls'
    }, 2000)

watchEffect與watch 區別

  • watchEffect 不需要指定監聽的屬性,他會自動收集依賴, 只要我們回調中引用到了響應式的屬性, 就達到了監聽效果,而 watch 只能監聽指定的屬性而做出變更(v3開始可以同時指定多個)。
  • watch可以獲取到新值與舊值(更新前的值),而 watchEffect 是拿不到的。
  • watchEffect如果存在的話,在組件初始化的時候就會執行一次用以收集依賴(與computed同理),而后收集到的依賴發生變化,這個回調才會再次執行,而 watch 不需要,因為他一開始就指定了依賴
  • watchEffect會返回一個用于停止這個監聽的函數

提供/注入
我們也可以在組合式 API 中使用 provide/inject。兩者都只能在當前活動實例的 setup() 期間調用。

<!-- src/components/MyMap.vue -->
<template>
  <MyMarker />
</template>

<script>
import { provide, reactive, ref } from 'vue'
import MyMarker from './MyMarker.vue

export default {
  components: {
    MyMarker
  },
  setup() {
    const location = ref('North Pole')
    const geolocation = reactive({
      longitude: 90,
      latitude: 135
    })

    const updateLocation = () => {
      location.value = 'South Pole'
    }

    provide('location', location)
    provide('geolocation', geolocation)
    provide('updateLocation', updateLocation) // 注入修改方法
  }
}
</script>

<!-- src/components/MyMarker.vue -->
<script>
import { inject } from 'vue'

export default {
  setup() {
    const userLocation = inject('location', 'The Universe')
    const userGeolocation = inject('geolocation')
    const updateUserLocation = inject('updateLocation')

    return {
      userLocation,
      userGeolocation,
      updateUserLocation
    }
  }
}
</script>

其他響應式API

readonly
接受一個對象 (響應式或純對象) 或 ref 并返回原始對象的只讀代理。只讀代理是深層的:任何被訪問的嵌套 property 也是只讀的。

isProxy
檢查對象是否是由 reactive 或 readonly 創建的 proxy。

isReactive
檢查對象是否是由 reactive 創建的響應式代理。
如果該代理是 readonly 創建的,但包裹了由 reactive 創建的另一個代理,它也會返回 true。

isReadonly
檢查對象是否是由 readonly 創建的只讀代理。

toRaw
返回 reactive 或 readonly 代理的原始對象。這是一個“逃生艙”,可用于臨時讀取數據而無需承擔代理訪問/跟蹤的開銷,也可用于寫入數據而避免觸發更改。不建議保留對原始對象的持久引用。請謹慎使用。

markRaw
標記一個對象,使其永遠不會轉換為 proxy。返回對象本身。

shallowReactive
創建一個響應式代理,它跟蹤其自身 property 的響應性,但不執行嵌套對象的深層響應式轉換 (暴露原始值)。

shallowReadonly
創建一個 proxy,使其自身的 property 為只讀,但不執行嵌套對象的深度只讀轉換 (暴露原始值)。

unref
如果參數是一個 ref,則返回內部值,否則返回參數本身。這是 val = isRef(val) ? val.value : val 的語法糖函數。

isRef
檢查值是否為一個 ref 對象。

customRef
創建一個自定義的 ref,并對其依賴項跟蹤和更新觸發進行顯式控制。它需要一個工廠函數,該函數接收 track 和 trigger 函數作為參數,并且應該返回一個帶有 get 和 set 的對象。

<template>
  <input v-model="text" />
  {{text}}
</template>

<script>
import {customRef} from 'vue'
export default {
  setup() {
    function useDebouncedRef(value, delay = 500) {
      let timeout
      return customRef((track, trigger) => {
        return {
          get() {
            track()
            return value
          },
          set(newValue) {
            clearTimeout(timeout)
            timeout = setTimeout(() => {
              value = newValue
              trigger()
            }, delay)
          },
        }
      })
    }
    return {
      text: useDebouncedRef('hello'),
    }
  },
}
</script>

shallowRef
創建一個跟蹤自身 .value 變化的 ref,但不會使其值也變成響應式的。

triggerRef
手動執行與 shallowRef 關聯的任何副作用。

<script>
import { shallowRef, ref, watchEffect, triggerRef } from 'vue'
export default {
  setup() {
    // 會打印 兩次
    // const shallow = ref({
    //   greet: 'Hello, world',
    // })
    // watchEffect(() => {
    //   console.log(shallow.value.greet)
    // })
    // shallow.value.greet = 'Hello, universe'

    // 在triggerRef后才會打印第二次
    const shallow = shallowRef({
      greet: 'Hello, world',
    })
    watchEffect(() => {
      console.log(shallow.value.greet)
    })
    shallow.value.greet = 'Hello, universe'

    setTimeout(() => {
      triggerRef(shallow)
    }, 1500);      
  },
}
</script>
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容