Vue.js 3.0 和2.0的区别
源码组织方式的变化
- vue3.0的源码全部采用TypeScript重写。(在大型项目中推荐使用类型化的语言)
- 使用Monorepo管理项目。使用一个项目管理多个包,把独立的功能模块都提取到不同的包中,把不同功能的包放到一个package里面管理,这样每个功能模块划分都很明确,模块之间的依赖关系也很明确,并且每个功能模块都可以单独测试单独发布单独使用
- packages目录结构
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。
- 生命周期函数。 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对象
依赖收集
- 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服务器,在启动服务器的时候不需要编译所有的代码文件,启动速度快
图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文件中
- 浏览器并不支持模块化
- 零散的模块文件会产生大量的http请求
HMR
- Vite HMR。立即编译当前所修改的文件
- Webpack HMR。会自动以这个文件为入口重写build一次,所有的涉及到的依赖都会被加载一遍,所以反应速度会慢一点。
Vite创建项目
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的所有构建版本
API
Computed
计算属性:他帮我们创建一个响应式数据,这个响应式数据依赖于其他响应式数据。他的作用是:简化模版中的代码,可以缓存计算的结果,当数据变化后才会重新计算
<!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
<!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>