前端面试题的学习与总结

系列文章目录

目录

目录

系列文章目录

前言

一、HTML部分

1、语义化标签

1.1.HTML 语义化的优点?

2、块状元素和内联元素

二、CSS部分

1、计算盒模型

2、margin纵向重叠

3、BFC问题

​编辑

4、圣杯和双飞翼布局

5、flex和grid

5.1什么是Flexbox和Grid布局,它们的主要区别是什么?

6、relative和absolute定位

7、水平居中和垂直居中方式

8、line-height行高继承

9、选择器权重问题

三、JS基础部分

1、值类型和引用类型

2、手写深拷贝 

3、变量计算和类型转换

4、原型及原型链(重中之重)!!!

5、作用域和闭包

6、this

7、闭包应用场景

 四、Promise异步部分

1、event loop

2、Promise

3、async和await

4、宏任务和微任务

5、手写Promise

五、从JS基础知识到JS Web API

1、DOM

1.1 DOM是哪种数据结构

1.2 DOM操作常用API

1.3 attribute和 property

1.4 DOM优化

2、事件

2.1事件绑定

2.2事件冒泡和事件代理

3、ajax

3.1跨域

3.2 Session、Cookie、Token

3.2.1 它们之间有什么关系?

3.3  localStorage和sessionStorage

六、HTTP和HTTPS

1、状态码分类:

2、methods

3、http和https缓存

4、刷新操作以及刷新对缓存的影响

5、https和http的区别

七、开发环境

1、从输入url到渲染出页面的整个过程

2、window.onload和DOMContentLoaded的区别

3、前端性能优化

4、安全

5、手写防抖节流

八、JS的一些基础以及数组方法的应用

1、一些基础面试题

1.1、var和let const的区别

1.2、typeof返回哪些类型

1.3、列举强制类型转换和隐形类型转换

2、手写深度比较模拟lodash库的isEqual

3、有关数组方法和一些常考的面试题

3.1split()和join的区别

3.2 数组 pop push  unshift  shift 分别做什么

3.3数组的API,有哪些是纯函数

3.4数组slice和splice的区别

3.5 [10,20,30].map(parseInt)

3.6ajax请求get和post的区别

3.7函数call 和 apply的区别

3.8闭包是什么?有何特性?有何影响?

4、有关DOM操作的一些API

4.1如何阻止事件冒泡和默认的行为?

4.2查找、添加、删除、移动DOM节点的方法

4.3如何减少DOM操作

4.4document load和ready的区别

4.5 ==和===的不同

5、有关函数面试题

5.1函数声明和函数表达式的区别

5.2 new Object()和Object.create()的区别

5.3关于this的场景题

5.4关于作用域和自由变量的场景题

5.5正则表达式

5.6关于作用域和自由变量的场景题2

5.7 如何获取多个数字中的最大值

5.8 如何用JS实现继承

6、JS的一些基础问题和一些数据储存方式

6.1如何捕获JS程序中的异常?

6.2什么是JSON?

6.3获取当前页面url参数

6.4Map和Set

6.5数组求和 Array reduce

九、框架篇:从VUE2到VUE3(1)

1、指令、插值

2、computed和watch

3、条件渲染和循环渲染(v-if和v-show和v-for)

3.1v-if v-else v-show写用法

3.2 v-if 和v-show的区别

3.3 v-if 和 v-show的使用场景

3.4 v-for的用法

3.5key的重要性

3.6一个注意点

4、事件

5、按键修饰符

6、表单

7、组件

8、生命周期(单个组件)和生命周期(父子组件)

9、Vue的高级特性

9.1 自定义 v-model

9.2 $nextTick

9.3 slot插槽

9.4 异步组件

9.5路由懒加载

9.6 keep-alive组件缓存

9.7 mixin

10、Vuex使用

11、路由router

11.1 基本使用

11.2路由切换的两种方式

11.3多级路由

11.4路由的两种传参方式

11.5方便路由接参\传参的几种配置

11.6路由守卫

11.7 两个新的生命周期钩子

11.8 路由器的两种工作模式

十、Vue原理:

1、数据驱动视图 MVVM模型

2、Vue响应式

2.1核心API

2.2数据代理

3、DOM操作

3.1 vdom

4、模板编译

4.1with语法

4.2编译模板

5、组件渲染/更新过程

​编辑

6、前端路由原理

7、面试题汇总

十一、Vue3

1、Vue3比Vue2有什么优势?

2、Compositions API(组合式API) 和 Options API(选项API)

3、生命周期

4、ref、toRef和toRefs

5、进阶深入理解

5.1为何需要ref?

5.2为何需要 .value

5.3为何需要 toRef toRefs

6、Vue3升级了哪些重要的功能?

6.1createApp

6.2多事件

6.3emits 属性

6.4Fraginent

6.5生命周期6.6移除.sync

6.7异步组件的写法

6.8Suspense

6.9移除filter

6.10Composition API

6.11Teleport

7、Composition API实现逻辑复用

8、Proxy实现响应式

9、对vue3的一些补充

9.1setup中如何获取组件实例

9.2watch和watchEffect的区别

9.3Vue3为何比Vue2快

9.4Vite

9.5Vue3 script setup

 9.6 ref和reactive的区别

9.7 vue2和vue3的区别

9.8 setup的注意点

9.9vue3响应式原理

9.10 watcheffect函数

9.11 vue3 watch函数的注意点​编辑

9.12Class组件和函数组件有什么区别?

9.13Hooks是什么,

9.14 你能列举几个常用的Vue 3中的Composition API吗?

9.15 请解释Vue中的状态(state)和属性(props)的概念,以及它们之间的区别。

9.16什么是Vue的路由(Vue Router)?它的主要功能是什么?

9.17Vue中的Vuex是什么,以及它的主要作用是什么?

9.18Vue中的插槽(Slot)是什么,以及它的主要作用是什么?

9.19什么是Vue Mixin?它有什么作用,以及在使用时需要注意什么?

9.20什么是Vue Mixin?它有什么作用,以及在使用时需要注意什么?

9.21请解释Vue中的过渡效果(Transition)和动画效果(Animation)的区别,以及它们的主要应用场景。

十二、uniapp相关面试题

1、什么是 uniapp?它有哪些特点和优势?

2、请解释 uniapp 中的生命周期钩子函数有哪些,以及它们在组件生命周期中的调用顺序。

3、uniapp 中的组件通信方式有哪些?请简要说明它们的特点和适用场景。

4、在 uniapp 中,什么是微信小程序的 setData 方法,以及它的作用是什么?在什么情况下你会使用 setData?

5、在 uniapp 中,什么是事件处理,以及常见的事件处理方式有哪些?请简要说明。

6、在 uniapp 中,什么是页面路由,以及常见的页面路由方式有哪些?请简要说明。

7、在 uniapp 中,什么是组件,以及组件的基本结构是怎样的?请简要说明。

8、在 uniapp 中,什么是状态管理,以及常见的状态管理工具有哪些?请简要说明。

9、什么是前端工程化,有哪些常用的前端构建工具?

        10、移动端开发:

总结



前言

自己刷前端面试题的个人总结。

一、HTML部分

1、语义化标签

什么是语义化

HTML 语义化是指在创建 HTML 页面时,使用正确的标记来正确地描述内容的含义和结构。简单来说,它是一种用正确的标记来表示正确的内容的做法。

1.1.HTML 语义化的优点?
  • (1). 有助于 SEO

HTML语义化可以帮助搜索引擎更好地理解页面内容,从而提高页面排名,增加流量。

  • (2). 提高可访问性

HTML 语义化可以帮助屏幕阅读器和其他辅助技术更好地理解页面内容,从而提高可访问性。

  • (3). 提高代码可维护性

使用正确的标记来表示正确的内容可以使代码更加清晰和易于维护。

  • (4). 提高代码的可读性

HTML 语义化可以使代码更加易于理解和阅读。

何为语义化标签?

语义化标签就是让标签有自己的语义,利用自己的语义信息传递信息。

 语义化标签的好处是什么?

①让人更容易读懂(增加代码易读性)

②让搜索引擎更容易读懂(SEO)

使用语义化标识前:

<div>标题</div>
<div>
    <div>一段文字</div>
    <div>
        <div>列表1</div>
        <div>列表2</div>
    </div>
</div>

使用语义化标识后:

<h1>标题</h1>
<div>
    <p>一段文字</p>
    <ul>
        <li>列表1</li>
        <li>列表2</li>
    </ul>
</div>

 实行效果相同,但是使用语义化标签更让人易懂。

2、块状元素和内联元素

块状元素和内联元素最大的差别就是:块状元素独占一行,内联元素不是独占一行的。

块状元素:disply:block/table;有div h1 h2 table ul ol p 等。

内联元素:disply:  inline/inline-block;有span img input button 等。

二、CSS部分

1、计算盒模型

面试题一般会让计算offectWidth,offectWidth=内容宽度+内边距+边框,切记无外边距!!!

代码如下(示例):计算下面的offectWidth

 #div1 {
            width: 100px;
            padding: 10px;
            border: 1px solid #ccc;
            margin: 10px
        }

计算着并不难,难得是知道offectWidth该怎么算,如上面的计算为:padding*2+border*2+width=122px

可以通过dom元素验证:

document.getElementById('div1').offsetWidth

说到计算盒模型还有一个重要的东西:box-sizing:border-box属性,加了box-sizing:border-box属性,padding和border的值就不会在影响元素的宽高,相当于把padding和border的值都算在content里,这时的offectWidth就为100。

2、margin纵向重叠

什么为margin纵向重叠?

垂直外边距的重叠
相邻的垂直方向外边距会发生重叠现象
1兄弟元素之间
如果两个相邻元素的上下外边距相遇,那么会产生外边距折叠。
2父元素与它的第一个元素或者最后一个元素之间
父元素与它的子元素之间隔着边框、内边距、内容。如果这些没有了,那么父元素和子元素的外边距相邻,就会发生外边距折叠。
3空的块级元素
盒模型是内容、内边距、边框、外边距,如果margin-top和margin-bottom相遇了,就会产生外边距折叠。
总而言之,不能让两个垂直 margin 相遇,否则就会发生外边距折叠。

如何避免外边距重叠?

避免外边距折叠的方法
●浮动元素不会与任何元素发生叠加,也包括它的子元素
●绝对定位元素和其他任何元素之间不发生外边距叠加,也包括它的子元素
●inline-block 元素和其他任何元素之间不发生外边距叠加,也包括它的子元素

●创建了 BFC 的元素不会和它的子元素发生外边距叠加 (兄弟元素还是会叠加)


关于外边距重叠还有面试题,如计算下面AAA和BBB的距离。

 <style>
        p {
            font-size: 16px;
            line-height: 1;
            margin-top: 10px;
            margin-bottom: 15px;
        }
    </style>
<body>
    
    <p>AAA</p>
    <p></p>
    <p></p>
    <p></p>
    <p>BBB</p>
</body>

相邻元素的margin-top和margin-bottom会发生重叠,而且空白区域也会重叠,所以本题答案为:15px。

3、BFC问题

BFC我认为是css中比较重要的一个问题,也应该是面试中比较常考的一个问题,BFC(Block Formatting Context)块级格式化环境,BFC是解决高度塌陷的,什么叫高度塌陷?

在浮动布局中,父元素的高度默认是被子元素撑开的
●当子元素浮动后,会完全脱离文档流,将会无法撑起父元素高度,导致父元素的高度丢失
●父元素高度丢失以后,其下的元素会自动上移,导致页面的布局混乱
所以高度塌陷是浮动布局中比较常见的一个问题,这个问题我们必须要进行处理!

就是这种情况,父元素被子元素撑开,那么如何解决呢?就聊到了开启BFC。BFC是一个CSS中的一个隐含的属性,可以为一个元素开启BFC,开启BFC该元素会变成一个独立的布局区域。

元素开启BFC后的特点
●不会被浮动元素覆盖
●可以包含浮动的元素
●父子元素外边距不会重叠

 那么开启BFC有哪几种方式?有好几种,推荐以下三种。

1、设置浮动 float。

2、overflow: hidden。

3、设置为flex布局

4、clearfix

clearfix这样写,既可以解决高度塌陷,又可以解决外边距重叠。

.clearfix::before,
.clearfix:affter{
    content: '';
    display: table;
    clear: both;

}

4、圣杯和双飞翼布局

实现圣杯和双飞翼布局的方式有很多种,我个人喜欢用flex布局实现,很简单很方便。我还是推荐不用flex布局来实现圣杯和双飞翼布局,毕竟考察的多。

/*css部分:*/

  #header {
            text-align: center;
            background-color: #f1f1f1;
        }
        #context{
            /*开启flex布局*/
            display: flex;
            width: 100%;
        }
        #left{
            height: 300px;
            width: 500px;
            background-color: yellow;
        }
        #main{
            /*
            flex-grow 指定弹性元素的伸展系数,默认值为0
            当父元素有多余空间的时,子元素如何伸展
            父元素的剩余空间,会按照比例进行分配*/
            flex-grow: 1;
            height: 300px;
           background-color: rebeccapurple;
        }
        #right{
            height: 300px;
            width: 500px;
           background-color: darkblue;
        }
        #footer {
            text-align: center;
            background-color: #f1f1f1;
        }

   /* HTML部分:*/

<header id="header">我是头</header>
<div id="context">
    <div id="left">我是左边</div>
    <div id="main">我是中间</div>
    <div id="right">我是右边</div>
</div>
<footer id="footer">我是脚</footer>

5、flex和grid

5.1什么是Flexbox和Grid布局,它们的主要区别是什么?

Flexbox和Grid是两种常用的CSS布局技术,它们可以帮助我们创建灵活、可维护的布局。 在使用这些技术时,我们需要熟悉其属性和最佳实践,并且需要根据具体需求选择合适的技术。

  1. Flexbox 是一维布局系统,适合做局部布局,比如导航栏组件。
  2. Grid 是二维布局系统,通常用于整个页面的规划。
  3. 二者从应用场景来说并不冲突。虽然 Flexbox 也可以用于大的页面布局,但是没有 Grid 强大和灵活。二者结合使用更加轻松。

关于flex布局,主要记住该怎么用:

1、flex-direction 指定容器中弹性元素的排列方式。
2、flex-wrap 设置弹性元素是否在弹性容器中自动换行。
3、justify-content 如何分配主轴上的空白空间(主轴上的元素如何排列)。
4、align-items元素在辅轴上如何对齐。
5、flex-grow 指定弹性元素的伸展系数,默认值为0。

具体用法参考文档。

6、relative和absolute定位

相对定位 relative
当元素的 position 属性值设置为relative时,则开启了元素的相对定位
1、需要设置偏移量来产生变化。
2、使用了相对定位后,只会移动自身的布局位置,而不会对已存在的其他元素产生任何影响。

offset属性

含义

top

定位元素和定位位置的上边距离

bottom

定位元素和定位位置的下边距离

left

定位元素和定位位置的左侧距离

right

定位元素和定位位置的右侧距离

绝对定位 absolute
当元素的position属性值设置为absolute时,则开启了元素的绝对定位
特点
1、开启绝对定位后,如果不设置偏移量,元素的位置不会发生变化(只是位置不变,其他很多变了)
2、绝对定位元素是相对于其包含块进行定位的(与相对定位不同)
3、绝对定位会使元素提升一个层级
4、开启绝对定位后,元素会从文档流中脱离
5、绝对定位会改变元素的性质:行内变成块,块的宽高默认被内容撑开(与相对定位相反)

绝对定位中的位移量和相对定位的不同,绝对定位是依据包含块元素或者body进行偏移的。

7、水平居中和垂直居中方式

这个问题面试常考,我水平居中和垂直居中各总结了四条:

水平居中:

 /*水平居中*/
        #main{
            height: 1000px;
            width: 1000px;
            background-color: #0000FF;
            position: relative;
            /*第四种:利用flex元素*/
            display: flex;
            justify-content: center;
        }
        #son{
            height: 200px;
            width: 200px;
            background-color: red;
            /*第一种:*/
            /*margin: auto;*/
            /*第二种:transform元素 先向右边平移,在平移自己元素一半的距离从而居中*/
           /*position: absolute;*/
           /* right: 50%;*/
           /* transform:translateX(50%);*/
            /*第三种: 知道自己宽高,用absolute元素 left:50% +margin-left负值*/
            /*position: absolute;*/
            /*left: 50%;*/
            /*margin-left: -100px;*/
            /*inline元素:text-align:center*/
            text-align: center;
        }

垂直居中:

 /*垂直居中*/
        #main{
            height: 800px;
            width: 1000px;
            background-color: #0000FF;
            position: relative;
            /*第四种利用flex元素*/
            display: flex;
            align-items: center;
        }
        #son{
            height: 200px;
            width: 200px;
            background-color: red;
            /*第一种:还是使用盒子计算的特点 利用absolute元素*/
            /*position: absolute;*/
            /*top: 0;*/
            /*bottom: 0;*/
            /*margin: auto ;*/
            /*第二种:利用absolute向下偏移50%+margin-top:自身的一半的负值(知道自身宽高情况下)*/
            /*position: absolute;*/
            /*top: 50%;*/
            /*margin-top: -100px;*/
            /*第三种:transform元素 先向下边平移,在平移自己元素一半的距离从而垂直居中(不知道自己宽高情况下)*/
            /*position: absolute;*/
            /*top: 50%;*/
            /*transform: translateY(-50%);*/
            /*inline元素:line-height的值等于height值*/
            /*line-height: 200px;*/
        }

如果想实现水平垂直居中可以将两者结合起来。

8、line-height行高继承

祖先元素的line-height的属性会让后代元素继承,由此产生了面试题,计算真正的继承后的line-height的值。

body {
            font-size: 20px;
            line-height: 200%;
        }
        p {
            background-color: #ccc;
            font-size: 16px;
        }

本题继承的line-height为20px*200%=40px,误区:如果父代为百分比元素,不要将line-height和子代的font-size相乘,而是父亲计算后传给子。

9、选择器权重问题

选择器权重问题忘总结了。这类题应该也不少,主要我在开发中遇到过这种情况权重小的样式怎么改也没用的情况。

三、JS基础部分

js基础部分是整个面试最重要的一个部分,有些面试题不好理解,需要一遍又一遍的练习才能搞懂。我的建议是查看文档也好查看自己总结或者别人总结的知识点也好,多看多学。

1、值类型和引用类型

为什么从这开始,是因为要为后面手写JS深拷贝做铺垫。从下面一到面试题来了解值类型和引用类型。

const obj1={x:100,y:200}
const obj2=obj1
let x1=obj1.x
obj2.x=101
x1=102
console.log(obj1.x)

上面这段代码输出什么?答案:输出101,为什么不是102呢,这就涉及到值类型和引用类型。

值类型:number、string、undefined、boolen、Symbol

引用类型:Object、Array、特殊引用类型null、特殊引用类型function(但不用于存储数据,所以没有‘拷贝’、复制函数这一说法)。

let a=100
let b=a
 a=200
console.log(b)

值类型的数据存储堆栈图:

根据值类型的数据储存图可知b输出为100

let a={age:20}
let b=a
b.age=21
console.log(a.age)

引用类型的数据存储堆栈图:当时我学的时候就感觉和java的对象内存解析很像,我将两个配合起来一起展出。

                                                                JS:

JAVA:

 两个很像,只要不是新创造一个对象,例如P3=P1 两者指向的是同一内存地址,所以修改p3.age,p1也会改变,在JS中同样适用,所以上面的面试题a.age答案为:21。

2、手写深拷贝 

浅拷贝(shallow copy)    

- 通常对对象的拷贝都是浅拷贝  

  - 浅拷贝顾名思义,只对对象的浅层进行复制(只复制一层)    

- 如果对象中存储的数据是原始值,那么拷贝的深浅是不重要  

  - 浅拷贝只会对对象本身进行复制,不会复制对象中的属性(或元素)  

深拷贝(deep copy)    

- 深拷贝指不仅复制对象本身,还复制对象中的属性和元素    

- 因为性能问题,通常情况不太使用深拷贝     structuredClone()

铺垫完之后开始介绍深拷贝,如果上面的a.age我偏要让他等于20怎么办,这里就采用了深拷贝。虽然现在衍生出很多深拷贝的API,例如:es6新特性{...XXX} 、Object.assign{}、lodash库的clone deep等,面试考察的基础,还是要学会手写深拷贝。

手写之前要了解typeof运算符,typeof作用:识别所有值类型、识别函数、判断是否是引用类型(不可再细分)。

let a={age:20
name:'张三'}
let b=deepclone(a)
b.age=21
function deepclone(obj={}){
    //判断是否为引用类型并且不为null
    if ( typeof obj!=='object'&&typeof obj==null){
        return obj
    }
    //创建一个空..是对象还是数组由instanceof判断的结果决定
        let result
        if (obj instanceof Array){
            result=[]
        }else {
            result={}
        }
        //遍历每一个obj
    for (let key in obj) {
        // 保证 每个key 比如name age是obj的一个属性
        if (obj.hasOwnProperty(key)) {
            //最关键的一步递归,何为递归?就是方法体内调用本身。递归的目的就是找 值类型,当为值类型时(比如age的20)函数结束,
            // 并且因为值类型,按照值类型堆栈图,所以a b不影响。
            result[key] = deepclone(obj[key])
        }
    }
    //返回数据
    return result
}
console.log(a.age)//20

补充应用场景:

在后续总结插槽时,想起来了这个场景,当引入element-ui的对话框时,修改input的内容即使没点确定,但是修改了:

<template>
  <div id="app">
  输入框:<input type="text" v-model="info" >
    <el-button type="text" @click="upinfo" >修改</el-button>
    <el-dialog title="对话框" :visible.sync="isshow">
      <el-form :model="info" >
        <el-form-item label="修改"  :label-width="formLabelWidth">
          <el-input v-model="info" autocomplete="off"></el-input>
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button @click="isshow= false">取 消</el-button>
        <el-button type="primary" @click="isshow = false">确 定</el-button>
      </div>
    </el-dialog>
  </div>
</template>
<script>

export default {
  name: 'App',
  data(){
    return{
      isshow:false,
      info:'',
      formLabelWidth: '120px'
    }
  },
methods:{
    upinfo(){
   this.isshow=true
    }
}
}

可以看到没点确定但是input框确发生了变化 ,那么如何避免这种变化呢,就引出了浅拷贝和深拷贝

<template>
  <div id="app">
  输入框:<input type="text" v-model="info" >
    <el-button type="text"  @click="upinfo">修改</el-button>
    <el-dialog title="对话框" :visible.sync="isshow">
      <el-form  >
        <el-form-item label="修改"  :label-width="formLabelWidth">
          <el-input v-model="newinfo" autocomplete="off"></el-input>
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button @click="isshow= false">取 消</el-button>
        <el-button type="primary" @click="changeinfo" >确 定</el-button>
      </div>
    </el-dialog>
  </div>
</template>
<script>

export default {
  name: 'App',
  data(){
    return{
      isshow:false,
      info:'',
      newinfo:'',
      formLabelWidth: '120px'
    }
  },
methods:{
    upinfo() {
      this.isshow = true
      
    },
    changeinfo(){
      this.isshow = false
      this.info=this.newinfo
    }
}
}

在el-table中因为有插槽所以操作更为简单:

 //修改数据
    updateinfo(row){
      this.dialogFormVisible=true
      console.log(row)
      //this.tmForm=row //为什么不能这么写 是因为model双向绑定 你改页面就改,你改完后悔点取消页面还变
      //将已有的品牌信息赋值给tmForm进行展示
      //将服务器返回品牌的信息,直接赋值给了tmForm进行展示。
      //也就是tmForm存储即为服务器返回品牌信息
      //可以浅复制当确定后数据才变动
      this.tmForm={...row}
    }
  },
但是遇到这种对象里嵌套数组的就需要深拷贝
 AttrInfo: {
       
       attrValueList: [   //属性名中属性值,因为属性值可以是多个,因此需要的是数组
        ],
     
      }
按需引入clonedeep在lodash中
import cloneDeep from "lodash/cloneDeep";
  this.AttrInfo=cloneDeep(row)

手写深拷贝很重要,建议多写,一定要理解这个流程,清楚怎么实现。场景只是做个参考。

3、变量计算和类型转换

字符串拼接问题

const a=100+10//110
const b=100+'10'//10010
const c=true+'10'//true10

==运算符

console.log(100=='100')//true
console.log(0=='')//true
console.log(0==false)//true
console.log(false=='')//true
console.log(null==undefined)//true
//除了==null之外,其他一律用===,例如:
const obj={x:100}
if (obj.a==null){}
//相当于:
//if(obj.a===null||obj.a===undefined)

if语句和逻辑运算

truly变量: !!a===true的变量

falsely变量:!!a===false的变量

//以下是falsely变量。除此之外都是truly变量
!!0===false
!!NaN===false
!!''===false
!!null===false
!!undefined===false
!!false===false

4、原型及原型链(重中之重)!!!

原型和原型链应该属于面试官最喜欢问的问题了,在实际开发中也非常常用,例如在后续学习VUE中:VUE一个重要的内置关系:VueComponent.prototype.__proto__ === Vue.prototype,都设计到了原型,可见原型是多么重要,(另外因为我学JS之前先学的JAVA,当我学到原型时就感觉和JAVA的继承很像)所以我们从JS中class实现继承开始来一步步学习原型。

//父类
class Person{
    constructor(name) {
        this.name=name
    }
    sleep(){
        console.log(`${this.name}会睡觉`)
    }
}
//子类继承父类
class Student extends Person{
    constructor(name,grade) {//每个学生都有年级
        super(name)
        this.grade=grade
    }
    info(){//信息
        console.log(`姓名:${this.name},班级:${this.grade}`)
    }
}
//new一个实例
const Tom=new Student('Tom','高三')
console.log(Tom.name)//Tom
console.log(Tom.grade)//高三
Tom.sleep()//Tom会睡觉
Tom.info()//姓名:Tom,班级:高三

上面我们创建了一个父类,并创建了一个子类继承父类,而且还new了一个实例Tom,在手写深拷贝时用到了instanceof类型判断:

我们都知道instanceof做类型判断时可以判断这个东西是什么类型比如:

console.log([]instanceof Array)//true
console.log([] instanceof Object)//true
console.log({}instanceof  Object)//true

但是当我们这样判断时:

console.log(Tom instanceof Person)//true
console.log(Tom instanceof Student)//true
console.log(Tom instanceof  Object)//true

同样也能判断而且都为true,为什么呢?这个问题先放着,从原型入手开始追查原因。

原型分为:隐式原型也就是__proto__,和显式原型prototype,我们还应该知道一个问题对象的内存结构。

对象中存储属性的区域实际有两个:
    1. 对象自身
        - 直接通过对象所添加的属性,位于对象自身中
        - 在类中通过 x = y 的形式添加的属性,位于对象自身中

    2. 原型对象(prototype)
        - 对象中还有一些内容,会存储到其他的对象里(原型对象)
        - 在对象中会有一个属性用来存储原型对象,这个属性叫做__proto__
        - 原型对象也负责为对象存储属性,
            当我们访问对象中的属性时,会优先访问对象自身的属性,
            对象自身不包含该属性时,才会去原型对象中寻找
        - 会添加到原型对象中的情况:
            1. 在类中通过xxx(){}方式添加的方法,位于原型中
            2. 主动向原型中添加的属性或方法

如下图:

 在类中通过xxx(){}方式添加的方法,位于原型中,当我们Tom.__proto__时指向的正是Student的原型:

也就解释了为什么 

 console.log(Student.prototype===Tom.__proto__)//true

原型关系:

  • 每个构造函数内部都有一个显式原型prototype属性
  • 每个实例都有隐式原型__proto__
  • 在 JavaScript 中,所有的对象都有一个隐藏的 [[Prototype]] 属性也就是__proto__
  • 实例的__proto__指向对于class的prototype

为什么Tom能调用 Info()方法,因为有一套基于原型的执行规则。

  • 获取属性Tom.name或执行方法Tom.info()时
  • 先在自身属性和方法寻找
  • 如果找不到则自动去__proto__中查找

原型链:

原型链:

    console.log(Student.prototype.__proto__)
    console.log(People.prototype)
    console.log(People.prototype==Student.prototype.__proto__)//true

原型链图:

console.log(Tom instanceof Person)//true
console.log(Tom instanceof Student)//true
console.log(Tom instanceof  Object)//true

根据原型链图也能解释了上面代码为true的原因,因为instanceof是基于原型链实现的。这个应该在理解的情况下自己多画画。 

5、作用域和闭包

了解闭包之前先要了解作用域,什么是作用域?

作用域:一个变量或某个变量合法的使用范围,变量逃出这个范围将会报错,如图:

作用域(scope)
    - 作用域指的是一个变量的可见区域
    - 作用域有两种:
        全局作用域如(Document、window等)
            - 全局作用域在网页运行时创建,在网页关闭时消耗
            - 所有直接编写到script标签中的代码都位于全局作用域中
            - 全局作用域中的变量是全局变量,可以在任意位置访问

        局部作用域
            - 块作用域
                - 块作用域是一种局部作用域
                - 块作用域在代码块执行时创建,代码块执行完毕它就销毁
                - 在块作用域中声明的变量是局部变量,只能在块内部访问,外部无法访问 
                函数作用域
                - 函数作用域也是一种局部作用域
                - 函数作用域在函数调用时产生,调用结束后销毁
                - 函数每次调用都会产生一个全新的函数作用域
           - 在函数中定义的变量是局部变量,只能在函数内部访问,外部无法访问         
       

     

提到作用域还有一个概念叫:自由变量和局部变量

自由变量

  • 一个变量在当前作用域没有定义,但被使用了。
  • 向上级作用域,一层一层一次寻找,知道找到为止
  • 如果到全局作用域都没有找到,则报错xxx is  not defined
function outerFunction() {
  var outerVar = 5;

  function innerFunction() {
    console.log(outerVar); // outerVar 是自由变量
  }

  return innerFunction;
}

var closureFunction = outerFunction();
closureFunction(); // 输出 5

局部变量:

局部变量是在函数或块级作用域中声明的变量,其作用范围仅限于声明它的函数或块。这意味着在函数外部或块外部无法直接访问这些变量。局部变量在函数执行结束后通常会被销毁,因为它们的生命周期受到函数或块的影响。

function exampleFunction() {
  var localVar = 10; // localVar 是局部变量
  console.log(localVar);
}

exampleFunction();
// console.log(localVar); // 错误,localVar 在函数外不可用

举个例子来说明:

上述代码a、a1、a2都是自由变量,当找a2是会查看外层的作用域也就是fn2()的作用域,找到a2=200,这个时候更外面的作用域比如fn1()中如果还有其他定义a2的变量就不管了,因为已经认定fn2()中的a2了。a2找到后找a1 ,fn2()中没有a1,这个时候返回更外一层的作用域中找也就是fn()1,找到了a1=100,最后找a,fn2()、fn1()都没有定义a的变量,就从全局作用域中找,如果全局还没有的话就报defined。

了解完作用域和自由变量后开始引入闭包这个概念。

闭包:

闭包是有权限访问其他函数作用域的局部变量的一个函数

  • 作用域应用的特殊情况,有两种常见闭包表现:
  • 函数作为参数被传递
  • 函数作为返回值被返回

从下面两个面试题中来分别了解函数作为参数被传递和函数作为返回值被返回的情况,以及自由变量怎么找。

函数作为参数:

// 函数作为参数被传递
function print(fn) {
    const a = 200
    fn()
}
const a = 100
function fn() {
    console.log(a)
}
print(fn) 

这是函数作为参数的闭包情况,最后要的是a的值,a的值是100,因为在fn()作用域向上找自由变量已将找到了是100。

函数作为返回值:

// 函数作为返回值
function create() {
    const a = 100
    return function () {
        console.log(a)
    }
}

const fn = create()
const a = 200
fn() 

 此时a的值还是100,可以自己画下作用域图。这类题有个总结:

重点: 所有的自由变量的查找,是在函数定义的地方,向上级作用域查找 ,不是在执行的地方!!!切记是在函数定义的地方查找!!!!!!!!!!!!!!!!!

闭包的作用:

全局变量可以重复使用,但是容易造成变量污染。局部变量仅在局部作用域内有效,不可以重复使用,不会造成变量污染。

6、this

JS中的this是很恶心的东西,指向感觉变去的,this大概有5种指向的情况:

  1. 当作为普通函数调用时,this指向的是window
  2. 当以方法的形式调用时,this指向的是调用方法的对象(实例本身)
  3. 在class方法调用时,this指向也是当前实例本身
  4. 当使用call、apply、bind方法时,看这些方法绑定什么,this就指向什么
  5. 特别注意箭头函数,箭头函数没有自己的this,而是找上一层作用域

通过下面几个题来找下this

   function fn1(){
        console.log(this)
    }
    //情况一:
    fn1()//window
    //情况二:
    fn1.call({x:100})//{x: 100}
    class People{
        constructor(name,age) {
            this.name=name
            this.age=age
        }
        showthis(){
            console.log(this)
        }
    }
    const Tom=new People('张三',15)
    //请况三:
    Tom.showthis()//People{name: '张三', age: 15}
    //情况四:
    const fn2=()=>{
        console.log(this)//Window
    }
    fn2()

上面的虽然很绕但是能总结出一个关键点:

this取什么值是在函数执行的时候确定的!!!!!不是在函数定义的时候确定的,在配合以上五种情况应该可以明白this指向问题了。

7、闭包应用场景

闭包应用场景:数据安全:

//闭包的应用场景:数据安全
// 闭包隐藏数据,只提供 API
function Message(){
    const data={}//闭包中数据被隐藏不被外界访问
    return{
        set(key,value){
            data[key]=value
        },
        get(key){
            return data[key]
        }
    }
}
const person=Message()
person.set('Name','Jack')
console.log(person.get('Name'))//Jack

此外在引用lodash库用防抖、节流时也用到了闭包。

防抖举例:

//html部分
请输入<input type="text" id="input1">
//JS部分
<script>
    const input1 = document.getElementById('input1')
   function debounce(fn,delay=300) {
    // timer 是闭包中的
       let timer = null
       return function () {
           if (timer) {
               clearTimeout(timer)
           }
           timer = setTimeout(() => {
               fn.apply(this, arguments)
        //清空定时器
               timer=null
           }, delay)
       }
   }
   input1.addEventListener('keyup',debounce(function (){
       console.log(input1.value)
   },500))

 四、Promise异步部分

异步部分是很重要的内容,当时学node.Js时就感觉很抽象,对于这方面有很多面试题,还都是很重要的。接下来从event loop事件循环开始一点一点学习异步。

异步是通过一些代码操作,让执行顺序发生改变不是向同步一样一行一行执行输出而是根据需要来决定执行顺序

1、event loop

event loop事件循环,我感觉这是执行异步的核心部分,是浏览器或Node解决单线程运行时不会阻塞的一种机制。既然提到异步那么必然就有同步,那么什么是异步什么是同步呢?

①异步和同步

当初学node.js时看的时李立超老师的视频,当时给我们举了个例子:去一家餐厅,在餐厅中有好多客人,餐厅的流程分为三部分:点菜、厨师做菜、吃。同步就为:一个人一个人的执行这个流程,异步的就为人多就先点菜,谁做好就上谁的。总的来说:

同步情况:
通常情况代码都是自上向下一行一行执行的
- 前边的代码不执行后边的代码也不会执行
- 同步的代码执行会出现阻塞的情况
- 一行代码执行慢会影响到整个程序的执行
异步情况:
- 一段代码的执行不会影响到其他的程序
- 异步的问题:
    异步的代码无法通过return来设置返回值
- 特点:
    1.不会阻塞其他代码的执行
    2.需要通过回调函数来返回结果
- 基于回调函数的异步带来的问题
    1. 代码的可读性差
    2. 可调试性差

那么event loop又是什么?通过代码和图来理解:

console.log('Hi')

setTimeout(function cb1() {
    console.log('cb1') // cb 即 callback
}, 5000)

console.log('Bye')

刚开始大概是这么一个部分,

第一次:Hi会先到Call Stack中然后console输出,Call Stack清空 。

第二次:回调函数到Call Stack中但是由于定时器是Web API有延迟这个时候,不会立马输出而是存在这里面,Call Stack清空:

 第三次:Bye进Call Stack控制台输出,当Call Stack不在进数据时,Web API会根据要求等5s,时机成熟后推到Callback Queue中事件循环(一旦同步代码执行完就会执行,event loop机制)检测到然后又推到Call Stack执行,然后清空。

所以顺序为Hi Bye cb1。

总结event loop过程:

  • 同步代码,一行一行放在Call Stack执行
  • 遇到异步,会先记录下,等待时机(定时、网络请求等)
  • 时机到了,就移动到Callback Queue
  • 如Call Stack为空(即同步代码执行完)event loop开始工作
  • 轮询查找Callback Queue,如有则移动到Call Stack执行
  • 然后继续轮询查找

2、Promise

Promise的执行原理
   - Promise在执行,then就相当于给Promise了回调函数
       当Promise的状态从pending 变为 fulfilled时,
           then的回调函数会被放入到任务队列中

提到Promise就应先知道Promise的三种状态,要知道哪种情况下状态会变成什么形态:

pending   (进行中)
fulfilled(完成) 通过resolve存储数据时
rejected(拒绝,出错了) 出错了或通过reject存储数据时

then正常返回resolved,里面有报错则返回rejected。

catch正常返回resolved,里面有报错则返回rejected。

在这中then和catch的链式调用就是一个常考的问题,接下来从三个面试题,来深入了解

// 第一题
Promise.resolve().then(() => {
    console.log(1)
}).catch(() => {
    console.log(2)
}).then(() => {
    console.log(3)
})

这道题的的顺序为1、3,为什么呢?因为状态从pending开时执行输出1,此时的状态是fulfilled,不会调用错误的代码,而是去执行3。

// 第二题
Promise.resolve().then(() => { // 返回 rejected 状态的 promise
    console.log(1)
    throw new Error('erro1')
}).catch(() => { // 返回 resolved 状态的 promise
    console.log(2)
}).then(() => {
    console.log(3)
})

这道题的顺序为:1、2、3,原因:这个题要理解rejected状态,刚开始是1没错,然后抛出了一个错误,有错误就执行catch所以执行2,此时的状态返回resolved,然后执行3。catch正常返回resolved 这句话是关键。

// 第三题
Promise.resolve().then(() => { // 返回 rejected 状态的 promise
    console.log(1)
    throw new Error('erro1')
}).catch(() => { // 返回 resolved 状态的 promise
    console.log(2)
}).catch(() => {
    console.log(3)
})

这道题的顺序为:1、2,因为当执行2后返回的resolved,没错误不执行catch里面的代码。

3、async和await

这也是个重要的知识点,我做项目练手时经常会用。Promise虽好解决了最初的回调地狱问题,但是却有了连续then连环的问题,async和await就成了解决这些问题的关键。

  • promise是一个对象,从它可以获取异步操作的消息
  • async/await基于Promise实现,相当于Promise的升级版,不能用于普通的回调函数
  • async/await和Promise的关系
  • async/await 是消灭异步回调的终极武器
  • 但和Promise并不互斥,反而两者相辅相成
  • 执行async函数,返回的是Promise对象
  • await相当于Promise的then
  • try...catch可捕获异常,代替了Promise的catch
  • async/await 可以让里面代码同步执行

重点:当我们使用await调用函数后,当前函数后边的所有代码,后面的请注意!!会在当前函数执行完毕后,被放入到微任务队里中,微任务和宏任务会在后面讲,先通过以下面试题,来深一步了解:


async function async1 () {
  console.log('async1 start')
  await async2()
  console.log('async1 end') 

async function async2 () {
  console.log('async2')
}

console.log('script start')
async1()
console.log('script end')

执行顺序为:

async function async1 () {
  console.log('async1 start')//2
  await async2()
  console.log('async1 end') //5 关键在这一步,它相当于放在 callback 中,最后执行
}

async function async2 () {
  console.log('async2')//3
}

console.log('script start')//1
async1()
console.log('script end')//4
 

重点:await 是同步写法,但本质还是异步调用。因为await后面的叫相当于发在cb中。

4、宏任务和微任务

这也是面试中经常考的一个点,接下来从下面的代码,引入到宏任务和微任务。

console.log(100)
setTimeout(() => {
    console.log(200)
})
Promise.resolve().then(() => {
    console.log(300)
})
console.log(400)

上面的执行顺序是?答案为:100、400、300、200,那为什么300比200输出更早呢?就扯出了宏任务和微任务的概念:

JS是单线程的执行流程有调用栈即要执行的代码,任务队列即将要执行的代码,当调用栈的代码执行完毕后,队列中的代码会按照顺序依次进入到栈中执行。

那么在任务队列中有两种,宏任务队列和微任务队列。那么宏任务队列和微任务队列有什么区别呢?

宏任务:DOM渲染后触发,如setTimeout、setInterval、Ajax、DOM事件

微任务:DOM渲染前触发,如Promise,async/await

所以微任务比宏任务执行早,也就解答了为啥300比200输出更早的问题,但是为什么会出现这么一种情况呢?还是得回归到event loop中。

在上面的事件循环中知道了大概的执行流程:

但是中间省略了一个流程,当栈清空时不是直触发Event Loop而是中间会尝试DOM渲染,正确的流程应该为:1、Call Stack空闲 2、尝试 DOM 渲染 3、触发Event Loop 在这里就能看出宏任务是在DOM渲染后触发的。

微任务的情况和宏任务的还不相同,当在Call Stack中执行Promise(微任务类型)时,不会经过Web API,因为Promise是ES规范,不是W3C规范,而且会进入micro task queue(微任务队列)中。其流程图为:

为什么?因为微任务是ES6语法规定的,宏任务是由浏览器规定的。所以微任务会比宏任务更快执行。

5、手写Promise

手写promise相关的内容建议找视频学习,因为非常的难,想学会不容易。

五、从JS基础知识到JS Web API

  • JS基础指数,规定语法(ECMA 262 标准)
  • JS Web API,网页操作的API(W3C标准)
  • 前者是后者的基础,两者结合才能真正实际应用

  

                                          JS Web API

  DOM、 BOM、  事件绑定、   ajax、    储存

1、DOM

1.1 DOM是哪种数据结构

DOM是树结构

1.2 DOM操作常用API

  • DOM 节点操作
  • DOM 结构操作
  • attribute和 property

1.3 attribute和 property

第一个问题:什么是attribute什么是property?

  • attribute:修改html属性,会改变html结构
  • property:修改对象属性,不会体现到html结构中。
  • 两者都有可能引起DOM重新渲染

通过下面代码来深入了解:

attribute 是我们在 html 代码中经常看到的键值对, 例如:

 <input id="name" type="text" value="Name:">

上面代码中的 input 节点有三个 attribute:

  • id:name
  • type:text
  • value:Name

property 是 attribute 对应的 DOM 节点的 对象属性 (Object field), 例如:

 HTMLInputElement.id === 'name'
 HTMLInputElement.type === 'text'
 HTMLInputElement.value === 'Name:'

1.4 DOM优化

  • DOM操作非常‘昂贵’,避免频繁的DOM操作
  • 对DOM查询做缓存
  • 将频繁操作改为一次性操作

2、事件

事件(event)
 - 事件就是用户和页面之间发生的交互行为
     比如:点击按钮、鼠标移动、双击按钮、敲击键盘、松开按键...  
 - 可以通过为事件绑定响应函数(回调函数),来完成和用户之间的交互
 - 绑定响应函数的方式:
     1.可以直接在元素的属性中设置
     2.可以通过为元素的指定属性设置回调函数的形式来绑定事件(一个事件只能绑定一个响应函数)
     3.可以通过元素addEventListener()方法来绑定事件 前面是描写事件的字符串 第二个是函数

接下来从事件绑定、事件冒泡、事件代理三个方面来深入了解事件

2.1事件绑定

             // 获取到按钮对象
             const btn=document.getElementById('btn')
             // 为按钮对象的事件属性设置响应函数
             btn.addEventListener('click', function(){
                    alert('1')
             })

做一个demo来引出事件冒泡,实现点击a标签输出文本的效果即:

通用的事件绑定函数:

HTML中
<div>
    <a id="btn1" href="#">a1</a><br>
    <a id="btn2" href="#">a2</a><br>
    <a id="btn3" href="#">a3</a><br>
    <a id="btn4" href="#">a4</a><br>
    <button>加载更多...</button>
</div>
<script>
通用的事件绑定函数
    function bindEvent(elem,type,fn){
        elem.addEventListener(type,fn)
    }
    //获得elem
    const btn1=document.getElementById('btn1')
    bindEvent(btn1,'click',event=>{
    event.preventDefault()
    alert(event.target.text)
    })
    const btn2=document.getElementById('btn2')
    bindEvent(btn2,'click',event=>{
    event.preventDefault()
    alert(event.target.text)
    })
    const btn3=document.getElementById('btn3')
    bindEvent(btn3,'click',event=>{
    event.preventDefault()
    alert(event.target.text)
    })
    const btn4=document.getElementById('btn4')
    bindEvent(btn4,'click',event=>{
    event.preventDefault()
    alert(event.target.text)
    })
</script>

可见上面的代码有多么的繁琐,这个时候就体现出事件冒泡和事件代理的好处,那么什么是事件冒泡呢?

2.2事件冒泡和事件代理

什么是事件冒泡呢?什么又是事件代理(事件委托)呢?事件冒泡和事件代理又有什么关联呢?

DOM事件流(event flow )存在三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段。

JavaScript 事件分为三个阶段:

  • 捕获阶段:事件从父元素开始向目标元素传播,从 Window 对象开始传播。
  • 目标阶段:该事件到达目标元素或开始该事件的元素。
  • 冒泡阶段:这时与捕获阶段相反,事件向父元素传播,直到 Window 对象。

事件冒泡:

在DOM中存在着多种不同类型的事件对象
    - 多种事件对象有一个共同的祖先 Event
        - event.target 触发事件的对象
        - event.currentTarget 绑定事件的对象(同this)
        
        - event.stopPropagation() 停止事件的传导
        - event.preventDefault() 取消默认行为
    - 事件的冒泡(bubble)
        - 事件的冒泡就是指事件的向上传导
        - 当元素上的某个事件被触发后,其祖先元素上的相同事件也会同时被触发
        - 冒泡的存在大大的简化了代码的编写,但是在一些场景下我们并不希望冒泡存在
            不希望事件冒泡时,可以通过事件对象来取消冒泡
  1. 事件冒泡: 事件冒泡是指在触发了某个元素上的特定事件后,这个事件会沿着元素的祖先元素向上传播。也就是说,它从最具体的元素(事件的目标)冒泡到最不具体的元素(文档对象)。这是事件传播的一种机制。

  2. 事件委托(Event Delegation): 事件委托是一种基于事件冒泡的技术,通过将事件处理程序绑定到祖先元素,来处理后代元素上的事件。通过这种方式,你只需要一个事件处理程序就能处理一组元素上的事件,而不是为每个元素分别添加事件处理程序。

在事件委托中,你依然是利用了事件冒泡的机制。当你在祖先元素上绑定了事件处理程序,该处理程序能够捕获子元素上触发的事件,因为事件会冒泡上来。

所以,事件冒泡是一种机制,而事件委托是一种利用这种机制的编程模式。它们并不是相反的概念,而是相辅相成的。事件委托是基于事件冒泡的,通过它可以更有效地处理大量元素或动态生成的元素上的事件。

事件冒泡
1:事件介绍:假设body,div,a都分别绑定了单击事件。
事件冒泡过程 a->div->body
2:引发问题,怎样阻止 event.stopPropagation()方法

事件委托
就是当事件触发时,把要做的事件委托给父元素处理
作用:
1、节约内存
2、能为之后新增DOM元素依然添加事件

这些都是概念,接下来从下面代码来更直观了解事件冒泡和事件代理,在上方没用事件代理的时候,要想触发事件得给每一个都绑定一个事件,代码多且繁琐,用了事件绑定,只需要给父元素绑定事件,利用事件冒泡的机制,事件向上传导,达到触发子元素事件的效果。

<div id="btn">
    <a href="#">a1</a><br>
    <a href="#">a2</a><br>
    <a href="#">a3</a><br>
    <a href="#">a4</a><br>
    <button>加载更多...</button>
</div>
<script>
 // 代理绑定
    function bindEvent(elem, type, selector, fn) {
        if (fn == null) {
            fn = selector
            selector = null
        }
        elem.addEventListener(type, event => {
            const target = event.target
            if (selector) {
                // 代理绑定
                //matches判断一个DOM元素是不是符合选择器 点a符合点button就不符合
                if (target.matches(selector)) {
                    fn.call(target, event)
                }
            } else {
                //普通绑定
                fn.call(target, event)
            }
        })
    }

    const btn = document.getElementById('btn')
    bindEvent(btn, 'click','a', event => {
        event.preventDefault()
        alert(event.target.text)
    })
</script>

通过两段代码的对比,我们知道了事件冒泡的好处,和使用事件绑定的好处。 

                                                               事件代理

  • 事件代理让代码更简洁
  • 减少了浏览器内存占用
  • 但是,不要滥用

3、ajax

在js中向服务器发送的请求加载数的技术叫AJAX

                    AJAX
                        - A 异步  J JavaScript A 和 X xml
                        - 异步的js和xml
                        - 它的作用就是通过js向服务器发送请求来加载

                可以选择的方案:
                            ① XMLHTTPRequest(xhr)
                            ② Fetch
                            ③ Axios数据

3.1跨域

在解决跨域之前,先了解为什么产生跨域问题,这里就引出一个概念:同源策略

同源策略:

ajax请求时,浏览器要求当前网页和server必须同源(安全)

同源:协议、域名、端口,三者必须一致

跨域请求
                            - 如果两个网站的完整的域名不相同
                                a网站:http://haha.com 
                                b网站:http://heihei.com
                            - 跨域需要检查三个东西:
                                协议 域名 端口号
                                http://localhost:5000
                                http://127.0.0.1:5000
                                - 三个只要有一个不同,就算跨域
                            - 当我们通过AJAX去发送跨域请求时,

                                所有的跨域,都必须经过server端允许和配合

                                未经server端允许就实现跨域,说明浏览器有漏洞,危险信号
                                浏览器为了服务器的安全,会阻止JS读取到服务器的数据

了解到产生跨域的原因,开始解决跨域问题

1、JSONP解决。原理:

<script>可绕过跨域限制

服务器可以任意动态拼接数据返回

所以,<script>就可以获得跨域的数据,只要服务端愿意返回

2、在服务器中设置一个允许跨域的头 Access-Control-Allow-Origin

res.setHeader("Access-Control-Allow-Origin", "http://127.0.0.1:5500")

3、在Vue中可以设置服务器代理解决跨域问题

vue.config.js文件中配置代理服务器
module.exports= {
  devServer: {
    proxy: {
      '/api': {
        target:'http://xxxxxx.cn',
      }
    },
  }
}

3.2 Session、Cookie、Token

session:

session的中⽂翻译是“会话”,当⽤户打开某个web应⽤时,便与web服务器产⽣⼀次session。服务器使⽤session把⽤户的信息临时保存在了服务器上,⽤户离开⽹站后session会被销毁。这种⽤户信息存储⽅式相对cookie来说更安全,可是session有⼀个缺陷:如果web服务器  做了负载均衡,那么下⼀个操作请求到了另⼀台服务器的时候session会丢失。 

cookie:

cookie是保存在本地终端的数据。cookie由服务器⽣成,发送给浏览器,浏览器把cookie以kv形式保存到某个⽬录下的⽂本⽂件内,下⼀次请求同⼀⽹站时会把该cookie发送给服务器。由于cookie是存在客户端上的,所以浏览器加⼊了⼀些限制确保cookie不会被恶意使⽤,同  时不会占据太多磁盘空间,所以每个域的cookie数量是有限的。

cookie的组成有:名称(key)、值(value)、有效域(domain)、路径(域的路径,⼀般设置为全局:"\")、失效时间、安全标志(指定后,cookie只有在使⽤SSL连接时才发送到服务器(https))。

token:

token的意思是“令牌”,是⽤户⾝份的验证⽅式,最简单的token组成:uid(⽤户唯⼀的⾝份标识)、time(当前时间的时间戳)、sign(签名,由token的前⼏位+盐以哈希算法压缩成⼀定长的⼗六进制字符串,可以防⽌恶意第三⽅拼接token请求服务器)。还可以把不变的参数也放进token,避免多次查库。

3.2.1 它们之间有什么关系?

cookie,session都可以是token存储的一种方式。

cookie为存储在本地的数据,请求时会将该数据提交到服务器验证使用。

session为存储在服务器上的内存数据,只要会话没有中断,那么该数据持续有效。

token通常上来说属于令牌,cookie,session为一种数据存储和使用方式,令牌可以存储在cookie,session,但是实际上通过url参数或者表单参数一样可以达到同样的效果。但是对于开发和维护来说成本较高,一旦后端要求修改参数,这样的使用方式对于修改来说是很头疼的事情。

3.3  localStorage和sessionStorage

易混点:Session位于服务器端,sessionStorage位于客户端,无任何关联。

  localStorage和sessionStorage:

特点:

  • HTML专门为储存设计,最大可存5M
  • API简单易用setItem getItem
  • 不会随着http请求被发送出去

区别:

  • localStorage 数据会永久存储,除非代码或手动删除
  • sessionStorage 数据只存在当前会话,浏览器关闭则情况
  • 一般用localStorage多些

常用方法:

setItem() 用来存储数据
getItem() 用来获取数据
removeItem() 删除数据
clear() 清空数据

如果面试让描述cookie、localStorage和sessionStorage的区别,可从容量、易用性、是否跟随http请求发送出去来回答

六、HTTP和HTTPS

1、状态码分类:

  • 1xx :服务器收到请求
  • 2xx :请求成功,如200
  • 3xx:重定向。如302
  • 4xx:客户端错误,如404
  • 5xx:服务端错误,如505

常见状态码:

  • 200 成功
  • 301 永久重定向(配合location,浏览器自动处理)
  • 302 临时重定向(配合location,浏览器自动处理)
  • 304 资源未修改
  • 404 资源未找到
  • 403 没有权限
  • 500 服务器错误
  • 504 网关超时

2、methods

传统的methods

  • get 获取服务器的数据
  • post 向服务器提交数据

简单的网页功能,就这两个操作

现在的methods

  • get 获取服务器的数据
  • post 新建数据
  • patch/put 更新数据
  • delete 删除数据

3、http和https缓存

为什么要缓存:没有必要获取一遍的东西再重新获取。缓存的优点:减少网络请求的数量和体积。

缓存分为:通常根据是否需要向服务器重新发起HTTP请求去确认缓存是否有效将缓存分为强制缓存和协商缓存。

强制缓存:是直接从浏览器缓存查找该结果,并根据结果的缓存规则来决定是否使用该缓存的过程。

  • 不存在该缓存结果和标识,强制缓存失效,则直接向服务器发起请求(跟第一次发起请求一致)
  • 存在缓存结果和标识,但结果已失效,强制缓存失效,则使用协商缓存
  • 存在缓存结果和标识,并且结果未失效,强制缓存生效,直接返回该结果

控制强制缓存的字段分别是ExpiresCache-Control,其中Cache-Control优先级比Expires高。

协商缓存是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,有服务器根据缓存标识决定是否使用缓存的过程,主要有以下两种情况:

  1. 协商缓存生效,返回304,服务器告诉浏览器资源未更新,则再去浏览器缓存中访问资 源
  2. 协商缓存失效,返回200和请求结果

同样,协商缓存的标识也是在响应报文的HTTP头和请求结果一起返回给浏览器的,控制协商缓存的字段分别有:

  • Last-Modified/If-Modified-Since
  • Etag/If-None-Match

其中Etag/If-None-Match优先级比Last-Modified/If-Modified-Since

4、刷新操作以及刷新对缓存的影响

三种刷新操作:

  • 正常操作:地址栏输入url,跳转链接,前进后退等
  • 手动刷新:F5,点击刷新按钮,右击菜单刷新
  • 强制刷新:ctrl+F5

对缓存的影响:

  • 正常操作:强制缓存有效,协商缓存有效
  • 手动刷新:强制缓存失效,协商缓存有效
  • 强制刷新:强制缓存失效,协商缓存失效

5、https和http的区别

  • http 是明文传输,敏感信息容易被中间劫持
  • https = http + 加密,劫持了也无法解密
  • 现代浏览器已开始强制https协议

加密方式:

对称加密:一个key通负责加密、解密

非对称加密:一对key,A加密之后,只能用B来解密

https同时用到了这两种加密方式

七、开发环境

1、从输入url到渲染出页面的整个过程

第一个加载资源的形式:

  • html代码
  • 媒体文件,如图片、视频等
  • javascript css

第二个加载资源的过程:

  • DNS 解析:域名->IP地址
  • 浏览器根据IP地址向服务器发起http请求
  • 服务器处理http请求,并返回给浏览器

第三个渲染页面的过程:

  • 根据HTML代码生成DOM Tree
  • 根据CSS 代码生成CSSOM
  • 将DOM Tree和CSSOM整合行程Render Tree
  • 根据Render Tree渲染页面
  • 遇到<script>则暂停渲染,优先加载并执行JS代码,完成再继续
  • 直至把Render Tree渲染完成

2、window.onload和DOMContentLoaded的区别

      window.addEventListener('load',function (){
            //页面的全部资源加载完后才会执行,包括图片、视频等
        })
        document.addEventListener('DOMContentLoaded',function (){
            ///DOM 渲染完即可执行,此时图片、视频还可能没有加载完
        })

3、前端性能优化

性能优化原则:

  • 多使用内存、缓存或其他方法
  • 减少CPU计算量,减少网络加载耗时
  • (适用于所有编程的性能优化——空间换时间)

从哪方面入手:加载更快、渲染更快

让加载更快:

  • 减少资源体积:压缩代码
  • 减少访问次数:合并代码,SSR服务器渲染,缓存
  • 使用更快的网络:CDN

让渲染更快:

  • CSS放在head,JS放在body最下面
  • 尽早开始执行JS,用DOMContentLoaded触发
  • 懒加载(图片懒加载,上滑加载更多)
  • 对DOM查询进行缓存
  • 频繁DOM操作,合并到一起插入DOM结构
  • 节流throttle 防抖 debounce

解答问题:

为什么建议把css放在head中

比如说我们有很老的一些电脑,很卡很老的一些浏览器,你可能就会看出来。比如text文本渲染完成之后,本来是一个很小的一个字,然后突然就变成一个50像素很大的一个字了,为什么呢?因为当时渲染的时候没有CSS。后来CSS来了之后,又把这个text这个文字给它变大了,所以说就会出现这种可能,把CSS代码在这个DOM树生成完成之前就给它加载完,就把这个规则放在这儿,然后。DOM树当直接生成完之后,直接和所有的CSS整合,生成一个渲染树,生成一个printer tree,然后一步渲染完成,这样的话就不要再重复了。


为什么建议js放在body最后

js应该放在body最后。为什么要建议把js放到最后呢?假如js没有放到最后,它会出现一种什么情况呢?就极端情况下,它会出现一种本来渲染的一部分,然后突然卡住了,就是突然说停止渲染了。停止渲染之后,又去进行渲染,就加载完js之后,又去进行渲染,所以说它就会导致一个页面渲染的过程比较长本来应该是我们期望是什么样子呢?本来应该期望是说,比如说这个页面一共的渲染时间可能是一秒钟,我们期望在零点三秒钟之内或零点五秒之内,让用户看到页面的所有内容。然后剩下零点五秒钟对我们执js代码,然后把这个页面渲染完成。
 

SSR是什么

服务器端渲染:网页和数据一起加载,一起渲染

非SSR(前后端分离):先加载网页,在加载数据,在渲染数据

早先的JSP ASP PHP ,     现在的vue React SSR

4、安全

主要为:XXS攻击和XSRF攻击 

在 Web 安全领域中,XSS 和 CSRF 是最常见的攻击方式。 简单的理解: XSS攻击: 跨站脚本攻击。 攻击者脚本 嵌入 被攻击网站,获取用户cookie等隐私信息。 CSRF攻击: 跨站请求伪造。 已登录用户 访问 攻击者网站,攻击网站向被攻击网站发起恶意请求(利用浏览器会自动携带cookie)。

XSS预防

  • 替换特殊字符,如<变为&lt;>变为&gt
  • <script>变为&lt;script&gt; ,直接显示,而不会作为脚本执行
  • 前端要替换,后端也要替换,都做总不会有错

XSRF预防

  • 使用post接口
  • 增加验证,例如密码、短信验证码、指纹等

5、手写防抖节流

防抖和节流也是非常重要的一个内容,除了引用lodash库,能够自己手写debounce、throttle,知道其中原理也相当重要。防抖和节流可以理解回城和冷却

函数防抖:将多次操作合并为一次操作进行。原理是维护一个计时器,规定在delay时间后触发函数,但是在delay时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。

函数节流:使得一定时间内只触发一次函数。原理是通过判断是否有延迟调用函数未执行。

区别: 函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数,而函数防抖只是在最后一次事件后才触发一次函数。 比如在页面的无限加载场景下,我们需要用户在滚动页面时,每隔一段时间发一次 Ajax 请求,而不是在用户停下滚动页面操作时才去请求数据。这样的场景,就适合用节流技术来实现。
 

接下来通过几个场景来更好了解防抖和节流

防抖(debounce)场景:

监听一个输入框的,文字变化后触发change事件

直接用keyup事件,则会触发change事件

防抖:用户输入结束或暂停时,才会触发change事件

//html部分
请输入<input type="text" id="input1">
//JS部分
<script>
    const input1 = document.getElementById('input1')
   function debounce(fn,delay=300) {
    // timer 是闭包中的
       let timer = null
       return function () {
           if (timer) {
               clearTimeout(timer)
           }
           timer = setTimeout(() => {
               fn.apply(this, arguments)
        //清空定时器
               timer=null
           }, delay)
       }
   }
   input1.addEventListener('keyup',debounce(function (){
       console.log(input1.value)
   },500))

其原理就是开一个定时器,然后比如input中输入1,2,3。

输入1后会进入判断没有定时器就给加一个定时器时间默认为300ms,后续设置为500ms。

在500ms之内输入了2会继续进入判断这次有了定时器是(因为上次的定时器还没到500ms所以没有清空),但是进入if判断后会清空,然后重新赋值一个定时器

在500ms之内输入了3,只一次不在输入其他数字了,等待500ms后关闭定时器然后打印。

节流 (throttle)场景:

拖拽一个元素时,要随时拿到该元素被拖拽的位置

直接用drag事件,则会频繁触发,很容易卡顿

节流:无论拖拽速度多快,都会每隔100ms触发一次

    <style>
        #div1 {
            border: 1px solid #ccc;
            width: 200px;
            height: 100px;
        }
    </style>

<script>
    const div1=document.getElementById('div1')
   function throttle(fn,delay=300) {
    // timer 是闭包中的
       let timer = null
       return function () {
           if (timer) {
               return
           }
        timer=setTimeout(()=>{
            fn.apply(this,arguments)
             //清空定时器
            timer=null
        },delay)
       }
   }
   div1.addEventListener('drag',throttle(function (e){
       console.log(e.offsetX, e.offsetY)
   },100))

拖拽输出拖拽后的位置,第一次移动时绑定个定时器,第二次移动因为是连续拖拽的所以在100ms以内,定时器还没有被清空,所以进入到判断然后return不被受理(如果return后不跟任何值,则相当于返回undefined)直到100ms结束,然后执行后续的拖拽.....

防抖和节流闭包的作用:

这是非常非常重要的问题,面试官通常会问闭包的应用场景,闭包在防抖和节流中运用,其作用是:

  • 可以读取函数内部的变量
  • 可以让访问的变量一直保存在内存中
  • 通过闭包处理防抖的必要性。定义一个全局timer一样可以。之所以用闭包,还是为了达到防抖函数的复用,而不必给每个防抖函数创建一个全局变量timer,避免造成变量污染。

八、JS的一些基础以及数组方法的应用

1、一些基础面试题

1.1、var和let const的区别

var是ES5语法,let const是ES6语法;var有变量提升

变量的提升
    - 使用var声明的变量,它会在所有代码执行前被声明 没有赋值
        所以我们可以在变量声明前就访问变量

console.log(a) // undefined
var a = 200
console.log(b)//报错
let b=200

var和let是变量,可修改;const是常量,不可修改

var c=100
c=200
console.log(c)//200
let c=100
c=200
console.log(c)//200
const c=200
c=300
console.log(c)//报错

let const 有块级作用域,var没有

- 作用域有两种:
    全局作用域
        - 全局作用域在网页运行时创建,在网页关闭时消耗
        - 所有直接编写到script标签中的代码都位于全局作用域中
        - 全局作用域中的变量是全局变量,可以在任意位置访问

    局部作用域
        - 块作用域
            - 块作用域是一种局部作用域
            - 块作用域在代码块执行时创建,代码块执行完毕它就销毁
            - 在块作用域中声明的变量是局部变量,只能在块内部访问,外部无法访问
{
   const a=100
}
console.log(a)//报错
{
    let a=100
}
console.log(a)//报错
{
    var a=100
}
console.log(a)//100

1.2、typeof返回哪些类型

值类型:number、string、undefined、boolen、Symbol

引用类型:Object、Array类型判断也是ONject、特殊引用类型null、特殊引用类型function

1.3、列举强制类型转换和隐形类型转换

强制:parsetInt parseFloat toString 等

隐式:if、逻辑运算、==、+拼接字符串

2、手写深度比较模拟lodash库的isEqual

//判断是否是对象或数组
function isObject(obj){
    return typeof (obj)==='object'&&obj!==null
}
function isEqual(obj1,obj2){
    // 判断是否是对象或数组
    if (!isObject(obj1)||!isObject(obj2)){
        // 值类型(注意,参与 equal 的一般不会是函数)
        return obj1===obj2
    }
    if (obj1===obj2){
        return true
    }
    // 两个都是对象或数组,而且不相等
    // 1. 先取出 obj1 和 obj2 的 keys ,比较个数
    const obj1Keys=Object.keys(obj1)
    const obj2Keys=Object.keys(obj2)
    if (obj1Keys.length!==obj2Keys.length){
        return false
    }
    // 2. 以 obj1 为基准,和 obj2 依次递归比较 走到这一步key个数已经相等了,比的是key的值
        for (let key in obj1){
            //console.log(key)//a b x y
            // 比较当前 key 的 val —— 递归!!! 
            const res=isEqual(obj1[key],obj2[key])
            if (!res){
                return false
            }
        }
    // 3. 全相等
    return true

}
// 测试
const obj1 = {
    a: 100,
    b: {
        x: 100,
        y: 200
    }
}
const obj2 = {
    a: 100,
    b: {
        x: 100,
        y: 200
    }
}
console.log( isEqual(obj1, obj2) )

3、有关数组方法和一些常考的面试题

3.1split()和join的区别

console.log('1-2-3'.split('-'))//[ '1', '2', '3' ]
console.log([1,2,3].join('-'))//[1-2-3]

split()将字符串拆分为数组,join()将数组合并为字符串

3.2 数组 pop push  unshift  shift 分别做什么

功能是什么?返回值是什么?否对原数组造成影响?

// pop() 删除并返回数组的最后一个元素
const arr = [10, 20, 30, 40]
const item=arr.pop()
console.log(item)//40
//push()  向数组的末尾添加一个或多个元素,并返回新的长度
const arr = [10, 20, 30, 40]
arr.push(50)
console.log(arr)//[ 10, 20, 30, 40, 50 ]
//shift()删除并返回数组的第一个元素
const arr = [10, 20, 30, 40]
const item=arr.shift()
console.log(item)//10
//unshift()向数组的开头添加一个或多个元素,并返回新的长度
const arr = [10, 20, 30, 40]
arr.unshift(0)
console.log(arr)//[ 0, 10, 20, 30, 40 ]

属于破坏性方法,对原数组造成影响

3.3数组的API,有哪些是纯函数

这里要明白纯函数数组API和非纯函数的区别:

 纯函数:1. 不改变源数组(没有副作用);2. 返回一个数组  没破坏性 

concat map filter  slice....

 非纯函数  有破坏性

push pop shift unshift forEach some every reduce

3.4数组slice和splice的区别

/*
*  slice()
                    - 用来截取数组(非破坏性方法)     
                    - 参数:
                        1. 截取的起始位置(包括该位置)
                        2. 截取的结束位置(不包括该位置)   
                            - 第二个参数可以省略不写,如果省略则会一直截取到最后
                            - 索引可以是负值
                        如果将两个参数全都省略,则可以对数组进行浅拷贝(浅复制)*/
const arr = [10, 20, 30, 40, 50]
let result=arr.slice(0,3)//[ 10, 20, 30 ]
console.log(result)
/*
*  splice()
                - 可以删除、插入、替换数组中的元素
                - 参数:
                    1. 删除的起始位置
                    2. 删除的数量
                    3. 要插入的元素

                - 返回值:
                    - 返回被删除的元素*/
const arr = [10, 20, 30, 40, 50]
arr.splice(0,2)
console.log(arr)//[ 30, 40, 50 ]
arr.splice(2,3,45,46,47)
console.log(arr)//[ 30, 40, 45, 46, 47 ]

slice是纯函数没破坏性,splice不是纯函数对原数组有破坏性

3.5 [10,20,30].map(parseInt)

这道题主要考的是看能不能对map()进行拆解,和知不知道map和parseInt的返回值。

parseInt(string, radix)
  • s -- 十进制表示的字符串。

  • radix -- 指定的基数。

对map()进行拆解

const res=[10,20,30].map(parseInt)
//拆解
[10,20,30].map((num,index)=>{
    return parseInt(num,index)
})

parseInt(10,0):数字基数为0,数字以 10进制解析,故结果为 10;
parseInt(20,1):数字基数为1,数字以 1进制解析,1进制出现了2,1进制无法解析,结果返回NaN;
parseInt(30,2):数字基数为2,数字以 2进制解析,2进制出现了3,3进制无法解析,结果返回NaN;

3.6ajax请求get和post的区别

  • get一般用于查询操作,post一般用户提交操作
  • get参数拼接在url上,post放在请求体内(数据体积可能更大)
  • 安全性:post易于防止CSRF

3.7函数call 和 apply的区别

调用函数除了通过 函数() 这种形式外,还可以通过其他的方式来调用函数
    比如,我们可以通过调用函数的call()和apply()来个方法来调用函数
        函数.call()
        函数.apply()
        - call 和 apply除了可以调用函数,还可以用来指定函数中的this
        - call和apply的第一个参数,将会成为函数的this
        - 通过call方法调用函数,函数的实参直接在第一个参数后一个一个的列出来
        - 通过apply方法调用函数,函数的实参需要通过一个数组传递
const obj = { name: "孙悟空"}
function fn(a,b){
    console.log('a=',a,'b=',b,this);
}
 fn.call(obj,1,2)//a= 1 b= 2 { name: '孙悟空' }
fn.apply(obj,[1,2])//a= 1 b= 2 { name: '孙悟空' }

fn.call(this,p1,p2,p3)

fn.apply(this,arguments)

3.8闭包是什么?有何特性?有何影响?

回顾作用域和自由变量

在函数中定义的变量是局部变量,只能在函数内部访问,外部无法访问 ,在没有在作用域定义但是需要访问的变量为自由变量,自由变量向上依次寻找。

回顾闭包应用场景

闭包是作用域的一种特殊场景:有权限访问函数作用域的局部变量,应用场景在防抖和节流,因为其需要复用,所以利用闭包可以读取内部变量。而且数据安全并且能防止变量污染。

回顾自由变量的查找

要在函数定义的地方而非执行的地方

4、有关DOM操作的一些API

4.1如何阻止事件冒泡和默认的行为?

event.stopPropagation()//阻止事件冒泡
event.preventDefault()//阻止默认行为 

补充:

超链接取消默认跳转方式:
1、return false但有局限性只能xxx.xxxx=>function()格式实现
2.<href=javascript:;> 意思是跳转执行javascript:;这个代码但这个代码什么也不做
3、 event.preventDefault() 取消默认行为
4丶button type变为button
5、Vue中 prevent可以阻止默认行为 例如<form @submit.prevent="demo">意思是表单提交后 阻止默认行为并且执行demo方法

4.2查找、添加、删除、移动DOM节点的方法

(1)创建新节点

      createDocumentFragment()    //创建一个DOM片段

      createElement()   //创建一个具体的元素

      createTextNode()   //创建一个文本节点

(2)添加、移除、替换、插入

      appendChild()

      removeChild()

      replaceChild()

      insertBefore()

(3)查找

      getElementsByTagName()    //通过标签名称

      getElementsByName()    //通过元素的Name属性的值

      getElementById()    //通过元素Id,唯一性

4.3如何减少DOM操作

  • 缓存DOM查询结果
  • 多次DOM操作,合并到一次插入

4.4document load和ready的区别

window.addEventListener('load',function (){
    //页面全部资源加载完后才会执行,包括图片、视频等
})
document.addEventListener('DOMContentLoaded',function(){
    //DOM 渲染完即可执行,此时图片、视频还可能没有加载完
} )

4.5 ==和===的不同

  • ==会尝试类型转换
  • ===严格相等

5、有关函数面试题

5.1函数声明和函数表达式的区别

  • 函数声明 function fn(){....}
  • 函数表达式  const fn = function () {...}
  • 函数声明会在代码执行前预加载,而函数表达式不会
// 函数声明
const res = sum(10, 20)
console.log(res)//30
function sum(x, y) {
    return x + y
}
// 函数表达式
var res = sum(10, 20)
console.log(res)//报错
var sum = function (x, y) {
    return x + y
}

5.2 new Object()和Object.create()的区别

  • { } 等同于 new Object(),原型 Object.prototype
  • Object.create(null)没有原型
  • Object.create({...})可指定原型

5.3关于this的场景题

const User={
    count:1,
    getCount:function (){
        return this.count
    }
}
console.log(User.getCount())//1
const func=User.getCount
console.log(func())//undefined
  1. 当作为普通函数调用时,this指向的是window
  2. 当以方法的形式调用时,this指向的是调用方法的对象(实例本身)
  3. 在class方法调用时,this指向也是当前实例本身
  4. 当使用call、apply、bind方法时,看这些方法绑定什么,this就指向什么
  5. 特别注意箭头函数,箭头函数没有自己的this,而是找上一层作用域

this取什么值是在函数执行的时候确定的!!!!!不是在函数定义的时候确定的

5.4关于作用域和自由变量的场景题

let i
for (i=1;i<=3;i++){
    setTimeout(function (){
        console.log(i)// 4 4 4
    },0)
}

5.5正则表达式

1.在正则表达式中大部分字符都可以直接写
2.| 在正则表达式中表示或
3.[] 表示或(字符集)
[abc] =a|b|c
    [a-z] 任意的小写字母
    [A-Z] 任意的大写字母
    [a-zA-Z] 任意的字母
    [0-9]任意数字
4.[^] 表示除了
    [^x] 除了x
5. . 表示除了换行外的任意字符
6. 在正则表达式中使用\作为转义字符
7. 其他的字符集
    \w 任意的单词字符 [A-Za-z0-9_]
    \W 除了单词字符 [^A-Za-z0-9_]
    \d 任意数字 [0-9]
    \D 除了数字 [^0-9]
    \s 空格
    \S 除了空格
    \b 单词边界
    \B 除了单词边界
8. 开头和结尾
    ^ 表示字符串的开头
    $ 表示字符串的结尾

5.6关于作用域和自由变量的场景题2

let a=100
function test(){
    console.log(a)//100
    a=10
    console.log(a)//10
}
test()
console.log(a)//10

5.7 如何获取多个数字中的最大值

let max=Math.max(10,20,30,40)
console.log(max)//40
let min=Math.min(10,20,30,40)
console.log(min)//10

可以用多种方法实现,这里采用了调用API的简单方式

5.8 如何用JS实现继承

继承
    - 可以通过extends关键来完成继承
    - 当一个类继承另一个类时,就相当于将另一个类中的代码复制到了当前类中(简单理解)
    - 继承发生时,被继承的类称为 父类(超类),继承的类称为 子类
    - 通过继承可以减少重复的代码,并且可以在不修改一个类的前提对其进行扩展

    封装 —— 安全性
    继承 —— 扩展性
    多态 —— 灵活性

6、JS的一些基础问题和一些数据储存方式

6.1如何捕获JS程序中的异常?

try {
    //todo
}catch (e){
    console.error(e)//手动捕获 catch
}finally {
    //todo
}
//自动捕获
window.onerror=function (message,source,lineNom,colNom,error){
    //第一,对跨域的js,如 CDN 的,不会有详细的报错信息
    //第二,对于压缩的 js ,还要配合 sourceMap 反查到未压缩代码的行、列

6.2什么是JSON?

  • json是一种数据格式,本质是一段字符串
  • json格式和JS对象结构一致,对JS语言更友好
  • window.JSON是一个全局对象:
JSON.stringify() 可以将一个对象转换为JSON字符串
JSON.parse() 可以将一个JSON格式的字符串转换为JS对象

6.3获取当前页面url参数

传统方式,查找location.search

function query(name) {
    const search = location.search.substr(1) // 类似 array.slice(1)
    // search: 'a=10&b=20&c=30'
    const reg = new RegExp(`(^|&)${name}=([^&]*)(&|$)`, 'i')
    const res = search.match(reg)
    if (res === null) {
        return null
    }
    return res[2]
}
query('d')

新API,URLSearchParams

// URLSearchParams
function query(name) {
    const search = location.search
    const p = new URLSearchParams(search)
    return p.get(name)
}
console.log( query('b') )

6.4Map和Set

从三个点来深入了解:

6.41 有序和无序:

纯函数:1. 不改变源数组(没有副作用);2. 返回一个数组  没破坏性 

concat map filter  slice....

 非纯函数  有破坏性

push pop shift unshift forEach some every reduce

无序就是第一种,无序排列想加就加相减就减不受限制所以就快,

有序是第二种按照顺序排列,想要在中间新增一个,后面的就必须退后一个,想要删除一个,后面的就要前进 一个

有序:操作慢

无序:操作快,但无序

如何结合两者有点?——二叉树、及其变种,Map就是利用了这个特点既有序又快

6.42 Map和Object的区别:

  • API不同,Map可以以任意类型为key
  • Map是有序结构(重要)
  • Map操作同样很快
Map
    - Map用来存储键值对结构的数据(key-value)
    - Object中存储的数据就可以认为是一种键值对结构
    - Map和Object的主要区别:
        - Object中的属性名只能是字符串或符号,如果传递了一个其他类型的属性名,
            JS解释器会自动将其转换为字符串
        - Map中任何类型的值都可以称为数据的key
        Map k唯一 可用这个可以去重
创建:
    new Map()

属性和方法:
    map.size 获取map中键值对的数量
    map.set(key, value) 向map中添加键值对
    map.get(key) 根据key获取值   
    map.delete(key) 删除指定数据
    map.has(key) 检查map中是否包含指定键
    map.clear() 删除全部的键值对
forEach()
                - 用来遍历数组
                - 它需要一个回调函数作为参数,这个回调函数会被调用多次
                    数组中有几个元素,回调函数就会调用几次
                    每次调用,都会将数组中的数据作为参数传递
                - 回调函数中有三个参数:
                    element 当前的元素
                    index 当前元素的索引
                    array 被遍历的数组

6.43 Set和数组的区别:

  • API不同
  • Set元素不能重复
  • Set是无序结构,操作很快
Set
    - Set用来创建一个集合
    - 它的功能和数组类似,不同点在于Set中不能存储重复的数据

- 使用方式:
    创建
        - new Set()
        - new Set([...])

    方法
        size 获取数量
        add() 添加元素
        has() 检查元素
        delete() 删除元素

可以利用Set元素不能重复来去重操作

 const arr2 = [1,2,3,2,1,3,4,5,4,6,7,7,8,9,10]
 const set2=new Set(arr2)
const arr3=[...set2]
console.log(arr3);//[ 1, 2, 3, 4,  5, 6, 7, 8, 9, 10 ]

6.5数组求和 Array reduce

reduce()
    - 可以用来将一个数组中的所有元素整合为一个值
    - 参数:
        1. 回调函数,通过回调函数来指定合并的规则
        2. 可选参数,初始值
// reduce() - 可以用来将一个数组中的所有元素整合为一个值*/
                   arr = [1, 2, 3, 4, 5, 6, 7, 8]
                  const  result3 =arr.reduce((a,b)=>a+b)
                    console.log(result3);//36

计数:

// 计数
        const arr = [10, 20, 30, 40, 50, 10, 20, 30, 20]
        const n = 30
        const count = arr.reduce((count, val) => {
            return val === n ? count + 1 : count
        }, 0)
        console.log('count', count)//count 2

九、框架篇:从VUE2到VUE3(1)

Vue.js被定义为一个渐进式的框架,由于简单性、规模、速度和广泛功能的成功结合,它的需求量已经很大。Vue具有模板、指令、CSS转换和动画等特性,开发效率高,已经成为了前端工程师必不可少的技能之一。

Class组件和函数组件区别:

  • Class组件: 是传统的Vue组件形式,使用class关键字定义组件,包含datamethodscomputed等选项。

  • 函数组件: 是Vue 2.x 中引入的一种更简洁的组件形式,通过函数定义组件,组件的状态和逻辑都在函数内部,适用于简单的展示型组件。

1、指令、插值

Vue模板语法有2大类:
   1.插值语法:
         功能:用于解析标签体内容。
         写法:{{xxx}},xxx是js表达式,且可以直接读取到data中的所有属性。
   2.指令语法:
         功能:用于解析标签(包括:标签属性、标签体内容、绑定事件.....)。
         举例:v-bind:href="xxx" 或  简写为 :href="xxx",xxx同样要写js表达式,
                且可以直接读取到data中的所有属性。
         备注:Vue中有很多的指令,且形式都是:v-????,此处我们只是拿v-bind举个例子。
 <div id="app">
    <h1>插值语法</h1>
    <h1>{{name}}</h1>
    <hr>
    <h1>指令语法</h1>
    <a v-bind:href="school.url">点我跳转{{school.name}}</a>
    <hr>
    <!-- 简写-->
    <a :href="school.url">点我跳转{{school.name}}</a>
  </div>


export default {
 name: 'App',//el用于指定当前Vue实例为哪个容器服务,值通常为css选择器字符串。
  data(){///data中用于存储数据,数据供el所指定的容器去使用,值我们暂时先写成一个对象。
    return{
      name:'hello',
      school:{
        name:"CSDN",
        url:'https://www.csdn.net/'
      }
    }
  },

2、computed和watch

计算属性:
      1.定义:要用的属性不存在,要通过已有属性计算得来。
      2.原理:底层借助了Objcet.defineproperty方法提供的getter和setter。
      3.get函数什么时候执行?
               (1).初次读取时会执行一次。
               (2).当依赖的数据发生改变时会被再次调用。
      4.优势:与methods实现相比,内部有缓存机制(复用),效率更高,调试方便。
      5.备注:
            1.计算属性最终会出现在vm上,直接读取使用即可。不能compoted.
            2.如果计算属性要被修改,那必须写set函数去响应修改,且set中要引起计算时依赖的数据发生改变。

监视属性watch:
   1.当被监视的属性变化时, 回调函数自动调用, 进行相关操作
   2.监视的属性必须存在,才能进行监视!!
   3.监视的两种写法:
         (1).new Vue时传入watch配置
         (2).通过vm.$watch监视

3、条件渲染和循环渲染(v-if和v-show和v-for)

3.1v-if v-else v-show写用法

v-if 写法:
(1).v-if="表达式"
(2).v-else-if="表达式"
(3).v-else="表达式"
v-show写法:v-show="表达式"

3.2 v-if 和v-show的区别

v-if适用于:切换频率较低的场景。
特点:不展示的DOM元素直接被移除。
注意:v-if可以和:v-else-if、v-else一起使用,但要求结构不能被“打断”。

 v-show适用于:切换频率较高的场景。
 特点:不展示的DOM元素未被移除,仅仅是使用样式隐藏掉

备注:使用v-if的时,元素可能无法获取到,而使用v-show一定可以获取到。

3.3 v-if 和 v-show的使用场景

  • v-if 与 v-show 都能控制dom元素在页面的显示
  • v-if 相比 v-show 开销更大的(直接操作dom节点增加与删除)
  • 如果需要非常频繁地切换,则使用 v-show 较好
  • 如果在运行时条件很少改变,则使用 v-if 较好

3.4 v-for的用法

v-for指令:
        1.用于展示列表数据
        2.语法:v-for="(item, index) in xxx" :key="yyy"  for of 也可以
        3.可遍历:数组、对象、字符串(用的很少)、指定次数(用的很少)

3.5key的重要性

面试题:react、vue中的key有什么作用?(key的内部原理)
diff算法中通过tag和key来判断是否为sanmeNode

      1. 虚拟DOM中key的作用:
                  key是虚拟DOM对象的标识,当数据发生变化时,Vue会根据【新数据】生成【新的虚拟DOM】,
                  随后Vue进行【新虚拟DOM】与【旧虚拟DOM】的差异比较,比较规则如下:

      2.对比规则:
               (1).旧虚拟DOM中找到了与新虚拟DOM相同的key:
                        ①.若虚拟DOM中内容没变, 直接使用之前的真实DOM!
                        ②.若虚拟DOM中内容变了, 则生成新的真实DOM,随后替换掉页面中之前的真实DOM。

               (2).旧虚拟DOM中未找到与新虚拟DOM相同的key
                        创建新的真实DOM,随后渲染到到页面。

      3. 用index作为key可能会引发的问题:
                     1. 若对数据进行:逆序添加、逆序删除等破坏顺序操作:
                                 会产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低。

                     2. 如果结构中还包含输入类的DOM:
                                 会产生错误DOM更新 ==> 界面有问题。

      4. 开发中如何选择key?:
                     1.最好使用每条数据的唯一标识作为key, 比如id、手机号、身份证号、学号等唯一值。
                     2.如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,
                        使用index作为key是没有问题的。

3.6一个注意点

v-for和v-if不能一起使用

Vue的v-for指令用于循环渲染一个列表,而 v-if 指令则用于根据条件动态地渲染元素。 这两个指令无法同时 使用 ,因为v-for指令优先于 v-if 指令,所以如果同时 使用 , v-if 指令中的条件判断将不会被执行。

4、事件

在DOM中存在着多种不同类型的事件对象
                - 多种事件对象有一个共同的祖先 Event
                    - event.target 触发事件的对象
                    - event.currentTarget 绑定事件的对象(同this)

                    - event.stopPropagation() 停止事件的传导
                    - event.preventDefault() 取消默认行为-->

            事件的基本使用:
                     1.使用v-on:xxx 或 @xxx 绑定事件,其中xxx是事件名;
                     2.事件的回调需要配置在methods对象中,最终会在vm上;
                     3.methods中配置的函数,不要用箭头函数!否则this就不是vm了;
                     4.methods中配置的函数,都是被Vue所管理的函数,this的指向是vm 或 组件实例对象;
                     5.@click="demo" 和 @click="demo($event)" 效果一致,但后者可以传参;

5、按键修饰符

1.Vue中常用的按键别名:
         回车 => enter
         删除 => delete (捕获“删除”和“退格”键)
         退出 => esc
         空格 => space
         换行 => tab (特殊,必须配合keydown去使用)
         上 => up
         下 => down
         左 => left
         右 => right

2.Vue未提供别名的按键,可以使用按键原始的key值去绑定,但注意要转为kebab-case(短横线命名)

3.系统修饰键(用法特殊):ctrl、alt、shift、meta
         (1).配合keyup使用:按下修饰键的同时,再按下其他键,随后释放其他键,事件才被触发。
         (2).配合keydown使用:正常触发事件。

4.也可以使用keyCode去指定具体的按键(不推荐)

5.Vue.config.keyCodes.自定义键名 = 键码,可以去定制按键别名

6、表单

Vue中有2种数据绑定的方式:
      1.单向绑定(v-bind):数据只能从data流向页面。
      2.双向绑定(v-model):数据不仅能从data流向页面,还可以从页面流向data。
         备注:
               1.双向绑定一般都应用在表单类元素上(如:input、select等)
               2.v-model:value 可以简写为 v-model,因为v-model默认收集的就是value值。

7、组件

 组件通信方式有哪些:面试几率高


props:用于父给子组件通信  :age="myAge"
自定义事件: @emit 可以实现子给父通信 
<子组件 @sendMsg="process" ref="child"></子组件>
全局事件总线:$bus 父子 子父 兄弟 全能
pubsub-js:vue几乎不用 全能
插槽
vuex

8、生命周期(单个组件)和生命周期(父子组件)

生命周期图:

 在vue2中生命周期有三个阶段:

  • 挂载阶段
  • 更新阶段
  • 销毁阶段

生命周期mounted和created的区别?

  • created:在模板渲染成html前调用,即通常初始化某些属性值,然后再渲染成视图。
  • mounted:在模板渲染成html后调用,通常是初始化页面完成后,再对html的dom节点进行一些需要的操作

带有父子组件的生命周期:

就是创建初始化Vue的一个实例是从外到内的。但是渲染呢,是从内到外的,初始化是父组件到子组件 ,渲染是子组件到父组件

以Father父组件和Son子组件为例

Father,beforeCreate执行了
Father,created执行了
Father,beforeMount执行了
Son,beforeCreate执行了
Son,created执行了
Son,beforeMount执行了
Son,mounted执行了
Father,mounted执行了

 

 

子组件销毁时的执行顺序:

父组件beforeCreate --> 父组件created --> 父组件beforeMount  --> 子组件beforeCreate --> 子组件created --> 子组件beforeMount  -->  子组件 mounted  --> 父组件mounted -->父组件beforeUpdate -->子组件beforeDestroy--> 子组件destroyed --> 父组件updated

9、Vue的高级特性

9.1 自定义 v-model

v-model都知道是双向绑定:

1.双向绑定一般都应用在表单类元素上(如:input、select等)
2.v-model:value 可以简写为 v-model,因为v-model默认收集的就是value值。

注意点:

这一点其实都学过只不过简写时间太久所以都忘了,在这提一嘴:

单向绑定和双向绑定运用于表单时的完整写法:

单向数据绑定:<input type="text" v-bind:value="name">
双向数据绑定:<input type="text" v-model:value="name">

简写写法:

单向数据绑定:<input type="text"  :value="name"><br>
双向数据绑定:<input type="text"  v-model="name">

注意:v-model:value 可以简写为 v-model,因为v-model默认收集的就是value值。但是v-bind可一定不能省略。

v-model指令的原理是什么?

原生dom中有oninput事件,他经常结合表单元素使用,当表单元素文本内容发生变化时。就会发出一次回调

  1. v-bind绑定一个value属性  (单向绑定:v-bind,数据只能从data流向页面)
  2. v-on监听当前元素的input事件,当数据变化时,将值传递给value实时更新数据(v-on指令是事件绑定指令  这一步控制页面流向data)
   输入框:<input type="text" :value="info" @input="info=$event.target.value">

等价于:

  输入框:<input type="text" v-model="info">
<template>
  <div id="app">
    父组件输入框:<input v-model="info">
<Son :value="info" @input="info=$event"></Son>
  </div>
</template>
<template>
  <div>
    <!--  // $emit 方法可以触发当前实例上的事件,这里触发的事input事件,附加参数都会传给监听器回调
    // input 事件在用户输入时触发,它是在元素值发生变化时立即触发
 -->
 输入框:<input type="text" :value="value" @input="$emit('input',$event.target.value)" >
  </div>
</template>

以上可以使用下列方法可以实现组件信息同步

9.2 $nextTick

  • Vue是异步渲染
  • data改变之后,DOM不会立刻渲染
  • $nextTick 会在DOM渲染之后被触发,以获取最新DOM节点
  • $nextTick在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

// 修改数据
vm.msg = 'Hello'
// DOM 还没有更新
Vue.nextTick(function () {
  // DOM 更新了
})

常见场景有轮播图:

 watch:{
    //监听bannerList数据的变化,因为这条数据发生过变化---由空数组变为数组里面有四个元素
         immediate: true,
      handler(newValue,oldValue){
        //现在通过watch监听bannerList属性的属性值的变化
        //如果执行handle方法,代表组件实例身上这个属性已经有了【数组:四个元素】
        //当前这个函数执行:只能保证bannerList数据已经有了但没办法保证V-for循环已经结束
        //v-for执行完毕,才有结构【你现在在watch当中没办法保证】
        //nextTick:在下次DOM更新 循环结束之后 执行延迟回调,在 修改数据之后 立即使用这个方法,过去更新后的DOM
    bannerList:{
      //监听属性:立即监听,不管你属性有没有发生变化,立即执行一次。
 //nextTick:在下次DOM更新 循环结束之后 执行延迟回调,在 修改数据之后 立即使用这个方法,过去更新后的DOM
        this.$nextTick(()=>{
          //当你执行这个回调的时候:保证服务器数据回来了,v-for执行完毕了【一定轮播图的解构一定有了】
          new Swiper (xxx, {
            xxx
        }
        }

}
}}

为什么要用watch而不用monted:因为disptch中涉及到异步所以new Swiper实例直接放在monted中是不行的要么也让他异步(加个定时器),要么用watch等他数据变化在执行。

watch只是监视数据更新,但是界面还没有更新,所以Swiper没有效果,所以要想一个办法要等DOM更新后在立即渲染,所以用到了$nextTick

9.3 slot插槽

  • 1. 作用:让父组件可以向子组件指定位置插入html结构,也是一种组件间通信的方式
  • 2. 分类:默认插槽、具名插槽、作用域插槽
  • 3. 使用方式:

1. 默认插槽:

父组件中:
               <Category>
                  <div>html结构1</div>
               </Category>
       子组件中:
               <template>
                   <div>
                      <!-- 定义插槽 -->
                      <slot>插槽默认内容...</slot>
                   </div>
               </template>

2. 具名插槽:

       父组件中:
               <Category>
                   <template slot="center">
                     <div>html结构1</div>
                   </template>
       
                   <template v-slot:footer>
                      <div>html结构2</div>
                   </template>
               </Category>
       子组件中:
               <template>
                   <div>
                      <!-- 定义插槽 -->
                      <slot name="center">插槽默认内容...</slot>
                      <slot name="footer">插槽默认内容...</slot>
                   </div>
               </template>

3、作用域插槽:

作用域插槽其实就是带数据的插槽,即带参数的插槽,简单的来说就是父组件想要获得子组件的数据,该参数仅限于插槽中使用,父组件可根据子组件传过来的插槽数据来进行不同的方式展现和填充插槽内容。 作用域插槽绑定了一套数据,父组件可以拿来用。

Category子组件:

<template>
  <div class="category">
    <h3>{{title}}分类</h3>
    <!-- 定义一个插槽(挖个坑,等着组件的使用者进行填充) -->
    <slot :games="games">我是一些默认值,当使用者没有传递具体结构时,我会出现</slot>
  </div>
</template>
<script>
export default {
  name: "Category",
  data(){
    return{
      games:['红色警戒','穿越火线','劲舞团','超级玛丽'],
    }
  },
  props:['title']
}

App.vue父组件:

<template>
    <div class="container">
      <Category title="游戏">
        <template v-slot="{games}">
          <h4 v-for="(g,index) in games" :key="index">{{g}}</h4>
        </template>
      </Category>
</div>
</template>
<script>
//引入组件
import Category from "./components/Category";
//Category 类别
export default {
  name: "App",
  components: {Category},
}

通过作用域插槽可以实现父子组件通信,在实际场景中使用element-ui的el-table组件。需要渲染数据,就用了插槽。

<el-table style="width: 100%" border :data="skuList" v-loading="loading">
        <el-table-column label="默认图片" width="width">
          <template v-slot="{row,$index}">
            <img :src="row.skuDefaultImg" style="width:100px;height:100px;">
          </template>
        </el-table-column>
      </el-table>

row代表数据,这些是el-table利用插槽设计的,清楚怎样实现渲染就行。

9.4 异步组件

在这些大型的Vue应用中,不管是为了代码的抽离,还是逻辑的划分,不可避免的会将应用分割成一些很小的代码块,形成我们意识上的组件,在需要的地方可以进行 import 引入

import TypeNav from "@/components/TypeNav";
//全局组件注册方式:
//第一参数:组件名字  第二个参数:哪一个组件
Vue.component(TypeNav.name,TypeNav)

但是在某些场景中,比如:

1、这个组件的体积很大

2、它不是页面一开始就需要的

可以采用异步组件,

异步组件的好处:

提高性能。在大型应用中,我们可以将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。通常的使用就是在配置路由的时候,使用异步组件的加载方式,只有路由被触发时,才会加载对应的组件。而不是一次性加载所有的组件,这样很有利于提高性能。

// 全局注册
Vue.component('child2', () => import('./components/child2'))
 
// 局部注册
components: {
  Child2: () => import('./components/child2')
}

高阶组件

高阶组件的方式可以处理异步组件的加载状态和错误状态。loading 和 error 支持传入组件。

// 全局注册
Vue.component('child3', () => ({
    component: import('./components/child3.vue'),
    loading: {template: '<div>Loading</div>'},
    error: {template: '<div>error</div>'},
    delay: 200,
    timeout: 3000
  })
)
// 局部注册
components: {
  Child3: () => ({
    // 需要加载的组件 (应该是一个 `Promise` 对象)
    component: import('./components/child3.vue'),
    // 异步组件加载时使用的组件
    loading: {template: '<div>Loading</div>'},
    // 加载失败时使用的组件
    error: {template: '<div>error</div>'},
    // 展示加载时组件的延时时间。默认值是 200 (毫秒)
    delay: 200,
    // 如果提供了超时时间且组件加载也超时了,
    // 则使用加载失败时使用的组件。默认值是:`Infinity`
    timeout: 3000
  })
}

9.5路由懒加载

当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就会更加高效。
Vue Router 支持开箱即用的动态导入,这意味着你可以用动态导入代替静态导入:
component (和 components) 配置接收一个返回 Promise 组件的函数,Vue Router 只会在第一次进入页面时才会获取这个函数,然后使用缓存数据。这意味着你也可以使用更复杂的函数,只要它们返回一个 Promise :

实际就是
 //这种写法称为路由的懒加载 用户访问的时候才引入组件
        component: () => import("../pages/Center")

路由懒加载就是在异步组件基础上实现的
 

9.6 keep-alive组件缓存

作用:

  • 缓存组件
  • 频繁切换,不需要重复渲染
  • Vue常见性能优化

什么情况下需要使用keep-alive(组件缓存)?

比如跳转到详情页面,需要保持列表页的滚动条的深度,等返回的时候依然在这个位置,这样可以提高用户的体验,在vue中,对于这种“页面缓存”的需求,我们可以使用keep-alive组件来解决这个需求。

在组件切换过程中将状态保留在内存中,防止重复渲染DOM,减少加载时间及性能消耗,提高用户体验性

 {
        path: '/detail/:skuid',
        component: () => import('../pages/Detail'),
        //true则显示footer
        meta: {
            keepAlive: true  // 需要缓存
        }
    },

9.7 mixin

mixin(混入)

1. 功能:可以把多个组件共用的配置提取成一个混入对象

2. 使用方式:

第一步定义混合:

 {
        data(){....},
        methods:{....}
        ....
    }

  第二步使用混入:

        全局混入:Vue.mixin(xxx)
   ​    局部混入:mixins:['xxx']

10、Vuex使用

### 3.搭建vuex环境

1. 创建文件:```src/store/index.js```

   ```js
   //引入Vue核心库
   import Vue from 'vue'
   //引入Vuex
   import Vuex from 'vuex'
   //应用Vuex插件
   Vue.use(Vuex)
   
   //准备actions对象——响应组件中用户的动作
   const actions = {}
   //准备mutations对象——修改state中的数据
   const mutations = {}
   //准备state对象——保存具体的数据
   const state = {}
   
   //创建并暴露store
   export default new Vuex.Store({
       actions,
       mutations,
       state
   })
   ```

2. 在```main.js```中创建vm时传入```store```配置项

   ```js
   ......
   //引入store
   import store from './store'
   ......
   
   //创建vm
   new Vue({
       el:'#app',
       render: h => h(App),
       store
   })
   ```

###    4.基本使用

1. 初始化数据、配置```actions```、配置```mutations```,操作文件```store.js```

   ```js
   //引入Vue核心库
   import Vue from 'vue'
   //引入Vuex
   import Vuex from 'vuex'
   //引用Vuex
   Vue.use(Vuex)
   
   const actions = {
       //响应组件中加的动作
       jia(context,value){
          // console.log('actions中的jia被调用了',miniStore,value)
          context.commit('JIA',value)
       },
   }
   
   const mutations = {
       //执行加
       JIA(state,value){
          // console.log('mutations中的JIA被调用了',state,value)
          state.sum += value
       }
   }
   
   //初始化数据
   const state = {
      sum:0
   }
   
   //创建并暴露store
   export default new Vuex.Store({
       actions,
       mutations,
       state,
   })
   ```

2. 组件中读取vuex中的数据:```$store.state.sum```

3. 组件中修改vuex中的数据:```$store.dispatch('action中的方法名',数据)``` 或 ```$store.commit('mutations中的方法名',数据)```

   >  备注:若没有网络请求或其他业务逻辑,组件中也可以越过actions,即不写```dispatch```,直接编写```commit```

### 5.getters的使用

1. 概念:当state中的数据需要经过加工后再使用时,可以使用getters加工。

2. 在```store.js```中追加```getters```配置

   ```js
   ......
   
   const getters = {
       bigSum(state){
          return state.sum * 10
       }
   }
   
   //创建并暴露store
   export default new Vuex.Store({
       ......
       getters
   })
   ```

3. 组件中读取数据:```$store.getters.bigSum```

### 6.四个map方法的使用

1. <strong>mapState方法:</strong>用于帮助我们映射```state```中的数据为计算属性

   ```js
   computed: {
       //借助mapState生成计算属性:sum、school、subject(对象写法)
        ...mapState({sum:'sum',school:'school',subject:'subject'}),
            
       //借助mapState生成计算属性:sum、school、subject(数组写法)
       ...mapState(['sum','school','subject']),
   },
   ```

2. <strong>mapGetters方法:</strong>用于帮助我们映射```getters```中的数据为计算属性

   ```js
   computed: {
       //借助mapGetters生成计算属性:bigSum(对象写法)
       ...mapGetters({bigSum:'bigSum'}),
   
       //借助mapGetters生成计算属性:bigSum(数组写法)
       ...mapGetters(['bigSum'])
   },
   ```

3. <strong>mapActions方法:</strong>用于帮助我们生成与```actions```对话的方法,即:包含```$store.dispatch(xxx)```的函数

   ```js
   methods:{
       //靠mapActions生成:incrementOdd、incrementWait(对象形式)
       ...mapActions({incrementOdd:'jiaOdd',incrementWait:'jiaWait'})
   
       //靠mapActions生成:incrementOdd、incrementWait(数组形式)
       ...mapActions(['jiaOdd','jiaWait'])
   }
   ```

4. <strong>mapMutations方法:</strong>用于帮助我们生成与```mutations```对话的方法,即:包含```$store.commit(xxx)```的函数

   ```js
   methods:{
       //靠mapActions生成:increment、decrement(对象形式)
       ...mapMutations({increment:'JIA',decrement:'JIAN'}),
       
       //靠mapMutations生成:JIA、JIAN(对象形式)
       ...mapMutations(['JIA','JIAN']),
   }
   ```

> 备注:mapActions与mapMutations使用时,若需要传递参数需要:在模板中绑定事件时传递好参数,否则参数是事件对象。

### 7.模块化+命名空间

1. 目的:让代码更好维护,让多种数据分类更加明确。

2. 修改```store.js```

   ```javascript
   const countAbout = {
     namespaced:true,//开启命名空间
     state:{x:1},
     mutations: { ... },
     actions: { ... },
     getters: {
       bigSum(state){
          return state.sum * 10
       }
     }
   }
   
   const personAbout = {
     namespaced:true,//开启命名空间
     state:{ ... },
     mutations: { ... },
     actions: { ... }
   }
   
   const store = new Vuex.Store({
     modules: {
       countAbout,
       personAbout
     }
   })
   ```

3. 开启命名空间后,组件中读取state数据:

   ```js
   //方式一:自己直接读取
   this.$store.state.personAbout.list
   //方式二:借助mapState读取:
   ...mapState('countAbout',['sum','school','subject']),
   ```

4. 开启命名空间后,组件中读取getters数据:

   ```js
   //方式一:自己直接读取
   this.$store.getters['personAbout/firstPersonName']
   //方式二:借助mapGetters读取:
   ...mapGetters('countAbout',['bigSum'])
   ```

5. 开启命名空间后,组件中调用dispatch

   ```js
   //方式一:自己直接dispatch
   this.$store.dispatch('personAbout/addPersonWang',person)
   //方式二:借助mapActions:
   ...mapActions('countAbout',{incrementOdd:'jiaOdd',incrementWait:'jiaWait'})
   ```

6. 开启命名空间后,组件中调用commit

   ```js
   //方式一:自己直接commit
   this.$store.commit('personAbout/ADD_PERSON',person)
   //方式二:借助mapMutations:
   ...mapMutations('countAbout',{increment:'JIA',decrement:'JIAN'}),
   ```

11、路由router

11.1 基本使用

  • 1. 安装vue-router,命令:```npm i vue-router```
  • 2. 应用插件:```Vue.use(VueRouter)```
  • 3. 编写router配置项:

   //引入VueRouter
   import VueRouter from 'vue-router'
   //引入Luyou 组件
   import About from '../components/About'
   
   //创建router实例对象,去管理一组一组的路由规则
   const router = new VueRouter({
       routes:[
          {
             path:'/about',
             component:About
          },
          {
             path:'/home',
              component: () => import('../components/Home'),//路由懒加载式引用
          }
       ]
   })
   
   //暴露router
   export default router

11.2路由切换的两种方式

路由实现切换的方式有两种:声明式导航编程式导航

声明式导航:

 声明式导航实现切换(要写完整路径):
  <router-link to="/about">About</router-link>

 指定展示位置
   <router-view></router-view>

几个注意点

  • 1. 路由组件通常存放在```pages```文件夹,一般组件通常存放在```components```文件夹。
  • 2. 通过切换,“隐藏”了的路由组件,默认是被销毁掉的,需要的时候再去挂载。
  • 3. 每个组件都有自己的```$route```属性,里面存储着自己的路由信息。
  • 4. 整个应用只有一个router,可以通过组件的```$router```属性获取到

<router-link>```的replace属性

  • 1. 作用:控制路由跳转时操作浏览器历史记录的模式
  • 2. 浏览器的历史记录有两种写入方式:分别为```push```和```replace```,```push```是追加历史记录,```replace```是替换当前记录。路由跳转时候默认为```push```
  • 3. 如何开启```replace```模式:```<router-link replace .......>News</router-link>

缓存路由组件

1. 作用:让不展示的路由组件保持挂载,不被销毁。

2. 具体编码:

  

 <keep-alive include="News"> 
       <router-view></router-view>
   </keep-alive>

编程式路由导航:

 1. 作用:不借助```<router-link> ```实现路由跳转,让路由跳转更加灵活

2. 具体编码:

     //$router的两个API
   this.$router.push({
       name:'xiangqing',
          params:{
             id:xxx,
             title:xxx
          }
   })
   
   this.$router.replace({
       name:'xiangqing',
          params:{
             id:xxx,
             title:xxx
          }
   })
   this.$router.forward() //前进
   this.$router.back() //后退
   this.$router.go() //可前进也可后退

11.3多级路由

配置路由规则,使用children配置项

routes:[
       {
          path:'/about',
          component:About,
       },
       {
          path:'/home',
          component:Home,
          children:[ //通过children配置子级路由
             {
                path:'news', //此处一定不要写:/news
                component:News
             },
             {
                path:'message',//此处一定不要写:/message
                component:Message
             }
          ]
       }
   ]

11.4路由的两种传参方式

路由的传参方式有两种:通过query传参和通过params参数传参

路由的query参数:

1. 传递参数:

<!-- 跳转并携带query参数,to的字符串写法 -->
   <router-link :to="/home/message/detail?id=666&title=你好">跳转</router-link>
                
   <!-- 跳转并携带query参数,to的对象写法 -->
   <router-link 
       :to="{
          path:'/home/message/detail',
          query:{
             id:666,
               title:'你好'
          }
       }"
   >跳转</router-link>

2. 接收参数:

   $route.query.id
   $route.query.title

路由的params参数


1. 配置路由,声明接收params参数

  {
       path:'/home',
       component:Home,
       children:[
          {
             path:'news',
             component:News
          },
          {
             component:Message,
             children:[
                {
                   name:'xiangqing',
                   path:'detail/:id/:title', //使用占位符声明接收params参数
                   component:Detail
                }
             ]
          }
       ]
   }

2. 传递参数

 <!-- 跳转并携带params参数,to的字符串写法 -->
   <router-link :to="/home/message/detail/666/你好">跳转</router-link>
                
   <!-- 跳转并携带params参数,to的对象写法 -->
   <router-link 
       :to="{
          name:'xiangqing',
          params:{
             id:666,
               title:'你好'
          }
       }"
   >跳转</router-link>

特别注意:路由携带params参数时,若使用to的对象写法,则不能使用path配置项,必须使用name配置!

编程式导航传递参数的示例:


当使用 Vue Router 时,你可以通过 $router.push 方法进行编程式导航,并且通过对象中的 params 选项传递参数。以下是一个简单的 Vue 组件的例子:


<template>
  <div>
    <p>This is the source component.</p>
    <button @click="navigateWithParams">Navigate</button>
  </div>
</template>

<script>
export default {
  methods: {
    navigateWithParams() {
      // 传递 params 参数
      const userId = 123; // 这是你要传递的参数值
      this.$router.push({ name: 'destination', params: { id: userId } });
    }
  }
};
</script>

在这个例子中,我们通过 this.$router.push 方法进行编程式导航,指定了目标路由的 name'destination',并使用 params 选项传递了一个参数 id,其值为 123

然后,你需要在你的路由配置中匹配这个路由,并在目标组件中接收和使用这个参数:

// 在路由配置中
const routes = [
  // ...其他路由配置
  {
    path: '/destination/:id',
    name: 'destination',
    component: DestinationComponent
  }
];

// 在目标组件中
<template>
  <div>
    <p>This is the destination component.</p>
    <p>Param ID: {{ $route.params.id }}</p>
  </div>
</template>

<script>
export default {
  mounted() {
    // 访问路由参数
    const id = this.$route.params.id;
    console.log('Received Param ID:', id);
  }
};
</script>

3. 接收参数:

 $route.params.id
   $route.params.title

11.5方便路由接参\传参的几种配置

一、命名路由简化跳转

1. 作用:可以简化路由的跳转。

2. 如何使用

    1. 给路由命名:

  {
           path:'/demo',
           component:Demo,
           children:[
               {
                   path:'test',
                   component:Test,
                   children:[
                       {
                             name:'hello' //给路由命名
                           path:'welcome',
                           component:Hello,
                       }
                   ]
               }
           ]
       }

         2. 简化跳转:

 <!--简化前,需要写完整的路径 -->
       <router-link to="/demo/test/welcome">跳转</router-link>
       
       <!--简化后,直接通过名字跳转 -->
       <router-link :to="{name:'hello'}">跳转</router-link>
       
       <!--简化写法配合传递参数 -->
       <router-link 
           :to="{
               name:'hello',
               query:{
                  id:666,
                   title:'你好'
               }
           }"
       >跳转</router-link>

二、路由的props配置

作用:让路由组件更方便的收到参数

{
   name:'xiangqing',
   path:'detail/:id',
   component:Detail,

   //第一种写法:props值为对象,该对象中所有的key-value的组合最终都会通过props传给Detail组件
   // props:{a:900}

   //第二种写法:props值为布尔值,布尔值为true,则把路由收到的所有params参数通过props传给Detail组件
   // props:true
   
   //第三种写法:props值为函数,该函数返回的对象中每一组key-value都会通过props传给Detail组件
   props(route){
      return {
         id:route.query.id,
         title:route.query.title
      }
   }
}

11.6路由守卫

1. 作用:对路由进行权限控制

2. 分类:全局守卫、独享守卫、组件内守卫

一、全局守卫:

 //全局前置守卫:初始化时执行、每次路由切换前执行
   router.beforeEach((to,from,next)=>{
       console.log('beforeEach',to,from)
       if(to.meta.isAuth){ //判断当前路由是否需要进行权限控制
          if(localStorage.getItem('school') === 'atguigu'){ //权限控制的具体规则
             next() //放行
          }else{
             alert('暂无权限查看')
             // next({name:'guanyu'})
          }
       }else{
          next() //放行
       }
   })
   
   //全局后置守卫:初始化时执行、每次路由切换后执行
   router.afterEach((to,from)=>{
       console.log('afterEach',to,from)
       if(to.meta.title){ 
          document.title = to.meta.title //修改网页的title
       }else{
          document.title = 'vue_test'
       }
   })

二、独享守卫:

beforeEnter(to,from,next){
       console.log('beforeEnter',to,from)
       if(to.meta.isAuth){ //判断当前路由是否需要进行权限控制
          if(localStorage.getItem('school') === 'atguigu'){
             next()
          }else{
             alert('暂无权限查看')
             // next({name:'guanyu'})
          }
       }else{
          next()
       }
   }

三、组件内守卫:

   //进入守卫:通过路由规则,进入该组件时被调用
   beforeRouteEnter (to, from, next) {
   },
   //离开守卫:通过路由规则,离开该组件时被调用
   beforeRouteLeave (to, from, next) {
   }

11.7 两个新的生命周期钩子

1. 作用:路由组件所独有的两个钩子,用于捕获路由组件的激活状态。
2. 具体名字:
    1. ```activated```路由组件被激活时触发。
    2. ```deactivated```路由组件失活时触发。

11.8 路由器的两种工作模式

1. 对于一个url来说,什么是hash值?—— #及其后面的内容就是hash值。
2. hash值不会包含在 HTTP 请求中,即:hash值不会带给服务器。
3. hash模式:
    1. 地址中永远带着#号,不美观 。
    2. 若以后将地址通过第三方手机app分享,若app校验严格,则地址会被标记为不合法。
    3. 兼容性较好。
4. history模式:
    1. 地址干净,美观 。
    2. 兼容性和hash模式相比略差。
    3. 应用部署上线时需要后端人员支持,解决刷新页面服务端404的问题。

十、Vue原理:

1、数据驱动视图 MVVM模型

MVVM模型
            1. M:模型(Model) :data中的数据
            2. V:视图(View) :模板代码
            3. VM:视图模型(ViewModel):Vue实例
观察发现:
            1.data中所有的属性,最后都出现在了vm身上。
            2.vm身上所有的属性 及Vue原型上所有属性,在Vue模板中都可以直接使用。

2、Vue响应式

2.1核心API

学习Vue响应式之前先学习数据代理,在这之前先复习一个方法Object.defineProperty使用该方法给对象添加属性能够有更加细致的控制 比如:可以控制追加在对象中的元素是否可以用forEach()等遍历;添加之后是否可以删除等,所以在Vue数据劫持数据代理计算属性都利用了该方法。

核心API  Object.defineProperty(obj,property,descriptor)

参数一:obj
绑定属性的目标对象
参数二:property
绑定的属性名
参数三:descriptor
属性描述(配置),且此参数本身为一个对象

属性值1:value
设置属性默认值
属性值2:writable
设置属性是否能够修改
属性值3:enumerable
设置属性是否可以枚举,即是否允许遍历
属性值4:configurable
设置属性是否可以删除或编辑
属性值5:get
获取属性的值
属性值6:set
设置属性的值

Object.defineProperty实现响应式:

监听对象,监听数组:

let number=18
 let person={
     name:"张三",
     gender:"男",
 }
    Object.defineProperty(person,'age',{
   // value:18,
        // enumerable:true,//控制属性是否可以枚举,默认值是false
        // writable:true,//控制属性是否可以被修改,默认值是false
        // configurable:true//控制属性是否可以被删除,默认值是false
        //当有人读取person的age属性时,get函数(getter)就会被调用,且返回值就是age的值
        get() {
            console.log("有人读取age值了")
            return number
        },
        set(value) {
            console.log("有人修改age值了")
            number=value
            return value

        }
    })
    //console.log(Object.keys(person))//遍历对象返回结果:返回对象中每一项key的数组
    console.log(person)

当读取或修改对象或数组时都能检测到,监听到数组前提,重新定义原型,重写push pop等方法

复杂对象,深度监听:

 let person={
        name:"张三",
        gender:"男",
        arr:['张三','李四'],
        info:{
            id: 19555
        }
    }
    //封装监听数据变化的函数
    function defineProperty(obj, key, val) {
     observer(val)
        Object.defineProperty(person, key, {
            // 读取方法
            get() {
                console.log('读取数据成功')
                return val
            },
            set(newvalue) {
                // 赋值监听方法
                if (newvalue === val) return
                // 遍历监听数据的每一项
                observer(newvalue)
                val = newvalue
                console.log('数据修改成功')
                // 可以执行渲染操作

            }
        })
    }
    //深度监听需要递归
    function observer(obj){
     if (typeof obj !=='object'||obj==null){
         return
     }
     for (const key in obj){
         // 给对象中的每一项都设置响应式
         defineProperty(obj, key, obj[key])
     }
    }
    observer(person)

    console.log(person)

对于嵌套对象和数组,得需要深度监听进行递归。

几个缺点:

  • 深度监听,需要一次性递归到底,一次性计算量大
  • 无法监听新增属性/删除属性(Vue.set Vue.delete)
  • 无法原生监听数组,需要特殊处理

2.2数据代理

1.Vue中的数据代理:
         通过vm对象来代理data对象中属性的操作(读/写)
2.Vue中数据代理的好处:
         更加方便的操作data中的数据
3.基本原理:
         通过Object.defineProperty()把data对象中所有属性添加到vm上。
         为每一个添加到vm上的属性,都指定一个getter/setter。
         在getter/setter内部去操作(读/写)data中对应的属性。

Vue使用了数据代理(Data Proxy)来监听数据的变化,从而实现响应式的数据绑定。
在Vue中,当我们把一个普通的JavaScript对象传入Vue实例作为数据对象时,Vue会遍历这个对象的所有属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。这些 getter/setter 的实现中会进行依赖收集和派发更新的操作,从而实现了响应式数据绑定。
具体来说,当我们访问 Vue 实例中的数据时,实际上是通过代理实现的。Vue会将我们的数据对象代理到 Vue 实例上,例如:

3、DOM操作

虚拟DOM(Virtual Dom)和 diff

  • vdom是实现vue和react的重要基石
  • diff算法是vdom中最核心、最关键的部分
  • vdom是一个门话题,也是面试中的热门问题

3.1 vdom

真实DOM:

DOM意思是文档对象模型(Dcoument Object Model),它是一个结构化文本的抽象

操作DOM

所以,只要我们想要动态修改网页的内容的时候,我们就修改DOM。

var item = document.getElementById("myLI");
item.parentNode.removeChild(item);

虚拟DOM:

虚拟dom Virtual DOM

Virtual DOM 只是js模拟的DOM结构。 虚拟DOM是HTML DOM的抽象 。

虚拟DOM是由js实现的避免DOM树频繁更新,通过js的对象模拟DOM中的节点,然后通过特定的render方法将它渲染成真实的节点,数据更新时,渲染得到新的 Virtual DOM,与上一次得到的 Virtual DOM 进行 diff,得到所有需要在 DOM 上进行的变更,然后在 patch 过程中应用到 DOM 上实现UI的同步更新。

优点:

  1. Real DOM 操作可以直接对页面元素进行修改,但它的性能较差,而 Virtual DOM 利用 JavaScript 对象的高效性能,可以减少实际 DOM 操作的次数,提高页面性能。

  2. Virtual DOM 可以更方便地进行跨平台开发,因为它只是一个 JavaScript 对象,可以使用相同的代码来生成不同平台的页面。

缺点:

  1. 使用 Virtual DOM 需要额外的内存开销,因为它需要创建并维护一个 DOM 对象的副本。

  2. Virtual DOM 可能会导致比较复杂的逻辑,因为需要将 JavaScript 对象转换成实际 DOM,这可能会导致代码难以维护和理解。

DOM操作非常耗费性能,以前用JQuery,可以自行控制DOM操作的时机,手动调整,Vue和React是数据驱动视图,如何有效控制DOM操作?

解决方案—vdom

  • 有了一定复杂度,想减少计算次数比较难
  • 能不能把计算,更多的转移JS计算?因为JS执行速度很快
  • vdom-用JS模拟DOM结构,计算出最小的变更,操作DOM

用JS模拟DOM结构:

<div id="div1" class="container">
    <p>vdom</p>
    <ul style="font-size: 20px">
        <li>a</li>
    </ul>
</div>
 {
        tag:'div',
            props:{
            className:'container', 
            id:'div'
    }
    children:[
        {
            tag:'p',
            children:'vdom'
        },
        {
            tag:'p',
            props:{style:'font-size: 20px'}
            children:[
                {
                    tag:'li',
                    children:'a'
                }
                ]
        }
    ]
    }

vdom总结

  • 用JS模拟DOM结构(vnode)
  • 新旧vnode对比,得出最小的更新范围,最后更新DOM
  • 数据驱动视图的模式下,有效控制DOM操作

在之前学key的时候也提到了:

旧虚拟DOM中找到了与新虚拟DOM相同的key: ①.若虚拟DOM中内容没变, 直接使用之前的真实DOM! ②.若虚拟DOM中内容变了, 则生成新的真实DOM,随后替换掉页面中之前的真实DOM。虚拟DOM会根据key。

那么怎么对比呢?就有了diff算法:

diff 算法是一种通过同层的树节点进行比较的高效算法,避免了对树进行逐层搜索遍历,所以时间复杂度只有 O (n)。. diff 算法的在很多场景下都有应用,例如在 vue 虚拟 dom 渲染成真实 dom 的新旧 VNode 节点比较更新时,就用到了该算法。. diff 算法有两个比较显著的特点:. 比较只会在同层级进行, 不会跨层级比较。. 在 diff 比较的过程中,循环从两边向中间收拢。

diff算法总结:

patchVnode:

patchVnode 是在虚拟 DOM 实现中常见的一个函数,特别是在像 Vue.js 和 React 这样的框架中。

简单来说,patchVnode 是一个比较两个虚拟节点并更新 DOM 以反映它们之间任何差异的函数。通常在虚拟 DOM 重新渲染周期中调用,当框架检测到组件的数据发生更改时。

下面是一个 patchVnode 可能的工作示例:

  1. 框架创建一个新的虚拟节点,表示组件的更新状态。

  2. 框架使用旧的虚拟节点和新的虚拟节点调用 patchVnode

  3. patchVnode 比较这两个节点,并确定需要更新的 DOM 的哪些部分。

  4. patchVnode 对 DOM 进行任何必要的更新,以反映旧的虚拟节点和新的虚拟节点之间的更改。

  5. 虚拟 DOM 重新渲染周期完成,更新后的组件现在显示在页面上。

patchVnode 在虚拟 DOM 实现中是一个重要的函数,因为它允许在不必重新渲染整个页面的情况下有效地更新 DOM。这可以带来更快和更响应的用户界面。

addVnodes  removeVnodes:

addVnodesremoveVnodes 是在虚拟 DOM 实现中常用的两个函数,特别是在像 Vue.js 和 React 这样的框架中。

简单来说,addVnodesremoveVnodes 分别用于向 DOM 中添加或移除一组子节点。它们通常在虚拟 DOM 重新渲染周期中调用,当框架检测到组件的子节点发生更改时。

下面是 addVnodesremoveVnodes 可能的工作示例:

  1. addVnodes 函数将一组新的子节点添加到指定的 DOM 节点中。

  2. removeVnodes 函数将一组旧的子节点从指定的 DOM 节点中移除。

  3. 虚拟 DOM 重新渲染周期完成,更新后的组件现在显示在页面上。

addVnodesremoveVnodes 在虚拟 DOM 实现中是重要的函数,因为它们允许框架高效地更新 DOM,而不必重新渲染整个页面。这可以提高应用程序的性能和响应速度。

需要注意的是,addVnodesremoveVnodes 只是 DOM 操作的一部分,它们通常与 patchVnode 一起使用,以使整个虚拟 DOM 的更新更加高效和正确。

updateChildren:

updateChildren 是常用于 JavaScript 框架(如 React 和 Vue)中的一种方法,用于更新虚拟 DOM 中组件或元素的子元素。

总的来说,updateChildren 是一种有用的方法,可有效地更新虚拟 DOM 中组件或元素的子元素,对现代 Web 应用程序的性能起着至关重要的作用。

4、模板编译

模板编译的总体过程:使用 vue template complier 将模板编译为 render 函数,然后执行 render 函数生成vnode
模板编译的主要目标就是生成渲染函数,而渲染函数的作用是每次执行它,它就会使用当前最新的状态生成一份新的 vnode,然后使用这个 vnode 进行渲染。

  • 前置知识:JS的with语法
  • vue template complier将模板编译为render函数
  • 执行render函数生成vnode

4.1with语法

  • 改变{}内自由变量的查找规则,当做obj属性来查找
  • 如果找不到匹配的obj属性,就会报错
  • with要慎用,它打破了作用域规则,易读性变差

4.2编译模板

  • 模板不是html ,有指令、插值、JS表达式,能实现判断、循环
  • html是标签语言,只有JS才能实现判断、循环(图灵完备的)
  • 因此,模板一定是转换为某种JS代码,即编译模板
  • 模板编译为render函数,执行render 函数返回vnode
  • 基于vnode 再执行patch和diff(后面会讲)
  • 使用webpack vue-loader ,会在开发环境下编译模板(重要)
     

总结:

  • with语法
  • 模板到render函数,再到vnode ,再到渲染和更新
  • vue组件可以用render代替template
     

5、组件渲染/更新过程

一个组件渲染到页面,修改data触发更新(数据驱动视图) (一个组件渲染到页面,修改Data触发更新(数据驱动视图)其背后原理是什么。

执行render函数会触发gettter:

<template>
  <p>{{message}}</p>
</template>

<script>
export default {
 data(){
   return{
     message:'hello',//会触发 get
     city:'北京', //不会触发 get,因为模板没有用到,即和视图没关系
   }
 }
}
</script>

       

异步渲染:

先总结一下原理,在Vue中异步渲染实际上是在数据每次变化时,将其所要引起页面变化的部分都放到一个异步API的回调函数里,直到同步代码执行完之后,异步回调开始执行,最终将同步代码里所有的需要渲染变化的部分合并起来,最终执行一次渲染操作。可以参考之前的异步

6、前端路由原理

vue-router的路由模式:hash和H5 history

                                        hash 的特点

  • hash变化会触发网页跳转,即浏览器的前进、后退
  • hash变化不会刷新页面,SPA必需的特点
  • hash永远不会提交到server端(前端自生自灭)

                        H5 history

  • 用url规范的路由,但跳转时不刷新页面
  • history.pushState
  • window.onpopstate

                        正常页面浏览:
https://github.com/xxx刷新页面
https:l/github.com/xxx/yyy刷新页面
https://github.com/xxx/yyy/zzz刷新页面

                        改造成H5 history模式
https://github.com/xxx刷新页面
https://github.com/xxx/yyy前端跳转,不刷新页面
https://github.com/xxx/yyy/zzz前端跳转,不刷新页面

                                        总结 
hash - window.onhashchange 
H5 history - history.pushState和window.onpopstate
H5 history需要后端支持

                                        两者选择
to B的系统推荐用hash ,简单易用,对url规范不敏感
to C的系统,可以考虑选择H5 history ,但需要服务端支持
能选择简单的,就别用复杂的,要考虑成本和收益

7、面试题汇总

这几个章节重要的面试题有:

  1. 何时用v-if,何时用v-show
  2. key的重要性
  3. 父子生命周期
  4. v-model的实现原理
  5. 描述组件渲染和更新的过程   
  6. mvvm模型
  7. vnode模拟一个DOM结构
  8. 响应式原理
  9. 何时用异步组件
  10. 何时用keep-alive

补充:

1、computed有何特点?

缓存,data不变不会重新计算

提高性能

2、ajax请求应该放在哪个生命周期

mounted

JS是单线程的,ajax异步获取数据

放在mounted之前没有用,只会让逻辑更加混乱

3、如何将组件所有props传递给子组件?
◆$props
◆<User v-bind= "$props" />
◆细节知识点,优先级不高

4、何时需要使用beforeDestory
◆解绑自定义事件event.$off
◆清除定时器
◆解绑自定义的DOM事件,如window scroll等

5、Vuex中action和mutation有何区别
◆action 中处理异步, mutation不可以
◆mutation 做原子操作
◆action 可以整合多个mutation
◆解绑自定义的DOM事件,如window scroll等

6、Vue如何监听数组变化
◆Object.defineProperty 不能监听数组变化
◆重新定义原型,重写push pop等方法,实现监听
◆Proxy 可以原生支持监听数组变化

7、Vue常见性能优化方式
◆合理使用v-show和v-if
◆合理使用computed
◆v-for 时加key ,以及避免和v-if同时使用

◆自定义事件、DOM事件及时销毁
◆合理使用异步组件
◆合理使用keep-alive

◆webpack 层面的优化(后面会讲)
◆前端通 用的性能优化,如图片懒加载
◆使用SSR

十一、Vue3

1、Vue3比Vue2有什么优势?

  • 性能更好
  • 更好的代码组织
  • 体积更小
  • 更好的逻辑抽离
  • 更好的ts支持
  • 更多新功能

2、Compositions API(组合式API) 和 Options API(选项API)

Compositions API 和 Options API 都是 Vue.js 提供的 API,但它们有着不同的用途和工作方式。

Options API 是 Vue.js 最初引入的 API,它基于组件选项来描述组件的行为。通过定义不同的选项,开发者可以指定组件的数据、计算属性、方法、生命周期钩子等等。Options API 通常被用于开发简单的组件,因为它易于理解和使用。

Compositions API 是 Vue.js 3.0 新引入的 API,它基于函数和逻辑组合来描述组件的行为。开发者可以将相关的逻辑组合在一起,形成一个可重用的逻辑单元,然后在组件中使用这些逻辑单元。Compositions API 通常被用于开发复杂的组件,因为它可以更好地组织代码和重用逻辑。

总之,Options API 和 Compositions API 都是用于描述组件行为的 API,但是 Compositions API 更适合于开发复杂的组件,而 Options API 更适合于开发简单的组件。

代码说明:

当使用 Options API 定义一个简单的组件时,通常需要在组件选项中指定数据、计算属性、方法、生命周期钩子等:

<template>
    <div>
      <p>Count: {{ count }}</p>
      <p>Double count: {{ doubleCount }}</p>
      <button @click="increment">Increment</button>
    </div>
   </template>
Vue.component('my-component', {
  data() {
    return {
      count: 0
    }
  },
  computed: {
    doubleCount() {
      return this.count * 2
    }
  },
  methods: {
    increment() {
      this.count++
    }
  },
  mounted() {
    console.log('Component mounted')
  }
})

当使用 Compositions API 定义相同的组件时,需要使用 setup 函数,将数据、计算属性、方法等相关逻辑组合在一起:

  <template>
    <div>
      <p>Count: {{ count }}</p>
      <p>Double count: {{ doubleCount }}</p>
      <button @click="increment">Increment</button>
    </div>
</template>

Vue.component('my-component', {
  setup() {
    const count = Vue.ref(0)
    const doubleCount = Vue.computed(() => count.value * 2)
    
    function increment() {
      count.value++
    }
    
    Vue.onMounted(() => {
      console.log('Component mounted')
    })
    
    return {
      count,
      doubleCount,
      increment
    }
  }

可以看到,Compositions API 使用 setup 函数来组织相关逻辑,将数据、计算属性、方法等作为函数内的变量或函数来定义。在 setup 函数中,可以使用 Vue 提供的辅助函数如 refcomputedonMounted 等来管理状态和逻辑。最后,需要从 setup 函数中返回一个对象,这个对象中包含组件的数据和方法,以及其他需要在模板中使用的变量。

3、生命周期

在Options API中生命周期只改变了两个:

  // beforeDestroy 改名
    beforeUnmount() {
        console.log('beforeUnmount')
    },
    // destroyed 改名
    unmounted() {
        console.log('unmounted')
    }

在组件API中写法如下:

通过组合式api的形式去使用生命周期钩子:setup()相当于beforeCreate()和created()
<script>
import { onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue'

export default {
    name: 'LifeCycles',

    props: {
        msg: String
    },

    // 等于 beforeCreate 和 created
    setup() {
        console.log('setup')

        onBeforeMount(() => {
            console.log('onBeforeMount')
        })
        onMounted(() => {
            console.log('onMounted')
        })
        onBeforeUpdate(() => {
            console.log('onBeforeUpdate')
        })
        onUpdated(() => {
            console.log('onUpdated')
        })
        onBeforeUnmount(() => {
            console.log('onBeforeUnmount')
        })
        onUnmounted(() => {
            console.log('onUnmounted')
        })
    },
}
</script>

4、ref、toRef和toRefs

这个是非常重要的一个知识点,在学这之前先了解ref和reactive的区别和用法:

在 Vue 3 中,"ref" 和 "reactive" 都是用来定义响应式变量的方式。二者之间的主要区别在于,"ref" 创建一个对值的响应式引用,而"reactive"创建一个具有多个属性的响应式对象。以下是每个方式的一个示例代码:

import { ref, reactive } from 'vue';

// 通过 ref 创建响应式引用
const count = ref(0); // 创建一个响应式引用指向值 0

// 更新引用的值
count.value++;

console.log(count.value); // 输出引用的新值

// 通过 reactive 创建响应式对象
const state = reactive({ count: 0 }); // 创建一个具有 count 属性的响应式对象

// 更新 count 属性的值
state.count++;

console.log(state.count); // 输出 count 属性的新值

在第一个示例中,我们使用 "ref" 函数创建一个对值 0 的响应式引用。我们使用 ".value" 属性更新引用的值,并将新值输出到控制台。

在第二个示例中,我们使用 "reactive" 函数创建一个响应式对象。对象具有一个名为 "count" 的属性。我们使用 ".count" 属性更新 "count" 属性的值,并将新值输出到控制台。

当我们需要定义多个相互关联的属性时,使用具有多个属性的响应式对象可能比使用多个引用更方便。

区分了ref和reactive之后,来了解ref、toRef和toRefs的区别:

在Vue 3中,reftoReftoRefs 是用来处理响应式数据的函数。

  • ref 函数用于生成值类型的响应数据,返回一个包含响应式数据的 Ref 对象。可以用于模板和reactive,通过.value修改值
  • toRef 针对一个响应式对象(reactive封装)的prop,返回一个 Ref 对象,它与源对象的属性相关联。两者保持引用关系。
  • toRefs 函数就是将响应式对象( reactive封装)转换为普通对象,对象的每个prop都是对应的ref,返回一个响应式引用对象的副本。两者保持引用关系。

可以将 ref 看作是将基本数据类型(值类型)转换为响应式数据类型的工具,toRef 用于创建一个指向源对象属性的引用,而 toRefs 将源对象中的所有属性转换为响应式引用。

需要注意的是,使用 toRefs 转换的响应式引用对象中的每个属性是一个独立的 Ref 对象,如果需要修改源对象中的属性,则需要使用 value 属性来访问和修改 Ref 对象的值。而使用 toRef 创建的引用对象是与源对象中的属性相关联的,因此可以直接修改源对象中的属性值。

toRef:

<template>
	<div>{{ state.age }} --- {{ ageRef }}</div>
</template>

<script>
import { toRef, reactive } from 'vue'
export default {
	setup() {
		const state = reactive({
			name: 'JL',
			age: 18
		})
		const ageRef = toRef(state, 'age')
		setTimeout(() => {
			state.age = 20
		}, 1000)
		
		setTimeout(() => {
			ageRef.value = 21
		}, 2000)
		
		return {
			state,
			ageRef
		}
	},
}
</script>

上面的代码中,使用toRef将state的age属性变成一个响应式变量,然后在1秒后将state的age值变为20,此时ageRef也会变成20;在2秒后将ageRef的值变为21,此时state的age值也会变成21,说明了两者保持相互引用关系

toRef针对的是响应式,针对的不是普通对象,如果用于非响应式,产出的结果不具有响应式

 toRefs:

<template>
	<div>{{ name }}---{{ age }}</div>
</template>

<script>
import { reactive, toRefs } from 'vue'
export default {
	setup() {
		const state = reactive({
			name: 'JL',
			age: 18
		})

		const stateRefs = toRefs(state)

		setTimeout(() => {
			state.age = 20
		}, 1000)

		setTimeout(() => {
			stateRefs.age.value = 21
		}, 2000)

		return stateRefs
	},
}
</script>

上面的代码中,使用toRefs将state转变成一个普通对象,这时候就可以直接返回stateRefs,这时候在template就可以直接调用name和age。然后在1秒后将state的age值变为20,此时页面中的age也会变成20;在2秒后将stateRefs中的name的值变为21,此时页面中的age值也会变成21,说明了两者保持相互引用关系

toRefs将响应式对象变成普通对象后,每一个属性都具有响应式ref,此时需要使用 .value才能获取其值

最佳使用方式:

◆用reactive做对象的响应式,用ref做值类型响应式
◆setup 中返回toRefs(state) , 或者toRef(state,'xxx’' )
◆ref 的变量命名都用xxxRef

◆合成函数返回响应式对象时,使用toRefs

5、进阶深入理解

5.1为何需要ref?

  • 返回值类型,会丢失响应式:

  在Vue中,当你从一个函数或方法中返回一个非响应式的值(如字符串、数字、布尔值 、等),那么这个值就会失去响应式,因为Vue只能追踪响应式对象的属性和方法的变化。

在Vue 3中,使用ref 和reactive 来创建响应式数据。当你使用ref 创建一个响应式数据时,它实际上是—个包装器,将一个普通值(例如数字、字符串等)转换为响应式数据。当你从ref 中获取这个值时,Vue会自动追踪这个响应式数据的使用,并在数据发生变化时更新视图。

  • 如在setup、computed、 合成函数,都有可能返回值

ref是一一个对象(不丢失响应式) , value存储值
通过.value属性的get和set实现响应式
用于模板、reactive 时,不需要.value ,其他情况都需要

代码示例:

<template>
<button @click="changenum">点击1s后改变数字</button>
  <h1>{{sum}}</h1>
</template>
<script>
import {ref,reactive} from "vue";
export default {
  name: 'App',
 setup(){
    let sum=ref(5)
   function changenum(){
      setTimeout(()=>{
        sum.value=8
      },1000)
   }
   return{
      sum,
      changenum
   }
 }
}
</script>

为什么reactive不行呢?因为ref修饰返回的是一个ref对象,但是使用reactive后在通过.value修改对象,此时改的是值类型,已经丢失响应式了如:

let num=reactive({
  sum:5})
num.sum.value=8

被reactive定义的要想修改数据得引用toReftoRefs 从而将值类型变为ref对象类型

  •  Vue如不定义ref ,用户将自造ref ,反而混乱

5.2为何需要 .value

  • ref是一个对象(不丢失响应式) , value存储值
  • 通过.value属性的get和set实现响应式
  • 用于模板、reactive 时,不需要.value ,其他情况都需要

5.3为何需要 toRef toRefs

  • 初衷:不丢失响应式的情况下,把对象数据分解/扩散
  • 前提:针对的是响应式对象( reactive封装的)非普通对象
  • 注意:不创造响应式,而是延续响应式

6、Vue3升级了哪些重要的功能?

6.1createApp

在Vue 3中,createApp 是用于创建一个Vue应用程序,实例的工厂函数。它返回-个应用程序对象,该对象包含了Vue应用程序的主要配置和功能。

在Vue2中

const app=new Vue({/*选项*/})
Vue.use(/*...*/)
Vue.mixin(/*...*/)

在Vue3中

const app=Vue.createApp({/*选项*/})
app.use(/*...*/)
app.mixin(/*...*/)


6.2多事件

template模板
<button @click="one($event), two($event)">
  Submit
</button>

methods: {
  one(event) {
    // first handler logic...
  },
  two(event) {
    // second handler logic...
  }
}


6.3emits 属性

父组件
<HelloWorld :msg="msg" @sayHello="sayHello">

子组件
export default {
  name: 'HelloWorld',
 props:{
    msg:String
 },
 emits:['sayHello'],
  setup(props,{emit}){
    emit('sayHello','bbb')
  }
}


6.4Fraginent

在vue2多文件组件得用div包裹,在Vue3中不需要


6.5生命周期
6.6移除.sync

vue 修饰符sync的功能是:当一个子组件改变了一个 prop 的值时,这个变化也会同步到父组件中所绑定。如果我们不用.sync,我们想做上面的那个弹窗功能,我们也可以props传初始值,然后事件监听,实现起来也不算复杂。这里用sync实现,只是给大家提供一个思路,让其明白他的实现原理,可能有其它复杂的功能适用sync。
例如:

<demo :money.sync=''money'/>


 :money.sync  :第一,父组件给子组件传递props money
第二:给当前子组件绑定了一个自定义事件,而且事件名称为update:money
子组件props接收 还通过$emit(update:money)给父亲传数据
实现了父子之间 !!相互!! 传递信息

在Vue3中移除了

6.7异步组件的写法

vue2写法

new Vue({
//...
    components:{
    'my-component':()=>{'./test.vue'}
})

Vue3则是使用了defineAsyncComponent

import{createApp, defineAsyncComponent}
createApp({
components:{
AsyncComponent:  defineAsyncComponent(() =>
import('./components/AsyncComponent,vue' )
}
}
})


6.8Suspense

<Suspense>
    <template>
        <Test1/> <!-- 是一个异步组件-->
    </template>
<!-- #fallback 就是一个具名插槽。
即Suspense 组件内部,有两个slot
其中一个具名为fallback -->
<template #fallback>
Loading...
</template>
</Suspense


6.9移除filter

<!--以下filter 在vue3中不可用了! ! ! -->
<!--在双花括号中-->
{{ message | capitalize }}
<!--在、v-bind 中-->
<div v-bind:id=" rawId| formatId"></ div>


6.10Composition API

  • reactive
  • watch 和watchEffect
  • ref相关
  • setup .
  • readonly
  • 生命周期钩子函数


6.11Teleport

data中设置moda LOpen: false
<button @click="modalopen = true">
    Open full screen modal! (With teleport!)
</ button>
<teleport to=" body">
    <div v- if="moda LOpen" class="modal">
        <div>
           telePort弹窗(父元素是body)
        <button @click="modalopen = fa Lse">Close</ button>    
        </div> 
   </div>
</teleport>

7、Composition API实现逻辑复用

◆抽离逻辑代码到一个函数
◆函数命名约定为 useXxxx格式( React Hooks也是)
◆在setup中弓|用useXxx函数

 具体:“”

Composition API 是 Vue 3 中引入的一种新的 API 设计模式,旨在解决 Vue 2 中代码复用和逻辑复杂度增加的问题。它允许您将组件逻辑分解为更小、更易于维护的功能块,同时提供了更直观的语法和更好的类型推断支持。

在 Composition API 中,逻辑复用可以通过以下方式实现:

  1. 函数提取:将多个组件中共用的逻辑抽取出来,封装成一个单独的函数,以便在多个组件中重复使用。这样可以避免代码重复,提高代码的可维护性和可复用性。

  2. 自定义 Hook:自定义 Hook 是一种将多个逻辑抽象到单个可重用函数中的方法。这些自定义 Hook 可以在多个组件之间共享,使得逻辑复用更加简单。

  3. Provide/Inject:Provide/Inject API 允许在祖先组件中提供数据,并在子孙组件中注入它们。这可以帮助您在多个组件之间共享状态,从而避免 prop 层层传递的问题。

  4. Ref 和 Reactive:在 Composition API 中,Ref 和 Reactive 是管理组件状态的主要方式。您可以将多个组件之间共享的状态包装在 Ref 或 Reactive 中,并在需要时从一个组件传递到另一个组件。

总之,Composition API 是一种非常强大的工具,可以帮助您更好地管理代码复杂度和实现逻辑复用。通过使用这些技术,您可以在不牺牲可读性和可维护性的情况下实现高效的代码复用。

8、Proxy实现响应式
 

//源数据
			let person = {
				name:'张三',
				age:18
			}
const p = new Proxy(person,{
				//有人读取p的某个属性时调用
				get(target,propName){
					console.log(`有人读取了p身上的${propName}属性`)
					return Reflect.get(target,propName)
				},
				//有人修改p的某个属性、或给p追加某个属性时调用
				set(target,propName,value){
					console.log(`有人修改了p身上的${propName}属性,我要去更新界面了!`)
					Reflect.set(target,propName,value)
				},
				//有人删除p的某个属性时调用
				deleteProperty(target,propName){
					console.log(`有人删除了p身上的${propName}属性,我要去更新界面了!`)
					return Reflect.deleteProperty(target,propName)
				}
			})

在上面的代码中Reflect作用:

  • 和Proxy 能力一-对应
  • 规范化、标准化、函数式
  • 替代掉Object .上的工具函数

具体点来说

在Vue3中,Reflect是一个内置的全局对象,它提供了一些静态方法,用于在对象上进行反射操作,例如获取、设置、删除属性等。

在Vue3中,Reflect主要用于代替一些Object方法,如Object.definePropertyObject.getOwnPropertyDescriptor等。具体来说,Reflect提供了以下方法:

  1. Reflect.get(target, propertyKey[, receiver]): 返回对象的某个属性值。

  2. Reflect.set(target, propertyKey, value[, receiver]): 将对象的某个属性设置为指定的值。

  3. Reflect.has(target, propertyKey): 返回一个布尔值,表示对象是否具有指定的属性。

  4. Reflect.deleteProperty(target, propertyKey): 删除对象的某个属性。

  5. Reflect.defineProperty(target, propertyKey, attributes): 定义对象的一个新属性。

  6. Reflect.getOwnPropertyDescriptor(target, propertyKey): 返回指定对象上一个自有属性对应的属性描述符。

  7. Reflect.isExtensible(target): 判断对象是否可扩展。

  8. Reflect.preventExtensions(target): 让一个对象变为不可扩展。

使用Reflect进行对象操作时,如果操作成功,返回true,否则返回false。另外,Reflect的方法可以使用Proxy进行拦截和代理,从而实现更灵活的对象操作。

Proxy实现响应式

  • 深度监听, 新能更好

在Vue2中实现深度监听是Observe()一次性递归,Vue3是需要的在递归,所以对性能有了优化。

在Vue 3中,使用Proxy实现深度监听的过程如下:

  1. 创建一个Proxy对象,该对象包装了一个原始对象(例如,Vue实例中的data对象)和一个处理器对象。
  2. 处理器对象中有一个名为“get”的方法,该方法在访问被包装对象的属性时被调用。当Vue组件模板中访问data中的属性时,Proxy对象会拦截这些访问,并将其转发给原始对象。
  3. 处理器对象中还有一个名为“set”的方法,该方法在修改被包装对象的属性时被调用。当Vue组件模板中修改data中的属性时,Proxy对象会拦截这些修改,并将其转发给原始对象。
  4. 如果被包装对象的属性值是一个对象,那么在访问该属性值的属性时,Proxy对象会递归地创建新的Proxy对象,从而实现对该对象的深度监听。

因此,通过使用Proxy对象来包装Vue组件实例中的data对象,Vue 3能够深度监听该对象的所有属性变化,从而实现了响应式数据的特性。

  • 可监听新增/删除属性

vue2中是没有这些功能的

  • 可监听数组变化

总结

  • ◆Proxy 能规避Object.defineProperty的问题
  • ◆Proxy 无法兼容所有浏览器,无法polyfill

9、对vue3的一些补充

9.1setup中如何获取组件实例

  • 在setup和其他Composition API中没有this

  • 可通过getCurrentInstance获取当前实例

  • 若使用Options API可照常使用this

9.2watch和watchEffect的区别


◆两者都可监听data属性变化
◆watch 需要明确监听哪个属性
◆watchEffect 会根据其中的属性,自动监听其变化

setup() {
    let sum = ref(0)
    let msg = ref('你好啊')
    let person=reactive({ //ref修饰对象内部会自动通过reactive转换为代理对象
      name:'张三',
      age:18,
      job:{
        j1:{
          salary:20
        }
      }
    })
    watchEffect(()=>{//watchEffect所指定的回调中用到的数据只要发生变化,则直接重新执行回调
      const x1 = sum.value
      const x2 = person.job.j1.salary
      console.log('watchEffect所指定的回调执行了')
    })

9.3Vue3为何比Vue2快


◆Proxy 响应式
◆cacheHandler

缓存事件


●PatchFlag

◆编译模板时,动态节点做标记
◆标记,分为不同的类型,如TEXT PROPS
◆diff 算法时,可以区分静态节点,以及不同类型的动态节点


◆SSR 优化
◆hoistStatic

◆将静态节点的定义,提升到父作用域,缓存起来
◆多个相邻的静态节点,会被合并起来
◆典型的拿空间换时间的优化策略


◆tree-shaking

9.4Vite

Vite是什么

  • 一个前端打包工具, Vue作者发起的项目
  • 借助Vue的影响力,发展较快,和webpack竞争
  • 优势:开发环境下无需打包,启动快

Vite为何启动快?

  • 开发环境使用ES6 Module,无需打包一 非常快
  • 生产环境使用rollup , 并不会快很多

9.5Vue3 script setup

  • Vue3引入了composition API
  • composition API最重要的是setup函数
  • <script> 只有一个setup函数太“孤单”, 如何简化一下 ?

Vue3 的 script setup 是一种新的语法糖,它可以使得 Vue 单文件组件的代码更加简洁、易读、易维护。

在 Vue2 中,我们需要在 <script> 标签中定义 datamethodscomputedwatch 等选项,并且需要使用 this 来访问组件实例。这样做的问题在于,如果组件较为复杂,那么这些选项会变得很冗长,而且由于使用了 this,代码也不够清晰。

在 Vue3 中,我们可以使用 script setup 来简化组件定义。script setup 的基本语法如下:

<template>
  <!-- 组件的模板 -->
</template>

<script setup>
  // 在这里定义组件的选项
</script>

使用 script setup,我们可以在组件中以更加简洁的方式定义 datamethodscomputedwatch 等选项,而且不需要使用 this 来访问组件实例。例如:

<template>
  <div>{{ message }}</div>
</template>

<script setup>
  let message = 'Hello, Vue3!'
</script>

在这个例子中,我们使用了 let 关键字来定义一个名为 message 的变量,并将其绑定到模板中。

除了 letscript setup 还支持其他关键字,例如 constrefreactivecomputed 等。使用这些关键字,我们可以更加方便地定义组件的选项。

总之,script setup 是一个非常有用的新特性,它可以让我们以更加简洁、易读、易维护的方式编写 Vue 组件。

Vue3 script setup -总结

  • 基本使用,<script>写在<template>前面
  • 定义属性defineProps , 定义事件defineEmits
  • defineExpose 暴露数据给父组件

 9.6 ref和reactive的区别

9.7 vue2和vue3的区别

vue3组件中的模板结构可以没有根标签 
vue3向下兼容  vue2和vue3尽量不要混用
vue2实现响应式是object.prototype配合getter setter实现数据劫持
vue3想实现响应式得需要ref函数 和getter setter只不过给隐藏了起来,修改时找不到会找原型上的getter setter方法进行数据劫持修改数据
vue3处理嵌套数据是用的是封装了proxy的reactive函数,通过代理对象操作源对象内部都是响应式的

9.8 setup的注意点

9.9vue3响应式原理

9.10 watcheffect函数

9.11 vue3 watch函数的注意点

9.12Class组件和函数组件有什么区别?

Class组件和函数组件区别:

  • Class组件: 是传统的Vue组件形式,使用class关键字定义组件,包含datamethodscomputed等选项。

  • 函数组件: 是Vue 2.x 中引入的一种更简洁的组件形式,通过函数定义组件,组件的状态和逻辑都在函数内部,适用于简单的展示型组件。比如render

9.13Hooks是什么,

在Vue中,"Hooks" 这个术语通常与Vue 3的Composition API关联。Vue 3引入了Composition API来更好地组织组件的逻辑,而这个API的一部分被称为 "Hooks"。

在Vue 3的Composition API中,Hooks是一组函数,比如 refreactivecomputedwatch 等,用于处理组件的状态、副作用等。通过使用这些Hooks,可以更灵活地组织和共享组件逻辑。

9.14 你能列举几个常用的Vue 3中的Composition API吗?

在Vue 3中,引入了Composition API,它是一组函数式的API,用于更灵活地组织组件的逻辑。你提到的 refsetupreactive 是Composition API的一部分:

  • ref: 用于创建响应式数据,类似于Vue 2.x的data。示例:const count = ref(0);

  • setup: 用于组件的设置阶段,可以进行响应式数据的创建和组件逻辑的初始化。示例:setup() { return { count: ref(0) }; }

  • reactive: 用于创建响应式对象,类似于Vue 2.x的data,但在Vue 3中推荐使用。示例:const state = reactive({ count: 0 });

9.15 请解释Vue中的状态(state)和属性(props)的概念,以及它们之间的区别。

状态(State)是指组件中的数据,它可以是响应式的,即当状态发生变化时,组件会自动更新。在Vue中,我们可以使用data选项来定义组件的状态。状态通常用于存储组件内部的数据,例如表单输入的值、计数器的当前值等。

属性(Props)是指从父组件传递给子组件的数据。父组件可以通过在子组件上使用属性绑定的方式将数据传递给子组件。子组件可以通过props选项来声明接收属性,并在组件内部使用这些属性。属性是单向数据流的,即父组件可以修改传递给子组件的属性,但子组件不能直接修改属性的值。

在组件中的应用和区别:

  • 状态通常用于存储组件内部的数据,并且可以在组件内部进行修改和更新。状态的变化会触发组件的重新渲染。
  • 属性用于父组件向子组件传递数据,子组件可以使用这些属性进行渲染和展示。属性的值通常是只读的,子组件不能直接修改属性的值。

示例代码如下:

<template>
  <div>
    <p>状态:{{ count }}</p>
    <p>属性:{{ message }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      count: 0 // 状态
    }
  },
  props: {
    message: String // 属性
  }
}
</script>

9.16什么是Vue的路由(Vue Router)?它的主要功能是什么?

  1. 路由的定义: 正确,路由是用于控制页面切换和视图变化的机制。

  2. 通过跳转路由引起页面变化: 正确,通过切换路由可以导致应用程序显示不同的视图或页面。

在Vue中,Vue Router 是官方的路由管理器,提供了一些重要的功能:

  • 路由映射: 将不同的路由映射到对应的组件,使得页面能够根据路由的变化加载不同的组件。

  • 嵌套路由: 允许在组件内部定义子路由,形成嵌套关系。

  • 导航守卫: 提供了一些导航守卫,允许在路由变化前后执行一些逻辑。

  • 路由参数: 允许定义动态路由,通过参数来传递信息。

  • 状态管理: 与 Vuex 等状态管理工具集成,可以更好地管理应用的状态。

Vue Router的使用可以使得单页面应用(SPA)更容易管理,实现页面的切换和导航逻辑。

9.17Vue中的Vuex是什么,以及它的主要作用是什么?

  1. Vuex是状态机: 正确,Vuex 是一个状态管理模式,用于管理应用中的状态。

  2. 核心概念: 正确,你提到了 statemutationactiongetter,这些是Vuex的核心概念。

  3. 组件通信和数据管理: 正确,Vuex不仅可以用于全局状态的管理,还能方便组件之间的通信。通过 state 存储数据,通过 mutation 修改数据,通过 action 处理异步逻辑,通过 getter 获取经过计算的状态。

  4. 处理后端数据: 正确,Vuex通常用于管理从后端获取的数据,以便在应用的不同组件中获取并渲染这些数据。

9.18Vue中的插槽(Slot)是什么,以及它的主要作用是什么?

插槽:简单理解就是组件内部留一个或多个的插槽位置,可供组件传对应的模板代码进去

在 Vue 中,插槽分为三种:默认插槽 具名插槽 自定义(作用域)插槽

插槽使得组件的模板更加灵活,可以动态地传递和渲染内容。

9.19什么是Vue Mixin?它有什么作用,以及在使用时需要注意什么?

mixins是对vue组件的一种扩展,将一些公用的常用数据或者方法,构建一个可被混入的数据结构,被不同的vue组件进行合并,就可以在不同的vue组件中使用相同的方法或者基础数据

9.20什么是Vue Mixin?它有什么作用,以及在使用时需要注意什么?

mixins是对vue组件的一种扩展,将一些公用的常用数据或者方法,构建一个可被混入的数据结构,被不同的vue组件进行合并,就可以在不同的vue组件中使用相同的方法或者基础数据

需要注意的事项:

  1. 命名冲突: 当多个混入对象中包含相同的选项时,Vue 的合并策略可能会引起命名冲突。这时可以使用 beforeCreate 钩子等方式解决。

  2. 全局影响: 混入的内容会影响到引入它的每个组件,因此在使用时需要小心不要引入过多的全局影响。

  3. 适度使用: 混入是一个强大的工具,但过度使用它可能导致组件难以理解和维护。只在确实有需要共享的逻辑时使用混入。

9.21请解释Vue中的过渡效果(Transition)和动画效果(Animation)的区别,以及它们的主要应用场景。

在Vue中,过渡效果(Transition)和动画效果(Animation)是用于处理页面元素进入或离开的两种不同方式。

  1. 过渡效果(Transition):

    • 定义: 过渡效果是Vue提供的一种简单的方式,用于在元素进入或离开 DOM 的时候,添加或删除类名,实现一些简单的过渡效果。
    • 应用场景: 主要应用于处理元素的显示和隐藏、简单的过渡效果,如淡入淡出、滑动等。 
  2. 动画效果(Animation):

  3. 定义: 动画效果是更为复杂和灵活的方式,通过使用<transition>组件的JavaScript钩子函数或直接使用CSS @keyframes 来定义复杂的动画效果。
  4. 应用场景: 适用于需要更高度自定义和复杂动画效果的场景,例如旋转、缩放、路径运动等。

总的来说,过渡效果更适合处理简单的进入和离开过渡,而动画效果则更适用于复杂的、需要更多控制的动画场景。

9.22 ES6新增语法

ES6(ECMAScript 2015)是JavaScript的一个重要更新版本,引入了许多新的语法和功能。以下是ES6引入的一些主要语法特性:

  1. let 和 const:

    • let 声明变量,具有块级作用域。
    • const 声明常量,一旦赋值就不能再改变。
     

    javascriptCopy code

    let x = 10; const PI = 3.1415;

  2. 箭头函数:

    • 提供了一种更简洁的函数声明语法。
     

    javascriptCopy code

    // 传统函数 function add(a, b) { return a + b; } // 箭头函数 const add = (a, b) => a + b;

  3. 模板字符串:

    • 使用反引号(`)创建多行字符串,并支持插值。
     

    javascriptCopy code

    const name = "World"; const greeting = `Hello, ${name}!`;

  4. 解构赋值:

    • 允许按模式匹配从数组或对象中提取数据。
     

    javascriptCopy code

    // 数组解构赋值 const [a, b] = [1, 2]; // 对象解构赋值 const { x, y } = { x: 10, y: 20 };

  5. 默认参数值:

    • 允许在函数声明中为参数指定默认值。
     

    javascriptCopy code

    function greet(name = "Guest") { console.log(`Hello, ${name}!`); }

  6. 类:

    • 引入了类的概念,使得面向对象编程更加方便。
     

    javascriptCopy code

    class Person { constructor(name) { this.name = name; } sayHello() { console.log(`Hello, ${this.name}!`); } } const person = new Person("Alice"); person.sayHello();

  7. 模块化:

    • 引入了 importexport,使得模块化编程更加容易。
     

    javascriptCopy code

    // 导出模块 // math.js export const add = (a, b) => a + b; // 导入模块 // main.js import { add } from './math';

  8. Map 和 Set:

    • 引入了新的数据结构 MapSet,提供了更灵活的键值对存储和集合操作。
     

    javascriptCopy code

    const myMap = new Map(); myMap.set("key", "value"); const mySet = new Set(); mySet.add(1); mySet.add(2);

  9. Promise:

    • 提供了更好的处理异步操作的方式。
     

    javascriptCopy code

    const fetchData = () => { return new Promise((resolve, reject) => { // 异步操作 if (/* 操作成功 */) { resolve(data); } else { reject(error); } }); };

这只是 ES6 引入的一些语法特性的概览,实际上还有其他很多有用的特性。随着 ECMAScript 规范的不断演进,后续版本(如 ES7、ES8 等)也引入了更多的新功能和语法。

9.23 文件上传git操作

1、先输入git remote rm origin 删除关联的origin的远程库
2、关联自己的仓库 git remote add origin https://gitee.com/xxxxxx.git
3、最后git push origin master,这样就推送到自己的仓库了。
 

十二、uniapp相关面试题

1、什么是 uniapp?它有哪些特点和优势?

uniapp 是基于 Vue 框架的跨平台应用开发框架。

  • 一次开发多端部署: uniapp 支持一次编写代码,同时生成到多个平台,包括但不限于微信小程序、H5、App等,方便开发者在多个平台上进行应用发布。

  • 减少开发时间和成本: uniapp 提供了一系列的组件和开发工具,可以帮助开发者更高效地完成应用开发,减少开发的时间和成本。

  • 性能体验: uniapp 针对不同平台进行了性能优化,提供了类似原生应用的体验。

  • 开放生态系统: uniapp 具有开放的生态系统,支持插件市场,开发者可以通过使用插件来扩展应用的功能。

2、请解释 uniapp 中的生命周期钩子函数有哪些,以及它们在组件生命周期中的调用顺序。

 1、应用级(App)生命周期钩子函数——App.vue —— 类似于小程序

              onLaunch:应用启动了,每次运行只能执行一次

              onShow:应用再次显示出来,每次显示出来都会调用

              onHide:应用再次隐藏起来,每次隐藏出来都会调用,例如:手机切换到切换到其它应用

              onPageNotFound:当用户访问了一个不存在的页面时调用

  2、页面级(Page)生命周期钩子函数——page.vue —— 类似于小程序

              onLoad(data):页面加载完成,每个页面仅执行一次

              onShow:页面开始显示,可以多次执行

              onReady:页面已就绪,每个页面仅执行一次

              onHide:页面隐藏了,可以多次执行

              onUnload:页面卸载/销毁了,每个页面仅执行一次

  3、组件级(Component)生命周期钩子函数——component.vue —— 类似于Vue.js2.0

              beforeCreate:组件创建之前

              created:组件创建完成

              beforeMount:组件挂载到节点树之前

              mounted:组件挂载到节点树

              beforeUpdate:组件状态更新之前

              updated:组件状态更新完成

              beforeDestroy:组件从节点树上销毁之前

              destroyed:组件从节点树上销毁了

3、uniapp 中的组件通信方式有哪些?请简要说明它们的特点和适用场景。

  1. props

    • 特点: 用于父组件向子组件传递数据。父组件通过属性的形式将数据传递给子组件。
    • 适用场景: 主要用于父子组件之间的单向数据传递。
  2. 自定义事件($emit$on):

    • 特点: 通过 $emit 触发自定义事件,通过 $on 监听自定义事件,实现子组件向父组件传递数据。
    • 适用场景: 适用于需要子组件主动向父组件传递信息的场景。
  3. Vuex(状态管理):

    • 特点: Vuex 是一个专门为 Vue.js 应用设计的状态管理库,用于管理应用中的共享状态。
    • 适用场景: 适用于大型应用或多个组件之间需要共享状态的场景。可以实现父子通信、子父通信、兄弟组件通信。
  4. 全局事件总线:

    • 特点: 可以通过创建一个空的 Vue 实例作为事件总线,利用其 $emit$on 方法进行组件间的通信。
    • 适用场景: 简单应用中,可以用作组件之间的解耦
    • 通信。

4、在 uniapp 中,什么是微信小程序的 setData 方法,以及它的作用是什么?在什么情况下你会使用 setData

在微信小程序中,setData 是一个用于将数据从逻辑层发送到视图层的方法。它是小程序框架提供的一个重要的数据绑定和更新机制。

作用:

  1. 更新数据: 通过 setData 方法,你可以将逻辑层的数据更新到视图层,触发视图的重新渲染。
  2. 实现双向绑定: 通过在 setData 中传递更新的数据,你可以实现逻辑层和视图层之间的双向数据绑定。

使用情况:

  1. 响应用户操作: 当用户触发了某些操作,导致数据变化时,使用 setData 来更新视图,确保用户界面的及时响应。
  2. 异步更新: 当数据的变化是异步的(例如从后端获取数据后),通过 setData 来更新视图,以确保数据更新后视图能够及时反映最新状态。
// 在 Page 中使用 setData
Page({
  data: {
    message: 'Hello, uniapp!',
  },
  changeMessage() {
    // 通过按钮点击触发数据变化
    this.setData({
      message: 'Updated Message!',
    });
  },
});

在 uniapp 中,尤其是在使用微信小程序时,了解并正确使用 setData 是非常重要的,因为它关系到数据和视图的同步更新,确保应用的正常运行。

5、在 uniapp 中,什么是事件处理,以及常见的事件处理方式有哪些?请简要说明。

在 uniapp 中,事件处理是指在用户与应用程序交互时触发的动作,例如点击按钮、输入文字等。常见的事件处理方式包括:

1.@click 事件:

特点: 用于处理点击事件。

使用方式: 在组件上添加 @click 属性,并指定事件处理函数。

2.@change 事件:

特点: 用于处理输入框等表单元素的值变化事件。

使用方式: 在表单元素上添加 @change 属性,并指定事件处理函数。

3.@submit 事件:

特点: 用于处理表单提交事件。

使用方式: 在表单元素上添加 @submit 属性,并指定事件处理函数。

这些是 uniapp 中常见的事件处理方式,通过这些事件,你可以响应用户的交互,

6、在 uniapp 中,什么是页面路由,以及常见的页面路由方式有哪些?请简要说明。

  1. 通过 navigateTo 进行页面跳转:

    • 特点: 使用 navigateTo 可以在当前页面栈中新开页面,支持返回。
    • 使用方式: 在事件处理函数中调用 uni.navigateTo 方法,并指定目标页面路径。
    <!-- 在 template 中 -->
     <view @click="navigateToPage">Go to Page B</view>
    
    
    
    // 在 methods 中 
    methods: { navigateToPage() { 
    uni.navigateTo({ url: '/pages/pageB/pageB', });
     } }

  2. 通过 redirectTo 进行页面重定向:

    • 特点: 使用 redirectTo 可以关闭当前页面,重定向到目标页面。
    • 使用方式: 在事件处理函数中调用 uni.redirectTo 方法,并指定目标页面路径。
    htmlCopy code
    
    <!-- 在 template 中 -->
     <view @click="redirectToPage">
    Redirect to Page C</view>
    
    
    javascriptCopy code
    
    // 在 methods 中 
    methods: { redirectToPage() { 
    uni.redirectTo({ url: '/pages/pageC/pageC', })
    ; } }

  3. 通过 reLaunch 进行页面重启动:

    • 特点: 使用 reLaunch 可以关闭所有页面,打开到应用内的某个页面。
    • 使用方式: 在事件处理函数中调用 uni.reLaunch 方法,并指定目标页面路径。
    htmlCopy code
    
    <!-- 在 template 中 -->
     <view @click="reLaunchPage">ReLaunch to Page A
    </view>
    
    
    javascriptCopy code
    
    // 在 methods 中 
    methods: { reLaunchPage() {
     uni.reLaunch({ url: '/pages/pageA/pageA', })
    ; } }

  4. 通过 switchTab 进行页面切换(仅适用于 TabBar 页面):

    • 特点: 使用 switchTab 可以切换到 TabBar 页面。
    • 使用方式: 在事件处理函数中调用 uni.switchTab 方法,并指定目标 TabBar 页面路径。
    htmlCopy code
    
    <!-- 在 template 中 --> 
    <view @click="switchTab">Switch to Tab B
    </view>
    
    
    javascriptCopy code
    
    // 在 methods 中
     methods: { switchTab() {
     uni.switchTab({ url: '/pages/tabB/tabB', })
    ; } }

这些是 uniapp 中常见的页面路由方式,通过它们,你可以在不同页面之间进行切换、跳转和重定向。

7、在 uniapp 中,什么是组件,以及组件的基本结构是怎样的?请简要说明。

在 uniapp 中,组件是一种将 UI 结构、样式和行为封装在一起的可复用的构建单元。让我对组件的基本结构进行一些补充:

一个基本的 uniapp 组件通常包括三个主要部分:

  1. HTML 结构(template):

    • 这是组件的界面结构,使用标准的 HTML 标签和组件内部定义的组件标签构成。
  2. JavaScript 逻辑代码(script):

    • script 部分定义了组件的行为,包括数据定义、生命周期钩子函数、方法等。
  3. 样式定义(style):

    • style 部分定义了组件的样式,包括组件内部的样式定义。

这三个部分一起形成了一个完整的 uniapp 组件。这种组件化的方式使得应用程序更易于维护和扩展,也提高了代码的可重用性。

8、在 uniapp 中,什么是状态管理,以及常见的状态管理工具有哪些?请简要说明。

状态管理是一种用于管理全局状态(数据)的方法,以便于不同组件之间的数据管理和共享。

具体到 uniapp 中,常见的状态管理工具主要有:

  1. Vuex:

    • 特点: Vuex 是 Vue.js 官方提供的状态管理库,可以用于管理应用中的共享状态。它包括了一整套的状态管理流程,包括 state、mutations、actions、getters 等。
    • 使用方式: 在 uniapp 中同样可以使用 Vuex 来进行状态管理,通过 uni-app 提供的插件 uni-simple-router 可以更好地与 uniapp 集成。
  2. Pinia:

    • 特点: Pinia 是一个由 Vue.js 团队维护的状态管理库,它采用了更现代的 API 设计,提供了简洁的 API 来进行状态管理。
    • 使用方式: 可以在 uniapp 中使用 Pinia,它适用于 Vue 3.x 版本,通过安装和配置后即可在 uniapp 中使用。

在状态管理的选择上,可以根据项目需求和开发团队的经验来决定使用哪个工具。Vuex 是比较成熟且广泛使用的选项,而 Pinia 则是一个更现代的选择,具有更清晰的 API 设计。

9、什么是前端工程化,有哪些常用的前端构建工具?

前端工程化是一种通过使用工具、设计模式和最佳实践来提高前端开发效率、代码质量和可维护性的方法。常见的前端工程化工具包括构建工具(如Webpack、Parcel)、版本控制(如Git)、包管理工具(如npm、Yarn)、自动化测试(如Jest、Mocha)等。通过这些工具,可以实现自动化构建、模块化开发、代码规范检查、自动化测试等功能。

10、移动端开发:

10.1介绍一下 PWA(Progressive Web App)。

移动端适配方案包括响应式设计、手淘的flexible.js方案、vw/vh单位、媒体查询等。响应式设计通过媒体查询和弹性网格布局,使网站能够在不同设备上呈现出最佳的视觉和交互体验。flexible.js是淘宝团队推出的一个移动端适配库,通过动态设置viewport的缩放比例,实现不同设备的适配。vw/vh单位是CSS3新增的相对单位,表示视窗宽度(Viewport Width)和视窗高度(Viewport Height)的百分比,可以用于简化移动端适配。PWA是一种使用现代Web技术增强Web应用体验的方法,通过Service Worker提供离线缓存、推送通知等功能。

10.2 移动端适配方案有哪些?

以下是使用rem进行移动端适配的基本步骤:

  1. 设置根元素字体大小: 在CSS中,使用rem(相对于根元素字体大小的单位)。通常,将根元素的字体大小设置为设备宽度的一部分是一种常见的做法。例如,设置根元素的字体大小为设备宽度的1/10,即font-size: 10vw;,其中vw表示视口宽度的百分比。

    cssCopy code
    
    html { font-size: 10vw; }

  2. 使用rem进行元素样式定义: 通过使用rem单位,定义页面中的元素大小和间距。例如,如果要将某个元素的宽度设置为设备宽度的一半,可以使用width: 5rem;,其中假设根元素字体大小为设备宽度的1/10。

    cssCopy code
    
    .example { width: 5rem; font-size: 1.2rem; margin-top: 2rem; }

  3. 媒体查询: 在必要时,使用媒体查询来根据屏幕尺寸应用不同的样式。这有助于在小屏幕设备上提供更好的用户体验。

    cssCopy code
    
    @media screen and (max-width: 768px) { /* 在小屏幕上的样式调整 */ html { font-size: 5vw; } .example { width: 100%; } }

  4. 使用flexible布局或网格布局: 结合rem单位使用弹性布局(flexbox)或网格布局(grid),以确保页面中的元素能够灵活地适应不同屏幕尺寸。

     

    cssCopy code

    .container { display: flex; flex-direction: column; align-items: center; }

通过使用rem单位和其他相对单位,你可以实现更灵活的移动端适配,使你的网站或应用在各种移动设备上都能够提供良好的用户体验。在实际开发中,可以根据项目的需求和设计来调整上述步骤中的具体数值。

总结

以上是自己的总结还有很多没总结比如webpack部分的,后续会更新一些面试题例如算法面试题等。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值