Vue3新特性一篇搞懂

歡迎你們加入一塊兒共同窗習進步。

最新消息和優秀文章我會第一時間推送的。

視頻講解 b站視頻 www.bilibili.com/video/BV1fa…javascript

關於發佈時間

具體時間能夠看你們能夠看看官方時間表。 官方時間表css

目前在Vue3處於Beta版本,後面主要是處理穩定性問題。也就是說主要Api不會有不少改進。尤大神從直播中說雖然不少想法,可是大的變化最快也會出如今3.1上面了。因此目前的版本應該應該和正式版差別很小了。 看來Q2能發佈的可能性極大。html

腦圖

>>>>>>>️腦圖連接<<<<<<<vue

Vue3全家桶地址

Project Status
vue-router Alpha [Proposed RFCs] [GitHub] [npm]
vuex Alpha, with same API [GitHub] [npm]
vue-class-component Alpha [Github] [npm]
vue-cli Experimental support via vue-cli-plugin-vue-next
eslint-plugin-vue Alpha [Github] [npm]
vue-test-utils Alpha [Github] [npm]
vue-devtools WIP
jsx WIP

優雅管理源碼版本

爲了能夠在測試項目中引用vue3源碼,而且在下次版本更新中能夠優雅的進行升級。 我決定使用git的子模塊功能來進行管理java

git子模塊介紹 blog.csdn.net/guotianqing…react

初始化子模塊

git submodule add https://github.com/vuejs/vue-next source/vue-next
複製代碼

子模塊內容記錄在.gitmodules文件中linux

# 更新模塊
git submodule init
# 更新模塊
git submodule update --init --recursive
複製代碼

編譯源代碼

安裝依賴

## 修改鏡像
yarn config set registry https://registry.npm.taobao.org --global
yarn config set disturl https://npm.taobao.org/dist --global

## 去除pupteer
# 忽略下載Chromium
yarn --ignore-scripts
複製代碼

打包編譯

yarn dev
複製代碼

HelloWorld

經典API

<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <script src="../../source/vue-next/packages/vue/dist/vue.global.js"></script>
</head>

<body>
    <div id='app'></div>
    <script> const App = { template: ` <button @click='click'>{{message}}</button> `, data() { return { message: 'Hello Vue 3!!' } }, methods: { click() { console.log('click ....', this.message) this.message = this.message.split('').reverse().join('') } } } let vm = Vue.createApp(App).mount('#app') // console.log(vm) </script>
</body>

</html>
複製代碼

複合API

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <script src="../../source/vue-next/packages/vue/dist/vue.global.js"></script>
</head>

<body>
    <div id="app"></div>
    <script> const { createApp, reactive,watchEffect } = Vue const App = { template: ` <button @click="click"> {{ state.message }} </button> `, setup() { const state = reactive({ message:'Hello Vue 3!!' }) watchEffect(() => { console.log('state change ', state.message) }) function click() { state.message = state.message.split('').reverse().join('') } return { state, click } } } createApp(App).mount('#app') </script>
</body>

</html>

複製代碼

瀏覽器中斷點調試

安裝依賴編譯源碼

直接經過瀏覽器就能夠打開本地文件webpack

能夠試一下點擊的效果 接下來若是你要debug一下源碼的時候你會發現代碼是通過打包的沒法直接在源碼上打斷點調試git

添加SourceMap文件

爲了能在瀏覽器中看到源碼 並可以斷點調試 須要再打包後代碼中添加sourcemapgithub

# 設置環境變量 windows
 set SOURCE_MAP=true && yarn dev
 # 設置環境變量 mac/linux
 export SOURCE_MAP=true && yarn dev
複製代碼

CompositionApi介紹

Alpha源碼。這一篇咱們來嚐鮮一下3.0版中最重要的一個RFC CompositionAPI。

概念

CompositionAPI被若是隻是字面的意思能夠被翻譯成組合API。他之前的名字是Vue Function-based API ,我認爲如今的名字更爲達意。本質上CompositionAPI就是爲了更爲方便的實現邏輯的組合而生的。

回顧歷史

Vue2若是要在組件中實現邏輯的符合,譬如全部按鈕組件都要實現防抖,可選的方式大概有如下三個:

  • Mixins
  • 高階組件 (Higher-order Components, aka HOCs)
  • Renderless Components (基於 scoped slots / 做用域插槽封裝邏輯的組件

但三者都不是很是的理想,主要問題存在

  • 模板數據來源不清晰, 譬如mixin光看模板很難分清一個屬性是哪裏來的。
  • 命名空間衝突
  • 性能問題。 譬如HOC須要額外的組件邏輯嵌套 會致使無謂的性能開銷。

可是我更在乎的是對於邏輯的組合這種原始的編程行爲我不得不引入其餘概念來處理。固然這個也是爲何不少從java轉過來的架構師更喜歡react的緣由。vue算是笑着進去哭着出來的語言。入門好像很容易看看helloworld就能夠工做了,但一遇到問題就須要引入一個新概念。不像React函數即組件那麼清爽天然。

動機

首先先看一下

const App = {
            template: ` <button @click='click'>{{message}}</button> `,
            data() {
                return {
                    message: 'Hello Vue 3!!'
                }
            },
            methods: {
                click() {
                    console.log('click ....', this.message)
                    this.message = this.message.split('').reverse().join('')
                }
            }
        }
        let vm = Vue.createApp().mount(App, '#app')
複製代碼

options api源代碼位置

經典的Vue API能夠概括爲options API,能夠理解爲基於配置的方式聲明邏輯的API。啥意思基本是已定義爲基礎的。想想vue2的helloworld是否是好像只是完成了幾個簡單的定義就能夠實現功能。我認爲這個也是爲何vue那麼流行的緣由 對於描述通常的邏輯的確很是簡單。固然這也和尤大神是從設計師出身有很大的關係。別讓了css和html語言是徹頭徹尾的定義性代碼。

可是一旦業務邏輯複雜的話這種表達方式就會存在必定問題。由於邏輯一旦複雜你不能給他寫成一坨,必需要考慮如何組織,你要考慮抽取邏輯中的共用部分考慮複用問題,否則程序將變成很是難以維護。上文中已經提到了哪三種複用方式一方面來說須要由於全新的概念另外確實編程體驗太差還有性能問題。

CompositionAPI的靈感來源於React Hooks的啓發(這個是尤大認可的)。好的東西須要借鑑這個你們不要鄙視鏈。使用函數組合API能夠將關聯API抽取到一個組合函數 該函數封裝相關聯的邏輯,並將須要暴露給組件的狀態以響應式數據源的形式返回。

實戰

好了上代碼,第一段邏輯是尤大的邏輯鼠標位置監聽邏輯

// 尤大神的經典例子 鼠標位置偵聽邏輯 
    function useMouse() {
            const state = reactive({
                x: 0,
                y: 0
            })
            const update = e => {
                state.x = e.pageX
                state.y = e.pageY
            }
            onMounted(() => {
                window.addEventListener('mousemove', update)
            })
            onUnmounted(() => {
                window.removeEventListener('mousemove', update)
            })
            return toRefs(state)
        }

複製代碼

咱們還想組合另一段邏輯 好比隨時刷新的時間邏輯

function useOtherLogic() {
            const state = reactive({
                time: ''
            })
            onMounted(() => {
                setInterval(() => {
                    state.time = new Date()
                }, 1000)
            })
            return toRefs(state)
        }
複製代碼

在實際的工做中咱們能夠認爲這兩個邏輯可能被不少組件複用,想一想你要是用mixin和hoc你將多麼無所是從。可是利用CompositionAPI,咱們只須要把他組合並暴露 like this

const MyComponent = {
            template: `<div>x:{{ x }} y:{{ y }} z:{{ time }} </div>`,

            setup() {
                const {
                    x,
                    y
                } = useMouse()
                // 與其它函數配合使用
                const {
                    time
                } = useOtherLogic()

                return {
                    x,
                    y,
                    time
                }
            }
        }
        createApp().mount(MyComponent, '#app')
複製代碼

呃呃 這個好像就是react hooks 不要緊啦好用就能夠啦。。。 完整的例子歡迎star

完整API介紹

咱們先看看Vue3的基礎API都有哪些?

const {
            createApp,
            reactive, // 建立響應式數據對象
            ref, // 建立一個響應式的數據對象
            toRefs, // 將響應式數據對象轉換爲單一響應式對象
            isRef, // 判斷某值是不是引用類型
            computed, // 建立計算屬性
            watch, // 建立watch監聽
            // 生命週期鉤子
            onMounted,
            onUpdated,
            onUnmounted,
        } = Vue
複製代碼

setup使用composition API的入口

setup函數會在 beforeCreate以後 created以前執行

setup(props,context){
    console.log('setup....',)
    console.log('props',props) // 組件參數
    console.log('context',context) // 上下文對象
} 

複製代碼

好的其實你們能夠和本身原來的React偶不Vue2代碼對號入座。

reactive

reactive() 函數接受一個普通對象 返回一個響應式數據對象

const state = reactive({
        count: 0,
        plusOne: computed(() => state.count + 1)
    })
複製代碼

ref 與 isRef

  • ref 將給定的值(確切的說是基本數據類型 ini 或 string)建立一個響應式的數據對象
  • isRef 其實就是判斷一下是否是ref生成的響應式數據對象

首先這裏面有一個重要的概念叫包裝對象(value wrapper),談到wrapper從java那邊轉過來的小朋友確定記得java裏面的wrapclass其實概念差很少啦。咱們知道基本數據類型只有值沒有引用,這樣也就形成了一個問題返回一個基礎數據類型好比一個字符串是沒法跟蹤他的狀態的,因此咱們就須要講基礎數據類型包裝一下,這有點像ReactHooks中的useRef,可是Vue包裝的對象自己就是響應式數據源。好了咱們看一下實例理解一下

// 定義建立響應式數據
    const time = ref(new Date())
    // 設置定時器爲了測試數據響應
    setInterval(() => time.value = new Date(), 1000)

    // 判斷某值是不是響應式類型
    console.log('time is ref:', isRef(time))
    console.log('time', time)
    console.log('time.value', time.value)
    
    // 咱們看看模板裏面咱們這樣展現
    template: ` <div> <div>Date is {{ time }}</div> </div> `
複製代碼

toRefs

  • toRefs 能夠將reactive建立出的對象展開爲基礎類型
// 若是不用toRefs
    const state = reactive({
        count: 0,
        plusOne: computed(() => state.count + 1)
    })
    return {
        state
    }
    // 模板渲染要這樣寫
    template: ` <div> <div>count is {{ state.count }} </div> <div>plusOne is {{ state.plusOne }}</div> </div> `
    
    // 咱們再看看用了toRefs
    const state = reactive({
        count: 0,
        plusOne: computed(() => state.count + 1)
    })
    return {
        ...toRefs(state)
    }
    // 模板渲染要這樣寫
    template: ` <div> <div>count is {{ count }} </div> <div>plusOne is {{ plusOne }}</div> </div> `
複製代碼

watch 定義監聽器

這個其實沒有什麼新東西

watch(() => state.count * 2, val => {
        console.log(`count * 2 is ${val}`)
    })
複製代碼

effect 反作用函數

響應式對象修改會觸發這個函數

// 反作用函數
    effect(() => {
        console.log('數值被修改了..',state.count)
    })
複製代碼

computed 計算屬性

const state = reactive({
    count: 0,
    plusOne: computed(() => state.count + 1)
})
複製代碼

生命週期鉤子Hooks

Vue3 Vue3
beforeCreate setup(替代)
created setup(替代)
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeDestroy onBeforeUnmount
destroyed onUnmounted
errorCaptured onErrorCaptured

完整代碼實現

經過Jest深度瞭解源碼

如今準備向原理源碼進軍了。有個小問題先要處理一下。就是研究一下如何把Vue3的單元測試跑起來。畢竟光讀代碼不運行是沒有靈魂的。歪歪一下中國的球迷是否是就是光看不踢。

Vue3代碼是基於Jest進行測試,咱們先簡單看看什麼是jest

Jest簡介

Jest 是Facebook的一個專門進行Javascript單元測試的工具,適合JS、NodeJS使用,具備零配置、內置代碼覆蓋率、強大的Mocks等特色。

總之目前來說JS界Jest是一套比較成體系的測試工具。爲何這麼說呢好比拿之前的測試框架Mocha對比 他只是一個測試框架,若是你須要斷言還須要專門的斷言庫好比assert shoud expect等等 若是須要Mock還須要住啊們的庫來支持很不方便。不過Jest基本上能夠一次性搞定。

目錄文件名約定

Jest測試代碼和邏輯代碼是聽從約定優於配置(convention over configuration)其實這個也是目前編程世界廣泛接受的原則。

Jest的測試代碼是基於如下約定

  • 測試文件名要以spec結果
  • 測試文件後綴爲js,jsx,ts,tsx
  • 測試文件須要放在tests/unit/目錄下或者是/tests/目錄下

只要知足這三個要求的測試文件,使用運行jest時就會自動執行。

其實這個規定相似於Maven對於測試代碼和邏輯代碼的約定只是test目錄換成了__tests__

下面咱們具體看一下Vue3源碼的目錄結構

其實邏輯代碼和測試代碼對應放置仍是很方便的 咱們再看看另一個reactive這個包

運行全量測試

package.json文件中已經配置好了jest

npm run test
複製代碼

覆蓋率

咱們增長一個參數把覆蓋率跑出來

npx jest --coverage 
複製代碼

實際上跑覆蓋率的時候是有錯的 咱們先不去管他 咱們先解析一下這個報告怎麼看,若是你們學過軟件工程會知道通常從理論上講覆蓋率包括

  • 語句覆蓋
  • 節點覆蓋
  • 路徑覆蓋
  • 條件組合覆蓋

可是通常來說不一樣框架理解不同 在Jest這裏大概是這樣分解的

  • %stmts是語句覆蓋率(statement coverage):是否是每一個語句都執行了?
  • %Branch分支覆蓋率(branch coverage):是否是每一個if代碼塊都執行了?
  • %Funcs函數覆蓋率(function coverage):是否是每一個函數都調用了?
  • %Lines行覆蓋率(line coverage):是否是每一行都執行了?

單獨運行一個測試

好比咱們看看vue的index這個測試

有兩種方法進行單獨測試

// 全局安裝
npm i jest -g
jest index

// 或者更更簡便一點
npx jest index
複製代碼

index.spec.ts

import { createApp } from '../src'

it('should support on-the-fly template compilation', () => {
  const container = document.createElement('div')
  const App = {
    template: `{{ count }}`,
    data() {
      return {
        count: 0
      }
    }
  }
  createApp().mount(App, container)
  // 斷言 
  expect(container.innerHTML).toBe(`0`)
})
複製代碼

聲明中說爲了確認模板編譯能夠生效,這個就是一個簡單的數據綁定 最後 斷言也是看了一下 count是否爲 0 這個例子其實除了斷言部分其實直接拷貝到第一次講的那個html文件裏面是能夠運行的。

響應式Reactive的單元測試

看一下每一個包對應的測試代碼都放在__tests__文件件中

npx jest reactive --coverage
複製代碼

好了後面咱們就能夠開始向源碼進軍了

代碼結構

源碼位置是在package文件件內,實際上源碼主要分爲兩部分,編譯器和運行時環境。

  • 編譯器

    • compiler-core 核心編譯邏輯

      • 基本類型解析
      • AST
    • compiler-dom 針對瀏覽器的編譯邏輯

      • v-html
      • v-text
      • v-model
      • v-clock
  • 運行時環境

    • runtime-core 運行時核心
      • inject
      • 生命週期
      • watch
      • directive
      • component
    • runtime-dom 運行時針對瀏覽器的邏輯
      • class
      • style
    • runtime-test 測試環境仿真

      主要爲了解決單元測試問題的邏輯 在瀏覽器外完成測試比較方便

  • reactivity 響應式邏輯

  • template-explorer 模板解析器 能夠這樣運行

    yarn dev template-explorer
    複製代碼

    而後打開index.html

  • vue 代碼入口

    整合編譯器和運行時

  • server-renderer 服務器端渲染(TODO)

  • share 公用方法

Vue2和Vue3響應方式對比

Vue2響應式是什麼

首先咱們說說什麼是響應式。經過某種方法能夠達到數據變了能夠自由定義對應的響應就叫響應式。

具體到咱們MVVM中 ViewModel的須要就是數據變了須要視圖做出響應。 若是用Jest用例便表示就是這樣

it('測試數據改變時 是否被響應', () => {
        const data = reactive({
            name: 'abc',
            age: {
                n: 5
            }
        })
        // Mock一個響應函數
        const fn = jest.fn()
        const result = fn()

        // 設置響應函數
        effect(fn)

        // 改變數據
        data.name = 'efg'

        // 確認fn生效
        expect(fn).toBeCalled()
    })
複製代碼

假定咱們須要的是數據data變化時能夠觸發fn函數也就是做出相應,固然相應通常是觸發視圖更新固然也能夠不是。咱們這裏面用jest作了一個Mock函數來檢測是否做出相應。

最後代碼expect(fn).toBeCalled()有效即表明測試經過也就是做出了相應

Vue2的解決方案

下面展現的是vue2的實現方式是經過Object.defineProperty來從新定義getter,setter方法實現的。

let effective
function effect(fun) {
    effective = fun
}

function reactive(data) {
    if (typeof data !== 'object' || data === null) {
        return data
    }


    Object.keys(data).forEach(function (key) {
        let value = data[key]
        Object.defineProperty(data, key, {
            emumerable: false,
            configurable: true,
            get: () => {
                return value
            },
            set: newVal => {
                if (newVal !== value) {
                    effective()
                    value = newVal
                }
            }
        })

    })
    return data
}

module.exports = {
    effect, reactive
}
複製代碼

固然還有兩個重要的問題須要處理 第一個就是這樣作只能作淺層響應 也就是若是是第二層就不行了。

it('測試多層數據中改變時 是否被響應', () => {
        const data = reactive({
            age: {
                n: 5
            }
        })
        // Mock一個響應函數
        const fn = jest.fn()

        // 設置響應函數
        effect(fn)

        // 改變多層數據
        data.age.n = 1

        // 確認fn生效
        expect(fn).toBeCalled()
    })
複製代碼

好比如下用例 就過不去了 固然解決的辦法是有的 遞歸調用就行了

固然這樣也遞歸也帶來了性能上的極大損失 這個你們先記住。

而後是數組問題 數組問題咱們能夠經過函數劫持的方式解決

const oldArrayPrototype = Array.prototype
const proto = Object.create(oldArrayPrototype);

['push','pop','shift','unshift','splice','sort','reverse'].forEach(method => {
    
    // 函數劫持
    proto[method] = function(){
        effective()
        oldArrayPrototype[method].call(this,...arguments)
    }
})
// 數組經過數據劫持提供響應式
if(Array.isArray(data)){
    data.__proto__ = proto
}

複製代碼

Vue3

新版的Vue3使用ES6的Proxy方式來解決這個問題。以前遇到的兩個問題就簡單的多了。首先Proxy是支持數組的也就是數組是不須要作特別的代碼的。對於深層監聽也不沒必要要使用遞歸的方式解決。當get是判斷值爲對象時將對象作響應式處理返回就能夠了。你們想一想這個並不不是發生在初始化的時候而是設置值得時候固然性能上獲得很大的提高。

function reactive(data) {
    if (typeof data !== 'object' || data === null) {
        return data
    }
    const observed = new Proxy(data, {
        get(target, key, receiver) {
            // Reflect有返回值不報錯
            let result = Reflect.get(target, key, receiver)

            // 多層代理
            return typeof result !== 'object' ? result : reactive(result) 
        },
        set(target, key, value, receiver) {
            effective()
            // proxy + reflect
            const ret = Reflect.set(target, key, value, receiver)
            return ret
        },

        deleteProperty(target,key){
            const ret = Reflect.deleteProperty(target,key)
            return ret
        }

    })
    return observed
}

複製代碼

固然目前仍是優缺點的缺點,好比兼容性問題目前IE11就不支持Proxy。不過相信ES6的全面支持已是不可逆轉的趨勢了,這都不是事。

爲了對比理解Vue二、3的響應式實現的不一樣我把兩種實現都寫了一下,而且配上了jest測試。你們能夠參考一下 github.com/su37josephx…

// clone代碼
yarn
npx jest reactivity-demo
複製代碼

自定義渲染器

這個自定義渲染器和React的Render很相似。 能夠根據須要定義各類各樣的渲染器

具體內容近期更新

新工具vite

咱們知道ES6語法中 import在瀏覽器中徹底可用。能夠用於加載後端資源只不過這個特性一直被咱們忽略。多是因爲webpack搞得太好了。咱們一直忽略了他的存在。 Vite正是利用這個特性,只不過又更進一步,提供了對於vue文件的支持。不過多是有點前衛。

  • 簡易Http服務器
  • 無需webpack
  • Vue文件直接渲染
  • 熱更新
相關文章
相關標籤/搜索