Vue3新特性一篇搞懂

29,857 阅读14分钟

欢迎大家加入一起共同学习进步。

最新消息和优秀文章我会第一时间推送的。

视频讲解 b站视频 www.bilibili.com/video/BV1fa…

关于发布时间

具体时间可以看大家可以看看官方时间表。 官方时间表

目前在Vue3处于Beta版本,后面主要是处理稳定性问题。也就是说主要Api不会有很多改进。尤大神从直播中说虽然很多想法,但是大的变化最快也会出现在3.1上面了。所以目前的版本应该应该和正式版差异很小了。 看来Q2能发布的可能性极大。

脑图

>>>>>>>️脑图链接<<<<<<<

Vue3全家桶地址

ProjectStatus
vue-routerAlpha [Proposed RFCs] [GitHub] [npm]
vuexAlpha, with same API [GitHub] [npm]
vue-class-componentAlpha [Github] [npm]
vue-cliExperimental support via vue-cli-plugin-vue-next
eslint-plugin-vueAlpha [Github] [npm]
vue-test-utilsAlpha [Github] [npm]
vue-devtoolsWIP
jsxWIP

优雅管理源码版本

为了可以在测试项目中引用vue3源码,并且在下次版本更新中可以优雅的进行升级。 我决定使用git的子模块功能来进行管理

git子模块介绍 blog.csdn.net/guotianqing…

初始化子模块

git submodule add https://github.com/vuejs/vue-next source/vue-next

子模块内容记录在.gitmodules文件中

# 更新模块
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>

浏览器中断点调试

安装依赖编译源码

直接通过浏览器就可以打开本地文件

可以试一下点击的效果 接下来如果你要debug一下源码的时候你会发现代码是经过打包的无法直接在源码上打断点调试

添加SourceMap文件

为了能在浏览器中看到源码 并能够断点调试 需要再打包后代码中添加sourcemap

 # 设置环境变量 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

Vue3Vue3
beforeCreatesetup(替代)
createdsetup(替代)
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeDestroyonBeforeUnmount
destroyedonUnmounted
errorCapturedonErrorCaptured

完整代码实现

通过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的全面支持已经是不可逆转的趋势了,这都不是事。

为了对比理解Vue2、3的响应式实现的不同我把两种实现都写了一下,并且配上了jest测试。大家可以参考一下 github.com/su37josephx…

// clone代码
yarn
npx jest reactivity-demo

自定义渲染器

这个自定义渲染器和React的Render很类似。 可以根据需要定义各种各样的渲染器

具体内容近期更新

新工具vite

我们知道ES6语法中 import在浏览器中完全可用。可以用于加载后端资源只不过这个特性一直被我们忽略。可能是由于webpack搞得太好了。我们一直忽略了他的存在。 Vite正是利用这个特性,只不过又更进一步,提供了对于vue文件的支持。不过可能是有点前卫。

  • 简易Http服务器
  • 无需webpack
  • Vue文件直接渲染
  • 热更新