vue相关面试点-2021

924 阅读15分钟

style-scope作用及实现原理

1.什么是scoped

在Vue文件中的style标签上有一个特殊的属性,scoped。当一个style标签拥有scoped属性时候,它的css样式只能用于当前的Vue组件,可以使组件的样式不相互污染。如果一个项目的所有style标签都加上了scoped属性,相当于实现了样式的模块化。

2.scoped的实现原理

Vue中的scoped属性的效果主要是通过PostCss实现的。以下是转译前的代码:

<style scoped>
.example{
color:red;
}
</style>

<template>

   <div>scoped测试案例</div>

</template>

转译后:

.example[data-v-5558831a] {

color: red;

}

<template>

<div class="example" data-v-5558831a>scoped测试案例</div>

</template>

既:PostCSS给一个组件中的所有dom添加了一个独一无二的动态属性,给css选择器额外添加一个对应的属性选择器,来选择组件中的dom,这种做法使得样式只作用于含有该属性的dom元素(组件内部的dom)。

总结:scoped的渲染规则:

  1. 给HTML的dom节点添加一个不重复的data属性(例如: data-v-5558831a)来唯一标识这个dom 元素
  2. 在每句css选择器的末尾(编译后生成的css语句)加一个当前组件的data属性选择器(例如:[data-v-5558831a])来私有化样式

在scope中修改全局样式要怎么做

scoped穿透

当我们引入第三方组件库时(如使用vue-awesome-swiper实现移动端轮播),需要在局部组件中修改第三方组件库的样式,而又不想去除scoped属性造成组件之间的样式覆盖。这时我们可以通过特殊的方式穿透scoped。

stylus的样式穿透 使用>>>

外层 >>> 第三方组件

样式
.wrapper >>> .swiper-pagination-bullet-active

background: #fff

sass和less的样式穿透 使用/deep/

外层 /deep/ 第三方组件 {
样式
.wrapper /deep/ .swiper-pagination-bullet-active{
background: #fff;
}

虚拟dom有什么作用?如何实现

一、什么是虚拟DOM

虚拟 DOM (Virtual DOM )为这框架都带来了跨平台的能力

实际上它只是一层对真实DOM的抽象,以JavaScript 对象 (VNode 节点) 作为基础的树,用对象的属性来描述节点,最终可以通过一系列操作使这棵树映射到真实环境上

在Javascript对象中,虚拟DOM 表现为一个 Object对象。并且最少包含标签名 (tag)、属性 (attrs) 和子元素对象 (children) 三个属性,不同框架对这三个属性的名命可能会有差别

创建虚拟DOM就是为了更好将虚拟的节点渲染到页面视图中,所以虚拟DOM对象的节点与真实DOM的属性一一照应

二、为什么需要虚拟DOM

DOM是很慢的,其元素非常庞大,页面的性能问题,大部分都是由DOM操作引起的.真实的DOM节点,哪怕一个最简单的div也包含着很多属性,操作DOM的代价仍旧是昂贵的,频繁操作还是会出现页面卡顿,影响用户的体验

举个例子:

你用传统的原生api或jQuery去操作DOM时,浏览器会从构建DOM树开始从头到尾执行一遍流程

当你在一次操作时,需要更新10个DOM节点,浏览器没这么智能,收到第一个更新DOM请求后,并不知道后续还有9次更新操作,因此会马上执行流程,最终执行10次流程

而通过VNode,同样更新10个DOM节点,虚拟DOM不会立即操作DOM,而是将这10次更新的diff内容保存到本地的一个js对象中,最终将这个js对象一次性attach到DOM树上,避免大量的无谓计算

很多人认为虚拟 DOM 最大的优势是 diff 算法,减少 JavaScript 操作真实 DOM 的带来的性能消耗。虽然这一个虚拟 DOM 带来的一个优势,但并不是全部。虚拟 DOM 最大的优势在于抽象了原本的渲染过程,实现了跨平台的能力,而不仅仅局限于浏览器的 DOM,可以是安卓和 IOS 的原生组件,可以是小程序,也可以是各种GUI

三、如何实现虚拟DOM

首先,我们要定义清楚什么是虚拟节点。一个虚拟节点可以是一个普通 JavaScript 对象,也可以是一个字符串。

我们定义一个函数 createNode 来创建虚拟节点。一个虚拟节点至少包含三个信息:

  • tag:保存虚拟节点的标签名,字符串
  • props:保存虚拟节点的 properties/attributes,普通对象
  • children:保存虚拟节点的子节点,数组

下面的代码是 createNode 实现样例:

const createNode = (tag, props, children) => ({ tag, props, children, });

我们通过 createNode 可以轻松的创建虚拟节点:

createNode('div', { id: 'app' }, ['Hello World']);
// 返回如下: 
{ tag: 'div', props: { id: 'app' }, children: ['Hello World'], }

现在,我们需要定义一个 createElement 函数来根据虚拟节点创建真实的 DOM 元素。

在 createElement 中,我们需要创建一个新的 DOM 元素,然后遍历虚拟节点的 props 属性,将其中的属性添加到 DOM 元素上去,之后再遍历 children 属性。如下代码是一个实现样例:

const createElement = vnode => {
    if (typof vnode === 'string') {
        return document.createTextNode(vnode); // 如果是字符串就直接返回文本元素
    }
    const el = document.createElement(vnode.tag);
    if (vnode.props) {
        Object.entries(vnode.props).forEach(([name, value]) => {
            el[name] = value;
        });
    }
    if (vnode.children) {
        vnode.children.forEach(child => {
            el.appendChild(createElement(child));
        });
    }
    return el;
}

现在,我们可以通过 createElement 将虚拟节点转变成真实 DOM 了。

createElement(createNode("div", { id: "app" }, ["Hello World"])); 
// 输出: <div id="app">Hello World</div>

我们再来定义一个 diff 函数来实现 'diff' 算法。这个 diff 函数接收三个参数,一个是已经存在的 DOM 元素,一个是旧的虚拟节点,一个是新的虚拟节点。在这个函数中,我们将对比两个虚拟节点,在需要的时候,将旧的元素替换掉。

const diff = (el, oldVNode, newVNode) => {
    const replace = () => el.replaceWith(createElement(newVNode));
    if (!newVNode) return el.remove();
    if (!oldVNode) return el.appendChild(createElement(newVNode));
    // 处理纯文本的情况
    if (typeof oldVNode === 'string' || typeof newVNode === 'string') {
        if (oldVNode !== newVNode) return replace();
    } else {
        // 对比标签名
        if (oldVNode.tag !== newVNode.tag) return replace();
        // 对比 props
        if (!oldVNode.props ? .some((prop) => oldVNode.props ? . [prop] === newVNode.props ? . [prop])) return replace();
        // 对比 children
        [...el.childNodes].forEach((child, i) => {
            diff(child, oldVNode.children ? . [i], newVNode.children ? . [i]);
        });
    }
}

在这个函数中,我们先处理纯文本的情况,如果新旧两个字符串不相同,则直接替换。之后,我们就可以假定两个虚拟节点都是对象了。我们先对比两个节点的标签名是否相同,不同则直接替换。之后对比两个节点的 props 是否相同,不同也直接替换。最后我们在递归的使用 diff 函数对比两个虚拟节点的 children。

至此,我们就实现了一个简版虚拟 DOM 系统所必须的所有功能。下面是使用样例:

const oldVNode = createNode("div", {
    id: "app"
}, ["Hello World"]);
const newVNode = createNode("div", {
    id: "app"
}, ["Goodbye World"]);
const el = createElement(oldVNode);
// <div id="app">Hello World</div>

diff(el, oldVNode, newVNode);
// el will become: <div id="app">Goodbye World</div>

template是如何渲染的

vue中的模板template无法被浏览器解析并渲染,因为这不属于浏览器的标准,不是正确的HTML语法,所有需要将template转化成一个JavaScript函数,这样浏览器就可以执行这一个函数并渲染出对应的HTML元素,就可以让视图跑起来了,这一个转化的过程,就成为模板编译。

模板编译又分三个阶段,解析parse,优化optimize,生成generate,最终生成可执行函数render。

parse阶段:用大量的正则表达式对template字符串进行解析,将标签、指令、属性等转化为抽象语法树AST。

optimize阶段:遍历AST,找到其中的一些静态节点并进行标记,方便在页面重渲染的时候进行diff比较时,直接跳过这一些静态节点,优化runtime的性能。

generate阶段:将最终的AST转化为render函数字符串。

v-modle实际上做了什么事情

如何理解v-model指令

v-model指令的本质是: 它负责监听用户的输入事件,从而更新数据,并对一些极端场景进行一些特殊处理。 同时,v-model会忽略所有表单元素的value、checked、selected特性的初始值,它总是将vue实例中的数据作为数据来源。 然后当输入事件发生时,实时更新vue实例中的数据。

实现原理

 <input v-bind:value="message" v-on:input="message = $event.target.value" />  
 //把input输入框的value属性值和vue实例的message属性进行绑定,同时监听输入事件。

用v-bind和v-on指令实现v-model

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <!--引入最新的vue稳定版本-->
    <script type="text/javascript" src="https://unpkg.com/vue/dist/vue.min.js"></script>
    <link rel="stylesheet" href="./css/style.css" type="text/css">
</head>
<body>


<!--input输入框-->
<div id="app">
    <!--把message字段的值作为input标签的value属性值,同时监听输入事件,实时更新message的值-->
    <input type="text" @input="handleInput($event)"  placeholder="请输入"  v-bind:value="message">
    <p>输入的内容是: {{message}}</p>
</div>

<script>
    var  vue=new Vue({
        el:'#app',
        data:{
            message:''
        },
        methods:{
            handleInput: function (event) {
                console.info("控制台打印event详情")
                console.info(event)
                console.info(event.toLocaleString());
                this.message=event.target.value;
            }
        }
    });
</script>
</body>
</html>

v-for的时候为什么要加key,有什么作用

key的作用:

  key是为了给Vue一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,需要为每项提供一个唯一的key属性。

通俗点讲 :三胞胎战成一排,你怎么知道谁是老大?

如果老大皮了一下子,和老三换了一下位置,你又如何区分出来?

给他们挂个牌牌,写上老大、老二、老三。

这样就不会认错了。key就是这个作用。

再举个小例子: key的作用很简单,就是为了复用。正是因为会复用,所以用index来做key会出现复用错的问题。

比如[10,11,12],对应key是0,1,2,如果我把11这项删了,就是[10,12],key是0,1,这是发现11对应的key和12对应的key都是1,既然相同,那就复用咯,然后不应该复用的。

vue中通讯方式有哪些

1. 组件通信方式大体有以下8种:

props

emit/emit/on

children/children/parent

attrs/attrs/listeners

ref

$root

eventbus

vuex

根据组件之间关系大概应用如下

1. 父子组件

props

emit/emit/on

parent/parent /children

ref

attrs/attrs / listeners

2. 兄弟组件

$parent eventbus vuex

3. 跨层级关系

provide/inject

$root

eventbus

vuex

在父组件中如果要获取子组件的dom结构,如何定义

1、使用this.$refs 获取dom元素

<div ref="box">abc</div>

获取上述div 中的文本
this.$refs.box.innerText   //输出abc
复制代码

2、使用this.$refs 获取子组件

//cus-form 自定义表单组件
//message 消息弹框组件

<div v-for="(item, index) in list">
    <cus-form ref="form"></cus-form>
</div>
<message ref="message"></message>

获取的时候发现
this.$refs.form   返回的是一个数组
this.$refs.message 返回的是一个对象
原因:ref本身dom或者父级dom 中,有通过"v-for" 循环渲染的dom场景,vue框架会将refInFor 设置为true,然后ref.key 在registerRef函数中就被设置成了数组。
复制代码

3、 在父组件中调用子组件的方法和属性 在第二点中,如果我们想要在父页面按钮点击的时候,对cus-form的表单内容进行校验,就需要调用表单内部的校验事件,写法如下:

CusForm.vue
<template>
...
</template>
<script>
export default {
    methods:{
        validForm(){
            ...
        }
    }
    
}
</script>

在父组件中调用
this.$refs.form[0].validForm();

v-if和v-show的区别

共同点:v-if 和 v-show 都能实现元素的显示隐藏

区别:

    1. v-show 只是简单的控制元素的 display 属性,而 v-if 才是条件渲染(条件为真,元素将会被渲染,条件为假,元素会被销毁);

    2.  v-show 有更高的首次渲染开销,而 v-if 的首次渲染开销要小的多;

    3. v-if 有更高的切换开销,v-show 切换开销小;

    4. v-if 有配套的 v-else-if 和 v-else,而 v-show 没有

    5. v-if 可以搭配 template 使用,而 v-show 不行

路由守卫是什么?

官方这样说

导航守卫主要用来通过跳转或取消的方式守卫导航。有多种机会植入路由导航过程中:全局的, 单个路由独享的, 或者组件级的。

导航守卫 | Vue Router​router.vuejs.org

简单的说,导航守卫就是路由跳转过程中的一些钩子函数。路由跳转是一个大的过程,这个大的过程分为跳转前中后等等细小的过程,在每一个过程中都有一函数,这个函数能让你操作一些其他的事儿的时机,这就是导航守卫。

2.导航守卫全解析

先看一个钩子函数执行后输出的顺序截图

image.png

导航守卫分为:全局的、单个路由独享的、组件内的三种。分别来看一下:

【全局的】:是指路由实例上直接操作的钩子函数,特点是所有路由配置的组件都会触发,直白点就是触发路由就会触发这些钩子函数,如下的写法。钩子函数按执行顺序包括beforeEach、beforeResolve、afterEach三个。

[beforeEach] :在路由跳转前触发,参数包括to,from,next(参数会单独介绍)三个,这个钩子作用主要是用于登录验证,也就是路由还没跳转提前告知,以免跳转了再通知就为时已晚。

const router = new VueRouter({ ... })

router.beforeEach((to, from, next) => {
  // ...
})

[beforeResolve] :这个钩子和beforeEach类似,也是路由跳转前触发,参数也是to,from,next三个,和beforeEach区别官方解释为:

区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用。

即在 beforeEach 和 组件内beforeRouteEnter 之后,afterEach之前调用。

[afterEach] :和beforeEach相反,它是在路由跳转完成后触发,参数包括to,from没有了next,它发生在beforeEach和beforeResolve之后,beforeRouteEnter(组件内守卫)之前。

【路由独享的】是指在单个路由配置的时候也可以设置的钩子函数,其位置就是下面示例中的位置,也就是像Foo这样的组件都存在这样的钩子函数。目前它只有一个钩子函数beforeEnter:

[beforeEnter] :和beforeEach完全相同,如果都设置则在beforeEach之后紧随执行,参数to、from、next

const router = new VueRouter({
  routes: [
    {
      path: '/foo',
      component: Foo,
      beforeEnter: (to, from, next) => {
        // ...
      }
    }
  ]
})

【组件内的】:是指在组件内执行的钩子函数,类似于组件内的生命周期,相当于为配置路由的组件添加的生命周期钩子函数。钩子函数按执行顺序包括beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave三个,执行位置如下

<template>
  ...
</template>
export default{
  data(){
    //...
  },
  beforeRouteEnter (to, from, next) {
    // 在渲染该组件的对应路由被 confirm 前调用
    // 不!能!获取组件实例 `this`
    // 因为当守卫执行前,组件实例还没被创建
  },
  beforeRouteUpdate (to, from, next) {
    // 在当前路由改变,但是该组件被复用时调用
    // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
    // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
    // 可以访问组件实例 `this`
  },
  beforeRouteLeave (to, from, next) {
    // 导航离开该组件的对应路由时调用
    // 可以访问组件实例 `this`
  }
}
<style>
  ...
</style>

[beforeRouteEnter] :路由进入之前调用,参数包括to,from,next。该钩子在全局守卫beforeEach和独享守卫beforeEnter之后,全局beforeResolve和全局afterEach之前调用,要注意的是该守卫内访问不到组件的实例,也就是this为undefined,也就是它在beforeCreate生命周期前触发。

在这个钩子函数中,可以通过传一个回调给 next来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数,可以在这个守卫中请求服务端获取数据,当成功获取并能进入路由时,调用next并在回调中通过 vm访问组件实例进行赋值等操作,(next中函数的调用在mounted之后:为了确保能对组件实例的完整访问)

 beforeRouteEnter (to, from, next) {
  // 这里还无法访问到组件实例,this === undefined
  next( vm => {
    // 通过 `vm` 访问组件实例
  })
}

[beforeRouteUpdate] (v 2.2+):在当前路由改变时,并且该组件被复用时调用,可以通过this访问实例。参数包括to,from,next。 what is 路由改变 or what is 组件被复用?

  • 对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,组件实例会被复用,该守卫会被调用
  • 当前路由query变更时,该守卫会被调用

[beforeRouteLeave] :导航离开该组件的对应路由时调用,可以访问组件实例this,参数包括to,from,next。

所有的钩子:

全局路由钩子: beforeEach(to,from, next)、 beforeResolve(to,from, next)、 afterEach(to,from);

独享路由钩子:beforeEnter(to,from, next);

组件内路由钩子:beforeRouteEnter(to,from, next)、beforeRouteUpdate(to,from, next)、beforeRouteLeave(to,from, next)

注意:afterEach钩子中参数没有next

3.导航守卫回调参数

to:目标路由对象;

from:即将要离开的路由对象;

next:它是最重要的一个参数,相当于佛珠的线,把一个一个珠子逐个串起来。以下注意点务必牢记:

1.但凡涉及到有next参数的钩子,必须调用next() 才能继续往下执行下一个钩子,否则路由跳转等会停止。

2.如果要中断当前的导航要调用next(false)。如果浏览器的 URL 改变了 (可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到from路由对应的地址。(主要用于登录验证不通过的处理)

3.当然next可以这样使用,next('/') 或者 next({ path: '/' }): 跳转到一个不同的地址。意思是当前的导航被中断,然后进行一个新的导航。可传递的参数与router.push中选项一致。

4.在beforeRouteEnter钩子中next((vm)=>{})内接收的回调函数参数为当前组件的实例vm,这个回调函数在生命周期mounted之后调用,也就是,他是所有导航守卫和生命周期函数最后执行的那个钩子。

5.next(error): (v2.4.0+) 如果传入 next 的参数是一个 Error 实例,则导航会被终止且该错误会被传递给 router.onError() 注册过的回调。

4.总结

顺序:

当点击切换路由时:beforeRouterLeave-->beforeEach-->beforeEnter-->beforeRouteEnter-->beforeResolve-->afterEach-->beforeCreate-->created-->beforeMount-->mounted-->beforeRouteEnter的next的回调