【八股】Vue面试题

345 阅读13分钟

MVVM

视图模型双向绑定,是Model-View-ViewModel的缩写,也就是把MVC中的Controller演变成ViewModel。Model层代表数据模型,View代表UI组件,ViewModelViewModel层的桥梁,数据会绑定到viewModel层并自动将数据渲染到页面中,视图变化的时候会通知viewModel层更新数据。以前是操作DOM结构更新视图,现在是数据驱动视图

M:模型(Model) :data中的数据

V:视图(View) :模板代码

VM:视图模型(ViewModel):Vue实例

数据代理

data的初始化:data的数据会保存到vm._data中。在vm(vue实例)上设置一个代理,使vm.x可以访问到vm._data.x。然后通过observer类让data转换成响应式数据。

Object的监听

通过Object.defineProperty()将属性设置成getter/setter的形式实现了数据劫持,每次读取数据触发getter,修改数据触发setter

d98338fe29b894a267048a90c1b4c91.jpg

为每一个数据设置了Dep来收集依赖。

Watcher就是依赖,指哪些地方使用了数据,一个组件实例、计算属性、watch都是watcher的实例。

Data 通过Observer类 把数据转换成getter/setter的形式 实现数据劫持

外界通过Watcher读数据的时候,会触发getter将Watcher加入Dep中。

数据发生变化时,会触发setter,向Dep里的Watcher发通知。

Watcher接到通知后,通知外界,引发视图更新/调用回调等。

由于使用的是Object.defineProperty() 只能追踪属性是否被修改 无法追踪对象新增和删除属性,所以才有vm.$set()和vm.$delete()

Array的监听

创建了一个拦截器来覆盖数组原型,拦截器继承自Array.prototype,但新加了处理过的push pop shift unshift splice reverse sort方法,调用这些方法的时候,实际调用的是处理过的这些方法,可以监听到变化

对数组操作时只有使用这些方法才能监听 使用数组下标的形式不能监听到

单页应用和多页应用的区别

单页应用 single page application(SPA):指只有一个主页面的应用,只需要加载一次公共资源(js css等)。页面中按功能模块划分为不同组件,切换页面时只要切换相应组件即可,即局部刷新。

多页应用multi page application(MPA):多个页面,每次切换页面都要跳转到新的页面,重新加载资源,整个刷新。

image.png

单页应用的适用场景和不适用的场景

单页应用

优点:切换页面流畅

缺点:1.不利于SEO(搜索引擎优化)页面内容是用js渲染出来的 不利于检索 要用SSR(服务器端渲染)优化 2.首屏加载耗时多

适用于对流畅度体验高的应用

客户端渲染:使用 JavaScript 框架进行页面渲染

服务端渲染:服务端将HTML文本组装好,并返回给浏览器,这个HTML文本被浏览器解析之后,不需要经过 JavaScript 脚本的执行,即可直接构建出希望的 DOM 树并展示到页面中。

多页应用

优点:1.利于搜索引擎优化 2.首屏加载快

缺点:1.切换页面较慢 2.重复加载资源

适用于对SEO要求高的如官网首页

Vue和原生js的对比

vue基于MVVM模式,数据驱动修改视图,不用对dom进行直接操作。

vue按功能抽离出组件进行开发,使代码可以尽可能的复用,避免了大量重复代码,增加了可读性。

vue有各种页面之间传值的方法以及状态管理工具vuex,原生开发的页面传值方法很复杂。

vue是单页应用,且vue有虚拟dom和diff算法可以做到局部刷新,dom尽可能复用,提升了用户的切换页面的体验。

Vue2 Vue3的对比

1、响应式原理

Vue2对于对象类型,通过Object.defineProperty给data添加getter和setter实现数据拦截,每个data都有dep属性来收集依赖watcher对象。对于数组类型,通过重写数组原型对象上的七大方法来进行数据拦截。

Vue2的缺点是,只能监听到对对象属性的修改,如果新增或删除对象属性,必须要用Vue.set和Vue.delete方法。

Vue3通过给data创建代理proxy来拦截对数据属性的增删改,通过反射reflect完成对数据属性的操作。

Vue3实现了对数据的完全响应,对数据属性的新增和删除也可以响应到。

2、v-if和v-for的优先级

vue2里v-for的优先级高于v-if,vue3里v-if的优先级高于v-for。实际使用的时候不要把v-for和v-if放在同一节点,推荐用template包裹v-if,内部在用v-for遍历。

3、新增组合式API

Vue2中是选项式API,按data、watch、computed放在一起。而Vue3新增了组合式API,按照逻辑来写。

4、生命周期的改变

Vue3删除了beforeCreate和Created两个钩子,用hooks函数的形式引用,对Vue2中的生命周期名字进行了修改,比如mounted改为onMounted。。。

data为什么是函数

1.因为对象是引用数据类型 如果把data单纯的写成对象形式 组件复用时 所有data都指向同一个对象 修改时会相互影响

2.data写成函数形式 每次组件被复用的时候 都会返回一个新的对象 让每个组件维护自己的data

v-for和v-if的优先级

vue2中v-for高于v-if vue3中v-if高于v-for

不建议同时使用v-for和v-if 每次循环之后都要v-if判断 造成性能浪费 可以用template绑v-if然后在里面用v-for遍历

虚拟DOM和diff算法

虚拟DOM

直接操作真实DOM:把旧DOM全删了,然后根据状态生成新的DOM,造成性能浪费。

虚拟DOM:是一个树形结构的JS对象,对应真实DOM。

组件首次渲染时,将模板编译进render函数,调用render函数会生成虚拟dom,再生成对应的真实dom,并挂载到页面中的指定位置。

组件的数据更新时,setter触发dep.notify通知watcher进行patch,组件内通过diff算法比较新旧虚拟DOM,渲染不同的部分,其他部分直接复用。

(Vue1是细粒度侦测,每个节点是一个watcher,状态改变的时候直接通知该节点。Vue2是中等粒度,每个组件是一个watcher,状态改变时通知组件,然后组件内部用虚拟DOM和diff算法重新渲染。)

VNode:是一个类,可以实例成不同的VNode实例,对应真实的DOM节点。所有的VNode组成一个VDom树。

和真实DOM一样有文本节点 注释节点 元素节点等。如元素节点VNode包括tag(标签) data(attribute class等) children(子节点) context(当前组件的vue实例)属性

diff算法 (patch)

概念:对比新旧虚拟dom的差异,只对变化了的虚拟dom更新对应的真实dom,其他没变化的直接对真实dom复用,从而提高效率。

真实DOM操作远不如JavaScript的运算速度快 虚拟DOM实质就是js对象

blog.csdn.net/weixin_4270…

对比方式:深度优先,同层比较

主要的几个函数

  • render函数
  • patch函数
  • patchVnode函数
  • updateChildren函数

1、从根节点开始遍历,对节点执行patch(oldVnode,newVnode)函数,比较两个根节点是否是相同节点 (key相同且sel选择器相同)。如果不同,直接替换(删除旧的真实DOM,用newVnode生成新的真实DOM)

2、如果是相同节点,对两个根节点执行patchVnode(oldVnode, newVnode),比较属性、文本、子节点。

属性、文本直接用newVNode的去替换真实DOM的属性和文本。

都存在子节点时,执行updateChildren函数,去进一步比较他们的子节点。

image.png 3、新旧虚拟节点的子节点对比,执行updateChildren函数,(Vue2的双端diff算法)

双端diff算法的思想是对新旧虚拟DOM的两端进行对比,并做相应的移动操作

对于两组子节点,分别有四个指针,指向新前、新后、旧前、旧后,每一轮循环依次做四个对比

进入循环内的节点都是未处理过的节点

while(oldStart<=oldEnd && newStart<=newEnd)

1 oldStart === newStart找到,执行patchVnode进一步比较,新旧指针往中间移(oldStart++ newStart++)

2 oldEnd === newEnd找到,执行patchVnode进一步比较,新旧指针往中间移

3 oldEnd === newStart找到,执行patchVnode进一步比较,指针中间移,且把oldEnd对应的真实DOM移动到oldStart对应的(没处理过的DOM的开头)真实DOM的最前面,oldEnd对应旧节点置undifined标记已移动,指针往中间移

4 oldStart === newEnd 同3,比较+移动真实DOM

5 没有找到 则newStartVnode去oldChildren里找是否有可复用的节点(找相同的key)

如果找到的话,把该oldNode放到oldStart对应的真实DOM的最前面,对应旧节点置undifined标记已移动,newStart++

如果没有,说明是新节点,新增直接放到真实DOM的头部,newStart ++

出循环

oldStart>oldEnd && newStart <=newEnd 说明 [newStart,newEnd]之间的都是新节点,逐一挂载

oldStart<=oldEnd && newStart >newEnd 说明[oldStart,oldEnd]之间的都是旧节点,都删除

key的作用

key主要是方便复用的 用来判断新旧节点是否是相同节点(key相同且选择器相同)

1.如果在列表最前面新加节点 假设他们只有text文本不同 不设key的话

那么新旧dom进行对比的时候 他们都会被认为是相同的节点 然后进行patchNode又发现它们的文本不一样 要进行文本的删除修改 又对DOM做了操作 没有做到真正的复用

2.如果出现双端算法找不到的情况 又没有key 那这个新节点就会创建一个新的节点 不能做到复用

生命周期

参考:

blog.csdn.net/weixin_4270…

生命周期.png

生命周期.png 生命周期: 从一个vue实例从创建到销毁的过程,就是这个vue实例的生命周期。

生命周期钩子/生命周期函数: 在生命周期中相应阶段时自动调用的函数

整个生命周期过程:

一、初始化阶段:(new Vue()到created)

1.new Vue()实例化一个vue实例,初始化event事件、lifecycle生命周期

2.执行beforeCreate函数

3.初始化状态,顺序是props methods data computed watch ,完成数据代理

4.执行created函数

二、模板编译阶段(created到beforeMount)

5.把el的outerHTML或template模板编译成可执行的render函数

6.调用beforeMount函数

三、挂载阶段(beforeMount到Mount)

7.执行render函数,生成虚拟DOM,渲染为相应真实DOM,挂载到页面指定的位置

8.调用mounted函数。

四、更新阶段

9.数据发生变化,完成挂载后,当数据发生改变时,watcher会通知虚拟dom重新渲染

10.调用beforeUpdate函数 (此时vm的_isMounted属性为true,_isDestroyed为false)

11.执行render函数,生成新的虚拟DOM,用diff算法进行比较后,更新真实DOM。

12.调用updated函数

五、销毁阶段

13.调用beforeDestroy函数

14.销毁实例

15.调用destroyed函数

父子组件生命周期执行顺序

挂载阶段:父组件beforeCreate->父组件created->父组件beforeMount->子组件beforeCreate->子组件created-子组件beforeMount->子组件mounted-> -> 父组件 mounted

更新阶段:父组件 beforeUpdate -> 子组件 beforeUpdate -> 子组件 updated -> 父组件 updated

销毁阶段:父组件 beforeDestroy -> 子组件 beforeDestroy -> 子组件 destroyed -> 父组件 destroyed

Vuex

vuex是怎么实现的(vuex的本质)

vuex的state怎么判断是外部修改的还是内部修改的

vuex 中唯一更改状态的方法就是 mutation

底层修改state是通过设置_committing 标志变量为 true,然后才能修改 state,修改完毕还需要还原_committing 变量。外部修改虽然能够直接修改 state,但是并没有修改_committing 标志位,所以只要 watch 一下 state,state change 时判断是否_committing 值为 true,即可判断修改的合法性。

this.$store._committing

vm.$nextTick()

要获取到数据更新之后的最新dom时使用

数据更新后虚拟dom重新渲染的任务是异步的,当发生数据变化时,相应的Watcher会推入一个队列里,然后在该轮事件循环中,修改多次数据会合并成最新的一次,在下一轮事件循环中触发

$nextTick()接收一个回调,将回调包装成一个异步任务

vue2提供了几种包装方式包括promise.then mutationobserver(微任务)/ setimmediate settimeout(宏任务)

vue3直接是包装成promise.then

优先promise.then因为它是微任务 优先级更高 也就是nexttick返回一个promise juejin.cn/post/684490…

对dom的修改也是一个微任务,和nexttick属于同级,执行先后看推入队列的先后顺序,所以在修改数据之后写this.nextTick就可以获取最新的dom。

组件间通信

blog.csdn.net/qq_41370833…

数据在哪里 操作数据的方法就在哪里

props 父-子

单向数据流

父传子:props

自定义事件+ref 子-父

双向数据流

父里给子组件绑定自定义事件指定回调

1.@myEvent="callback"

2.this.$refs获取子组件后用.$on('myEvent',callback)

子里emit触发自定义事件并传参给父

this.$emit('myEvent',args)

解绑:this.$off('myEvent')

或者父传一个函数给子 子里传参

全局事件总线 任意

new Vue

在beforeCreate()里初始化的时候Vue.prototype.$bus = this

在要接受数据的地方绑事件和写回调

组件中通过this.$bus.$onthis.$bus.$offthis.$bus.$emit 使用 --使用方法类似组件的自定义事件

1.bus所有的组件都能看到 注意命名

2.注意在绑了事件的组件在destroy前解绑事件

vuex

全局状态管理

插槽

v2.cn.vuejs.org/v2/guide/co…

父-》子插入相应的html结构

1.匿名(默认)插槽

<!-- 父组件中-->
<child-component>
    会被插入子组件的内容
</child-component>
<!-- 子组件中-->
<slot></slot>

2.具名插槽 父组件通过template和v-slot 子组件通过name 插入不同的插槽

<!-- 父组件中-->
<child-component>
    <template v-slot:slotName1>
        slotName1
    <template/>
    <--简写方式-->
    <template #slotName1>
        slotName2
    <template/>
</child-component>
<!-- 子组件中-->
<slot name="slotName1></slot>
<slot name="slotName2></slot>

3.作用域插槽 把子组件的数据传到父组件中方便插槽使用

父组件中 <child-component>{child.prop} <child-component>是无效的 父组件的模板无法访问子组件的数据

使用作用域插槽 将子要传的数据作为属性绑定上去 父组件声明v-slot:slotName=""来接收

<!-- 父组件中 默认插槽-->
<child-component>
    <template v-slot:default="slotProps">
        {{slotProps.childProp}}
    <template/>
</child-component>

也可以直接解构赋值

<!-- 父组件中 默认插槽-->
<child-component>
    <template v-slot:default="{childProp}">
        {{childProp}}
    <template/>
</child-component>
<!-- 子组件中-->
<slot :childProp="childProp"></slot>

inject和provide-祖先和子孙组件之间传值

inject在data/props之前初始化,provide在data/props之后初始化,注入内容时,是将内容注入到当前vc的__provide中

inject配置key先在当前组件读取内容(__provide),读取不到则取它的父组件读取,找到最终内容保存到当前实例(vc)中,这样可以直接通过this读取到inject注入的内容。

// 父组件
export default {
  provide: {
    name: "父组件数据",
    say() {
      console.log("say say say");
    },
  },
  // 当需要用到this的时候需要使用函数形式
  provide() {
    return {
      todoLength: this.todos.length
    }
  },
}

// 子组件
<template>
  <div>
    <div>provide inject传递过来的数据: {{ name }}</div>
    <div>provide inject传递过来的方法<button @click="say">say</button></div>
  </div>
</template>

<script>
export default {
  inject: ['name', 'say'],
  },
}
</script>

父组件怎么获取子组件的方法和数据

用$refs获取子组件 然后直接访问

watch和computed

computed:计算属性

计算属性,一个数据依赖于其他数据

有缓存: 当依赖数据不变时,即使页面重新渲染,计算属性会立即返回之前的计算结果,而不必再次执行函数

只能处理同步数据:因为computed是通过return来返回计算属性的 return是同步执行的

watch:监听属性

监听一个数据,在它发生变化时执行回调

无缓存,页面重新渲染,值不变也会执行

可以写异步代码

执行顺序

1.如果watch没有设置immediate

初始化时 beforeMount -> computed -> mounted

修改watch和computed共同的依赖属性 watch->beforeUpdate->computed->updated

2.如果watch设置了immediate

初始化时 watch->created->beforeMount->computed->mounted

如果监听了计算属性的值,那么watch监听的计算属性会先执行

修改watch和computed共同的依赖属性 methods(如果触发了)->watch->beforeUpdate->computed->updated

编译

参考juejin.cn/post/716103…

编译阶段(created->beforeMount) 把template编译为可执行的render函数

将template模板字符串解析成AST抽象语法树(parse解析)

优化AST(optimize优化)

将优化后的AST生成代码字符串(generate代码生成)

转换成render函数

render:h=>h(app) h是createElement的简写,render函数接收第一个参数是createElement函数,生成虚拟dom VNode