Skip to content

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描述
beforeCreatesetup组件实例被创建之初,组件的属性生效之前,如 data 属性
created组件实例已经完全创建,属性也绑定,但真实 dom 还没有生成,$el 还不可用
beforeMountonBeforeMount在挂载开始之前被调用:相关的 render 函数首次被调用
mountedonMountedel 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子
beforeUpdateonBeforeUpdate组件数据更新之前调用,发生在虚拟 DOM 打补丁之前
updatedonUpdated组件数据更新之后
beforeDestoryonBeforeUnmount组件销毁前调用
destoryedonUnmounted组件销毁后调用
activitedkeep-alive 专属,组件被激活时调用
deadctivatedkeep-alive 专属,组件被销毁时调用

组件生命周期调用顺序

  • 组件的调用顺序都是先父后子,渲染完成的顺序是先子后父。
  • 组件的销毁操作是先父后子,销毁完成的顺序是先子后父。
html
<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>

生命周期示意图

image.png

通信

方法描述
$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:计算的属性只有在它的相关依赖发生改变时才会重新求值

image.png

computed 监测的是依赖值,依赖值不变的情况下其会直接读取缓存进行复用,变化的情况下才会重新计算;而 watch 监测的是属性值, 只要属性值发生变化,其都会触发执行回调函数来执行一系列操作。

computed 能做的,watch 都能做,反之则不行;能用 computed 的尽量用 computed

functional

html
<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存放,上报到监控平台

javascript
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 只作用与当前组件中的元素

vue
<style scoped>
.red {
  color: red;
}
</style>

实现原理则是通过 POSTCSS 来实现转换

vue
<template>
  <div class="red" data-v-0013a924>Hello</div>
</template>

<style>
.red[data-v-0013a924] {
  color: red;
}
</style>

深度作用选择器

使用 >>> 操作符可以使 scoped 样式中的一个选择器能够作用得“更深”,例如影响子组件

vue
<style scoped>
.red >>> a {
  color: red;
}
</style>

Sass 之类的预处理器无法正确解析 >>> 。这种情况下你可以使用 /deep/::v-deep 操作符取代

vue
<style lang="scss" scoped>
.red {
  color: red;
  /deep/ a {
    color: blue;
  }
  ::v-deep a {
    color: yellow;
  }
}
</style>

module css

vue
<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

javascript
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 属性

vue
<template>
  <div>
    <p>{{ text }}</p>
    <button v-on:click="$emit('accepted')">OK</button>
  </div>
</template>
<script>
export default {
  props: ['text'],
  emits: ['accepted'],
}
</script>

多事件处理

vue
<!-- 这两个 one() 和 two() 将执行按钮点击事件 -->
<button @click="one($event), two($event)"> Submit </button>

Fragment

之前组建的节点必须有一个根元素,Vue3 可以有多个根元素,也可以有把文本作为根元素

尽管 Fragment 看起来像一个普通的 DOM 元素,但它是虚拟的,根本不会在 DOM 树中呈现。这样我们可以将组件功能绑定到一个单一的元素中,而不需要创建一个多余的 DOM 节点。

目前你可以在 Vue2 中使用 vue-fragments 库来使用 Fragments,而在 Vue3 中,你将会在开箱即用!

vue
<template>
  <h1>{{ msg }}</h1>
  <ul>
    <li v-for="product in products" :key="product.id">{{ product.title }}</li>
  </ul>
</template>

render(JSX/TSX)

可以使用空标签替代

jsx
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
<!-- vue 2.x -->
<MyCompontent v-bind:title.sync="title" />

<!-- vue 3.x -->
<MyCompontent v-model:title="title" />

异步组件的引用方式

创建一个只有在需要时才会加载的异步组件

defineAsyncComponent

移除 filter

teleport

React 有个 Portalshttps://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 将只是一个具有插槽的组件

vue
<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 初始化时,一定会执行一次, 主要是为了收集需要监听数据

钩子函数声明周期

编译优化的点(面试常问)

https://vue-next-template-explorer.netlify.app

  • PatchFlag 静态标记
    • 编译模板时,动态节点做标记
    • 标记,分为不同的类型,如 TEXT/CLASS/PROPS
    • diff 算法时,可区分静态节点,以及不同类型的动态节点
  • hoistStatic 静态提升
    • 将静态节点的定义,提升到父作用域,缓存起来
    • 多个相邻的静态节点,会被合并起来
    • 典型的拿空间换时间的优化策略
  • cache Handler 缓存事件
  • SSR 优化
    • 静态节点直接输出,绕过 vdom
    • 动态节点动态渲染(以之前一致)
  • Tree-shaking 优化

Composition API 与 React Hooks 区别对比(面试常问)

  • 前者 **setup** 只会调用一次,而后者函数会被多次调用
  • 前者不需要缓存数据(因为 setup 只会调用一次),后者需要手动调用函数进行缓存(useMemouseCallback
  • 前者不需要考虑调用顺序,而后者需要保证 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 的代码分割功能
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')

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>

动态路由匹配

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">
	<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>

嵌套路由

html
<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()] : [],
})

持久化可以使用

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 ) 如果属性发生变化会通知相关依赖进行更新操作。

image.png

  • 深入理解
    • 监听器 Observer:对数据对象进行遍历,包括子属性对象的属性,利用 Object.defineProperty() 对属性都加上 settergetter。这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化。
    • 解析器 Compile:解析 Vue 模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新。
    • 订阅者 Watcher:Watcher 订阅者是 ObserverCompile 之间通信的桥梁 ,主要的任务是订阅 Observer 中的属性值变化的消息,当收到属性值变化的消息时,触发解析器 Compile 中对应的更新函数。每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新——这是一个典型的观察者模式
    • 订阅器 Dep:订阅器采用 发布-订阅 设计模式,用来收集订阅者 Watcher,对监听器 Observer 和 订阅者 Watcher 进行统一管理。

Proxy 替代 Object.defineProperty

  • Proxy 只会代理对象的第一层,Vue3 是怎样处理这个问题的呢?
    • 判断当前 Reflect.get 的返回值是否为 Object,如果是则再通过 reactive 方法做代理, 这样就实现了深度观测。
    • 监测数组的时候可能触发多次 get/set,那么如何防止触发多次呢?我们可以判断 key 是否为当前被代理对象 target 自身属性,也可以判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行 trigger
  • 优势
    • 直接监听对象而非属性;
    • 直接监听数组的变化
      • Proxy 有多达 13 种拦截方法,不限于 applyownKeysdeletePropertyhas 等等是 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)

Vue异步渲染

因为如果不采用异步更新,那么每次更新数据都会对当前组件进行重新渲染。 所以为了性能考虑,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 方法就是异步方法

javascript
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 是具备缓存的,只要当依赖的属性发生变化时才会更新视图

javascript
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

javascript
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,那么不用依次遍历查找,只需要查找一次就能获取结果,提高了查找效率。

javascript
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方法。

javascript
// 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内部拦截了这些方法。

javascript
// 重写数组方法,然后再把原型指回原方法
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继承

javascript
// ...

执行一次

闭包

javascript
function once(fn) {
  var called = false
  return function () {
    if (!called) {
      called = true
      fn.apply(this, arguments)
    }
  }
}

浅拷贝

简单的深拷贝我们可以用 JSON.stringify() 来实现。 vue源码中的looseEqual 浅拷贝思路,先类型判断再递归调用

javascript
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

javascript
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

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 压缩

javascript
/* 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

javascript
/* 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)

javascript
/* 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, 应该程序有覆盖这个值的情况出现

背景

项目过大遇到打包栈异常情况

image.png

默认情况

内存为 2048 MB

image.png

调整后

设置 12288 MB(可根据机器自行配置)

image.png

coding

javascript
/* 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))
  },
}

按照模块大小自动分割第三方库

javascript
/* 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,并不会快很多

Layout Switch

Adjust the layout style of VitePress to adapt to different reading needs and screens.

Expand all
The sidebar and content area occupy the entire width of the screen.
Expand sidebar with adjustable values
Expand sidebar width and add a new slider for user to choose and customize their desired width of the maximum width of sidebar can go, but the content area width will remain the same.
Expand all with adjustable values
Expand sidebar width and add a new slider for user to choose and customize their desired width of the maximum width of sidebar can go, but the content area width will remain the same.
Original width
The original layout width of VitePress

Page Layout Max Width

Adjust the exact value of the page width of VitePress layout to adapt to different reading needs and screens.

Adjust the maximum width of the page layout
A ranged slider for user to choose and customize their desired width of the maximum width of the page layout can go.

Content Layout Max Width

Adjust the exact value of the document content width of VitePress layout to adapt to different reading needs and screens.

Adjust the maximum width of the content layout
A ranged slider for user to choose and customize their desired width of the maximum width of the content layout can go.

Spotlight

Highlight the line where the mouse is currently hovering in the content to optimize for users who may have reading and focusing difficulties.

ONOn
Turn on Spotlight.
OFFOff
Turn off Spotlight.