前端面试准备,长期更新~(欢迎评论与补充)

181 阅读17分钟

1.HTML5

1.1 html5新增了哪些特性?

  • 新增了几个语义化元素(nav、header、aside、footer等)
  • 新增了两个媒体元素(video和audio)
  • 等等

2.CSS3

2.1 圣杯/双飞翼布局

圣杯布局和双飞翼布局解决的问题是一样的,中间栏要在放在标准流前面以优先渲染。

双飞翼是圣杯的优化版

需求

  • 3列布局
  • 两侧定宽,中间自适应

结构

<div class="main">
    <div class="center">中间自适应</div>
    <div class="left">左列定宽</div>
    <div class="right">右列定宽</div>
  </div>

样式

.center {
      float: left;
      width: 100%;
      height: 500px;
      background: lightgray;
    }
    .left {
      float: left;
      width: 200px;
      height: 500px;
      background: lightpink;
    }
    .right {
      float: left;
      width: 300px;
      height: 500px;
      background: lightgreen; 
    }

由于中间栏占满父容器,所以左右两栏换行排

image-20220314164928756.png

如何使他们同一行并排?

.left {
      margin-left: -100%;
}
.right {
      margin-left: -300px;
}

参考文章www.cnblogs.com/2050/archiv…~

问题

“中间自适应”几个字不见了,这是因为 left 和 right 已经盖在了 center 上边。如果 center 中有更多内容,依然将无法显示。

圣杯布局

float+margin负值+父元素margin+相对定位

1、给三列的父元素(main), 加上 左margin 和 右margin(也可以使用 padding),将三列挤到中间来,这样左边和右边就会预留出位置。

.main { 
    margin-left: 200px;
    margin-right: 300px;
}

2、给 left 和 right 设置相对定位,将它们移动到相应的位置。

.left {
    position: relative;
    left: -200px;
}
.right {
    position: relative;
    right: -300px;
}

这时,你可以放大或缩小,看看中间栏是否自适应

image-20220314123222306.png

双飞翼

float+margin负值+子元素margin

1、给 center 加一个子元素 inner。

<div class="center">
     <div class="inner">中间自适应</div>
</div>

2、给 inner 设置 左margin 和 右 margin,将 inner 挤到中间显示。

.inner {
      /* 为了突出显示 inner 元素,给它加上了 height 和 border 以及 上margin */
      height: 300px;
      border: 1px solid red;
      margin-top: 10px;

      /* 以下是设置的 margin */
      margin-left: 200px;
      margin-right: 300px;
}

总结

圣杯布局双飞翼布局解决问题的方案在前一半是相同的:三栏全部float浮动,但左右两栏加上负margin让其跟中间栏div并排,以形成三栏布局

不同在于解决”中间栏div内容不被遮挡“问题的思路不一样:

1)圣杯布局为了中间div内容不被遮挡,将父容器设置了左右margin-left和margin-right后,将左右两个div用相对定位position: relative并分别配合right和left属性,以便左右两栏div移动后不遮挡中间div;

2)双飞翼布局为了中间div内容不被遮挡,直接在中间div内部创建子div用于放置内容,在子div里用margin-left和margin-right为左右两栏div留出位置。

2.2 flex布局

两个重要的概念

  • flex container(开启flex的元素)
  • flex items(直接子元素)

开启

display: flex;

flex-container上的css属性

属性-值默认
flex-derection(决定主轴)rowrow-reverscolumncolumn-reverse
justify-content(决定item在主轴上的对齐方式)flex-startflex-endcenterspace-betweenspace-evenlyspace-around
align-content(决定多行item在交叉轴上的对齐方式)类似
align-items(决定item在交叉轴上的对齐方式)normalstretchflex-startflex-endcenterbaseline
flex-wrapno-wrapwrap

flex items上的css属性

flex

是flex-grow || flex-shrink || flex-basis的简写

flex-grow

决定了flex items如何拓展

  • 默认值 0
  • flex containermain axis方向上有剩余size时,flex-grow属性才会有效
  • flex-grow总和大于等于1时,等比拓展(加上乘以size的值,用完剩余size);小于1时,各自乘以小数(有剩余size)

flex-shrink

决定了flex items如何收缩

  • 默认 1
  • 当flex items在main axis方向上超过了flex containersize时(oversize),flex-grow属性才会有效
  • 当flex items 的**flex-shrink总和大于1时,**等比收缩(减去乘以oversize的值);小于1时就用得少,因为还是溢出

flex-basis

决定flex itemsmain axis主轴上的base size

  • auto 自身宽/高
  • 具体值

决定flex items在main axis主轴上的base size的优先级

  • max-size...
  • flex-base
  • size
  • 内容本身的size

order

决定flex items的排布顺序,值越,排越

  • 默认是0

flex实现圣杯布局

思路

  1. header、content、footer垂直flex,其中header、footer高度固定,content高度flex:1高度自适应
  2. content里面的nav、main、aside水平flex,其中nav、aside宽度固定,flex的收缩和拓展属性设为0,main的宽度flex:1宽度自适应
  3. 最后别忘了nav的flex-order:-1,将它排在main前面

html

<header>
    header
  </header>
  <div class="content">
    <main>main</main>
    <nav>nav</nav>
    <aside>aside</aside>
  </div>
  <footer>
    footer
  </footer>

css

html,
    body {
      display: flex;
      flex-direction: column;
      min-height: 600px;
      height: 100%;
      font-size: 28px;
      font-weight: bolder;
    }
    header,
    footer {
      height: 150px;
      background-color: #666;
      /* 子元素水平、垂直居中三件套 */
      display: flex;
      justify-content: center;
      align-items: center;
    }
    .content {
      flex: 1; /* 高度自适应 */
      display: flex;
    }
    nav,
    aside {
      background-color: #eb6f43;
      flex: 0 0 200px;
      display: flex;
      justify-content: center;
      align-items: center;
    }
    main {
      display: flex;
      justify-content: center;
      align-items: center;
      flex: 1; /* 宽度自适应 */
      background-color: #d6d6d6;
    }
    nav {
      order: -1; /* 调整顺序在main前面 */
    }

2.3 定位

2.4 清除浮动

为什么要清除浮动?

真实开发父元素高度不确定的(auto),但是浮动会使子元素脱标不再向父元素汇报高度,这时父元素计算总高度时,就不会计算浮动子元素的高度,导致了高度坍塌问题

而解决父元素高度坍塌问题的过程,叫清除浮动

如何清除浮动?(推荐方案)

伪元素

.clear-fix::after {
    content: "";
    clear: both;
    display: block;
}

给需要的元素加上clear-fix类即可

2.5 margin负值

2.6 你怎么理解盒模型的?

盒模型分两种:标准盒模型IE盒模型(替代盒模型、怪异盒模型)

标准盒模型:如果你给盒子设置widthheight,设置的是content,盒子的实际宽高是border+padding+content

IE盒模型:如果给盒子设置widthheight,设置的是 border+padding+content

默认使用的是标准盒模型,如果想使用IE盒模型,使用 box-sizing: border-box 切换

2.7 常见的水平和垂直居中方式有哪些?

方式一,flex布局

<div class="father">
    <div class="son"></div>
  </div>
.father {
      display: flex;
      justify-content: center;
      align-items: center;
      width: 100px;
      height: 100px;
      background-color: skyblue;
    }
    .son {
      width: 50px;
      height: 50px;
      background-color: grey;
    }

image-20220406160600654.png

方式二,父相子绝+translate

先将元素的左上角定位到父元素中间,然后再将子元素的中心点移动到父元素的中间

<div class="father">
    <div class="son"></div>
  </div>
.father {
      position: relative;
      width: 100px;
      height: 100px;
      background-color: skyblue;
    }
    .son {
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      width: 50px;
      height: 50px;
      background-color: grey;
    }

image-20220406160600654.png

3.JavaScript

3.1 作用域、作用域链、执行上下文

作用域(Scope)

可以理解为变量、函数、对象的可访问范围

3.2 什么是原型?什么是原型链?

原型

js中,原型分隐式原型显式原型

每个对象都有一个隐式原型__proto__

每个函数都有一个显式原型prototype,而js函数又特殊,也是个对象,所以也有隐式原型__proto__,但是这两个不相等。(指向的对象不一样)

  • 函数的prototype来自哪里?创建了一个函数 foo.prototype = {constructor: foo }
  • 函数的**__proto__来自哪里?** new Function(), foo.__proto__ = Function.prototype, 而 Function.prototype = { constructor: Function }

原型链

从一个对象上获取属性,如果在当前对象中没有获取到就会去它的原型上面获取,如果它的原型上还没有,它原型本质是个对象,也有对应的原型,就再去它原型的原型上找,以此类推,沿着原型一层一层往上找,像链条一样

3.3 箭头函数和普通函数有什么区别?

  • 箭头函数不会绑定this、arguments属性

  • 箭头函数不能作为构造函数来使用(不能和new关键字一起使用)

  • 箭头函数不绑定this,而是根据外层作用域来决定this

3.4 New操作符做了什么事情?

js中,使用new的时候,是调用了某个构造函数

  1. 在构造函数内部新建一个空对象obj
  2. 构造函数的显式原型prototype赋值给刚刚创建的空对象obj的隐式原型 __proto__
  3. 构造函数内部的this,会指向创建出来的新对象obj
  4. 然后执行构造函数的代码
  5. 如果该函数没有返回对象,则返回this

3.5 说一下eventloop(事件循环)

先说说浏览器的事件循环吧

有个前提,js是单线程的

参与事件循环有3个角色:

  • js线程
  • 其它线程
  • 事件队列
  1. js线程执行js代码
  2. 当发现耗时操作时,会将这操作(会有回调函数)交给其它线程处理
  3. 其它线程处理完,会将回调函数放到事件队列
  4. js线程会定时地来事件队列执行那些回调函数

这3个角色形成一个闭环,不停地循环着这过程,所以叫事件循环

node的事件循环原理大同小异,不过比浏览器多了一些阶段,对事件队列的划分更加详细,暂时就了解这么多~

3.6 什么是闭包,闭包的应用场景是什么

广义上js中所有函数都是闭包可以访问外层作用域的自由变量)

狭义上,js中的函数如果访问了外层作用域的变量(访问了),那这函数就是一个闭包

闭包是一种保护私有变量的机制,在函数执行时形成私有的作用域,保护里面的私有变量不受外界干扰。

for (let i = 0; i < 5; i++) {
  (function p(){
    console.log(i)
  })()
}

闭包应用场景:

  • 一个函数返回另外一个对外层作用域存在引用的函数
  • IIFE(立即执行函数
  • 循环赋值独立的计数器(闭包可以形成互不干扰的私有作用域)
  • 用闭包可以模拟私有方法(无法在外部直接访问,必须通过内部返回的函数访问,也就是模块模式

3.7 Set 和 Map有什么区别?

  • Map是键值对,Set是值的集合
  • Map有get(key)方法,而set只有值,没有get(key)方法
  • Set更多用于数组去重,而Map更多用于存储数据

3.8 localstorage sessionstorage cookie 有什么区别

cookiesessionStoragelocalStorage
生命周期会话级,如果不设置有效期,存储在内存中;如果设置有效期,存在硬盘里,有效期到自动消失~会话级永久,除非主动删除
网络流量每次都会发送给服务器不会与服务器通信,纯粹为了保存数据,所以webStorage更节省网络流量同样
大小限制4kb5M5M
安全性明文传输WebStorage不会随着HTTP header发送到服务器端,所以安全性相对于cookie来说比较高一些,不会担心截获同样
方便?WebStorage提供了一些方法,数据操作比cookie方便同样

3.9 深拷贝和浅拷贝

浅拷贝是创建一个新对象,这个对象有着原始对象属性值的拷贝。如果属性是基本类型,拷贝的就是基本类型的,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。(两个对象依然共享引用类型属性的内存)

深拷贝是从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象,(两个对象不再共享引用类型属性的内存)

如何实现浅拷贝?

  • 展开运算符

如何实现深拷贝?

  • JSON的序列化(stringify)和解析(parse),但是这不会对函数进行处理~

4.计算机网络

4.1 post和get的区别

参数角度

  • GET参数通过URL传递,POST放在Request body
  • GET请求在URL中传送的参数是有长度限制的,而POST没有。(服务器处理长 URL 要消耗比较多的资源,为了性能和安全考虑,会给 URL 长度加限制)

安全角度

  • GET比POST不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。
  • GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。

然而,从传输的角度来说,他们都是不安全的,因为 HTTP 在网络上是明文传输的,只要在网络节点上捉包,就能完整地获取数据报文

只有使用HTTPS才能加密安全

请求方面

  • GET在浏览器回退时是无害的,而POST会再次提交请求。

4.2 浏览器输入url到页面加载完毕发生了什么

1.启动浏览器网络线程

2.根据是否有缓存,浏览器进行自己的请求规则,决定是否发出请求

3.dns域名解析(查询)

4.tcp建立连接

5.接到数据,开始页面渲染,启动浏览器渲染引擎(渲染引擎与js引擎互斥,只能运行一个)

第5步还分以下步骤:

1.解析HTML,构建DOM树

2.构建CSS树

3.解析HTML过程中如果遇到script元素,要停止渲染,启动js引擎执行script中的代码,如果是src格式,要启动网络线程,加载js脚本,加载完毕开始执行js脚本

4.当DOM树构建完毕,css树也构建完毕时,浏览器进入layout阶段

5.合成渲染树render-tree

6.下一步进入painting阶段,页面绘制

7.绘制完毕,结束

4.3 UDP和TCP有什么区别

UDPTCP
连接?无连接面向连接
可靠?不可靠,不能流量控制和拥塞控制可靠
连接对象支持一对一、一对多和多对多只能一对一
传输方式面向报文面向字节流
首部开销开销少,8字节最小20字节,最大60字节
适用场景实时应用,比如视频会议、直播可靠传输应用,比如文件传输

4.4 怎么解决跨域问题?

为什么会出现跨域?

浏览器从一个域名的网页去请求另一个域名的资源时,根据同源策略 协议、主机名、端口号任一不同,就出现了跨域

同源政策是什么?

两个域名的协议、主机名、端口号任一不同,都不能发送请求~

为什么需要同源策略?

购物网站、银行卡、sessionStorage、危险网站,剩下脑补~

怎么解决?

方案一,CORS

Cross-origin resource sharing,跨域资源共享

跨域请求的请求头需要设置

Access-Control-Allow-:

  • Origin 为发起请求的主机地址
  • Credentials 当它被设置为 true 时,允许跨域
  • Headers 设置跨域请求允许的请求头
  • Methods 设置跨域请求允许的请求方式

一般使用第三方包~

方案二,jsonp

只支持get请求~

JSONP的优势在于支持老式浏览器~,对于不支持CORS浏览器可以使用JSONP

  • 接口参数要带一个自定义函数名
  • 通过该函数名去接收后台返回数据
var script = document.createElement("script"); 
script.src = "HTTP://127.0.0.1:8888/index.php?callback=jsonpCallback"; 

document.head.appendChild(script); 
//通过定义函数名去接收后台返回数据 
function jsonpCallback(data){ 
//注意 jsonp 返回的数据是 json 对象可以直接使用 //Ajax 取得数据是 json 字符串需要转换成 json 对象才可以使用。 
}

方案三,反向代理

webpack.config.js

module.exports = {
  //...
  devServer: {
    proxy: {
      '/api': 'http://localhost:3000',
    },
  },
};

现在,对 /api/users 的请求会将请求代理到 http://localhost:3000/api/users

5.算法

6.框架

Vue

6.1 vue实现双向数据绑定原理是什么?

双向数据绑定的的由来

对于传统的dom操作,当数据变化更新视图需要先获取到目标节点,然后将改变后的值放入节点中,视图发生变化时,需要绑定事件修改数据

Vue实现双向数据绑定是采用数据劫持和发布者-订阅者模式

核心组成部分:

  1. 监听器Observer数据劫持
  2. 订阅者容器: 监听器监听到数据变动时,遍历订阅者容器发布消息
  3. Compile:解析模板指令,将模板中的变量替换成数据,比如{{title}}
  4. Watcher: 连接ObserveCompile的桥梁

vue2

数据劫持是利用ES5的Object.defineProperty方法来劫持每个属性的getter和setter,在数据变动时发布消息给订阅者

Object.defineProperty有一些缺陷,不仅要遍历data逐个劫持,还不能监听到数组的改变~

vue3

Vue3的数据劫持使用了ES6的Proxy-Reflect,(Proxy的监听数组实现是把数组变成了一个类数组对象),

为什么使用Proxy?

首先,Object.defineProperty设计的初衷不是为了监听一个对象中所有属性的,而是定义普通的属性

其次,Object.defineProperty要是想监听其它操作,如新增、删除属性,它做不到

Proxy可以监听原对象进行了哪些操作

reflect有什么用呢?

提供很多操作js对象的方法,有点像Object操作对象的方法

已经有Object了,为什么还要reflect呢?

  • 早期ECMA规范没有考虑到对对象本身的一些操作如何设计更加规范,所以将这些api都放到Object上
  • 但是Object作为构造函数,这些操作放在它身上并不合适
  • 所以,es6新增了reflect对象,将这些操作集中到reflect身上

6.2 v-model语法糖是怎么实现的?

v-model的原理就是v-bind数据绑定 和 v-on监听事件的语法糖

作用在普通表单元素上

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

//event指代当前触发的事件对象;//event 指代当前触发的事件对象; //event.target 指代当前触发的事件对象的dom; //$event.target.value 就是当前dom的value值;

  • 「接收一个value属性」
  • 「在value值改变时 触发xxx事件」

在自定义组件中

v-model 默认会利用名为 valueprop 和名为 input 的事件

6.3 vue-router如何实现?

为什么会出现前端路由?

因为后端路由有一个很大的缺点,每次路由切换的时候都需要去刷新页面,然后发出ajax请求,然后将请求数据返回

所以我们的需求而是更新视图而不发生请求。这需要通过hash值来实现。而hash 值的变化,并不会导致浏览器向服务器发出请求,所以不会刷新页面,另外每次 hash 值的变化,还会触发hashchange 事件,通过这个事件我们就可以知道 hash 值发生了哪些变化。(hash 出现在 URL 中,但不会被包含在 http 请求中)

6.4 hash/history的区别?

用户体验角度

  • hash模式会在url中自带#
  • history 模式则不会带#(但是这需要服务器把所有路由都重定向到根页面)

改变路径方式

  • 通过改变location.hash
  • 而history通过pushState()和replaceState()

6.5 Vuex有哪些基本属性 怎么使用?

state

存放所有共享的状态

getters

基于state的派生属性,类似于计算属性,当需要计算后在使用时就可以使用getters

mutations

提交mutation是更改Vuex中的store中的状态的唯一方法

mutation必须是同步的,如果要异步需要使用action

const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    //无提交荷载
    increment(state) {
        state.count++
    }
    //提交荷载
    incrementN(state, obj) {
      state.count += obj.n
    }
  }
})

//无提交荷载
store.commit('increment')
//提交荷载
store.commit('incrementN', {
    n: 100
    })

Action 类似于 mutation,不同在于:

  • Action 提交的是 mutation,而不是直接变更状态。
  • Action 可以包含任意异步操作。
const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    incrementA (context) {
      setInterval(function(){
        context.commit('increment')
      }, 1000)
    }
  }
})

Action 通过 store.dispatch 方法触发

store.dispatch('incrementA')

Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象

因此你可以调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters。

modules

使用单一状态树,导致应用的所有状态集中到一个很大的对象。但是,当应用变得很大时,store 对象会变得臃肿不堪。

为了解决以上问题,Vuex 允许我们将 store 分割到模块(module)。每个模块拥有自己的 state、mutation、action、getters

const moduleA = {
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: { ... },
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

6.6 计算属性和watch有什么区别?以及它们的运用场景?

来认识一下计算属性的缓存妙处~

<div id="example">
  <p>"{{ message }}"</p>
  <p>"{{ reversedMessage }}"</p>
</div>
var vm = new Vue({
  el: '#example',
  data: {
    message: 'Hello'
  },
  computed: {
    // 计算属性的 getter
    reversedMessage: function () {
      // `this` 指向 vm 实例
      return this.message.split('').reverse().join('')
    }
  }
})

当然,使用methods结果也是一样

methods: {
  reversedMessage: function () {
    return this.message.split('').reverse().join('')
  }
}

但是,计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。这就意味着只要 message 还没有发生改变,多次访问 reversedMessage 计算属性会立即返回之前的计算结果,而不必再次执行函数

为什么需要缓存?

假设我们有一个性能开销比较大的计算属性 A,它需要遍历一个巨大的数组并做大量的计算。然后我们可能有其他的计算属性依赖于 A。如果没有缓存,我们将不可避免的多次执行 A 的 getter!

当然,如果你不希望有缓存,也可以使用方法来替代~

区别

  • computed支持缓存,watch不支持~

  • 计算属性是声明式编程,而watch是命令式编程,而且可能有很多重复代码

运用场景

  • 当数据需要经过处理再显示时,使用计算属性computed~
  • 当需要在数据变化时执行异步开销较大的操作时,使用watch~

6.7 你知道组件之间的传值方式有哪些?

props父传子
$emit/v-on子传父
EventBus非父子可以使用

props

父组件fpn

<div id="fpn">
    <spn :s-msg="fMsg"></spn>
  </div>
data: {
    fMsg: '父亲的数据'
  }

子组件spn

<div>
   <h2>{{ sMsg }}</h2>
</div>
props: {
	sMsg: {
	  type: String,
	  default: 'hhh'
	}

父组件的fMsg通过props传给子组件的sMsg

HTML 中的 attribute 名是大小写不敏感的,浏览器会把所有大写字符解释为小写字符,并在前面加个横线分隔(js中是sMsg,在html中就变s-msg啦~),组件名注册时大写使用时可以小写横线分隔就是这个原因~

$emit/v-on

子组件spn

<div>
  <button @click="spn"></button>
</div>
data () {
  return {
    list: [
      {id: 1, name: '热门'}
    ]
  }
},
methods: {
  spn () {
    // 发射事件
    this.$emit('spnClick',this.list)
  }
}  

父组件fpn

@是v-on的语法糖~

<div id="fpn">
  <spn @spnClick="fpn"></spn>
</div>
methods: {
  fpn (value) {
      console.log(value)
  }
}

子组件通过点击,将自定义事件spnClick发射出去~,并且可以携带一些信息(this.list)一起发射;

当父组件fpn使用子组件spn时,要是监听到子组件的点击,可以绑定一个处理函数,该处理函数的参数可以拿到子组件传递过来的信息(spn的list)。

EventBus

1.初始化

第一种方式

将一个空的vue对象挂载到Vue原型上,这样每个组件对象都可以使用~

Vue.prototype.$EventBus = new Vue()

第二种方式

创建一个模块Bus.js,导出一个空的vue对象,需要就导入

// Bus.js
import Vue from 'vue'
export const EventBus = new Vue();

实质上,它是一个不具备 DOM 的组件,它具有的仅仅只是组件的实例方法而已,因此它非常的轻便

2.发送和接收事件

  • EventBus.$emit('emit事件名',数据)发送
  • EventBus.$on("emit事件名", callback(payload1,…)) 接收

举例导入Bus.js模块的方式通过事件总线传递信息

<!-- A.vue -->
<template>
    <p>{{msgB}}</p>
    <button @click="sendMsgA()">-</button>
</template>

<script> 
import { EventBus } from "../Bus.js";
export default {
    data(){
        return {
        msg: ''
        }
    },
    mounted() {
        EventBus.$on("bMsg", (msg) => {
            // a组件接受 b发送来的消息
            this.msg = msg;
        });
    },
    methods: {
        sendMsgA() {
            EventBus.$emit("aMsg", '来自A页面的消息'); // a 发送数据
        }
    }
}; 
</script>


<!-- B.vue -->
<template>
  <p>{{msgA}}</p>
    <button @click="sendMsgB()">-</button>
</template>

<script> 
import { EventBus } from "../event-bus.js";
export default {
    data(){
        return {
        msg: ''
        }
    },
    mounted() {
        EventBus.$on("aMsg", (msg) => {
            // b组件接受 a发送来的消息
            this.msg = msg;
        });
    },
    methods: {
        sendMsgB() {
            EventBus.$emit("bMsg", '来自b页面的消息'); // b发送数据
        }
    }
};
</script>

如果只想接收一次,可以使用EventBus.$once('事件名', callback(payload1,…)

优缺点

优点

  • 解决了多层组件之间繁琐的事件传播。
  • 使用原理十分简单,代码量少。

缺点

  • vue是单页面应用,如果在某一个页面刷新了之后,与之相关的EventBus会被移除,这样可能出现一下意外bug
  • 如果有反复操作的页面,EventBus在监听的时候就会触发很多次,也是一个非常大的隐患。通常会用到,在vue页面销毁时,同时移除EventBus事件监听。
  • 由于是都使用一个Vue实例,所以容易出现重复触发的情景,两个页面都定义了同一个事件名,并且没有用$off销毁(常出现在路由切换时)。

6.8 父组件到子组件更新的方式是什么样的?

prop使得其父子 prop 之间形成了一个单向下行绑定父级 prop 的更新会向下流动到子组件中,但是反过来则不行。

这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。

每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部直接改变 prop。

那要是想变更prop呢?

通过发射事件去通知父组件修改

两种常见的试图变更一个 prop 的情形:

这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用

最好定义一个本地的 data property 并将这个 prop 用作其初始值(仅限于基本类型)

props: ['initialCounter'],
data () {
  return {
    counter: this.initialCounter
  }
}

这个 prop 以一种原始的值传入且需要进行转换。最好依赖于这个 prop 的值来派生一个计算属性

props: ['size'],
computed: {
  normalizedSize: function () {
    return this.size.trim().toLowerCase()
  }
}

注意

在 JavaScript 中对象和数组是通过引用传入的,所以对于一个数组或对象类型的 prop 来说,在子组件中改变变更这个对象或数组本身将会影响到父组件的状态。

6.9 vue的生命周期?

lifecycle.png

vue的生命周期怎么理解?

每个 Vue 实例创建到销毁的过程叫vue的生命周期。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。

vue有哪些生命周期钩子?

钩子调用时机工作内容注意
beforeCreate实例初始化之后,同步调用。数据侦听事件/侦听器配置
created实例创建完成后,立即同步调用。计算属性方法事件/侦听器的回调函数$el property 目前尚不可用。
beforeMount挂载开始之前调用相关的 render 函数首次被调用该钩子在服务器端渲染期间不被调用
mounted实例被挂载后调用el 被新创建的 vm.el替换了。如果根实例挂载到了一个文档内的元素上,当mounted被调用时vm.el **替换**了。如果根实例挂载到了一个文档内的元素上,当 mounted 被调用时 vm.el 也在文档内。1.不会保证所有的子组件也都被挂载完成。如果你希望等到整个视图都渲染完毕再执行某些操作,可以在 mounted 内部使用
2.vm.$nextTick;该钩子在服务器端渲染期间不被调用
beforeUpdate数据发生改变后,DOM 被更新之前调用。这里适合在现有 DOM 将要被更新之前访问它,比如移除手动添加的事件监听器。该钩子在服务器端渲染期间不被调用,因为只有初次渲染会在服务器端进行
updated数据更改导致的虚拟 DOM 重新渲染和更新完毕之后调用当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态。如果要相应状态改变,通常最好使用计算属性watcher 取而代之1.不会保证所有的子组件也都被重新渲染完毕。如果你希望等到整个视图都渲染完毕,可以在 updated 里使用 vm.$nextTick
2.该钩子在服务器端渲染期间不被调用
activatedkeep-alive 缓存的组件激活时调用。该钩子在服务器端渲染期间不被调用
deactivatedkeep-alive 缓存的组件失活时调用。该钩子在服务器端渲染期间不被调用
beforeDestroy实例销毁之前调用,在这一步,实例仍然完全可用该钩子在服务器端渲染期间不被调用
destroyed实例销毁后调用。对应 Vue 实例的所有指令都被解绑,所有的事件监听器被移除,所有的子实例也都被销毁。该钩子在服务器端渲染期间不被调用。

6.10 你了解nextTick多少?

在此之前,需要聊聊

什么是异步更新队列?

Vue 在更新 DOM 时是异步执行的。

只要侦听到数据变化,Vue 将开启一个队列,并缓冲同一事件循环中发生的所有数据变更。

如果同一个 watcher 被多次触发,只会被推入到队列中一次

在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。

在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。

Vue 在内部对异步队列尝试使用原生的 Promise.thenMutationObserversetImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

例如,当你设置 vm.someData = 'new value',该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。

要是想基于更新后的 DOM 状态来做点什么呢?

那就使用nextTick吧!这样回调函数将在 DOM 更新完成后被调用。

$nextTick() 返回一个 Promise 对象哦~

6.11 keep-alive是什么?

是一个抽象组件。(它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。)

包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。

当组件在 <keep-alive> 内被切换,该组件的的 activateddeactivated 这两个生命周期钩子函数将会被对应执行。

应用场景

当需要保存组件的某些状态(如滚动位置)下一次返回就恢复那些状态时,就不能销毁组件,而是将这些状态缓存起来,这时就可以使用keep-alive

6.12 你怎么使用插槽?

插槽有个非常棒的的功能:

复用组件的前提下,你还可以动态的拓展该组件。

<navigation-link> 的模板

<navigation-link url="/profile">
  <slot>123</slot>
</navigation-link>

复用

<navigation-link url="/profile">
  Your Profile
</navigation-link>

当组件渲染的时候,<slot></slot> 将会被替换为“Your Profile”。插槽内可以包含任何模板代码,当没内容替换插槽的内容时,会默认使用插槽的模板代码(123)

<navigation-link url="/profile">
  Your Profile
</navigation-link>

这个又叫匿名插槽

这时你可能会想:一个插槽不够用啊~

你放心,可以使用多个插槽

这时你可能又会问:那多个插槽之间怎么区分使用的哪一个?

起个名字呗

这就是具名插槽的由来~

具名插槽的使用

  • 定义时 slot元素加name="xxx"属性
  • 使用时 替换元素加 v-slot="xxx" 属性

基础模板

<base-layout> 组件

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

拓展

在向具名插槽提供内容的时候,可以在一个 <template> 元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称

<template> 元素可以换成任何html元素~)

<base-layout>
  <template v-slot:header>
    <h1>Here might be a page title</h1>
  </template>

  <p>A paragraph for the main content.</p>
  <p>And another one.</p>

  <template v-slot:footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>

最终会被渲染出

<div class="container">
  <header>
    <h1>Here might be a page title</h1>
  </header>
  <main>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </main>
  <footer>
    <p>Here's some contact info</p>
  </footer>
</div>

你可以理解为对号入座式拓展,有了名字,拓展起来的不会乱啦~

6.13 你怎么使用自定义指令的

除了核心功能默认内置的指令 (v-modelv-show),Vue 也允许注册自定义指令。

有的情况下,仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。

全局注册一个v-focus指令

Vue.directive('focus', {
  // 当被绑定的元素插入到 DOM 中时……
  inserted: function (el) {
    // 聚焦元素
    el.focus()
  }
})

局部注册也可以

directives: {
  focus: {
    // 指令的定义
    inserted: function (el) {
      el.focus()
    }
  }
}

然后你可以在template中的元素上使用了

<input v-focus>

7.webpack

7.1 Loader和Plugin 有什么区别

Webpack将一切文件视为模块,但是webpack原生是只能解析js文件,如果想将其他文件也打包的话,就会用到loader

Loader的作用是让webpack拥有了加载和解析非JavaScript文件的能力。

Plugin可以扩展webpack的功能,让webpack具有更多的灵活性。 在 Webpack 运行的生命周期中会广播出许多事件Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

8.性能

8.1 项目中常用的性能优化方式是什么?

  • 减少HTTP请求
  • 静态资源使用 CDN托管
  • 将样式表放在顶部,将脚本放在底部
  • 善用缓存,不重复加载相同的资源
  • 图片懒加载,滑动到可视区域再加载
  • webpack 按需加载代码,提取第三库代码,减少 ES6 转为 ES5 的冗余代码
  • 压缩代码(利用插件js丑化、html和css也有相应的压缩插件,还有就是gzip)
  • 等等

参考:segmentfault.com/a/119000002…

8.2 你知道重排和重绘吗?

image-20220329161823288.png

先说说重排(relayout)

构建渲染树后,开始布局。

浏览器中的布局是什么?

第一次确定节点大小和位置称为布局

随后对节点大小和位置的重新计算回流,也叫重排

比如,假设初始布局发生在返回图像之前。由于没有声明图像的大小,一旦知道图像大小,就会有回流

再说重绘(repaint)。

在元素的绘制阶段,会将元素的每个可视部分绘制到屏幕上,包括文本、颜色、边框、阴影和替换的元素(如按钮和图像)。

当元素的可视部分发生更新,将会重新绘制,这个过程便是重绘

网页生成的时候,至少会渲染一次,而在用户访问的过程中,还会不断重复触发重排和重绘,重排非常浏览器消耗性能,所以开发中尽量少触发重排~

当你修改元素的可视部分,不会影响元素的布局,但是,但你修改元素布局,在绘制阶段会重新绘制该元素。也就是说,重绘不一定导致重排,但重排一定会导致重绘

如何减少重排?

  • 样式集中改变
  • 改类名而不是直接修改样式
  • 让元素脱离文档流,减少重排的Render Tree的规模

9.安全

10.适配

10.1 rem你是怎么做适配的?

  1. 根据不同设备屏幕,设置不同html的font-size大小
  2. 将所有需要适配的图片、元素、字体大小的单位,统一使用rem

第1步可以使用媒体查询js动态计算(用的最多)

但是px值转换rem太过于复杂,可以使用less(强大的计算能力)解决这一问题