高频面试题集锦

interview

HTML&CSS

浏览器标准模式和怪异模式之间的区别

标准模式怪异模式
盒模型标准盒子模型(实际宽度为设置的width+padding+border+margin)IE盒模型(实际宽度为设置的width+margin)
图片元素的垂直对齐方式inline元素和table-cell元素,vertical-align的默认取值为baselinetable单元格中的的图片的vertical-align属性为bottom,因此图片底部有几像素的空间
<table>元素中的字体font的属性都是可以继承的对于table元素,字体的某些属性是不会从body等封装元素中得到继承,特别是font-size
元素百分比高度高度取决于内容变化百分比高度都正确使用

行内元素/块级元素/空元素

行内元素

  • span

  • a

  • img

  • b/strong

  • i/em

  • small/big

  • input

  • select

块级元素

  • div

  • p

  • table

  • ul/ol/li

  • h1-h6

空元素

  • br

  • hr

  • img

  • input

  • link

  • meta

介绍一下你对浏览器内核的理解

浏览器主要分为两个部分:渲染引擎和JS引擎

渲染引擎主要负责渲染静态页面

JS引擎主要负责解析和执行JS实现页面的动态效果

Flex布局

flex其实是flexible box的缩写,也就是弹性布局,这种布局给盒子模型提供了很大的灵活性.容器的主要属性有flex-direction,用来设置主轴的排列方向,flex-wrap用来设置换行.justify-content用来设置主轴的对齐方式,align-items用来设置纵轴的对齐方式,项目主要用到的属性就是flex.通过将自适应高度的盒子设置为flex:1

实现一个左侧宽度固定,右侧自适应的布局

  1. 利用左侧浮动元素脱离文档流,右侧元素会直接向父级元素左侧靠拢,然后再设置左侧的边距把内容推出来

  2. 父元素利用弹性布局,子元素中将左侧设置为固定宽度,右侧flex属性设置为1

  3. 利用BFC,因为BFC不能与浮动元素重叠,可以将左侧元素设置为左浮动,然后通过overflow:hidden让右侧盒子形成一个BFC.

实现一个两侧宽度固定,中间自适应布局

  1. 弹性布局,左右固定宽度,中间flex:1

  2. 浮动,左右元素浮动为固定宽度,中间元素放到最后

  3. 利用左右两侧都固定定位,中间设置左右margin为左右的width

水平垂直居中

垂直改变top或者bottom的值

水平改变left或者right的值

1.

.box1{
            width: 100px;
            height: 100px;
            background-color: pink;
            position: absolute;
            top: 50%;
            left: 50%;
            margin-left: -50px;
            margin-top: -50px;
        }
这种方式只是适用于已知盒子大小的情况

2.

.box2{
            width: 200px;
            height: 200px;
            background-color: blue;
            position: absolute;
            top: 0;
            right: 0;
            bottom: 0;
            left: 0;
            margin: auto;
        }
是用绝对定位和margin:auto.

3.

.box3{
            width: 300px;
            height: 300px;
            background-color: green;
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%,-50%);
        }
       
利用绝对定位和transform

4.

弹性布局,弹性布局有两种实现方式
给父盒子设置display:flex;和align-items:center
还有一种就是给父盒子设置display:flex;flex-direction:colunm;justify-content:center;

清除浮动

浮动产生的原因:子元素脱离了文档流,父元素没有了高度.

清除浮动主要有三种方法

1.在浮动元素的后面使用一个空元素<div> 或者 ,给他们设置clear:both;属性即可清除浮动

2.给浮动元素的父元素添加overflow:hidden属性

3.使用伪元素:after.给浮动元素的父元素添加一个clearfix的类名,然后给这个类名添加一个伪元素:after设置一下属性{content:'';height:0;display:block;clear:both;}

BFC

什么是BFC

BFC其实就是一个容器,然后容器的内部元素不能影响外部元素,反之也成立

BFC的布局规则

  • 所有的块级元素在垂直方向上从左排列,左边距紧挨着父元素,独占一行

  • 同一个BFC两个相邻的块级元素设置同一方向的margin会发生margin塌陷

  • BFC的区域不会与浮动元素重合

  • 计算BFC的高度时,浮动元素也参与计算

  • BFC内部的子元素不能影响到外部元素,反之也成立

如何创建一个BFC

  • Float的值不为none

  • position的值不为static和relative

  • display的值是inline-block,flex,inline-flex,table-caption,table-cell

  • overflow的值不是Visible

BFC的作用

  1. 利用margin可以避免margin塌陷

    • 让其中的一个元素形成一个独立的BFC

    • 在一个元素身上设置足够的margin

  2. 清除浮动

    overflow:hidden

什么是FOUC,如何避免

FOUC就是无样式内容闪烁,是因为在IE中通过@import导入css文件引起的

因为通过@import导入的css样式是在整个HTML文档DOM加载完毕之后才会开始加载css样式,我们可以将引入css的方法改成通过link引入就好了,因为通过link标签引入css样式是和HTML文档DOM一起加载

页面导入样式,link和@import的区别

  1. link属于XHTML标签,@import完全是CSS提供的一种方式.link标签除了可以加载CSS以外,还可以定义RSS,定义rel连接属性等,@import只能加载CSS

  2. 加载顺序的区别;当一个页面在加载的时候.link引入的CSS会同时和页面一起加载,@import引入的CSS文件要等页面加载完之后再加载.所以有时候浏览@import加载CSS的页面时会没有样式(就是闪烁),特别是网速比较慢的时候

  3. 兼容性的差别;@import是CSS2之后提出的,只支持IE5以上的浏览器.link没有兼容性问题

  4. 使用Javascript控制DOM改变样式时,只能使用link标签,因为通过@import不是DOM能够控制的

动画

GET和POST的区别

GETPOST
浏览器回退无害再次提交请求
编码方式只支持url编码支持多种编码格式
参数长度有长度限制无长度限制
参数是否保留在历史记录参完整的保留在浏览器历史记录不会保留在浏览器历史记录
参数传递方式通过url传递放在Request Body请求体中
参数数据类型只接受ASCII字符没有限制
安全性比POST更不安全(参数直接暴露在url)不安全
是否被浏览器主动cache不会,除非手动设置
产生TCP数据包的个数一个两个

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

2.GET请求会被浏览器主动cache,而POST不会,除非手动设置

3.GET请求只能进行url编码,而POST支持多种编码方式

4.GET请求参数会被完整保留在浏览器历史记录里,而POST的参数不会保留

5.GET请求在URL中传送的参数有长度限制,而POST没有

6.GET比POST更不安全,因为参数直接暴露在URl上,不能用来传递敏感信息.

7.GET参数通过URl传递,POST参数放在Request body中

8.对于参数的数据类型,GET只接受ASCII字符,而POST没有限制

9.GET和POST本质上就是TCP链接,并无差别。但是由于HTTP的规定和浏览器/服务器的限制,导致他们在应用过程中体现出一些不同。

10.GET产生一个TCP数据包;POST产生两个TCP数据包

H5新特性

  1. 新增了语义化标签;header,main,footer,nav,section,article

  2. 增强型表单

  3. 双向通信webSocket

  4. 视频,音频 audio video 标签

  5. 图形 canvas,svg矢量图

  6. 提高性能充分利用电脑的CPU webWorks

  7. 本地存储;localstorage,sessionstorage

  8. 媒体查询(移动端适配)

  9. CSS3的美化

  10. 地理定位

CSS3新特性

  1. 选择器

    添加了一些结构伪类选择器;first-of-type,last-of-type

  2. 背景和边框

    边框border-radius,box-shadow

    背景background-clip

  3. 文本效果

    word-wrap 允许对单词进行拆分

  4. 2D/3D的转换

  5. 动画、过渡

    transition,transform,animation

    transform:scale,translate,rotate

  6. 多列布局

  7. 用户界面

    box-sizing

如果需要手写动画,最小时间间隔是多少

大部分的显示器默认的60Hz,就是一秒刷新60次,1000/60=16.7ms

fixed和absolute的共同点

  1. 两者的display属性值都是block

  2. 都会脱离文档流

浏览器的最小字体是12px,如果还想要小,该怎么做?

  1. 通过设置元素的transfor:scale()属性

怎么实现0.5px的线

  1. 通过transform的scale缩放

  2. 使用linear-gradient(0deg,#fff,#000)

  3. 移动端可以通过通过初始缩放比例

  4. 通过box-shadow模糊距离为0.5px

媒体查询

概念

媒体查询就是根据不同的设备设定不同的CSS样式,使页面在不同的终端设备下达到不同的渲染效果

媒体查询使用方法

@media 媒体类型 and 媒体特性{自己设置的样式}

比如说 @media screen and max-width:600px

处理PC端浏览器兼容问题

  1. 是用normalize.css或者自己的reset.css解决不同浏览器的默认样式

  2. 引入html5shiv.js解决ie9以下浏览器对H5新增标签不识别的问题

  3. 引入respond.js解决ie9以下浏览器不支持媒体查询的问题

  4. picturefill.js解决ie9,10,11等浏览器不支持pictrue标签的问题

  5. 添加私有前缀

  6. 求窗口大小的兼容写法

    // 浏览器窗口可视区域大小(不包括工具栏和滚动条等边线)// 1600 * 525
    var client_w = document.documentElement.clientWidth || document.body.clientWidth;
    var client_h = document.documentElement.clientHeight || document.body.clientHeight;
     
    // 网页内容实际宽高(包括工具栏和滚动条等边线)// 1600 * 8
    var scroll_w = document.documentElement.scrollWidth || document.body.scrollWidth;
    var scroll_h = document.documentElement.scrollHeight || document.body.scrollHeight;
     
    // 网页内容实际宽高 (不包括工具栏和滚动条等边线)// 1600 * 8
     
    var offset_w = document.documentElement.offsetWidth || document.body.offsetWidth;
    var offset_h = document.documentElement.offsetHeight || document.body.offsetHeight;
    // 滚动的高度
    var scroll_Top = document.documentElement.scrollTop||document.body.scrollTop;

移动端适配问题

  1. 媒体查询

    通过查询设备的宽度执行不同的css代码

    优点

    • 可以做到设备像素比的判断,方法简单,成本低,特别是对移动端和PC端维护同一套代码的时候.像Bootstrap等框架就是使用这种布局方式

    • 图片便于修改,只需修改css文件

    • 调整屏幕宽度的时候不用刷新页面即可响应式展示

    缺点

    • 代码量大,维护不方便

    • 为了兼顾大屏幕或高清设置,会造成其他设备资源浪费,特别是加载图片资源

    • 为了兼顾移动和PC端各自响应式的展示效果,会损失各自特有的交互方式

  2. Flex弹性布局

  3. rem+viewport缩放

    根据屏幕宽度设定rem值,需要适配的元素都是用rem为单位,不需要适配的元素是用px为单位

    rem和em的区别

    rem是相对于HTML根元素的相对大小单位,通过它既可以做到只修改根元素就可以成比例的调整所有是用rem设置大小的元素

    em是相对于父元素的相对大小单位

    原理

    根据rem将页面放大dpr倍,然后将viewport设置为1/dpr

  4. 是用vw和vh

    这个vw和vh都是相当于把视口分成了100份

移动端兼容问题

  1. 移动端click事件有300ms延迟,往往会造成点击延迟甚至是失效,这是由于区分单击事件和双击缩放屏幕的历史原因造成的

    解决方式

    • fastclick路由解决手机端点击300ms延迟的问题

    • zepto的touch模块,tap事件也是为了解决在click上的延迟问题

    • 触摸屏的相应顺序为touchstart-->touchmove-->touchend-->click,也可以通过绑定ontouchstart事件,加快事件的响应,解决300ms的问题

  2. 一些情况下对非可点击元素(label,span)监听click事件,iso下不会触发,css添加cursor:pointer就行了.

  3. 禁止IOS识别长串数字为电话

    <meta content= "telephone=no" name="format-detection">

解决移动端1px边框问题

因为css中1px并不等于移动设备的1px,这是因为不同的手机有不同的像素密度.在window对象中有一个devicePixelRatio属性,它可以反映css中的像素与设备的像素比.

devicePixelRatio的定义为:设备物理像素和设备独立像素的比例

  • 使用0.5px边框;通过javascript检测浏览器能否处理0.5px的边框,如果可以的话就给html标签元素加个border-width:0.5px.

    缺点

    无法兼容安卓设备和IOS8以下设备

  • 使用borde-image实现

  • 对viewport+rem实现

    在devicePixelRatio=2时,输出viewport:

    <meta name="viewport" content="initial-scale=0.5,maximum-scale=0.5,minimum-scale=0.5,user-scalabel=no">

    当devicePixelRatio=3时,输出viewport

    <meta name="viewport" content="initial-scale=0.3333333333333333, maximum-scale=0.3333333333333333, minimum-scale=0.3333333333333333, user-scalable=no">

    优点

    • 所有场景都能满足

    • 一套代码基本可以兼容所有布局

    缺点

    老项目修改代码过大,只适用于新项目

  • 伪元素+transform实现

    把之前元素的border去掉,利用伪元素重做设置border,并使用transform的scale将border缩小一半

实现一个正方面始终居中,正方面的大小自适应

 * {
      margin: 0;
      padding: 0;
    }

    .father {
      width: 50vh;
      height: 50vh;
      background: skyblue;
      position: absolute;
      top: 0;
      left: 0;
      bottom: 0;
      right: 0;
      margin: auto;
    }

px,rem,em的区别

px:相对于显示器屏幕分辨率长度单位

rem:是CSS3新增的一个相对单位,是相对于HMTL根元素大小的相对单位

em:相对于当前对象内文本的字体尺寸的相对单位

px与rem的选择

对于只需要适配少部分手机设备,且分辨率对页面影响不大的,使用px即可 。

对于需要适配各种移动设备,使用rem,例如只需要适配iPhone和iPad等分辨率差别比较挺大的设备。

Stylus,Less,Scss的区别

styluslesssass
基本语法没有花括号和分号有花括号和分号有花括号和分号
嵌套语法没有花括号和分号有花括号和分号有花括号和分号
变量对变量没有任何设定,,但是不能以@开头变量以@开头变量以$开头

实现多行文本居中超出显示省略号

   display: -webkit-box;
   /-webkit-line-clamp: 3;
   /-webkit-box-orient: vertical;
   overflow: hidden;
   text-overflow: ellipsis;

JavaScript

JS执行机制

首先,javascript是一门单线程的语言.javascript代码从上到下执行,有同步和异步代码,代码执行的过程中,同步任务和异步任务会进入到不同的"场所",同步任务进入主线程,异步任务进入到一个事件队列当中,当所有的同步任务都执行完了,就会去读取事件队列里面的函数,进入主线程执行,重复上述的过程就是常说的Event Loop.虽然在最新的HTML-5中提出了Web-Worker,但是javascript是单线程这一核心仍未改变.

Web-Worker

Web-Worker的作用,就是为Javascript创造多线程的环境,允许主线程创建Worker线程,将一些任务分配给Worker运行.在主线程运行的同时,Worker在后台运行,两者互不干扰,等到Worker线程完成计算任务,再把结果返回给主线程.这样的好处就是可以将一些计算比较密集或者高延迟的任务交给Worker线程负担,主线程就会很流畅,不会被阻塞.

Worker 线程一旦新建成功,就会始终运行,不会被主线程上的活动(比如用户点击按钮、提交表单)打断。这样有利于随时响应主线程的通信。但是,这也造成了 Worker 比较耗费资源,不应该过度使用,而且一旦使用完毕,就应该关闭。

Web-Worker使用注意点:

  1. 同源限制

  2. DOM限制

  3. 通信联系

  4. 脚本限制

JS数据类型

基本数据类型

  • 字符串String

  • 数字Number

  • 布尔值Boolean

  • Undefined

  • Null

  • Symbol

引用数据类型

  • 对象Object

    • 函数Function

    • 时间Date

    • 正则RegExp

    • Error

    • Math

    • Arguments

    • 数组Array

    • 对象Object

JS内置对象

基本对象

  • 函数Function

  • 时间Date

  • 正则RegExp

  • 错误Error

  • 默认参数Arguments

  • Math

数据对象

基本类型

  • 数字Number

  • 字符串String

  • 布尔Boolean

数据结构

  • 数组Array

  • 对象Object

JS实现异步编程的方法

优点缺点
回调函数简单,容易理解不利于维护,代码耦合度搞高
事件监听容易理解,可以绑定多个事件,每个事件可以指定多个回调函数事件驱动,流程不清晰
发布/订阅(观察模式)类似于事件监听,但是可以通过消息中心,了解现在有多少发布者和订阅者
Promise对象可以利用.then方法,进行链式书写,书写成功和失败后的回调函数编写和理解相对比较难
async函数内置执行器、更好的语义、更广的适用性、返回的是Promise、结构清晰。错误处理机制

JS延迟加载的方法

JS延迟加载就是等页面加载完成之后再加载JS文件,JS延迟加载有助于提高页面加载速度

1.defer

在script标签设置defer的属性值为defer,等于告诉浏览器,脚本立即下载,但是延迟执行

只适用于外部脚本文件

2.async属性

在script标签设置async属性.目的是不让页面等待脚本下载和执行,从而页面加载页面其他内容

只适用于外部脚本文件,不能控制加载的顺序

3.动态创建DOM

4.使用setTimeout延迟方法的加载时间

5.让JS最后加载

栈和堆的区别

存放内容变量,基本数据类型和引用数据类型的地址引用数据类型
储存特点体积小,数据经常发生变化体积大,数据不会经常变化

Event Loop

在javascript中,任务被分为两种,一种是宏任务(MacroTask)也叫Task,一种是微任务(MicroTask)

宏任务

scrip全部代码,setTimeout,setInterval,setImmediate,I/O,ULRendering

微任务

process.nextTick(Node独有),Promise,Object.observer(废弃),MutationObserver

浏览器中的Event Loop

javascript中有一个主线程和调用栈,所有的任务都在调用栈中等待主线程执行

ES6新特性

  • 变量声明方式:let和const

  • Promise

  • 增强的对象字面量

  • 箭头函数

  • class类

  • 字符串模板

  • 解构

  • 模块

  • for...of循环

  • Map,Set

面向对象和面向过程编程,他们之间的优缺点和异同

区别

面向过程

面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步的实现,然后使用的时候一个一个一次调用就可以了

面向对象

面向对象就是把构成问题事务分解成各个对象,比如说这个对象负责这类功能,那个对象负责其他类功能,每一个对象完成的功能所整合起来完成整个问题的编程方式就是面向对象编程

面向对象的特点

封装性

函数的封装

对象的封装

模块的封装

继承性

原型链的继承

类的继承

组合寄生式继承

多态性

JS函数对类型没有要求,不同函数得到不同的结果,导致多态.其实严格来说的话,JS面向对象不存在多态,或者很难实现多态

各自的优缺点

面向对象面向过程
优点易维护,易复用,易扩展,耦合度低性能比面向对象高
缺点因为类调用时需要实例化,性能比面向过程低不易维护,不易复用,不易扩展

继承

继承就是子类可以继承父类身上所有的属性和方法,子类也可以在自己身上自定义属性和方法.ECMAscript的继承主要是通过原型链的方式实现的.

继承的六种方法

  1. 原型链的继承

  2. 借用构造函数继承

  3. 组合继承(综合原型链和借用构造函数)

    基本思路就是使用原型链继承原型上的属性和方法,通过借用构造函数继承实例属性

  4. 原型式继承

  5. 寄生式继承

  6. 寄生组合式继承(ES5最理想的状态)

    //父类构造函数
    function Person(name,sex,age){
    this.name=name,
    this.sex=sex,
    this.skil=['吃饭','睡觉',]
    }
    //父类方法
    Person.prototype.getSkill=function(){
        return this.skill
    }
    //子类构造函数
    function Son(name,sex,age,sno){
        //调用父类的构造函数 从而继承父类的属性
        Person.call(this,name,sex,age)
        this.sno=sno
    }
    //子类继承父类原型上的方法
    Son.prototype=Object.create(Person.prototype)
    //完善子类的constructor指向  将Son的原型对象重新再指向构造函数本身
    Son.prototype.constructor=Son
    //在子类身上添加方法
    Son.prototype.doHomework=function(){
       	console.log('每天做十个小时作业')
    }
    let S1=new Son('小明','男',18,1001)
    console.log(S1)

    首先就是写一个父类的构造函数,然后在函数里面写属性和方法,父类的方法是挂载在父类的原型上的,然后子类的构造函数里面可以通过call或者apply改变this指向,从而调用父类的构造函数,继承父类的属性,然后子类可以通过object.create的方法继承父类原型上的方法,最后要做的就是通过原型对象的constructor将子类原型对象重新指向子类构造函数本身.

原型链

原型.每一个函数都有一个原型对象prototype,他的值是一个对象;

每个实例对象里面都有一个隐式原型__proto__,他指向构造函数的原型对象prototype

每一个原型对象里面都有一个构造器constructor,他指向函数本身.

原型链就是每一个构造函数有对应的原型对象,而原型对象又可以是另一个构造函数所创建的实例,这个创建出来的实例也有自己对应的原型对象,这种关系层层递进,所构成的链式结构就是原型链.

原型链也有自己的查找规则,当一个对象在查找标识符时,会先在对象自身身上找,没找到就沿着原型链逐层向上找,直到找到null

说到原型链的查找规则,我们平常说的万物皆空也可以从这里解释,因为不管哪一种数据类型,只要沿着原型链往上找,最终都能找到Object,Object再往上找就是null.

This指向

主要有7种this指向

1.一般函数中的this.谁调用指向谁

2.构造函数中的this,指向实例化的那个对象

3.对象中的this,谁调用就指向谁

4.事件处理函数中的this,指向事件源

5.定时器里面的this,指向window

6.箭头函数中的this,指向定义时的作用域

7.全局的this,指向window

设计模式

Call、Apply、Bind的用法和区别

call,apply,bind的作用就是改变函数运行是this的指向

call,apply他们两个在用法上几乎相同,唯一的差别就在于,在传递参数时,call接收的参数是单独的变量,而apply接收的参数时一个数组

bind和call以及apply 的区别在于bind返回值是一个函数,便于稍后调用,而apply,call则是立即调用

New实现

1.创建一个空的对象

2.将this挂载在这个空对象上(为了继承函数的原型)

3.给对象添加属性和方法

4.返回一个对象

  • 以构造器的prototype属性为原型,创建新对象

  • 将this和调用参数传给构造器执行

  • 如果构造器没有手动返回对象,则返回第一步创建的对象

防抖节流

防抖

短时间内多次触发,最终在停止触发后的某个指定时间执行一次

// 实现滚动条的防抖功能
// 在第一次触发事件的时候,不会立即执行函数,而是给出一个delay值,比如1000ms
// 如果在在1000ms之内没有再次触发该事件,就执行函数
// 如果在1000ms之内再次触发该事件,就清除当前的计时器,重新开始计时
// fn是需要防抖的函数,daley是防抖指定的时间
function debounce(fn,delay){
    let timer = null //借助闭包解决防抖问题
    return function(){
        if(timer){
            clearTimeout(timer)
            timer=setTimeout(fn,delay)// 进入该分支说明正在计时,所以就把之前正在计时的计时器关掉,然后重新开始计时
        }else{
            timer=setTimeout(fn,delay) // 进入该分支,说明timer为空,就是没有在计时.所以就进入一个计时
        }
    }
function showTop(){
    let scrollTop=document.body.scrollTop || document.documentElement.scrollTop
}
    window.οnscrοll==debounce(showTop,1000)

节流

在指定时间内触发过一次函数之后,函数会进入一个休眠状态,指定时间过了之后函数再次被激活

// 让函数在执行一次之后,暂时失效,过了指定的时间之后再次激活
const throttle=(fn,daley)=>{
    let valid = true
    return ()=>{
        if(!valid){
            return false
        }
        valid = false
        setTimeout(()=>{
            fn()
            valid=true
        },delay)
    }
}

利用lodash实现防抖节流

// 节流
_.throttle(要节流的函数,需要节流的毫秒数,选项对象{指定调用在节流开始前还是节流结束后leading:Booleadn,trailing:Boolean})

let var const的区别

letvarconst
变量/常量变量变量常量
作用域块级作用域函数作用域块级作用域
创建位置作用域顶部作用域顶部作用域顶部
初始化let语句作用域顶部const语句
重复赋值可以可以不行
重复声明不行可以不行
暂时性死区没有

首先,const生命的值其实是一个常量,所以呢,他在初始化的时候就必须要赋值,不然就会报错,然后let和var都是可以进行重复的赋值的,但是let不能重新声明,而var是可以重新声明的,然后let和const都是块级作用,var是函数作用域,也就是局部作用域,然后let和const是有暂时性死区的,而var没有暂时性死区

Promise

谈谈你对Promise的理解

简单点说Promise是用来管理异步操作的,主要是用来解决地狱回调(本次请求依赖上一次请求响应回来的数据)的问题.还可以配合async和await实现异步代码同步化

Promise有三种状态:pending(等待),resolve(成功),rejecrt(失败)

Promise的实例是一个对象,从它可以获取异步操作的消息,并且可以通过Promise提供的API处理各种异步操作

promise使用及实现

function getData(url, data = {}) {
        return new Promise(function (resolve, reject) {
            $.ajax({
                url,
                data,
                type: 'GET',
                success(res) {
                    resolve(res)
                }
            })
        })
    }
    getData('../data/first.json')
        .then(function (res) {
            return getData('../data/second.json', { params: { name: res.name } })
        }).then(function (res) {
            return getData('../data/third.json', { params: { name: res.name } })
        }).then(function (res) {
            console.log(res);
        })
//箭头函数的写法
function getData(url, data = {}) {
        return new Promise((resolve, reject) => {
            $.ajax({
                url,
                data,
                type: 'GET',
                success(res) {
                    resolve(res)
                }
            })
        })
    }
    getData('../data/first.json')
        .then(res => getData('../data/second.json', { params: { name: res.name } }))
        .then(res => getData('../data/third.json', { params: { name: res.name } }))
        .then(res => console.log(res))

Promise的主要方法

  • .then(),这个方法里面有两个参数,第一个参数是resolve状态下的回调函数,第二个参数(可选)是reject状态下的回调函数.此方法返回的是一个新的实例

  • .catch()是.then(null,rejection)的别名,用于指定发生错误时的回调函数.Promise内部的错误不会影响外部的错误,也就是通常说的Promise会吃掉错误

  • .finally()就是不管promise最后状态如何都会执行的操作

  • Promise.all()用于将多个Promise实例,包装成一个新的Promise实例,Promsie.all()参数为数组.

    (1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。

    (2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

手写一个promise以及async await

function timeOut(time){
    return new promise(function(resolve,reject){
        setTimeout(function(){
            resolve(12345)
        },time)
    })
}
async function test(){
    const num = await timeOut(1000)
    console.log(num)
}
test()

async await

  • 使用async await 的前提是必须封装一个Promise函数

  • async告知函数内有异步操作,返回Promise

  • 有async就必须有await

  • await修饰Promise后的代码必须等待其结束后才执行

Promise并行执行和顺序执行

闭包

函数跨作用域,内部函数可以访问外部函数的标识符,闭包有两种写法,一种是外部函数包含内部函数,外部函数将内部函数返回,第二种是外部函数包含内部函数,外部函数把内部函数挂载在window上.

特点

让外部访问函数内部变量成为可能;

局部变量会常驻在内存中;

可以避免使用全局变量,防止全局变量污染;

如果大规模使用闭包,可能会造成内存泄漏

垃圾回收和内存泄漏

数组方法

1.连接一个或者多个数组

let arr1=[1,2,3,4]
        let arr2=[5,6,7,8]
        // 数组与数组进行连接
        let arr3=arr1.concat(arr2)
        console.log(arr3);
        // 数组与数字进行连接
        let arr4=arr3.concat(9,10,11,12)
        console.log(arr4);
        //数组与多个数组连接
        let arr5=arr4.concat(arr1,arr2)
        console.log(arr5);

2.翻转数组

let arr=[1,2,3,4,5]
        let arr1=arr.reverse()
        console.log(arr1);

3.往数组里面添加元素

let arr=[1,2,3,4,5,6]
        let arr1=arr.push(parseInt(prompt()))
        console.log(arr);
        // 在数组的末尾添加元素     返回的是新的长度

        //在数组的开头添加元素   可以一次性添加多个元素
        let arr2=arr.unshift(10,11)
        console.log(arr);

4.从数组里面删除元素

 //pop删除数组的最后一个元素  并返回删除的元素
        let arr=[1,2,3,4,5]
        let arr1=arr.pop()
        console.log(arr1);
        //shift删除并返回数的第一个元素 并返回删除的元素
        let arr2=arr.shift()
        console.log(arr2);

5.把数组转化成字符串

//join通过引号里面的内容决定按照怎样的方式进行装换
let arr=[1,2,3,4,5];
let str=arr.join();
//输出为1,2,3,4,5
let str1=arr.join('');
输出为12345
let str2=arr.join(' ')
输出为1 2 3 4 5

6.元素在数组中的索引(数组去重的方法之一)

let arr=[123,2,73,55,67,55,2,98,37,590,73];
//声明一个新的数组,用来保存去重后的数组
let arr1=[];
for(let i=0;i<arr.length;i++){
	if(arr1.indexOf(arr[i])===-1){
		arr1.push(arr[i])
		}
	}

7.数组中是否包含指定元素(数组去重的方法之一)

let arr=[12,44,23,76,55,44,12,87,76];
let arr1=[];
for(let i=0;i<arr.length;i++){
    if(!arr1.includes(arr[i])){
        arr1.push(arr[i])
    }
}

8.展开任意深度的嵌套数组

//此方法可以将任何深度的数组转成一维数组
//arr.flat(参数)参数可以为数字,数字代表着展开的深度,也可以为英文,Infinity,为无限层级,就是可以将任何深度的数组转换成一维
let arr=[1,[2,3,[13,22],[33,2112]],2]
let arr1=arr.flat(infinity)//[1,2,3,13,22,33,2112,2]

9.满足条件时返回的元素和索引方法

let arr=[2,1,421,11,231,11,12,44]
//返回的是第一个大于100的元素
let element=arr.find(function(value){
    return value>100
})
console.log(element)//421
//返回的是第一个大于100的元素的索引
let index=arr.findindex(function(value){
    return value>100
})
console.log(index)//2

10.截取数组

//此方法返回的是一个新的数组
let arr=[1,2,3,4,5];
//截取的是从索引为1到索引为3之前的元素
let arr1=arr.slice(1,3);
console.log(arr1)//[2,3]

11.增删改数组

//arr.splice(参数1,参数2,参数3)
//参数1为元素的索引,参数2为删除的个数,参数3为添加的元素,可以写多个,此方法是改变了原数组,返回的是被删除的元素
//let arr=[1,42,332,66,87,98,80]
//删除第二个元素,并在末尾添加一个元素为999,,将332改成55
arr.splice(indexOf(332),1,55)
arr.push(999)
arr.splice(1,1)
//splice不能使用链式编程,因为splice返回的是被删除的值,是一个字符串

12.遍历数组

//1.forEach
let arr=[1,2,3,4,5];
let sum=0
arr.forEach(function(value){
    return sun+=value
})
console.log(sum)//15
//2.for in
let  arr=[1,2,3,4,5,6,7]
    let sum=0
    for(let i in arr){
        //i直接可以取到arr中的索引
        sum +=arr[i]
    }
    console.log(sum);
//3.for of
let arr=[1,2,3,4,5,6,7];
    let  sum=0;
    for(let ele of arr){
        //ele可以直接取到arr中的元素
        sum+=ele
    }
    console.log(sum);

13.对数组的每一个元素进行操作,返回操作后的数组

let arr=[1,2,3,4,5,6]
let arr1=arr.map(function(value){
    return value*2
})
console.log(arr1)//2,4,6,8,10,12

14.返回所有满足条件的元素

let arr=[122,321,222,155,533,2221,1243,2465];
let arr1=arr.filter(function(value){
	return value>500
})
console.log(arr1)//[533,2221,1243,2465]

15.全部元素是否满足条件

let  arr=[1,234,342,11,887,56,98]
        let boo=arr.every(function(value){
            return value>10
        })
        console.log(boo);


        
        let bool=arr.every(function(value){
            return value<1000000
        })
        console.log(bool);

16.数组里是否有满足条件的元素

//检查数组中是否有满足条件的元素  返回布尔值
        let arr=[9876,2314,543221,11,6453,2342]

        let bool=arr.some(function(value,index,arr){
            return value>1000
        })
        console.log(bool);
//只要有满足条件的元素,就返回true

17.keys()

18.sort()

19.reduce()

let arr=[1,31,112,554,221]
let sum=arr.reduce(function(previousValue,currentValue){
    return previousValue+currentValue
},0)
console.log(sum)

数组乱序

 // 数组乱序   
    // 先从数组末尾开始,选取最后一个元素  与数组中随机一个位置的元素交换位置
    // 然后在已经排好的最后一个元素以外的位置,随机产生一个位置,让该位置元素与倒数第二个元素进行交换
    Array.prototype.shuffle = function () {
      var input = this;
      for (var i = input.length - 1; i >= 0; i--) {

        var randomIndex = Math.floor(Math.random() * (i + 1));
        var itemAtIndex = input[randomIndex];

        input[randomIndex] = input[i];
        input[i] = itemAtIndex;
      }
      return input;
    }
    console.log([1, 5, 9, 4, 22, 8, 543].shuffle());

数组扁平化

1.普通for循环递归遍历

// 递归遍历,遍历最外层数组的每一个元素,判断是否为数组,如果是数组,继续执行递归,不是数组就将arr[i]push到空数组中去
let arr = [1, [2, [3, 4]]];
 
 function flattern(arr) {
        let result = [];
        for(let i = 0; i < arr.length; i++) {
            if(Array.isArray(arr[i])) {
                flattern(arr[i])
            } else {
                result.push(arr[i])
            }
        }
        return result;
    }

2.reduce数组方法

let arr = [1, [2, [3, 4]]];
function flattern(arr){
    return arr.reduce(function(prev,cur){
        return prev.concat(Array.isArray(next)?flattern(next):next)
    },[])
}
    

3.flat数组方法

function flattern(arr){
    return arr.flat(infinity)
}

数组去重

1.利用indexOf去重

function unique(arr){
    if(!Array.isArray(arr)){
        console.log('这不是一个数组')
        return
    }
    let array=[]
    for(let i =0 ;i <arr.length;i++){
        if(array.indexOf(arr[i])===-1){
            array.push(arr[i])
        }
    }
    retutn array
}

2.利用splice去重

function unique(arr){
    for(let i = 0; i<arr.length;i++){
        for(let j = i+1;j<arr.length;j++){
            if(arr[i]===arr[j]){
                arr.splice(j,1)
                j--
            }
        }
    }
    return arr
}

3.利用ES6 Set去重(ES6最常用)

function unique(arr){
    return Array.from(new Set(arr))
}
let arr1 =new Set(arr)

4.利用includes去重

function unique(arr){
    if(!Arrry.isArray(arr)){
        console.log('这不是一个数组')
        return
    }
    let array=[]
    for(let i =0;i<arr.length;i++){
        if(!array.includes(arr[i])){
            array.push(arr[i])
        }
    }
    return array
}

数组排序的方法

  1. 字符串数组排序的方法

    let fruits = ["Banana", "Orange", "Apple", "Mango"];
    // sort()默认是按照字母从A-Z的顺序进行排列的
    fruits.sort();//默认升序  Apple,Banana,Mango,Orange
    fruits.sort().reverse();//降序  Orange,Mango,Banana,Apple

  2. 数字数组进行排序

     	let arr = [1, 5, 3, 978, 543, 213, 774, 11]
        // 使用数组的sort方法,然后sort包括里面传递一个函数,参数为a,b,函数返回a-b就为升序 b-a就为降序
        console.log(arr.sort((a, b) => { return a - b }));
        console.log(arr.sort((a, b) => { return b - a }));

递归算法

什么是递归

所谓递归,简单点来说就是一个函数直接调用或者间接调用自身的一种方法,然后会有一个结束的条件.他通常可以把一个大型复杂的问题层层转化成一个与原问题相似的规模较小的问题来求解.

递归算法案例

1.求任意数的阶乘

function factorial(n) {
    if (n === 0) {
      return 1
    }
    return factorial(n - 1) * n
  }
  console.log(factorial(5));//120

2.斐波那契数列

 function fibonacci(n) {
    if (n <= 2) {
      return 1
    }
    return fibonacci(n - 2) + fibonacci(n - 1)
  }
  console.log(fibonacci(8));// 21

事件委托

什么是事件委托

事件委托又称事件代理,JavaScript高级程序设计上讲:事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件

为什么要用事件委托

一般来说,dom需要有时间处理程序,我们直接给它设置事件处理程序就好了,比如有100个li.每个li都有相同的点击事件,一般的做法就是用for循环遍历所有的li,给他们添加事件,但是这么做事件处理程序的数量很大,事件处理程序数量又直接关系到页面的整体运行性能,因为需要不断的与dom节点进行交互,访问dom的次数越多,引起浏览器重绘和重排的次数也就多,就会延长整个页面的交互就绪时间,这就是为什么性能优化的主要思想之一就是减少dom操作的原因:如果要用事件委托,就会将所有的操作放到js程序里面,与dom的操作就只需要交互一次,这样就能大大的减少与dom的交互次数,提高性能;

每一个函数都是一个对象,是对象就会占用内存,对象越多,内存占用率就会越大,自然性能就越差了;比如上面的100个li,就要占用100个内存空间,如果是1000个,10000个呢,那只能说呵呵了,如果用事件委托,那么我们就可以只对它的父级(如果只有一个父级)这一个对象进行操作,这样我们就需要一个内存空间就够了,是不是省了很多,自然性能就会更好。

事件委托的原理

事件委托的原理是利用事件的冒泡来实现的,冒泡事件就是从最深的节点开始,然后逐步向上传播事件,举个例子,页面有一个节点树为以下这样,div>ul>li>a,当给a设置点击事件的时候,那么这个事件就会一级一级的往外执行,执行顺序为a>li>ul>div,有了这么一个机制,我们可以直接给div设置点击事件,那么里面的ul,li,a做点击事件的时候,都会冒泡到最外层,这就是事件委托,委托他们的父级代为执行事件

事件监听

事件模型

Typescript

如何检测一个对象为空对象

1.使用for...in遍历

let obj={}
function isEmptyObj(obj){
    for(let key in obj){
        return false
    }
    return true
}

2.Object.keys()

let obj ={}
function isEmptyObj(obj){
    if(Object.keys(obj)===0){
        return  true
    }else{
        return false
    }
}

判断变量类型的方法

1.type of

type of 只能判断出三种数据类型 Number String Boolean 其余的全都检测为object

2.Array.isArray()

判断是否是一个数组

3.instance of

4.Object.prototype.toString.call()

可以检测所有的数据类型结构

Localstorage,sessionstorage,cookies的特点

特性CookiesLocalStorageSessionStorag
生命期可以设置失效的时间,如果在浏览器端自动生成Cookies,关闭浏览器之后失效需要手动清除,否则永久保存仅在当前会话窗口有效,关闭会话就自动清除
大小4KB左右一般5MB一般5MB
与服务器通信每次都会携带在HTTP中,可以用来保存用户的登录信息仅保存到本地,不参与和服务器的通信仅保存到本地,不参与和服务器的通信

浅拷贝和深拷贝

浅拷贝只是增加地址指向已存在的内存

  1. 通过for..in将对象内的键值赋值给新的对象;只循环第一层

  2. Object.assgin方法

深拷贝是申请了一个新的地址,使这个新的地址指向一个新开辟的内存

  1. 通过递归遍历所有层级的属性进行拷贝

  2. 通过JSON对象的两个Api实现深拷贝;缺点:无法实现对对象中的方法的深拷贝,会显示为undefined

  3. lodash函数库实现_.cloneDeep(要进行拷贝的对象)

  4. Object.assgin方法其实也可以实现深拷贝,但是只针对于对象只有一层的状态

箭头函数和普通函数的区别

  • 箭头函数的语法和普通函数的语法不一样

  • 箭头函数不能作为构造函数

  • 箭头函数的this指向函数声明时所处的作用域

  • 箭头函数没有arguments

异步函数编程的实现

  • promise

  • async+await

  • 回调函数:ajax,定时器,事件处理函数

createElement和createDocumentFragment的异同

共同点

  • 添加子元素后返回值都是新添加的子元素

  • 都可以通过appendChild添加子元素,并且子元素必须是node类型,不能为文本类型

  • 若添加的子元素是文档中存在的元素,则通过appendChild在为其添加子元素时会从文档中删除之前存在的元素

不同点

createElementcreateDocumentFragment
节点类型创建的是元素节点,节点类型为1创建的是文档碎片,节点类型为11
是否指定元素TagName必须指定元素TagName,因为其可以使用innerHTML添加子元素不必
插入文档方式创建的元素直接插入到文档中创建的元素插入到文档中的是他的子元素

一次性插入1000个div,如何优化插入的性能

使用document.createDocumentFragment()

Webpack

Webpack的基本配置

  1. 安装node.js

  2. 在命令窗口全局安装webpack和webpack-cli npm i webpack webpack-cli -g

  3. 在本地项目安装webpack webpack-cli npm install webpack webpack-cli --save-dev

  4. 通过 npm init -y 快速创建package.json文件

  5. 添加webpack.config.js文件,然后在此文件中进行配置

    const path = require('path')
    const HtmlPlugin = require('html-webpack-plugin')
    module.export = {
        // 入口  指示webpack以哪个文件作为入口起点分析构建内部依赖图进行打包
        entry:'需要打包的的文件',
        // 输出 在output指定生成的文件目录,和文件名
        output:{
            filename:'生成的文件名',
            path:path.resolve(__dirname, './dist/js') //指定生成的文件目录
        },
        // 模块 在module里面可以配置一下loader loader可以处理非JS语言的文件
        module:{
            rules:[{
                test:'',use:'xxx-loader'  
                // eslint-loader  会在打包和编译的时候提示语法问题
                // css-loader   // less-loader处理入口文件中的css   // style-loader  
                // 打包图片资源  url-loader file-loader
            }]
        },
        // 插件  通过require引入插件
        plugins:[
            new HtmlPlugin({
                filename:'生成的html文件名',  //html
                template:'模板文件的地址',
                excludeChucks:['不引入的chunk名'], // login
                chucksSortMode:'排序规则'   //dependency是按照依赖关系进行排序
            },
             new HtmlPlugins({
                 filename:'生成的html文件名',  //login
                 template:'模板文件的地址',
                 excludeChucks:['不引入的chunk名'],  //html
                 chucksSortMode:'排序规则'   //dependency是按照依赖关系进行排序
            }))
        ],
        // 模式
        mode:'development' //开发模式 将process.env.的值设置为development
        mode:'production' // 生产模式 将process.env.的值设置为production  优化代码
    }
  6. 还可以下载插件 npm install --save-dev clean-webpack-plugin 删除输出目录之前的旧文件

Tree shaking

概念

tree shaking 是一个性能优化的范畴.具体来说就是在webpack项目中,有一个入口文件,相当于一棵树的主干,入口文件有很多依赖的模块,相当于数值,实际情况中是虽然依赖了某个模块.但是只是用了其中的某些功能.通过tree-shaking将无用的一些代码给摇掉,达到一个删除无用代码的目的.

本质

Tree-shaking的本质是消除无用的js代码,无用代码消除广泛存在于传统的编程语言编译器中,编译器可以判断出某些代码根本不影响输出,然后消除这些代码.这个称之为DCE(dead code elimination)

对组件库引用的优化

当我们使用组件库的时候,按需导入相对于全局注册的性能消耗会更低,但是我们将组件的导入具体到文件的引用,bundle的体积将会变得更少.我们可以通过下载babel-plugin-import-fix插件,然后再.babelrc文件进行配置

CSS Tree-shaking

随着less,sacc等各种css预处理器语言的普及,css文件在整个工程中占比是不可忽视的.随着大项目功能的迭代,导致css中可能存在着无用的代码.可以通过webpack-css-treeshaking-plugin,对css进行tree-shaking

webpack bundle文件去重

通过CommonsChunkPlugin自动提取所有的node_modules或者引用次数两次以上的模块,然后插件中进行配置

如果是针对按需加载的文件可以配置另一个CommonsChunkPlugin,添加async属性,async接收布尔值或字符串.当时字符串时,默认是输出文件的名称

使用tree shaking的前提

  • 使用ES6模块语法(import和export)

  • 在项目的pack.json文件中添加一个"sideEffects"入口

  • 引入一个能够删除未引用代码的压缩工具(例如UfgifyJSPlugin)

webpack常见的loader以及解决什么问题

  • file-loader : 把文件输出到一个文件夹中,在代码中通过相对URL引用输出的文件

  • source-map-loader : 加载额外的Source Map文件,方便断电调试

  • image-loader : 加载并且压缩图片文件

  • babel-loader : 将ES6转换成ES5

  • css-loader : 加载CSS,支持模块化、压缩、文件导入等特性

  • style-loader:把CSS代码注入到JS中,通过DOM操作去加载CSS

webpack常见的plugin

  • html-webpack-plugin:创建HTML文件到输出目录,将webpack打包后的chunk自动引入到这个HTML文件

  • ModeleConcatenationPlugin:用于开启Scope Hoisting,可以让打包出来的文件更小,运行更快,ModuleConcatenationPlugin是webpack集成的优化插件,可以直接使用,使用是webpack4,mode已经自动集成了。

  • commons-chunk-plugin:提取公共代码

  • uglifyjs-webpack-plugin:通过uglifyES压缩ES6代码

如何利用webpack优化前端性能

  • 压缩代码。删除多余的代码、注释、简化代码的写法等等。可以利用webpack的Uglifyjs-webpack-Plugin和ParallelUglifyPlugin来压缩JS文件.

  • Tree-shaking,就是将代码中的一些死代码删除掉.可以通过在启动webpack时追加参数--optimize-minimize来实现

  • 通过Commons-Chunk-Plugin提取公共代码

是否写过Loader和Plugin?描述一下编写loader或plugin的思路

loader的作用就好比一个翻译官,把读取到的文件转义成新的文件内容,然后通过loader通过链式操作,将源文件一步步翻译成想要的样子.所以编写loader要遵循单一原则,每一个loader只做一件事,loader可以拿到源文件的内容,然后通过一系列的操作,将我们处理好的内容通过返回值的方式输出出去

webpack在运行的生命周期中会广播出许多事件,plugin可以监听这些事件,在合适的时机通过webpack提供的API改变输出的结果

webpack中loader和plugin的区别

实现功能的区别

因为webpack只能打包符合common.js的js文件,所以针对css,图片等格式的文件时没法打包的,就需要引入loader进行打包,loader偏向于转化文件

plugin相当于是扩展了webpack的功能.可以实现比如打包的优化,重新定义环境变量,

运行时间的区别

  1. loader运行在打包文件之前

  2. plugin在整个编译周期都起作用

webpack的热更新是如何做到的?说明其原理

webpack的热更新又称热替换,缩HMR,这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧模块

  1. 当文件系统中某一个文件发生改变时,webpack的watch能监听到文件的变化,对模块重新编译打包,并将打包后的代码通过简单的js对象保存在内存

  2. webpack-dev-serve和webpack之间的接口交互,而在这一步,dev-server的中间件webpack-dev-middleware和webpack之间的交互,webpack-dev-middleware调用webpaclk暴露的API对代码进行监控,并告诉webpack,将代码打包到内存中

  3. webpack-dev-server对文件的一个监控

  4. 通过webpack-dev-server的依赖在浏览器和服务器之间一个websock长链接

  5. webpack/hot/der-server根据webpack-dev-server传递给他的信息决定是刷新浏览器还是热更新

webpack怎么配置单页面,怎么配置多页面

单页面可以理解成webpack的标准模式,直接在entry入口中指定文件就好了

多页面应用的话,可以使用webpack的AutoWebPlugin来完成简单的自动化的构建,项目目录结构要遵循预设的规范.多页面值得注意的是:

  • 每个页面都有公共的代码,可以将这些代码抽离出来,避免重复的加载.比如每个页面都引用了同一套样式表

  • 随着业务的扩展,页面可能会不断的追加,所以一定要让入口的配置更加灵活,避免每次添加新页面还需要修改构建配置

webpack的构建流程

  1. 初始化参数:从配置文件和shell语句中读取与合并参数,得出最终的参数

  2. 开始编译:用上异步得到的参数初始化Compiler对象,加载所配置的插件,执行对象的run方法开始执行比编译

  3. 确定入口:根据entry找出所有的入口文件

  4. 编译模块:入口文件触发,调用所有配置的loader对模块进行编译,再找出该模块依赖的模块,再递归本步骤知道所有入口的依赖的文件都经过了处理

  5. 完成模块编译:

  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的Chuck,再把每个Chuck转换成一个单独的文件加入到输出列表,这不是修改输出内容的最后机会

  7. 输出完成,根据配置匹配路径和文件名

如何提高webpack的构建速度

  1. 多入口的情况下,使用commonChunkPlugin提取公共代码

  2. 使用webpack-uglify-parallel来提升uglifyPlugin的压缩速度

  3. 使用Tree-shaking和Scope Hoisting剔除多余代码

Axios

概念

Axios是一个基于promise的HTTP库,可以用在浏览器和node.js

特性

  • 从浏览器中创建XMLHttpRequests

  • 从node.js创建http请求

  • 支持Promise API

  • 拦截请求和响应

  • 装换请求数据和响应数据

  • 取消请求

  • 自动装换JSON数

  • 客户端支持防止CSRF

    就是让你的每个请求都带一个从cookie中拿到的key,根据浏览器同源策略,假冒的网站是拿不到cookie中的key的,这样,后台就能轻松辨别这个请求是否是用户在假冒网站上的误导输入,从而采取正确的策略

Vue

讲一讲什么是MVVM

MVVM是Model-View-ViewModel的缩写,也就是把MVC中的controller演变成ViewModel,Model层代表数据层,View代表UI组件(视图层),ViewModel是View和Model层的桥梁,数据会绑定到ViewModel层并自动将数据渲染到页面中,视图变化的时候会通知ViewModel层更新数据

MVVM的优点

  1. 分离视图和模型

  2. 降低代码耦合,提高视图或者逻辑的复用性

  3. 提高了模块的可测试性

为什么组件中的data必须是一个函数

因为组件可以被用来创建多个实例,如果data是一个纯粹的对象,则所有的实例将共享引用同一个数据对象,这样会造成数据共享,就是当你改变一个状态的时候,引用了同一个对象的实例同时也会发生改变!通过提供data函数,每次创建一个新的实例后,我们能够调用data函数,从而返回初始数据的一个全新副本数据对象

Vue2.x如何监听数组变化

vue通过重写数组的某些方法来监听数组变化,重写后的方法中会手动触发通知该数组的所有依赖进行更新。

在Observer构造函数中,在监听数据的时候会判断数据类型是否为数组,如果是数组,就将当前数组的原型对象指向重写后的数组方法对象

关于vue无法侦听数组及对象属性的变化的解决方案

我之前看过别人的一篇源码解析,其实object.defineProperty是可以监听到数组下标的变化的,他在源码里面对数组进行了特殊处理,尤雨溪说是因为性能代码和获得的用户体验收益不成正比,所以就放弃了这个特性

数组

可以监听到的情况

push,splice,赋值

无法监听到的情况

通过下标或者修改数组的长度

解决方法

  • this.$set(arr,index,value) -- 会修改原数组

  • 通过splice进行增删改

  • 利用临时变量进行中转

对象

可以监听到的情况

赋值

不可以监听的情况

通过打点的方式进行增删改

解决方法

  • this.$set(obj,key,value) -- 可以实现增加和修改

  • 使用深度监听,只能监听到对象的变化

  • 直接监听对象中的key

  • object.assgin将多个源对象分配到目标对象,再赋值给那个要监听的对象

Vue的响应式原理

vue2.x

Vue在初始化数据时,会使用Object.defineProperty重新定义data中的所有属性,当页面使用对应属性时,首先会进行依赖收集(收集当前组件的watcher),如果属性发生变化会通知相关依赖进行更新操作(发布订阅)

vue3.x

Vue3.x改用proxy代替Object.defineProperty.因为proxy可以直接监听对象和数组的变化,并且有多达13中拦截方法,并且作为新标准受到浏览器厂商重点持续的性能优化

Proxy只会代理对象的第一层,那么Vue3又是怎样处理这个问题的呢?

判断当前Reflect.get的返回值是否为Object,如果是则再通过reactive方法做代理,这样就实现了深度观测

监测数组的时候可能触发多次get/set,那么如何防止触发多次呢?

我们可以判断key是否为当前被代理对象target的自身属性,也可以判断旧值和新值是否相等,只有满足以上两个条件之一时,才有可能执行trigger

Vue双向数据绑定原理

Vue采用的是数据劫持结合发布订阅者模式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据或者视图发生变化时发布消息给订阅者,触发相应的方法.

双向数据绑定具体实现步骤:

  1. observer对所有对象进行递归遍历,给所有的属性都加上setter和getter,这样的话,当数据发生变化时,就会触发setter,实现监听数据的功能,并且拿到最新值将最新值通知订阅者.

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

  3. watcher订阅者是Observer和Compile之前通信的桥梁.主要做以下事件:

    • 在自身实例化时往属性订阅器里面添加自己

    • 属性变动时,setter会立即调用dep.notify(),订阅者调用自身的update()方法,并触发Compile中绑定的回调函数

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

    当执行 new Vue() 时,Vue 就进入了初始化阶段,一方面Vue 会遍历 data 选项中的属性,并用 Object.defineProperty 将它们转为 getter/setter,实现数据变化监听功能;另一方面,Vue 的指令编译器Compile 对元素节点的指令进行扫描和解析,初始化视图,并订阅 Watcher 来更新视图, 此时Wather 会将自己添加到消息订阅器中(Dep),初始化完毕。

      当数据发生变化时,Observer 中的 setter 方法被触发,setter 会立即调用Dep.notify(),Dep 开始遍历所有的订阅者,并调用订阅者的 update 方法,订阅者收到通知后对视图进行相应的更新。

发布订阅模式

定义

发布-订阅模式其实是一种对象间一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都讲得到状态改变的通知

订阅者把自己想要订阅的事件注册到调度中心,当不发布者发布该事件到调度中心,也就是触发该事件时,由调度中心统一调度订阅者注册到调度中心的处理代码

例子

比如我们喜欢看某个公众号的文章,但是我们并不知道什么发布新文章,需要不定时的去翻阅,这时候,我们可以关注该公众号,当有文章推送时,会有消息及时通知我们文章更新了

如何实现发布-订阅模式?

  • 创建一个对象

  • 在该对象上创建一个缓存列表(调度中心)

  • on方法用来把函数都加到缓存列表中(订阅者注册事件到调度中心)

  • emit方法取到arguments里第一个当做event,根据event值取执行对象缓存列表中的函数(发布者发布事件到调度中心,调度中心处理代码)

  • off方法可以根据event值取消订阅

  • once方法只监听一次,调用完毕后删除缓存函数(订阅一次)

观察者模式

观察者直接订阅主题,而当主题被激活的时候,会触发观察者里事件

$set的作用

我们平常在开发的时候,会遇到这样一种情况,当我们向已经在vue的data里面声明或者已经赋值过的对象中添加新的属性,我们更新此属性的值的时候,视图不会跟着一起更新.

因为在Vue里面,只有在data里面存在的数据才能监听为响应式数据,新增的属性是不会成为响应式数据的,如果我们想让新添加的属性改变之后视图能够立即更新,我们可以使用vue提供的$set方法重新劫持新的数据,让新的数据也变成响应式数据,这样数据发生改变时,视图也就能够立即变化了

用法

Vue.set(Object,key,value)

object:想要添加属性的对象

key:添加到对象上的键

value:添加到对象上的值

Vue computed

计算属性的用法

Computed 本质是一个具备缓存的watcher,依赖的属性发生变化就会重新计算。 适用于计算比较消耗性能的计算场景。

计算属性的原理

Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性并返回此对象

Object.defineProperty(obj,prop,descriptor)的三个参数

obj:要定义属性的对象

prop:要定义或者修改属性的名称

descriptor:要定义或者要修改的属性描述符

descriptor里面有set,get访问对象中的属性就会触发get函数 改变对象中的属性就会触发get和set函数

通过路由传递参数,接收的参数就是发请求需要的参数

  1. this.$router.push({meta:{name:'bbb'},params:{id:id})传递.接收this.$route.params.id,类似于post传参.

    参数在url中动态绑定,在路由上的path属性上通过:参数的方式进行传参,接收也是通过this.$route.params.参数的方式

  2. 可以通过this.$router.push({path:'/b'},query:{id:id,value:value})传递

    通过this.$route.query.id&&this.$route.query.value可以获取到id和query的值.然后参数的值显示在url地址栏上,可以认为是get的传参query通过path传参params通过meta中的name传参

Computed和Watch的区别

Computed

computed相当于一个具有缓存机制的watcher,依赖的属性发生变化进行重新计算,然后更新视图,适用于比较复杂的计算场景.

Watch

watch没有缓存性,更多是观察的作用,可以监听数据发生改变后执行对应的回调.当我们需要深度监听对象中的属性时,可以打开deep:true选项,这样便会对对象中的每一项进行监听.不过这样来带来性能问题,优化的话可以使用字符串形式进行监听.如果没有写到组件中,需要手动使用unwatch进行注销

Vue编译器结构图

宏观理论上讲compile编译分成parse,optimize,generate三个阶段,最后得到render function

parse

parse会用正则等方式解析template模板中的指令,class,style等数据,形成AST(Abstract Syntax Trees)

optimize

主要作用是标记static静态节点,这是Vue在编译过程中的一处优化,后面当update更新界面时,会有一个patch的过程,diff算法会直接跳过静态节点,从而减少了比较的过程,优化了patch的性能

generate

generate是将AST转化成render function 字符串的过程,得到结果是render的字符串以及staticRenderFnc字符串

经历过parse,optimize与generate三个阶段后,组件中就会存在渲染VNode所需的render function

Vue生命周期

什么是生命周期

vue中的每一个组件都是独立的,每个组件都有属于自己的生命周期函数,从一个组件创建,数据初始化,挂载,更新和销毁,这就是所谓的一个完整的生命周期

第一次页面加载会触发几个钩子函数

第一次加载会触发 beforeCreate、created、beforeMount、mounted

生命周期的阶段

主要分为八个阶段

  1. beforeCreate 是new Vue()之后触发的第一个钩子,在当前阶段data、methods、computed以及 watch上的数据和方法都不能被访问。

  2. created 在实例创建完成后发生,当前阶段已经完成了数据观测,也就是可以使用数据更改数据,在这里更改数据不会触发updated函数。可以做一些初始数据的获取,在当前阶段无法与Dom进行交互, 如果非要想,可以通过vm.$nextTick来访问Dom。

  3. beforeMount 发生在挂载之前,在这之前template模板已导入渲染函数编译。而当前阶段虚拟Dom已经创建完成即将开始渲染。在此时也可以对数据进行更改不会触发updated。

  4. mounted 在挂载完成后发生,在当前阶段,真实的Dom挂载完毕,数据完成双向绑定可以访问到 Dom节点,使用$refs属性对Dom进行操作。

  5. beforeUpdate 发生在更新之前,也就是响应式数据发生更新,虚拟dom重新渲染之前被触发,你可以在当前阶段进行更改数据,不会造成重渲染。

  6. updated 发生在更新完成之后,当前阶段组件Dom已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新。

  7. beforeDestroy 发生在实例销毁之前,在当前阶段实例完全可以被使用,我们可以在这时进行善后收尾工作,比如清除计时器。

  8. destroyed 发生在实例销毁之后,这个时候只剩下了dom空壳。组件已被拆解,数据绑定被卸除,监听被移出,子实例也统统被销毁。

导航守卫

  • 全局前置守卫

    可以使用router.beforeEach((to,from,next)=>{})注册全局前置守卫

    三个参数

    • to:Router 即将要进入的路由对象

    • from:Router 当前导航正要离开的路由

    • next:Function 满足条件就可以调用next()

  • 全局解析守卫

  • 全局后置钩子

Vue的优缺点

Vue优点

  1. 轻量级框架,也比较容易上手

  2. 组件化开发,增强了代码可维护性,可复用性,

Vue缺点

  1. vue使用的是SPA也就是单页面技术,没有SEO

Vue组件通信

父传子

在父组件中,通过v:bind将数据绑定引入进来的在子组件上,在子组件中通过props进行接收父组件里传递是数据

子传父

通过自定义事件,this.$emit('子组件中自定义的事件',要传递的数据)

EventBUS(兄弟,跨层级)

使用EventBUS的步骤

创建一个BUS.js文件,在里面引入Vue,并默认暴露vue的实例

EventBUS的具体实现

首先创建一个BUS.js,在BUS.js里面导出一个新的vue实例.在需要使用EventBUS的组件中引入BUS.比如一个组件A要传递数据给组件B,在组件A中可以通过BUS.$emit()指定一个自定义事件,将要传递的数据也写在$emit里面,然后在组件B中通过BUS.$on()监听$emit里面的事件,然后定义事件触发之后执行的回调函数

$parent/$children与ref

ref在普通元素和组件上的区别

如果在普通元素上使用,通过this.$refs指向的就是DOM元素;如果在子组件上,指向的就是组件本身,取到组件本身之后就直接调用组件内的方法和访问数据.

$parent/$children

可以直接获取到父组件或者子组件调用组件内的方法和访问数据

provide/inject

祖先组件中通过provide提供变量,然后在子孙组件中通过inject来注入变量

注意事项

provide和inject绑定并不是响应的.

provide和inject怎么实现数据响应式

Vue.Observerable()

$attrs/$listeners

多级组件嵌套需要传递数据,通常使用的方法是通过vuex,但是只需要传递数据,而不做中间处理,使用vuex有点大材小用的感觉.就可以使用$attrs,$listeners.

$attrs包含了父作用域中不被prop所识别的特性绑定,当一个组件没有声明任何props时,$attrs里面会包括所有父作用域的绑定,并且可以通过v-bind="$attrs"传入内部组件,可以配合inheritAttrs一起使用

$listeners包含了父作用域中的v-on事件监听器,可以通过v-on="$listeners"传入内部组件

vuex(兄弟,跨层级,数据量大)

有哪几种属性

State

Getters

Mutations

Actions

Modules

Vuex的State特性是什么

  • vuex就是一个仓库,仓库里面放了很多对象.其中的state就是存放数据源的地方,相当于一般vue对象里面的data

  • state里面存放的数据时响应式的,vue组件从store读取数据,若是store中的数据发生改变,依赖这项数据的组件也会发生更新

  • 它通过mapState把全局的State和Getters映射到当前组件的Computed计算属性中

Vuex的Getters特性

  • getters可以对state进行计算操作,他相当于store的计算属性

  • 虽然在组件内也可以做计算属性,但是getters可以在多组件之间复用

  • 如果一个状态值在一个组件内使用,可以不用getters

Vuex的Mutations特性

  • 可以同步更新state中的数据

  • 在store文件的mutations里面定义函数类型,mutations里面还可以接受组件传递过来的参数

  • 组件中使用this.$store.commit('mutations里面定义的函数',传递的参数)

Vuex的actions特性

  • action是里面可以包括任意的异步操作,比如可以把请求都在仓库的actions里面进行

  • action有一个context对象,这个context对象里面有当前store实例相同的方法和属性

  • 在组件中使用this.$store.dispatch('action是里面定义的函数')

Vuex的modules

作用

使用不同板块管理不同板块的代码,实现一定程度上的解耦,便于维护管理

命名空间

  • 一旦使用modelu管理仓库,所有module的state状态都会默认以其module名为其命名空间

  • 使用命名空间后,无法直接mapState在组件中获取getters的数据

  • dispatch和commit调用拥有命名空间的模块的方法需要在括号内方法名的前面加上命名空间

  • 获取state和getter的值

    • 需要通过import 引入createNameSpacedHelpers

    • 通过const {mapState,mapGetters}=createNameSpacedHelpers('模块名')

nextTick实现原理

因为Vue实现响应式不是数据发生变化之后DOM立即发生变化,而是按照一定的策略进行DOM的更新,而$nextTick的用法就是在下次DOM更新循环之后执行延迟回调,也就是数据更新后,在DOM中渲染完成之后就执行$nextTick方法中的代码.

nextTick的运用场景

  1. created生命周期函数中想要操作DOM就必须使用$nextTick

  2. 因为在mounted生命周期函数阶段,并不是所有的子组件都能百分之百的挂载成功,如果想要在整个视图渲染完毕之后再操作DOM,也可以使用$nextTick

  3. 总的来说就是你希望在DOM完成更新之后操作DOM就应该使用$nextTick

Keep-alive

概念

keep-alive是Vue的一个内置组件,当他包裹动态组件时,会缓存不活动的组件实例,而不是销毁他们.他是一个抽象组件,他自身不会渲染一个DOM元素,也不会出现在父组件中

作用

在组件切换过程中将状态保留在内存中,防止重复渲染DOM,减少加载事件以及性能消耗

原理

在created函数调用时将需要缓存的虚拟DOM节点保存在this.cache中,在页面渲染时,如果虚拟DOM的name符合缓存条件(用include和exclude控制),则会从this.cache中取出之前缓存的虚拟DOM实例进行渲染

Props

  • include - 字符串或者正则表达式.只有名称匹配的组件会被缓存

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

  • max - 数组.最多可以缓存多少个组件实例

生命周期函数

  • activated

    在keep-alive组件激活时调用

    该钩子函数在服务器端渲染期间不被调用

  • deactivated

    在keep-alive组件停用时调用

    该钩子函数在服务器端渲染期间不被调用

如果每次进入页面需要获取最新的数据.可以在activated阶段获取数据,相当于原来在created生命周期函数中获取数据的任务

keep-alive的应用场景

比如说在开发中,经常会遇到这一场景,从列表页进入详情页,需要缓存列表页的原来数据以及滚动位置,这时候就需要keep-alive缓存状态

mvc模式理解

Vue-router

Vue-router的配置

  1. 引入 import VueRouter from 'vue-router'

  2. 全局注册 Vue.use(VueRouter)

  3. 定义routes数据结构;包括路径path,路由组建component等

  4. 实例化路由对象 const router = new VueRouter({ routes:routes})

  5. 将路由挂载在Vue实例上

Virtual DOM和KEY属性的作用

Virtual DOM

为什么需要虚拟DOM

因为现在页面上的功能越来越多,需要实现的需求也越来越复杂,对DOM的操作也越来越频繁.但是经常操作DOM的代价会很高,会引起页面的重绘和重排,增加浏览器的性能开销.

什么是Virtual DOM

Virtual DOM本质就是用一个原生的JS对象去描述一个DOM节点.是对真实DOM的一层抽象.最后可以通过一系列的操作将Virtual DOM映射到真是环境上.

Virtual DOM在Vue.js中主要做了两件事:

  • 提供与真实DOM节点所对应的虚拟节点VNode

  • 将虚拟节点VNode和旧虚拟节点OldVNode进行对比,然后更新视图

真实DOM和虚拟DOM的区别

使用原生JS或者JQ操作DOM,浏览器会从构建DOM树开始从头到尾执行一遍流程.比如说,在一次操作中,我需要更新10个DOM节点,浏览器收到第一个DOM请求后并不知道还有9次更新操作,所以会马上执行流程,最终会执行10次.

而使用虚拟DOM一次性更新10个DOM,虚拟DOM不会立即操作DOM,而是将这10次更新保存在一个JS对象中,最后对这个JS对象只需要操作一次,避免了大量无畏的计算量

使用虚拟DOM的好处

页面的更新可以先全部反应在JS对象上,操作内存中的JS对象的速度要快一点,等更新完成后,再将最终的JS对象映射成真是的DOM,交由浏览器去绘制

KEY的作用

key的作用就是尽可能的复用DOM元素

新旧children中的节点只有顺序是不同的时候,最佳的操作应该是通过移动元素的位置来达到更新的目的.

需要在新旧children的节点中保存映射关系,以便能够在旧children的节点中找到可以复用的节点,key就是children中节点的唯一标识

key值大多情况下使用在循环语句中,从本质来讲主要作用大概有以下两点:

  1. 主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes,相当于唯一标识ID。

  2. Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染, 因此使用key值可以提高渲染效率,同理,改变某一元素的key值会使该元素重新被渲染。

全局注册一个组件

基于webpack的require.context方法遍历指定目录,获取所有匹配条件的文件,然后使用Vue.component方法将组件全局暴露

.hash路由和history路由实现原理说一下

hash

hash就是一个路径中#后面的内容,hash虽然说出现在HTTP中,但是路由之间的跳转不会向后端发送请求,而是在本地的index.html中直接匹配

history

history模式中,每一次页面之间的跳转,我都会向后端发送请求,然后后端返一个index.html,我再根据路由匹配符在本地进行匹配

v-mdoel的原理

model指定prop属性名来接收外界传递进来的数据,然后再props里面定义prop的属性

model还可以指定event事件名,通过在合适的位置调用this.$emit('事件名',要传递的数据)将数据传递到外界

Nuxt.js

为什么使用Nuxt.js?

因为Vue单页面应用渲染从服务器获取所需js,在客户端将其解析生成html挂在于id为app的DOM元素上,这样会存在两个问题

  • 由于资源请求量大,会造成网站首屏加载缓慢,不利用用户体验

  • 由于页面内容通过js插入,对于内容性网站来说,搜索引擎无法抓取网站内容,不利用SEO

什么是Nuxt.js?

Nuxt.js是一个基于Vue.js的通用应用框架,预设了利用Vue.js开发服务器端渲染的应用到所需要的各种配置.可以将html在服务器端渲染,合成完成的html文件再输出到浏览器端

Nuxt.js和Vue.js的区别

NuxtVue
路由按照pages文件夹自动生成目录结构需要手动在src/router/index.js配置
入口页面页面入口文件为layouts/default.vue页面入口文件为src/APP.vue
webpack配置内置webpack,允许根据服务器需求,在nuxt.config.js中的build属性自定义构建webpack的配置,覆盖默认配置关于webpack的配置放在build文件夹下

安装nuxt.js

npx create-nuxt-app项目名

使用axios并跨域

因为nuxt默认安装axios,所以就需要安装proxy 通过 npm i @nuxtjs/proxy 然后在proxy里面进行配置

引入第三方插件

Mixin

平时我在开发中使用Mixin也主要就是将与实现功能无关的代码都分离出来,尽量的让我们的代码处于一种低耦合的状态,以便于自己日后的管理和维护.比如在封装表单的时候,会把表单的验证功能抽离出来,放在一个单独的Mixin文件中,在Mixin文件中处理我们需要的验证功能,然后将Mixin文件import到我们的表单组件中去.

为什么要避免V-if和V-for一起使用

因为V-for比V-if的优先级要高,如果两个指令一起使用的话,每次v-for之后都会执行v-if,会造成不必要的计算,影响性能,特别是for循环的元素中只有小部分使用v-if的时候.

V-if和V-show的异同

相同点

v-if和v-show都能控制元素的显示和隐藏

不同点

v-ifv-show
实现方法通过动态的向DOM树内添加或者删除DOM元素显示元素的显示与隐藏通过设置元素CSS的Display属性的值实现元素的显示与隐藏
编译如果初始值为false不会编译不管初始值是false还有true都会进行编译
性能对DOM不停的添加和删除,性能比v-show差操作css,性能更好
使用场景不需要频繁切换时使用需要频繁切换时使用

Vue中使用代理配置解决跨域

  1. 创建一个vue.config.js文件

  2. 在文件中配置proxy代理

  3. 去axios的配置文件中修改baseURl为代理中设置的匹配符

实现api的三层封装,基于页面管理api

  1. 在axios.js里面设置请求拦截器,响应拦截器,baseUrl,超时

  2. 创建页面api文件,默认暴露对象,对象中是所有的请求方法

  3. 在api.js入口文件引入所有页面的api对象,默认暴露对象,在对象中将所有页面的api对象展开

  4. 在main.js中引入api.js暴露的对象,并且将api.js暴露的对象注册到Vue的原型上

SPA页面如何进行SEO

预渲染(Prerender)

使用场景

静态页面的展示,比如说一些比较简单的官网

使用Prerender-SPA-Plugin

在config.js文件中进行配置 Prerender-SPA-Plugin插件

项目中所有的路由,最终生成后有几个页面,都以这个配置为依据

new PrerenderSPAPlugin({
    // 代码打包目录
    staticDir:path.join(__dirname.'dist'),
    // 要预渲染的页面路由
    routes:['/','/home','/information','......'],
    renderer:new Render({
        // 渲染时是否显示浏览器窗口
        headless:false,
        // 等到事件触发后渲染
        renderAfterDocumentEvent:'render-event'
    })
})

原理

在Webpack构建阶段的最后,在本地启动一个Puppeteer的服务,访问配置了预渲染的路由,然后将Puppeteer中渲染的页面输出到HTML文件中,并建立路由对应的目录

服务器渲染(SSR)

服务器渲染(SSR)

SSR从根本上解决了SEO的问题,但是实践起来比较麻烦,一方面是因为上手难度高:必须使用Node.js、业务代码要进行同构化改造、Webpack需要多一套配置、还需要审查第三方库是否支持SSR.另一方面,SSR使得前端的工作需要在浏览器和服务器之间回来切换.单纯更加复杂

什么是服务端渲染

服务端将完整的html页面输出到客户端进行显示

为什么使用服务端渲染

  • 更好的SEO

  • 内容到达时间更快

使用场景

适用于大型的,页面数据处理较多且较为复杂,与服务端有数据交互的功能性网站,比如说电商网站

如何优化SPA首屏加载速度慢的问题

  1. 路由懒加载

  2. 使用cdn的方式引入第三方组件库

  3. lodash按需加载

  4. 减少DOM数量和请求数

  5. 图片懒加载

  6. SSR

Vue项目打包上线

  1. 在命令窗口执行npm run build 打包时注意检查一下环境变量

  2. 打完包后将Dist目录发给后端 打包完之后将index.html里面的路径改成相对路径

Vue的高级技巧

事件总线

React

dom-diff

列表key属性

jsx原理(createElement)

React-router原理

Redux组件通信

生命周期

React setState

React组件通信

性能优化

Internet

URL的组成

协议(http:// |https://)+服务器主机地址(可以是域名,主机名,ip地址)+端口(一般都是默认的)+路径+参数+hash

HTTP1 HTTP2 HTTP3

HTTP请求方式

HTTP1.0定义的请求方式

  • GET

  • POST

  • HEAD

HTTP2.0新增的请求方式

  • OPTIONS

  • PUT

  • DELETE

  • TRACE

  • CONNECT

浏览器从输入网址到回车发生了什么

1.DNS解析,每当我们在浏览器中输入一个网址时,其实不是真正意义上的访问的浏览器的地址,互联网上的每一台计算机的唯一标识就是他们的ip地址,但是ip地址并不好记,所以互联网设计者做了一件可以让网址到ip地址的转换的事情,这个过程就是DNS解析.

1.1.DNS解析的过程.DNS解析是一个递归查询的过程;例如说查找www.google.com的IP地址.首先在本地域名服务器中查询IP地址,如果没有找到的情况下,本地域名服务器会向根域名服务器发送一个请求,如果根域名服务器也不存在该域名时,本地域名向com顶级域名服务器发送一个请求,以此类推下去.知道最后本地域名服务器得到Google的IP地址并把它缓存在本地,供下次查询使用,

2.进行TCP连接,也就是通常说的三次握手

3.发送HTTP请求

4.服务器处理请求并返回HTTP报文

5.浏览器解析渲染页面

浏览器是一个边解析边渲染的过程.首先浏览器解析HTML文件构建DOM树,然后解析CSS文件构建渲染树,等到渲染树构建完成后,浏览器开始布局渲染树并将其绘制到屏幕上.这个过程比较复杂,设计到两个概念:回流和重绘.DOM节点中的各个元素都是以盒模型的形式存在,这些都需要浏览器去计算其位置和大小等,这个过程称为回流,当盒模型的位置,大小等确定下来之后.浏览器便开始绘制内容,这个过程称为重绘,页面在首次加载时必然会经历回流和重绘,而这两者又是非常消耗性能的,尤其是在移动端,它会破坏用户体验,有时会造成页面卡顿,所以我们应该尽量减少回流和重绘.

5.5导致回流发生的一些因素

  1. 调整窗口大小

  2. 改变字体

  3. 增加或者移除样式表

  4. 内容变化,比如用户在 input 框中输入文字, CSS3 动画等

  5. 激活 CSS 伪类,比如 :hover

  6. 操作class属性

  7. 脚本操作DOM

  8. 计算offsetWidthoffsetHeight属性

  9. 设置 style 属性的值

6.连接结束关闭TCP连接(四次挥手)

6.1为什么建立连接的时候进行的是三次握手,断开连接时要进行四次挥手

前端跨域

什么是同源

协议,域名,端口都相同

什么是跨域

就是不同域之间进行的相互资源请求

为什么出现跨域

浏览器就解析javascript处于安全方面的考虑,只允许在同域名下页面进行相互资源请求调用,不允许调用其他域名下的页面的对象;简单的理解就是因为javascript同源策略的限制.

注意:跨域并不是请求发布出去,请求能发出去,服务器能收到请求并正常返回的结果,只是结果被浏览器拦截了,所以页面无法正常使用数据

什么是同源策略

同源策略就是要求源相同才能进行正常的资源交互,即要求当前页面与调用资源的协议,域名,子域名,端口完全一致,不一致就是跨域

同源策略的限制

同源策略限制一个资源地址加载的文档或脚本与来自另一个资源地址的资源进行交互,这是浏览器的一个用于隔离潜在恶意文件的关键的安全机制.它的存在可以保护用户隐私信息,防止身份伪造的等(读取cookie)

同源限制的内容

1.cookie.localstorage.indexedDB等存储型内容

2.不允许进行DOM节点的操作

3.不能进行AJAX请求

同源策略的天然支持跨域请求的特性属性

<img src ='xxx'>
<link href = 'xxx'>
<script src = 'xxx'></script>

解决同源策略的方法

1.JSONP方法

原理

利用<script src ='xxx'>元素的天然支持跨域的策略,网页跨域得到从其他来源动态产生的JSON数据,JSONP请求一定需要对方的服务器做支持才可以

JSONP和AJAX对比

JSONP和AJAX相同,都是客户端向服务器发发送请求,从服务器端获取数据方式.但AJAX属于同源策略,JSONP属于非同源策略(跨域请求)

JSONP优缺点

优点是兼容性好,可用于解决主流浏览器的跨域数据访问的问题.

缺点是只支持get方法,具有局限性

JSONP的流程

<script>
function fn(data){
	alert(data.msg)
}
</script>
<script src='http://crossdomain.com/jsonServerResponse?jsonp=fn'>
 </script>

1.声明一个回调函数,其函数名当做参数值,要传递给跨域请求数据的服务器,函数形参为获取目标数据(服务器返回的data)

2.创建你一个<scipt>标签,把那个跨域的API数据接口地址,赋值为script标签的src属性,还要在这个地址中向服务器传递该函数名(跨域通过问号传递:?jsonp=fn)

2.2服务器收到请求后,需要进行特殊的处理,把传递进来的函数名和它需要给你的数据拼接成一个字符串,例如:传递进去的函数名为fn,它准备好的数据时fn([{'name':'jianshu'}])

3.最后服务器把准备的数据通过HTTP协议返回给客户端,客户端再调用执行之前声明的回调函数,对返回的数据进行操作

4.最后服务器返回给客户端的数据的格式为

fn([{msg:'this is json data'}])

2.CORS

原理

整个CROS通信过程,都是浏览器自动完成,不需要用户参与.对于开发者来说,CORS通信和同源的AJAX通信没有差别,代码完全一样.浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但是用户不会有感觉,因此,实现CORS通信的关键是服务器.只要服务器实现了CROS接口,就可以跨源通信

CORS优缺点

CORS要求浏览器和服务器的同时支持,是跨域的根本解决方法,由浏览器自动完成

优点在于功能更加强大支持各种HTTP请求的方式,缺点是兼容性不如JSONP

实现过程

header('Access-Control-Allow-origin:*')
header('Access-Control-Allow-Methods:POST,GET')

例如:网站http://localhost:63342/页面要请求http://localhost:3000/users/userlist页面,userlist页面返回json字符串{name:'xxx',gender:'xxx',career:'xxx'}

//在服务器端设置同源策略地址
router.get('/userlist',function(req,res,next){
var user={name:'xxx',gender:'xxx',career:'xxx'};
res.writerHeader(200,{'Access-Control-Allow-Origin':'http://localhost:63342'});
res.write(JSOn.stringify(user));
res.end()
})

在响应头上添加了Access-Control-Allow-Origin属性,指定同源策略的地址.同源策略默认地址是网页的本身.只要浏览器检测到响应头是哪个带了CORS.并且允许的包括了本网站,那么久不会拦截请求响应.

header('Access-Control-Allow-Origin:*')
//*表示所有其他的域可以向当前域发送请求
header('Access-Control-Allow-Origin:http://test.com:80)
//http://test.com:80表示指定具体的这个域可以向当前域发送请求,也可以指定多个域名,用逗号分隔

3.WebSocket

WebSocket是HTML5一个持久化的协议,他实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案.WebSocker和HTTP都是应用层面协议,都基于TCP协议.但是WebSocket是一种双向通信协议,在建立连接之后,WebSocket和server与client都能主动向对放发送或接收数据.同时,WebSocket在建立连接时需要借助HTTP协议,连接建立好了之后client和server之间的双向通信就与HTTP无关了

4.postMessage

如果两个网页不同源,就无法拿到对象的DOM.典型的例子就是iframe窗口的window.open方法打开的窗口,他们与父窗口无法通信.HTML5为了解决这个问题,引入了一个全新的API,跨文档API(Cross-document messaging).这个APIwindow对象新增了一个window.postMEssage方法.允许窗口通信.不论这两个窗口是否同源postMessage方法的第一个参数是具体的信息内容,第二个参数是接收信息的窗口的源,即协议+域名+端口/.也可以设置为*,表示不限制域名,像所有窗口发送

//例子:http://localhost:63342/index.html页面向http://localhost:3000/message.html传递'跨域请求信息'
//发送信息http://localhost:63342/index.html
<html lang='en>
<head>
<meta charset ='UTF-8'>
<title>跨域请求</title>
</head>
<body>
<iframe src ='http://localhost:3000/user/reg' id='frm'></iframe>
<input type ='button' value ='ok' οnclick='run()'>
</body>
</html>
<script>
function run(){
var frm =document.getElemntById('frm')
frm.contentWindow.postMessage('跨域请求信息','localhost:3000')
}
</script>

//接收信息页面http://localhost:3000/message.html
window.addEventLIstener('message,function(e){
console.log(e.data)
},false)

5.代理

因为同源策略是针对于浏览器的安全策略,而不是服务器的.所以我们可以将浏览器的请求代理给服务器去完成.

在vue.config.js设置proxy的配置

浏览器缓存

JWT

全称为Json Web token,就是token秘钥

  1. 后端将用户信息加密,返回到前端

  2. 前端接收后保存在本地

  3. 在axios请求中,带上token(一般放在请求头)

Cookies,session,token,localstorage,sessionstorage

状态码

状态码的类型

http状态码由三个十进制数字组成,第一个十进制数字定义了状态码的类型

1**信息,服务器收到请求,需要请求者继续执行操作

2**成功,操作被成功接收并处理

3**重定向,需要进一步的操作以完成请求

4**客户端错误,请求包含语法错误或无法完成请求

5**服务器错误,服务器就处理请求的过程中发生了错误

常见的状态码

200-请求成功

301-资源(网页等)被永久转移到其他URL

403-服务器理解请求客户端的请求,但是拒绝执行此请求

404-请求的资源(网页等)不存在

500-内部服务器错误

TCP连接和断开(三次握手,四次挥手)

三次握手

第一次握手:建立连接时,客户端发送syn包到服务器,并进入到SYN-SENT状态(请求连接状态),等待服务器确认;SYN:同步序列编号(Synchronize SEquence NUmbers)

第二次握手:服务器收到SYN包,必须确认客户的SYN,同时自己也发送一个SYN+ACK包,此时服务器进入SYN-RECV状态(服务端被动打开后,接收到了客户端的SYN并且发送了ACK时的状态。再进一步接收到客户端的ACK就进入ESTABLISHED状态。)

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACk,此包发送完毕,客户端和服务器进行ESTABLIHED(TCP连接成功)状态,完成三次握手

四次挥手

TCP连接的释放必须是一方主动释放,另一方被动释放

1.首先客户端想要释放连接,想服务端发送一段TCP报文,其中:

标记位为FIN,表示请求释放连接

序号为Seq=U

随着客户端进入FIN-WAIT-1阶段,即半关闭状态,并且停止在客户端到 服务端方向上发送数据,但是客户端任然能够从服务端接受数据.

2.服务器端收到从客户端发来的TCP报文之后,确认了客户端想要释放连接,随后服务器端结束了ESTABLISHED阶段,进入了CLOSE-WAIT阶段(半关闭状态)并返回一段报文,其中:

标记位为ACK,表示收到了客户端发送的释放连接的请求

序号为Seq=V

确认号为ACK=U+1,表示在收到客户端报文的基础上,将其序号Seq的 值加1作为本段报文确认号ACK的值

随后服务器端开始释放从服务器端到客户端方向上的连接

客户端收到从服务器端发送的TCP报文之后,确认了服务器收到了客户端发送的释放连接请求,随后客户端接收FIN-WAIT-1阶段,进入了FIN-WAIT-2阶段.

前"两次挥手"既让服务器端知道了客户端想要释放连接,也让客户端知道了服务器端了解了自己想要释放连接的请求。于是,可以确认关闭客户端到服务器端方向上的连接了

3.服务器端自从发出ACK报文之后,经过CLOSED-WAIT阶段,做好了释放从服务器端到客户端方向上 连接准备,再次向客户端发送一段TCP报文,其中:

  • 标记位为FIN,ACK,表示已经做好了释放连接的准备了;注意:这里的ACK报文并不是收到服务器端报文的确认报文

  • 序号为Seq=W

  • 确认号为ACK=U+1;表示收到客户端报文的基础上,将其序号Seq的值加1作为本段报文确认号ACk的值

随后服务器端结束CLOSE-WAIT阶段,进入LAST-ACK阶段。并且停止在服务器端到客户端的方向上发送数据,但是服务器端仍然能够接收从客户端传输过来的数据。

4.客户端收到了从服务器端发出的TCP报文,确认了服务器端做好释放连接的准备了,结束FIN-WAIT-2阶段,进入TIME-WAIT阶段,并向服务器端发送一段报文,其中:

  • 标记位为ACK,表示收到服务器准备好释放连接的信号

  • 序号为Seq=U+1;表示是在收到了服务器端报文的基础上,将其确认号Ack值作为本段报文序号的值。

  • 确认号为Ack=W+1;表示是在收到了服务器端报文的基础上,将其序号Seq值作为本段报文确认号的值。

随后客户端开始在TIME-WAIT阶段等2MSL

后“两次挥手”既让客户端知道了服务器端准备好释放连接了,也让服务器端知道了客户端了解了自己准备好释放连接了。于是,可以确认关闭服务器端到客户端方向上的连接了,由此完成“四次挥手”。

Project

对于token的及时更新问题

原理

  1. 登录成功之后会从后端获取到token,刷新token的令牌,token的时效以及登录的时间点存储下来

  2. 当用户向后台发送请求时,将当前时间和登录时间的差和token的时效进行对比,如果当前时间和登录时间的差即将接近token的时效,就是用refresh_token去重新获取token以及新的refresh_token

  3. 使用refres_token重新获取token的操作实际上就是再次进行了一次登录,只不过本次的登录不是账号密码的登录,而是利用refresh_token,这个操作是用户不知情的

组件的二次封装

项目优化

编码阶段

  • 尽量减少data中数据,data中的数据都会增加getter和setter,会收集对应的watcher

  • v-if和v-for不能连用

  • 如果需要使用v-for给每项元素绑定事件时使用事件代理

  • SPA页面采用keep-alive缓存组件

  • 在更多的情况下,用v-if替代v-show

  • key保证唯一,而且尽量不使用index

  • 使用路由懒加载、异步组件

  • 防抖、节流

  • 第三方模块按需导入

  • 长列表滚动到可视区域动态加载

  • 图片懒加载

SEO优化

  • 预渲染

  • 服务器渲染SSR

打包优化

  • 压缩代码

  • 使用cdn加载第三方模块

  • Tree Shaking/Scope Hoisting

  • 多线程打包happypack

  • splitChuck抽离公共文件

  • sourceMap优化

用户体验

  • 骨架屏

  • PWA

怎么实现设置权限

基本实现思路

  1. 我们可以在用户登录的时候,获取到登录的角色,然后可以将角色保存在本地,

  2. 然后我们可以在给需要进行权限设置的路由添加添加元信息

  3. 将路由分离成静态路由和动态路由

  4. 将保存在本地的角色与路由里面的元信息进行对比,就是判断路由中元信息中的role是否包含存储在本地的角色,如果有子路由的话还需要进行递归遍历

  5. 通过router.addRoutes将筛选出来的路由追加到router实例中.

如果说有的路由不是通过菜单栏进行跳转的,我们还需要给有争议的路由再添加一个是否显示的标记,然后计算要显示出来的菜单,把菜单存储在本地上,然后在需要渲染的页面上获取menu,在页面上进行遍历渲染

怎么实现登录功能

  1. 导航守卫会对所有的请求进行拦截,如果有token就放行,没有token就跳转到登录页面

  2. 用户登录成功之后,拿到都后端返回来的token,将token存储在localstorage

  3. 就在axios请求头中添加token

描述一下你最近做过的一个项目

  1. 一品茗外卖后台管理系统.

  2. 首先是登录功能的实现嘛,通过的导航守卫和token,路有权限的管理

  3. 列表展示,就是将请求回来的数据通过element-ui的组件进行渲染

  4. 列表的删除和编辑功能,批量操作.批量操作通过element-ui复选框有的select自定义事件,将选中的数据存入一个空的数组,然后遍历整个列表的数组,对比新数组和列表数组的项,将新数组与列表数组相同的项的数据重新赋值

  5. 实现列表的查询功能

  6. 使用v-model的语法糖进行表单封装,

  7. 表单验证,将表单验证的代码使用mixin分离出来

  8. Echarts图表展示

Echarts使用步骤

  1. 通过npm i echarts -S引入echarts

  2. 通过echarts.init(Dom)初始化实例

  3. 指定图表的参数和配置

  4. 通过setOption显示图表

搭建一个项目

  1. 通过vue-cli 在命令窗口执行vue create 项目名称

  2. 选配置

项目流程

  • 比如说,现在公司接了一个项目,首先产品经理就要开始规划项目了,也就是要开会了

  • 开会讨论完了就要规划人员分配了

  • 架构师搭建项目的架构技术选型

  • UI出图,写基本代码

  • 对接口(swagger)

  • 记录项目里程

  • 测试通过,上线

Git

Git主要操作(日常上班操作)

  1. 第一件事 git pull 如果主分支有更新则回到你的分支 git rebase -i main

  2. 我自己分支上做的事情就是

    • 在自己本地写代码

    • 通过git add .将代码提供到暂存区

    • 通过git commit -m '修改面试' 提供到本地仓库

    • git push到远程

    • 进入在线仓库,找到自己的分支,发送pull request 合并请求 将自己的分支合并到主分支

    • 等我们的项目负责人同意合并请求

Git解决冲突

  1. 在vscode中处理相关冲突文件

  2. git add . 将所有的修复行为提交

  3. git rebase --continue 继续合并

  4. 合并成功 git push

让你印象最深刻的一个技术难点,最后怎么解决的,有什么心得?

你做的时间最长的一个项目,你能看到这个项目有哪些问题,你能做什么

你能给我们团队或者产品带来什么?

说一说你最近的一个项目

项目背景

Application

如何打包成apk

首先要登录,然后再工具栏的发行菜单点击云打包,然后在菜单上进行配置,在manifest文件中的基础配置里面获取appID,然后就可以进行打包了

封装微信小程序的数据请求

微信小程序中的数据请求封装和vue差不多

  1. 先创建一个env.js文件封装环境

    module export={
        // 开发环境
        dev:{
            baseUrl:''
        },
        // 生产环境
        production:{
            baseUrl:''
        },
        // 测试环境
        test:{
            baseUrl:''
        }
    }
  2. 创建一个request.js文件,来封装请求方法

    const {baseUrl} = require('./env.js').prod //引入你要使用的环境
    //封装ajax
    
    //配置专属域名
    const vipUrl = 'hjl'
    
    module.exports = {
      request: function (url, method = "GET", data = {}, isSubDomain = true) {
        let fullUrl = `${baseUrl}/${isSubDomain ? vipUrl : ''}/${url}`;
        wx.showLoading({
          title: '玩命加载中',
        })
    //isSubDomain就是代表是否需要专属域名
    
        return new Promise((resolve, reject) => {
    
          wx.request({
            url: fullUrl,
            method,
            data,
            header: {
              'Content-type': 'application/x-www-form-urlencoded'
              //根据要求来配置
            },
            success(res) {
              console.log('wx.request打印结果:', res)
              resolve(res.data.data)
              wx.hideLoading()
              
              if (res.statusCode === 200 && res.data.code === 0) {
                resolve(res.data)
                wx.hideLoading()
              } else {
                wx.showToast({
                  title: '接口有问题,请检查',
                })
                reject('接口有问题,请检查')
              }
            },
            
            fail(error) {
              wx.showToast({
                title: '数据接口有问题',
              })
              reject('数据接口有问题')
    
            }
          })
        })
      }
    }
  3. 在index.js中封装接口

  4. 在需要使用的页面的js中引入index直接调用

微信小程序如何引入第三方UI库

  1. 在微信开发者工具中打开终端

  2. 通过 npm init -y初始化一个package.json文件

  3. 然后通过npm 输入对应的指令下载对应的第三方UI库

  4. 构建npm

  5. 微信开发者工具的详情里面将使用npm模块勾选上

  6. 按照官网文档进行引用就行了

Problem

贵公司主营业务是什么?目前正在做什么项目?

目前技术团队大体的一个情况是怎么样的?

如果我有幸来到贵公司,我上班之前应该做哪些准备?

贵公司晋升机制是怎么样的?

Problem Project

Element-ui自身的分页问题

删除分页中最后一条数据的时候,会显示前一页的数据为空,currentPage并没有改变,需要手动改变currentPage

el-switch开关的问题

当我们做开关的时候,后端传过来的值是number或者string类型的,但是el-switch默认值是Boolean类型,想用number,string类型代替Boolea类型的值,就可以在标签的身上添加两个属性,active-value和inactive-value,如果是number类型,属性前面要加冒号变成绑定属性

鉴权

  1. 我们可以在用户登录的时候,获取到登录的角色,然后可以将角色保存在本地,

  2. 然后我们可以在给需要进行权限设置的路由添加添加元信息

  3. 将路由分离成静态路由和动态路由

  4. 将保存在本地的角色与路由里面的元信息进行对比,就是判断路由中元信息中的role是否包含存储在本地的角色,如果有子路由的话还需要进行递归遍历

  5. 通过router.addRoutes将筛选出来的路由追加到router实例中.

后端鉴权

  1. 首先是判断是否为登录状态嘛,未登录状态的话,就只能访问白名单的页面,访问需要登录的页面就需要重定向登录页

  2. 登录完成

    • 将用户session存储到cookie

    • 路由钩子获取动态路由信息

    • 递归解析后端路由

    • 通过addRoutes与基本路由信息进行拼接

    • 存储到vuex,用于侧边栏的显示

  3. 刷新要重新获取动态路由信息

  4. 与后端约定特殊的状态码,前端一旦受到就删除cookie,让系统成为未登录状态

移动端

移动端的滑动问题

步进器

打包性能优化

v-if,自定义指令,components

BScroll页面刷新后,滚动事件失效

在mounted声明周期函数初始化,刷新后,滚动事情失效,在初始化实例的时候,使用$nextTick,虽然是在Mounted生命周期函数,但是可能并不是所有的DOM都挂载完毕了,获取不到页面的DOM结构

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小杜coding

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值