【VUE】 Function-based API 嚐鮮

早在六月初,vue做者尤雨溪在知乎上發佈了一篇vue3.0的RFC [Vue Function-based API RFC], 這篇文章指明瞭在19年底即將發佈的vue3.0的初步路線。
複製代碼

RFC (Request For Comments),中文翻譯爲 "意見徵求稿"javascript

一. 設計目的

咱們能夠思考一下,爲何 vue團隊 要選擇function-based API?vue

1. 減小麪條代碼,提升靈活性。

  • 衆所周知, vue.js 的 api 對開發者十分的友好。在開發中,vue.js 的API強制要求開發者將組件代碼基於選項切分開來。java

  • 理想很美好,現實很骨感。缺少經驗的新手在項目不斷迭代中可能會將邏輯寫在同一個文件,使得代碼邏輯很是不易閱讀及抽離。react

  • 新的API制約不多,它提倡開發者根據邏輯去抽離成函數,經過返回值將邏輯數據返回回來,也不僅是能夠根據邏輯去抽離代碼,也能夠爲了寫出更好的漂亮代碼而抽離函數。webpack

2. 減小mixin,提升代碼重複利用率。

  • 在咱們的平常開發中,咱們發現一個組件變的很大的時候,會將組件拆分紅各個小組件,邏輯也會拆成一個個的 mixin 來複用(表格分頁mixin,對話框mixin等)。但在引入了多個 mixin 的狀況下,會出現引用賦值混亂/命名重複的困擾,雖然解決了快速開發的問題,但這使得事情更加的糟糕。
// mixin-mouse.js
export default {
  data () {
    return {
      x: 0,
      y: 0
    }
  },
  
  methods: {
    update(e) {
      x = e.pageX;
      y = e.pageY;
    }
  },
  
  mounted() {
    window.addEventListener('mousemove', this.update)
  },
  
  destroyed() {
    window.removeEventListener('mousemove', this.update)
  }
}
複製代碼
// mouse.js
import { binding, onMounted, onUnmouted } from "vue";
export const useMouse = () => {
  const x = binding(0)
  const y = binding(0)
  const update = e => {
    x.value = e.pageX
    y.value = e.pageY
  }
  onMounted(() => {
    window.addEventListener('mousemove', update)
  })
  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
  })
  return { x, y }
}
複製代碼
// app.js
import { binding } from "vue";
import { useMouse } from "./mouse";

export default {
  setup () {
    const { x, y } = useMouse();
    return { x, y };
  }
}
複製代碼

引入的時候,開發者可以更清晰的識別及處理合並進來的值,這樣更容易維護。git

二. 方案

1. setup函數

  • 這個函數將會是咱們 setup 咱們組件邏輯的地方,它會在一個組件實例被建立時,初始化了 props 以後調用。setup() 會接收到初始的 props 做爲參數:
import { reactive, toBindings } from "vue";

export default {
  // props, ctx不必定會有
  setup(props) {
    const state = reactive({
      name: "lxs"
    });
    
    return {
      ...toBindings(state)
    }
  }
}
複製代碼
  • 尤大大指出,setup 函數裏面可使用 this 獲取到當前上下文,但不必用。因此liximomo的vue-function-api版本,setup 函數裏面的 this 爲 undefined,並且不只僅有 props 參數,還有 context 參數,裏面有如下參數:github

    attrs: Object
    emit: ƒ ()
    parent: VueComponent
    refs: Object
    root: Vue
    slots: Objectweb

export default {
  props: {
    visible: {
      type: Boolean,
      default: false
    }
  },
  
  setup(props, { attrs, emit, parent, refs, root, slots }) {
    return {}
  }
}

複製代碼

2. Binding函數 -> value

binding()返回的是一個包裝對象(value wrapper), 裏面只有一個 .value 屬性,該屬性指向內部被包裝的值。算法

import { reactive, binding, isBinding, toBindings } from "vue";

export default {
  setup() {
    const name = binding("lxs");
    
    console.log(name);
    // ValueWrapper {
    // value: Object
    // _internal: {__ob__: Observer}
    // __proto__: AbstractWrapper
    // } 
    
    const state = reactive({
      name: "test"
    })
    
    const changeName = () => {
      if (isBinding(name)) {
        name.value = "new lxs";
      }
    }
    
    return {
      ...toBindings(state),
      name,
      changeName
    }
  }
}
複製代碼

binding函數附帶了兩個功能性函數:vue-router

isBinding: 判斷是否爲包裝對象
toBindings: 將原始值轉化成包裹對象
複製代碼

這裏引起兩個思考

爲何須要包裝對象?
  • 咱們知道在 JavaScript 中,原始值類型如 string 和 number 是隻有值,沒有引用的。若是在一個函數中返回一個字符串變量,接收到這個字符串的代碼只會得到一個值,是沒法追蹤原始變量後續的變化的。

  • 所以,包裝對象的意義就在於提供一個讓咱們可以在函數之間以引用的方式傳遞任意類型值的容器。這有點像 React Hooks 中的 useRef —— 但不一樣的是 Vue 的包裝對象同時仍是響應式的數據源。有了這樣的容器,咱們就能夠在封裝了邏輯的組合函數中將狀態以引用的方式傳回給組件。組件負責展現(追蹤依賴),組合函數負責管理狀態(觸發更新)

  • 聲明數據和更新數據更加清晰。

在template下時候須要使用.value來展開數據?
  • 否。雖然在 setup() 中返回的是一個包裝對象,可是在模版渲染的過程或者嵌套在另外一個包裝對象的時候,若是判斷類型爲包裝對象,都會被自動展開爲內部的值。

3. Reactive函數 -> state

  • reactive() 返回一個沒有包裝的響應式對象,等同於 vue 2.6 版本之後的 Vue.observable() 函數。Vue.observable() 提供了讓 data 塊裏面的值可以在外面定義的功能。
import { reactive } from 'vue'

const object = reactive({
  count: 0
})

object.count++
複製代碼

當一個包裝對象被做爲另外一個響應式對象的屬性引用的時候也會被自動展開:

const count = binding(0)
const obj = reactive({
  count
})

console.log(obj.count) // 0

obj.count++
console.log(obj.count) // 1
console.log(count.value) // 1

count.value++
console.log(obj.count) // 2
console.log(count.value) // 2

複製代碼
  • 以上這些關於包裝對象的細節可能會讓你以爲有些複雜,但實際使用中你只須要記住一個基本的規則:只有當你直接以變量的形式引用一個包裝對象的時候纔會須要用.value 去取它內部的值 —— 在模版中你甚至不須要知道它們的存在。

4. Computed value

除了直接包裝一個可變的值,咱們也能夠包裝經過計算產生的值:

import { binding, computed } from "vue";

import dayjs from "dayjs";
export const timeHook = () => {
   const time = binding(new Date().getTime());
   
   const formatTime = computed(() => time.value, val => {
     return dayjs(val).format("YYYY-MM-DD hh:mm:ss");
   });
   
   return {
       time,
       formatTime
   }
}
複製代碼
  • 計算值的行爲跟計算屬性 (computed property) 同樣:只有當依賴變化的時候它纔會被從新計算。

  • computed()返回的是一個只讀的包裝對象,它能夠和普通的包裝對象同樣在 setup()中被返回 ,也同樣會在渲染上下文中被自動展開。默認狀況下,若是用戶試圖去修改一個只讀包裝對象,會觸發警告。

  • 雙向計算值能夠經過傳給computed 第二個參數做爲 setter來建立:

import { binding, computed } from "vue";
import dayjs from "dayjs";

export const timeHook = () => {
   const time = binding(new Date().getTime());
   
   const formatTime = computed(
     // read
     () => time.value + 2000, 
     // write
     val => {
     return dayjs(val).format("YYYY-MM-DD hh:mm:ss");
   });
   
   return {
       time,
       formatTime
   }
}
複製代碼

5. Watchers

watch()函數與舊API的 $watch同樣提供了觀察狀態變化的能力。它的做用相似React Hooks 的 useEffect,但實現原理和調用時機其實徹底不同。

不一樣之處:

1. 它能夠接收多種數據源:
一個返回任意值的函數
一個包裝對象
一個包含上述兩種數據源的數組
複製代碼
2. watch()函數的回調,會在建立時就執行一次,至關於2.x的 watcher 的immediate: true
3. 默認狀況下,watch()的回調在觸發時,DOM總會在一個更新過的狀態。
export default {
  props: {
    tableHeight: {
      type: [String, Number],
      default: 200
    }
   },
   
  setup(props) {
    watch(
      () => props.tableHeight, 
       val => {
        console.log("DOM render flush")
      }, 
      {
        flush: 'post', // default, fire after renderer flush
        flush: 'pre', // fire right before renderer flush
        flush: 'sync' // fire synchronously
      }
    )    
  }
}

複製代碼
4. 觀察多個數據源
  • 任意一個數據源發生變化時,回調函數都會被觸發
watch(
  [valueA, () => valueB.value],
  ([a, b], [prevA, prevB]) => {
    console.log(`a is: ${a}`)
    console.log(`b is: ${b}`)
  }
)

複製代碼
5. 中止觀察
const stop = watch(...)

// stop watching
stop()
複製代碼
  • 若是 watch()是在一個組件的setup() 或是生命週期函數中被調用的,那麼該 watcher 會在當前組件被銷燬時也一同被自動中止,不然將要本身自動中止。
6. 清理反作用
  • 有時候當觀察的數據源變化後,咱們可能須要對以前所執行的反作用進行清理。舉例來講,一個異步操做在完成以前數據就產生了變化,咱們可能要撤銷還在等待的前一個操做。爲了處理這種狀況,watcher 的回調會接收到的第三個參數是一個用來註冊清理操做的函數。調用這個函數能夠註冊一個清理函數。清理函數會在下屬狀況下被調用,咱們常常須要在 watcher 的回調中用async function 來執行異步操做:

    在回調被下一次調用前
      在 watcher 被中止前
    複製代碼
watch(idValue, (id, oldId, onCleanup) => {
  const token = performAsyncOperation(id)
  onCleanup(() => {
    // id 發生了變化,或是 watcher 即將被中止.
    // 取消還未完成的異步操做。
    token.cancel()
  })
})
複製代碼
const data = value(null)
watch(getId, async (id) => {
  data.value = await fetchData(id)
}

複製代碼

生命週期函數

  • 全部現有的生命週期鉤子都會有對應的 onXXX 函數(只能在setup() 中使用)-> destroyed 調整爲 unmounted
import { onMounted, onUpdated, onUnmounted } from 'vue'

const MyComponent = {
  setup() {
    onMounted(() => {
      console.log('mounted!')
    })
    onUpdated(() => {
      console.log('updated!')
    })
    // destroyed 調整爲 unmounted
    onUnmounted(() => {
      console.log('unmounted!')
    })
  }
}
複製代碼
  • 現有的 vue-function-api是在 install 裏面 設置Vue.config.optionMergeStrategies.setup,讓 setup 函數在beforeCreate 中注入。
// setup.ts
Vue.mixin({
  beforeCreate: functionApiInit,
});

複製代碼

依賴注入

// hook.js
import { provide, binding } from 'vue'

export const randomKeyHook = () => {
  const randomKey = binding(
    Math.random()
      .toString(36)
      .substr(2)
  )

  provide({
    randomKey
  })

  return {
    randomKey
  }
}
複製代碼
import { inject } from 'vue';

export default {
  setup() {
  
    const randomKey = inject('randomKey')
    return {
      randomKey
    }
  }
}
複製代碼
  • 若是注入的是一個包裝對象,則該注入綁定會是響應式的

Typescript支持

正確類型推導

  • 3.0 的一個主要設計目標是加強對 TypeScript 的支持。本來vue團隊指望經過Class API 來達成這個目標,可是通過討論和原型開發,vue團隊認爲 Class 並非解決這個問題的正確路線,基於 Class 的 API 依然存在類型問題。

  • 基於函數的 API 自然對類型推導很友好,由於TS對函數的參數、返回值和泛型的支持已經很是完備。更值得一提的是基於函數的 API 在使用 TS 或是原生 JS 時寫出來的代碼幾乎是徹底同樣的。

三. 調整

標準版剔除如下選項

  1. el (應用將再也不由 new Vue()來建立,而是經過新的 createApp 來建立,)
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)
複製代碼
  1. 疑似要廢除.sync, 添加 v-model 指令參數

可替代項

data(由 setup() + binding) + reactive) 取代)
computed(由 computed 取代)
methods( 由 setup() 中聲明的函數取代)
watch (由 watch() 取代)
provide/inject(由 provide() 和 inject() 取代)
mixins (由組合函數取代)
extends (由組合函數取代)
複製代碼
  • 全部的生命週期選項 (由 onXXX 函數取代)

四. vue-router 的 Functional API 猜測

以依賴注入的形式

// main.ts
import { provideRouter, useRouter } from 'vue-router'
import router from './router'

new Vue({
  setup() {
    provideRouter(router)    
  }
})

複製代碼
// ... in a child component
export default {
  setup() {
    const { route /*, router */ } = useRouter()
    const isActive = computed(() => route.name === 'myAwesomeRoute')
    return {
      isActive
    }
  }
}
複製代碼

以hook的形式導給上下文

import router from './router'

new Vue({
  setup() {
    router.use()
  }
})

複製代碼

五. vuex 的 Functional API 猜測

採用函數轉化成module

// useState, useGetter, useMutation, useAction, useModule 
import { useModule } from 'vuex'
import { value, computed } from 'vue';
export const itemsModule = () => {
    const items = value([])
    const size = computed(() => items.value.length);

    const addItem = (content) => {
        items.value.push(content)
    }
    return {
        items,
        size,
        addItem
    }
}

useModule(itemsModule)
複製代碼

效仿nuxt.js,在vue-cli4中提供統一的 store 目錄,若是store 目錄存在,程序將本身作如下事情

引用 vuex 模塊
將 vuex 模塊 加到 vendors 構建配置中去
設置 Vue 根實例的 store 配置項
複製代碼

六. 和React的區別

  • 尤大大文章剛出的時候,不少技術帖子對於 vue3.0 的語法更新表示不滿,他們表示,若是是大規模模仿react,倒不如去學習 react,這樣也是學習新的語法。

  • 這樣大錯特錯,vuereact 的實現有很大的差異, vue 實際上模仿的是hooks,而不是 react

Template 機制仍是沒變

  • vue仍是將 TemplateSetup 分開,react 則是寫在了一塊兒。

減小 GC 壓力

  • vuesetup 函數只在初始化以前執行一次,而 React 在更新數據的時候,都會執行一次 render

  • vuehooksmutable 深度結合,在數據更新的時候,經過包裝對象的 obj.value,在obj的數據變動的時候,引用保持不變,只有值改變了,vue 經過 新的API Proxy 監聽數據的變化,能夠作到 setup 函數不從新執行。而Template 的重渲染則徹底繼承 Vue 2.0的依賴收集機制,它無論值來自哪裏,只要用到的值變了,就能夠從新渲染了。

  • React Hooks 存在的問題:全部Hooks都在渲染閉包中執行,每次重渲染都有必定性能壓力,並且頻繁的渲染會帶來許多閉包,雖然能夠依賴 GC 機制回收,但會給 GC 帶來不小的壓力。

v8的垃圾回收算法

  從宏觀上來看,V8的堆分爲3部分,年輕分代/年老分代/大對象空間。對於各類類型,v8有對應的處理方式:

  1. 年輕分代(短暫)

 分爲兩堆,一半使用,一半用於垃圾清理。在堆中分配而內容不夠時,會將超出生命期的垃圾對象清除出去,釋放掉空間。

  2. 年老分代 
 主要類型:

 (1) 從年輕分代中移動過來的對象
 (2) JIT (即時編輯器) 以後產生的代碼
 (3) 全局對象

 標記清除和標記整理算法將可清除的垃圾對象和有效的對象區分開來,但超過度配給年老分代但空間時,V8會清除垃圾代碼,造成了碎片的內存塊,而後壓縮內存塊。固然,這個過程是走走停停的。(上述問題)

  3. 大對象空間

 整塊分配,一次性回收。
複製代碼

七. 總結

  • vue3.0 對 vue 的主要3個特色:響應式、模板、對象式的組件聲明方式,進行了全面的更改,底層的實現和上層的api都有了明顯的變化,基於 Proxy從新實現了響應式,基於 treeshaking 內置了更多功能,提供了類式的組件聲明方式。並且源碼所有用 Typescript 重寫。以及進行了一系列的性能優化。

  • 取其精華,去其糟糠, vue團隊但願剔除掉代碼靈活性差的帽子,大膽的改變了原有的開發模式,更加親和於 vue生態javascript 生態。vue一直在不斷的進步,讓開發者減小框架的約束,放飛開發者的思想。

  • 感謝 vue 團隊一向的高出產率~

八. 觀察

2019-07-29在github上的截的路線圖

1. vue對兼容模式的開發目前完成了80%;
2. 還沒有進行破壞性更新的討論;
3. vue3.0會伴着vue-cli4的出現 -> webpack5;
4. 各生態目前正在同步更新;
5. Q3季度將要開始vue3.0的測試;
等
複製代碼

期待吧~

參考文章:

尤雨溪 - Vue Function-based API RFC (https://zhuanlan.zhihu.com/p/68477600)
黃子毅 - 精讀《Vue3.0 Function API》(https://zhuanlan.zhihu.com/p/71667382)
vuejs - rfcs (https://github.com/vuejs/rfcs/issues)
複製代碼

ps: 我的公衆號求加個閱讀量哈,二維碼以下:

相關文章
相關標籤/搜索