vue 父组件数据更新后子组件不变_vue 经典面试题+答案

以下面试题的答案都尽量扩展了知识面,并且不是那种一堆专业词的堆积,希望让新手也能看得懂。

1、vue 解决了什么问题

要回答这个问题,要先理解核心知识点:组件化

组件化就是一种代码设计理念。最开始的面向过程编程使一个文件的代码特别多,难以维护,所以借鉴了后端的面向对象编程,使一个本需要几千行代码的文件可以被拆分成几个几百行的文件。

一个组件就是一个类 new 出来的一个对象,是一个页面的一个部分,一个组件要负责这一部分的 CSS 展示,DOM 节点的设计,以及 JS 的逻辑。合理拆解组件可以提升代码的可读性以及可维护性,将组件间共性抽离出一个通用组件,让子组件去继承这个通用组件,可以少写很多代码,最小的组件颗粒度是一个元素。

组件化的演进 :

1)Angular 提出了通过面向数据编程,不用操作 DOM,但是刚出来时,性能上有瓶颈
2)React 引入了虚拟 DOM 机制,将 DOM 的比对换成了 JS 的比对,加了diff 算法,慢慢的性能的问题就解决了,前端开始大量使用 MVVM 和 MVC 这种框架了。
3)vue 是一个轻量级的 MVVM 模式的框架,vue 引入虚拟 DOM 的目的跟 React 不同,后者是为了解决性能问题,前者是为了让组件高度抽象化(虚拟 DOM 使渲染过程抽象化了)。其主要作用是使前端开发变得简单,它的设计模式让没有组件化思维的使用者也在用组件化作开发,所以写的代码不会太差。
PS:如果对前面两个框架不熟悉的话,最好不要在面试时提到它们,否则有被继续问的风险,只谈 vue 就好。
另外 Vue 的一个优势是速度快,当我们改变数据后,会引起页面重新渲染,引起的重排和重绘的代价是高昂的,有可能会破坏用户体验,让 UI 展示迟缓,重排会引起周围的 DOM 重新排列,这个范围可能是全局也可能是局部,重排的性能花销跟有多少 DOM 节点需要重新构建有关系,因此应该尽可能减少重排的次数,以及它波及的范围。
那么 Vue 将数据更新引起的 watcher 回调放入到 nextTick 中(是一个微任务,当执行栈为空时,就从微任务中一次性拿取所有任务),并且同一个 watcher 只会放入一次,也就是说在当前的事件循环中,无论一个响应式数据改变多少次,最终都只会渲染一次,nextTick 在下面有讲解。
要了解这段知识点要具备这些基础知识:浏览器渲染过程、JS 运行机制、异步任务、Vue 的双向绑定。确实所有知识都是从基础知识而来的,所以学框架最终就是对基础知识有更深入的理解。

2、MVVM 的理解

回答思路:先聊下 MVC,再聊下 MVVM 的定义,最后进行对比。

1bbd36de6579d3044e4a23ccc53570be.png
View 传送指令到 Controller
Controller 完成业务逻辑后,要求 Model 改变状态
Model 将新的数据发送到 View,用户得到反馈

所有通信都是单向的。MVC 接收用户指令,可以先通过 View 来接收,然后传递给 Controller,也可以直接通过 Controller 来接收指令。

df78c065c999d38d9413e5562f7eb639.png
ViewModel 和 View 之间是通过双向绑定来实现数据的变更
ViewModel 和 Model 之间是浏览器通过 ajax 跟服务器相互通信的过程

这两个通信是双向的,且 View 和 Model 之间没有通信。

MVVM 模式的出现是因为很多后端的代码放到了前端(大前端的到来),前端的代码可维护性、可扩展性以及安全性出现了问题,随着前端框架的演变才有了 MVVM 模式,MVVM 模式和 MVC 模式主要区别在于让开发者的注意力从对 DOM 的操作上,转移到对数据的管理上,即数据是什么,视图就展示什么,使前后端分离更容易,并大大提升了开发效率和代码的可维护性。

3、双向绑定的原理?数据劫持?

d41103b3e4090a70922836e3dfbd3747.png
反向是页面数据的变化映射到 data 中,通过 input 事件监听 input 框数据的改变,JS 得到通知再赋值给 data,只是 VM 框架使手动的过程自动化了。
正向是数据驱动页面,通过 Object.defineProperty() 这个核心 API,将所有数据变成响应式数据,当访问到一个响应式数据时,就会触发它的 getter 函数,收集依赖,当一个响应式数据变化时,就会触发 setter 函数,通知依赖使视图得到更新。

4、nextTick

nextTick 把要执行的任务推入到一个队列中,在下一个 tick 同步执行队列的所有任务,它是异步任务中的微任务。(关于 JS 的运行机制,这是基础知识,篇幅有限)

在 Vue 中,不是每一次数据的改变都会触发所有 wather 的回调,而是将这些回调推入到一个队列中,相同的 id 的 watcher 的回调不会被重复添加,然后在下一个 tick 中再执行这些回调,因此重新渲染是异步的。这么做的好处是:比如我们写了一个将某个响应式数据不断加 1 直到 1000 的代码块,那么视图只会重新渲染一次,即从 0 到 1000,而不是重新渲染 1000 次,这是一种有效的优化手段。

如果我们在更新了一个响应式数据后,需要同步拿到这个渲染后的 DOM 结果,那么就使用 $nextTick 这个方法,异步拿到这个结果。

使用方式有两个:回调方式和 Promise 方式

this.$nextTick(cb)
this.$nextTick().then(cb)

请注意第一种方式的使用,虽然它们都是异步执行的,但是如果你将第一种方式放在响应式数据更新的前面,那么你拿到的将是老的 DOM 结果。

因为 wather 的回调函数是在 nextTick 之后执行的,使用第一种方式 wather 的回调函数将和 this.$nextTick(cb) 中的 cb 同步执行,你将它放在响应式数据更新之前的话,会先执行这个 cb,所以拿到的是未被重新渲染的 DOM 结果,而如果是放在之后,你就能拿到被重新渲染的 DOM 结果。而第二种方式的 cb 跟 watcher 的回调函数是异步执行的,所以没有顺序问题。

5、生命周期

生命周期图示太长了,不希望一篇文章太长,就不上传这个图片了。

回答技巧:有哪些生命周期以及在每个生命周期的时机中我们能做的事情。

beforeCreate 钩子的执行时机是在 initState 函数之前,这个函数会初始化 props、data、methods、watch、computed 等属性,也就意味着,这个钩子是不可以访问到以上这些属性中的数据的;
created 钩子的执行时机是在 iniState 函数之后,因此可以访问到以上这些属性中的数据;
beforeMount 钩子的执行时机是在 DOM 挂载之前,执行顺序是先父后子;
mounted 钩子的执行时机是在 DOM 挂载之后,执行顺序是先子后父;
beforeUpdate 钩子的执行时机是在数据发生改变,还没有渲染之前;
updated 钩子的执行时机是数据发生改变,并且被渲染后;
beforeDestroy 钩子的执行时机是组件即将被销毁之前,也因此组件实例上属性的数据还可以被访问到;
destroyed 钩子的执行时机是组件被销毁之后。

还有两个钩子跟 keep-alive 抽象组件相关的:

activated 钩子的执行时机是在 mounted 钩子之后执行;
deactivated 钩子的执行时机是在页面退出时,由于会缓存,所以不会销毁组件。

6、虚拟 DOM 的原理

Virtual DOM 本质上是一个 JS 对象,这个对象是更轻量级对 DOM 的描述。

React 框架引入虚拟 DOM 是为了解决性能问题,在这之前,只要数据发生变化,就会整体刷新页面,还不能保持页面当前状态,所以想通过 diff 算法来判断哪些 DOM 的数据发生了变化,然后局部刷新需要更新的 DOM,而真实 DOM 的属性特别多,一个空的 div 就有 231 个属性,且大部分属性是不需要进行比较的,所以出现了一个轻量级的对真实 DOM 描述的 JS 对象,称为虚拟 DOM,与真实 DOM 一一对应,将新旧虚拟 DOM 进行 diff,然后生成变更,将变更应用于真实 DOM,最终生成最新的真实 DOM。这是将大量的 DOM 层面操作,转换成 JS 层面的操作,是很划算的。

但是虚拟 DOM 的 diff 算法是有成本的,因为需要作整体的同级元素的 diff(没有变动的 DOM 也需要 diff),而手动优化 DOM 操作,可以精确地操作我们需要变更的部分,因此,从性能上来说,手动优化 DOM 确实是最好的,但问题是如果每个地方都需要手动优化 DOM 操作的话,效率极低且维护性差,使用虚拟 DOM 是通过牺牲一点性能来换取更高的可维护性、可扩展性以及开发效率。

这就是为什么 vue 即便有数据劫持的情况下依然采用了虚拟 DOM 的原因,可以将组件高度抽象化,使 vue 组件可以跨平台,在后端运行(因为虚拟 DOM 是一个 JS 对象)。

7、如何实现一个自定义组件,不同组件之间如何通信的?

全局注册:Vue.component(tagName, options)

局部注册:在其他组件中的 components 属性中注册某个组件,就可以使用它了

import HelloWorld from './components/HelloWorld'

export default {
  components: {
    HelloWorld
  }
}

不同组件间通过 props 和事件中心来通信:

props 特性和非 props 特性:父组件通过属性方式将数据传递给子组件,子组件通过写明 props 来接收数据,如果没有在 props 中接收这个数据,那么这个数据称为非 props 特性(除了 class 和 style),所有非 props 特性存储在 $attrs 中,在子组件中声明 inheritAttrs 为 false,非 props 特性不赋予在子组件的根元素上,再配合 v-bind="$attrs" 绑定 $attrs 到非根元素上,这样非 props 特性就会赋予在非根元素上,如果是绑定到子组件的子组件,就可以达到跨组件通讯的目的了。

事件中心简单来说就是将所有事件放在一个对象中,通过一些 API 来管理这些事件。

我们知道子组件通过 $emit 来触发定义在父组件的自定义事件,并且可以以此传递数据给父组件。这个过程并不是往父组件派发事件,而是往子组件实例派发事件,因为在子组件初始化时,就将所有定义在父组件的绑定在子组件的自定义事件的回调函数,通过 listeners 参数传递给子组件,然后将所有的回调函数以事件名为 key,以对应的回调函数组成的数组为 value,通过 $on 这个 API 添加到 vm._event 对象中(vm 为子组件实例),然后通过 $emit 可以触发具体的回调函数,通过 $off 可以删除具体的回调函数,通过 $once 可以让回调函数只执行一次(先执行 $emit 再执行 $off)。

所以,我们可以通过 $listeners (等同于 listeners 参数)来达到跨组件的目的,在子组件 A 的子组件 B 中这样使用:v-on="$listeners" 可以将父组件的所有绑定在子组件 A 的自定义事件函数传给孙子组件 B,在初始化组件 B 时,就会将那些回调函数都添加到组件 B 实例的对象中,那么在组件 B 中执行 $emit 就可以触发对应的回调函数了。

也因此 bus 总线机制也就很好理解了,是在根实例的原型上生成一个 vue 实例,然后通过 $on 在这个实例中添加的回调函数,所有地方都可以通过 $emit 来触发这个回调函数,也达到了跨组件通信的目的(所有组件都可以通信了),因为它们都可以访问原型上的这个 vue 实例。

使用状态管理模式 vuex 实现数据共享,下面这道面试题来回答。

总结:不同组件之间的通信方式是多样的,因此应该统一规范通信方式,如此有利于代码的维护和开发效率,否则容易混淆一个数据的来源。

8、vuex 的理解

vuex 是什么?

当我们的项目中多个组件之间进行复杂的数据传值困难的时候,如果我们能把这些公用数据放在一个公共的存储空间去存储,一个组件改变了一个数据,其他的组件就能感知到。

将这个存储空间理解成一个 store 仓库,由几部分组成:

f0a2fb2ec723f15682a6e5dadf9a6ccd.png

所有的公用数据都存储在 state 之中,如果组件想调用数据,直接调用 state 就行,有的时候要改变 state 数据,不能直接用组件去改变 state 数据,必须走一个流程,如果我有异步操作,那么我把异步操作放在 actions 中,或者比较复杂的同步操作,比如批量的同步操作,也可以放在 actions 里面,组件先去调用 actions,actions 去调用 mutations,mutations 放的是同步的对 state 的修改,只有通过 mutations 才能改变公用数据的值。

但这个流程也不是绝对的,有的时候可以绕过 actions,让组件直接去调用 mutations 修改state 里面的数据,这块需要额外注意的是:当组件调用 actions 的时候,我们调用的是dispatch 方法,然后组件去调用 mutations 的时候,或者 actions 去调用 mutations 的时候,我们都需要调用的 commit 这个方法,其实就是一个单向数据的改变流程。

在使用 vuex 的时候,还可以借助 Devtools 这个开发者工具,帮助我们做代码的调试。

9、Proxy 相比于 defineProperty 的优势

Proxy 是 ES6 提供的一个对象代理,简单来说就是我们不直接对一个(私有)对象进行读写,而是通过这个对象代理对这个对象进行读写,通过对象代理可以对这个对象的数据作过滤保护,使得这个对象可以成为私有属性。

创建私有属性(只能通过内部提供的方法访问,不能直接访问),ES3 使用的是闭包,ES5 使用的是 defineProperty API,ES6 使用对象代理。

优势一:Proxy 可以直接监听对象本身,而 defineProperty 只能监听属性,只有通过递归调用才能监听对象;
优势二:当我们使用数组方法来操作数组后,Proxy 可以直接监听数组变化,而 defineProperty 是无法监听到的,常用八种数组方法 push, pop, shift, unshift, splice, sort, reverse 在 vue 中能监听得到,是因为 vue 源码中使用了 defineReactive 方法来渲染页面;
优势三:Proxy 有 13 中拦截方式,多于 defineProperty;
优势四:Proxy 返回一个新对象,可以只操作新对象,不会污染原对象,而 defineProperty 只能遍历对象属性直接修改。

而 defineProperty 的兼容性更好,vue 3.0 版本将会用 Proxy 代替 definProperty。

10、computed 和 watch 区别

计算属性是依赖其他数据计算出一个新的数据,其在初始化时就会执行我们定义在计算属性中的函数,或者对象中的 getter 函数,在首次执行这个函数过程中,依赖的数据就会收集这个计算属性的 watcher 依赖,从而在依赖的数据发生变换时,就会执行我们定义的函数了。
侦听器是监听一些数据的变换,一旦变化就执行一些业务逻辑,在其初始化时不会执行我们定义在 watch 中的回调函数,如果想初始化执行的话,声明 immediate 为 true;当在数据变换后,会异步执行回调函数(nextTick 之后执行),如果想同步执行的话,声明 sync 为 true;注意侦听器不能对对象进行深度观测,如果需要深度观测,声明 deep 为 true。
它们的共同点是都可以检测到数据的变换,且都有缓存机制。但是如果是处理复杂的业务逻辑,应该使用侦听器,如果是需要根据其他值生成一个新值,就应该使用计算属性。
computed: {
  fullName: {
    // getter
    get: function () {
      return this.firstName + ' ' + this.lastName
    },
    // setter
    set: function (newValue) {
      var names = newValue.split(' ')
      this.firstName = names[0]
      this.lastName = names[names.length - 1]
    }
  }
},
watch: {
    a: {
      handler(newVal) {
        console.log(newVal)
      },
      deep: true,
      immediate: true
    }
  }
})

11、vue-router(hash, HTML5 新增的 pushState)

单页应用,如何实现其路由功能---路由原理

vue-router 默认启用 hash 模式,如果只是 hash 的改变在任何时候都不会向后端发起请求,如果想要在一个页面中跳转到一个区域,有两种方法:设置一个锚点:使用 a 标签的 name 属性,二是使用 id 元素,可以在任何元素上使用。
核心原理是利用了 hash 的 onhashchange 事件,一旦 hash 发生改变就会触发这个事件
还可以在路由配置中,用 mode 配置项,值为 history,就可以改成 history 模式,实际上就是使用了浏览器的 history API
通过这种模式可以去掉 hash 的丑陋,但是也有一个问题,如果用户刷新或者手动输入 URL 按回车,那么就会向后端发起请求,如果后端没有匹配的路径,就会报 404 错误,所以后端要配置支持。
核心原理(跟使用何种模式无关):利用的的是 history.pushState 和 history.replaceState 方法,并且在后退和前进以及跳转时利用的是 popstate 事件

vue-router 如何做用户登录权限等

使用 meta 来检测一个目标页面是否需要登陆权限,然后向服务器发送一个带有 Cookie 字段的请求,询问浏览器端的 cookie 中的 sessionID 是否已过期(因为 session 是保存在服务器端的),如果过期就需要重新登陆,跳转到登陆页面,否则表示已是登陆状态,进入目标页面。

你在项目中怎么实现路由的嵌套

一个被渲染组件同样可以包含自己的<router-view>(一个出口)
要在嵌套的出口中渲染组件,需要在 VueRouter 中使用 children 配置
在 children 中的路由不需要以 / 开头,因为是嵌套的路由
除此之外就像和 routes 配置一样的路由配置数组

vue-router 有哪几种导航钩子

全局的:
全局前置守卫 beforeEach
全局解析守卫(在组件路由所有守卫之后) beforeResolve
全局后置钩子(没有 next 参数,因为不在导航守卫队列中,此时导航被确认) afterEach
router.beforeEach((to,from.next) => {})
单个路由独享(在路由配置上直接定义):beforeEnter
组件级:在路由组件内直接调用的守卫,像调用生命周期那样
beforeRouteEnter
这是唯一可以给 next() 传递回调函数作为参数的守卫(对于所有守卫而言的)
其他两个已经可以直接使用 this,所以不需要再传入回调
beforeRouteUpdate 在重复组件中调用这个守卫,所以可以访问 this
主要是因为在重用组件中使用了这个守卫,因此在这里请求数据可以在组件复用是更新数据
beforeRouteLeave 离开失活组件时执行的守卫,所以可以访问到失活组件的 this
当导航守卫的队列都清空时,表示导航被确认,然后调用全局的 afterEach 钩子,然后触发 DOM 更新,然后再会执行 beforeRouteEnter 中的 next 方法中的回调函数。
只要有 next 参数的,最后都要执行 next() 才能跳转到下一个守卫

$route 和 $router 的区别:

this.$router 是访问路由器
this.$route 是访问当前路由
如何使用 vue-router
1)import 进来,然后通过 Vue.use() 明确安装路由功能
2)定义路由组件,或者引入单文件组件
3)定义路由配置,配置是一个数组,元素是对象
4)创建一个 vue-router 实例,new vueRouter({})
5)创建 vue 根实例,将 router 作为配置参数传入
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值