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