前端面试题汇总-Vue篇

Posted aiguangyuan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了前端面试题汇总-Vue篇相关的知识,希望对你有一定的参考价值。

1. Vue的基本原理

当一个Vue实例创建时,Vue会遍历data中的属性,用 Object.defineProperty(vue3.0使用proxy )将它们转为 getter/setter,并且在内部追踪相关依赖,在属性被访问和修改时通知变化。 每个组件实例都有相应的 watcher 程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter被调用时,会通知watcher重新计算,从而致使它关联的组件得以更新。

2. 双向数据绑定的原理

Vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter和getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

主要分为以下几个步骤:

1. 需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上setter和getter这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化;

2. compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图;

3. Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是:

(1). 在自身实例化时往属性订阅器(dep)里面添加自己;

(2). 自身必须有一个update()方法 ;

(3). 待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退;

4. MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 =>视图更新;视图交互变化(input) => 数据model变更的双向绑定效果;

3. 使用 Object.defineProperty() 来进行数据劫持有什么缺点?

在对一些属性进行操作时,使用这种方法无法拦截,比如通过下标方式修改数组数据或者给对象新增属性,这都不能触发组件的重新渲染,因为 Object.defineProperty 不能拦截到这些操作。更精确的来说,对于数组而言,大部分操作都是拦截不到的,只是 Vue 内部通过重写函数的方式解决了这个问题。

在 Vue3.0 中已经不使用这种方式了,而是通过使用 Proxy 对对象进行代理,从而实现数据劫持。使用Proxy 的好处是它可以完美的监听到任何方式的数据改变,唯一的缺点是兼容性的问题,因为 Proxy 是 ES6 的语法。

4. MVC、MVP、MVVM有什么区别?

MVC、MVP 和 MVVM 是三种常见的软件架构设计模式,主要通过分离关注点的方式来组织代码结构,优化开发效率。

在开发单页面应用时,往往一个路由页面对应了一个脚本文件,所有的页面逻辑都在一个脚本文件里。页面的渲染、数据的获取,对用户事件的响应所有的应用逻辑都混合在一起,这样在开发简单项目时,可能看不出什么问题,如果项目变得复杂,那么整个文件就会变得冗长、混乱,这样对项目开发和后期的项目维护是非常不利的。

4.1. 什么是MVC?

MVC 通过分离 Model、View 和 Controller 的方式来组织代码结构。其中 View 负责页面的显示逻辑,Model 负责存储页面的业务数据,以及对相应数据的操作。并且 View 和 Model 应用了观察者模式,当 Model 层发生改变的时候它会通知有关 View 层更新页面。Controller 层是 View 层和 Model 层的纽带,它主要负责用户与应用的响应操作,当用户与页面产生交互的时候,Controller 中的事件触发器就开始工作了,通过调用 Model 层,来完成对 Model 的修改,然后 Model 层再去通知 View 层更新。

4.2. 什么是MVVM?

MVVM 分为 Model、View、ViewModel:

1. Model代表数据模型,数据和业务逻辑都在Model层中定义;

2. View代表UI视图,负责数据的展示;

3. ViewModel负责监听Model中数据的改变并且控制视图的更新,处理用户交互操作;

Model和View并无直接关联,而是通过ViewModel来进行联系的,Model和ViewModel之间有着双向数据绑定的联系。因此当Model中的数据改变时会触发View层的刷新,View中由于用户交互操作而改变的数据也会在Model中同步。

这种模式实现了 Model和View的数据自动同步,因此开发者只需要专注于数据的维护操作即可,而不需要自己操作DOM。

4.3. 什么是MVP?

MVP 模式与 MVC 唯一不同的在于 Presenter 和 Controller。在 MVC 模式中使用观察者模式,来实现当 Model 层数据发生变化的时候,通知 View 层的更新。这样 View 层和 Model 层耦合在一起,当项目逻辑变得复杂的时候,可能会造成代码的混乱,并且可能会对代码的复用性造成一些问题。MVP 的模式通过使用 Presenter 来实现对 View 层和 Model 层的解耦。MVC 中的Controller 只知道 Model 的接口,因此它没有办法控制 View 层的更新,MVP 模式中,View 层的接口暴露给了 Presenter 因此可以在 Presenter 中将 Model 的变化和 View 的变化绑定在一起,以此来实现 View 和 Model 的同步更新。这样就实现了对 View 和 Model 的解耦,Presenter 还包含了其他的响应逻辑。

5. Computed 和 Watch 有什么区别?

5.1. 关于Computed

1. 它支持缓存,只有依赖的数据发生了变化,才会重新计算;

2. 不支持异步,当Computed中有异步操作时,无法监听数据的变化;

3. computed的值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的,也就是基于data声明过,或者父组件传递过来的props中的数据进行计算的;

4. 如果一个属性是由其他属性计算而来的,这个属性依赖其他的属性,一般会使用computed;

5. 如果computed属性的属性值是函数,那么默认使用get方法,函数的返回值就是属性的属性值;在computed中,属性有一个get方法和一个set方法,当数据发生变化时,会调用set方法;

5.2. 关于Watch

1. 它不支持缓存,数据变化时,它就会触发相应的操作;

2. 支持异步监听;

3. 监听的函数接收两个参数,第一个参数是最新的值,第二个是变化之前的值;

4. 当一个属性发生变化时,就需要执行相应的操作;

5. 监听数据必须是data中声明的或者父组件传递过来的props中的数据,当发生变化时,会触发其他操作,函数有两个的参数:

(1). immediate:组件加载立即触发回调函数;

(2). deep:深度监听,发现数据内部的变化,在复杂数据类型中使用,例如数组中的对象发生变化。需要注意的是,deep无法监听到数组和对象内部的变化;

当想要执行异步或者昂贵的操作以响应不断的变化时,就需要使用watch。

5.3. 运用场景

当需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时都要重新计算。

当需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许执行异步操作 ( 访问一个 API ),限制执行该操作的频率,并在得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

5.4. 总结

computed 计算属性 : 依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值。

watch 侦听器 : 更多的是观察的作用,无缓存性,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作。

6. Computed 和 Methods 有什么区别?

可以将同一函数定义为一个 method 或者一个计算属性。对于最终的结果,两种方式是相同的,而不同点如下:

1. computed: 计算属性是基于它们的依赖进行缓存的,只有在它的相关依赖发生改变时才会重新求值;

2. method 调用总会执行该函数;

7.  Slot是什么?有什么作用?原理是什么?

slot又名插槽,是Vue的内容分发机制,组件内部的模板引擎使用slot元素作为承载分发内容的出口。插槽slot是子组件的一个模板标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决定的。slot分三类,默认插槽,具名插槽和作用域插槽。

1. 默认插槽:又名匿名插槽,当slot没有指定name属性值的时候一个默认显示插槽,一个组件内只有有一个匿名插槽;

2. 具名插槽:带有具体名字的插槽,也就是带有name属性的slot,一个组件可以出现多个具名插槽;

3. 作用域插槽:默认插槽、具名插槽的一个变体,可以是匿名插槽,也可以是具名插槽,该插槽的不同点是在子组件渲染作用域插槽时,可以将子组件内部的数据传递给父组件,让父组件根据子组件的传递过来的数据决定如何渲染该插槽;

实现原理:当子组件vm实例化时,获取到父组件传入的slot标签的内容,存放在vm.$slot中,默认插槽为vm.$slot.default,具名插槽为vm.$slot.xxx,xxx 为插槽名,当组件执行渲染函数时候,遇到slot标签,使用$slot中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。

8. 过滤器的作用,如何实现一个过滤器?

根据过滤器的名称,过滤器是用来过滤数据的,在Vue中使用filters来过滤数据,filters不会修改数据,而是过滤数据,改变用户看到的输出(计算属性 computed ,方法 methods 都是通过修改数据来处理数据格式的输出显示)。

使用场景:

1. 需要格式化数据的情况,比如需要处理时间、价格等数据格式的输出 / 显示;

2. 后端返回一个 年月日的日期字符串,前端需要展示为 多少天前 的数据格式,此时就可以用fliters过滤器来处理数据;

过滤器是一个函数,它会把表达式中的值始终当作函数的第一个参数。过滤器用在插值表达式   和 v-bind 表达式 中,然后放在操作符 “|” 后面进行指示。

例如,在显示金额,给商品价格添加单位:

<li>商品价格:item.price | filterPrice</li>

 filters: 
    filterPrice (price) 
      return price ? ('¥' + price) : '--'
    
 

9. 如何保存页面的当前的状态?

既然是要保持页面的状态(其实也就是组件的状态),那么会出现以下两种情况:

1. 当前组件会被卸载;

2. 当前组件不会被卸载;

那么可以按照这两种情况分别得到以下方法:

组件会被卸载:

9.1. 将状态存储在LocalStorage / SessionStorage;

只需要在组件即将被销毁的生命周期 componentWillUnmount (react)中在 LocalStorage / SessionStorage 中把当前组件的 state 通过 JSON.stringify() 储存下来就可以了。在这里面需要注意的是组件更新状态的时机。

比如从 B 组件跳转到 A 组件的时候,A 组件需要保持更新的状态。但是如果从别的组件跳转到 A 组件的时候,实际上是希望 A 组件保持状态的,也就是要从 Storage 中读取信息。所以需要在 Storage 中的状态加入一个 flag 属性,用来控制 A 组件是否读取 Storage 中的状态。

优点:

1. 兼容性好,不需要额外库或工具;

2. 简单快捷,基本可以满足大部分需求;

缺点:

1. 状态通过 JSON 方法储存(相当于深拷贝),如果状态中有特殊情况(比如 Date 对象、Regexp 对象等)的时候会得到字符串而不是原来的值;

2. 如果 B 组件后退或者下一页跳转并不是当前A组件,那么 flag 判断会失效,导致从其他页面进入 A 组件页面时 A 组件会重新读取 Storage,会造成很奇怪的现象;

9.2. 路由传值

通过 react-router 的 Link 组件的 prop - to 可以实现路由间传递参数的效果。

在这里需要用到 state 参数,在 B 组件中通过 history.location.state 就可以拿到 state 值,保存它。返回 A 组件时再次携带 state 达到路由状态保持的效果。

优点:

1. 简单快捷,不会污染 LocalStorage / SessionStorage;

2. 可以传递 Date、RegExp 等特殊对象(不用担心 JSON.stringify / parse 的不足);

缺点:

1. 如果 A 组件可以跳转至多个组件,那么在每一个跳转组件内都要写相同的逻辑;

组件不会被卸载:

9.3. 单页面渲染

要切换的组件作为子组件全屏渲染,父组件中正常储存页面状态。

优点:

1. 代码量少;

2. 不需要考虑状态传递过程中的错误;

缺点:

1. 增加 A 组件维护成本;

2. 需要传入额外的 prop 到 B 组件;

3. 无法利用路由定位页面;

除此之外,在Vue中,还可以是用keep-alive来缓存页面,当组件在keep-alive内被切换时组件的activated、deactivated这两个生命周期钩子函数会被执行,被包裹在keep-alive中的组件的状态将会被保留。

<keep-alive>
    <router-view v-if="$route.meta.keepAlive"></router-view>
</kepp-alive>

router.js


    path: '/',
    name: 'xxx',
    component: ()=>import('../src/views/xxx.vue'),
    meta:
        keepAlive: true // 需要被缓存
    
,

10. 常见的事件修饰符及其作用

1.stop:等同于 javascript 中的 event.stopPropagation() ,防止事件冒泡;

2. .prevent :等同于 JavaScript 中的 event.preventDefault() ,防止执行预设的行为(如果事件可取消,则取消该事件,而不停止事件的进一步传播);

3. .capture :与事件冒泡的方向相反,事件捕获由外到内;

4. .self :只会触发自己范围内的事件,不包含子元素;

5. .once :只会触发一次;

11. v-if、v-show、v-html 的原理

1. v-if会调用addIfCondition方法,生成vnode的时候会忽略对应节点,render的时候就不会渲染;

2. v-show会生成vnode,render的时候也会渲染成真实节点,只是在render过程中会在节点的属性中修改show属性值,也就是常说的display;

3. v-html会先移除节点下的所有节点,调用html方法,通过addProp添加innerHTML属性,归根结底还是设置innerHTML为v-html的值;

12. v-if 和 v-show的区别

实现手段:v-if是动态的向DOM树内添加或者删除DOM元素;v-show是通过设置DOM元素的display样式属性控制显隐;

编译过程:v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show只是简单的基于css切换;

编译条件:v-if是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始局部编译; v-show是在任何条件下,无论首次条件是否为真,都被编译,然后被缓存,而且DOM元素保留;

性能消耗:v-if有更高的切换消耗;v-show有更高的初始渲染消耗;

使用场景:v-if适合运营条件不大可能改变;v-show适合频繁切换;

13. v-model 是如何实现的,语法糖实际是什么?

13.1. 作用在表单元素上

动态绑定了 input 的 value 指向了 messgae 变量,并且在触发 input 事件的时候去动态把 message设置为目标值:

<input v-model="message" />

// 等同于

<input v-bind:value="message" v-on:input="message=$event.target.value">

// $event 指代当前触发的事件对象;
// $event.target 指代当前触发的事件对象的dom;
// $event.target.value 就是当前dom的value值;
// 在@input方法中,value => message;
// 在:value中,message=> value;

13.2. 作用在组件上

在自定义组件中,v-model 默认会利用名为 value 的 prop和名为 input 的事件。

本质是一个父子组件通信的语法糖,通过prop和$.emit实现。 因此父组件 v-model 语法糖本质上可以修改为:

<child :value="message"  @input="function(e)message = e"></child>

在组件的实现中,可以通过 v-model属性来配置子组件接收的prop名称,以及派发的事件名称。

// 父组件
<aa-input v-model="aa"></aa-input>
// 等价于
<aa-input v-bind:value="aa" v-on:input="aa=$event.target.value"></aa-input>

// 子组件:
<input v-bind:value="aa" v-on:input="onmessage"></input>

props:value:aa,
methods:
    onmessage(e)
        $emit('input',e.target.value)
    

默认情况下,一个组件上的v-model 会把 value 用作 prop且把 input 用作 event。但是一些输入类型比如单选框和复选框按钮可能想使用 value prop 来达到不同的目的。使用 model 选项可以回避这些情况产生的冲突。js 监听input 输入框输入数据改变,用oninput,数据改变以后就会立刻触发这个事件。通过input事件把数据$emit 出去,在父组件接受。父组件设置v-model的值为input $emit过来的值。

14.  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 与父组件的交互如下:

1. 父组件将searchText变量传入custom-input 组件,使用的 prop 名为value

2. 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)"
        >
    `
)

15. data为什么是一个函数而不是对象?

JavaScript中的对象是引用类型的数据,当多个实例引用同一个对象时,只要一个实例对这个对象进行操作,其他实例中的数据也会发生变化。

而在Vue中,更多的是想要复用组件,那就需要每个组件都有自己的数据,这样组件之间才不会相互干扰。

所以组件的数据不能写成对象的形式,而是要写成函数的形式。数据以函数返回值的形式定义,这样当每次复用组件的时候,就会返回一个新的data,也就是说每个组件都有自己的私有数据空间,它们各自维护自己的数据,不会干扰其他组件的正常运行。

16. 谈谈你对keep-alive的理解,它是如何实现的,具体缓存的是什么?

如果需要在组件切换的时候,保存一些组件的状态防止多次渲染,就可以使用 keep-alive 组件包裹需要保存的组件。

16.1. keep-alive的属性

keep-alive有以下三个属性:

1. include 字符串或正则表达式,只有名称匹配的组件会被匹配;

2. exclude 字符串或正则表达式,任何名称匹配的组件都不会被缓存;

3. max 数字,最多可以缓存多少组件实例;

注意:keep-alive 包裹动态组件时,会缓存不活动的组件实例。

主要流程

1. 判断组件 name ,不在 include 或者在 exclude 中,直接返回 vnode,说明该组件不被缓存;

2. 获取组件实例 key ,如果有获取实例的 key,否则重新生成;

3. key生成规则,cid +“∶∶”+ tag ,仅靠cid是不够的,因为相同的构造函数可以注册为不同的本地组件;

4. 如果缓存对象内存在,则直接从缓存对象中获取组件实例给 vnode ,不存在则添加到缓存对象中;

5. .最大缓存数量,当缓存组件数量超过 max 值时,清除 keys 数组内第一个组件;

16.2. keep-alive 的实现

const patternTypes: Array<Function> = [String, RegExp, Array] // 接收:字符串,正则,数组

export default 
    name: 'keep-alive',
    // 抽象组件,是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在父组件链中。
    abstract: true, 
    props: 
        // 匹配的组件,缓存
        include: patternTypes, 
        // 不去匹配的组件,不缓存
        exclude: patternTypes, 
        // 缓存组件的最大实例数量, 由于缓存的是组件实例(vnode),数量过多的时候,会占用过多的内存,可以用max指定上限
        max: [String, Number], 
    ,

    created() 
        // 用于初始化缓存虚拟DOM数组和vnode的key
        this.cache = Object.create(null)
        this.keys = []
    ,

    destroyed() 
        // 销毁缓存cache的组件实例
        for (const key in this.cache) 
            pruneCacheEntry(this.cache, key, this.keys)
        
    ,

    mounted() 
        // 去监控include和exclude的改变,根据最新的include和exclude的内容,来实时削减缓存的组件的内容
        this.$watch('include', (val) => 
            pruneCache(this, (name) => matches(val, name))
        )
        this.$watch('exclude', (val) => 
            pruneCache(this, (name) => !matches(val, name))
        )
    ,

render函数:

1. 会在 keep-alive 组件内部去写自己的内容,所以可以去获取默认 slot 的内容,然后根据这个去获取组件;

2. keep-alive 只对第一个组件有效,所以获取第一个子组件;

3. 和 keep-alive 搭配使用的一般有:动态组件 和router-view;

render() 
    
    function getFirstComponentChild(children: ?Array<VNode>): ?VNode 
        if (Array.isArray(children)) 
            for (let i = 0; i < children.length; i++) 
                const c = children[i]
                if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) 
                    return c
                
            
        
    
    const slot = this.$slots.default // 获取默认插槽
    const vnode: VNode = getFirstComponentChild(slot)// 获取第一个子组件
    const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions // 组件参数
    if (componentOptions)  // 是否有组件参数
        // check pattern
        const name: ?string = getComponentName(componentOptions) // 获取组件名
        const  include, exclude  = this
        if (
            // not included
            (include && (!name || !matches(include, name))) ||
            // excluded
            (exclude && name && matches(exclude, name))
        ) 
            // 如果不匹配当前组件的名字和include以及exclude
            // 那么直接返回组件的实例
            return vnode
        

        const  cache, keys  = this

        // 获取这个组件的key
        const key: ?string = vnode.key == null
            // same constructor may get registered as different local components
            // so cid alone is not enough (#3269)
            ? componentOptions.Ctor.cid + (componentOptions.tag ? `::$componentOptions.tag` : '')
            : vnode.key

        if (cache[key]) 
            // LRU缓存策略执行
            vnode.componentInstance = cache[key].componentInstance // 组件初次渲染的时候componentInstance为undefined

            // make current key freshest
            remove(keys, key)
            keys.push(key)
            // 根据LRU缓存策略执行,将key从原来的位置移除,然后将这个key值放到最后面
         else 
            // 在缓存列表里面没有的话,则加入,同时判断当前加入之后,是否超过了max所设定的范围,如果是,则去除
            // 使用时间间隔最长的一个
            cache[key] = vnode
            keys.push(key)
            // prune oldest entry
            if (this.max && keys.length > parseInt(this.max)) 
                pruneCacheEntry(cache, keys[0], keys, this._vnode)
            
        
        // 将组件的keepAlive属性设置为true
        vnode.data.keepAlive = true // 作用:判断是否要执行组件的created、mounted生命周期函数
    
    return vnode || (slot && slot[0])

keep-alive 具体是通过 cache 数组缓存所有组件的 vnode 实例。当 cache 内原有组件被使用时会将该组件 key 从 keys 数组中删除,然后 push 到 keys数组最后,以便清除最不常用组件。

实现步骤:

1. 获取 keep-alive 下第一个子组件的实例对象,通过他去获取这个组件的组件名;

2. 通过当前组件名去匹配原来 include 和 exclude,判断当前组件是否需要缓存,不需要缓存,直接返回当前组件的实例vNode;

3. 需要缓存,判断他当前是否在缓存数组里面:

(1). 存在,则将他原来位置上的 key 给移除,同时将这个组件的 key 放到数组最后面(LRU);

(2). 不存在,将组件 key 放入数组,然后判断当前 key数组是否超过 max 所设置的范围,超过,那么削减未使用时间最长的一个组件的 key;

4. 最后将这个组件的 keepAlive 设置为 true;

16.3. keep-alive 本身的创建过程和 patch 过程

缓存渲染的时候,会根据 vnode.componentInstance(首次渲染 vnode.componentInstance 为 undefined) 和 keepAlive 属性判断不会执行组件的 created、mounted 等钩子函数,而是对缓存的组件执行 patch 过程∶ 直接把缓存的 DOM 对象直接插入到目标元素中,完成了数据更新的情况下的渲染过程。

首次渲染

组件的首次渲染∶判断组件的 abstract 属性,才往父组件里面挂载 DOM。

function initLifecycle(vm: Component) 
    const options = vm.$options
    let parent = options.parent
    // 判断组件的abstract属性,才往父组件里面挂载DOM
    if (parent && !options.abstract)  
        while (parent.$options.abstract && parent.$parent) 
            parent = parent.$parent
        
        parent.$children.push(vm)
    

    vm.$parent = parent
    vm.$root = parent ? parent.$root : vm

    vm.$children = []
    vm.$refs = 

    vm._watcher = null
    vm._inactive = null
    vm._directInactive = false
    vm._isMounted = false
    vm._isDestroyed = false
    vm._isBeingDestroyed = false

判断当前 keepAlive 和 componentInstance 是否存在,以此来决定是否要执行组件 prepatch 还是执行创建 componentlnstance。

// core/vdom/create-component
init(vnode: VNodeWithData, hydrating: boolean): ?boolean 
    if (
        vnode.componentInstance &&
        !vnode.componentInstance._isDestroyed &&
        vnode.data.keepAlive
    )  
        // componentInstance在初次是undefined!!!
        // kept-alive components, treat as a patch
        const mountedNode: any = vnode // work around flow
        componentVNodeHooks.prepatch(mountedNode, mountedNode) // prepatch函数执行的是组件更新的过程
     else 
        const child = vnode.componentInstance = createComponentInstanceForVnode(
            vnode,
            activeInstance
        )
        child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    
,

prepatch 操作就不会在执行组件的 mounted 和 created 生命周期函数,而是直接将 DOM 插入

16.4. 缓存策略 LRU (least recently used)

LRU 缓存策略∶ 从内存中找出最久未使用的数据并置换新的数据。

LRU(Least rencently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是 “如果数据最近被访问过,那么将来被访问的几率也更高”

最常见的实现是使用一个链表保存缓存数据,详细算法实现如下∶

1. 新数据插入到链表头部;

2. 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;

3. 链表满的时候,将链表尾部的数据丢弃;

17. $nextTick 原理及作用

Vue 的 nextTick 其本质是对 JavaScript 执行原理 EventLoop 的一种应用。

nextTick 的核心是利用了如 Promise 、MutationObserver、setImmediate、setTimeout的原生 JavaScript 方法来模拟对应的微/宏任务的实现,本质是为了利用 JavaScript 的这些异步回调任务队列来实现 Vue 框架中自己的异步回调队列。

nextTick 不仅是 Vue 内部的异步队列的调用方法,同时也允许开发者在实际项目中使用这个方法来满足实际应用中对 DOM 更新数据时机的后续逻辑处理。

nextTick 是典型的将底层 JavaScript 执行原理应用到具体案例中的示例,引入异步更新队列机制的原因∶

1. 如果是同步更新,则多次对一个或多个属性赋值,会频繁触发 UI/DOM 的渲染,可以减少一些无用渲染;

2. 同时由于 VirtualDOM 的引入,每一次状态发生变化后,状态变化的信号会发送给组件,组件内部使用 VirtualDOM 进行计算得出需要更新的具体的 DOM 节点,然后对 DOM 进行更新操作,每次更新状态后的渲染过程需要更多的计算,而这种无用功也将浪费更多的性能,所以异步渲染变得更加至关重要;

Vue采用了数据驱动视图的思想,但是在一些情况下,仍然需要操作DOM。有时候,可能遇到这样的情况,DOM1的数据发生了变化,而DOM2需要从DOM1中获取数据,那这时就会发现DOM2的视图并没有更新,这时就需要用到了nextTick了。

由于Vue的DOM操作是异步的,所以,在上面的情况中,就要将DOM2获取数据的操作写在$nextTick中。

this.$nextTick(() =>     // 获取数据的操作...)

所以,在以下情况下,会用到nextTick:

1. 在数据变化后执行的某个操作,而这个操作需要使用随数据变化而变化的DOM结构的时候,这个操作就需要方法在nextTick()的回调函数中;

2. 在vue生命周期中,如果在created()钩子进行DOM操作,也一定要放在nextTick()的回调函数中;

因为在created()钩子函数中,页面的DOM还未渲染,这时候也没办法操作DOM,所以,此时如果想要操作DOM,必须将操作的代码放在nextTick()的回调函数中。

18. Vue 中给 data 中的对象属性添加一个新的属性时会发生什么?如何解决?

<template> 
    <div>
        <ul>
            <li v-for="value in obj" :key="value"> value </li> 
        </ul> 
        <button @click="addObjB">添加 obj.b</button> 
    </div>
</template>

<script>
    export default  
        data ()  
            return  
                obj:  
                    a: 'obj.a' 
                 
             
        ,
        methods:  
            addObjB() 
                this.obj.b = 'obj.b' 
                console.log(this.obj) 
             
        
    
</script>

点击 button 会发现,obj.b 已经成功添加,但是视图并未刷新。这是因为在Vue实例创建时,obj.b并未声明,因此就没有被Vue转换为响应式的属性,自然就不会触发视图的更新,这时就需要使用Vue的全局API $set().

addObjB () (
    this.$set(this.obj, 'b', 'obj.b')
    console.log(this.obj)

$set()方法相当于手动的去把 obj.b 处理成一个响应式的属性,此时视图也会跟着改变了。

19. Vue中封装的数组方法有哪些,其如何实现页面更新?

在Vue中,对响应式处理利用的是Object.defineProperty对数据进行拦截,而这个方法并不能监听到数组内部变化,数组长度变化,数组的截取变化等,所以需要对这些操作进行hack,让Vue能监听到其中的变化。

 那Vue是如何实现让这些数组方法实现元素的实时更新的呢,下面是Vue中对这些方法的封装:

// 缓存数组原型
const arrayProto = Array.prototype;
// 实现 arrayMethods.__proto__ === Array.prototype
export const arrayMethods = Object.create(arrayProto);
// 需要进行功能拓展的方法
const methodsToPatch = [
    "push",
    "pop",
    "shift",
    "unshift",
    "splice",
    "sort",
    "reverse"
];


methodsToPatch.forEach(function (method) 
    // 缓存原生数组方法
    const original = arrayProto[method];
    def(arrayMethods, method, function mutator(...args) 
        // 执行并缓存原生数组功能
        const result = original.apply(this, args);
        // 响应式处理
        const ob = this.__ob__;
        let inserted;
        switch (method) 
            // push、unshift会新增索引,所以要手动observer
            case "push":
            case "unshift":
                inserted = args;
                break;
            // splice方法,如果传入了第三个参数,也会有索引加入,也要手动observer。
            case "splice":
                inserted = args.slice(2);
                break;
        
        // 获取插入的值,并设置响应式监听
        if (inserted) ob.observeArray(inserted);
        // 通知依赖更新
        ob.dep.notify();
        // 返回原生数组方法的执行结果
        return result;
    );
);

简单来说就是,重写了数组中的那些原生方法,首先获取到这个数组的__ob__,也就是它的Observer对象,如果有新的值,就调用observeArray继续对新的值观察变化(也就是通过target__proto__ == arrayMethods来改变了数组实例的型),然后手动调用notify,通知渲染watcher,执行update。

20. Vue 单页应用与多页应用的区别?

概念:

1. SPA单页面应用(SinglePage Web Application),指只有一个主页面的应用,一开始只需要加载一次JS、CSS等相关资源。所有内容都包含在主页面,对每一个功能模块组件化。单页应用跳转,就是切换相关组件,仅仅刷新局部资源;

2. MPA多页面应用 (MultiPage Application),指有多个独立页面的应用,每个页面必须重复加载 JS、CSS等相关资源。多页应用跳转,需要整页资源刷新。

区别:

21. Vue template 到 render 的过程

vue的模版编译过程主要如下:template => ast => render函数.

vue 在模版编译版本的码中会执行 compileToFunctions 将 template转化为render函数。

CompileToFunctions中的主要逻辑如下∶

1. 调用parse方法将template转化为ast(抽象语法树);

const ast = parse(template.trim(), options)

解析的目标:把tamplate转换为AST树,它是一种用 JavaScript对象的形式来描述整个模板。

解析的过程:利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的 回调函数,来达到构造AST树的目的。

AST元素节点总共三种类型:type为1表示普通元素、2为表达式、3为纯文本。

2. 对静态节点做优化;

optimize(ast,options)

这个过程主要分析出哪些是静态节点,给其打一个标记,为后续更新渲染可以直接跳过静态节点做优化。

深度遍历AST,查看每个子树的节点元素是否为静态节点或者静态节点根。如果为静态节点,他们生成的DOM永远不会改变,这对运行时模板更新起到了极大的优化作用。

3. 生成代码;

const code = generate(ast, options)

generate将ast抽象语法树编译成 render字符串并将静态部分放到 staticRenderFns 中,最后通过 new Function(`` render``) 生成render函数。

22. Vue data 中某一个属性的值发生改变后,视图会立即同步执行重新渲染吗?

不会立即同步执行重新渲染。Vue 实现响应式并不是数据发生变化之后 DOM 立即变化,而是按一定的策略进行 DOM 的更新。Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化, Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。

如果同一个watcher被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环中,Vue 刷新队列并执行实际(已去重的)工作。

23. 简述 mixin、extends 的覆盖逻辑

1. mixin 和 extends

mixin 和 extends均是用于合并、拓展组件的,两者均通过 mergeOptions 方法实现合并。

(1). mixins 接收一个混入对象的数组,其中混入对象可以像正常的实例对象一样包含实例选项,这些选项会被合并到最终的选项中。Mixin 钩子按照传入顺序依次调用,并在调用组件自身的钩子之前被调用;

(2). extends 主要是为了便于扩展单文件组件,接收一个对象或构造函数;

 2. mergeOptions 的执行过程

(1). 规范化选项(normalizeProps、normalizelnject、normalizeDirectives);

(2). 对未合并的选项,进行判断;

if (!child._base)  
    if (child.extends)  
        parent = mergeOptions(parent, child.extends, vm) 
    

(3). 合并处理。根据一个通用 Vue 实例所包含的选项进行分类逐一判断合并,如 props、data、 methods、watch、computed、生命周期等,将合并结果存储在新定义的 options 对象里;

(4). 返回合并结果 options;

24. 描述下Vue自定义指令

在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。

一般需要对DOM元素进行底层操作时使用,尽量只用来操作 DOM展示,不修改内部的值。当使用自定义指令直接修改 value 值时绑定v-model的值也不会同步更新;如必须修改可以在自定义指令中使用keydown事件,在vue组件中使用 change事件,回调中修改vue数据。

1. 自定义指令基本内容;

全局定义Vue.directive("focus",)

局部定义directives:focus:

钩子函数:指令定义对象提供钩子函数。

(1). bind:只调用一次,指令第一次绑定到元素时调用,在这里可以进行一次性的初始化设置;

(2). inserted:被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中);

(3). update:所在组件的VNode更新时调用,但是可能发生在其子VNode更新之前调用。指令的值可能发生了改变,也可能没有。但是可以通过比较更新前后的值来忽略不必要的模板更新;

(4). ComponentUpdate:指令所在组件的 VNode及其子VNode全部更新后调用;

(5). unbind:只调用一次,指令与元素解绑时调用;

钩子函数参数:

(1). el:绑定元素;

(2). bing: 指令核心对象,描述指令全部信息属性;

(3). name;

(4). value;

(5). oldValue;

(6). expression;

(7). arg;

(8). modifers;

(9). vnode 虚拟节点;

(10). oldVnode:上一个虚拟节点(更新钩子函数中才有用);

2. 自定义指令使用场景;

(1). 普通DOM元素进行底层操作的时候,可以使用自定义指令;

(2). 自定义指令是用来操作DOM的。尽管Vue推崇数据驱动视图的理念,但并非所有情况都适合数据驱动。自定义指令就是一种有效的补充和扩展,不仅可用于定义任何的DOM操作,并且是可复用的;

3. 自定义指令使用案例;

初级应用:

(1). 鼠标聚焦;

(2). 下拉菜单;

(3). 相对时间转换;

(4). 滚动动画;

高级应用:

(1). 自定义指令实现图片懒加载;

(2). 自定义指令集成第三方插件;

25. 子组件可以直接改变父组件的数据吗?

子组件不可以直接改变父组件的数据。这样做主要是为了维护父子组件的单向数据流。每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。如果这样做了,Vue 会在浏览器的控制台中发出警告。

Vue提倡单向数据流,即父级 props 的更新会流向子组件,但是反过来则不行。这是为了防止意外的改变父组件状态,使得应用的数据流变得难以理解,导致数据流混乱。如果破坏了单向数据流,当应用复杂时,debug 的成本会非常高。

只能通过 $emit 派发一个自定义事件,父组件接收到后,由父组件修改。

26. 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() 进行依赖收集。

1. 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 的管理。

2. Watcher

Watcher 是一个 class,它定义了一些方法,其中和依赖收集相关的主要有 get、addDep 等。

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

3. 收集过程

在实例化 Vue 时,依赖收集的相关过程如下∶

初始化状态 initState , 这中间便会通过defineReactive 将数据变成响应式对象,其中的 getter 部分便是用来依赖收集的。

初始化最终会走 mount 过程,其中会实例化 Watcher ,进入 Watcher 中,便会执行 this.get() 方法,get 方法中的 pushTarget 实际上就是把 Dep.target 赋值为当前的 watcher。

updateComponent = () => 
    vm._update(vm._render())

new Watcher(vm, updateComponent)

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,这样便已经完成了一个依赖收集的过程。

27. React 和 Vue 有什么异同?

相似之处:

1. 都将注意力集中保持在核心库,而将其他功能如路由和全局状态管理交给相关的库;

2. 都有自己的构建工具,能让你得到一个根据最佳实践设置的项目模板;

3. 都使用了Virtual DOM(虚拟DOM)提高重绘性能;

4. 都有props的概念,允许组件间的数据传递;

5. 都鼓励组件化应用,将应用分拆成一个个功能明确的模块,提高复用性;

不同之处 :

1. 数据流

Vue默认支持数据双向绑定,而React一直提倡单向数据流;

2. 虚拟DOM

Vue2.x开始引入"Virtual DOM",消除了和React在这方面的差异,但是在具体的细节还是有各自的特点。

(1). Vue宣称可以更快地计算出Virtual DOM的差异,这是由于它在渲染过程中,会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树;

(2). 对于React,每当应用的状态被改变时,全部子组件都会重新渲染。当然,这可以通过 PureComponent/shouldComponentUpdate这个生命周期方法来进行控制,但Vue将此视为默认的优化;

3. 组件化

React与Vue最大的不同是模板的编写。

(1). Vue鼓励写近似常规HTML的模板。写起来很接近标准 HTML元素,只是多了一些属性;

(2). React推荐你所有的模板通用JavaScript的语法扩展JSX书写;

具体来讲:React中render函数是支持闭包特性的,所以import的组件在render中可以直接调用。但是在Vue中,由于模板中使用的数据都必须挂在 this 上进行一次中转,所以 import 一个组件完了之后,还需要在 components 中再声明下。

4. 监听数据变化的实现原理不同

(1). Vue 通过 getter/setter 以及一些函数的劫持,能精确知道数据变化,不需要特别的优化就能达到很好的性能;

(2). React 默认是通过比较引用的方式进行的,如果不优化(PureComponent/shouldComponentUpdate)可能导致大量不必要的vDOM的重新渲染。这是因为 Vue 使用的是可变数据,而React更强调数据的不可变;

5. 高阶组件

react可以通过高阶组件(HOC)来扩展,而Vue需要通过mixins来扩展。

高阶组件就是高阶函数,而React的组件本身就是纯粹的函数,所以高阶函数对React来说易如反掌。相反Vue.js使用HTML模板创建视图组件,这时模板无法有效的编译,因此Vue不能采用HOC来实现。

6. 构建工具

两者都有自己的构建工具:

React => Create React APP

Vue => vue-cli

7. 跨平台

React => React Native

Vue => Weex

28. Vue的优点

1. 轻量级框架:只关注视图层,是一个构建数据的视图集合,大小只有几十 kb ;

2. 简单易学:国人开发,中文文档,不存在语言障碍 ,易于理解和学习;

3. 双向数据绑定:保留了 angular 的特点,在数据操作方面更为简单;

4. 组件化:保留了 react 的优点,实现了 html 的封装和重用,在构建单页面应用方面有着独特的优势;

5. 视图,数据,结构分离:使数据的更改更为简单,不需要进行逻辑代码的修改,只需要操作数据就能完成相关操作;

6. 虚拟DOM:dom 操作是非常耗费性能的,不再使用原生的 dom 操作节点,极大解放 dom 操作,但具体操作的还是 dom 不过是换了另一种方式;

7. 运行速度更快:相比较于 react 而言,同样是操作虚拟 dom,就性能而言, vue 存在很大的优势;

29. assets和static的区别

相同:assets 和 static 两个都是存放静态资源文件。项目中所需要的资源文件图片,字体图标,样式文件等都可以放在这两个文件下。

不同:assets 中存放的静态资源文件在项目打包时,也就是运行 npm run build 时会将 assets 中放置的静态资源文件进行打包上传,而压缩后的静态资源文件最终也都会放置在 static 文件中跟着 index.html

以上是关于前端面试题汇总-Vue篇的主要内容,如果未能解决你的问题,请参考以下文章

前端面试题整理—Vue篇

前端面试题汇总

前端面试题汇总

前端面试题汇总(主要为 Vue)

阿里前端常考vue面试题汇总

前端面试题vue-element汇总