万字总结大厂面试题—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首页加载慢怎么解决(商汤科技)
- 减小入口文件体积,常见的手段就是进行路由的懒加载,动态加载路由
- 合理利用静态资源本地缓存
- 按需加载UI组件
- 合理使用webpack等打包工具进行前端性能优化webpack初学者看这篇就够了 - 掘金 (juejin.cn)
组件通信的方式(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-router
和transition
的高级使用
// 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函数字符串
// 开始
<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子组件和父组件的执行顺序(快手)
加载渲染过程:
- 父组件 beforeCreate
- 父组件 created
- 父组件 beforeMount
- 子组件 beforeCreate
- 子组件 created
- 子组件 beforeMount
- 子组件 mounted
- 父组件 mounted
更新过程:
- 父组件 beforeUpdate
- 子组件 beforeUpdate
- 子组件 updated
- 父组件 updated
销毁过程:
- 父组件 beforeDestroy
- 子组件 beforeDestroy
- 子组件 destroyed
- 父组件 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的用法,你会不会发出以下疑问:
- 为什么要
store.commit('add')
才能触发事件执行呢? 可不可以进行直接调用mutation
函数进行操作呢? - 为什么不可以直接对
state
存储的状态进行修改,只能通过调用mutation
函数的方式修改呢? - 为什么存在异步调用的函数需要
store.dispatch('asyncAdd')
函数才能完成呢?可以直接调用store.commit('asyncAdd')
嘛?如果不可以,为什么呢? 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里引入了reactive
、inject
和computed
,并且对很大一部分的源码进行了省略,dispatch
和commit
远比这复杂多了,有兴趣去了解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()
函数,其构造函数就会接收一个包含 state
、mutations
、actions
和 getters
函数的对象 options
, 然后将它们保存到实例属性中,此时state
中的值都会转换为响应式对象,同时遍历所有的getters
函数,将其封装成computed
属性并保存到实例属性getters
中,而在main.js里调用了app.use(), install方法自动执行,将将当前 store 实例注册到 Vue.js 应用程序中,只需要调用useStore()
就可以拿到全局状态管理的store实例了,可以靠inject
和provide
实现全局共享。
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的双端比较算法:
// 当新的 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
整体策略为:深度优先,同层比较
- 比较只会在同层级进行, 不会跨层级比较
- 比较的过程中,循环从两边向中间收拢
下面举个vue
通过diff
算法更新的例子:
新旧VNode
节点如下图所示:
第一次循环后,发现旧节点D与新节点D相同,直接复用旧节点D作为diff
后的第一个真实节点,同时旧节点endIndex
移动到C,新节点的 startIndex
移动到了 C,我们可以看到进行了container.insertBefore(oldEndVNode.el, oldStartVNode.el)
函数的调用,进行了一次DOM的插入操作。
第二次循环后,同样是旧节点的末尾和新节点的开头(都是 C)相同,同理,diff
后创建了 C 的真实节点插入到第一次创建的 D 节点后面。同时旧节点的 endIndex
移动到了 B,新节点的 startIndex
移动到了 E
第三次循环中,发现E没有找到,这时候只能直接创建新的真实节点 E,插入到第二次创建的 C 节点之后。同时新节点的 startIndex
移动到了 A。旧节点的 startIndex
和 endIndex
都保持不动
第四次循环中,发现了新旧节点的开头(都是 A)相同,于是 diff
后创建了 A 的真实节点,插入到前一次创建的 E 节点后面。同时旧节点的 startIndex
移动到了 B,新节点的startIndex
移动到了 B
第五次循环中,情形同第四次循环一样,因此 diff
后创建了 B 真实节点 插入到前一次创建的 A 节点后面。同时旧节点的 startIndex
移动到了 C,新节点的 startIndex 移动到了 F
新节点的 startIndex
已经大于 endIndex
了,需要创建 newStartIdx
和 newEndIdx
之间的所有节点,也就是节点F,直接创建 F 节点对应的真实节点放到 B 节点后面
什么时候需要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 函数,然后执行。
测试文件目录
├──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
)
}
梳理思路:
- 此时通过reactive包裹的obj对象,返回的对象是一个Proxy实例对象。
- 定义一个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响应式的理解
- 一句话来概括vue响应式就是,把JavaScript对象或者原始数据类型的值包裹成响应式对象,通过拦截获取和修改操作,相应触发track和trigger,实现依赖数据的自动化更新
- 因为在MVVM框架中,核心问题就是连接数据层和视图层,数据驱动来更新数据、视图,有了响应式,数据变化了,马上可以作出更新。vue响应式就显得极其重要。
- 开发人员只需要操作数据,关心业务,完全不用接触繁琐的DOM操作,从而大大提升开发效率,降低开发难度。
响应式模型实现的流程
- 讲述一下vue响应式模型关于reactive和ref
- 当要把一个js对象包裹成一个reactive对象,需要通过Proxy代理来实现,当读这个属性的值时,
Proxy
会拦截get操作,先执行track函数,把effect注册到依赖地图中。当修改这个属性的值时,拦截set操作,执行trigger函数,把关于改属性的effect函数挨个执行。 - 如果是要包裹成ref对象的话,对于
原始数据类型
的值来说,直接用对象语法的getter和setter配置,监听value属性,相应触发track和trigger函数,对于是引用数据类型
来说,实际上就是用到了reactive的那一套流程,不过想要获取属性的值,需要.value才可以拿到值。
vue实现功能的同时所做的性能优化
- 看源码让我比较惊艳的地方就是它在实现功能的同时,去做的性能优化,可以收获很多。
- vue响应式原型模型来说,不是去直接存储effect函数,修改一次就马上执行,而是
包装了一层对象对这个函数的执行实际进行管理
,执行的方式是通过lazy
和scheduler
来控制函数执行的时机, - 当使用
lazy
选项时,effect函数只有在被依赖项实际被访问时才会被计算,而不是在每次变化时都立即计算。 - 当使用
scheduler
,使用数组管理传递的执行任务,最后使用 Promise.resolve 只执行最后一次。 - 注册全局地图依赖是使用
WeakMap数据类型
,Set数据类型
存储effect函数,优化了性能。
转载自:https://juejin.cn/post/7257441765976260665