MVC
MVC 的思想:一句话描述就是 Controller 负责将 Model 的数据用 View 显示出来,换句话说就是在 Controller 里面把 Model 的数据赋值给 View。
MVVM
MVVM 与 MVC 最大的区别就是:它实现了 View 和 Model 的自动同步,也就是当 Model 的属性改变时,我们不用再自己手动操作 Dom 元素,来改变 View 的显示,而是改变属性后该属性对应 View 层显示会自动改变(对应Vue数据驱动的思想)
注意:Vue 并没有完全遵循 MVVM 的思想 这一点官网自己也有说明
虽然没有完全遵循 MVVM 模型,但是 Vue 的设计也受到了它的启发。因此在文档中经常会使用 vm (ViewModel 的缩写) 这个变量名表示 Vue 实例。
严格的 MVVM 要求 View 不能和 Model 直接通信,而 Vue 提供了$refs 这个属性,让 Model 可以直接操作 View,违反了这一规定,所以说 Vue 没有完全遵循 MVVM。
2.x
生命周期
Vue 实例从创建到销毁的过程,就是生命周期。也就是从开始创建、初始化数据、编译模板、挂载 Dom → 渲染、更新 → 渲染、销毁等一系列过程,称之为 Vue 的生命周期
生命周期 | 3.x | 描述 |
---|---|---|
beforeCreate | setup | 组件实例被创建之初,组件的属性生效之前,如 data 属性 |
created | 组件实例已经完全创建,属性也绑定,但真实 dom 还没有生成,$el 还不可用 | |
beforeMount | onBeforeMount | 在挂载开始之前被调用:相关的 render 函数首次被调用 |
mounted | onMounted | el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子 |
beforeUpdate | onBeforeUpdate | 组件数据更新之前调用,发生在虚拟 DOM 打补丁之前 |
updated | onUpdated | 组件数据更新之后 |
beforeDestory | onBeforeUnmount | 组件销毁前调用 |
destoryed | onUnmounted | 组件销毁后调用 |
activited | keep-alive 专属,组件被激活时调用 | |
deadctivated | keep-alive 专属,组件被销毁时调用 |
组件生命周期调用顺序
- 组件的调用顺序都是先父后子,渲染完成的顺序是先子后父。
- 组件的销毁操作是先父后子,销毁完成的顺序是先子后父。
<div id="app">{{ a }}</div>
<script>
var vm = new Vue({
el: '#app',
data() {
return {
a: 'vuejs',
}
},
beforeCreate() {
console.log('创建前')
console.log(this.a)
console.log(this.$el)
},
created() {
console.log('创建之后')
console.log(this.a)
console.log(this.$el)
},
beforeMount() {
console.log('mount之前')
console.log(this.a)
console.log(this.$el)
},
mounted() {
console.log('mount之后')
console.log(this.a)
console.log(this.$el)
},
beforeUpdate() {
console.log('更新之前')
console.log(this.a)
console.log(this.$el)
},
updated() {
console.log('更新完成')
console.log(this.a)
console.log(this.$el)
},
beforeDestroy() {
console.log('组件销毁之前')
console.log(this.a)
console.log(this.$el)
},
destroyed() {
console.log('组件销毁之后')
console.log(this.a)
console.log(this.$el)
},
})
</script>
生命周期示意图
通信
方法 | 描述 |
---|---|
$parent/$children | 获取父子组件实例 |
props/$emit | 父组件通过 props 的方式向子组件传递数据,而通过$emit 子组件可以向父组件通信 |
eventBus | 通过 EventBus 进行信息的发布与订阅 |
Vuex | 是全局数据管理库,可以通过 vuex 管理全局的数据流 |
$attrs/$listeners | 跨级的组件通信 |
provide/inject | 以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效,这成为了跨组件通信的基础(封装组件库时很常用) |
组件中的data是一个函数?
- 一个组件被复用多次的话,也就会创建多个实例。本质上,这些实例用的都是同一个构造函数。
- 如果
data
是对象的话,对象属于引用类型,会影响到所有的实例。所以为了保证组件不同的实例之间data
不冲突,data
必须是一个函数。
子组件为什么不可以修改父组件传递的Prop?/怎么理解vue的单向数据流?
Vue
提倡单向数据流,即父级props
的更新会流向子组件,但是反过来则不行。- 这是为了防止意外的改变父组件状态,使得应用的数据流变得难以理解。
- 如果破坏了单向数据流,当应用复杂时,
debug
的成本会非常高。
状态 data vs 属性 props
- 状态是组件自身的数据
- 属性是来自父组件的数据
- 状态的改变未必会触发更新
- 属性的改变未必会触发更新
computed vs watch
很多时候页面会出现 watch
的滥用而导致一系列问题的产生,而通常更好的办法是使用 computed
属性,首先需要区别它们有什么区别:
watch
:当监测的属性变化时会自动执行对应的回调函数computed
:计算的属性只有在它的相关依赖发生改变时才会重新求值
computed
监测的是依赖值,依赖值不变的情况下其会直接读取缓存进行复用,变化的情况下才会重新计算;而 watch
监测的是属性值, 只要属性值发生变化,其都会触发执行回调函数来执行一系列操作。
computed
能做的,watch
都能做,反之则不行;能用 computed
的尽量用 computed
functional
<template>
<div>
{{ name }}
<button @click="handleChange">change name</button>
<!-- {{ slotDefault }} -->
<VNodes :vnodes="slotDefault" />
<VNodes :vnodes="slotTitle" />
<VNodes :vnodes="slotScopeItem({ value: 'vue' })" />
</div>
</template>
<script>
export default {
name: 'BigProps',
components: {
VNodes: {
functional: true,
render: (h, ctx) => ctx.props.vnodes,
},
},
props: {
name: String,
onChange: {
type: Function,
default: () => {},
},
slotDefault: Array,
slotTitle: Array,
slotScopeItem: {
type: Function,
default: () => {},
},
},
methods: {
handleChange() {
this.onChange('Hello vue!')
},
},
}
</script>
错误日志收集
可收集报错日志 vuex存放,上报到监控平台
function isPromise(ret) {
return ret && typeof ret.then === 'function' && typeof ret.catch === 'function'
}
const errorHandler = (error, vm, info) => {
console.error('抛出全局异常')
console.error(vm)
console.error(error)
console.error(info)
}
function registerActionHandle(actions) {
Object.keys(actions).forEach((key) => {
let fn = actions[key]
actions[key] = function (...args) {
let ret = fn.apply(this, args)
if (isPromise(ret)) {
return ret.catch(errorHandler)
} else {
// 默认错误处理
return ret
}
}
})
}
const registerVuex = (instance) => {
if (instance.$options.store) {
let actions = instance.$options.store._actions || {}
if (actions) {
let tempActions = {}
Object.keys(actions).forEach((key) => {
tempActions[key] = actions[key][0]
})
registerActionHandle(tempActions)
}
}
}
const registerVue = (instance) => {
if (instance.$options.methods) {
let actions = instance.$options.methods || {}
if (actions) {
registerActionHandle(actions)
}
}
}
let VueError = {
install: (Vue, options) => {
/**
* 全局异常处理
* @param {*} error
* @param {*} vm
*/
console.log('VueErrorInstallSuc')
Vue.config.errorHandler = errorHandler
Vue.mixin({
beforeCreate() {
registerVue(this)
registerVuex(this)
},
})
Vue.prototype.$throw = errorHandler
},
}
export default VueError
// TODO: use
import ErrorPlugin from '@/utils/error'
Vue.use(ErrorPlugin)
scoped css
当 <style>
标签存在 scoped
属性时,它的 CSS 只作用与当前组件中的元素
<style scoped>
.red {
color: red;
}
</style>
实现原理则是通过 POSTCSS 来实现转换
<template>
<div class="red" data-v-0013a924>Hello</div>
</template>
<style>
.red[data-v-0013a924] {
color: red;
}
</style>
深度作用选择器
使用
>>>
操作符可以使 scoped 样式中的一个选择器能够作用得“更深”,例如影响子组件
<style scoped>
.red >>> a {
color: red;
}
</style>
Sass 之类的预处理器无法正确解析 >>>
。这种情况下你可以使用 /deep/
或 ::v-deep
操作符取代
<style lang="scss" scoped>
.red {
color: red;
/deep/ a {
color: blue;
}
::v-deep a {
color: yellow;
}
}
</style>
module css
<template>
<div>
<!-- 模板中通过 $style.xxx 访问 -->
<span :class="$style.red">test</span>
<span :class="{ [$style.red]: isRed }">test</span>
<span :class="[$style.red, $style.bold]">test</span>
</div>
</template>
<script>
export default {
data() {
return {
isRed: true,
}
},
created() {
// js 中访问
console.log(this.$style.red)
},
}
</script>
<style lang="scss" module>
.red {
color: red;
}
.bold {
font-weight: bold;
}
</style>
3.x
代码仓库:https://github.com/WuChenDi/Front-End/blob/master/05-Vue/vite-vue-ts
createApp
import { createApp } from 'vue'
import App from './App'
import router from './router'
import { setupStore } from './store'
import VueHighcharts from './directive/highcharts'
async function bootstrap() {
const app = createApp(App)
app.use(router)
setupStore(app)
app.use(VueHighcharts)
app.mount('#app', true)
}
void bootstrap()
emits 属性
<template>
<div>
<p>{{ text }}</p>
<button v-on:click="$emit('accepted')">OK</button>
</div>
</template>
<script>
export default {
props: ['text'],
emits: ['accepted'],
}
</script>
多事件处理
<!-- 这两个 one() 和 two() 将执行按钮点击事件 -->
<button @click="one($event), two($event)"> Submit </button>
Fragment
之前组建的节点必须有一个根元素,
Vue3
可以有多个根元素,也可以有把文本作为根元素尽管
Fragment
看起来像一个普通的DOM
元素,但它是虚拟的,根本不会在DOM
树中呈现。这样我们可以将组件功能绑定到一个单一的元素中,而不需要创建一个多余的DOM
节点。目前你可以在
Vue2
中使用vue-fragments
库来使用Fragments
,而在Vue3
中,你将会在开箱即用!
<template>
<h1>{{ msg }}</h1>
<ul>
<li v-for="product in products" :key="product.id">{{ product.title }}</li>
</ul>
</template>
render(JSX/TSX)
可以使用空标签替代
import { defineComponent, ref } from 'vue'
import HelloWorldTSX from './components/HelloWorldTSX'
import logo from './assets/logo.png'
export default defineComponent({
name: 'App',
setup() {
const menu = ref([
{ path: '/', name: 'index' },
{ path: '/LifeCycles', name: 'LifeCycles' },
{ path: '/Ref', name: 'Ref' },
{ path: '/RefTemplate', name: 'RefTemplate' },
{ path: '/ToRef', name: 'ToRef' },
{ path: '/ToRefs', name: 'ToRefs' },
{ path: '/watch', name: 'watch' },
{ path: '/watchEffect', name: 'watchEffect' },
{ path: '/chart', name: 'ChartDemo' },
])
return () => (
<>
<img alt='Vue logo' src={logo} />
<HelloWorldTSX msg='Hello Vue 3' onChange={(e) => console.log(e)} />
<ul>
{menu.value.map((i) => (
<li key={i.path}>
<router-link to={i.path}>{i.name}</router-link>
</li>
))}
</ul>
<router-view />
</>
)
},
})
移除 .sync 改为 v-model 参数
<!-- vue 2.x -->
<MyCompontent v-bind:title.sync="title" />
<!-- vue 3.x -->
<MyCompontent v-model:title="title" />
异步组件的引用方式
创建一个只有在需要时才会加载的异步组件
defineAsyncComponent
移除 filter
teleport
React
有个Portals
(https://zh-hans.reactjs.org/docs/portals.html) 按照我的理解,这两个其实是类似的
Vue2
可以通过portal-vue
库来实现(https://github.com/LinusBorg/portal-vue)
Suspense
来自 React 生态的一个
idea
(应该在v16.6.x
就已发布使用),运用到Vue3
中(试验性)Suspense
会暂停你的组件渲染,并重现一个回落组件,直到满足一个条件 个人使用之后证明,Suspense
将只是一个具有插槽的组件
<template>
Home组件
<Suspense>
<template #default>
<HelloWorld msg="Hello Vue 3 + TypeScript + Vite" />
</template>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
Composition API
reactive
ref/toRef/toRefs
为何需要toRef 和 toRefs
初衷:不丢失响应式的情况,吧对象数据 分解/扩散 前提:针对是响应式对象(
reactive
封装的)非普通对象 注意:不创造响应式,而是延续响应式
readonly
computed
watch/watchEffect
- 两者都可监听
data
属性变化 watch
需要明确监听哪个属性watchEffect
会根据其中的属性,自动监听其变化watchEffect
初始化时,一定会执行一次, 主要是为了收集需要监听数据
钩子函数声明周期
编译优化的点(面试常问)
PatchFlag
静态标记- 编译模板时,动态节点做标记
- 标记,分为不同的类型,如
TEXT/CLASS/PROPS
- diff 算法时,可区分静态节点,以及不同类型的动态节点
hoistStatic
静态提升- 将静态节点的定义,提升到父作用域,缓存起来
- 多个相邻的静态节点,会被合并起来
- 典型的拿空间换时间的优化策略
cache Handler
缓存事件SSR
优化- 静态节点直接输出,绕过 vdom
- 动态节点动态渲染(以之前一致)
Tree-shaking
优化
Composition API 与 React Hooks 区别对比(面试常问)
- 前者
**setup**
只会调用一次,而后者函数会被多次调用 - 前者不需要缓存数据(因为
setup
只会调用一次),后者需要手动调用函数进行缓存(useMemo
,useCallback
) - 前者不需要考虑调用顺序,而后者需要保证
hooks
执行的顺序 - 前者 reactive + ref ,后者 useState 更难理解
Vue Router
vue路由hash模式和history模式实现原理分别是什么,他们的区别是什么?
hash
模式:- #后面
hash
值的变化,不会导致浏览器向服务器发出请求,浏览器不发出请求,就不会刷新页面 - 通过监听
**hashchange**
事件可以知道 hash 发生了哪些变化,然后根据hash
变化来实现更新页面部分内容的操作。
- #后面
history
模式:history
模式的实现,主要是HTML5
标准发布的两个 API,**pushState**
和**replaceState**
,这两个 API 可以在改变 url,但是不会发送请求。这样就可以监听 url 变化来实现更新页面部分内容的操作
- 区别
URL
展示上,hash
模式有“#”,history
模式没有- 刷新页面时,
hash
模式可以正常加载到 hash 值对应的页面,而history
没有处理的话,会返回 404,一般需要后端将所有页面都配置重定向到首页路由 - 兼容性,
hash
可以支持低版本浏览器和 IE。
路由懒加载是什么意思?如何实现路由懒加载?
- 路由懒加载的含义:把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件
- 实现:结合 Vue 的异步组件和 Webpack 的代码分割功能
// 1.可以将异步组件定义为返回一个 Promise 的工厂函数(该函数返回的 Promise 应该 resolve 组件本身)
const Foo = () => Promise.resolve({ /* 组件定义对象 */ })
// 2.在 Webpack 中,我们可以使用动态 import语法来定义代码分块点 \(split point\)
import('./Foo.vue') // 返回 Promise
// 结合这两者,这就是如何定义一个能够被 Webpack 自动代码分割的异步组件
const Foo = () => import('./Foo.vue');
const router = new VueRouter({ routes: [ { path: '/foo', component: Foo } ]})
// 使用命名 chunk,和webpack中的魔法注释就可以把某个路由下的所有组件都打包在同个异步块 (chunk) 中
chunkconst Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue')
Vue-router 导航守卫有哪些
- 全局前置/钩子:beforeEach、beforeResolve、afterEach
- 路由独享的守卫:beforeEnter
- 组件内的守卫:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
<div id="app">
<h1>Hello App!</h1>
<p>
<!-- 使用 router-link 组件来导航. -->
<!-- 通过传入 `to` 属性指定链接. -->
<!-- <router-link> 默认会被渲染成一个 `<a>` 标签 -->
<router-link to="/foo">Go to Foo</router-link>
<router-link to="/bar">Go to Bar</router-link>
</p>
<!-- 路由出口 -->
<!-- 路由匹配到的组件将渲染在这里 -->
<router-view></router-view>
</div>
<script>
// 0. 如果使用模块化机制编程,导入Vue和VueRouter,要调用 Vue.use(VueRouter)
// 1. 定义 (路由) 组件。
// 可以从其他文件 import 进来
const Foo = { template: "<div>foo</div>" };
const Bar = { template: "<div>bar</div>" };
// 2. 定义路由
// 每个路由应该映射一个组件。 其中"component" 可以是
// 通过 Vue.extend() 创建的组件构造器,
// 或者,只是一个组件配置对象。
// 我们晚点再讨论嵌套路由。
const routes = [
{ path: "/foo", component: Foo },
{ path: "/bar", component: Bar },
];
// 3. 创建 router 实例,然后传 `routes` 配置
// 你还可以传别的配置参数, 不过先这么简单着吧。
const router = new VueRouter({
routes,
});
// 4. 创建和挂载根实例。
// 记得要通过 router 配置参数注入路由,
// 从而让整个应用都有路由功能
const app = new Vue({ router }).$mount("#app");
</script>
动态路由匹配
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
<div id="app">
<p>
<router-link to="/user/foo">/user/foo</router-link>
<router-link to="/user/bar">/user/bar</router-link>
</p>
<router-view></router-view>
</div>
<script>
const User = {
template: `<div>User {{ $route.params.id }}</div>`,
};
const router = new VueRouter({
routes: [{ path: "/user/:id", component: User }],
});
const app = new Vue({ router }).$mount("#app");
</script>
嵌套路由
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
<div id="app">
<p>
<router-link to="/user/foo">/user/foo</router-link>
<router-link to="/user/foo/profile">/user/foo/profile</router-link>
<router-link to="/user/foo/posts">/user/foo/posts</router-link>
</p>
<router-view></router-view>
</div>
<script>
const User = {
template: `
<div class="user">
<h2>User {{ $route.params.id }}</h2>
<router-view></router-view>
</div>
`,
}
const UserHome = { template: '<div>Home</div>' }
const UserProfile = { template: '<div>Profile</div>' }
const UserPosts = { template: '<div>Posts</div>' }
const router = new VueRouter({
routes: [
{
path: '/user/:id',
component: User,
children: [
// 当 /user/:id 匹配成功,
// UserHome 会被渲染在 User 的 <router-view> 中
{ path: '', component: UserHome },
// 当 /user/:id/profile 匹配成功,
// UserProfile 会被渲染在 User 的 <router-view> 中
{ path: 'profile', component: UserProfile },
// 当 /user/:id/posts 匹配成功,
// UserPosts 会被渲染在 User 的 <router-view> 中
{ path: 'posts', component: UserPosts },
],
},
],
})
const app = new Vue({ router }).$mount('#app')
</script>
Vuex
Vuex 使用单一状态树,用一个对象就包含了全部的应用层级状态。至此它便作为一个“唯一数据源 (SSOT)”而存在。这也意味着,每个应用将仅仅包含一个 store 实例。单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。——Vuex官方文档
- Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
- 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。 | State | this.$store.state.xxx | mapState | 提供一个响应式数据 | 定义了应用状态的数据结构,可以在这里设置默认的初始状态。 | | --- | --- | --- | --- | --- | | Getter | this.$store.getters.xxx | mapGetters | 借助 Vue 的计算属性 computed 来实现缓存 | 允许组件从 Store 中获取数据,mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性。 | | Mutation | this.$store.commit('xxx') | mapMutations | 更改 state 方法 | 是唯一更改 store 中状态的方法,且必须是同步函数。 | | Action | this.$stroe.dispatch('xxx') | mapActions | 触发 mutation 方法 | 用于提交 mutation,而不是直接变更状态,可以包含任意异步操作。 | | Module | | | Vue.set 动态添加 state 到响应式数据中 | 允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中。 |
什么情况下使用 Vuex
不要为了用
vuex
而用vuex
- 如果应用够简单,最好不要使用
Vuex
,一个简单的store
模式即可 - 需要构建一个中大型单页应用时,使用
Vuex
能更好地在组件外部管理状态
Vuex和单纯的全局对象有什么区别?
Vuex
的状态存储是响应式的。当Vue
组件从store
中读取状态的时候,若store
中的状态发生变化,那么相应的组件也会相应地得到高效更新。- 不能直接改变
store
中的状态。改变store
中的状态的唯一途径就是显式地提交(commit) mutation
。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。
为什么 Vuex 的 mutation 中不能做异步操作?
Vuex
中所有的状态更新的唯一途径都是mutation
,异步操作通过Action
来提交mutation
实现,这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。- 每个
mutation
执行完成后都会对应到一个新的状态变更,这样devtools
就可以打个快照存下来,然后就可以实现time-travel
了。如果mutation
支持异步操作,就没有办法知道状态是何时更新的,无法很好的进行状态的追踪,给调试带来困难。
Vuex 中的 action返回值
一个
store.dispatch
在不同模块中可以触发多个action
函数。在这种情况下,只有当所有触发函数完成后,返回的Promise
才会执行。
store.dispatch
可以处理被触发的action
的处理函数返回的Promise
,并且store.dispatch
仍旧返回Promise
Action
通常是异步的,要知道action
什么时候结束或者组合多个action
以处理更加复杂的异步流程,可以通过定义action
时返回一个promise
对象,就可以在派发action
的时候就可以通过处理返回的Promise
处理异步流程
Vuex 日志
import Vue from 'vue'
import Vuex from 'vuex'
import createLogger from 'vuex/dist/logger'
const debug = process.env.NODE_ENV === 'development'
Vue.use(Vuex)
const modulesFiles = require.context('./modules', true, /\.js$/)
const modules = modulesFiles.keys().reduce((modules, modulePath) => {
const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1')
const value = modulesFiles(modulePath)
modules[moduleName] = value.default
return modules
}, {})
export default new Vuex.Store({
state: {},
mutations: {},
actions: {},
modules,
plugins: debug ? [createLogger()] : [],
})
持久化可以使用
vuex-persistedstate
源码学习
Vue响应式数据/双向绑定原理
2.x(Object.defineProperty
)
缺点如下:
- 深度监听需要一次性递归
- 无法监听新增属性/删除属性(
Vue.set
/Vue.delete
) - 无法原生监听数组,需要特殊处理
- 可兼容到
IE9
3.x(Proxy
)
Vue
数据双向绑定主要是指:数据变化更新视图,视图变化更新数据。其中,View
变化更新Data
,可以通过事件监听的方式来实现,所以Vue
数据双向绑定的工作主要是如何根据**Data**
变化更新**View**
。- 默认 Vue 在初始化数据时,会给
data
中的属性使用Object.defineProperty
重新定义所有属性,当页面取到对应属性时。会进行依赖收集(收集当前组件的watcher
) 如果属性发生变化会通知相关依赖进行更新操作。
- 深入理解
- 监听器 Observer:对数据对象进行遍历,包括子属性对象的属性,利用
Object.defineProperty()
对属性都加上setter
和getter
。这样的话,给这个对象的某个值赋值,就会触发setter
,那么就能监听到了数据变化。 - 解析器 Compile:解析 Vue 模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新。
- 订阅者 Watcher:Watcher 订阅者是
Observer
和Compile
之间通信的桥梁 ,主要的任务是订阅Observer
中的属性值变化的消息,当收到属性值变化的消息时,触发解析器Compile
中对应的更新函数。每个组件实例都有相应的watcher
实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter
被调用时,会通知watcher
重新计算,从而致使它关联的组件得以更新——这是一个典型的观察者模式 - 订阅器 Dep:订阅器采用 发布-订阅 设计模式,用来收集订阅者
Watcher
,对监听器Observer
和 订阅者Watcher
进行统一管理。
- 监听器 Observer:对数据对象进行遍历,包括子属性对象的属性,利用
Proxy 替代 Object.defineProperty
Proxy
只会代理对象的第一层,Vue3
是怎样处理这个问题的呢?- 判断当前
Reflect.get
的返回值是否为Object
,如果是则再通过reactive
方法做代理, 这样就实现了深度观测。 - 监测数组的时候可能触发多次
get/set
,那么如何防止触发多次呢?我们可以判断key
是否为当前被代理对象target
自身属性,也可以判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行trigger
。
- 判断当前
- 优势
- 直接监听对象而非属性;
- 直接监听数组的变化
Proxy
有多达 13 种拦截方法,不限于apply
、ownKeys
、deleteProperty
、has
等等是Object.defineProperty
不具备的;Proxy
返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty
只能遍历对象属性直接修改;Proxy
作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利;
{
{
// 2.0(Object.defineProperty)
let definedObj = {}
let age
Object.defineProperty(definedObj, 'age', {
get: function () {
console.log('For age')
return age
},
set: function (newVal) {
console.log('Set the age')
age = newVal
},
})
definedObj.age = 24
console.log(definedObj.age)
}
{
// 3.0(Proxy)
let obj = { a: 1 }
let proxyObj = new Proxy(obj, {
get: function (target, prop) {
return prop in target ? target[prop] : 0
},
set: function (target, prop, value) {
target[prop] = 0530
},
})
console.log(proxyObj.a) // 1
console.log(proxyObj.b) // 0
proxyObj.a = 0353
console.log(proxyObj.a) // 0530
}
}
检测数组
- 使用函数劫持的方式,重写了数组的方法
- Vue 将
data
中的数组,进行了原型链重写。指向了自己定义的数组原型方法,这样当调用数组 api 时,可以通知依赖更新.如果数组中包含着引用类型。会对数组中的引用类型再次进行监控。
var arrayProto = Array.prototype
var arrayMethods = Object.create(arrayProto)
var methodsToPatch = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
// 重写原型方法
methodsToPatch.forEach(function (method) {
// 调用原数组的方法
var original = arrayProto[method]
def(arrayMethods, method, function mutator() {
var args = [],
len = arguments.length
while (len--) args[len] = arguments[len]
var result = original.apply(this, args)
var ob = this.__ob__
var inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) {
ob.observeArray(inserted)
}
// notify change
ob.dep.notify() // 当调用数组方法后,手动通知视图更新
return result
})
})
// 进行深度监听
this.observeArray(value)
Vue异步渲染
因为如果不采用异步更新,那么每次更新数据都会对当前组件进行重新渲染。 所以为了性能考虑,Vue会在本轮数据更新后,再去异步更新视图。
function update() {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
// 当数据发生变化时会将watcher放到一个队列中批量更新
queueWatcher(this)
}
}
export function queueWatcher(watcher) {
// 会对相同的watcher进行过滤
var id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
var i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
if (!config.async) {
flushSchedulerQueue()
return
}
// 调用nextTick方法 批量的进行更新
nextTick(flushSchedulerQueue)
}
}
}
nextTick实现原理
nextTick
方法主要是使用了宏任务和微任务,定义了一个异步方法。多次调用 nextTick
会将方法存入 队列中,通过这个异步方法清空当前队列。 所以这个 nextTick
方法就是异步方法
var timerFunc;
// promise
if (typeof Promise !== "undefined" && isNative(Promise)) {
var p = Promise.resolve();
timerFunc = function () {
p.then(flushCallbacks);
if (isIOS) {
setTimeout(noop);
}
};
isUsingMicroTask = true;
} else if (
!isIE &&
typeof MutationObserver !== "undefined" &&
(isNative(MutationObserver) ||
MutationObserver.toString() === "[object MutationObserverConstructor]")
) {
var counter = 1;
var observer = new MutationObserver(flushCallbacks);
var textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true,
});
timerFunc = function () {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
isUsingMicroTask = true;
} else if (typeof setImmediate !== "undefined" && isNative(setImmediate)) {
timerFunc = function () {
setImmediate(flushCallbacks);
};
} else {
timerFunc = function () {
setTimeout(flushCallbacks, 0);
};
}
// nextTick实现
export function nextTick(cb?: Function, ctx?: Object) {
let _resolve;
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx);
} catch (e) {
handleError(e, ctx, "nextTick");
}
} else if (_resolve) {
_resolve(ctx);
}
});
if (!pending) {
pending = true;
timerFunc();
}
}
Computed特点
默认 computed
也是一个 watcher 是具备缓存的,只要当依赖的属性发生变化时才会更新视图
function initComputed(vm: Component, computed: Object) {
var watchers = (vm._computedWatchers = Object.create(null));
var isSSR = isServerRendering();
for (var key in computed) {
var userDef = computed[key];
var getter = typeof userDef === "function" ? userDef : userDef.get;
if (getter == null) {
warn('Getter is missing for computed property "' + key + '".', vm);
}
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
);
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef);
} else {
if (key in vm.$data) {
warn('The computed property "' + key + '" is already defined in data.', vm);
} else if (vm.$options.props && key in vm.$options.props) {
warn('The computed property "' + key + '" is already defined as a prop.', vm);
}
}
}
}
function createComputedGetter(key) {
return function computedGetter() {
var watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
// 如果依赖的值没发生变化,就不会重新求值
if (watcher.dirty) {
watcher.evaluate();
}
if (Dep.target) {
watcher.depend();
}
return watcher.value;
}
};
}
watch 中 deep:true 实现
当用户指定了 watch 中的deep属性为 true 时,如果当前监控的值是数组类型。会对对象中的每 一项进行求值,此时会将当前 watcher 存入到对应属性的依赖中,这样数组中对象发生变化时也 会通知数据更新。
Vue源码-发现函数
数据类型判断
Object.prototype.toString.call()
返回的数据格式为[object Object]
类型,然后用slice截取第8位到倒一位,得到结果为Object
var _toString = Object.prototype.toString
function toRawType(value) {
return _toString.call(value).slice(8, -1)
}
console.log(toRawType({})) // Object
console.log(toRawType([])) // Array
console.log(toRawType(true)) // Boolean
console.log(toRawType(undefined)) // Undefined
console.log(toRawType(null)) // Null
console.log(toRawType(() => {})) // Function
利用闭包构造map缓存数据
vue中判断我们写的组件名是不是html内置标签的时候,如果用数组类遍历那么将要循环很多次获取结果,如果把数组转为对象,把标签名设置为对象的key,那么不用依次遍历查找,只需要查找一次就能获取结果,提高了查找效率。
function makeMap(str, expectsLowerCase) {
var map = Object.create(null)
var list = str.split(',')
for (var i = 0; i < list.length; i++) {
map[list[i]] = true
}
return expectsLowerCase
? function (val) {
return map[val.toLowerCase()]
}
: function (val) {
return map[val]
}
}
// 利用闭包,每次判断是否是内置标签只需调用isHTMLTag
var isHTMLTag = makeMap('html,body,base,head,link,meta,style,title')
console.log('res', isHTMLTag('body')) // true
二维数组扁平化
vue中_createElement格式化传入的children的时候用到了simpleNormalizeChildren函数,原来是为了拍平数组,使二维数组扁平化,类似lodash中的flatten方法。
// lodash flatten
console.log(_.flatten([1, [2, [3, [4]], 5]])) // [1, 2, [3, [4]], 5]
// vue中
function simpleNormalizeChildren(children) {
for (var i = 0; i < children.length; i++) {
if (Array.isArray(children[i])) {
return Array.prototype.concat.apply([], children)
}
}
return children
}
// es6
function simpleNormalizeChildren(children) {
return [].concat(...children)
}
方法拦截
vue中利用Object.defineProperty收集依赖,从而触发更新视图,但是数组却无法监测到数据的变化,但是为什么数组在使用push pop等方法的时候可以触发页面更新呢,那是因为vue内部拦截了这些方法。
// 重写数组方法,然后再把原型指回原方法
var methodsToPatch = ['push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice']
var arrayMethods = Object.create(Array.prototype)
methodsToPatch.forEach((method) => {
arrayMethods[method] = function () {
// 拦截方法
console.log(`调用的是拦截的 ${method} 方法,进行依赖收集`)
return Array.prototype[method].apply(this, arguments)
}
})
var arr = [1, 2, 3]
arr.__proto__ = arrayMethods
arr.push(4) // 调用的是拦截的 push 方法,进行依赖收集
继承的实现
vue中调用Vue.extend实例化组件,Vue.extend就是VueComponent构造函数,而VueComponent利用Object.create继承Vue,所以在平常开发中Vue 和 Vue.extend区别不是很大 es5原生方法实现继承的,es6中 class类直接用extends继承
// ...
执行一次
闭包
function once(fn) {
var called = false
return function () {
if (!called) {
called = true
fn.apply(this, arguments)
}
}
}
浅拷贝
简单的深拷贝我们可以用 JSON.stringify() 来实现。 vue源码中的looseEqual 浅拷贝思路,先类型判断再递归调用
function isObject(obj) {
return obj !== null && typeof obj === 'object'
}
function looseEqual(a, b) {
if (a === b) {
return true
}
var isObjectA = isObject(a)
var isObjectB = isObject(b)
if (isObjectA && isObjectB) {
try {
var isArrayA = Array.isArray(a)
var isArrayB = Array.isArray(b)
if (isArrayA && isArrayB) {
return (
a.length === b.length &&
a.every(function (e, i) {
return looseEqual(e, b[i])
})
)
} else if (a instanceof Date && b instanceof Date) {
return a.getTime() === b.getTime()
} else if (!isArrayA && !isArrayB) {
var keysA = Object.keys(a)
var keysB = Object.keys(b)
return (
keysA.length === keysB.length &&
keysA.every(function (key) {
return looseEqual(a[key], b[key])
})
)
} else {
return false
}
} catch (e) {
return false
}
} else if (!isObjectA && !isObjectB) {
return String(a) === String(b)
} else {
return false
}
}
Vue的性能优化
编码阶段
SEO优化
打包优化
用户体验
Vue CLI
使用cdn优化打包
vue.config.js
const CompressionWebpackPlugin = require('compression-webpack-plugin')
const productionGzipExtensions = ['js', 'css']
const isProd = process.env.NODE_ENV === 'production'
const assetsCDN = {
// webpack build externals
externals: {
vue: 'Vue',
'vue-router': 'VueRouter',
vuex: 'Vuex',
axios: 'axios',
nprogress: 'NProgress',
clipboard: 'ClipboardJS',
'js-cookie': 'Cookies',
},
css: [
'//cdn.jsdelivr.net/npm/ant-design-vue@1.6.5/dist/antd.css',
'//cdn.jsdelivr.net/npm/nprogress@0.2.0/nprogress.css',
],
js: [
'//cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.min.js',
'//cdn.jsdelivr.net/npm/ant-design-vue@1.6.5/dist/antd.min.js',
'//cdn.jsdelivr.net/npm/vue-router@3.3.4/dist/vue-router.min.js',
'//cdn.jsdelivr.net/npm/vuex@3.5.1/dist/vuex.min.js',
'//cdn.jsdelivr.net/npm/axios@0.20.0/dist/axios.min.js',
'//cdn.jsdelivr.net/npm/nprogress@0.2.0/nprogress.min.js',
'//cdn.jsdelivr.net/npm/clipboard@2.0.6/dist/clipboard.min.js',
'//cdn.jsdelivr.net/npm/js-cookie@2.2.1/src/js.cookie.min.js',
'//cdn.jsdelivr.net/npm/nprogress@0.2.0/nprogress.js',
],
}
module.exports = {
lintOnSave: false, // 是否开启eslint保存检测
productionSourceMap: isProd, // 是否生成sourcemap文件,生成环境不生成以加速生产环境构建
assetsDir: 'static',
publicPath: isProd ? '/dd/' : '/',
outputDir: 'dist',
configureWebpack: (config) => {
// 生产环境下将资源压缩成gzip格式
if (isProd) {
// add `CompressionWebpack` plugin to webpack plugins
config.plugins.push(
new CompressionWebpackPlugin({
algorithm: 'gzip',
test: new RegExp('\\.(' + productionGzipExtensions.join('|') + ')$'),
threshold: 10240,
minRatio: 0.8,
})
)
}
// if prod, add externals
if (isProd) {
config.externals = assetsCDN.externals
// delete console
config.optimization.minimizer[0].options.terserOptions.compress.drop_console = true
// delete console.log
// config.optimization.minimizer[0].options.terserOptions.compress.pure_funcs = ["console.log"];
}
},
chainWebpack: (config) => {
// 生产环境下使用CDN
if (isProd) {
config.plugin('html').tap((args) => {
args[0].cdn = assetsCDN
return args
})
}
},
}
index.html
<!doctype html>
<html lang="en" class="beauty-scroll">
<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" />
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
<title><%= process.env.VUE_APP_NAME %></title>
<!-- require cdn assets css -->
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) {
%>
<link rel="stylesheet" href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" />
<% } %>
</head>
<body class="beauty-scroll">
<noscript>
<strong
>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly
without JavaScript enabled. Please enable it to continue.</strong
>
</noscript>
<div id="app"></div>
<!-- require cdn assets js -->
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) {
%>
<script
type="text/javascript"
src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"
></script>
<% } %>
<!-- built files will be auto injected -->
</body>
</html>
开启 Gzip 压缩
/* vue.config.js */
const isProd = process.env.NODE_ENV === 'production'
module.exports = {
// ...
configureWebpack: (config) => {
if (isProd) {
config.plugins.push(
new CompressionWebpackPlugin({
// 目标文件名称。[path] 被替换为原始文件的路径和 [query] 查询
asset: '[path].gz[query]',
algorithm: 'gzip',
// 处理与此正则相匹配的所有文件
test: new RegExp('\\.(js|css)$'),
// 只处理大于此大小的文件
threshold: 10240,
// 最小压缩比达到 0.8 时才会被压缩
minRatio: 0.8,
})
)
}
},
}
去掉debugger和console
/* vue.config.js */
const isProd = process.env.NODE_ENV === 'production'
module.exports = {
// ...
configureWebpack: (config) => {
if (isProd) {
config.optimization.minimizer[0].options.terserOptions.compress.warnings = false
config.optimization.minimizer[0].options.terserOptions.compress.drop_console = true
config.optimization.minimizer[0].options.terserOptions.compress.drop_debugger = true
config.optimization.minimizer[0].options.terserOptions.compress.pure_funcs = [
'console.log',
]
}
},
}
limit (244 KiB)
/* vue.config.js */
module.exports = {
// ...
configureWebpack: (config) => {
// TODO: Webpack - WARNING in asset size limit: The following asset(s) exceed the recommended size limit (244 KiB)
config.performance = {
// maxEntrypointSize: 1024 * 400,
maxAssetSize: 1024 * 400,
assetFilter: function (assetFilename) {
return assetFilename.endsWith('.js')
},
}
},
}
vue项目构建调整内存大小
参考地址:https://stackoverflow.com/questions/55258355/vue-clis-type-checking-service-ignores-memory-limits
尝试过这种:https://support.snyk.io/hc/en-us/articles/360002046418-JavaScript-heap-out-of-memory,但是没有效果,永远都是 2048MB, 应该程序有覆盖这个值的情况出现
背景
项目过大遇到打包栈异常情况
默认情况
内存为 2048 MB
调整后
设置 12288 MB(可根据机器自行配置)
coding
/* vue.config.js */
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')
module.exports = {
// ...
configureWebpack: (config) => {
const existingForkTsChecker = config.plugins.filter(
(p) => p instanceof ForkTsCheckerWebpackPlugin
)[0]
// remove the existing ForkTsCheckerWebpackPlugin
// so that we can replace it with our modified version
config.plugins = config.plugins.filter(
(p) => !(p instanceof ForkTsCheckerWebpackPlugin)
)
// copy the options from the original ForkTsCheckerWebpackPlugin
// instance and add the memoryLimit property
const forkTsCheckerOptions = existingForkTsChecker.options
forkTsCheckerOptions.memoryLimit = 12288
config.plugins.push(new ForkTsCheckerWebpackPlugin(forkTsCheckerOptions))
},
}
按照模块大小自动分割第三方库
/* vue.config.js */
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')
module.exports = {
// ...
configureWebpack: (config) => {
// 按照模块大小自动分割第三方库
config.optimization.splitChunks = {
maxInitialRequests: Infinity,
minSize: 300 * 1024,
/**
* initial 入口 chunk, 对于异步导入的文件不处理
* async 异步 chunk, 只对异步导入的文件处理
* all 全部 chunk
*/
chunks: 'all',
// 缓存分组
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name(module) {
const packageName = module.context.match(
/[\\/]node_modules[\\/](.*?)([\\/]|$)/
)[1]
return `npm.${packageName.replace('@', '')}`
},
},
},
}
},
}
Vite
ES6 module
为什么快?
开发环境使用 ES6 Module,无需打包,非常快 生产环境使用 rollup,并不会快很多