静态文本
静态文本
{ message }
静态文本
...静态文本
1. 哪些变化
从上图中,我们可以概览Vue3
的新特性,如下:
1.1 速度更快
vue3
相比vue2
Dom
实现undate
性能提高1.3~2倍SSR
速度提高了2~3倍1.2 体积更小
通过webpack
的tree-shaking
功能,可以将无用模块“剪辑”,仅打包需要的
能够tree-shaking
,有两大好处:
vue
实现更多其他的功能,而不必担忧整体体积过大vue
可以开发出更多其他的功能,而不必担忧vue
打包出来的整体体积过多
1.3 更易维护
compositon Api
Options API
一起使用Vue3
模块可以和其他框架搭配使用更好的Typescript支持
VUE3
是基于typescipt
编写的,可以享受到自动的类型定义提示
1.4 编译器重写
1.5 更接近原生
可以自定义渲染 API
1.6 更易使用
响应式 Api
暴露出来
轻松识别组件重新渲染原因
2. Vue3新增特性
Vue 3 中需要关注的一些新功能包括:
framents
Teleport
composition Api
createRenderer
2.1 framents
在 Vue3.x
中,组件现在支持有多个根节点
...
...
复制
2.2 Teleport
Teleport
是一种能够将我们的模板移动到 DOM
中 Vue app
之外的其他位置的技术,就有点像哆啦A梦的“任意门”
在vue2
中,像 modals
,toast
等这样的元素,如果我们嵌套在 Vue
的某个组件内部,那么处理嵌套组件的定位、z-index
和样式就会变得很困难
通过Teleport
,我们可以在组件的逻辑位置写模板代码,然后在 Vue
应用范围之外渲染它
我是一个 Toast 文案
复制
2.3 createRenderer
通过createRenderer
,我们能够构建自定义渲染器,我们能够将 vue
的开发模型扩展到其他平台
我们可以将其生成在canvas
画布上
关于createRenderer
,我们了解下基本使用,就不展开讲述了
import { createRenderer } from '@vue/runtime-core'
const { render, createApp } = createRenderer({
patchProp,
insert,
remove,
createElement,
// ...
})
export { render, createApp }
export * from '@vue/runtime-core'
复制
2.4 composition Api
composition Api,也就是组合式api
,通过这种形式,我们能够更加容易维护我们的代码,将相同功能的变量进行一个集中式的管理
关于compositon api
的使用,这里以下图展开
简单使用:
export default {
setup() {
const count = ref(0)
const double = computed(() => count.value * 2)
function increment() {
count.value++
}
onMounted(() => console.log('component mounted!'))
return {
count,
double,
increment
}
}
}
复制
3. 非兼容变更
3.1 Global API
Vue API
已更改为使用应用程序实例API
已经被重构为可 tree-shakable
3.2 模板指令
v-model
用法已更改
和 非 v-for
节点上key
用法已更改v-if
和 v-for
优先级已更改v-bind="object"
现在排序敏感v-for
中的 ref
不再注册 ref
数组3.3 组件
functional
属性在单文件组件 (SFC)
defineAsyncComponent
方法来创建3.4 渲染函数
API
改变$scopedSlots
property 已删除,所有插槽都通过 $slots
作为函数暴露class
被重命名了:
v-enter
-> v-enter-from
v-leave
-> v-leave-from
watch
选项和实例方法 $watch
不再支持点分隔字符串路径,请改用计算函数作为参数Vue 2.x
中,应用根容器的 outerHTML
将替换为根组件模板 (如果根组件没有模板/渲染选项,则最终编译为模板)。VUE3.x
现在使用应用程序容器的 innerHTML
。3.5 其他小改变
destroyed
生命周期选项被重命名为 unmounted
beforeDestroy
生命周期选项被重命名为 beforeUnmount
[prop default
工厂函数不再有权访问 this
是上下文data
应始终声明为函数mixin
的 data
选项现在可简单地合并attribute
强制策略已更改class
被重命名$watch
不再支持以点分隔的字符串路径。请改用计算属性函数作为参数。
没有特殊指令的标记 (v-if/else-if/else
、v-for
或 v-slot
) 现在被视为普通元素,并将生成原生的
元素,而不是渲染其内部内容。Vue 2.x
中,应用根容器的 outerHTML
将替换为根组件模板 (如果根组件没有模板/渲染选项,则最终编译为模板)。Vue 3.x
现在使用应用容器的 innerHTML
,这意味着容器本身不再被视为模板的一部分。3.6 移除 API
keyCode
支持作为 v-on
的修饰符$on
,$off
和$once
实例方法filter
attribute
$destroy
实例方法。用户不应再手动管理单个Vue
组件的生命周期。不会立即同步执行重新渲染。Vue 实现响应式并不是数据发生变化之后 DOM 立即变化,而是按一定的策略进行 DOM 的更新。Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化, Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。
如果同一个watcher被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环tick中,Vue 刷新队列并执行实际(已去重的)工作。
v-if 是真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 “display” 属性进行切换。
所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。
Vue组件通信的方法如下:
props/$emit+v-on
: 通过props将数据自上而下传递,而通过$emit和v-on来向上传递信息。$attrs/$listeners
: Vue2.4中加入的$attrs/$listeners
可以进行跨级的组件通信还有一些用solt插槽或者ref实例进行通信的,使用场景过于有限就不赘述了。
参考:前端vue面试题详细解答
vuex 是专门为 vue 提供的全局状态管理系统,用于多个组件中数据共享、数据缓存等。(无法持久化、内部核心原理是通过创造一个全局实例 new Vue)
主要包括以下几个模块:
轻量级框架:只关注视图层,是一个构建数据的视图集合,大小只有几十kb;
简单易学:国人开发,中文文档,不存在语言障碍 ,易于理解和学习;
双向数据绑定:保留了angular的特点,在数据操作方面更为简单;
组件化:保留了react的优点,实现了html的封装和重用,在构建单页面应用方面有着独特的优势;
视图,数据,结构分离:使数据的更改更为简单,不需要进行逻辑代码的修改,只需要操作数据就能完成相关操作;
虚拟DOM:dom操作是非常耗费性能的,不再使用原生的dom操作节点,极大解放dom操作,但具体操作的还是dom不过是换了另一种方式;
运行速度更快:相比较与react而言,同样是操作虚拟dom,就性能而言,vue存在很大的优势。
beforeCreate 在实例初始化之后,数据观测(data observer) 和 event/watcher 事件配置之前被调用。在当前阶段 data、methods、computed 以及 watch 上的数据和方法都不能被访问
created 实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算, watch/event 事件回调。这里没有$el,如果非要想与 Dom 进行交互,可以通过 vm.$nextTick 来访问 Dom
beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用。
mounted 在挂载完成后发生,在当前阶段,真实的 Dom 挂载完毕,数据完成双向绑定,可以访问到 Dom 节点
beforeUpdate 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁(patch)之前。可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程
updated 发生在更新完成之后,当前阶段组件 Dom 已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新,该钩子在服务器端渲染期间不被调用。
beforeDestroy 实例销毁之前调用。在这一步,实例仍然完全可用。我们可以在这时进行善后收尾工作,比如清除计时器。
destroyed Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。
activated keep-alive 专属,组件被激活时调用
deactivated keep-alive 专属,组件被销毁时调用
异步请求在哪一步发起?
可以在钩子函数 created、beforeMount、mounted 中进行异步请求,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。
如果异步请求不需要依赖 Dom 推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:
Vue 组件间通信是面试常考的知识点之一,这题有点类似于开放题,你回答出越多方法当然越加分,表明你对 Vue 掌握的越熟练。Vue 组件间通信只要指以下 3 类通信:父子组件通信、隔代组件通信、兄弟组件通信,下面我们分别介绍每种通信方式且会说明此种方法可适用于哪类组件间通信。
复制
(1)props / $emit
适用 父子组件通信
这种方法是 Vue 组件的基础,相信大部分同学耳闻能详,所以此处就不举例展开介绍。
(2)ref
与 $parent / $children
适用 父子组件通信
ref
:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例$parent
/ $children
:访问父 / 子实例(3)EventBus ($emit / $on)
适用于 父子、隔代、兄弟组件通信
这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件。
(4)$attrs
/$listeners
适用于 隔代组件通信
$attrs
:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( class 和 style 除外 )。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 ( class 和 style 除外 ),并且可以通过 v-bind="$attrs"
传入内部组件。通常配合 inheritAttrs 选项一起使用。$listeners
:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners"
传入内部组件(5)provide / inject
适用于 隔代组件通信
祖先组件中通过 provider 来提供变量,然后在子孙组件中通过 inject 来注入变量。 provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。
(6)Vuex 适用于 父子、隔代、兄弟组件通信
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。
(1)param方式
/router/:id
/router/123
1)路由定义
//在APP.vue中
用户
//在index.js
{
path: '/user/:userid',
component: User,
},
复制
2)路由跳转
// 方法1:
按钮
复制
3)参数获取
通过 $route.params.userid
获取传递的值
(2)query方式
/router
,也就是普通配置/route?id=123
1)路由定义
//方式1:直接在router-link 标签上以对象的形式
档案
// 方式2:写成按钮以点击事件形式
profileClick(){
this.$router.push({
path: "/profile",
query: {
name: "kobi",
age: "28",
height: 198
}
});
}
复制
2)跳转方法
// 方法1:
按钮
// 方法2:
this.$router.push({ name: 'users', query:{ uname:james }})
// 方法3:
按钮
// 方法4:
this.$router.push({ path: '/user', query:{ uname:james }})
// 方法5:
this.$router.push('/user?uname=' + jsmes)
复制
3)获取参数
通过$route.query 获取传递的值
复制
从编码风格、性能、安全等方面说几条:
编码风格方面:
HTML
元素冲突jsx
中使用“肉串命名”v-for
时务必加上key
,且不要跟v-if
写在一起性能方面:
SSR
减少首屏加载时间v-once
渲染那些不需要更新的内容shallowRef
或shallowReactive
降低开销安全:
template: + userProvidedString +
v-html
,:url
,:style
等,避免html
、url
、样式等注入思路分析:
首先思考vue
路由要解决的问题:用户点击跳转链接内容切换,页面不刷新。
hash
或者history api
实现url
跳转页面不刷新hashchange
事件或者popstate
事件处理跳转hash
值或者state
值从routes
表中匹配对应component
并渲染回答范例:
一个SPA
应用的路由需要解决的问题是 页面跳转内容改变同时不刷新 ,同时路由还需要以插件形式存在,所以:
createRouter
函数,返回路由器实例,实例内部做几件事hash
或者popstate
事件path
匹配对应路由router
定义成一个Vue
插件,即实现install
方法,内部做两件事router-link
和router-view
,分别实现页面跳转和内容显示$route
和$router
,组件内可以访问当前路由和路由器实例xss
攻击v-html
会替换掉标签内部的子元素let template = require('vue-template-compiler');
let r = template.compile(``)
// with(this){return _c('div',{domProps: {"innerHTML":_s('hello')}})}
console.log(r.render);
// _c 定义在core/instance/render.js
// _s 定义在core/instance/render-helpers/index,js
if (key === 'textContent' || key === 'innerHTML') {
if (vnode.children) vnode.children.length = 0
if (cur === oldProps[key]) continue // #6601 work around Chrome version <= 55 bug where single textNode // replaced by innerHTML/textContent retains its parentNode property
if (elm.childNodes.length === 1) {
elm.removeChild(elm.childNodes[0])
}
}
复制
computed 本质是一个惰性求值的观察者。
computed 内部实现了一个惰性的 watcher,也就是 computed watcher,computed watcher 不会立刻求值,同时持有一个 dep 实例。
其内部通过 this.dirty 属性标记计算属性是否需要重新求值。
当 computed 的依赖状态发生改变时,就会通知这个惰性的 watcher,
computed watcher 通过 this.dep.subs.length 判断有没有订阅者,
有的话,会重新计算,然后对比新旧值,如果变化了,会重新渲染。 (Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化时才会触发渲染 watcher 重新渲染,本质上是一种优化。)
没有的话,仅仅把 this.dirty = true。 (当计算属性依赖于其他数据时,属性并不会立即重新计算,只有之后其他地方需要读取属性的时候,它才会真正计算,即具备 lazy(懒计算)特性。)
异步方法,异步渲染最后一步,与JS事件循环联系紧密。主要使用了宏任务微任务(setTimeout
、promise
那些),定义了一个异步方法,多次调用nextTick
会将方法存入队列,通过异步方法清空当前队列。
这是Vue3
数据响应式中非常重要的两个概念,跟我们写代码关系也很大
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
const obj = reactive({ count: 0 })
obj.count++
复制
ref
接收内部值(inner value
)返回响应式Ref
对象,reactive
返回响应式代理对象ref
通常用于处理单值的响应式,reactive
用于处理对象类型的数据响应式ref
主要解决原始值的响应式问题ref
返回的响应式数据在JS中使用需要加上.value
才能访问其值,在视图中使用会自动脱ref
,不需要.value
;ref
可以接收对象或数组等非原始值,但内部依然是reactive
实现响应式;reactive
内部如果接收Re
f对象会自动脱ref
;使用展开运算符(...
)展开reactive
返回的响应式对象会使其失去响应性,可以结合toRefs()
将值转换为Ref
对象之后再展开。reactive
内部使用Proxy
代理传入对象并拦截该对象各种操作,从而实现响应式。ref
内部封装一个RefImpl
类,并设置get value/set value
,拦截用户对值的访问,从而实现响应式导航守卫
router.beforeEach
全局前置守卫
to: Route
: 即将要进入的目标(路由对象)from: Route
: 当前导航正要离开的路由next: Function
: 一定要调用该方法来 resolve
这个钩子。(一定要用这个函数才能去到下一个路由,如果不用就拦截)next()
: 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。next(false)
:取消进入路由,url地址重置为from路由地址(也就是将要离开的路由地址)// main.js 入口文件
import router from './router'; // 引入路由
router.beforeEach((to, from, next) => {
next();
});
router.beforeResolve((to, from, next) => {
next();
});
router.afterEach((to, from) => {
console.log('afterEach 全局后置钩子');
});
复制
路由独享的守卫 你可以在路由配置上直接定义
beforeEnter
守卫
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
}
}
]
})
复制
组件内的守卫你可以在路由组件内直接定义以下路由导航守卫
const Foo = {
template: `...`,
beforeRouteEnter (to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
},
beforeRouteUpdate (to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave (to, from, next) {
// 导航离开该组件的对应路由时调用,我们用它来禁止用户离开
// 可以访问组件实例 `this`
// 比如还未保存草稿,或者在用户离开前,
将setInterval销毁,防止离开之后,定时器还在调用。
}
}
复制
编码优化 :
v-show
复用DOM
:避免重复创建组件
复制
App
尺寸,访问时才异步加载const router = createRouter({
routes: [
// 借助webpack的import()实现异步组件
{ path: '/foo', component: () => import('./Foo.vue') }
]
})
复制
keep-alive
缓存页面:避免重复创建组件实例,且能保留缓存组件状态
复制
v-once
和v-memo
:不再变化的数据使用v-once
This will never change: {{msg}}
comment
{{msg}}
- {{i}}
复制
按条件跳过更新时使用v-momo
:下面这个列表只会更新选中状态变化项
ID: {{ item.id }} - selected: {{ item.id === selected }}
...more child nodes
复制
复制
Vue
组件销毁时,会自动解绑它的全部指令及事件监听器,但是仅限于组件本身的事件export default {
created() {
this.timer = setInterval(this.refresh, 2000)
},
beforeUnmount() {
clearInterval(this.timer)
}
}
复制
对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载,等到滚动到可视区域后再去加载
![]()
复制
https://tangbc.github.io/vue-virtual-scroll-list(opens new window)
babel-plugin-component
)像element-plus
这样的第三方组件库可以按需引入避免体积太大
import { createApp } from 'vue';
import { Button, Select } from 'element-plus';
const app = createApp()
app.use(Button)
app.use(Select)
复制
如果SPA
应用有首屏渲染慢的问题,可以考虑SSR
以及下面的其他方法
data
中,data
中的数据都会增加getter
和setter
,会收集对应的watcher
v-for
遍历为 item
添加 key
v-for
遍历避免同时使用 v-if
computed
和 watch
的使用用户体验
app-skeleton
骨架屏pwa
serviceworker
SEO优化
prerender-spa-plugin
ssr
打包优化
Webpack
对图片进行压缩cdn
的方式加载第三方模块happypack
splitChunks
抽离公共文件SourceMap
webpack-bundle-analyzer
可视化分析工具基础的 Web 技术的优化
gzip
压缩CDN
的使用Chrome Performance
查找性能瓶颈受现代 JavaScript 的限制 ,Vue 无法检测到对象属性的添加或删除。由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。但是 Vue 提供了 Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value)
来实现为对象添加响应式属性,那框架本身是如何实现的呢?
我们查看对应的 Vue 源码:vue/src/core/instance/index.js
export function set (target: Array | Object, key: any, val: any): any {
// target 为数组
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 修改数组的长度, 避免索引>数组长度导致splcie()执行有误
target.length = Math.max(target.length, key)
// 利用数组的splice变异方法触发响应式
target.splice(key, 1, val)
return val
}
// key 已经存在,直接修改属性值
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
// target 本身就不是响应式数据, 直接赋值
if (!ob) {
target[key] = val
return val
}
// 对属性进行响应式处理
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
复制
我们阅读以上源码可知,vm.$set 的实现原理是:
Vue3.0 性能提升体现在哪些方面
API
,基于Proxy
实现,初始化时间和内存占用均大幅改进;静态标记pachFlag
(diff
算法增加了一个静态标记,只对比有标记的dom
元素)、事件增加缓存
、静态提升
(对不参与更新的元素,会做静态提升,只会被创建一次,之后会在每次渲染时候被不停的复用)等,可以有效跳过大量diff
过程;tree-shaking
,因此整体体积更小,加载更快ssr
渲染以字符串方式渲染一、编译阶段
试想一下,一个组件结构如下图
静态文本
静态文本
{ message }
静态文本
...
静态文本
复制
可以看到,组件内部只有一个动态节点,剩余一堆都是静态节点,所以这里很多 diff
和遍历其实都是不需要的,造成性能浪费
因此,Vue3在编译阶段,做了进一步优化。主要有如下:
diff
算法优化SSR
优化1. diff 算法优化
Vue 2x
中的虚拟 dom
是进行全量的对比。Vue 3x
中新增了静态标记(PatchFlag
):在与上次虚拟结点进行对比的时候,值对比 带有 patch flag
的节点,并且可以通过 flag
的信息得知当前节点要对比的具体内容化Vue2.x的diff算法
vue2.x
的diff
算法叫做全量比较
,顾名思义,就是当数据改变的时候,会从头到尾的进行vDom
对比,即使有些内容是永恒固定不变的
Vue3.0的diff算法
vue3.0
的diff
算法有个叫静态标记(PatchFlag
)的小玩意,啥是静态标记呢?简单点说,就是如果你的内容会变,我会给你一个flag
,下次数据更新的时候我直接来对比你,我就不对比那些没有标记的了
已经标记静态节点的p
标签在diff
过程中则不会比较,把性能进一步提高
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("p", null, "'HelloWorld'"),
_createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
//上面这个1就是静态标记
]))
}
复制
关于静态类型枚举如下
TEXT = 1 // 动态文本节点
CLASS=1<<1,1 // 2//动态class
STYLE=1<<2,// 4 //动态style
PROPS=1<<3,// 8 //动态属性,但不包含类名和样式
FULLPR0PS=1<<4,// 16 //具有动态key属性,当key改变时,需要进行完整的diff比较。
HYDRATE_ EVENTS = 1 << 5,// 32 //带有监听事件的节点
STABLE FRAGMENT = 1 << 6, // 64 //一个不会改变子节点顺序的fragment
KEYED_ FRAGMENT = 1 << 7, // 128 //带有key属性的fragment 或部分子字节有key
UNKEYED FRAGMENT = 1<< 8, // 256 //子节点没有key 的fragment
NEED PATCH = 1 << 9, // 512 //一个节点只会进行非props比较
DYNAMIC_SLOTS = 1 << 10 // 1024 // 动态slot
HOISTED = -1 // 静态节点
// 指示在diff算法中退出优化模式
BALL = -2
复制
2. hoistStatic 静态提升
Vue 2x
: 无论元素是否参与更新,每次都会重新创建。Vue 3x
: 对不参与更新的元素,会做静态提升,只会被创建一次,之后会在每次渲染时候被不停的复用。这样就免去了重复的创建节点,大型应用会受益于这个改动,免去了重复的创建操作,优化了运行时候的内存占用HelloWorld
HelloWorld
{ message }
复制
开启静态提升前
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("p", null, "'HelloWorld'"),
_createVNode("p", null, "'HelloWorld'"),
_createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
]))
}
复制
开启静态提升后编译结果
const _hoisted_1 = /*#__PURE__*/_createVNode("p", null, "'HelloWorld'", -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/_createVNode("p", null, "'HelloWorld'", -1 /* HOISTED */)
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_hoisted_1,
_hoisted_2,
_createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
]))
}
复制
可以看到开启了静态提升后,直接将那两个内容为helloworld
的p
标签声明在外面了,直接就拿来用了。同时 _hoisted_1
和_hoisted_2
被打上了 PatchFlag
,静态标记值为 -1
,特殊标志是负整数表示永远不会用于 Diff
3. cacheHandlers 事件监听缓存
复制
开启事件侦听器缓存之前:
export const render = /*#__PURE__*/_withId(function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("button", { onClick: _ctx.onClick }, "点我", 8 /* PROPS */, ["onClick"])
// PROPS=1<<3,// 8 //动态属性,但不包含类名和样式
]))
})
复制
这里有一个8
,表示着这个节点有了静态标记,有静态标记就会进行diff
算法对比差异,所以会浪费时间
开启事件侦听器缓存之后:
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("button", {
onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.onClick(...args)))
}, "点我")
]))
}
复制
上述发现开启了缓存后,没有了静态标记。也就是说下次diff
算法的时候直接使用
4. SSR优化
当静态内容大到一定量级时候,会用createStaticVNode
方法在客户端去生成一个static node
,这些静态node
,会被直接innerHtml
,就不需要创建对象,然后根据对象渲染
你好
... // 很多个静态属性
{{ message }}
复制
编译后
import { mergeProps as _mergeProps } from "vue"
import { ssrRenderAttrs as _ssrRenderAttrs, ssrInterpolate as _ssrInterpolate } from "@vue/server-renderer"
export function ssrRender(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) {
const _cssVars = { style: { color: _ctx.color }}
_push(`你好...你好${
_ssrInterpolate(_ctx.message)
}`)
}
复制
二、源码体积
相比Vue2
,Vue3
整体体积变小了,除了移出一些不常用的API
,再重要的是Tree shanking
任何一个函数,如ref
、reactive
、computed
等,仅仅在用到的时候才打包,没用到的模块都被摇掉,打包的整体体积变小
import { computed, defineComponent, ref } from 'vue';
export default defineComponent({
setup(props, context) {
const age = ref(18)
let state = reactive({
name: 'test'
})
const readOnlyAge = computed(() => age.value++) // 19
return {
age,
state,
readOnlyAge
}
}
});
复制
三、响应式系统
vue2
中采用 defineProperty
来劫持整个对象,然后进行深度遍历所有属性,给每个属性添加getter
和setter
,实现响应式
vue3
采用proxy
重写了响应式系统,因为proxy
可以对整个对象进行监听,所以不需要深度遍历
- 可以监听动态属性的添加
- 可以监听到数组的索引和数组
length
属性
- 可以监听删除属性
说下你的vue项目的目录结构,如果是大型项目你该怎么划分结构和划分组件呢
一、为什么要划分
使用vue
构建项目,项目结构清晰会提高开发效率,熟悉项目的各种配置同样会让开发效率更高
在划分项目结构的时候,需要遵循一些基本的原则:
- 文件夹和文件夹内部文件的语义一致性
- 单一入口/出口
- 就近原则,紧耦合的文件应该放到一起,且应以相对路径引用
- 公共的文件应该以绝对路径的方式从根目录引用
/src
外的文件不应该被引入
文件夹和文件夹内部文件的语义一致性
我们的目录结构都会有一个文件夹是按照路由模块来划分的,如pages
文件夹,这个文件夹里面应该包含我们项目所有的路由模块,并且仅应该包含路由模块,而不应该有别的其他的非路由模块的文件夹
这样做的好处在于一眼就从 pages
文件夹看出这个项目的路由有哪些
单一入口/出口
举个例子,在pages
文件夹里面存在一个seller
文件夹,这时候seller
文件夹应该作为一个独立的模块由外部引入,并且 seller/index.js
应该作为外部引入 seller 模块的唯一入口
// 错误用法
import sellerReducer from 'src/pages/seller/reducer'
// 正确用法
import { reducer as sellerReducer } from 'src/pages/seller'
复制
这样做的好处在于,无论你的模块文件夹内部有多乱,外部引用的时候,都是从一个入口文件引入,这样就很好的实现了隔离,如果后续有重构需求,你就会发现这种方式的优点
就近原则,紧耦合的文件应该放到一起,且应以相对路径引用
使用相对路径可以保证模块内部的独立性
// 正确用法
import styles from './index.module.scss'
// 错误用法
import styles from 'src/pages/seller/index.module.scss'
复制
举个例子
假设我们现在的 seller 目录是在 src/pages/seller
,如果我们后续发生了路由变更,需要加一个层级,变成 src/pages/user/seller
。
如果我们采用第一种相对路径的方式,那就可以直接将整个文件夹拖过去就好,seller
文件夹内部不需要做任何变更。
但是如果我们采用第二种绝对路径的方式,移动文件夹的同时,还需要对每个 import
的路径做修改
公共的文件应该以绝对路径的方式从根目录引用
公共指的是多个路由模块共用,如一些公共的组件,我们可以放在src/components
下
在使用到的页面中,采用绝对路径的形式引用
// 错误用法
import Input from '../../components/input'
// 正确用法
import Input from 'src/components/input'
复制
同样的,如果我们需要对文件夹结构进行调整。将 /src/components/input
变成 /src/components/new/input
,如果使用绝对路径,只需要全局搜索替换
再加上绝对路径有全局的语义,相对路径有独立模块的语义
src 外的文件不应该被引入
vue-cli
脚手架已经帮我们做了相关的约束了,正常我们的前端项目都会有个src
文件夹,里面放着所有的项目需要的资源,js
,css
, png
, svg
等等。src
外会放一些项目配置,依赖,环境等文件
这样的好处是方便划分项目代码文件和配置文件
二、目录结构
单页面目录结构
project
│ .browserslistrc
│ .env.production
│ .eslintrc.js
│ .gitignore
│ babel.config.js
│ package-lock.json
│ package.json
│ README.md
│ vue.config.js
│ yarn-error.log
│ yarn.lock
│
├─public
│ favicon.ico
│ index.html
│
|-- src
|-- components
|-- input
|-- index.js
|-- index.module.scss
|-- pages
|-- seller
|-- components
|-- input
|-- index.js
|-- index.module.scss
|-- reducer.js
|-- saga.js
|-- index.js
|-- index.module.scss
|-- buyer
|-- index.js
|-- index.js
复制
多页面目录结构
my-vue-test:.
│ .browserslistrc
│ .env.production
│ .eslintrc.js
│ .gitignore
│ babel.config.js
│ package-lock.json
│ package.json
│ README.md
│ vue.config.js
│ yarn-error.log
│ yarn.lock
│
├─public
│ favicon.ico
│ index.html
│
└─src
├─apis //接口文件根据页面或实例模块化
│ index.js
│ login.js
│
├─components //全局公共组件
│ └─header
│ index.less
│ index.vue
│
├─config //配置(环境变量配置不同passid等)
│ env.js
│ index.js
│
├─contant //常量
│ index.js
│
├─images //图片
│ logo.png
│
├─pages //多页面vue项目,不同的实例
│ ├─index //主实例
│ │ │ index.js
│ │ │ index.vue
│ │ │ main.js
│ │ │ router.js
│ │ │ store.js
│ │ │
│ │ ├─components //业务组件
│ │ └─pages //此实例中的各个路由
│ │ ├─amenu
│ │ │ index.vue
│ │ │
│ │ └─bmenu
│ │ index.vue
│ │
│ └─login //另一个实例
│ index.js
│ index.vue
│ main.js
│
├─scripts //包含各种常用配置,工具函数
│ │ map.js
│ │
│ └─utils
│ helper.js
│
├─store //vuex仓库
│ │ index.js
│ │
│ ├─index
│ │ actions.js
│ │ getters.js
│ │ index.js
│ │ mutation-types.js
│ │ mutations.js
│ │ state.js
│ │
│ └─user
│ actions.js
│ getters.js
│ index.js
│ mutation-types.js
│ mutations.js
│ state.js
│
└─styles //样式统一配置
│ components.less
│
├─animation
│ index.less
│ slide.less
│
├─base
│ index.less
│ style.less
│ var.less
│ widget.less
│
└─common
index.less
reset.less
style.less
transition.less
复制