本模块主要介绍Vue3.0的最新特性,并介绍了Composition API相关的方法,分析了Vue3.0中响应式的原理,最后介绍了Vite的实现原理。
Vue3.0的变化
源码和组织方式的变化
Vue3.0使用TypeScript重写了源码,并且使用Monorepo的包管理方式来管理源码,使得每个模块都能单独引入。
Composition API
Vue3.0提供了Composition API,来解决业务功能分散,以及逻辑重用等问题。在Vue2.x中,组件的功能实现一般被分布到不同的对象中,data、computed、methods。而Vue3.0中通过使用Composition API将功能封装在一个函数中,函数返回一个包含一些属性和方法的对象,提供给组件来实现功能。
性能提升
Vue3.0的性能提升主要体现在两个方面:响应式系统的升级和打包编译的优化
- 响应式系统的升级
Vue3.0使用Proxy代理对象重写了响应式系统,Proxy本身性能就比defineProperty好,并且还可以监听动态新增的属性,以及数组的索引和长度。Vue2.x在初始化时就会对data中的数据进行递归处理,将其转化成响应式的数据,即使其中有些数据并没有使用到。而Vue3.0中只有访问到该属性才会对下一级属性进行处理。 - 打包编译的优化
Vue3.0对编译过程进行了优化,对静态节点进行标记,diff对比新旧节点差异时将跳过静态节点的对比,只比较动态节点,新增了Fragment片段,可以允许组件的根节点存在多个同级节点,避免添加额外的节点包裹。静态提升将静态节点的创建提取到render函数外,只有初始化的时候会被创建一次,避免render执行时重复创建静态节点。缓存事件处理函数,避免了不必要的更新。
Vue3.0还移除了一些不常用的API,如inline-template、filter等来减小打包体积。使用Tree-shaking过滤无效代码。模块按需引入,只引入需要使用的模块。
Vite的发布
Vue3.0发布时同时发布Vite,Vite是一款基于ESM的打包工具,具有快速冷启动、按需编译、模块热更新速度快等特点。在开发模式下不需要打包文件,极大的增加了页面打开的速度。
Composition API的使用
-
createApp():接受一个选项对象,并返回一个Vue实例。选项对象中的data必须是一个函数。
-
setup(props, context):compositionAPI的入口,在props被解析完毕,组件实例创建之前执行,即beforeCreate和created之间执行。由于Vue实例还未创建,所以没有this对象。props是外部传入的参数,是一个响应式的对象,不能被解构。context是运行上下文对象,包括attrs、emit、slots等属性。setup返回一个对象,可以在组件内部使用。类似于data中的数据。
setup中使用生命周期钩子函数,只需要引入on+生命周期函数名即可使用<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="app"> x: {{ position.x }} y: {{ position.y }} </div> <script type="module"> import { createApp, reactive, onMounted, onUnmounted } from './node_modules/vue/dist/vue.esm-browser.js'; const useMousePosition = () => { 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.addEventListener('mousemove', update) }) return position } const app = createApp({ setup(props, context) { // props:接收外部传入数据,是个响应式的对象,不能被解构 // context: attrs、emit、slots const position = useMousePosition() return { position } }, mounted() { this.position.x = 100 } }); app.mount('#app'); </script> </body> </html>
-
reactive():将对象设置成响应式对象。不使用observe是为了防止和rsts库函数重名
const position = reactive({ x: 0, y: 0 })
-
toRefs(proxyObj):proxyObj必须是代理对象,将对象的属性转换成响应式的ref对象。原理就是为对象的每一个属性创建一个具有value属性的对象,value具有setter和getter。普遍用于解构响应式对象的处理。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="app"> x: {{ x }} y: {{ y }} </div> <script type="module"> import { createApp, reactive, toRefs, onMounted, onUnmounted } from './node_modules/vue/dist/vue.esm-browser.js'; const useMousePosition = () => { 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.addEventListener('mousemove', update) }) return toRefs(position) } const app = createApp({ setup(props, context) { // props:接收外部传入数据,是个响应式的对象,不能被解构 // context: attrs、emit、slots const { x, y } = useMousePosition() return { x, y } } }); app.mount('#app'); </script> </body> </html>
-
ref(val):将数据转化为响应式的ref对象,对象具有一个value属性。如果在差值表达式中使用该变量,则可以省略.value
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="app"> <span>{{ count }}</span> <button @click="increase">增加</button> </div> <script type="module"> import { createApp, ref } from './node_modules/vue/dist/vue.esm-browser.js'; const useCount = () => { const count = ref(0) return { count, increase() { count.value++ } } } createApp({ setup() { return { ...useCount() } } }).mount('#app'); </script> </body> </html>
-
computed():计算属性,缓存计算结果。
-
接收一个getter函数,当函数的依赖项发生改变时,会重新调用该函数。返回一个不变的响应式ref对象。
<script type="module"> import { createApp, reactive, computed } from './node_modules/vue/dist/vue.esm-browser.js'; const todoList = () => { const todos = reactive([ { text: '吃饭', complete: true}, { text: '睡觉', complete: false}, { text: '打豆豆', complete: false}, ]); const unCompletes = computed(() => { return todos.filter(item => !item.complete).length }) return { unCompletes, add: () => { todos.push({text: '找老婆', complete: false}) } } } createApp({ setup() { return { ...todoList() } } }).mount('#app'); </script>
-
接收一个具有set和get函数的对象来创建可写的ref对象。
-
-
Watch(data, cb(newVal, oldVal), options): 监听数据变化,并执行cb。options为选项对象,包含deep和immediate。返回一个取消监听的函数。
import { createApp, ref, watch } from './node_modules/vue/dist/vue.esm-browser.js'; createApp({ setup() { const question = ref('') const answer = ref('') const img = ref('') let timer = 0 watch(question, (newVal, oldVal) => { if (timer) clearTimeout(timer) timer = setTimeout(async () => { const resopne = await fetch('https://www.yesno.wtf/api') const data = await resopne.json() answer.value = data.answer img.value = data.image }, 500) }) return { question, answer, img } } }).mount('#app');
-
watchEffect: 和watch类似,但只接收一个函数,函数会立即执行一次,当函数中依赖的变量发生变化时,会再次执行该函数,返回一个取消watch的函数。
Vue3.0响应式原理
Vue3.0响应式系统的特点
- 使用Proxy对象实现属性监听,初始化时不用遍历data属性进行响应式处理
- 对于多层属性嵌套,只在访问过程中处理下一级属性
- 默认监听动态属性的添加
- 默认监听属性的删除
- 默认监听数组索引和length属性
- 可以作为单独的模块使用
Proxy的两个需要注意的问题
-
set和deleteProperty方法在严格模式下必须返回boolean类型的值,否则将提示以下错误。esm默认情况下会开启严格模式。
const obj = { a: '12', b: 34 } const p = new Proxy(obj, { get(target, key, receiver) { return Reflect.get(target, key, receiver) }, set(target, key, receiver) { return Reflect.set(target, key, receiver) }, deleteProperty(target, key) { return Reflect.deleteProperty(target, key) } })
-
Reflect.get()方法如果没有传入receiver,原生对象obj的get方法中this将指向原生对象obj
const obj = { get foo() { console.log('obj get'); return this.bar } } const p = new Proxy(obj, { get(target, key) { console.log('proxy get'); if (key === 'bar') { return 'proxy-bar' } return Reflect.get(target, key) } })
如果传入了receiver对象,则原生对象obj的get方法中this将指向Proxy代理对象。const obj = { get foo() { console.log('obj get'); return this.bar } } const p = new Proxy(obj, { get(target, key, receiver) { console.log('proxy get'); if (key === 'bar') { return 'proxy-bar' } return Reflect.get(target, key, receiver) } })
Vue3.x响应式系统的核心方法实现
-
reactive:接受一个普通对象,并返回一个Proxy对象。
const isObj = val => val !== null && typeof val === 'object' const convert = target => isObj(target) ? reactive(target) : target // 将对象转化成响应式的对象 export function reactive(obj) { // 判断是否是对象,如果不是对象,直接返回 if (!isObj(obj)) return obj const handler = { get(target, key, receiver) { // 收集依赖 track(target, key) const result = Reflect.get(target, key, receiver) // 如果result是一个对象,需要再次调用reactive将对象转化为响应式的对象 return convert(result) }, set(target, key, value, receiver) { const oldVal = Reflect.get(target, key, receiver) let result = true if (oldVal !== value) { result = Reflect.set(target, key, value, receiver) // 触发更新 trigger(target, key) } return result }, deleteProperty(target, key) { const hasKey = hasOwn(traget, key) // 判断对象中是否存在该属性 const result = Reflect.deleteProperty(target, key) // 是否删除成功 if (hasKey && result) { // 触发更新 trigger(target, key) } return result } } return new Proxy(obj, handler) }
-
effect:和watchEffect类似,首先会执行传入的回调函数,然后当函数中的依赖项发生改变时,会再次执行回调函数
let activeEffect = null // 注册依赖事件 export function effect(callback) { activeEffect = callback callback() activeEffect = null }
-
track:收集依赖,即记录effect中传入的回调函数
let targetMap = new WeakMap() // 弱引用Map // 收集依赖 WeakMap(target: Map(key: Set(activeEffect))) function track(target, key) { if (!activeEffect) return // 主要是收集activeEffect // 首先找对象对应的Map存不存在 let depsMap = targetMap.get(target) if (!depsMap) { // 不存在向targetMap中设置一个target,并为其创建一个Map targetMap.set(target, (depsMap = new Map())) } let dep = depsMap.get(key) // 获取对象中key对应的activeEffect if (!dep) { depsMap.set(key, (dep = new Set())) } dep.add(activeEffect) // 添加对应的依赖 }
-
trigger:触发更新,执行effect中传入的回调函数
// 触发更新, 依次找到key对应的activeEffect function trigger(target, key) { const depsMap = targetMap.get(target) if (!depsMap) return const dep = depsMap.get(key) if (dep) { dep.forEach(effect => { effect() }) } }
-
ref:将数据转化为包含value属性的响应式对象,属性访问时触发依赖收集
export function ref(raw) { // 判断是否是ref对象,如果是直接返回 if (isObj(raw) && row.__v_isRef) return // 如果传入的参数是一个对象,则将对象转化成响应式的对象 let value = convert(raw) const r = { __v_isRef: true, get value() { // 收集依赖 track(r, 'value') return value }, set value(newVal) { if (newVal === value) return value = convert(newVal) trigger(r, 'value') } } return r }
-
toRefs:将proxy对象的属性都转化成响应式的ref对象
export function toRefs(proxy) { const ret = proxy instanceof Array ? new Array(proxy.length) : {} for (const key in proxy) { ret[key] = toProxyRef(proxy, key) } return ret } function toProxyRef(proxy, key) { return { __v_isRef: true, get value() { // proxy是响应式的数据,访问proxy[key]时会自动收集依赖 return proxy[key] }, set value(newVal) { proxy[key] = newVal } } }
-
computed:接受一个getter函数,返回一个响应式的ref对象,getter中的依赖项发生改变时,会重新调用getter函数,并将结果赋值给ref的value属性。
export function computed(getter) { const r = ref() // 执行callback时,访问r.value时触发了依赖收集 effect(() => r.value = getter()) return r }
Vite实现原理
vite介绍
Vite是一个面向现代浏览器的更轻、更快的Web应用开发工具。基于ESM实现。Vite的出现是为了解决Webpack冷启动时间过长,HMR反应速度慢的问题。
Vite只依赖Vite命令行工具Vite和用于单文件组件编译@vue/compiler-sfc。
Vite默认支持TypeScript,css预编译 less/sass/stylus/postcss(需要单独安装插件来完成编译),jsx以及Web Assembly。
Vite命令
-
Vite serve:开启一个web server服务,启动时不会编译所有的文件,当http请求文件时,会在服务端编译文件,然后将编译结果返回。按需编译。
webpack中会编译所有的文件,所以慢
Vite HMR:立即编译当前所修改的文件
webpack HMR:会自动以修改文件为入口,重新build一次,所有涉及的依赖都会被加载一遍 -
Vite build:使用Rollup打包。代码切割采用Dynamic Import(动态导入)特性实现,结果只支持现代浏览器。但有相应的polyfill支持。
Vite的原理实现
- Vite会基于KOA实现一个web服务器
- 接收到请求时,会根据请求读取对应的静态文件
- 将静态文件中的第三方模块的请求路径进行特殊处理
- 根据处理后的第三方模块的请求路径加载第三方模块
- 对.vue、.css以及文件进行编译处理,将编译后的结果返回给客户端
#!/usr/bin/env node
const { Readable } = require('stream')
const path = require('path')
const Koa = require('koa')
const send = require('koa-send')
const compilerSFC = require('@vue/compiler-sfc')
const app = new Koa()
// 流数据转化成字符串
const streamToString = stream => new Promise((resolve, reject) => {
const chunks = []
// 接收到chunk分片时,将数据片段暂存到chunks数组中
stream.on('data', chunk => chunks.push(chunk))
// 数据接收完毕之后,将流数据转化成字符串
stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')))
// 发生错误时,将错误对象传递下去
stream.on('error', reject)
})
// 将字符串转化成流
const stringToStream = text => {
const stream = new Readable()
stream.push(text)
stream.push(null) // 标志流结束
return stream
}
// 3. 加载第三方模块
app.use(async (ctx, next) => {
// 是否是加载第三方模块
if (ctx.path.startsWith('/@modules/')) {
//截取模块名
const moduleName = ctx.path.substr(10)
// 拼接package.json的路径
const pkgPath = path.resolve(process.cwd(), 'node_modules', moduleName, 'package.json')
// 读取package.json配置
const pkg = require(pkgPath)
// 将第三方模块的加载位置修改为第三方模块中package.json的module属性
ctx.path = path.join('/node_modules', moduleName, pkg.module)
}
await next()
})
// 1. 静态文件服务器
app.use(async (ctx, next) => {
await send(ctx, ctx.path, { root: process.cwd(), index: 'index.html' })
await next()
})
// 4.编译单文件组件
app.use(async (ctx, next) => {
if (ctx.path.endsWith('.vue')) { // 判断vue文件是否是.vue结尾
const contents = await streamToString(ctx.body) // 将流转化成字符串
const { descriptor } = compilerSFC.parse(contents) // 返回descriptor和error对象
let code
if(!ctx.query.type) { // 是否带有type参数
code = descriptor.script.content // 获取编译后的js源码
// console.log(code);
// 修改源码内容
code = code.replace(/export\s+default\s+/g, 'const __script = ')
code += `
import { render as _sfc_render } from "${ctx.path}?type=template"
__script.render = _sfc_render
export default __script
`
} else if (ctx.query.type === 'template') {
const templateRender = compilerSFC.compileTemplate({
id: '#app',
source: descriptor.template.content
}) // 将模板转化成render函数
code = templateRender.code
}
// 修改文件类型为js文件
ctx.type = 'application/javascript'
ctx.body = stringToStream(code) // 将字符串转化成流,交给下一个中间件处理
}
await next()
})
// 2.修改第三方模块的路径
app.use(async (ctx, next) => {
// 针对js文件进行处理
if (ctx.type === 'application/javascript') {
const contents = await streamToString(ctx.body)
// 使用正则替换 from 后面的第三方地址
// import Vue from 'vue'
ctx.body = contents
.replace(/(from\s+['"])(?![\.\/])/g, '$1/@modules/')
.replace(/process\.env\.NODE_ENV/g, '"development"')
}
})
app.listen(3000)
console.log('Server running http://localhost:3000');