1. 组件通讯总结
1-1. 父子组件通信
绝大部分
vue
本身提供的通信方式,都是父子组件通信
1-1-1. prop
最常见的组件通信方式之一,由父组件传递到子组件
1-1-2. event
最常见的组件通信方式之一,当子组件发生了某些事,可以通过event
通知父组件
1-1-3. style
和class
父组件可以向子组件传递style
和class
,它们会合并到子组件的根元素中
示例:
父组件
1 | <template> |
子组件
1 | <template> |
渲染结果:
1 | <div id="app"> |
1-1-4. attribute
如果父组件传递了一些属性到子组件,但子组件并没有声明这些属性,则它们称之为attribute
,这些属性会直接附着在子组件的根元素上
不包括
style
和class
,它们会被特殊处理
示例:
父组件
1 | <template> |
子组件
1 | <template> |
渲染结果:
1 | <div id="app"> |
子组件可以通过inheritAttrs: false
配置,禁止将attribute
附着在子组件的根元素上,但不影响通过$attrs
获取
1-1-5. natvie
修饰符
在注册事件时,父组件可以使用native
修饰符,将事件注册到子组件的根元素上
示例:
父组件
1 | <template> |
子组件
1 | <template> |
渲染结果
1 | <div id="app"> |
1-1-6. $listeners
子组件可以通过$listeners
获取父组件传递过来的所有事件处理函数
1-1-7. v-model
后续章节讲解
1-1-8 sync
修饰符
和v-model
的作用类似,用于双向绑定,不同点在于v-model
只能针对一个数据进行双向绑定,而sync
修饰符没有限制
示例
子组件
1 | <template> |
父组件
1 | <template> |
1-1-9. $parent
和$children
在组件内部,可以通过$parent
和$children
属性,分别得到当前组件的父组件和子组件实例
1-1-10. $slots
和$scopedSlots
后续章节讲解
1-1-11. ref
父组件可以通过ref
获取到子组件的实例
1-2. 跨组件通信
1-2-1. Provide
和Inject
示例
1 | // 父级组件提供 'foo' |
详见:https://v2.cn.vuejs.org/v2/api/#provide-inject
1-2-2. router
如果一个组件改变了地址栏,所有监听地址栏的组件都会做出相应反应
最常见的场景就是通过点击router-link
组件改变了地址,router-view
组件就渲染其他内容
1-2-3. vuex
适用于大型项目的数据仓库
1-2-4. store
模式
适用于中小型项目的数据仓库
1 | // store.js |
1-2-5. eventbus
组件通知事件总线发生了某件事,事件总线通知其他监听该事件的所有组件运行某个函数
2. 虚拟DOM详解
面试题:请你阐述一下对vue虚拟dom的理解
2-1. 什么是虚拟dom?
虚拟dom本质上就是一个普通的JS对象,用于描述视图的界面结构
在vue中,每个组件都有一个render
函数,每个render
函数都会返回一个虚拟dom树,这也就意味着每个组件都对应一棵虚拟DOM树
2-2. 为什么需要虚拟dom?
在vue
中,渲染视图会调用render
函数,这种渲染不仅发生在组件创建时,同时发生在视图依赖的数据更新时。如果在渲染时,直接使用真实DOM
,由于真实DOM
的创建、更新、插入等操作会带来大量的性能损耗,从而就会极大的降低渲染效率。
因此,vue
在渲染时,使用虚拟dom来替代真实dom,主要为解决渲染效率的问题。
2-3. 虚拟dom是如何转换为真实dom的?
在一个组件实例首次被渲染时,它先生成虚拟dom树,然后根据虚拟dom树创建真实dom,并把真实dom挂载到页面中合适的位置,此时,每个虚拟dom便会对应一个真实的dom。
如果一个组件受响应式数据变化的影响,需要重新渲染时,它仍然会重新调用render函数,创建出一个新的虚拟dom树,用新树和旧树对比,通过对比,vue会找到最小更新量,然后更新必要的虚拟dom节点,最后,这些更新过的虚拟节点,会去修改它们对应的真实dom
这样一来,就保证了对真实dom达到最小的改动。
2-4. 模板和虚拟dom的关系
vue框架中有一个compile
模块,它主要负责将模板转换为render
函数,而render
函数调用后将得到虚拟dom。
编译的过程分两步:
- 将模板字符串转换成为
AST
(抽象语法树) - 将
AST
转换为render
函数
如果使用传统的引入方式,则编译时间发生在组件第一次加载时,这称之为运行时编译。
如果是在vue-cli
的默认配置下,编译发生在打包时,这称之为模板预编译。
编译是一个极其耗费性能的操作,预编译可以有效的提高运行时的性能,而且,由于运行的时候已不需要编译,vue-cli
在打包时会排除掉vue
中的compile
模块,以减少打包体积
模板的存在,仅仅是为了让开发人员更加方便的书写界面代码
注意:vue最终运行的时候,最终需要的是render函数,而不是模板,因此,模板中的各种语法,在虚拟dom中都是不存在的,它们都会变成虚拟dom的配置
3. v-model
面试题:请阐述一下
v-model
的原理
v-model
即可以作用于表单元素,又可作用于自定义组件,无论是哪一种情况,它都是一个语法糖,最终会生成一个属性和一个事件
当其作用于表单元素时,vue
会根据作用的表单元素类型而生成合适的属性和事件。例如,作用于普通文本框的时候,它会生成value
属性和input
事件,而当其作用于单选框或多选框时,它会生成checked
属性和change
事件。
v-model
也可作用于自定义组件,当其作用于自定义组件时,默认情况下,它会生成一个value
属性和input
事件。
1 | <Comp v-model="data" /> |
开发者可以通过组件的model
配置来改变生成的属性和事件
1 | // Comp |
1 | <Comp v-model="data" /> |
4. 数据响应原理
面试题:请阐述
vue2
响应式原理
vue官方阐述:https://v2.cn.vuejs.org/v2/guide/reactivity.html
响应式数据的最终目标,是当对象本身或对象属性发生变化时,将会运行一些函数,最常见的就是render函数。
在具体实现上,vue用到了几个核心部件:
- Observer
- Dep
- Watcher
- Scheduler
4-1. Observer
Observer要实现的目标非常简单,就是把一个普通的对象转换为响应式的对象
为了实现这一点,Observer把对象的每个属性通过Object.defineProperty
转换为带有getter
和setter
的属性,这样一来,当访问或设置属性时,vue
就有机会做一些别的事情。
Observer是vue内部的构造器,我们可以通过Vue提供的静态方法Vue.observable( object )
间接的使用该功能。
在组件生命周期中,这件事发生在beforeCreate
之后,created
之前。
具体实现上,它会递归遍历对象的所有属性,以完成深度的属性转换。
由于遍历时只能遍历到对象的当前属性,因此无法监测到将来动态增加或删除的属性,因此vue
提供了$set
和$delete
两个实例方法,让开发者通过这两个实例方法对已有响应式对象添加或删除属性。
对于数组,vue
会更改它的隐式原型,之所以这样做,是因为vue需要监听那些可能改变数组内容的方法
总之,Observer的目标,就是要让一个对象,它属性的读取、赋值,内部数组的变化都要能够被vue感知到。
4-2. Dep
这里有两个问题没解决,就是读取属性时要做什么事,而属性变化时要做什么事,这个问题需要依靠Dep来解决。
Dep的含义是Dependency
,表示依赖的意思。
Vue
会为响应式对象中的每个属性、对象本身、数组本身创建一个Dep
实例,每个Dep
实例都有能力做以下两件事:
- 记录依赖:是谁在用我
- 派发更新:我变了,我要通知那些用到我的人
当读取响应式对象的某个属性时,它会进行依赖收集:有人用到了我
当改变某个属性时,它会派发更新:那些用我的人听好了,我变了
4-3. Watcher
这里又出现一个问题,就是Dep如何知道是谁在用我?
要解决这个问题,需要依靠另一个东西,就是Watcher。
当某个函数执行的过程中,用到了响应式数据,响应式数据是无法知道是哪个函数在用自己的
因此,vue通过一种巧妙的办法来解决这个问题
我们不要直接执行函数,而是把函数交给一个叫做watcher的东西去执行,watcher是一个对象,每个这样的函数执行时都应该创建一个watcher,通过watcher去执行
watcher会设置一个全局变量,让全局变量记录当前负责执行的watcher等于自己,然后再去执行函数,在函数的执行过程中,如果发生了依赖记录dep.depend()
,那么Dep
就会把这个全局变量记录下来,表示:有一个watcher用到了我这个属性
当Dep进行派发更新时,它会通知之前记录的所有watcher:我变了
每一个vue
组件实例,都至少对应一个watcher
,该watcher
中记录了该组件的render
函数。
watcher
首先会把render
函数运行一次以收集依赖,于是那些在render中用到的响应式数据就会记录这个watcher。
当数据变化时,dep就会通知该watcher,而watcher将重新运行render函数,从而让界面重新渲染同时重新记录当前的依赖。
4-4. Scheduler
现在还剩下最后一个问题,就是Dep通知watcher之后,如果watcher执行重运行对应的函数,就有可能导致函数频繁运行,从而导致效率低下
试想,如果一个交给watcher的函数,它里面用到了属性a、b、c、d,那么a、b、c、d属性都会记录依赖,于是下面的代码将触发4次更新:
1 | state.a = "new data"; |
这样显然是不合适的,因此,watcher收到派发更新的通知后,实际上不是立即执行对应函数,而是把自己交给一个叫调度器的东西
调度器维护一个执行队列,该队列同一个watcher仅会存在一次,队列中的watcher不是立即执行,它会通过一个叫做nextTick
的工具方法,把这些需要执行的watcher放入到事件循环的微队列中,nextTick的具体做法是通过Promise
完成的
nextTick 通过
this.$nextTick
暴露给开发者nextTick 的具体处理方式见:https://v2.cn.vuejs.org/v2/guide/reactivity.html#%E5%BC%82%E6%AD%A5%E6%9B%B4%E6%96%B0%E9%98%9F%E5%88%97
也就是说,当响应式数据变化时,render
函数的执行是异步的,并且在微队列中
4-5. 总体流程
5. diff
面试题:请阐述vue的diff算法
参考回答:
当组件创建和更新时,vue均会执行内部的update函数,该函数使用render函数生成的虚拟dom树,将新旧两树进行对比,找到差异点,最终更新到真实dom
对比差异的过程叫diff,vue在内部通过一个叫patch的函数完成该过程
在对比时,vue采用深度优先、同层比较的方式进行比对。
在判断两个节点是否相同时,vue是通过虚拟节点的key和tag来进行判断的
具体来说,首先对根节点进行对比,如果相同则将旧节点关联的真实dom的引用挂到新节点上,然后根据需要更新属性到真实dom,然后再对比其子节点数组;如果不相同,则按照新节点的信息递归创建所有真实dom,同时挂到对应虚拟节点上,然后移除掉旧的dom。
在对比其子节点数组时,vue对每个子节点数组使用了两个指针,分别指向头尾,然后不断向中间靠拢来进行对比,这样做的目的是尽量复用真实dom,尽量少的销毁和创建真实dom。如果发现相同,则进入和根节点一样的对比流程,如果发现不同,则移动真实dom到合适的位置。
这样一直递归的遍历下去,直到整棵树完成对比。
5-1. diff
的时机
当组件创建时,以及依赖的属性或数据变化时,会运行一个函数,该函数会做两件事:
- 运行
_render
生成一棵新的虚拟dom树(vnode tree) - 运行
_update
,传入虚拟dom树的根节点,对新旧两棵树进行对比,最终完成对真实dom的更新
核心代码如下:
1 | // vue构造函数 |
diff
就发生在_update
函数的运行过程中
5-2. _update
函数在干什么
_update
函数接收到一个vnode
参数,这就是新生成的虚拟dom树
同时,_update
函数通过当前组件的_vnode
属性,拿到旧的虚拟dom树
_update
函数首先会给组件的_vnode
属性重新赋值,让它指向新树
然后会判断旧树是否存在:
不存在:说明这是第一次加载组件,于是通过内部的
patch
函数,直接遍历新树,为每个节点生成真实DOM,挂载到每个节点的elm
属性上1
2
3
4//patch 方法中如果不存在旧树,则传输真是dom
if (!oldVnode) {
this.__patch__(this.$el, vnode);
}存在:说明之前已经渲染过该组件,于是通过内部的
patch
函数,对新旧两棵树进行对比,以达到下面两个目标:完成对所有真实dom的最小化处理
让新树的节点对应合适的真实dom
5-3. patch
函数的对比流程
术语解释:
- 「相同」:是指两个虚拟节点的标签类型、
key
值均相同,但input
元素还要看type
属性 - 「新建元素」:是指根据一个虚拟节点提供的信息,创建一个真实dom元素,同时挂载到虚拟节点的
elm
属性上 - 「销毁元素」:是指:
vnode.elm.remove()
- 「更新」:是指对两个虚拟节点进行对比更新,它仅发生在两个虚拟节点「相同」的情况下。具体过程稍后描述。
- 「对比子节点」:是指对两个虚拟节点的子节点进行对比,具体过程稍后描述
详细流程:
根节点比较
patch
函数首先对根节点进行比较如果两个节点:
「相同」,进 「更新」流程
- 将旧节点的真实dom赋值到新节点:
newVnode.elm = oldVnode.elm
- 对比新节点和旧节点的属性,有变化的更新到真实dom中
- 当前两个节点处理完毕,开始「对比子节点」
- 将旧节点的真实dom赋值到新节点:
不「相同」
- 新节点递归「新建元素」
- 旧节点「销毁元素」
「对比子节点」
在「对比子节点」时,vue一切的出发点,都是为了:
- 尽量啥也别做
- 不行的话,尽量仅改动元素属性
- 还不行的话,尽量移动元素,而不是删除和创建元素
- 还不行的话,删除和创建元素
代码示例:
1 | /** |
6. 生命周期详解
面试题:
new Vue
之后,发生了什么?数据改变后,又发生了什么?
创建vue实例和创建组件的流程基本一致
首先做一些初始化的操作,主要是设置一些私有属性到实例中
运行生命周期钩子函数
beforeCreate
进入注入流程:处理属性、computed、methods、data、provide、inject,最后使用代理模式将它们挂载到实例中
运行生命周期钩子函数
created
生成
render
函数:如果有配置,直接使用配置的render
,如果没有,使用运行时编译器,把模板编译为render
运行生命周期钩子函数
beforeMount
创建一个
Watcher
,传入一个函数updateComponent
,该函数会运行render
,把得到的vnode
再传入_update
函数执行。在执行
render
函数的过程中,会收集所有依赖,将来依赖变化时会重新运行updateComponent
函数在执行
_update
函数的过程中,触发patch
函数,由于目前没有旧树,因此直接为当前的虚拟dom树的每一个普通节点生成elm属性,即真实dom。如果遇到创建一个组件的vnode,则会进入组件实例化流程,该流程和创建vue实例流程基本相同,最终会把创建好的组件实例挂载vnode的
componentInstance
属性中,以便复用。运行生命周期钩子函数
mounted
重渲染?
数据变化后,所有依赖该数据的
Watcher
均会重新运行,这里仅考虑updateComponent
函数对应的Watcher
Watcher
会被调度器放到nextTick
中运行,也就是微队列中,这样是为了避免多个依赖的数据同时改变后被多次执行运行生命周期钩子函数
beforeUpdate
updateComponent
函数重新执行在执行
render
函数的过程中,会去掉之前的依赖,重新收集所有依赖,将来依赖变化时会重新运行updateComponent
函数在执行
_update
函数的过程中,触发patch
函数。新旧两棵树进行对比。
普通
html
节点的对比会导致真实节点被创建、删除、移动、更新组件节点的对比会导致组件被创建、删除、移动、更新
当新组件需要创建时,进入实例化流程
当旧组件需要删除时,会调用旧组件的
$destroy
方法删除组件,该方法会先触发生命周期钩子函数beforeDestroy
,然后递归调用子组件的$destroy
方法,然后触发生命周期钩子函数destroyed
当组件属性更新时,相当于组件的
updateComponent
函数被重新触发执行,进入重渲染流程,和本节相同。运行生命周期钩子函数
updated
7. 你不知道的computed
面试题:computed和methods有什么区别
标准而浅显的回答:
- 在使用时,computed当做属性使用,而methods则当做方法调用
- computed可以具有getter和setter,因此可以赋值,而methods不行
- computed无法接收多个参数,而methods可以
- computed具有缓存,而methods没有
更接近底层原理的回答:
vue对methods的处理比较简单,只需要遍历methods配置中的每个属性,将其对应的函数使用bind绑定当前组件实例后复制其引用到组件实例中即可
而vue对computed的处理会稍微复杂一些。
当组件实例触发生命周期函数
beforeCreate
后,它会做一系列事情,其中就包括对computed的处理它会遍历computed配置中的所有属性,为每一个属性创建一个Watcher对象,并传入一个函数,该函数的本质其实就是computed配置中的getter,这样一来,getter运行过程中就会收集依赖
但是和渲染函数不同,为计算属性创建的Watcher不会立即执行,因为要考虑到该计算属性是否会被渲染函数使用,如果没有使用,就不会得到执行。因此,在创建Watcher的时候,它使用了lazy配置,lazy配置可以让Watcher不会立即执行。
收到
lazy
的影响,Watcher内部会保存两个关键属性来实现缓存,一个是value
,一个是dirty
value
属性用于保存Watcher运行的结果,受lazy
的影响,该值在最开始是undefined
dirty
属性用于指示当前的value
是否已经过时了,即是否为脏值,受lazy
的影响,该值在最开始是true
Watcher创建好后,vue会使用代理模式,将计算属性挂载到组件实例中
当读取计算属性时,vue检查其对应的Watcher是否是脏值,如果是,则运行函数,计算依赖,并得到对应的值,保存在Watcher的value中,然后设置dirty为false,然后返回。
如果dirty为false,则直接返回watcher的value
巧妙的是,在依赖收集时,被依赖的数据不仅会收集到计算属性的Watcher,还会收集到组件的Watcher
当计算属性的依赖变化时,会先触发计算属性的Watcher执行,此时,它只需设置
dirty
为true即可,不做任何处理。由于依赖同时会收集到组件的Watcher,因此组件会重新渲染,而重新渲染时又读取到了计算属性,由于计算属性目前已为dirty,因此会重新运行getter进行运算
而对于计算属性的setter,则极其简单,当设置计算属性时,直接运行setter即可
8. filter过滤器
参见官网文档:https://v2.cn.vuejs.org/v2/guide/filters.html
9. 作用域插槽
参见官网文档:https://v2.cn.vuejs.org/v2/guide/components-slots.html#%E4%BD%9C%E7%94%A8%E5%9F%9F%E6%8F%92%E6%A7%BD
属性:
$slots
:用于访问父组件传递的普通插槽中的vnode$scopedSlots
:用于访问父组件传递的所有用于生成vnode的函数(包括默认插槽在内)
代码示例:
1 | <!-- async-content.vue --> |
10. 过渡和动画
10-1. 内置组件Transition
10-2. 时机
Transition
组件会监控slot
中唯一根元素的出现和消失,并会在其出现和消失时应用过渡效果
具体的监听内容是:
- 它会对新旧两个虚拟节点进行对比,如果旧节点被销毁,则应用消失效果,如果新节点是新增的,则应用进入效果
- 如果不是上述情况,则它会对比新旧节点,观察其
v-show
是否变化,true->false
应用消失效果,false->true
应用进入效果
10-3. 流程
类名规则:
- 如果
transition
上没有定义name
,则类名为v-xxxx
- 如果
transition
上定义了name
,则类名为${name}-xxxx
- 如果指定了类名,直接使用指定的类名
指定类名见:自定义过渡类名
1. 进入效果:
2. 消失效果:
10-4. 过渡组
Transision
可以监控其内部的单个dom元素的出现和消失,并为其附加样式
如果要监控一个dom列表,就需要使用TransitionGroup
组件
它会对列表的新增元素应用进入效果,删除元素应用消失效果,对被移动的元素应用v-move
样式
被移动的元素之所以能够实现过渡效果,是因为
TransisionGroup
内部使用了Flip过渡方案
代码示例:
1 | <template> |
11. 优化
11-1. 使用key
对于通过循环生成的列表,应给每个列表项一个稳定且唯一的key,这有利于在列表变动时,尽量少的删除、新增、改动元素
11-2. 使用冻结的对象
冻结的对象不会被响应化
代码示例:
1 | <template> |
11-3. 使用函数式组件
参见函数式组件
代码示例:
1 | <template functional> |
11-4. 使用计算属性
如果模板中某个数据会使用多次,并且该数据是通过计算得到的,使用计算属性以缓存它们
11-5. 非实时绑定的表单项
当使用v-model
绑定一个表单项时,当用户改变表单项的状态时,也会随之改变数据,从而导致vue
发生重渲染(rerender
),这会带来一些性能的开销。
特别是当用户改变表单项时,页面有一些动画正在进行中,由于JS执行线程和浏览器渲染线程是互斥的,最终会导致动画出现卡顿。
我们可以通过使用lazy
或不使用v-model
的方式解决该问题,但要注意,这样可能会导致在某一个时间段内数据和表单项的值是不一致的。
11-6. 保持对象引用稳定
在绝大部分情况下,vue
触发rerender
的时机是其依赖的数据发生变化
若数据没有发生变化,哪怕给数据重新赋值了,vue
也是不会做出任何处理的
下面是vue
判断数据没有变化的源码
1 | // value 为旧值, newVal 为新值 |
因此,如果需要,只要能保证组件的依赖数据不发生变化,组件就不会重新渲染。
对于原始数据类型,保持其值不变即可
对于对象类型,保持其引用不变即可
从另一方面来说,由于可以通过保持属性引用稳定来避免子组件的重渲染,那么我们应该细分组件来尽量避免多余的渲染
11-7. 使用v-show替代v-if
对于频繁切换显示状态的元素,使用v-show可以保证虚拟dom树的稳定,避免频繁的新增和删除元素,特别是对于那些内部包含大量dom元素的节点,这一点极其重要
关键字:频繁切换显示状态、内部包含大量dom元素
11-8. 使用延迟装载(defer)
首页白屏时间主要受到两个因素的影响:
打包体积过大
巨型包需要消耗大量的传输时间,导致JS传输完成前页面只有一个
<div>
,没有可显示的内容需要立即渲染的内容太多
JS传输完成后,浏览器开始执行JS构造页面。
但可能一开始要渲染的组件太多,不仅JS执行的时间很长,而且执行完后浏览器要渲染的元素过多,从而导致页面白屏
打包体积过大需要自行优化打包体积,本节不予讨论
本节仅讨论渲染内容太多的问题。
一个可行的办法就是延迟装载组件,让组件按照指定的先后顺序依次一个一个渲染出来
延迟装载是一个思路,本质上就是利用
requestAnimationFrame
事件分批渲染内容,它的具体实现多种多样
10-9. 使用keep-alive
面试题:请阐述keep-alive组件的作用和原理
keep-alive组件是vue的内置组件,用于缓存内部组件实例。这样做的目的在于,keep-alive内部的组件切回时,不用重新创建组件实例,而直接使用缓存中的实例,一方面能够避免创建组件带来的开销,另一方面可以保留组件的状态。
keep-alive具有include和exclude属性,通过它们可以控制哪些组件进入缓存。另外它还提供了max属性,通过它可以设置最大缓存数,当缓存的实例超过该数时,vue会移除最久没有使用的组件缓存。
1 | <!-- 数组形式 --> |
受keep-alive的影响,其内部所有嵌套的组件都具有两个生命周期钩子函数,分别是activated
和deactivated
,它们分别在组件激活和失活时触发。第一次activated
触发是在mounted
之后
在具体的实现上,keep-alive在内部维护了一个key数组和一个缓存对象
1 | // keep-alive 内部的声明周期函数 |
key数组记录目前缓存的组件key值,如果组件没有指定key值,则会为其自动生成一个唯一的key值
cache对象以key值为键,vnode为值,用于缓存组件对应的虚拟DOM
在keep-alive的渲染函数中,其基本逻辑是判断当前渲染的vnode是否有对应的缓存,如果有,从缓存中读取到对应的组件实例;如果没有则将其缓存。
当缓存数量超过max数值时,keep-alive会移除掉key数组的第一个元素
1 | render(){ |
代码示例:
1 | <template> |
10-10. 长列表优化
代码示例:[非插件]:
1 | <template> |
__END__