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文件直接渲染
  • 热更新
相关文章
相关标签/搜索