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 /><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`:计算的属性只有在它的相关依赖发生改变时才会重新求值<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: useimport 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仍旧返回PromiseAction通常是异步的,要知道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` ) 如果属性发生变化会通知相关依赖进行更新操作。- - 深入理解- **监听器 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); // 1console.log(proxyObj.b); // 0proxyObj.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 changeob.dep.notify(); // 当调用数组方法后,手动通知视图更新return result;});
});
// 进行深度监听 this.observeArray(value);
<a name="u2wG2"></a>### Vue**异步渲染**因为如果不采用异步更新,那么每次更新数据都会对当前组件进行重新渲染。<br />所以为了性能考虑,Vue会在本轮数据更新后,再去异步更新视图。```javascriptfunction 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 flushif (!waiting) {waiting = true;if (!config.async) {flushSchedulerQueue();return;}// 调用nextTick方法 批量的进行更新nextTick(flushSchedulerQueue);}}}
nextTick实现原理
nextTick 方法主要是使用了宏任务和微任务,定义了一个异步方法。多次调用 nextTick 会将方法存入
队列中,通过这个异步方法清空当前队列。 所以这个 nextTick 方法就是异步方法
var timerFunc;// promiseif (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({})); // Objectconsole.log(toRawType([])); // Arrayconsole.log(toRawType(true)); // Booleanconsole.log(toRawType(undefined)); // Undefinedconsole.log(toRawType(null)); // Nullconsole.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]; }}// 利用闭包,每次判断是否是内置标签只需调用isHTMLTagvar isHTMLTag = makeMap('html,body,base,head,link,meta,style,title')console.log('res', isHTMLTag('body')) // true
二维数组扁平化
vue中_createElement格式化传入的children的时候用到了simpleNormalizeChildren函数,原来是为了拍平数组,使二维数组扁平化,类似lodash中的flatten方法。
// lodash flattenconsole.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;}// es6function 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 externalsexternals: {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 pluginsconfig.plugins.push(new CompressionWebpackPlugin({algorithm: "gzip",test: new RegExp("\\.(" + productionGzipExtensions.join("|") + ")$"),threshold: 10240,minRatio: 0.8,}));}// if prod, add externalsif (isProd) {config.externals = assetsCDN.externals;// delete consoleconfig.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) => {// 生产环境下使用CDNif (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 versionconfig.plugins = config.plugins.filter(p => !(p instanceof ForkTsCheckerWebpackPlugin));// copy the options from the original ForkTsCheckerWebpackPlugin// instance and add the memoryLimit propertyconst 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,并不会快很多
