之前已经了解过了vue2.0版本,现在我们来了解一下3.0版本.
首先我们来看下变化:
- 源码组织方式的变化.
- Composition API
- 性能提升
- Vite
首先源码的组织方式
- 首先类型约束2.0是flow然后3.0源码采用TypeScript重写
- 然后就是使用Monorepo管理项目结构
3.0
2.0
然后就是Composition API
设计动机:
- 回顾OptionsAPI
- 包含一个描述组件选项(data、methods、props等)的对象
- OptionsAPI开发复杂组件,同一个功能逻辑的代码被拆分到不同选项
随着业务复杂度越来越高,代码量会不断的加大;由于相关业务的代码需要遵循option的配置写到特定的区域,导致后续维护非常的复杂,代码可复用性也不高
然后就是mixins混入,2.x中我们通过mixins来共享一些公共的东西.但是会带来一些问题
- 第一命名冲突的问题
- 第二 共享数据 会有隐式依赖的问题,mixin也可以使用假定在组件中定义的数据属性,如果我们以后想重构一个组件,改变了mixin需要的变量的名称,会发生什么情况呢?我们在看这个组件时,不会发现有什么问题。linter也不会发现它,我们只会在运行时看到错误。
现在想象一个有很多mixin的组件。我们重构本地数据属性时,它会破坏mixin吗?我们得手动搜索才能知道。
这太麻烦了
CompositionAPI
- Vue.js3.0新增的一组API
- 一组基于函数的API
- 可以更灵活的组织组件的逻辑
通过compostion API显然我们可以更加优雅的组织我们的代码,函数。让相关功能的代码更加有序的组织在一起(一个函数里)
我们来看如何解决optionsApi提出的问题
- 同一个功能逻辑我们可以放到同一个函数里,随着业务的复杂我们可以把拆分不同的业务到不同的函数里.
- 最后组装它们到setup函数,这样再不同的组件可以导入这些函数来自由组装使用,来达到代码重用的目的.
- 命名冲突:我们需要显式命名任何状态或从合成函数返回的方法,这样通过我们自己的命名就不会有冲突的这种问题了。
- 隐式依赖:前面看到mixin如何使用在组件上定义的 data 属性,这可能会使代码变得脆弱,并且很难进行推理,compostionAPI可以调用组件中定义的局部变量。不过,不同之处在于,现在必须将此变量显式传递给合成函数,这样一切都是显示的,就不会有之前组件的修改,有的问题到了最后阶段才发现这种情况了.
setup函数
vue3中专门为组件提供的新属性.它为我们使用vue3的Composition API 新特新提供了统一的入口.之前定义的data,methods等都统一放入setup中实现。
setup函数会在beforeCreate之后、created之前 所以它内部的this指向window
接收外界传入的props,接受的组件必须props里需要声明否则拿不到传值.
//home.vue里
< setup : data = "123" ></setup >
//setup.vue里
export default {
setup (props) {
console.log(props.data)
},
props: {
data: Number
}
}
第二个形参context,指的是上下文对象,在这个对象中包含了一些书信,这些属性在2.x中通过this才能访问到,3.0访问方式如下,3.0中无法访问到this
export default {
name: 'Setup',
setup (props, context) {
console.log(context)
},
props: {
data: Number
}
}
//输出结果
{
root: (...) ,
parent: (...) ,
refs: (...) ,
attrs: (...) ,
listeners: (...) ,
isServer: (...) ,
ssrContext: (...) ,
emit: (...) ,
slots: { } ,
}
在新版的生命周期函数,可以按需导入到组件中,且只能在setup()函数中使用.
import { onMounted, onUnmounted } from 'vue';
export default {
setup () {
onMounted(() => {
//
});
onUnmounted(() => {
//
});
}
};
2.x和3.0的setup中生命周期钩子的比对
然后来了解下常用的api:
- reactive:reactive()函数接受一个普通对象,返回一个响应式的数据对象.类似于2.x的组件里的data()函数,想要使用必须先引入reactive,模板中想要使用该数据必须return出去,此函数只能在setup函数里使用(返回一个proxy对象,不能直接解构它,直接解构就失去了响应能力,可以通过toRef转换.).
- ref:ref()函数用来根据给定制创建一个响应式的数据对象,ref()函数调用的返回值是一个对象,这个队形上只包含一个.value的属性.与reactvie类似,对比两者区别 reactive创建一个集合响应式数据,ref创建单一响应式数据.
- toRef:toRef()函数可以将reactive()创建出来的响应式对象,转换为普通的对象,只不过,这个对象上的每一个属性节点,都是ref()类型的响应式数据:最长见的引用场景.(reactive创建之后的数据如果返回使用扩展运算符(...),它创建的所有属性不再是响应式的.这是需要toRefs)
- computed,Watch,watchEffect的作用可以参考官方手册.
也可以参考这几个:
https://vue3js.cn/vue-composi...
https://www.jianshu.com/p/038...
https://juejin.cn/post/689201...
https://www.yuque.com/along-n...
我这里只是列举了几个常用的api,具体的用法可以参考上面几个网址.
这里我们来看如何来简易的实现vue3.0中的响应式
核心方法:
- reactive/ref/toRefs/computed
- effect:watchEffect
- track:收集依赖
- trigger:触发更新
首先来reactive
- 接收一个参数,判断这参数是否是对象
- 创建拦截器对象handler,设置get/set/deleteProperty
- 返回Proxy对象
const isObject = val => val !== null && typeof val === 'object' //判断是否是对象
const convert = target => isObject(target) ? reactive(target) : target //是对象转换成响应式
const hasOwnProperty = Object.prototype.hasOwnProperty
const hasOwn = (target, key) => hasOwnProperty.call(target, key) //检测属性存在
export function reactive (target) { //创建响应式对象
if (!isObject(target)) return target
const handler = { //创建proxy的get,set
get (target, key, receiver) {
// 收集依赖
track(target, key)
const result = Reflect.get(target, key, receiver) //获取值
return convert(result)//查看获取的值是否是对象,是的话转换成转换响应式对象返回(proxy)
//因为正常获取到对象的值,不是代理对象
},
set (target, key, value, receiver) {
const oldValue = Reflect.get(target, key, receiver) //获取老值
let result = true //proxy 对象set必须要返回布尔值标识是否成功
if (oldValue !== value) { //新旧值不一样
result = Reflect.set(target, key, value, receiver) //设置值
// 触发更新
trigger(target, key)
}
return result
},
deleteProperty (target, key) {//删除
const hadKey = hasOwn(target, key) //查看属性是否存在
const result = Reflect.deleteProperty(target, key) //删除,返回布尔值
if (hadKey && result) { //如果存在并且删除成功
// 触发更新
trigger(target, key)
}
return result //和set一样,必须要返回布尔值
}
}
return new Proxy(target, handler) //返回代理对象
}
然后我们来看依赖的收集
收集依赖的思路主要是
targetMap:(WeakMap类型,弱引用方便垃圾回收机制及时回收)
- key是储存目标对象,也就是当前使用的未被转换响应式(porxy)的那个对象
value值是一个map对象
- map的key是目标对象的属性名称,也就是当前访问的属性名
对应的值是一个set对象
- set里储存的是watcher(这里添加的是effect回调)
其实收集依赖的过程就是我们通过储存对象,和当前访问属性,来给他添加或已有的话就是找到它添加(set对象中添加),我们effect传入的函数(或者watcher)。
let activeEffect = null // 用来储存当前的effect的回调函数
export function effect (callback) {
activeEffect = callback // 储存回调
callback() // 执行回调,访问响应式对象属性,去收集依赖,放入对应的targetMap 对应目标对象的map对象,对应属性的set里
activeEffect = null //已经放入对应的set对象里,清空下
}
let targetMap = new WeakMap() //用来储存目标对象的WeakMap
export function track (target, key) { //收集依赖
if (!activeEffect) return //收集过程中如果不是通过effect创建退出
let depsMap = targetMap.get(target) //获取对应目标对象
if (!depsMap) { //不存在
targetMap.set(target, (depsMap = new Map())) //创建key为目标对象,值先为空map并赋值给depMap
}
let dep = depsMap.get(key) //从depmap中取对应访问的属性值
if (!dep) { //如果不存在
depsMap.set(key, (dep = new Set())) //给这个属性创建一个set,用来储存回调effect的
}
dep.add(activeEffect) //给set对象添加回调
}
而修改时的触发更新就是找得到目标对象对应属性里的set里储存的函数,依次执行触发更新.
export function trigger (target, key) {//触发更新
const depsMap = targetMap.get(target) //找到修改的目标对象
if (!depsMap) return //不存在退出
const dep = depsMap.get(key) //找到修改的属性的那个set对象
if (dep) {
dep.forEach(effect => { //依次执行set的存入的回到(effect传入的)
effect()
})
}
}
然后就是ref,根据它的特点,我们来
export function ref (raw) {
// 判断 raw 是否是ref 创建的对象,如果是的话直接返回
if (isObject(raw) && raw.__v_isRef) {
return
}
let value = convert(raw) //看是不是可以转换为响应式对象
const r = {
__v_isRef: true, //ref创建对象的标识
get value () {// 对应value的get函数
track(r, 'value') //收集依赖,目标对象是当前的r,属性是value,值是effect的回调函数
return value //返回初始传入的值
},
set value (newValue) {//修改
if (newValue !== value) { //如果新旧值不一样,证明修改了
raw = newValue //赋值
value = convert(raw) //如果是对象的话,转换为响应式对象
trigger(r, 'value') //触发收集过依赖里的那个set对象中的回调.
}
}
}
return r //返回我们包装过的r
}
然后根据规则是toRefs
export function toRefs (proxy) { //把传入的响应式对象内部的属性,解析成ref类型的对象
const ret = proxy instanceof Array ? new Array(proxy.length) : {}//查看是对象还是数组
for (const key in proxy) { //循环 proxy
ret[key] = toProxyRef(proxy, key) //解析成ref类型的对象
}
return ret //返回这个方便解构
}
function toProxyRef (proxy, key) {
const r = {
__v_isRef: true,
get value () { //对应获取值的get
return proxy[key] //获取value时,获取的是响应式对象里的值,会触发它的get,这样依赖就会触发依赖收集
//通过它包装,解构出来属性也变成了响应式的
},
set value (newValue) { //设置值时
proxy[key] = newValue //同上面一样
}
}
return r //返回r
}
最后是computed计算属性
export function computed (getter) {//计算属性
const result = ref() //创建一个ref类型对象
effect(() => (result.value = getter())) //借助effect 和这个ref对象储存 当前这个 函数到对应set
return result //返回这个引用,之后每次取值都去从value取
}
然后来测试
<script type="module">
import { reactive, effect, computed } from './reactivity/index.js'
const product = reactive({
name: 'iPhone',
price: 5000,
count: 3
})
let total = computed(() => {
return product.price * product.count
})
console.log(total.value)
product.price = 4000
console.log(total.value)
product.count = 1
console.log(total.value)
</script>
<script type="module">
import { reactive, effect, toRefs } from './reactivity/index.js'
function useProduct () {
const product = reactive({
name: 'iPhone',
price: 5000,
count: 3
})
return toRefs(product)
}
const { price, count } = useProduct()
let total = 0
effect(() => {
total = price.value * count.value
})
console.log(total)
price.value = 4000
console.log(total)
count.value = 1
console.log(total)
</script>
发现都是没问题的,到这里我们一个最简单版本就完成了.
和2.x对比来说要简单不少,不需要像2.x一样初始化响应式数据时每个属性节点一个dep对象了,而是在访问 该属性节点时,收集该节点的观察者.
总结下reactive vs ref
- ref可以把基本数据类型数据,转成响应式对象
- ref返回的对象,重新赋值成对象也是响应式的
- reactive返回的对象,重新赋值丢失响应式
- reactive返回的对象不可以解构
性能提升
- 响应式系统升级
- 编译优化
- 源码体积的优化
vue2.x中响应式系统的核心defineProperty
vue3.0中使用Proxy对象重写响应式系统
- 可以监听动态新增的属性
- 可以监听删除的属性
- 可以监听数组的索引和length属性
- proxy拦截操作更多
- 不需要一个一个侵入对象递归劫持属性,而是直接代理对象
- proxy新标准将受到浏览器厂商重点持续的性能优化
编译优化:
2.x中通过标记静态根节点,优化diff的过程
- vue2.0 标记静态根节点和静态节点,diff过程中会跳过静态根节点(因为不会变化),但是还会遍历静态节点
3.0中标记和提升所有的静态根节点,diff的时候只需要对比动态节点内容
- Fragments(升级vetur插件)(模板里面不用创建唯一根节点,可以直接放同级标签和文本内容,虚拟dom会默认生成 “片段”)
- 静态提升(静态节点的提升hoistStatic,开启该选项后提升所有静态节点,静态节点将被提升到 render() 函数外面生成,并被命名为 _hoisted_x 变量.)
- Patchflag( 针对动态节点的_createVNode,Vue 在运行时会生成number(大于 0)值的PatchFlag,用作标记,这个参数主要用于 diff 比较过程。当 DOM 节点有这个标志并且大于 0,就代表要更新,没有就跳过。Vue 在 diff 过程会根据不同的 patchflag 使用不同的 patch 方法 )
- 缓存事件处理函数
export function render(_ctx, _cache) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("span", {
onClick: _cache[1] || (_cache[1] = $event => (_ctx.onClick($event)))
}, _toDisplayString(_ctx.msg), 1 /* TEXT */)
]))
}
类似这样cache[1],会自动生成并缓存一个内联函数,“神奇”的变为一个静态节点。
ps:有点像React
中useCallback
自动化。
参考:
https://www.cnblogs.com/woai3...
https://blog.csdn.net/weixin_...
优化代码打包体积,Vue.js3.0中移除了一些不常用的API,例如:
inline-template、filter等,然后就是Tree-shaking 摇树优化
- 可以将无用模块“剪辑”,仅打包需要的(比如
v-model,<transition>
,用不到就不会打包)。 一个简单“
HelloWorld
”大小仅为:13.5kb- 11.75kb,仅
Composition API
。
- 11.75kb,仅
包含运行时完整功能:22.5kb
- 拥有更多的功能,却比
Vue 2
更迷你。
- 拥有更多的功能,却比
很多时候,我们并不需要 vue
提供的所有功能,在 vue 2
并没有方式排除掉,但是 3.0 都可能做成了按需引入。
最后是Vite
Vite是一个面向现代浏览器的一个更轻、更快的Web应用开发工具
它基于ECMAScript标准原生模块系统(ESModules)实现
依赖:Vite,@vue/compiler-sfc
首先我们来看esm
- 现代浏览器都支持ESModule(IE不支持)
通过下面的方式加载模块
- <script type="module" src="..."></script>
支持模块的script默认延迟加载,module默认延迟加载
- 类似于script标签设置defer
- 在文档解析完成后,触发DOMContentLoaded事件前执行
Vite对比Vue-CLI
Vite在开发模式下不需要打包可以直接运行
- 快速冷启动
- 按需编译
- 模块热更新
Vite在生产环境下使用Rollup打包
- 基于ESModule的方式打包,打出来的包体积更小
- Vue-CLI开发模式下必须对项目打包才可以运行
serve
vue-cli-service serve
hmr热加载
ViteHMR:立即编译当前所修改的文件
WebpackHMR:会自动以这个文件为入口重写build一次,所有的涉及到的依赖也都会被加载一遍
打包or不打包
使用Webpack打包的两个原因:
- 浏览器环境并不支持模块化
- 零散的模块文件会产生大量的HTTP请求
vite创建项目
基于模板创建
最后我们来探寻下它的大概实现原理:
其实主要是我们本地启动一个静态服务器,然后通过拦截请求,拦截不浏览器不认识的文件.vue进行能够识别的js文件返回.
其实主要是了解一下它实现的静态服务器。
首先是核心功能:
- 静态Web服务器
编译单文件组件
- 拦截浏览器不识别的模块,并处理
这里实现的是脚手架形式,也可以直接拿到工程里进行测试
#!/usr/bin/env node
const path = require('path') //node path
const { Readable } = require('stream') //node stream
const Koa = require('koa') //koa启动 静态服务器
const send = require('koa-send')
const compilerSFC = require('@vue/compiler-sfc') //vue中编译器转换vue代码
const app = new Koa()//koa实例
const streamToString = stream => new Promise((resolve, reject) => { //流转换为字符串
const 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) => { //vue文件中加载第三方模块
// ctx.path --> /@modules/vue
if (ctx.path.startsWith('/@modules/')) {
const moduleName = ctx.path.substr(10)
const pkgPath = path.join(process.cwd(), 'node_modules', moduleName, 'package.json')
//从node_modules拼接当前路径获取package.json
const pkg = require(pkgPath)
ctx.path = path.join('/node_modules', moduleName, pkg.module)
//获取模块最终路径
}
await next()
})
// 1. 静态文件服务器
app.use(async (ctx, next) => { //中间件,读取 indenx.html
await send(ctx, ctx.path, { root: process.cwd(), index: 'index.html' })
await next()//下一个
})
// 4. 处理单文件组件
app.use(async (ctx, next) => { //处理.vue,文件
if (ctx.path.endsWith('.vue')) { //路径有vue文件
const contents = await streamToString(ctx.body) //流转字符串
const { descriptor } = compilerSFC.parse(contents) //编译器转换代码
let code
if (!ctx.query.type) { // 我们看原始 vite转换后的vue代码会多一个type
code = descriptor.script.content //取出转换后js代码
// console.log(code)
code = code.replace(/export\s+default\s+/g, 'const __script = ') // 进行修改,修改为vite一样
code += `
import { render as __render } from "${ctx.path}?type=template"
__script.render = __render
export default __script
`
} else if (ctx.query.type === 'template') { //如果是转换后的二次访问
const templateRender = compilerSFC.compileTemplate({ source: descriptor.template.content })
//通过最后compileTemplate解析获取完成的js代码
code = templateRender.code //返回
}
ctx.type = 'application/javascript' //设置返回格式
ctx.body = stringToStream(code) //字符串转换为流
}
await next() //下一个
})
// 2. 修改第三方模块的路径
app.use(async (ctx, next) => {
if (ctx.type === 'application/javascript') { //如果是请求的js
const contents = await streamToString(ctx.body)//流转换
// import vue from 'vue'
// import App from './App.vue'
ctx.body = contents
.replace(/(from\s+['"])(?![\.\/])/g, '$1/@modules/')
.replace(/process\.env\.NODE_ENV/g, '"development"')
//把 from 'vue'转换为 from '/@modules/vue'然后更改依赖图中的环境变量判断
}
})
app.listen(3000)//端口监听
console.log('Server running @ http://localhost:3000')
//脚手架的形式
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。