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仍旧返回PromiseAction通常是异步的,要知道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,并不会快很多