likes
comments
collection
share

万字总结大厂面试题—Vue篇

作者站长头像
站长
· 阅读数 8

往期面试题

js手写题集合

Vue源码系列

字节面试题

vue篇

MVVM模式优缺点,以及MVVM的核心(vivo、商汤科技、滴滴)

  • 优点

    • 降低了代码的耦合性,提高了视图或者逻辑的重用性,因为视图和数据层都是独立的,不必因为谁而改变谁。
    • 自动更新DOM,利用双向绑定,关注业务逻辑
  • 缺点

    • bug调试困难,界面出现异常,可能是view或者model层发生错误,并且数据绑定的声明是无法进行打断点debug,可以用扩展的工具进行调试
    • 一个大的模块中model也会很大,会占用更多的内存,相应viewmodel的构建和维护也是成本
  • 核心:双向绑定

vue2和vue3的区别(vivo)

  • 响应式监测机制的改变,用es6的proxy来进行代理,解决了一大部分基于Object.defineProperty所带来的的问题
    • 当添加或者删除对象的属性时,Object.definePropert监听不到,只能通过set来处理,例如:this.set来处理,例如:this.set来处理,例如:this.set( this.data,'job','teacher' ),而proxy是是直接代理整个对象,而非对象的属性,可以监听到添加或者删除。
    • 无法检测到数组的长度的变化,proxy代理数组,可以解决
  • 生命周期的使用上的不同,setup围绕 beforeCreate 和 created 生命周期钩子运行,在script setup语法糖里进行使用,调用生命周期钩子函数
  • composition api,vue2是options api,一个逻辑会散落在文件不同位置,可读性差
  • teleport组件
  • 静态提升看图理解双端diff算法 - 掘金 (juejin.cn)
  • 全局api也只能通过导入的方式进行调用,做到更好的treesharking(可能是调用了全局api却没有使用...)

SPA首页加载慢怎么解决(商汤科技)

组件通信的方式(vivo)

  • props:父组件传递数据给子组件
  • $emit:子组件通过触发自定义事件传递数据给父组件
  • ref:获取组件实例,调用其方法
  • EventBus:兄弟组件传值
  • provide和inject:全局通信
  • 状态管理工具:vuex/pinia
  • 本地存储localstorage

Computed和Watch的区别(滴滴)

  • 前者支持缓存,不支持异步,当有异步操作的时候,无法发起监听的作用,根据依赖的响应式数据动态计算而来的属性,它具有缓存和实时更新的特点,适用于需要进行复杂计算的场景。
  • 后者支持异步,监听数据的变化,并在特定数据发生变化时执行自定义的回调函数,适用于需要执行异步操作或复杂逻辑的场景。
const count = ref(1)
const plusOne = computed(() => count.value + 1)

console.log(plusOne.value) // 2

plusOne.value++ // 错误
const state = reactive({ count: 0 })
watch(
  () => state.count,
  (count, prevCount) => {
    /* ... */
  }
)

你项目里用到了keepalive,有哪几种使用方式?(阅文集团)

keepalive是什么

keep-alive是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在父组件链中;使用keep-alive包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。

keepailve的基本用法

<script setup>
import { shallowRef } from 'vue'
import CompA from './CompA.vue'
import CompB from './CompB.vue'
// 进行浅层代理,避免不必要的花销
const current = shallowRef(CompA)
</script>

<template>
  <div class="demo">
    <!--切换时,current.value值会发生改变  -->
    <label><input type="radio" v-model="current" :value="CompA" /> A</label>
    <label><input type="radio" v-model="current" :value="CompB" /> B</label>
    <KeepAlive>
      <component :is="current"></component>
    </KeepAlive>
  </div>
</template>

  • 在vue-router中的应用
<keep-alive :include="whiteList" :exclude="blackList" :max="amount">
  <router-view></router-view>
</keep-alive>

include定义缓存白名单,keep-alive会缓存命中的组件;exclude定义缓存黑名单,被命中的组件将不会被缓存,优先级高于前者;max定义缓存组件上限,超出上限使用LRU的策略置换缓存数据。

// 只缓存组件name为a或者b的组件
<keep-alive include="a,b"> 
  <component />
</keep-alive>

// 组件name为c的组件不缓存(可以保留它的状态或避免重新渲染)
<keep-alive exclude="c"> 
  <component />
</keep-alive>

// 如果同时使用include,exclude,那么exclude优先于include, 下面的例子只缓存a组件
<keep-alive include="a,b" exclude="b"> 
  <component />
</keep-alive>

// 如果缓存的组件超过了max设定的值5,那么将删除第一个缓存的组件
<keep-alive exclude="c" max="5"> 
  <component />
</keep-alive>

配合router的高级使用

router-view也是一个组件,如果直接被包在keepalive里面,那么所有路径匹配到的视图组件都会被缓存,如下:

<keep-alive>
    <router-view>
        <!-- 所有路径匹配到的视图组件都会被缓存! -->
    </router-view>
</keep-alive>

如果只想要router-view里面的某个组件被缓存,怎么办?

  • 使用 include/exclude
  • 使用 meta 属性

1.使用 include (exclude例子类似)

//只有路径匹配到的 name 为 a 组件会被缓存
<keep-alive include="a">
    <router-view></router-view>
</keep-alive>

2.使用 meta 属性

// routes 配置
export default [
  {
    path: '/',
    name: 'home',
    component: Home,
    meta: {
      keepAlive: true // 需要被缓存
    }
  }, {
    path: '/profile',
    name: 'profile',
    component: Profile,
    meta: {
      keepAlive: false // 不需要被缓存
    }
  }
]
<keep-alive>
    <router-view v-if="$route.meta.keepAlive">
        <!-- 这里是会被缓存的视图组件,比如 Home! -->
    </router-view>
</keep-alive>

<router-view v-if="!$route.meta.keepAlive">
    <!-- 这里是不会被缓存的视图组件,比如 Profile! -->
</router-view>

配合vue-routertransition的高级使用

// App.vue
<script setup>
import { reactive } from 'vue';

const state = reactive({
  transitionName: 'slide-left'
})
const router = useRouter()

router.beforeEach((to, from, next) => {
  if (to.meta.index > from.meta.index) {
    // 从主页面 去到子页面
    state.transitionName = 'slide-left'
  } else if (to.meta.index < from.meta.index) {
    // 子页面回到主页面
    state.transitionName = 'slide-right'
  } else {
    // 平级
    state.transitionName = ''
  }
})

</script>
<template>
<!-- 渲染路由视图 -->
  <router-view v-slot="{ Component }">
    <!-- 过渡效果 -->
    <transition :name="state.transitionName">
      <!-- 缓存组件 -->
      <keep-alive> 
        <component :is='Component' :key="$route.name" v-if="$route.meta.keepAlive" />
      </keep-alive>
    </transition>
  </router-view>
  <!-- 渲染路由视图 -->
  <router-view v-slot="{ Component }">
    <!-- 过渡效果 -->
    <transition :name="state.transitionName">
      <!-- 不缓存组件 -->
      <component :is='Component' :key="$route.name" v-if="!$route.meta.keepAlive" />
    </transition>
  </router-view>

</template>

<style lang="stylus">
.slide-left-enter-active, 
.slide-left-leave-active, 
.slide-right-enter-active, 
.slide-right-leave-active
  height 100%
  /* 提前告知浏览器, 即将会有transform 渐变 */
  will-change transform
  transition all 600ms
  position absolute
  backface-visibility hidden 
.slide-right-enter-from 
  opacity 0
  transform translate3d(-100%, 0, 0)
.slide-right-leave-active 
  opacity 0
  transform translate3d(100%, 0, 0) 
.slide-left-enter-from 
  opacity 0
  transform translate3d(100%, 0, 0)
.slide-left-leave-active 
  opacity 0
  transform translate3d(-100%, 0, 0)
</style>
// router.js
import { createRouter, createWebHashHistory } from 'vue-router'
import Home from '@/views/Home/index.vue'

const routes = [
    {
      path: '/',
      redirect: '/home',
    },
    {
      path: '/home',
      name: 'home',
      component: Home,
      meta: {
        keepAlive: true,
        index: 1
      },
    },
    {
      path: '/detail/:id',
      name:'detail',
      meta: {
        index: 3
      },
      component: () => import('@/views/Detial/index.vue')
    },
    {
      path: '/login',
      name:'login',     
      meta: {
        index: 2
      },
      component: () => import('@/views/Login/index.vue')
    },
    {
      path: '/preferential',
      name: 'preferential',
      meta: {
        index: 1
      },
      component: () => import('@/views/Preferential/index.vue')
    },
    {
      path: '/cart',
      name: 'cart',
      meta: {
        index: 1
      },
      component: () => import('@/views/Cart/index.vue')
    },
    {
      path: '/user',
      name: 'user',
      meta: {
        index: 1,
        isPass: true
      },
      component: () => import('@/views/User/index.vue')
    }
]

 const router = createRouter({
    history: createWebHashHistory(),
    routes
})

export default router

实现的效果:

  • 使用vue的transition组件,在定义router时设置meta.index数值来判断切换动画的使用哪种效果
  • 使用vue的keepailve组件,在定义router时设置meta.keepalive数值来判断是否要进行组件的缓存

nextTick()的使用场景,为什么要有nextTick()?(快手)

  • 异步更新DOM:当你修改了Vue实例的数据后Vue会异步执行DOM更新。如果你想要在DOM更新完成后执行一些操作(例如获取更新后的DOM元素),可以使用nextTick()来确在DOM更新完成后执行回调函数。
  • 更新后的DOM操作:有时候,你可能需要更新后的DOM上进行一些操作,例如获取某个元素的尺寸、位置等信息。于Vue的DOM更新是异步,直接在数据变化后立即访问DOM可能无获取到最新的结果。通过将操作放在nextTick()的调函数中,可以保在DOM更新完成后再进行相关操作。
  • 批量更新优化:Vue在更新DOM时会对多个数据变化进行批量处理,以高性能。而nextTick()会将回调函数入到下一个DOM更新循中执行,这样可以将多个nextTick()回调函数合并为一个,减少必要的DOM操作,提高性能。

总结起来,nextTick()的主要用是在下次DOM更新循环结束后执行回调函数,用处理DOM更新后的操作或者确获取到最新的DOM状态。

有兴趣看源码的可以点这里

怎么实现v-model?(度小满)

点击这里

了解过vue的编译流程吗?(阅文集团)

  • parse解析阶段:使用正则对template模板进行解析,将标签、指令、属性等转化为AST抽象语法树
  • optimize优化阶段:遍历AST,对静态节点进行标记和提升,优化runtime的性能
  • generate生成阶段:将AST转化为render函数字符串

万字总结大厂面试题—Vue篇

// 开始
<div>  
  <h2 v-if="message">{{message}}</h2>  
  <button @click="showName">showName</button>  
</div>
// 编译后
with (this) {  
    return _c(  
      'div',  
      [  
        (message) ? _c('h2', [_v(_s(message))]) : _e(),  
        _v(' '),  
        _c('button', { on: { click: showName } }, [_v('showName')])  
      ])  
    ;  
}
// 这里的 `_c` 对应的是虚拟 DOM 中的 `createElement` 方法

Vue子组件和父组件的执行顺序(快手)

加载渲染过程:

  1. 父组件 beforeCreate
  2. 父组件 created
  3. 父组件 beforeMount
  4. 子组件 beforeCreate
  5. 子组件 created
  6. 子组件 beforeMount
  7. 子组件 mounted
  8. 父组件 mounted

更新过程:

  1. 父组件 beforeUpdate
  2. 子组件 beforeUpdate
  3. 子组件 updated
  4. 父组件 updated

销毁过程:

  1. 父组件 beforeDestroy
  2. 子组件 beforeDestroy
  3. 子组件 destroyed
  4. 父组件 destoryed

vuex怎么实现的(美团)

vuex的基本使用

//index.js
import { createStore } from './gvuex.js'

const store = createStore({
  // 定义store的状态
  state() {
    return {
      count: 1 
    }
  },
  // 定义获取state状态数据的计算属性getters,以下的值都被认为是state的派生值
  getters: {
    double(state) { 
      return state.count * 2
    }
  },
  // 定义修改state状态数据的方法mutations
  mutations: {
    add(state) { 
      state.count++
    }
  },
  // 定义异步操作的方法actions
  actions: {
    asyncAdd({ commit }) { 
      setTimeout(() => {
        commit('add')
      }, 1000)
    }
  }
})
// App.vue
<script setup>
import { useStore } from '../store/gvuex'
import { computed } from 'vue'

let store = useStore();
let count = computed(()=>{ store.state.count })
let double = computed(()=>{ store.getters.double })
function add() {
  store.commit('add')
}
function asyncAdd() {
  store.dispatch('asyncAdd')
}
</script>
<template>
<div class="">
  {{ count }} * 2 = {{ double }}
  <button @click="add">add</button>
  <button @click="asyncAdd">async add</button>
</div>
</template>
<style scoped>

</style>

知道了vuex的用法,你会不会发出以下疑问:

  1. 为什么要store.commit('add')才能触发事件执行呢? 可不可以进行直接调用mutation函数进行操作呢?
  2. 为什么不可以直接对state存储的状态进行修改,只能通过调用mutation函数的方式修改呢?
  3. 为什么存在异步调用的函数需要store.dispatch('asyncAdd')函数才能完成呢?可以直接调用store.commit('asyncAdd')嘛?如果不可以,为什么呢?
  4. createStore()useStore()到底发生了什么?

那么下面就来一一解密吧。

vue里注册全局组件

js
复制代码
import { createApp } from 'vue'
import store from './store'
import App from './App.vue'

const app = createApp(App)
app
    .use(store)
    .mount('#app')

app.use() 用来安装插件,接受一个参数,通常是插件对象,该对象必须暴露一个install方法,调用app.use()时,会自动执行install()方法。

解析Store类里的流程


import { reactive, inject } from 'vue'

// 定义了一个全局的 key,用于在 Vue 组件中通过 inject API 访问 store 对象
const STORE_KEY = '__store__'

// 用于获取当前组件的 store 对象
function useStore() {
    return inject(STORE_KEY)
}

// Store 类,用于管理应用程序状态
class Store {

    // 构造函数,接收一个包含 state、mutations、actions 和 getters 函数的对象 options,然后将它们保存到实例属性中
    constructor(options) {
        this.$options = options;

        // 使用 Vue.js 的 reactive API 将 state 数据转换为响应式对象,并保存到实例属性 _state 中        
        this._state = reactive({
            data: options.state()
        })

        // 将 mutations 和 actions 函数保存到实例属性中
        this._mutations = options.mutations
        this._actions = options.actions;

        // 初始化 getters 属性为空对象
        this.getters = {};

        // 遍历所有的 getters 函数,将其封装成 computed 属性并保存到实例属性 getters 中
        Object.keys(options.getters).forEach(name => {
            const fn = options.getters(name);
            this.getters[name] = computed(() => fn(this.state));
        })
    }

    // 用于获取当前状态数据
    get state() {
        return this._state.data
    }
    // 获取mutation内定义的函数并执行
    commit = (type, payload) => {
        const entry = this._mutations[type]
        entry && entry(this.state, payload)
    }
    // 获取actions内定义的函数并返回函数执行结果
    // 简略版dispatch
    dispatch = (type, payload) => { 
        const entry = this._actions[type];
        return entry && entry(this, payload)
    }

    // 将当前 store 实例注册到 Vue.js 应用程序中
    install(app) {
        app.provide(STORE_KEY, this)
    }
}

// 创建一个新的 Store 实例并返回
function createStore(options) {
    return new Store(options);
}

// 导出 createStore 和 useStore 函数,用于在 Vue.js 应用程序中管理状态
export {
    createStore,
    useStore
}

是不是很惊讶于vuex的底层实现就短短几十行代码就实现了,嘿嘿那是因为从vue里引入了reactiveinjectcomputed,并且对很大一部分的源码进行了省略,dispatchcommit远比这复杂多了,有兴趣去了解reactive的实现可以去看我另一篇文章学VUE源码之手写min版响应式原型 - 掘金 (juejin.cn),下面解答上面抛出的问题吧。

解答

问题一:为什么要store.commit('add')才能触发事件执行呢? 可不可以进行直接调用mutation函数进行操作呢?

解答:store类里根本没有mutation方法,只能通过调用commit方法来执行mutation里的函数列表。

问题二:为什么不可以直接对state存储的状态进行修改,只能通过调用函数的方式修改呢?

解答:Vuex 通过强制限制对 store 的修改方式来确保状态的可追踪性。只有通过 mutation 函数才能修改 store 中的状态,这样可以轻松地跟踪状态的变化,也可以避免无意中从不同的组件中直接修改 store 导致的代码难以维护和调试的问题。

问题三:为什么存在异步调用的函数需要store.dispatch('asyncAdd')函数才能完成呢?可以直接调用store.commit('asyncAdd')嘛?如果不可以,为什么呢?

解答:实际上dispatch方法和commit方法远不止这么简单,下面先贴出部分vuex的关于这两个方法的源码部分

Store.prototype.dispatch = function dispatch (_type, _payload) {
    var this$1$1 = this;

  // check object-style dispatch
  var ref = unifyObjectStyle(_type, _payload);
    var type = ref.type;
    var payload = ref.payload;

  var action = { type: type, payload: payload };
  var entry = this._actions[type];
  if (!entry) {
    if ((process.env.NODE_ENV !== 'production')) {
      console.error(("[vuex] unknown action type: " + type));
    }
    return
  }

  try {
    this._actionSubscribers
      .slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
      .filter(function (sub) { return sub.before; })
      .forEach(function (sub) { return sub.before(action, this$1$1.state); });
  } catch (e) {
    if ((process.env.NODE_ENV !== 'production')) {
      console.warn("[vuex] error in before action subscribers: ");
      console.error(e);
    }
  }

  var result = entry.length > 1
    ? Promise.all(entry.map(function (handler) { return handler(payload); }))
    : entry[0](payload);

  return new Promise(function (resolve, reject) {
    result.then(function (res) {
      try {
        this$1$1._actionSubscribers
          .filter(function (sub) { return sub.after; })
          .forEach(function (sub) { return sub.after(action, this$1$1.state); });
      } catch (e) {
        if ((process.env.NODE_ENV !== 'production')) {
          console.warn("[vuex] error in after action subscribers: ");
          console.error(e);
        }
      }
      resolve(res);
    }, function (error) {
      try {
        this$1$1._actionSubscribers
          .filter(function (sub) { return sub.error; })
          .forEach(function (sub) { return sub.error(action, this$1$1.state, error); });
      } catch (e) {
        if ((process.env.NODE_ENV !== 'production')) {
          console.warn("[vuex] error in error action subscribers: ");
          console.error(e);
        }
      }
      reject(error);
    });
  })
};

Store.prototype.commit = function commit (_type, _payload, _options) {
    var this$1$1 = this;

  // check object-style commit
  var ref = unifyObjectStyle(_type, _payload, _options);
    var type = ref.type;
    var payload = ref.payload;
    var options = ref.options;

  var mutation = { type: type, payload: payload };
  var entry = this._mutations[type];
  if (!entry) {
    if ((process.env.NODE_ENV !== 'production')) {
      console.error(("[vuex] unknown mutation type: " + type));
    }
    return
  }
  this._withCommit(function () {
    entry.forEach(function commitIterator (handler) {
      handler(payload);
    });
  });

  this._subscribers
    .slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
    .forEach(function (sub) { return sub(mutation, this$1$1.state); });

  if (
    (process.env.NODE_ENV !== 'production') &&
    options && options.silent
  ) {
    console.warn(
      "[vuex] mutation type: " + type + ". Silent option has been removed. " +
      'Use the filter functionality in the vue-devtools'
    );
  }
};

源码里我们能看到,dispatch方法返回的是一个Promise对象,而commit方法没有返回值,完全进行的是同步代码的操作,虽然返回值可能适用的场景不多,但是这样设计的主要目的还是为了确保状态的可追踪性

问题四: createStore()useStore()到底发生了什么?

当我们去调用 createStore()函数,其构造函数就会接收一个包含 statemutationsactions getters 函数的对象 options, 然后将它们保存到实例属性中,此时state中的值都会转换为响应式对象,同时遍历所有的getters函数,将其封装成computed属性并保存到实例属性getters中,而在main.js里调用了app.use(), install方法自动执行,将将当前 store 实例注册到 Vue.js 应用程序中,只需要调用useStore()就可以拿到全局状态管理的store实例了,可以靠injectprovide实现全局共享。

vue-router怎么实现的?compents组件干了什么?(美团)

Vue-router的基本使用


// index.js
// 本例子为了简化,使用 hash 路由模式
import { createRouter,createWebHashHistory } from 'vuex'

import Home from '../views/Home.vue'
import About from '../views/About.vue'

const routes = [
    {
        path: '/',
        name: 'Home',
        component: Home
    },
    {
        path: '/about',
        name: 'About',
        component: About
    },
]

const router = createRouter({
    history: createWebHashHistory(),
    routes
})

export default router
js
复制代码
// app.vue
<script setup>

</script>
<template>

<router-link to="/">首页</router-link>
<router-link to="/about">关于</router-link>
<router-view/>

</template>
<style> 

</style>
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router/index'

const app = createApp(App)
app
    .use(router)  
    .mount('#app')

我们就可以完成了一个简单的单页应用。

简单版本的实现

注册两个全局组件


// index.js
import { inject, ref } from 'vue'
import RouterLink from './RouterLink.vue'
import RouterView from './RouterView.vue'

// 定义一个常量,用于全局访问路由对象
const ROUTER_KEY = '__router__' 

// 返回路由实例
const createRouter = (options) => {
    return new Router(options)
}

// 使得全局可以获取路由对象
const useRouter = () => {
    return inject(ROUTER_KEY)
}

// 返回 hash 模式下的路由历史记录对象
const createWebHashHistory = () => {

}

// 路由类工厂
class Router{
    constructor(options) {

    }
    // 注册路由相关组件和提供全局路由对象
    install(app) {
        // 路由对象,提供给全局访问
        app.provide(ROUTER_KEY, this)
        // 注册全局组件
        app.component('router-link', RouterLink)
        app.component('router-view', RouterView)
    }
}

// 导出模块需要的变量和方法
export {
    useRouter,
    createRouter, // 返回路由实例
    createWebHashHistory // 返回hash 事件监听
}

// RouterLink.vue
<template>
    <a :href="'#' + props.to">
        <!-- 插槽 -->
        <slot />
    </a>
</template>

<script setup>
import { defineProps } from 'vue'

let props = defineProps({
    to: {
        type: String,
        required: true
    }
})
</script>

<style scoped>

</style>
// RouterView.vue
<template>
    <!-- 动态渲染组件 -->
    <component :is="component"></component>
</template>
<script setup>
// 引入 useRouter 和 computed
import { useRouter } from './index' 
import { computed } from 'vue'

// 获取当前路由
const router = useRouter()

// 计算属性 component,根据当前路由找到对应的组件
const component = computed(() => {
    const route = router.routes.find(
        (item) => item.path === router.current.value
    )
    return route ? route.component : null
})
</script>
<style scoped>
   
</style>

RouterLink 组件实现的流程:

  • 通过 props 组件通信的方式传递要重新指向的锚点
  • 通过 a 标签来进行路由的转换
  • 监听了 hashchange 事件,然后进行当前路由信息的更新

RouterView 组件实现的流程:

  • 通过路由信息来获取当前路由的内容
  • 调用 component 组件动态绑定来实现新组件内容的渲染

完善

import { inject, ref } from 'vue'
import RouterLink from './RouterLink.vue'
import RouterView from './RouterView.vue'

// 定义一个常量,用于全局访问路由对象
const ROUTER_KEY = '__router__' 

// 返回路由实例
const createRouter = (options) => {
    return new Router(options)
}

// 使得全局可以获取定义的路由对象
const useRouter = () => {
    return inject(ROUTER_KEY)
}

// 返回 hash 模式下的路由历史记录对象
const createWebHashHistory = () => {
    // 内部方法,用于绑定事件监听
    function bindEvents(fn) {
        window.addEventListener('hashchange', fn)
    }
    // 返回 hash 值和事件监听的方法
    return {
        bindEvents,
        url: window.location.hash.slice(1) 
    }
}

// 路由类工厂
class Router{
    constructor(options) {
        this.history = options.history  // 路由历史记录对象
        this.routes = options.routes  // 路由的配置数组
        this.current = ref(this.history.url) // 响应式的当前路由,自动更新
        // 给路由历史记录对象绑定事件监听器,当 URL 改变时,更新当前路由值
        this.history.bindEvents(() => {
            this.current.value = window.location.hash.slice(1) // 去除#号
        })
    }
    // 注册路由相关组件和提供全局路由对象
    install(app) {
        // 路由对象,提供给全局访问
        app.provide(ROUTER_KEY, this)
        // 注册全局组件
        app.component('router-link', RouterLink)
        app.component('router-view', RouterView)
    }
}

// 导出模块需要的变量和方法
export {
    useRouter,
    createRouter, // 返回路由实例
    createWebHashHistory // 返回 hash 事件监听
}

diff算法(度小满)

Vue的双端diff算法

react的diff算法有个很明显的缺点,对于旧节点的最后子节点挂载到最前面时,整个子节点都需要进行移动。比如:旧VNode:A、B、C、D;新VNode:D、A、B、C;

下面是vue的双端比较算法:

万字总结大厂面试题—Vue篇

// 当新的 children 中有多个子节点时,会执行该 case 语句块
let oldStartIdx = 0 // 定义旧节点开始索引
let oldEndIdx = prevChildren.length - 1 // 定义旧节点结束索引
let newStartIdx = 0 // 定义新节点开始索引
let newEndIdx = nextChildren.length - 1 // 定义新节点结束索引
let oldStartVNode = prevChildren[oldStartIdx] // 定义旧节点开始位置对应的虚拟节点
let oldEndVNode = prevChildren[oldEndIdx] // 定义旧节点结束位置对应的虚拟节点
let newStartVNode = nextChildren[newStartIdx] // 定义新节点开始位置对应的虚拟节点
let newEndVNode = nextChildren[newEndIdx] // 定义新节点结束位置对应的虚拟节点

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { // 循环比较旧节点和新节点
  if (!oldStartVNode) { 
  // 如果旧节点开始位置的虚拟节点不存在,则将旧节点开始索引增加,并重新定义旧节点开始位置对应的虚拟节点
    oldStartVNode = prevChildren[++oldStartIdx]
  } else if (!oldEndVNode) { 
  // 如果旧节点结束位置的虚拟节点不存在,则将旧节点结束索引减少,并重新定义旧节点结束位置对应的虚拟节点
    oldEndVNode = prevChildren[--oldEndIdx]
  } else if (oldStartVNode.key === newStartVNode.key) { 
  // 如果旧节点开始位置虚拟节点的 key 和新节点开始位置虚拟节点的 key 相同,则进行 patch 操作
    patch(oldStartVNode, newStartVNode, container)
    oldStartVNode = prevChildren[++oldStartIdx]// 对旧节点开始索引和新节点开始索引都加 1,表示这两个节点已经匹配完成
    newStartVNode = nextChildren[++newStartIdx]
  } else if (oldEndVNode.key === newEndVNode.key) { 
  // 如果旧节点结束位置虚拟节点的 key 和新节点结束位置虚拟节点的 key 相同,则进行 patch 操作
    patch(oldEndVNode, newEndVNode, container)
    oldEndVNode = prevChildren[--oldEndIdx] // 对旧节点结束索引和新节点结束索引都减 1,表示两个节点已经匹配完成
    newEndVNode = nextChildren[--newEndIdx]
  } else if (oldStartVNode.key === newEndVNode.key) { 
  // 如果旧节点开始位置虚拟节点的 key 和新节点结束位置虚拟节点的 key 相同,则进行 patch 操作
    patch(oldStartVNode, newEndVNode, container) // 这个操作是将旧节点开始位置虚拟节点移动到旧节点结束位置之后,并更新其对应的真实节点的位置
    container.insertBefore(
      oldStartVNode.el,
      oldEndVNode.el.nextSibling
    )
    oldStartVNode = prevChildren[++oldStartIdx] // 对旧节点开始索引和新节点结束索引都加 1,表示这两个节点已经匹配完成
    newEndVNode = nextChildren[--newEndIdx]
  } else if (oldEndVNode.key === newStartVNode.key) { 
  // 如果旧节点结束位置虚拟节点的 key 和新节点开始位置虚拟节点的 key 相同,则进行 patch 操作
    patch(oldEndVNode, newStartVNode, container) // 这个操作是将旧节点结束位置虚拟节点移动到旧节点开始位置之前,并更新其对应的真实节点的位置
    container.insertBefore(oldEndVNode.el, oldStartVNode.el)
    oldEndVNode = prevChildren[--oldEndIdx] // 对旧节点结束索引和新节点开始索引都减 1,表示两个节点已经匹配完成
    newStartVNode = nextChildren[++newStartIdx]
  } else {
    const idxInOld = prevChildren.findIndex( // 查找新节点开始位置虚拟节点在旧节点中的位置,并返回对应的索引
      node => node.key === newStartVNode.key
    )
    if (idxInOld >= 0) { // 如果返回的索引不小于 0,则说明新节点开始位置虚拟节点在旧节点中存在
      const vnodeToMove = prevChildren[idxInOld] // 根据返回的索引,找到旧节点中对应的虚拟节点,并将其移动到旧节点开始位置之前,并更新其对应的真实节点的位置
      patch(vnodeToMove, newStartVNode, container)
      prevChildren[idxInOld] = undefined // 将该虚拟节点在旧节点中的位置设为 undefined,表示其已经被移动过了
      container.insertBefore(vnodeToMove.el, oldStartVNode.el)
    } else { // 如果返回的索引小于 0,则说明新节点开始位置虚拟节点在旧节点中不存在
      // 新节点
      mount(newStartVNode, container, false, oldStartVNode.el) // 在旧节点开始位置之前插入新节点,并创建其对应的真实节点
    }
    newStartVNode = nextChildren[++newStartIdx] // 对新节点开始索引加 1,表示该节点已经处理完成
  }
}
if (oldEndIdx < oldStartIdx) { // 如果旧节点结束索引小于旧节点开始索引,则说明旧节点已经全部处理完成,此时需要将剩余的新节点添加到真实 DOM 中
  // 添加新节点
  for (let i = newStartIdx; i <= newEndIdx; i++) {
    mount(nextChildren[i], container, false, oldStartVNode.el)
  }
}

分析流程:

下面几种情况:

  • 旧节点的头指针和尾指针是否为空,重新指向位置。在后续的处理节点过程中,会使某些旧节点赋值为undefined
  • 头头和尾尾比较,进行patch函数调用,再使新旧头节点向下移动。
  • 头尾和尾头比较,进行patch函数调用,再将新节点进行调用。insertBefore方法,和拿到el.nextSibling属性,进行DOM的插入操作,再使指针分别移动。
  • 上面情况都不符合时,通过遍历去比对key值,能找到则进行patch操作,使该旧节点值为undefined,不能找到则进行mount方法的调用,进行挂载新节点,此时仅需要新节点头指针进行移动。

图解举例具体分析:

diff整体策略为:深度优先,同层比较

  1. 比较只会在同层级进行, 不会跨层级比较

万字总结大厂面试题—Vue篇

  1. 比较的过程中,循环从两边向中间收拢

万字总结大厂面试题—Vue篇

下面举个vue通过diff算法更新的例子:

新旧VNode节点如下图所示:

万字总结大厂面试题—Vue篇

第一次循环后,发现旧节点D与新节点D相同,直接复用旧节点D作为diff后的第一个真实节点,同时旧节点endIndex移动到C,新节点的 startIndex 移动到了 C,我们可以看到进行了container.insertBefore(oldEndVNode.el, oldStartVNode.el)函数的调用,进行了一次DOM的插入操作。 万字总结大厂面试题—Vue篇

第二次循环后,同样是旧节点的末尾和新节点的开头(都是 C)相同,同理,diff 后创建了 C 的真实节点插入到第一次创建的 D 节点后面。同时旧节点的 endIndex 移动到了 B,新节点的 startIndex 移动到了 E

万字总结大厂面试题—Vue篇

第三次循环中,发现E没有找到,这时候只能直接创建新的真实节点 E,插入到第二次创建的 C 节点之后。同时新节点的 startIndex 移动到了 A。旧节点的 startIndex 和 endIndex 都保持不动

万字总结大厂面试题—Vue篇

第四次循环中,发现了新旧节点的开头(都是 A)相同,于是 diff 后创建了 A 的真实节点,插入到前一次创建的 E 节点后面。同时旧节点的 startIndex 移动到了 B,新节点的startIndex 移动到了 B

万字总结大厂面试题—Vue篇

第五次循环中,情形同第四次循环一样,因此 diff 后创建了 B 真实节点 插入到前一次创建的 A 节点后面。同时旧节点的 startIndex移动到了 C,新节点的 startIndex 移动到了 F

万字总结大厂面试题—Vue篇

新节点的 startIndex 已经大于 endIndex 了,需要创建 newStartIdx 和 newEndIdx 之间的所有节点,也就是节点F,直接创建 F 节点对应的真实节点放到 B 节点后面

万字总结大厂面试题—Vue篇

什么时候需要diff?

当数据发生改变时,set方法会调用Dep.notify通知所有订阅者Watcher,订阅者就会调用patch给真实的DOM打补丁,更新相应的视图。这个设计模式是订阅发布者模式,有兴趣了解更详细的流程可以去看我的另一篇文章。你不会只知道MVVM的概念吧? - 掘金 (juejin.cn)

路由的两种模式(美团)

点击这里

vue3源码响应式实现(快手)

min版响应式原型

简单的小例子

Vue 的响应式是可以独立在其他平台使用的。比如你可以新建 test.js,使用下面的代码在 node 环境中使用 Vue 响应。以 reactive 为例,我们使用 reactive 包裹 JavaScript 对象之后,每一次对响应式对象 counter 的修改,都会执行 effect 内部注册的函数:

const {effect, reactive} = require('@vue/reactivity')

let dummy
const counter = reactive({ num1: 1, num2: 2 })
effect(() => {
  dummy = counter.num1 + counter.num2
  console.log(dummy)// 每次counter.num1修改都会打印日志
})
setInterval(()=>{
  counter.num1++
},1000)

执行 node test.js 之后,每次 count.value 修改之后都会执行effect 内部的函数。

流程图

我们先来看一下响应式整体的流程图,上面的代码中我们使用 reactive 把普通的 JavaScript 对象包裹成响应式数据了。所以,在 effect 中获取 counter.num1 和 counter.num2 的时候,就会触发 counter 的 get 拦截函数;get 函数,会把当前的 effect 函数注册到一个全局的依赖地图中去。这样 counter.num1 在修改的时候,就会触发 set 拦截函数,去依赖地图中找到注册的 effect 函数,然后执行。

万字总结大厂面试题—Vue篇

测试文件目录

    ├──reactivity
        ├── __test___
            ├──reactive.spec.js
            ├──ref.spec.js
        ├──baseHandler.js
        ├──effect.js
        ├──reactive.js
        ├──ref.js
        ├──shared.js

用jest构建测试用例

reactive.spec.js
import { reactive } from '../reactive'
import { effect } from '../effect'
describe('虚拟DOM', () => {
    it('测试', () => {
        expect(1 + 2).toBe(3)
    })
    it('reactive 基本使用', () => {
        // expect(1 + 2).toBe(3)
        let obj = {num: 0, num1: 1}
        const ret = reactive(obj)
        const ret2 = reactive(obj)
        let val
        effect(() => {
            val = ret.num // 运行 依赖收集
        })
        expect(val).toBe(0)
        ret.num++
        expect(val).toBe(1)
    })
    test('一个reactive 对象的属性在多个effect中', () => {
        const ret = reactive({num: 0})
        let val, val2
        effect(() => {
            val = ret.num
        })
        effect(() => {
            val2 = ret.num
        })
        expect(val).toBe(0)
        expect(val2).toBe(0)
        ret.num++
        expect(val).toBe(1)
        expect(val2).toBe(1)
    })
    test('shallowReactive基本使用', () => {
        const ret = shallowReactive({num: 0})
        let val
        effect(() => {
            val = ret.num
        })
        expect(val).toBe(0)
        ret.num++
        expect(val).toBe(1)
    })
    test('shallowReactive浅层响应式', () => {
        const ret = shallowReactive({
            info: {
                price: 129,
                type: 'f2e'
            }
        })
        let price
        effect(() => {
            price = ret.info.price
        })
        expect(price).toBe(129)
        ret.info.price++
        expect(price).toBe(129)
    })
    it('reactive 嵌套', () => {
        const ret = reactive({
            info: {
                price: 129,
                type: 'f2e'
            }
        })
        let price
        effect(() => {
            price = ret.info.price
        })
        expect(price).toBe(129)
        ret.info.price++
        expect(price).toBe(130)
    })
})
ref.spec.js
import { effect } from '../effect'
import { ref } from '../ref'

describe('ref测试', () => {
    it('ref 基本使用', () => {
        const r = ref(0)
        let val
        effect(() => {
            val = r.value
        })
        expect(val).toBe(0)
        r.value++
        expect(val).toBe(1)
    })
    it('should make nested properties reactive', () => {
        const a = ref({
          count: 1
        })
        let dummy
        effect(() => {
          dummy = a.value.count
        })
        expect(dummy).toBe(1)
        a.value.count = 2
        expect(dummy).toBe(2)
      })
})

reactive

import { mutableHandlers,shallowReactiveHandlers } from './baseHandlers'
export const reactiveMap = new WeakMap()
export const shallowReactiveMap = new WeakMap()
export const reactiveMap = new WeakMap() // 定义一个reactive对象地图

export function reactive(target) {
    return createReactiveObject(target, reactiveMap, mutableHandlers)
}

function createReactiveObject(target, proxyMap, proxyHandlers) {
    if (typeof target !== 'object') {
        console.warn('reactive ${target} 必须是一个对象')
        return target
    }
    //在reactive对象地图中查找是否有target,防止重复注册同一个reactive对象
    const existingProxy = proxyMap.get(target)
    if (existingProxy) {
        return existingProxy
    }

    // 通过Proxy 创建代理,不同的Map存储不同类型的reactive依赖关系
    const proxy = new Proxy(target, proxyHandlers)
    proxyMap.set(target, proxy) // 把从未注册过的reactive对象放入reactive地图中
    return proxy // 返回的是一个一个Proxy实例对象
}
// 浅层的代理
export function shallowReactive(target) {
    return createReactiveObject(
        target,
        shallowReactiveMap,
        shallowReactiveHandlers
    )
}

梳理思路:

  1. 此时通过reactive包裹的obj对象,返回的对象是一个Proxy实例对象。
  2. 定义一个reactive地图,防止重复注册同一个reactive对象。

此时const ret = reactive(obj) 的任务基本完成了。因为返回的是一个Proxy实例对象,可以拦截属性的读取(get)和设置(set)行为,如果对Proxy不太了解,可以参考Proxy - ECMAScript 6入门 (ruanyifeng.com),所以我们在这个Proxy实例上重写其handler参数。

baseHandlers

    import {
        reactive, 
        reactiveMap, 
        shallowReactiveMap 
    } from './reactive'
    import { track, trigger } from './effect'
    import { isObject } from './shared'

    const get = createGetter() 
    const set = craeteSeter()
    const has = () => {}
    const deleteProperty = () => {}

    const shallowReactiveGet = createGetter(true)

    function createGetter(shallow = false) { //默认是深层代理
        return function get(target, key, receiver) {
            const res = Reflect.get(target, key, receiver)
            track(target, "get", key)  // 收集依赖
            if (isObject(res)) { // 处理嵌套的情况
                return shallow ? res : reactive(res)
            }
            return res
        }
    }

    function craeteSeter() {
        return function set(target, key, value, receiver) {
            const result = Reflect.set(target, key, value, receiver)
            trigger(target, "set", key)
            return result
        }
    }
    //深层代理
    export const mutableHandlers = {
        get,
        set,
        has,
        deleteProperty
    }
    // 可以选择浅层代理
    export const shallowReactiveHandlers = {
        get: shallowReactiveGet,
        set,
        has,
        deleteProperty
    }

当触发了get和set拦截操作,我们再看看effect里是怎么处理track和trigger的。

依赖地图的格式,用代码描述如下:

    targetMap = {
     target: {
       key1: [回调函数1,回调函数2],
       key2: [回调函数3,回调函数4],
     }  ,
      target1: {
       key3: [回调函数5]
     }  

    }

effect

    let activeEffect = null
    const targetMap = new WeakMap()
    export function effect(fn, options = {}) { 
        // effect嵌套,通过队列管理
        const effectFn = () => {
            try {
                activeEffect = effectFn
                //fn执行的时候,内部读取响应式数据的时候,就能在get配置里读取到activeEffect
                return fn()
            } finally {
                activeEffect = null 
            }
        }
        // 第二个参数options,传递lazy和scheduler来控制函数执行的时机
        if (!options.lazy) {
            /没有配置lazy 直接执行
            effectFn() // proxy实例对象发起拦截操作
        }
        effectFn.scheduler = options.scheduler // 调度时机 watchEffect会用到
        return effectFn
    }

    export function track(target, type, key) {
        let depsMap = targetMap.get(target) 
        if (!depsMap) { // 防止重复注册
            targetMap.set(target, (depsMap = new Map()))
        }
        let deps = depsMap.get(key)
        if (!deps) { // 防止重复注册
            deps = new Set() 
        }
        if (!deps.has(activeEffect) && activeEffect) { // 防止重复注册
            deps.add(activeEffect)
        }
        depsMap.set(key, deps)
    }

    export function trigger(target, type, key) {
        const depsMap =  targetMap.get(target)
        if (!depsMap) {
            return
        }
        const deps = depsMap.get(key)
        if (!deps) {
            return
        }
        deps.forEach((effectFn) => { // 挨个执行effect函数
            effectFn()
        })
    }

梳理思路:

1.为什么定义注册全局地图依赖是使用WeakMap数据类型呢?

因为,它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用

2.effect 传递的函数,比如可以通过传递 lazy 和 scheduler 来控制函数执行的时机,默认是同步执行。scheduler 存在的意义就是我们可以手动控制函数执行的时机,方便应对一些性能优化的场景,比如数据在一次交互中可能会被修改很多次,我们不想每次修改都重新执行依次 effect 函数,而是合并最终的状态之后,最后统一修改一次。

3.track函数的作用就是把effect注册到依赖地图中,其中用Set数据类型存储effect,属于是一种性能优化,防止重复注册相同的effect。

4,trigger函数的作用就是把依赖地图中对应的effect函数数组全部执行一遍

ref

    export function ref(val) {
      if (isRef(val)) {
        return val
      }
      return new RefImpl(val)
    }
    export function isRef(val) {
      return !!(val && val.__isRef)
    }

    // ref就是利用面向对象的getter和setters进行track和trigget
    class RefImpl {
      constructor(val) {
        this.__isRef = true
        this._val = convert(val)
      }
      get value() {
        track(this, 'value')
        return this._val
      }

      set value(val) {
        if (val !== this._val) {
          this._val = convert(val)
          trigger(this, 'value')
        }
      }
    }

    // ref也可以支持复杂数据结构
    function convert(val) {
      return isObject(val) ? reactive(val) : val
    }

梳理思路:

1.ref 的执行逻辑要比 reactive 要简单一些,不需要使用 Proxy 代理语法,直接使用对象语法的 getter 和 setter 配置,监听 value 属性即可。

2.ref 函数实现的相对简单很多,只是利用面向对象的 getter 和 setter 拦截了 value 属性的读写,这也是为什么我们需要操作 ref 对象的 value 属性的原因。值得一提的是,ref 也可以包裹复杂的数据结构,内部会直接调用 reactive 来实现,但是需要去操作ref对象的value属性才能拿到复杂数据类型的值

3.此时我们对ref和reactive的理解又更加深刻了,当ref包裹的是一个原始类型的值时,例如:null、0等,并没有用到Proxy代理,直接用对象语法就可以完成监听。

总结

对vue响应式的理解

  1. 一句话来概括vue响应式就是,把JavaScript对象或者原始数据类型的值包裹成响应式对象,通过拦截获取和修改操作,相应触发track和trigger,实现依赖数据的自动化更新
  2. 因为在MVVM框架中,核心问题就是连接数据层和视图层,数据驱动来更新数据、视图,有了响应式,数据变化了,马上可以作出更新。vue响应式就显得极其重要。
  3. 开发人员只需要操作数据,关心业务,完全不用接触繁琐的DOM操作,从而大大提升开发效率,降低开发难度。

响应式模型实现的流程

  1. 讲述一下vue响应式模型关于reactive和ref
  2. 当要把一个js对象包裹成一个reactive对象,需要通过Proxy代理来实现,当读这个属性的值时,Proxy会拦截get操作,先执行track函数,把effect注册到依赖地图中。当修改这个属性的值时,拦截set操作,执行trigger函数,把关于改属性的effect函数挨个执行。
  3. 如果是要包裹成ref对象的话,对于原始数据类型的值来说,直接用对象语法的getter和setter配置,监听value属性,相应触发track和trigger函数,对于是引用数据类型来说,实际上就是用到了reactive的那一套流程,不过想要获取属性的值,需要.value才可以拿到值。

vue实现功能的同时所做的性能优化

  1. 看源码让我比较惊艳的地方就是它在实现功能的同时,去做的性能优化,可以收获很多。
  2. vue响应式原型模型来说,不是去直接存储effect函数,修改一次就马上执行,而是包装了一层对象对这个函数的执行实际进行管理,执行的方式是通过lazyscheduler 来控制函数执行的时机,
  3. 当使用lazy选项时,effect函数只有在被依赖项实际被访问时才会被计算,而不是在每次变化时都立即计算。
  4. 当使用scheduler,使用数组管理传递的执行任务,最后使用 Promise.resolve 只执行最后一次。
  5. 注册全局地图依赖是使用WeakMap数据类型Set数据类型存储effect函数,优化了性能。