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 专属,组件被销毁时调用 |
组件生命周期调用顺序
- 组件的调用顺序都是先父后子,渲染完成的顺序是先子后父。
- 组件的销毁操作是先父后子,销毁完成的顺序是先子后父。
```html
{{ a }}
<a name="93644237"></a>
### 生命周期示意图
<br />![Vue-生命周期.png](https://cdn.nlark.com/yuque/0/2020/png/1544252/1594043201114-26c7bdce-ccc7-46ed-b6e1-d800c8e1cb06.png#crop=0&crop=0&crop=1&crop=1&height=760&id=NIrmX&margin=%5Bobject%20Object%5D&name=Vue-%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F.png&originHeight=3039&originWidth=1200&originalType=binary&ratio=1&rotation=0&showTitle=false&size=77677&status=done&style=none&title=&width=300)
<a name="170e705c"></a>
### 通信
| 方法 | 描述 |
| --- | --- |
| $parent/$children | 获取父子组件实例 |
| props/$emit | 父组件通过 props 的方式向子组件传递数据,而通过$emit 子组件可以向父组件通信 |
| eventBus | 通过 EventBus 进行信息的发布与订阅 |
| Vuex | 是全局数据管理库,可以通过 vuex 管理全局的数据流 |
| $attrs/$listeners | 跨级的组件通信 |
| provide/inject | 以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效,这成为了跨组件通信的基础(封装组件库时很常用) |
<a name="ILJEd"></a>
### 组件中的data是一个函数?
- 一个组件被复用多次的话,也就会创建多个实例。本质上,这些实例用的都是同一个构造函数。
- 如果 `data` 是对象的话,对象属于引用类型,会影响到所有的实例。所以为了保证组件不同的实例之间 `data` 不冲突,`data` 必须是一个函数。
<a name="e79J4"></a>
### 子组件为什么不可以修改父组件传递的Prop?/怎么理解vue的单向数据流?
- `Vue` 提倡单向数据流,即父级 `props` 的更新会流向子组件,但是反过来则不行。
- 这是为了防止意外的改变父组件状态,使得应用的数据流变得难以理解。
- 如果破坏了单向数据流,当应用复杂时,`debug` 的成本会非常高。
<a name="VjmUP"></a>
### 状态 data vs 属性 props
- 状态是组件自身的数据
- 属性是来自父组件的数据
- 状态的改变未必会触发更新
- 属性的改变未必会触发更新
<a name="8fWtG"></a>
### computed vs watch
很多时候页面会出现 `watch` 的滥用而导致一系列问题的产生,而通常更好的办法是使用 `computed` 属性,首先需要区别它们有什么区别:
- `watch`:当监测的属性变化时会自动执行对应的回调函数
- `computed`:计算的属性只有在它的相关依赖发生改变时才会重新求值
![](https://cdn.nlark.com/yuque/0/2021/jpeg/1544252/1624948824636-08d7be3c-9c76-43f2-80db-c020acb3955f.jpeg)<br />`computed` 监测的是依赖值,依赖值不变的情况下其会直接读取缓存进行复用,变化的情况下才会重新计算;而 `watch` 监测的是属性值, 只要属性值发生变化,其都会触发执行回调函数来执行一系列操作。
`computed` 能做的,`watch` 都能做,反之则不行;能用 `computed` 的尽量用 `computed`
<a name="s707x"></a>
### functional
```html
<template>
<div>
{{ name }}
<br />
<button @click="handleChange">change name</button>
<br />
<!-- {{ slotDefault }} -->
<VNodes :vnodes="slotDefault" />
<br />
<VNodes :vnodes="slotTitle" />
<br />
<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" />
异步组件的引用方式
创建一个只有在需要时才会加载的异步组件
移除 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
- 动态节点动态渲染(以之前一致)
-
Composition API 与 React Hooks 区别对比(面试常问)
前者
**setup**
只会调用一次,而后者函数会被多次调用- 前者不需要缓存数据(因为
setup
只会调用一次),后者需要手动调用函数进行缓存(useMemo
,useCallback
) - 前者不需要考虑调用顺序,而后者需要保证
hooks
执行的顺序 前者 reactive + ref ,后者 useState 更难理解
Vue Router
vue路由hash模式和history模式实现原理分别是什么,他们的区别是什么?
hash
模式:history
模式:history
模式的实现,主要是HTML5
标准发布的两个 API,**pushState**
和**replaceState**
,这两个 API 可以在改变 url,但是不会发送请求。这样就可以监听 url 变化来实现更新页面部分内容的操作
区别
路由懒加载的含义:把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件
- 实现:结合 Vue 的异步组件和 Webpack 的代码分割功能 ```javascript // 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’)
<a name="LERQo"></a>
### Vue-router 导航守卫有哪些
- 全局前置/钩子:beforeEach、beforeResolve、afterEach
- 路由独享的守卫:beforeEnter
- 组件内的守卫:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave
```javascript
<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 日志
```javascript 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()] : [] });
<a name="oMZHQ"></a>
### 持久化可以使用
> vuex-persistedstate
<a name="yOwx0"></a>
## 源码学习
<a name="hyzjk"></a>
### Vue响应式数据/双向绑定原理
> **2.x(**`**Object.defineProperty**`**),**缺点如下:
> - 深度监听需要一次性递归
> - 无法监听新增属性/删除属性(`Vue.set` / `Vue.delete`)
> - 无法原生监听数组,需要特殊处理
> - 可兼容到 `IE9`
>
**3.x(**`**Proxy**`**)**
- `Vue` 数据双向绑定主要是指:**数据变化更新视图,视图变化更新数据**。其中,`View` 变化更新 `Data`,可以通过事件监听的方式来实现,所以 `Vue` 数据双向绑定的工作主要是如何**根据 **`**Data**`** 变化更新**`**View**`。
- 默认 Vue 在初始化数据时,会给 `data` 中的属性使用 `Object.defineProperty` 重新定义所有属性,当页面取到对应属性时。会进行依赖收集(收集当前组件的 `watcher` ) 如果属性发生变化会通知相关依赖进行更新操作。
- ![640.webp](https://cdn.nlark.com/yuque/0/2020/webp/1544252/1604715906583-bf59d19b-3e81-4a71-9686-40e8a2a716f8.webp#crop=0&crop=0&crop=1&crop=1&height=400&id=AzJ9A&margin=%5Bobject%20Object%5D&name=640.webp&originHeight=400&originWidth=640&originalType=binary&ratio=1&rotation=0&showTitle=false&size=8924&status=done&style=none&title=&width=640)
- 深入理解
- **监听器 Observer**:对数据对象进行遍历,包括子属性对象的属性,利用 `Object.defineProperty()` 对属性都加上 `setter` 和 `getter`。这样的话,给这个对象的某个值赋值,就会触发 `setter`,那么就能监听到了数据变化。
- **解析器 Compile**:解析 Vue 模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新。
- **订阅者 Watcher**:Watcher 订阅者是 `Observer` 和 `Compile` 之间通信的桥梁 ,主要的任务是订阅 `Observer` 中的属性值变化的消息,当收到属性值变化的消息时,触发解析器 `Compile` 中对应的更新函数。每个组件实例都有相应的 `watcher` 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 `setter` 被调用时,会通知 `watcher` 重新计算,从而致使它关联的组件得以更新——这是一个典型的观察者模式
- **订阅器 Dep**:订阅器采用 发布-订阅 设计模式,用来收集订阅者 `Watcher`,对监听器 `Observer` 和 订阅者 `Watcher` 进行统一管理。
<a name="XzWQy"></a>
#### 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` 作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利;
```javascript
{
{
// 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 时,可以通知依赖更新.如果数组中包含着引用类型。会对数组中的引用类型再次进行监控。 ```javascript 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);
<a name="u2wG2"></a>
### Vue**异步渲染**
因为如果不采用异步更新,那么每次更新数据都会对当前组件进行重新渲染。<br />所以为了性能考虑,Vue会在本轮数据更新后,再去异步更新视图。
```javascript
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, 应该程序有覆盖这个值的情况出现
背景
默认情况
调整后
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,并不会快很多