前端篇:朋友面试之Vue框架,回去等通知吧(一)
前言
最近和朋友富贵聊起天,临近毕业,他开始找工作,去面了一家公司。本着对Vue框架稍有了解,面试官也不会深问的想法,就写了精通二字。没想到,大意了。
开场
富贵约的下午两点半的面试,提前二十分钟就到了,然后安***在电脑旁等待面试开始,顺便回忆之前看的资料。快到时间,一个穿着格子衫的男子打开视频,说了句“你好,准备好了吗?准备好我们就来开始面试了!”,富贵不失礼貌笑着回了句“准备好了”。
面试官:你先来个自我介绍吧
朋友:面试官你好,我叫牛富贵,来自门头沟学院,今天是来应聘前端开发的岗位。
正文
面试官:看你简历说精通Vue,那我们就来讨论Vue,先讲一下你对Vue的理解先
牛富贵:(怎么一上来就问Vue,不按套路出牌,不是该先问JavaScript基础吗?不过常规题,小问题)
牛富贵:Vue是一个构建用户界面的渐进式框架,典型的 MVVM 框架。只关心图层;不关心具体是如何实现的。
Vue的优点主要有:
- 轻量级框架:只关注视图层,是一个构建数据的视图集合,大小只有几十
kb
; - 简单易学:国人开发,中文文档,不存在语言障碍 ,易于理解和学习;
- 双向数据绑定:保留了
angular
的特点,在数据操作方面更为简单; - 组件化:保留了
react
的优点,实现了html
的封装和重用,在构建单页面应用方面有着独特的优势; - 视图,数据,结构分离:使数据的更改更为简单,不需要进行逻辑代码的修改,只需要操作数据就能完成相关操作;
- 虚拟DOM:
dom
操作是非常耗费性能的,不再使用原生的dom
操作节点,极大解放dom
操作,但具体操作的还是dom
不过是换了另一种方式; - 运行速度更快:相比较于
react
而言,同样是操作虚拟dom
,就性能而言,vue
存在很大的优势。
面试官:你刚刚提到Vue是渐进式框架,为什么说Vue是一个渐进式框架?
牛富贵:渐进式,通俗点讲就是,你想用啥你就用啥,咱也不强求你。你想用component就用,不用也行,你想用vuex就用,不用也可以。
面试官:看你刚刚还提到MVVM ,那你讲讲这个吧,它跟MVC框架有什么区别?
牛富贵:(很好,面试官被我带节奏了)
牛富贵:首先先讲一下MVC框架,我来画个图~
牛富贵:MVC 通过分离 Model、View 和 Controller 的方式来组织代码结构。其中 View 负责页面的显示逻辑,Model 负责存储页面的业务数据,以及对相应数据的操作。并且 View 和 Model 应用了观察者模式,当 Model 层发生改变的时候它会通知有关 View 层更新页面。Controller 层是 View 层和 Model 层的纽带,它主要负责用户与应用的响应操作,当用户与页面产生交互的时候,Controller 中的事件触发器就开始工作了,通过调用 Model 层,来完成对 Model 的修改,然后 Model 层再去通知 View 层更新。
牛富贵:接下来讲一下MVVM,继续画个图~
牛富贵:MVVM 由 Model、View、ViewModel 三部分构成Model 代表数据模型,也可以在 Model 中定义数据修改和业务逻辑;View 代表 UI 组件,它负责将数据模型转化成 UI 展现出来;ViewModel 是一个同步 View 和 Model 的对象;
对于 MVVM 来说,其实最重要的并不是通过双向绑定或者其他的方式将 View 与 ViewModel 绑定起来,而是通过 ViewModel 将视图中的状态和用户的行为分离出一个抽象,这才是 MVVM 的精髓。
在 MVVM 架构下,View 和 Model 之间并没有直接的联系,而是通过 ViewModel 进行交互,Model 和 ViewModel 之间的交互是双向的, 因此 View 数据的变化会同步到 Model 中,而 Model 数据的变化也会立即反应到 View 上。
ViewModel 通过双向数据绑定把 View 层和 Model 层连接了起来,而 View 和 Model 之间的同步工作完全是自动的,无需人为干涉,因此开发者只需关注业务逻辑,不需要手动操作 DOM,不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理。
面试官:那v-model 双向绑定的原理是什么?
牛富贵:v-model实际上是语法糖,下面就是语法糖的构造过程。
而v-model自定义指令下包裹的语法是input的value属性、input事件,整个过程是:
<input v-modle="inputV" /> // 等同于 <input :value="inputV" @input="inputV = $event.target.value"/>
- input属性绑定——inputV变量,也就是将值传给input;
- input事件,该事件在input的值inputV改变时触发,事件触发时给inputV重新赋值,所赋的值是
$event.target.value
,也就是当前触发input事件对象的dom的value值,也就是该input的值。
这就完成了v-model的数据双向绑定。
我们会发现elementUI的所有自定义组件都适用v-model这一语法糖,除了input之外,select、textarea也用到这一语法糖。
- text和textarea元素:v-model使用value属性设置初始值并绑定变量,input事件更新值;
- checkbox和radio元素:v-model使用checked属性设置初始值并绑定变量,change事件更新值;
- select元素:v-model使用value属性设置初始值并绑定变量,change事件更新值;
比如checkbox:
// 看似执行了v-model一个指令 <input type="checkbox" v-model="checkedNames"> // 实际上 <input type="checkbox" :value="checkedNames" @change="checkedNames = $event.target.value" />
面试官:v-model 可以被用在自定义组件上吗?如果可以,如何使用?
牛富贵:可以。v-model 实际上是一个语法糖,如:
<input v-model="searchText">
实际上相当于:
<input v-bind:value="searchText" v-on:input="searchText = $event.target.value" >
用在自定义组件上也是同理:
<custom-input v-model="searchText">
相当于:
<custom-input v-bind:value="searchText" v-on:input="searchText = $event" ></custom-input>
显然,custom-input 与父组件的交互如下:
父组件将
searchText
变量传入custom-input 组件,使用的 prop 名为value
;custom-input 组件向父组件传出名为
input
的事件,父组件将接收到的值赋值给searchText
;
所以,custom-input 组件的实现应该类似于这样:
Vue.component('custom-input', { props: ['value'], template: ` <input v-bind:value="value" v-on:input="$emit('input', $event.target.value)" > ` })
面试官:讲一下Vue 2.0 响应式数据的原理
牛富贵:Vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty()来劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。主要分为以下几个步骤:
- 需要 observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter 这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化;
- compile 解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图;
- Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁,主要做的事情是: ①在自身实例化时往属性订阅器(dep)里面添加自己 ②自身必须有一个update()方法 ③待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退;
- MVVM 作为数据绑定的入口,整合 Observer、Compile 和 Watcher 三者,通过 Observer 来监听自己的 model 数据变化,通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile 之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据 model 变更的双向绑定效果。
面试官:(有点东西这小伙)那Vue3.0 和 2.0 的响应式原理区别
牛富贵:Vue 在实例初始化时遍历 data 中的所有属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。这样当追踪数据发生变化时,setter 会被自动调用。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。
但是这样做有以下问题:
- 添加或删除对象的属性时,Vue 检测不到。因为添加或删除的对象没有在初始化进行响应式处理,只能通过
$set
来调用Object.defineProperty()
处理。 - 无法监控到数组下标和长度的变化。
Vue3 使用 Proxy 来监控数据的变化。Proxy 是 ES6 中提供的功能,其作用为:用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。相对于 Object.defineProperty()
,其有以下特点:
- Proxy 直接代理整个对象而非对象属性,这样只需做一层代理就可以监听同级结构下的所有属性变化,包括新增属性和删除属性。
- Proxy 可以监听数组的变化。
面试官:诶,你谈到Vue监控不了数组变化,那如果我需要监听的话有什么解决办法吗?
牛富贵:(松了一口气,还好我有看过)
牛富贵:当在项目中直接设置数组的某一项的值,或者直接设置对象的某个属性值,这个时候,你会发现页面并没有更新。这是因为Object.defineProperty()限制,监听不到变化。那么解决方式主要有几种方式:
- this.$set(你要改变的数组/对象,你要改变的位置/key,你要改成什么value)
this.$set(this.arr, 0, "OBKoro1"); // 改变数组this.$set(this.obj, "c", "OBKoro1"); // 改变对象
- 调用以下几个数组的方法
splice()、 push()、pop()、shift()、unshift()、sort()、reverse()
vue源码里缓存了array的原型链,然后重写了这几个方法,触发这几个方法的时候会observer数据,意思是使用这些方法不用再进行额外的操作,视图自动进行更新。 推荐使用splice方***比较好自定义,因为splice可以在数组的任何位置进行删除/添加操作
vm.$set
的实现原理是:
- 如果目标是数组,直接使用数组的 splice 方法触发相应式;
- 如果目标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法)
面试官:那你讲一下Vue.set()的原理吧
牛富贵:(这问得有点深了哇)
牛富贵:因为响应式数据 我们给对象和数组本身都增加了ob属性,代表的是 Observer 实例。当给对象新增不存在的属性 首先会把新的属性进行响应式跟踪 然后会触发对象ob的 dep 收集到的 watcher 去更新,当修改数组索引时我们调用数组本身的 splice 方法去更新数组。
相关代码如下:
export function set(target: Array | Object, key: any, val: any): any { // 如果是数组 调用我们重写的splice方法 (这样可以更新视图) if (Array.isArray(target) && isValidArrayIndex(key)) { target.length = Math.max(target.length, key); target.splice(key, 1, val); return val; } // 如果是对象本身的属性,则直接添加即可 if (key in target && !(key in Object.prototype)) { target[key] = val; return val; } const ob = (target: any).__ob__; // 如果不是响应式的也不需要将其定义成响应式属性 if (!ob) { target[key] = val; return val; } // 将属性定义成响应式的 defineReactive(ob.value, key, val); // 通知视图更新 ob.dep.notify(); return val; }
牛富贵:(这可别让我实现代码,我直接猝死)
面试官:理解 Vue 的单向数据流吗?
牛富贵:(松了一口气,还好还好)数据总是从父组件传到子组件,子组件没有权利修改父组件传过来的数据,只能请求父组件对原始数据进行修改。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。
注意:在子组件直接用 v-model 绑定父组件传过来的 prop 这样是不规范的写法 开发环境会报警告
如果实在要改变父组件的 prop 值 可以再 data 里面定义一个变量 并用 prop 的值初始化它 之后用$emit 通知父组件去修改。
面试官:你能具体讲一下组件之间的通信吗?
牛富贵:组件之间通信主要有以下几种
1、props / $emit
父组件通过props
向子组件传递数据,子组件通过$emit
和父组件通信
父组件向子组件传值
props
只能是父组件向子组件进行传值,props
使得父子组件之间形成了一个单向下行绑定。子组件的数据会随着父组件不断更新。props
可以显示定义一个或一个以上的数据,对于接收的数据,可以是各种数据类型,同样也可以传递一个函数。props
属性名规则:若在props
中使用驼峰形式,模板中需要使用短横线的形式
// 父组件 <template> <div id="father"> <son :msg="msgData" :fn="myFunction"></son> </div> </template> <script> import son from "./son.vue"; export default { name: father, data() { msgData: "父组件数据"; }, methods: { myFunction() { console.log("vue"); } }, components: { son } }; </script> // 子组件 <template> <div id="son"> <p>{{msg}}</p> <button @click="fn">按钮</button> </div> </template> <script> export default { name: "son", props: ["msg", "fn"] }; </script>
- 子组件向父组件传值
$emit
绑定一个自定义事件,当这个事件被执行的时就会将参数传递给父组件,而父组件通过v-on
监听并接收参数。
// 父组件 <template> <div class="section"> <com-article :articles="articleList" @onEmitIndex="onEmitIndex"></com-article> <p>{{currentIndex}}</p> </div> </template> <script> import comArticle from './test/article.vue' export default { name: 'comArticle', components: { comArticle }, data() { return { currentIndex: -1, articleList: ['红楼梦', '西游记', '三国演义'] } }, methods: { onEmitIndex(idx) { this.currentIndex = idx } } } </script> //子组件 <template> <div> <div v-for="(item, index) in articles" :key="index" @click="emitIndex(index)">{{item}}</div> </div> </template> <script> export default { props: ['articles'], methods: { emitIndex(index) { this.$emit('onEmitIndex', index) // 触发父组件的方法,并传递参数index } } } </script>
2、eventBus事件总线(emit / on
)
eventBus
事件总线适用于父子组件、非父子组件等之间的通信,使用步骤如下:
- 创建事件中心管理组件之间的通信
// event-bus.js import Vue from 'vue' export const EventBus = new Vue()
- 发送事件:假设有两个兄弟组件
firstCom
和secondCom
:
<template> <div> <first-com></first-com> <second-com></second-com> </div> </template> <script> import firstCom from './firstCom.vue' import secondCom from './secondCom.vue' export default { components: { firstCom, secondCom } } </script>
在firstCom
组件中发送事件:
<template> <div> <button @click="add">加法</button> </div> </template> <script> import {EventBus} from './event-bus.js' // 引入事件中心 export default { data(){ return{ num:0 } }, methods:{ add(){ EventBus.$emit('addition', { num:this.num++ }) } } } </script>
- 接收事件:在
secondCom
组件中发送事件:
<template> <div>求和: {{count}}</div> </template> <script> import { EventBus } from './event-bus.js' export default { data() { return { count: 0 } }, mounted() { EventBus.$on('addition', param => { this.count = this.count + param.num; }) } } </script>
在上述代码中,这就相当于将num
值存贮在了事件总线中,在其他组件中可以直接访问。事件总线就相当于一个桥梁,不用组件通过它来通信。
虽然看起来比较简单,但是这种方法也有不变之处,如果项目过大,使用这种方式进行通信,后期维护起来会很困难。
3、依赖注入(provide / inject)
这种方式就是Vue中的依赖注入,该方法用于父子组件之间的通信。当然这里所说的父子不一定是真正的父子,也可以是祖孙组件,在层数很深的情况下,可以使用这种方法来进行传值。就不用一层一层的传递了。
provide / inject
是Vue提供的两个钩子,和data
、methods
是同级的。并且provide
的书写形式和data
一样。
provide
钩子用来发送数据或方法inject
钩子用来接收数据或方法
在父组件中:
provide() { return { num: this.num }; }
在子组件中:
inject: ['num']
还可以这样写,这样写就可以访问父组件中的所有属性:
provide() { return { app: this }; } data() { return { num: 1 }; } inject: ['app'] console.log(this.app.num)
注意: 依赖注入所提供的属性是非响应式的。
4、ref / $refs
这种方式也是实现父子组件之间的通信。
ref
: 这个属性用在子组件上,它的引用就指向了子组件的实例。可以通过实例来访问组件的数据和方法。
在子组件中:
export default { data () { return { name: 'JavaScript' } }, methods: { sayHello () { console.log('hello') } } }
在父组件中:
<template> <child ref="child"></component-a> </template> <script> import child from './child.vue' export default { components: { child }, mounted () { console.log(this.$refs.child.name); // JavaScript this.$refs.child.sayHello(); // hello } } </script>
5、parent / children
- 使用parent可以让组件访问父组件的实例(访问的是上一级父组件的属性和方法)
- 使用children可以让组件访问子组件的实例,但是,
children
并不能保证顺序,并且访问的数据也不是响应式的。
在子组件中:
<template> <div> <span>{{message}}</span> <p>获取父组件的值为: {{parentVal}}</p> </div> </template> <script> export default { data() { return { message: 'Vue' } }, computed:{ parentVal(){ return this.$parent.msg; } } } </script>
在父组件中:
// 父组件中 <template> <div class="hello_world"> <div>{{msg}}</div> <child></child> <button @click="change">点击改变子组件值</button> </div> </template> <script> import child from './child.vue' export default { components: { child }, data() { return { msg: 'Welcome' } }, methods: { change() { // 获取到子组件 this.$children[0].message = 'JavaScript' } } } </script>
在上面的代码中,子组件获取到了父组件的parentVal
值,父组件改变了子组件中message
的值。 需要注意:
- 通过
parent
访问到的是上一级父组件的实例,可以使用root
来访问根组件的实例 - 在组件中使用
children
拿到的是所有的子组件的实例,它是一个数组,并且是无序的 - 在根组件
#app
上拿parent
得到的是new Vue()
的实例,在这实例上再拿parent
得到的是undefined
,而在最底层的子组件拿children
是个空数组 children
的值是数组,而parent
是个对象
6、attrs / listeners
考虑一种场景,如果A是B组件的父组件,B是C组件的父组件。如果想要组件A给组件C传递数据,这种隔代的数据,该使用哪种方式呢?
如果是用props/emit
来一级一级的传递,确实可以完成,但是比较复杂;如果使用事件总线,在多人开发或者项目较大的时候,维护起来很麻烦;如果使用Vuex,的确也可以,但是如果仅仅是传递数据,那可能就有点浪费了。
针对上述情况,Vue引入了attrs / listeners
,实现组件之间的跨代通信。
先来看一下inheritAttrs
,它的默认值true,继承所有的父组件属性除props
之外的所有属性;inheritAttrs:false
只继承class属性 。
attrs
:继承所有的父组件属性(除了prop传递的属性、class 和 style ),一般用在子组件的子元素上listeners
:该属性是一个对象,里面包含了作用在这个组件上的所有监听器,可以配合v-on="$listeners"
将所有的事件监听器指向这个组件的某个特定的子元素。(相当于子组件继承父组件的事件)
A组件(APP.vue
):
<template> <div id="app"> //此处监听了两个事件,可以在B组件或者C组件中直接触发 <child1 :p-child1="child1" :p-child2="child2" @test1="onTest1" @test2="onTest2"></child1> </div> </template> <script> import Child1 from './Child1.vue'; export default { components: { Child1 }, methods: { onTest1() { console.log('test1 running'); }, onTest2() { console.log('test2 running'); } } }; </script>
B组件(Child1.vue
):
<template> <div class="child-1"> <p>props: {{pChild1}}</p> <p>$attrs: {{$attrs}}</p> <child2 v-bind="$attrs" v-on="$listeners"></child2> </div> </template> <script> import Child2 from './Child2.vue'; export default { props: ['pChild1'], components: { Child2 }, inheritAttrs: false, mounted() { this.$emit('test1'); // 触发APP.vue中的test1方法 } }; </script>
C 组件 (Child2.vue
):
<template> <div class="child-2"> <p>props: {{pChild2}}</p> <p>$attrs: {{$attrs}}</p> </div> </template> <script> export default { props: ['pChild2'], inheritAttrs: false, mounted() { this.$emit('test2');// 触发APP.vue中的test2方法 } }; </script>
在上述代码中:
- C组件中能直接触发test的原因在于 B组件调用C组件时 使用 v-on 绑定了
$listeners
属性 - 在B组件中通过v-bind 绑定
$attrs
属性,C组件可以直接获取到A组件中传递下来的props(除了B组件中props声明的)
面试官:(不错不错)那讲一下Vue 的父子组件生命周期钩子函数执行顺序
牛富贵:(晒晒水啦)
牛富贵:
- 加载渲染过程
父 beforeCreate->父 created->父 beforeMount->子 beforeCreate->子 created->子 beforeMount->子 mounted->父 mounted
- 子组件更新过程
父 beforeUpdate->子 beforeUpdate->子 updated->父 updated
- 父组件更新过程
父 beforeUpdate->父 updated
- 销毁过程
父 beforeDestroy->子 beforeDestroy->子 destroyed->父 destroyed
面试官:谈一下对 vuex 的个人理解
牛富贵:vuex 是专门为 vue 提供的全局状态管理系统,用于多个组件中数据共享、数据缓存等。(无法持久化、内部核心原理是通过创造一个全局实例 new Vue)
主要包括以下几个模块:
- State:定义了应用状态的数据结构,可以在这里设置默认的初始状态。
- Getter:允许组件从 Store 中获取数据,mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性。
- Mutation:是唯一更改 store 中状态的方法,且必须是同步函数。
- Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作。
- Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中。、
面试官:Vuex 页面刷新数据丢失怎么解决?
牛富贵:需要做 vuex 数据持久化 一般使用本地存储的方案来保存数据 可以自己设计存储方案 也可以使用第三方插件
推荐使用 vuex-persist 插件,它就是为 Vuex 持久化存储而生的一个插件。不需要你手动存取 storage ,而是直接将状态保存至 cookie 或者 localStorage 中。
面试官:Vuex 中 action 通常是异步的,那么如何知道 action 什么时候结束呢?
牛富贵:在 action 函数中返回 Promise,然后再提交时候用 then 处理。
面试官:在模块中,getter 和 mutation 和 action 中怎么访问全局的 state 和 getter?
牛富贵:
- 在 getter 中可以通过第三个参数 rootState 访问到全局的 state,可以通过第四个参数 rootGetters 访问到全局的 getter。
- 在 mutation 中不可以访问全局的 satat 和 getter,只能访问到局部的 state。
- 在 action 中第一个参数 context 中的
context.rootState
访问到全局的 state,context.rootGetters
访问到全局的 getter。
面试官:Vuex 中 action 和 mutation 有什么区别?
牛富贵:
- action 提交的是 mutation,而不是直接变更状态。mutation 可以直接变更状态。
- action 可以包含任意异步操作。mutation 只能是同步操作。
- 提交方式不同,action 是用
this.store.dispatch('ACTION_NAME',data)
来提交。mutation 是用this.store.commit('SET_NUMBER',10)
来提交。 - 接收参数不同,mutation 第一个参数是 state,而 action 第一个参数是 context
面试官:为什么 Vuex 的 mutation 中不能做异步操作?
牛富贵:
- Vuex 中所有的状态更新的唯一途径都是 mutation,异步操作通过 Action 来提交 mutation 实现,这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。
- 每个 mutation 执行完成后都会对应到一个新的状态变更,这样 devtools 就可以打个快照存下来,然后就可以实现 time-travel 了。如果 mutation 支持异步操作,就没有办法知道状态是何时更新的,无法很好的进行状态的追踪,给调试带来困难。
面试官:不错,那有了解nextTick 原理及作用吗
牛富贵:Vue 的 nextTick 其本质是对 JavaScript 执行原理 EventLoop 的一种应用。nextTick 的核心是利用了如 Promise 、MutationObserver、setImmediate、setTimeout的原生 JavaScript 方法来模拟对应的微/宏任务的实现,本质是为了利用 JavaScript 的这些异步回调任务队列来实现 Vue 框架中自己的异步回调队列。nextTick 不仅是 Vue 内部的异步队列的调用方法,同时也允许开发者在实际项目中使用这个方法来满足实际应用中对 DOM 更新数据时机的后续逻辑处理。
nextTick 是典型的将底层 JavaScript 执行原理应用到具体案例中的示例,引入异步更新队列机制的原因∶
- 如果是同步更新,则多次对一个或多个属性赋值,会频繁触发 UI/DOM 的渲染,可以减少一些无用渲染
- 同时由于 VirtualDOM 的引入,每一次状态发生变化后,状态变化的信号会发送给组件,组件内部使用 VirtualDOM 进行计算得出需要更新的具体的 DOM 节点,然后对 DOM 进行更新操作,每次更新状态后的渲染过程需要更多的计算,而这种无用功也将浪费更多的性能,所以异步渲染变得更加至关重要
Vue采用了数据驱动视图的思想,但是在一些情况下,仍然需要操作DOM。有时候,可能遇到这样的情况,DOM1的数据发生了变化,而DOM2需要从DOM1中获取数据,那这时就会发现DOM2的视图并没有更新,这时就需要用到了nextTick
了。
由于Vue的DOM操作是异步的,所以,在上面的情况中,就要将DOM2获取数据的操作写在$nextTick
中。
this.$nextTick(() => { // 获取数据的操作...}) 复制代码
所以,在以下情况下,会用到nextTick:
- 在数据变化后执行的某个操作,而这个操作需要使用随数据变化而变化的DOM结构的时候,这个操作就需要方法在
nextTick()
的回调函数中。 - 在vue生命周期中,如果在created()钩子进行DOM操作,也一定要放在
nextTick()
的回调函数中。
因为在created()钩子函数中,页面的DOM还未渲染,这时候也没办法操作DOM,所以,此时如果想要操作DOM,必须将操作的代码放在nextTick()
的回调函数中。
面试官: Vue是如何收集依赖的?
牛富贵:在初始化 Vue 的每个组件时,会对组件的 data 进行初始化,就会将由普通对象变成响应式对象,在这个过程中便会进行依赖收集的相关逻辑,如下所示
function defieneReactive (obj, key, val){ const dep = new Dep(); ... Object.defineProperty(obj, key, { ... get: function reactiveGetter () { if(Dep.target){ dep.depend(); ... } return val } ... }) }
以上只保留了关键代码,主要就是 const dep = new Dep()
实例化一个 Dep 的实例,然后在 get 函数中通过 dep.depend()
进行依赖收集。
- Dep
Dep是整个依赖收集的核心,其关键代码如下:
class Dep { static target; subs; constructor () { ... this.subs = []; } addSub (sub) { this.subs.push(sub) } removeSub (sub) { remove(this.sub, sub) } depend () { if(Dep.target){ Dep.target.addDep(this) } } notify () { const subs = this.subds.slice(); for(let i = 0;i < subs.length; i++){ subs[i].update() } } }
Dep 是一个 class ,其中有一个关 键的静态属性 static,它指向了一个全局唯一 Watcher,保证了同一时间全局只有一个 watcher 被计算,另一个属性 subs 则是一个 Watcher 的数组,所以 Dep 实际上就是对 Watcher 的管理,再看看 Watcher 的相关代码∶
- Watcher
class Watcher { getter; ... constructor (vm, expression){ ... this.getter = expression; this.get(); } get () { pushTarget(this); value = this.getter.call(vm, vm) ... return value } addDep (dep){ ... dep.addSub(this) } ... } function pushTarget (_target) { Dep.target = _target }
Watcher 是一个 class,它定义了一些方法,其中和依赖收集相关的主要有 get、addDep 等。
- 过程
在实例化 Vue 时,依赖收集的相关过程如下∶ 初 始 化 状 态 initState , 这 中 间 便 会 通 过 defineReactive 将数据变成响应式对象,其中的 getter 部分便是用来依赖收集的。 初始化最终会走 mount 过程,其中会实例化 Watcher ,进入 Watcher 中,便会执行 this.get() 方法,
updateComponent = () => { vm._update(vm._render()) } new Watcher(vm, updateComponent)
get 方法中的 pushTarget 实际上就是把 Dep.target 赋值为当前的 watcher。
this.getter.call(vm,vm),这里的 getter 会执行 vm._render() 方法,在这个过程中便会触发数据对象的 getter。那么每个对象值的 getter 都持有一个 dep,在触发 getter 的时候会调用 dep.depend() 方法,也就会执行 Dep.target.addDep(this)。刚才 Dep.target 已经被赋值为 watcher,于是便会执行 addDep 方法,然后走到 dep.addSub() 方法,便将当前的 watcher 订阅到这个数据持有的 dep 的 subs 中,这个目的是为后续数据变化时候能通知到哪些 subs 做准备。所以在 vm._render() 过程中,会触发所有数据的 getter,这样便已经完成了一个依赖收集的过程。
面试官:那你知道Vue的computed和watch的......(此时面试官放在桌面的手机响了一下,他下意识看了看手机,停顿下),今天要不先到这吧,我会把你这情况给Hr说一下,一周内Hr会给你答复。
至此,牛富贵的面试暂时告一段落(我们下回继续)
#高频知识点汇总##学习路径#