天天看点

Vue.js 3.0Vue.js 3.0 和2.0的区别Vue3.0的不同构建版本APIWatch

Vue.js 3.0 和2.0的区别

源码组织方式的变化

  • vue3.0的源码全部采用TypeScript重写。(在大型项目中推荐使用类型化的语言)
  • 使用Monorepo管理项目。使用一个项目管理多个包,把独立的功能模块都提取到不同的包中,把不同功能的包放到一个package里面管理,这样每个功能模块划分都很明确,模块之间的依赖关系也很明确,并且每个功能模块都可以单独测试单独发布单独使用
  • packages目录结构
    Vue.js 3.0Vue.js 3.0 和2.0的区别Vue3.0的不同构建版本APIWatch

Composition API(组合API)

他是为了解决在遇到大型项目时,遇到超大组件使用options api不好拆分和重用的问题

  • Options API
    • 包含一个描述组件选项(data,method,props等)的对象
    • Options API开发复杂组件,同一个功能逻辑的代码被拆分到不同选项
  • Composition API
    • Vue.js 3.0新增的一组API
    • 一组基于函数的api
    • 可以更灵活的组织组件的逻辑
使用方式
  • createApp 作用是创建一个vue对象,他可以接收一个选项作为参数
  • setup 是commposition api的入口
  • reactive 把一个对象转换成响应式的对象,并且把该对象嵌套的对象也转换成响应式的对象。把传入的对象包装成了proxy对象,将来访问该对象的时候会调用代理对象中的getter拦截收集依赖,当其中的属性变化之后会调用代理对象中的setter进行拦截触发更新
  • toRefs。可以把响应式对象中的所有属性也转换成响应式的,接收一个响应式变量作为参数。 当我们把代理对象解构的时候,就相当于定义了新的变量来代表代理对象中的属性,而基本类型的赋值就是把值在内存中复制一份,所以这里的新变量就是基本类型的变量和代理对象无关,因此新变量的访问和更新不会触发代理对象的响应式。而toRefs要求传入的参数必须是一个代理对象,然后他内部会创建一个新的对象,然后遍历传入代理对象的所有属性,把所有属性的值都转换成响应式对象,然后再挂载到新创建的对象上,最后返回新创建的对象。他内部会为代理对象的每一个属性创建一个具有value属性的对象,该对象是响应式的,value属性具有getter和setter,这和ref函数类似,getter中返回代理对象中对应属性的值,setter中给代理对象的属性赋值,所以返回的每一个属性都是响应式的,因此可以解构每一个属性,解构的每一个属性都是响应式的,因为他是对象,指向的是同一片内存空间。
  • ref 把基本类型数据转换成响应式的。ref的原理,如果传入的是对象,则内部调用reactive返回一个代理对象,如果传入的是基本类型的值,则内部会创建一个只有value属性的对象,该对象的value属性具有getter和setter。
  • 生命周期函数。
    Vue.js 3.0Vue.js 3.0 和2.0的区别Vue3.0的不同构建版本APIWatch
    setup是在组件初始化之前执行的,是在beforecreate和created之间执行的,所以在beforecreate和created中的代码都可以放在setup函数中,并且不需要在setup中有对应的实现。renderTracked和renderTriggered都是在render函数被重新调用的时候触发的,不同的是renderTracked在首次调用render的时候也会被触发renderTriggered在首次调用的时候不会触发。
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
    </head>
    <body>
      <div id="app">
        x: {{ x }} <br>
        y: {{ y }}
      </div>
      <script type="module">
        import { createApp, reactive, onMounted, onUnmounted, toRefs } from './node_modules/vue/dist/vue.esm-browser.js'
    
        function useMousePosition () {
          // 第一个参数 props
          // 第二个参数 context,attrs、emit、slots
          const position = reactive({
            x: 0,
            y: 0
          })
    
          const update = e => {
            position.x = e.pageX
            position.y = e.pageY
          }
    
          onMounted(() => {
            window.addEventListener('mousemove', update)
          })
    
          onUnmounted(() => {
            window.removeEventListener('mousemove', update)
          })
    
          return toRefs(position)
        }
    
        const app = createApp({
          setup () {
            // const position = useMousePosition()
            const { x, y } = useMousePosition()
            return {
              x,
              y
            }
          }
        })
        console.log(app)
    
        app.mount('#app')
      </script>
    </body>
    </html>
               
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
    </head>
    <body>
      <div id="app">
        <button @click="increase">按钮</button>
        <span>{{ count }}</span>
      </div>
      <script type="module">
        import { createApp, ref } from './node_modules/vue/dist/vue.esm-browser.js'
        
        function useCount () {
          const count = ref(0)
          return {
            count,
            increase: () => {
              count.value++
            }
          }
        }
    
        createApp({
          setup () {
            return {
              ...useCount()
            }        
          }
        }).mount('#app')
      </script>
    </body>
    </html>
               

性能提升

vue3.0中使用proxy代理对象重写了响应式的代码,并且对编译器做了优化,重写了虚拟dom,从而让渲染和update的性能都有了大幅度的提升。另外官方介绍,服务端渲染的性能也提升了2-3倍

响应式系统升级
  • Vue.js 2.x中响应式系统的核心defineProperty。在初始化的时候会编译data中的所有成员,通过defineProperty把对象中的属性转化成getter和setter,如果data中的属性又是对象的话,需要递归处理每一个子对象的属性。注:这些都是在初始化的时候进行的,也就是说如果没有使用这个属性的时候也会把他进行响应式的处理
Proxy
  • Vue.js 3.0中使用Proxy对象重写响应式系统。不需要再初始化的时候遍历所有的属性,如果有多层属性嵌套的话,只有访问某个属性的时候,才会递归处理下一层的属性
    • 可以监听动态新增的属性
    • 可以监听删除的属性
    • 可以监听数组的索引和length属性
    • 可以作为单独的模块使用
    • 注:
      • set 和 deleteProperty 中需要返回布尔类型的值。在严格模式下,如果返回false的话会出现Type Error的异常
      • Proxy和Reflect中使用receiver
        • Proxy中的receiver:他是当前创建的Proxy对象或者继承自当前proxy的子对象
        • Reflect 中的 receiver:如果target对象中设置了getter,getter中的this指向receiver
reactive
  • 接收一个参数,判断这个参数是否是对象
  • 创建拦截器对象handler,设置get/set/deleteProperty
  • 返回Proxy对象

依赖收集

Vue.js 3.0Vue.js 3.0 和2.0的区别Vue3.0的不同构建版本APIWatch
  • targetMap 用来记录目标对象和一个字典(就是中间的map),使用WeakMap弱引用,key就是目标对象(弱引用的对象),因为是弱引用,当目标对象失去引用之后就会销毁,
  • depsMap targetMap中的值就是depsMap。这个字典中的key是目标对象的属性名称,值是一个set集合,set集合中存储的元素不会重复
  • dep depsMap里面存储的值就是effect函数,因为我们可以多次调用effect函数,在effect函数中访问同一个属性,这样该属性就会收集多次依赖,对应多个effect函数
  • 通过这种结构,我们就可以存储目标对象,目标对象的属性以及属性对应的effect函数,一个属性可能对应多个函数,将来触发更新的时候可以来到这个结构中找到对应的effect函数。然后对应执行

reactive vs ref

  • ref可以把基本数据类型数据,转化成响应式对象
  • ref返回的对象,重新赋值成对象也是响应式的
  • reactive返回的对象,重新赋值丢失响应式
  • reactive返回的对象不可以解构
  • 如果是一个对象类型转化响应式用reactive比较方便,如果是基础类型就用ref
编译优化
  • Vue.js 2.x中通过标记静态根节点,优化diff的过程(diff的过程会跳过静态根节点,但静态节点还需要进行diff)
  • Vue.js 3.0中标记和提升所有的静态节点,diff的时候只需要对比动态节点内容
    • 引入Fragments(文档片段)这样模版中就不需要创建一个唯一的根节点(在vscode中需要升级vetur插件)
    • 静态提升
    • Patch flag 通过flag标记动态属性,在diff的过程中就只需要对比动态属性即可
    • 缓存事件处理函数。当绑定的处理函数变化时,就需要更新操作,开启缓存之后,会生成一个新的函数,函数中返回的就是事件处理函数,然后将这个新生成的函数绑定到缓存中,将来事件触发时会从缓存中重新获取新生成的函数,然后运行那个生成的函数,会重新获取新的事件处理函数,从而避免了不必要的更新
    • 按需引用
源码体积的优化
  • Vue.js 3.0中移除了一些不常用的API,如:inline-template,filter等
  • Tree-shaking。Tree-shaking依赖于esm,通过编译阶段的静态分析找到没有引入的模块,在打包的时候直接过滤掉,从而让打包后的体积更小

Vite(构建工具)

  • Vite是一个面向现代浏览器的一个更新,更快的web应用开发工具
  • 它基于ESMAScript标准原生模块系统(Es Modules)实现
  • 官方提供的开发工具,使用vite在开发阶段不需要打包项目可以直接运行,提升了开发的效率
ES Module
  • 现代浏览器都支持ES Module(IE不支持)
  • 支持模块的script默认延迟加载
    • 类似于script标签设置defer
    • 在文档解析完成后,触发DOMContentLoaded事件前执行
Vite 和 Vue-CLI的区别
  • Vite在开发模式下不需要打包可以直接运行(他使用type='module’的方式加载模块),当遇到浏览器不识别的文件时,会先对文件进行编译,然后将编译的结果返回给浏览器
  • Vue-CLI开发模式下必须对象打包才可以运行
  • Vite在生产环境下使用Rollup打包,Rollup基于ES Module的方式打包,不需要使用babel把import转换成require以及一些相应的辅助函数,因此打包的体积会比webpack打包的体积更小
  • Vue-CLI使用webpack打包
Vite特点
  • 快速冷启动
  • 按需编译
  • 模块热更新(模块热更新的性能和模块总数无关,不管有多少模块,都不会影响热更新的性能)
  • 开箱即用
Vite项目依赖
  • vite
  • @vue/compiler-sfc
基础使用
  • vite serve。开启一个用于开发的web服务器,在启动服务器的时候不需要编译所有的代码文件,启动速度快
    Vue.js 3.0Vue.js 3.0 和2.0的区别Vue3.0的不同构建版本APIWatch
    Vue.js 3.0Vue.js 3.0 和2.0的区别Vue3.0的不同构建版本APIWatch

    图1是Vite server。在启动vite server的时候,不需要打包直接开启一个web服务器,当浏览器请求服务器,例如请求一个单文件组件,这时在服务器端编译单文件组件,然后把编译的结果返回给浏览器。注:这里的编译的是在服务器端,模块的处理也是在请求到服务器端处理的。

    图2是vue-cli创建的应用。他在启动开发的web服务器的时候,使用的是vue-cli-service serve,当运行时,它内部首先会利用webpack打包所有的模块,如果模块非常多的话,打包速度会很慢,把打包结果存储到内存中,然后才会开启开发的web服务器,浏览器请求web服务器,把内存中打包的结果直接返回给浏览器。像webpack这样的工具,他的做法是将代码中所有的依赖模块提前编译打包到bundler里,也就是说不管模块是否被执行,是否使用到都要被编译和打包到bundler里,随着项目越来越大,打包的bundler也越来越大,打包的速度也越来越慢。而vite利用现代浏览器原生支持的es module模块化的特性省略了对模块的打包,对需要编译的文件,vite采用即时编译,只有具体请求某个文件的时候才会在服务端编译这个文件,这样可以实现按需编译,速度会更快

  • vite build
    • 内部使用Rollup进行打包,最终还是会把文件都提前编译并打包到一起
    • 对于代码切割的需求。vite内部采用的是原生的动态导入的特性实现的,所以打包结果还是只能支持现代化浏览器,不过动态导入的特性是有响应的polyfill
    • 之前使用Webpack打包的两个原因
      • 会把所有的模块都打包到bundle文件中
      1. 浏览器并不支持模块化
      2. 零散的模块文件会产生大量的http请求

HMR

  • Vite HMR。立即编译当前所修改的文件
  • Webpack HMR。会自动以这个文件为入口重写build一次,所有的涉及到的依赖都会被加载一遍,所以反应速度会慢一点。
Vite创建项目
Vue.js 3.0Vue.js 3.0 和2.0的区别Vue3.0的不同构建版本APIWatch
vue文件的解析

当访问vue文件的时候,vite开启的web服务器会劫持.vue的请求,首先会把.vue文件解析成js文件,并且把响应头中的content-type更改为application/javascript,目的是告诉浏览器现在发送的是一个js脚本

vite的工作原理

使用浏览器支出的es modules的方式加载模块,在开发环境下,他不会打包项目,把所有模块的请求都交给服务器来处理,在服务器去处理浏览器不能识别的模块,如果是单文件组件会调用compiler-sfc编译单文件组件,编译单文件组件并把编译的结果返回给浏览器。

Vite 核心功能

  • 静态web服务器(koa)
  • 编译单文件组件。拦截浏览器不识别的模块,并处理
  • HMR

Vue3.0的不同构建版本

  • vue3中不再构建umd的模块化方式,因为umd的模块化方式会让代码有更多的冗余
  • vue3的构建版本中把cjs,esm和自执行函数的方式分别打包到了不同的文件中,在packages/vue中存放了vue3的所有构建版本
  • Vue.js 3.0Vue.js 3.0 和2.0的区别Vue3.0的不同构建版本APIWatch

API

Computed

计算属性:他帮我们创建一个响应式数据,这个响应式数据依赖于其他响应式数据。他的作用是:简化模版中的代码,可以缓存计算的结果,当数据变化后才会重新计算

Vue.js 3.0Vue.js 3.0 和2.0的区别Vue3.0的不同构建版本APIWatch
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <button @click="push">按钮</button>
    未完成:{{ activeCount }}
  </div>
  <script type="module">
    import { createApp, reactive, computed } from './node_modules/vue/dist/vue.esm-browser.js'
    const data = [
      { text: '看书', completed: false },
      { text: '敲代码', completed: false },
      { text: '约会', completed: true }
    ]

    createApp({
      setup () {
        const todos = reactive(data)

        const activeCount = computed(() => {
          return todos.filter(item => !item.completed).length
        })

        return {
          activeCount,
          push: () => {
            todos.push({
              text: '开会',
              completed: false
            })
          }
        }
      }
    }).mount('#app')
  </script>
</body>
</html>
           

Watch

Vue.js 3.0Vue.js 3.0 和2.0的区别Vue3.0的不同构建版本APIWatch
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <p>
      请问一个 yes/no 的问题:
      <input v-model="question">
    </p>
    <p>{{ answer }}</p>
  </div>

  <script type="module">
    // https://www.yesno.wtf/api
    import { createApp, ref, watch } from './node_modules/vue/dist/vue.esm-browser.js'

    createApp({
      setup () {
        const question = ref('')
        const answer = ref('')

        watch(question, async (newValue, oldValue) => {
          const response = await fetch('https://www.yesno.wtf/api')
          const data = await response.json()
          answer.value = data.answer
        })

        return {
          question,
          answer
        }
      }
    }).mount('#app')
  </script>
</body>
</html>
           

WatchEffect

  • 是watch函数的简化版本,也用来监视数据的变化
  • 接收一个函数作为参数,监听函数内响应式数据的变化
  • 也同样返回一个取消监听的函数
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <button @click="increase">increase</button>
    <button @click="stop">stop</button>
    <br>
    {{ count }}
  </div>

  <script type="module">
    import { createApp, ref, watchEffect } from './node_modules/vue/dist/vue.esm-browser.js'

    createApp({
      setup () {
        const count = ref(0)
        const stop = watchEffect(() => {
          console.log(count.value)
        })

        return {
          count,
          stop,
          increase: () => {
            count.value++
          }
        }
      }
    }).mount('#app')
  </script>
</body>
</html>