说说你对的mvc,mvp,mvvm的理解

MVC
m-model,c-controller,v-view
View传送指令到controller
controller完成业务逻辑,通知model改变数据
model再将数据传给view发生相对应的变化
所有的通信都是单向的。
也有相关的,比如backbone
用户通过view(DOM事件)传送指令,通知model改变
model发生改变之后,通知view,这个时候的controller起到的作用比较小,类似于router.所以banckbone就只保留了router

MVP
M-model,V-view,P-presenter
MVP将controller那一层改成为presenter,并且所有的通信都改成双向的,View和model直接不直接通信,都是由presenter传递。

MVVM
m-model,V-view,VM-ViewModel
MVVM跟MVP的思想基本一致,唯一的区别的是mvvp采用的是双向绑定原理,将view的变化自动跟新到vm

说下vue双向绑定的原理

双向绑定主要是指:数据变化更新视图,视图变化更新数据。
vue双向绑定是基于数据劫持和发布订阅者实现的

  1. Vue创建了一个Observe类,通过Object.definePrototype的getter和settter对数据进行劫持,同时Observe类实现了依赖收集和通知更新的操作。在获取数据的时候,调用getter方法进行依赖收集,在修改数据的时候进行通知变化
  2. Vue里面还有一个Dep类,依赖收集器,每一个数据,都有一个对应的dep实例,该实例就是用来收集依赖的
  3. Vue 里面还有个Watcher类,该类的实例就是Dep需要收集的依赖。在初始化的 computed和用户自定义的watch的时候,会创建一个watcher实例,该watcher类会将自己本身挂在到Dep的静态属性target上面去,然后获取该数据的值,从而触发getter方法进行依赖收集
  4. 初始化完毕后,每个数据属性的依赖收集就完成了,队列里面包含computed watcher, watch watcher和render watcher三种watcher.
  5. 在数据发生改变的时候,会通过setter方法触发通知更新

    vue如何检测数组变化

  6. vue重写了会改变原始数组的方法(push,pop,shift,unshift,splice,sort,reverse)

  7. 当该数组方法被调用时,vue内部首先会调用数组的原始方法得到最终的结果
  8. 然后判断是否有新增的数组,如果有,就对新增的属性重新进行数据劫持操作
  9. 然后再调用dep实例通知变化

可以通过$set触发对象和数组的变化

Vue虚拟DOM

vue虚拟DOM是对dom节点一种数据描述
虚拟DOM避免了频繁操作真实DOM带来性能问题,开发者只需要关注业务逻辑,不用关注dom渲染问题
虚拟DOM作为中间层,提供了跨平台的功能
在首次渲染的时候,性能肯定是没有真实dom快,需要维护虚拟DOM

虚拟DOM的patch diff过程

虚拟DOM对比前后两个虚拟树,通过首位交叉对比,如果发现是一样的就复用,如果都不一样,则通过key对比
如果不加Key的话,vue就会复用之前的dom结构,该dom结构的状态就会包保存下来,导致一系列bug
而且key具有唯一性,能够快速被查找,节约性能

vue中的模板编译原理

如何将template转换成render函数。
正则匹配将template转换成ast语法树 -parseHtml
将静态语法做静态标记 — markUp
重新生成代码—codeGen
模板引擎是通过with,Function实现的

mixin

minx提供全局数据,数据不共享
缺点:如果mixin比较多,不知道数据源

vue组件之间的传值方式

vue $attr解决什么问题,provide、inject不能解决吗

v-if和v-show哪个优先级更高,如果同时出现,应该怎么优化得到更好的性能

v-for优先级高于v-if
同时出现的时候,一般需要把v-if写在template上,然后在内部进行v-for循环

为什么data是函数

Vue组件可能存在多个实例,如果是是对象形式,会导致共享同一个数据,实例对象就会被污染。
根组件不需要担心这个问题,因为根实例只有一个

vue中key的作用和工作原理

key可以唯一确定一个dom元素,在diff算法中可以更高效。其原理是在diff算法的patch过程中通过key可以精准的判断两个节点是否是同一个,从而避免的频繁的更新节点,使整个diff算法更高效,减少dom操作。diff算法的时间复杂度是o(n)
若不设置key,在列表渲染的时候会出现错误。
在使用相同元素进行动画过度的时候,可以通过设置不同的key触发过度内容。如果不设置key,只会更新内容

说说你对diff算法的理解

diff算法主要是为了比较两颗虚拟节点树,也是vue这歌框架决定需要用到diff算法的。通过新旧节点的对比,把差异更新到dom上。

vue2中为了降低watcher的粒度,内个组件只有一个渲染watcher,只有引入diff算法才能精准的 更新dom操作
vue diff是在改变data的时候会通过dep通知更新变化,执行watcher里面的uodate方法,这个时候就会进行对比新旧vnode
diff算法的整体过程是同层对比,使用的是深度 优先的算法。同层比较,判断两个节点是否是子节点或者文本节点做不同的操作。比较两组节点是diff算法的核心,也就是updateChilren。首先利用双指针多收尾四种对比尝试,如果没有找到相同节点,按照通过方式遍历查找。借助key可以精准判断是否是同一个节点,提高整个patch的速度

nextTick原理

nextTick有个timerFunc,首先会判断是否支持promise>mutationObsever>setImediate>settimeout

  1. nextTick是vue采用的异步事件队列。
  2. 原理是浏览器对任务分成宏任务和微任务。
  3. 对于修改data的操作是同步任务,所以会方法宏任务里面先执行
  4. 只有当前宏任务执行完毕之后,就会执行微任务队列,vue nexttick内部是优先使用微任务,然后再宏任务
  5. 所以当我们频繁改变数据的时候,其实是同步任务,当同步任务执行完毕之后就会执行Nexttick里面的微任务

调用Nexttick

  1. 把回调函数放到callbacks中等待被执行
  2. 将执行函数放到微任务或者宏任务中
  3. 事件循环执行到宏任务或者微任务的时候一次执行回调函数

computes/watch

计算属性是有缓存的,默认初始化的时候会创建computed watcher,这个watcher会有个lazy:true属性,默认watcher不会执行。给每个watcher创建一个getter(createComputeGetter),第一次取值的时候会把dirty变成false,再次取值的时候就会用缓存。当计算属性的依赖发生变化的时候就把dirty变成true.
createComputeGetter里面,如果dirty为true的话,就会执行取值操作,取值之前把这个watcher放到依赖中。
当响应式数据发生变化的时候会调用对用的update,该数据会有对应的compute watcher, watcher,render watch. 调用update的时候通过lazy为true,把计算属性的dirty设置为true,下次在读取到计算属性的时候就会执行更新值的操作。
计算属性是有缓存作用的,适合计算量比较大的数据
watch属性没有缓存,只要数据发生改变,就会触发watch的回调
计算属性实现原理:

  1. 每个computedWatcher实例上都挂在这lazy和dirty两个属性,lazy一直都是true,dirty初始化是true
  2. 第一次调用getter的时候需要计算computed的值,当计算完毕之后就会把dirty设为false
  3. 当computed依赖的数据发生改变的时候,通过判断该watcher的lazy如果为true,就把dirty设为true,说明该computed的依赖的数据发生了改变了,computed的需要重新计算

v-model

v-model默认情况下是input和value的语法糖,但是可以通过设置model,修改prop和event
会判断是组件还是check还是select还是input,还会解析一个directives

组件生命周期和运行顺序

初始化:先父和子
父beforeCreate 父created 父beforeMount 子beforeCreate 子created 子beforeMpunt
渲染:先子后父
子mounted 父mounted
更新:先父beforeUpdate 子beforeUpdate 子uppdate 父update
销毁:先父beforeDestroy 子beforeDestory 子destory 父destroy

组件之间的通信

$on/$emit 发布订阅者模式
provide/inject
$parent/$children
eventbus
vuex
$attr/$lisener(穿透)

  1. 从父组件传给子组件的属性,如果自组件没有prop接受,会自动添加到子组件的最外层标签上。这个时候可以通过inheritAttrs: false禁用继承,然后通过v-bind=’$attrs’将外部传入的非prop属性给希望的标签加上
  2. v-bind=’$listener’子组件继承父组件事件

.sync类似于v-model的作用 (this.$emit(“update:propName”,”Real Name”))

什么是作用域插槽什么是插槽

普通插槽
创建组件虚拟节点时,会将组件的儿子的虚拟节点保存起来。当初始化组件时,通过插槽属性将儿子进行分类{a:[vnode].b:[vnode]}
其实就是将父组件的生成的vnode直接复制过来
作用域插槽
作用域插槽会被渲染成一个function。作用域插槽是在子组件里面渲染的

插槽:就是子组件先挖个坑,留个位置,坑里面放什么东西由父组件决定
作用域插槽就是把子组件的内容挂在在slot上,父组件就可以通过slot-scope拿到子组件的数据

谈谈你对keep-alive的了解

keep-alive不会真正的渲染在dom节点上。keep-alive可以缓存组件。有exclude,include还有activied
keep-alive内部相当于一个插槽,内部会取出第一个组件做缓存
其原理keep-alive组件有个abstruct用来实现不添加到组件链中,因为在createComponent的时候如果是abstruct就直接跳过。组件内部也只有三个钩子函数,一个 created,destroyed,mounted。在created阶段生成cache,destroy阶段清除cache,在mounted阶段监听exclude和include的变化。在render的时候,将该组件keepAlive值设成true。在render 组件里面的组件时候,这个时候直接将vnode插入即可。
为啥其他钩子函数只执行一次,就是通过keepAlive参数跳过mount生命周期。
在每次patch完后就调用insertHook
LRU: 最近使用的,之后被使用的概率就大些

vue组件优化

不要把所有的数据都放到data中,会收集对应的watcher
object.freeze冻结数据
合理利用路由懒加载和异步组件
尽量采用runtime运行版
prerender-spa-plugin
key保证唯一性
数据持久化vuex-presist

Vue Router

原理
首先也是通过Vue.mixin的beforeMixin实现子组件也能获取_router
然后通过Vue.defineprototype注册$router,$route

vue-router hash和history

1.Vue.minxin beforeCreate注册this._router和_routerRoot
onhashchange #
history.pushState (导致页面不存在问题)

vue-router中导航守卫有哪些

说一下Vue3

支持Tree shaking
Composition API
更好的支持TS
暴露自定义渲染
更快
proxy替换object.definePrototype
可以坚挺到数组和对象的变化,但是proxy只会代理对象的第一层,并不会代理多层对象。这个时候vue3采用的是判断Reflect.get返回的是否为对象,如果是对象的再用reactive作代理,这样就实现了深度监听。
监听数组的时候可能触发多次getter/setter,如何防止多次书法

为什么更快
优化diff算法
添加节点属性
1=动态文本节点
2=动态style
hoistStatic静态提升(从render渲染方法提取出来保存起来,每次render的时候直接复用之前的)
更新的时候,只会创建动态。静态的会直接用之前的

事件侦听缓存cachehandler
对于事件的时候没有静态标记,只有静态标记的才会进行追踪。对于函数没有必要追踪

周边工具vite

Vuex

原理

  1. 通过Vue.mixin给组件全局注册一个beforeCreate钩子,赋值$state
  2. 内部通过new Vue把state做响应式处理
  3. 通过类Class里面的get state实现数据代理
  4. 模块module的概念,主要是把module格式解析成(ModuleCollection)
    1. {
    2. _raw: rootModule,
    3. _children: {
    4. a: {
    5. _raw:aModule,
    6. _children:{},
    7. state: aModule.state
    8. }
    9. },
    10. state: rootModule.state
    11. }
    实现方式
    1. register(path, rootModule) {
    2. const rawModule = {
    3. _raw: rootModule,
    4. _children: {},
    5. state: rootModule.state
    6. };
    7. if (!this.root) {
    8. this.root = rawModule;
    9. } else {
    10. const parentModule = path.slice(0, -1).reduce((root, current) => {
    11. return root._children[current];
    12. }, this.root);
    13. parentModule._children[path[path.length - 1]] = rawModule;
    14. }
    15. if (rawModule.modules) {
    16. Object.keys(rawModule.modules).forEach(moduleName => {
    17. this.register(path.concat(moduleName), rawModule.modules[moduleName])
    18. });
    19. }
    20. }

store是如何实现注入的

使用vuex只需要执行vue.use(Vuex),并在Vue的配置中传入一个store对象,那store是如何实现注入的?
Vue.use(Vuex)会执行Vuex里面的install方法,该方法会调用Vue.mixin注册一个beforeCreate方法。在beforeCreate方法内部,会判断$options是否有store,如果没有,就从$parent上面查找

state内部支持模块配置和模块嵌套,具体是如何实现的

在store的构造函数中,会把传入的state通过递归调用,根据匹配时的path进行匹配,最终转化为树结构。然后根据这个树结构注册module。所以在执行dispatch的时候,默认的拿到的当前module的state
在store构造方法中,有个makeLocalContext,所有的module都会有一个local context,根据配置时的path进行匹配

Vuex如何区分state是外部修改,还是通过mutation修改的

Vuex中修改state的唯一渠道是通过commit方法,其底层通过执行_withCommit设置_committing标志变量为true,然后才修改state,修改完毕之后将_committing还原为false。外部修改虽然能够直接修改state,但是没有修改_committing标志位,所哟只要watch一下state,state change的时候判断是否_committing值为true,即可判断修改的合法性

调试时的时空穿梭功能是如何实现的

devtoolPlugin中提供了此功能。因为dev模式下所有的state change都会被记录下来,时空穿梭功能其实就是将当前的state替换成某个时刻的state的状态,利用store.replaceState方法执行this._vm.state = state实现的。

Vuex module
解决当单一状态树太大,应用很复杂的时候,store对象很臃肿的问题。Vuex允许我们将store分割成module,每个模块拥有自己的state,mutation,action,getter