2022最全前端面试总结(下)

209 阅读42分钟

7d33d1b1-2624-40a1-b63c-3c21fe37c424.jpg

🎁✨✨✨

script 标签中 defer 和 async 的区别?

\

  • script :会阻碍 HTML 解析,只有下载好并执行完脚本才会继续解析 HTML。
  • async script :解析 HTML 过程中进行脚本的异步下载,下载成功立马执行,有可能会阻断 HTML 的解析。(异步)
  • defer script:完全不会阻碍 HTML 的解析,解析完成之后再按照顺序执行脚本。(异步)
  • URL 主要由 协议主机端口路径查询参数锚点6部分组成

  • 在发起http请求之前,浏览器首先要做去获得我们想访问网页的IP地址,浏览器会发送一个UDP的包给DNS域名解析服务器。
DNS域名解析

  • 我们的浏览器、操作系统、路由器都会缓存一些URL对应的IP地址,统称为DNS高速缓存。这是为了加快DNS解析速度,使得不必每次都到根域名服务器中去查询。

    迭代查询的方式就是,局部的DNS服务器并不会自己向其他服务器进行查询,而是把能够解析该域名的服务器IP地址返回给客户端,客户端会不断的向这些服务器进行查询,直到查询到了位置,迭代的话只会帮你找到相关的服务器,然后说我现在比较忙,你自己去找吧
    DNS还有负载均衡的作用,现在很多网站都有多个服务器,当一个网站访问量过大的时候,如果所有请求都请求在同一个服务器上,可能服务器就会崩掉,这时候就用到了DNS负载均衡技术,当一个网站有多个服务器地址时,在应答DNS查询的时候,DNS服务器会对每个查询返回不同的解析结果,也就是返回不同的IP地址,从而把访问引导到不同的服务器上去,来达到负载均衡的目的。

递归查询

迭代查询

DNS负载均衡

\

宏任务和微任务

\

Event Loop

\

JS到底是怎么运行的呢?

\

\

image

\

JS引擎常驻于内存中,等待宿主将JS代码或函数传递给它。
也就是等待宿主环境分配宏观任务,反复等待 - 执行即为事件循环。

\

Event Loop中,每一次循环称为tick,每一次tick的任务如下:

\

  • 执行栈选择最先进入队列的宏任务(一般都是script),执行其同步代码直至结束;
  • 检查是否存在微任务,有则会执行至微任务队列为空;
  • 如果宿主为浏览器,可能会渲染页面;
  • 开始下一轮tick,执行宏任务中的异步代码(setTimeout等回调)。

\

概念5:宏任务和微任务

\

ES6 规范中,microtask 称为 jobs,macrotask 称为 task
宏任务是由宿主发起的,而微任务由JavaScript自身发起。

\

.宏任务(macrotask )和微任务(microtask )

\

macrotask 和 microtask 表示异步任务的两种分类

\

在ES3以及以前的版本中,JavaScript本身没有发起异步请求的能力,也就没有微任务的存在。在ES5之后,JavaScript引入了Promise,这样,不需要浏览器,JavaScript引擎自身也能够发起异步任务了。

\

所以,总结一下,两者区别为:

宏任务(macrotask)微任务(microtask)
谁发起的宿主(Node、浏览器)JS引擎
具体事件1. script (可以理解为外层同步代码) 2. setTimeout/setInterval 3. UI rendering/UI事件 4. postMessage,MessageChannel 5. setImmediate,I/O(Node.js)1. Promise 2. MutaionObserver 3. Object.observe(已废弃;Proxy对象替代) 4. process.nextTick(Node.js)
谁先运行后运行先运行
会触发新一轮Tick吗不会

\

拓展 1:asyncawait是如何处理异步任务的?

\

简单说,async是通过Promise包装异步任务。

\

比如有如下代码:

\

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

\

改为ES5的写法:

\

new Promise((resolve, reject) => {
  // console.log('async2 end')
  async2() 
  ...
}).then(() => {
 // 执行async1()函数await之后的语句
  console.log('async1 end')
})

\

当调用 async1 函数时,会马上输出 async2 end,并且函数返回一个 Promise,接下来在遇到 await的时候会就让出线程开始执行 async1 外的代码(可以把 await 看成是让出线程的标志)。
然后当同步代码全部执行完毕以后,就会去执行所有的异步代码,那么又会回到 await 的位置,去执行 then 中的回调。

\

拓展 2:setTimeoutsetImmediate谁先执行?

\

setImmediateprocess.nextTick为Node环境下常用的方法(IE11支持setImmediate),所以,后续的分析都基于Node宿主。

\

Node.js是运行在服务端的js,虽然用到也是V8引擎,但由于服务目的和环境不同,导致了它的API与原生JS有些区别,其Event Loop还要处理一些I/O,比如新的网络连接等,所以与浏览器Event Loop不太一样。

\

执行顺序如下:

  1. timers: 执行setTimeout和setInterval的回调
  1. pending callbacks: 执行延迟到下一个循环迭代的 I/O 回调
  1. idle, prepare: 仅系统内部使用
  1. poll: 检索新的 I/O 事件;执行与 I/O 相关的回调。事实上除了其他几个阶段处理的事情,其他几乎所有的异步都在这个阶段处理。
  1. check: setImmediate在这里执行
  1. close callbacks: 一些关闭的回调函数,如:socket.on('close', ...)

\

一般来说,setImmediate会在setTimeout之前执行,如下:

\

console.log('outer');
setTimeout(() => {
  setTimeout(() => {
    console.log('setTimeout');
  }, 0);
  setImmediate(() => {
    console.log('setImmediate');
  });
}, 0);

\

其执行顺序为:

\

  1. 外层是一个setTimeout,所以执行它的回调的时候已经在timers阶段了
  1. 处理里面的setTimeout,因为本次循环的timers正在执行,所以其回调其实加到了下个timers阶段
  1. 处理里面的setImmediate,将它的回调加入check阶段的队列
  1. 外层timers阶段执行完,进入pending callbacks,idle, prepare,poll,这几个队列都是空的,所以继续往下
  1. 到了check阶段,发现了setImmediate的回调,拿出来执行
  1. 然后是close callbacks,队列是空的,跳过
  1. 又是timers阶段,执行console.log('setTimeout')

\

但是,如果当前执行环境不是timers阶段,就不一定了。。。。顺便科普一下Node里面对setTimeout的特殊处理:setTimeout(fn, 0)会被强制改为setTimeout(fn, 1)

\

看看下面的例子:

\

setTimeout(() => {
  console.log('setTimeout');
}, 0);

setImmediate(() => {
  console.log('setImmediate');
});

\

其执行顺序为:

\

  1. 遇到setTimeout,虽然设置的是0毫秒触发,但是被node.js强制改为1毫秒,塞入times阶段
  1. 遇到setImmediate塞入check阶段
  1. 同步代码执行完毕,进入Event Loop
  1. 先进入times阶段,检查当前时间过去了1毫秒没有,如果过了1毫秒,满足setTimeout条件,执行回调,如果没过1毫秒,跳过
  1. 跳过空的阶段,进入check阶段,执行setImmediate回调

\

可见,1毫秒是个关键点,所以在上面的例子中,setImmediate不一定在setTimeout之前执行了。

\

拓展 3:Promiseprocess.nextTick谁先执行?

\

因为process.nextTick为Node环境下的方法,所以后续的分析依旧基于Node。

\

process.nextTick() 是一个特殊的异步API,其不属于任何的Event Loop阶段。事实上Node在遇到这个API时,Event Loop根本就不会继续进行,会马上停下来执行process.nextTick(),这个执行完后才会继续Event Loop。

\

所以,nextTickPromise同时出现时,肯定是nextTick先执行,原因是nextTick的队列比Promise队列优先级更高。

\

拓展 4:应用场景 - Vue中的vm.$nextTick

\

vm.$nextTick 接受一个回调函数作为参数,用于将回调延迟到下次DOM更新周期之后执行。

\

这个API就是基于事件循环实现的。
“下次DOM更新周期”的意思就是下次微任务执行时更新DOM,而vm.$nextTick就是将回调函数添加到微任务中(在特殊情况下会降级为宏任务)。

\

因为微任务优先级太高,Vue 2.4版本之后,提供了强制使用宏任务的方法。

\

vm.$nextTick优先使用Promise,创建微任务。
如果不支持Promise或者强制开启宏任务,那么,会按照如下顺序发起宏任务:

  1. 优先检测是否支持原生 setImmediate(这是一个高版本 IE 和 Edge 才支持的特性)
  1. 如果不支持,再去检测是否支持原生的MessageChannel
  1. 如果也不支持的话就会降级为 setTimeout。

\

Vue2和Vue3的区别

\

  1. vue3.0可以通过解构的方式拿到createApp方法,通过该方法得到app调用mount进行挂载。这也是vue3函数式编程的设计理念,这种设计方式可以按需引入资源,更好的利用tree-shaking来减小打包体积。
    vue2.0 是通过new Vue创建实例,通过参数el确定挂载的dom进行挂载,也可以不传el直接使用app.$mount('#app')
  1. vue3中移除了beforeCreate 和 created,增加了setup函数。其他周期函数基本就是命名上在vue2.x的基础上加上on前缀,以驼峰命名方式命名,要写到setup函数里面。
    此外还增加了onRenderTracked和onRenderTriggered是用来调试的,这两个事件都带有一个DebuggerEvent,它使我们能够知道是什么导致了Vue实例中的重新渲染。
    vue3.0采用函数式编程方式,打破了this的限制,能够更好的复用性,真正体现实现功能的高内聚低耦合,更利于代码的可扩展性和可维护性。
  1. vue3.0提供了reactive和ref,一般ref可以用来定义基础类型,也可以是引用类型,reactive只能用来定义引用类型。
    ref的本质就是reactive({value: 原始数据}),例如Ref(10)=>Reactive({value:10});
    对于引用类型,什么时候用ref,什么时候用reactive?简单说,如果你只打算修改引用类型的一个属性,那么推荐用reactive,如果你打算变量重赋值,那么一定要用ref。
    ref定义的变量通过变量.value = xxx改变,reactive定义的变量通过 obj.xx = xx 即可。
    vue2.0直接将数据放到了data中,通过this.xx = xx 来改变。
    针对数据响应原理两者变化很大,vue2.0是利用object.defineProperty,vue3.0是利用Proxy和Reflect来实现,最大的优势就是vue3.0可以监听到数组、对象新增/删除或多层嵌套数据结构的响应。

\

react都核心思想就是,将一个页面拆成一堆独立的,可复用的组件,并且用自上而下的单向数据流的形式将这些组件串联起来

\

react规定我们必须把hooks写在函数的最外层,不能写在ifelse等条件语句当中,来确保hooks的执行顺序一致

\

响应式布局

\

1.媒体查询

\

使用@media媒体查询可以针对不同的媒体类型定义不同的样式,特别是响应式页面,可以针对不同屏幕的大小,编写多套样式,从而达到自适应的效果。

\

  • 超小屏幕 (max-width:576px) 如苹果4,苹果5
  • 小屏幕 (min-width: 576px) and (max-width: 768px) 如大部分手机
  • 中屏幕 (min-width: 768px) and (max-width: 992px) 如pad
  • 大屏幕 (min-width: 992px) and (max-width: 1200px) 如中屏电脑
  • 超大屏幕 (min-width: 1200px) 如:大屏幕电脑

\

2.百分比%

\

3.vw/vh(相对于视图窗口)

\

4.rem(字体)

\

rem布局的本质是等比缩放,作用于非根元素时,它是相对于根元素。作用于根元素时,相对于浏览器原始字体大小。
em是字体大小是相对于父元素

\

px是固定像素

\

5.flex弹性布局

\

  • 在父元素上,我们经常会用到的有关弹性布局的属性主要有 flex-direction , flex-wrap , justify-content , align-items , align-content ,这几个属性分别从 主轴的方向、是否换行、项目在主轴上的对齐方式、项目在交叉轴上的对齐方式、项目在多根轴线上的对齐方式来规范了项目在父元素中的弹性。
  • 在子元素上,我们经常会用到的有关弹性布局的属性主要有 order , flex-grow , flex-shrink ,flex-basis , align-self ,这几个属性分别从项目的排序、项目放大比例、项目缩小比例、项目占据主轴空间、单个项目在交叉轴上的对齐方式来规范了项目自身的弹性。

\

.组件之间的传值方式有哪些?

\

  • 父传子: 组件使用props进行接收
  • 子传父: 子组件使用$emit+事件对父组件进行传值
  • 父子之间通过$parent$chidren获取实例进而通信
  • 使用$refs获取组件实例,进而获取数据。使用vuex进行状态管理
  • 使用eventBus进行跨组件触发事件,进而传递数据
  • 使用浏览器本地缓存,例如localstorage``sessionStorage

\

Event Bus

\

\

  • 首先构造一个 EventBus 类,初始化一个空对象用于存放所有的事件
  • 在接受订阅时,将事件名称作为 key 值,将需要在接受发布消息后执行的回调函数作为 value 值,由于一个事件可能有多个订阅者,所以这里的回调函数要存储成列表
  • 在发布事件消息时,从事件列表里取得指定的事件名称对应的所有回调函数,依次触发执行即

\

正则函数中test和match的区别

\

用于检测一个字符串是否匹配某个模式

\

1.match()方法

\

match是String的方法,参数正则表达式返回值数组没有找到时返回的是null

\

stringObj.match(rgExp) 其中stringObj是必选项,对其进行查找的string对象或字符串文字。rgExp是必选项,为包含正则表达式模式和可用标志的正则表达式对象。

\

使用方法如下:

\

\

2.test()方法

\

test是RegExp的方法,参数字符串返回值boolean类型

\

\

返回值:如果字符串 string 中含有与 RegExpObject 匹配的文本,则返回 true,否则返回 false。

\

JS数组去重

\

第一种:遍历数组法

\

这种方法最简单最直观,也最容易理解,代码如下:

\

1  var arr = [2, 8, 5, 0, 5, 2, 6, 7, 2]
2   var newArr = []
3   for (var i = 0; i < arr.length; i++) {
4     if (newArr.indexOf(arr[i]) === -1) {
5       newArr.push(arr[i])
6     }
7   }
8   console.log(newArr) // 结果:[2, 8, 5, 0, 6, 7]

\

这种方法很好理解,利用了indexOf() 方法(indexOf()方法如果查询到则返回查询到的第一个结果在数组中的索引,如果查询不到则返回**-1**)。先创建一个新的空数组用来存储新的去重的数组,然后遍历arr数组,在遍历过程中,分别判断newArr数组里面是不是有遍历到的arr中的元素,如果没有,直接添加进newArr中,如果已经有了(重复),那么不操作,那么从头到尾遍历一遍,正好达到了去重的目的。

\

第二种:数组下标判断法

\

这种方法也比较好理解,代码如下:

\

1 var arr = [2, 8, 5, 0, 5, 2, 6, 7, 2]
2   var newArr = []
3   for (var i = 0; i < arr.length; i++) {
4     if (arr.indexOf(arr[i]) === i) {
5       newArr.push(arr[i])
6     }
7   }
8   console.log(newArr) // 结果:[2, 8, 5, 0, 6, 7]

\

这和第一种方法有重叠,不说多余的,直接看if这里,在遍历arr的过程中,如果在arr数组里面找当前的值,返回的索引等于当前的循环里面的i的话,那么证明这个值是第一次出现,所以推入到新数组里面,如果后面又遍历到了一个出现过的值,那也不会返回它的索引,indexof()方法只返回找到的第一个值的索引,所以重复的都会被pass掉,只出现一次的值都被存入新数组中,也达到了去重的目的。

\

第三种:排序后相邻去除法

\

这种方法用到了sort()方法,代码如下:

\

1 var arr = [2, 8, 5, 0, 5, 2, 6, 7, 2]
2   arr.sort()
3   var newArr = [arr[0]]
4   for (var i = 1; i < arr.length; i++) {
5     if (arr[i] !== newArr[newArr.length - 1]) {
6       newArr.push(arr[i])
7     }
8   }
9   console.log(newArr) // 结果:[0, 2, 5, 6, 7, 8]

\

这种方法的思路是:先用sort()方法把arr排序,那么排完序后,相同的一定是挨在一起的,把它去掉就好了,首先给新数组初始化一个arr[0],因为我们要用它和arr数组进行比较,所以,for循环里面i也是从1开始了,我们让遍历到的arr中的值和新数组最后一位进行比较,如果相等,则pass掉,不相等的,push进来,因为数组重新排序了,重复的都挨在一起,那么这就保证了重复的这几个值只有第一个会被push进来,其余的都和新数组的被push进来的这个元素相等,会被pass掉,也达到了去重的效果。

\

第四种:优化的遍历数组法

\

 1  var arr = [2, 8, 5, 0, 5, 2, 6, 7, 2, 8]
 2   var newArr = []
 3   for (var i = 0; i < arr.length; i++) {
 4     for (var j = i + 1; j < arr.length; j++) {
 5       if (arr[i] === arr[j]) {
 6         i++
 7         j = i
 8       }
 9     }
10     newArr.push(arr[i])
11   }
12   console.log(newArr) // 结果:[0, 5, 6, 7, 2, 8]

\

思路:两层for循环,外面一层是控制遍历到的前一个arr中的元素,里面一层控制的是第一层访问到的元素后面的元素,不断的从第0个开始,让第0个和他后面的元素比较,如果没有和这个元素相等的,则证明没有重复,推入到新数组中存储起来,如果有和这个元素相等的,则pass掉它,直接进入下一次循环。从第1个开始,继续和它后面的元素进行比较,同上进行,一直循环到最后就是:不重复的都被推入新数组里面了,而重复的前面的元素被pass掉了,只留下了最后面的一个元素,这个时候也就不重复了,则推入新数组,过滤掉了所有重复的元素,达到了去重的目的。

\

第五种:数组遍历法

\

 1 var arr = ['a', 'a', 'b', 'c', 'b', 'd', 'e', 'a']
 2   var newArr = []
 3   for (var i = 0; i < arr.length; i++) {
 4     var bl = true
 5     for (var j = 0; j < newArr.length; j++) {
 6       if (arr[i] === newArr[j]) {
 7         bl = false
 8         break
 9       }
10     }
11     if (bl) {
12       newArr.push(arr[i])
13     }
14   }
15   console.log(newArr) // 结果:["a", "b", "c", "d", "e"]

\

思路:也是两层for循环,外层for循环控制的是arr数组的遍历,内层for循环控制的是新数组的遍历,从第0位开始,如果新数组中没有这个arr数组中遍历到的这个元素,那么状态变量bl的值还是true,那么自然进入到了if中把这个值推入到新数组中,如果有这个元素,那么代表重复,则把状态变量bl取值改为false,并且跳出当前循环,不会进入到if内部,而进入下一次外层开始的循环。这样循环往复,最后也达到了去重的效果。

\

异步执行

\

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。

\

继承

\

js里常用的如下两种继承方式:

\

原型链继承(对象间的继承)
类式继承(构造函数间的继承)

\

由于js不像java那样是真正面向对象的语言,js是基于对象的,它没有类的概念。所以,要想实现继承,可以用js的原型prototype机制或者用applycall方法去实现

\

在面向对象的语言中,我们使用来创建一个自定义对象。然而js中所有事物都是对象,那么用什么办法来创建自定义对象呢?这就需要用到js原型

\

我们可以简单的把prototype看做是一个模版,新创建的自定义对象都是这个模版(prototype)的一个拷贝 (实际上不是拷贝而是链接,只不过这种链接是不可见,新实例化的对象内部有一个看不见的__Proto__指针,指向原型对象)。

\

js可以通过构造函数和原型的方式模拟实现类的功能。 另外,js类式继承的实现也是依靠原型链来实现的。

\

原型式继承与类式继承

\

类式继承是在子类型构造函数的内部调用超类型的构造函数。
严格的类式继承并不是很常见,一般都是组合着用:

\

\

1 function Super(){
2     this.colors=["red","blue"];
3 }
4  
5 function Sub(){
6     Super.call(this);
7 }

\

\

原型式继承是借助已有的对象创建新的对象,将子类的原型指向父类,就相当于加入了父类这条原型链

\

原型链继承

\

为了让子类继承父类的属性(也包括方法),首先需要定义一个构造函数。然后,将父类的新实例赋值给构造函数的原型。

\

1.isNaN() 函数。这个函数接受一个参数,该参数可以是任何类型,而函数会帮我们确定这个参数是否“不是数值” 。isNaN()在接受一个值后,会尝试将这个值转换为数值。某些不是数值的值会直接转换为数值,例如字符串“10”或Boolean值。而任何不能被转换为数值的值都会导致这个函数返回true。

\

eval:返回字符串表达式中的值

\

unEscape:返回字符串ASCI码

\

escape:返回字符的编码

\

parseFloat:返回实数

\

正则定义
\d匹配一个数字,等价于[0-9]
\w匹配字母、数字或者下划线,等价于 [A-Za-z0-9_]
+匹配前面一个表达式 1 次或者多次
*匹配前一个表达式 0 次或多次
/g全局匹配

\

因为"+"和"*"都是贪婪匹配,它们会尽可能多地匹配字符,

\

\

console.log(``1``+ +``"2"``+``"2"``);

\

第一个+"2"中的加号是一元加操作符,+"2"会变成数值2,因此1+ +"2"相当于1+2=3.
然后和后面的字符串“2”相合并,变成了字符串"32".

\

console.log(``"A"``- ``"B"``+``"2"``);

\

"A"-"B"的运算中,需要先把"A"和"B"用Number函数转换为数值,其结果为NaN,在剪发操作中,如果有一个是NaN,则结果是NaN,因此"A"-"B"结果为NaN。
然后和"2"进行字符串合并,变成了NaN2.

\

4.js的全局属性:Infinity、NAN、undefined

\

js的全局函数:decodeURI()、decodeURIcomponent()、

\

           encodeURI、encodeURIcomponent()、

\

            escape()、eval()、isFinite()、isNAN()、

\

           Number()、parseFloat()、parseInt()、String()、unescape()。

\

函数 描述

\

decodeURI() 解码某个编码的 URI。

\

decodeURIComponent() 解码一个编码的 URI 组件。

\

encodeURI() 把字符串编码为 URI。

\

encodeURIComponent() 把字符串编码为 URI 组件。

\

escape() 对字符串进行编码。

\

eval() 计算 JavaScript 字符串,并把它作为脚本代码来执行。

\

isFinite() 检查某个值是否为有穷大的数。

\

isNaN() 检查某个值是否是数字。

\

Number() 把对象的值转换为数字。

parseFloat() 解析一个字符串并返回一个浮点数。

\

parseInt() 解析一个字符串并返回一个整数。

\

String() 把对象的值转换为字符串。

\

unescape() 对由 escape() 编码的字符串进行解码。

\

5.instanceof 严格来说是Java中的一个双目运算符,用来测试一个对象是否为一个类的实例

\

6.call()方法和apply()方法的作用相同,他们的区别在于接收参数的方式不同。

\

对于call(),第一个参数是this值没有变化,变化的是其余参数都直接传递给函数。(在使用call()方法时,传递给函数的参数必须逐个列举出来。使用apply()时,传递给函数的是参数数组)

\

call和apply实现思路主要是:
判断是否是函数调用,若非函数调用抛异常
通过新对象(context)来调用函数
给context创建一个fn设置为需要调用的函数
结束调用完之后删除fn
bind实现思路
判断是否是函数调用,若非函数调用抛异常
返回函数
判断函数的调用方式,是否是被new出来的
new出来的话返回空对象,但是实例的proto指向_this的prototype
完成函数柯里化
Array.prototype.slice.call()

\

apply接受两个参数,第一个参数是this的指向,第二个参数是函数接受的参数,以数组的形式传入,且当第一个参数为null、undefined的时候,默认指向window(在浏览器中),使用apply方法改变this指向后原函数会立即执行,且此方法只是临时改变thi指向一次

\

call方法的第一个参数也是this的指向,后面传入的是一个参数列表(注意和apply传参的区别)。当一个参数为null或undefined的时候,表示指向window(在浏览器中),和apply一样,call也只是临时改变一次this指向,并立即执行

\

bind方法和call很相似,第一参数也是this的指向,后面传入的也是一个参数列表(但是这个参数列表可以分多次传入,call则必须一次性传入所有参数),但是它改变this指向后不会立即执行,而是返回一个永久改变this指向的函数

\

apply,call,bind三者的区别

\

  • 三者都可以改变函数的this对象指向。
  • 三者第一个参数都是this要指向的对象,如果如果没有这个参数或参数为undefined或null,则默认指向全局window。
  • 三者都可以传参,但是apply是数组,而call是参数列表,且apply和call是一次性传入参数,而bind可以分为多次传入。
  • bind 是返回绑定this之后的函数,便于稍后调用;apply 、call 则是立即执行 。

\

如下代码做出解释:

\

function add(c, d){
return this.a + this.b + c + d;
}
var o = {a:1, b:3};
add.call(o, 5, 7); // 1 + 3 + 5 + 7 = 16
add.apply(o, [10, 20]); // 1 + 3 + 10 + 20 = 34

\

7.// 1.调用对象未声明的属性会undifned
var user={};
console.log(user.name);//undifned

\

// 2.使用未赋值只声明的基本数据类型会undifned
var one;
console.log(one);//undifned

\

// 3.使用未声明的变量会报错
console.log(two);//new_file.html:15 Uncaught ReferenceError: two is not defined

\

\

宏任务和微任务

\

Event Loop

\

JS到底是怎么运行的呢?

\

\

image

\

JS引擎常驻于内存中,等待宿主将JS代码或函数传递给它。
也就是等待宿主环境分配宏观任务,反复等待 - 执行即为事件循环。

\

Event Loop中,每一次循环称为tick,每一次tick的任务如下:

\

  • 执行栈选择最先进入队列的宏任务(一般都是script),执行其同步代码直至结束;
  • 检查是否存在微任务,有则会执行至微任务队列为空;
  • 如果宿主为浏览器,可能会渲染页面;
  • 开始下一轮tick,执行宏任务中的异步代码(setTimeout等回调)。

\

概念5:宏任务和微任务

\

ES6 规范中,microtask 称为 jobs,macrotask 称为 task
宏任务是由宿主发起的,而微任务由JavaScript自身发起。

\

.宏任务(macrotask )和微任务(microtask )

\

macrotask 和 microtask 表示异步任务的两种分类

\

在ES3以及以前的版本中,JavaScript本身没有发起异步请求的能力,也就没有微任务的存在。在ES5之后,JavaScript引入了Promise,这样,不需要浏览器,JavaScript引擎自身也能够发起异步任务了。

\

所以,总结一下,两者区别为:

宏任务(macrotask)微任务(microtask)
谁发起的宿主(Node、浏览器)JS引擎
具体事件1. script (可以理解为外层同步代码) 2. setTimeout/setInterval 3. UI rendering/UI事件 4. postMessage,MessageChannel 5. setImmediate,I/O(Node.js)1. Promise 2. MutaionObserver 3. Object.observe(已废弃;Proxy对象替代) 4. process.nextTick(Node.js)
谁先运行后运行先运行
会触发新一轮Tick吗不会

\

拓展 1:asyncawait是如何处理异步任务的?

\

简单说,async是通过Promise包装异步任务。

\

比如有如下代码:

\

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

\

改为ES5的写法:

\

new Promise((resolve, reject) => {
  // console.log('async2 end')
  async2() 
  ...
}).then(() => {
 // 执行async1()函数await之后的语句
  console.log('async1 end')
})

\

当调用 async1 函数时,会马上输出 async2 end,并且函数返回一个 Promise,接下来在遇到 await的时候会就让出线程开始执行 async1 外的代码(可以把 await 看成是让出线程的标志)。
然后当同步代码全部执行完毕以后,就会去执行所有的异步代码,那么又会回到 await 的位置,去执行 then 中的回调。

\

拓展 2:setTimeoutsetImmediate谁先执行?

\

setImmediateprocess.nextTick为Node环境下常用的方法(IE11支持setImmediate),所以,后续的分析都基于Node宿主。

\

Node.js是运行在服务端的js,虽然用到也是V8引擎,但由于服务目的和环境不同,导致了它的API与原生JS有些区别,其Event Loop还要处理一些I/O,比如新的网络连接等,所以与浏览器Event Loop不太一样。

\

执行顺序如下:

  1. timers: 执行setTimeout和setInterval的回调
  2. pending callbacks: 执行延迟到下一个循环迭代的 I/O 回调
  3. idle, prepare: 仅系统内部使用
  4. poll: 检索新的 I/O 事件;执行与 I/O 相关的回调。事实上除了其他几个阶段处理的事情,其他几乎所有的异步都在这个阶段处理。
  5. check: setImmediate在这里执行
  6. close callbacks: 一些关闭的回调函数,如:socket.on('close', ...)

\

一般来说,setImmediate会在setTimeout之前执行,如下:

\

console.log('outer');
setTimeout(() => {
  setTimeout(() => {
    console.log('setTimeout');
  }, 0);
  setImmediate(() => {
    console.log('setImmediate');
  });
}, 0);

\

其执行顺序为:

\

  1. 外层是一个setTimeout,所以执行它的回调的时候已经在timers阶段了
  2. 处理里面的setTimeout,因为本次循环的timers正在执行,所以其回调其实加到了下个timers阶段
  3. 处理里面的setImmediate,将它的回调加入check阶段的队列
  4. 外层timers阶段执行完,进入pending callbacks,idle, prepare,poll,这几个队列都是空的,所以继续往下
  5. 到了check阶段,发现了setImmediate的回调,拿出来执行
  6. 然后是close callbacks,队列是空的,跳过
  7. 又是timers阶段,执行console.log('setTimeout')

\

但是,如果当前执行环境不是timers阶段,就不一定了。。。。顺便科普一下Node里面对setTimeout的特殊处理:setTimeout(fn, 0)会被强制改为setTimeout(fn, 1)

\

看看下面的例子:

\

setTimeout(() => {
  console.log('setTimeout');
}, 0);

setImmediate(() => {
  console.log('setImmediate');
});

\

其执行顺序为:

\

  1. 遇到setTimeout,虽然设置的是0毫秒触发,但是被node.js强制改为1毫秒,塞入times阶段
  2. 遇到setImmediate塞入check阶段
  3. 同步代码执行完毕,进入Event Loop
  4. 先进入times阶段,检查当前时间过去了1毫秒没有,如果过了1毫秒,满足setTimeout条件,执行回调,如果没过1毫秒,跳过
  5. 跳过空的阶段,进入check阶段,执行setImmediate回调

\

可见,1毫秒是个关键点,所以在上面的例子中,setImmediate不一定在setTimeout之前执行了。

\

拓展 3:Promiseprocess.nextTick谁先执行?

\

因为process.nextTick为Node环境下的方法,所以后续的分析依旧基于Node。

\

process.nextTick() 是一个特殊的异步API,其不属于任何的Event Loop阶段。事实上Node在遇到这个API时,Event Loop根本就不会继续进行,会马上停下来执行process.nextTick(),这个执行完后才会继续Event Loop。

\

所以,nextTickPromise同时出现时,肯定是nextTick先执行,原因是nextTick的队列比Promise队列优先级更高。

\

拓展 4:应用场景 - Vue中的vm.$nextTick

\

vm.$nextTick 接受一个回调函数作为参数,用于将回调延迟到下次DOM更新周期之后执行。

\

这个API就是基于事件循环实现的。
“下次DOM更新周期”的意思就是下次微任务执行时更新DOM,而vm.$nextTick就是将回调函数添加到微任务中(在特殊情况下会降级为宏任务)。

\

因为微任务优先级太高,Vue 2.4版本之后,提供了强制使用宏任务的方法。

\

vm.$nextTick优先使用Promise,创建微任务。
如果不支持Promise或者强制开启宏任务,那么,会按照如下顺序发起宏任务:

  1. 优先检测是否支持原生 setImmediate(这是一个高版本 IE 和 Edge 才支持的特性)
  2. 如果不支持,再去检测是否支持原生的MessageChannel
  3. 如果也不支持的话就会降级为 setTimeout。

\

\

VUE

\

src文件夹下文件夹目录:
① views 文件夹存放界面
② components 文件夹存放界面中局部组件
③ config 文件夹存放各种全局配置
④ images 文件夹存放图片
⑤ plugins 文件夹存放各种插件
⑥ router 文件夹存放路由
⑦ store 文件夹存放vuex相关文件
⑧ service 文件夹存放服务器端相关操作,接口等
⑨ style 文件夹存放样式

\

路由守卫

\

一.全局守卫

\

  1. router.beforeEach((to,from,next)=>{})
  2. 回调函数中的参数,to:进入到哪个路由去,from:从哪个路由离开,next:函数,决定是否展示你要看到的路由页面。
  3. 如下例:main.js中设置全局守卫

\

在main.js中,有一个路由实例化对象router。在main.js中设置守卫已是全局守卫。
如下,判断to.path当前将要进入的路径是否为登录或注册,如果是就执行next(),展示当前界面。如果不是,就弹出alert,然后移至登录界面。
这样就可实现,用户在未登录状态下,展示的一直是登录界面。

\

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

\

if(to.path == '/login' || to.path == '/register'){

\

next();

\

}else{

\

alert('您还没有登录,请先登录');

next('/login');

\

}

\

})

\

  1. 全局后置钩子router.afterEach((to,from)=>{})

\

只有两个参数,to:进入到哪个路由去,from:从哪个路由离。
如下,每次切换路由时,都会弹出alert,点击确定后,展示当前页面。

\

router.afterEach((to,from)=>{

\

alert("after each");

\

})

\

  1. 判断store.gettes.isLogin === false 是否登录

\

二.组件内的守卫

\

  1. 到达这个组件时,beforeRouteEnter:(to,from,next)=>{}

\

在Admin.vue文件中,点击转到admin路由时,执行beforeRouteEnter函数
to,from参数与上面使用方法一致。next回调函数略有不同。
如下例,data 组件内守卫有特殊情况,如果我们直接以
beforeRouteEnter:(to,from,next)=>{ alert("hello" + this.name);}进行访问admin页面,会发现alert输出hello undefined。这是因为,现在访问不到我们的data属性,执行顺序是不一致,这与的声明周期有关。在执行完之前,data数据还未渲染。所以这里,next()会给一个对应的回调,帮助完成。

\

  1. 离开这个组件时,beforeRouteLeave:(to,from,next)=>{}

\

点击其他组件时,判断是否确认离开。确认执行next();取消执行next(false),留在当前页面。

\

beforeRouteLeave:(to,from,next)=>{

\

    if(confirm("确定离开此页面吗?") == true){

        next();

    }else{

        next(false);

    }

}

\

三.路由独享的守卫

\

  1. beforeEnter:(to,from,next)=>{},用法与全局守卫一致。只是,将其写进其中一个路由对象中,只在这个路由下起作用。

\

对vuex的理解?

\

vuex是专门为vue提供的全局状态管理系统,用于多个组件中数据共享、数据缓存等。
主要包括以下模块对象:
State: 定义了应用状态的数据结构,可以在这里设置默认的初始状态,类似data。

\

存储公共管理的数据

\

Getter: 类似于计算属性computed,允许组件从Store中获取数据(this.$store.getters.doneTodosCount)。

\

定义 store 的计算属性

\

Mutation: 是唯一更改store中状态的方法,且必须是同步函数(this.$store.commit(‘mutation的方法’,arg))。

\

定义改变state中数据的方法

\

Action: 用于提交mutation,而不是直接变更状态,可以写入异步操作(this.$store.dispatch(‘action的方法’,arg))。

\

类似于 mutation,不同在于:Action 提交的是 mutation,而不是直接变更状态。Action 可以包含任意异步操作。 moudle属性是将store分割成模块。

\

Module: Vuex允许我们将store分隔成模块,每个模块拥有自己的state、getter、mutation、action、Module允许将多个store保存在同一个状态树中。

\

mixins和vuex的区别?
vuex公共状态管理,如果其中一个组件改变了vuex的数据。那么其它引入vuex的组件数据也会相应改变,类似于浅拷贝
mixins如果被多个组件引入,在每个组件中是独立的,互不影响,改变其中一个组件的数据不会影响到其它,类似于深拷贝

\

一、创建前 / 后

\

在beforeCreate生命周期函数执行的时候,data和method 还没有初始化

\

在created 生命周期函数执行的时候,data和method已经初始化完成

\

二、渲染前/后

\

在beforeMount 生命周期函数执行的时候,已经编译好了模版字符串、但还没有真正渲染到页面中去

\

在mounted 生命周期函数执行的时候,已经渲染完,可以看到页面

\

三、数据更新前/后

\

在beforeUpdate生命周期函数执行的时候,已经可以拿到最新的数据,但还没渲染到视图中去。

\

在updated生命周期函数执行的时候,已经把更新后的数据渲染到视图中去了。

\

四、销毁前/后

\

在beforeDestroy 生命周期函数执行的时候,实例进入准备销毁的阶段、此时data 、methods 、指令等还是可用状态

\

在destroyed生命周期函数执行的时候,实例已经完成销毁、此时data 、methods 、指令等都不可用

\

预加载

\

1.vue3:defineAsyncComponent可以接受一个加载器函数,该函数将承诺解析返回给实际的组件。
如果解析后的值是ES模块,则模块的默认导出将自动用作组件。

\

Vue 3.x 中,对异步组件的使用跟 Vue 2.x 不同了。变化主要有三点:

\

异步组件声明方法的改变:Vue 3.x 新增一个辅助函数defineAsyncComponent,用来显示声明异步组件;
异步组件高级声明方法中的 component 选项更名为loader;
loader绑定的组件加载函数不再接收resolve和reject参数,而且必须返回一个Promise;

\

2.v-lazy

\

3.resolve  路由懒加载

\

包装函数(wrapped functions),一种用于函数组件PureComponent / shouldComponentUpdate形式的React.memo(),是一个高阶函数,它与 React.PureComponent类似,但是一个函数组件而非一个类。

\

React.memo()可接受2个参数,第一个参数为纯函数的组件,第二个参数用于对比props控制是否刷新,与shouldComponentUpdate()功能类似。eg:export default React.memo(XX,XX)

\

eg:现在有一个显示时间的组件,每一秒都会重新渲染一次,对于Child组件我们肯定不希望也跟着渲染,所有需要用到PureComponent(类)

\

for in 和 for of的区别

\

1.循环数组

\

区别一:for in 和 for of 都可以循环数组,for in 输出的是数组的index下标,而for of 输出的是数组的每一项的值。

\

const arr = [1,2,3,4]

\

// for ... in
for (const key in arr){
console.log(key) // 输出 0,1,2,3
}

\

// for ... of
for (const key of arr){
console.log(key) // 输出 1,2,3,4
}
2.循环对象

\

区别二:for in 可以遍历对象,for of 不能遍历对象,只能遍历带有iterator接口的,例如Set,Map,String,Array

\

const object = { name: 'lx', age: 23 }
// for ... in
for (const key in object) {
console.log(key) // 输出 name,age
console.log(object[key]) // 输出 lx,23
}

\

// for ... of
for (const key of object) {
  console.log(key) // 报错 Uncaught TypeError: object is not iterable
}

\

3.数组对象

\

const list = [{ name: 'lx' }, { age: 23 }]
for (const val of list) {
console.log(val) // 输出{ name: 'lx' }, { age: 23 }
for (const key in val) {
console.log(val[key]) // 输出 lx,23
}
}

\

总结:for in适合遍历对象,for of适合遍历数组。for in遍历的是数组的索引,对象的属性,以及原型链上的属性。

\

Vue Router 是 Vue.js

\

(opens new window) 官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌。包含的功能有:

\

  • 嵌套的路由/视图表
  • 模块化的、基于组件的路由配置
  • 路由参数、查询、通配符
  • 基于 Vue.js 过渡系统的视图过渡效果
  • 细粒度的导航控制
  • 带有自动激活的 CSS class 的链接
  • HTML5 历史模式或 hash 模式,在 IE9 中自动降级
  • 自定义的滚动条行为

\

双向数据绑定

\

Object.defineProperty、getter、setter

\

Vue响应式指的是:组件的data发生变化,立刻触发试图的更新

\

原理: Vue 采用数据劫持结合发布者-订阅者模式的方式来实现数据的响应式,通过Object.defineProperty来劫持数据的setter,getter,在数据变动时发布消息给订阅者,订阅者收到消息后进行相应的处理。 通过原生js提供的监听数据的API,当数据发生变化的时候,在回调函数中修改dom

\

核心API:Object.defineProperty Object.defineProperty API的使用

\

作用: 用来定义对象属性 特点: 默认情况下定义的数据的属性不能修改 描述属性和存取属性不能同时使用,使用会报错 响应式原理: 获取属性值会触发getter方法 设置属性值会触发setter方法 在setter方法中调用修改dom的方法 加分回答 Object.defineProperty的

\

缺点 1. 一次性递归到底开销很大,如果数据很大,大量的递归导致调用栈溢出 2. 不能监听对象的新增属性和删除属性 3. 无法正确的监听数组的方法,当监听的下标对应的数据发生改变时

\

虚拟DOM是将状态映射成试图的众多解决方案之一。页面交互的本质还是通过改变状态(变量)来改变试图渲染,而框架(像主流框架vue、react、angular)的应用可以让我们把关注的焦点更多的放在状态上,省略对DOM的操作(框架内部已经帮我们完成了)。
而虚拟DOM映射视图的方式是通过状态生成一个虚拟节点树,然后通过虚拟节点树进行渲染。虚拟节点树本质上是一个普通的js对象,它包含了创建一个DOM元素所需要的属性

\

vue的组件传值方式


1、路由传参

\

①定义路由时加上参数props: true,在定义路由路径时要留有参数占位符: name『用法: to="'路径/'+value"』

\

②在跳转到的页面加上参数props:['name']

\

③在跳转到的页面就获取到了name『用法: js中直接this. name;html中直接插值{{ name}}』

\

2、父组件向子组件传值

\

①父组件内设置要传的数据『data(){ id: value}』

\

②在父组件中引用的子组件上绑定一个自定义属性并把数据绑定在自定义属性上『< myBtn :atrid='id'></ mybtn>』

\

③在子组件添加参数props:['atrid'],即可

\

3、子组件向父组件传值

\

①由于父组件需要参数,所以在父组件中的标签上定义自定义事件,在事件内部获取参数;『@myEvent=" callback"在callback函数中接收参数』

\

②在子组件中触发自定义事件,并传参。『this.$ emit('父组件中的自定义事件',参数)』

\

4、组件之间传值

\

(1)方法一、

\

①建立一个公共的通信组件( Vue),需要传值的组件里引入该通信组件

\

②在一个中绑定一个事件this.on('eventname', this. id)

\

③在另一个组件中触发事件this.$ emit('eventname',( options)=>{})

\

(2)方法二、

\

在本地存储中添加公共数据,可以在两个页面中获取

\

4、依赖注入(provide / inject)
这种方式就是vue中依赖注入,该方法用于 父子组件之间 的通信。当然这里所说的父子不一定是真正的父子,也可以是祖孙组件,在层数很深的情况下,可以使用这种方式来进行传值。就不用一层一层的传递数据了。

\

provide和inject是vue提供的两个钩子,和data、methods是同级的。并且provide的书写形式和data一样。

\

provide 钩子用来发送数据或方法。
inject钩子用来接收数据或方法
用法:
父组件中:

\

provide() {
return {
num: this.num
};
}
子组件中:

\

inject: ['num']
1
还有另一种写法,这种写法可以访问父组件中的所有属性:

\

provide() {
return {
app: this
};
}
data() {
return {
num: 1
};
}

\

inject: ['app']
console.log(this.app.num)

\

计算属性computed :

\

\1. 支持缓存,只有依赖数据发生改变,才会重新进行计算

\

\2. 不支持异步,当computed内有异步操作时无效,无法监听数据的变化

\

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

\

\4. 如果一个属性是由其他属性计算而来的,这个属性依赖其他属性,是一个多对一或者一对一,一般用computed

\

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

\

\

get和set用法

\

data: {
    firstName: 'Foo',
    lastName: 'Bar'
  },
  computed: {
  fullName:{
   get(){//回调函数 当需要读取当前属性值是执行,根据相关数据计算并返回当前属性的值
      return this.firstName + ' ' + this.lastName
    },
   set(val){//监视当前属性值的变化,当属性值发生变化时执行,更新相关的属性数据
       //val就是fullName的最新属性值
       console.log(val)
        const names = val.split(' ');
        console.log(names)
        this.firstName = names[0];
        this.lastName = names[1];
   }
   }
  }

\

侦听属性watch: 当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。这是和computed最大的区别。

\

\1. 不支持缓存,数据变,直接会触发相应的操作;

\

2.watch支持异步;

\

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

\

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

\

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

\

immediate:组件加载立即触发回调函数执行,

\

deep: 深度监听,为了发现对象内部值的变化,复杂类型的数据时使用,例如数组中的对象内容的改变,注意监听数组的变动不需要这么做。注意:deep无法监听到数组的变动和对象的新增,参考vue数组变异,只有以响应式的方式触发才会被监听到。

\

\

data(){
      return{
        'first':{
          second:0
        }
      }
    },
    watch:{
      secondChange:{
        handler(oldVal,newVal){
          console.log(oldVal)
          console.log(newVal)
        },
        deep:true
      }
    },

\

console.log打印的结果,发现oldVal和newVal值是一样的,所以深度监听虽然可以监听到对象的变化,但是无法监听到具体对象里面那个属性的变化

\

监听的对象也可以写成字符串的形式

\

\

\

\

promise

\

then 接收两个回调函数并返回一个新的 promise 对象,这两个回调函数分别对应成功回调 onFullfilled 和失败回调 onRejected,这两个回调函数接收 promise 的返回值;
always (finally) 接收一个回调函数并返回一个新的 promise 对象,回调函数在上一个 promise 解析完成之后调用,也就是不管前面是 then 还是 catch 被调用了,它都会被调用,该回调函数不会接收参数。

\

直接上代码:
//第一种
promise.then((res) =>{
	console.log('then:', res);
	}).catch((err) =>{
    console.log('catch:', err);})
//第二种
promise.then((res) =>{
	console.log('then:', res);
	}, (err) => {
    console.log('catch:', err);})

\

第一种 catch 方法可以捕获到 catch 之前整条 promise 链路上所有抛出的异常。

\

第二种 then 方法的第二个参数捕获的异常依赖于上一个 Promise 对象的执行结果。

\

promise.then(successCb, faildCd) 接收两个函数作为参数,来处理上一个promise 对象的结果。then f 方法返回的是 promise 对象。第一种链式写法,使用catch,相当于给前面一个then方法返回的promise 注册回调,可以捕获到前面then没有被处理的异常。第二种是回调函数写法,仅为为上一个promise 注册异常回调。

\

如果是promise内部报错 reject 抛出错误后,then 的第二个参数就能捕获得到,如果then的第二个参数不存在,则catch方法会捕获到。

\

如果是then的第一个参数函数 resolve 中抛出了异常,即成功回调函数出现异常后,then的第二个参数reject 捕获捕获不到,catch方法可以捕获到。

\

token

\

主要有两个作用:

\

①:防止表单重复提交(防止表单重复提交一般还是使用前后端都限制的方式,比如:在前端点击提交之后,将按钮置为灰色,不可再次点击,然后客户端和服务端的token各自独立存储,客户端存储在Cookie或者Form的隐藏域(放在Form隐藏域中的时候,需要每个表单)中,服务端存储在Session(单机系统中可以使用)或者其他缓存系统(分布式系统可以使用)中)

\

//在页面初始化的时候调用后端代码像前端返回token
public String initLogin(ModelMap model, HttpSession session, String loginUrl) {
      model.put("extLoginView", clientManager.getExtLoginView());
      // 生成token
      String token = UUID.randomUUID().toString().substring(0,16);
      model.put(LOGIN_TOKEN, token);
      //返回地址与方法的  String loginUrl一致,即初始化的时候调用完方法后,又回到初始化页面
	  return loginUrl;
}

\

②:用来作身份验证
主要的理念是,客户端初始化的时候,一般就是刚刚进入页面的时候就调用后端代码,后端代码生成一个token,返回给客户端,客户端储存token(可以在前台使用Form表单中使用隐藏域来存储这个Token,也可以使用cookie),然后就将request(请求)中的token与(session)中的token进行比较:

\

	//跳转到添加页面
    @RequestMapping("/add.do")
    public String add(HttpServletRequestrequest,HttpServletResponse response){
        //生成token
        UUID token=UUID.randomUUID();
        System.out.println("token的值"+token);
        //放入session中
    request.getSession().setAttribute("token",token.toString());
		//放入request作用域中传到前台
		request.setAttribute("token",token);
	    return "add";
    }
    //前台穿过来的token进行比对
    @RequestMapping("/addMessage.do")
    public synchronized String addMessage(HttpServletRequest request){
        //获取session中的token
		Objecttoken1=request.getSession().getAttribute("token");
		//获取前台穿过来的token
		String token=request.getParameter("token");
		System.out.println("token1的值"+token1);
		if(token1==null){
			System.out.println("提交出错");
		}
		else if(!token1.equals(token)){
			System.out.println("提交出错");
		}else{
			System.out.println("提交成功");
		//移除session 防止重复提交
		request.getSession().removeAttribute("token");
		 }
		return "";
		}

\

3:使用基于 Token 的身份验证方法,在服务端不需要存储用户的登录记录。大概的流程是这样的:
客户端使用用户名跟密码请求登录
服务端收到请求,去验证用户名与密码
验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里
客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据
4:ajax中传递token的几种方式
方法一:放在请求头中

\

$.ajax({
	type: “POST”,
	headers: {
	Accept: “application/json; charset=utf-8”,
	userToken: “” + userToken
},
url: “/index”,
data: JSON.stringify(mytable.params),
contentType: “application/json”,
dataType: “json”,
success:function(data){
},

error:function(data){
}
});

\

方法二:使用beforeSend方法设置请求头

\

$.ajax({
	type: “POST”,
	url: “/index”,
	data: JSON.stringify(mytable.params),
	contentType: “application/json”,
	dataType: “json”,
	beforeSend: function(request) {
	request.setRequestHeader(“Authorization”, token);
	},
	success: function(data) {
	},
	error: function(data) {
	}
});

\

\

\

\

react

\

3. react

\

3.1 react 虚拟dom

\

参考答案:

\

虚拟dom是什么?

\

虚拟 dom 相当于在 js 和真实 dom 中间加了一个缓存,利用 dom diff 算法避免了没有必要的 dom 操作,从而 提高性能。

\

实现过程

\

  1. 用 JavaScript 对象结构表示 DOM 树的结构
  1. 用这个树构建一个真正的 DOM 树,插到文档当中当状态变更的时候,重新构造一棵新的对象树。
  1. 用新的树和旧的树进行比较,记录两棵树差异把 2 所记录的差异应用到步骤 1 所构建的真正的 DOM 树上,视图就更新了。

\

3.2 虚拟dom和real dom区别 性能差异

\

参考答案:

\

减少DOM的操作:虚拟dom可以将多次操作合并为一次操作,减少DOM操作的次数

Real DOMVirtual DOM
1. 更新缓慢。1. 更新更快。
2. 可以直接更新 HTML。2. 无法直接更新 HTML。
3. 如果元素更新,则创建新DOM。3. 如果元素更新,则更新 JSX 。
4. DOM操作代价很高。4. DOM 操作非常简单。
5. 消耗的内存较多。5. 很少的内存消耗。

\

3.3 react组件间通信

\

参考答案:

\

以下6种方法是react组件间通信的方式:

\

  • 父组件向子组件通讯: 父组件可以向子组件通过传 props 的方式,向子组件进行通讯
  • 子组件向父组件通讯: props+回调的方式,父组件向子组件传递props进行通讯,此props为作用域为父组件自身的函数,子组件调用该函数,将子组件想要传递的信息,作为参数,传递到父组件的作用域中
  • 兄弟组件通信: 找到这两个兄弟节点共同的父节点,结合上面两种方式由父节点转发信息进行通信
  • 跨层级通信:Context设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言,对于跨越多层的全局数据通过Context通信再适合不过
  • 发布订阅模式: 发布者发布事件,订阅者监听事件并做出反应,我们可以通过引入event模块进行通信
  • 全局状态管理工具: 借助Redux或者Mobx等全局状态管理工具进行通信,这种工具会维护一个全局状态中心Store,并根据不同的事件产生新的状态

\

3.4 radux的原理

\

参考答案:

\

Redux: Redux 是当今最热门的前端开发库之一。它是 JavaScript 程序的可预测状态容器,用于整个应用的状态管理。使用 Redux 开发的应用易于测试,可以在不同环境中运行,并显示一致的行为。

\

数据如何通过 Redux 流动?

\

  1. 首先,用户(通过View)发出Action,发出方式就用到了dispatch方法。
  1. 然后,Store自动调用Reducer,并且传入两个参数:当前State和收到的Action,Reducer会返回新的State
  1. State一旦有变化,Store就会调用监听函数,来更新View。

\

Redux遵循的三个原则是什么?

\

  1. 单一事实来源:整个应用的状态存储在单个 store 中的对象/状态树里。单一状态树可以更容易地跟踪随时间的变化,并调试或检查应用程序。
  1. 状态是只读的:改变状态的唯一方法是去触发一个动作。动作是描述变化的普通 JS 对象。就像 state 是数据的最小表示一样,该操作是对数据更改的最小表示。
  1. 使用纯函数进行更改:为了指定状态树如何通过操作进行转换,你需要纯函数。纯函数是那些返回值仅取决于其参数值的函数。

\

你对“单一事实来源”有什么理解?

\

Redux 使用 “Store” 将程序的整个状态存储在同一个地方。因此所有组件的状态都存储在 Store 中,并且它们从 Store 本身接收更新。单一状态树可以更容易地跟踪随时间的变化,并调试或检查程序。

\

Redux 由以下组件组成:

\

  1. Action – 这是一个用来描述发生了什么事情的对象。
  1. Reducer – 这是一个确定状态将如何变化的地方。
  1. Store – 整个程序的状态/对象树保存在Store中。
  1. View – 只显示 Store 提供的数据。

\

如何在 Redux 中定义 Action?

\

React 中的 Action 必须具有 type 属性,该属性指示正在执行的 ACTION 的类型。必须将它们定义为字符串常量,并且还可以向其添加更多的属性。在 Redux 中,action 被名为 Action Creators 的函数所创建。以下是 Action 和Action Creator 的示例:

\

function` `addTodo(text) {``    ``return` `{``        ``type: ADD_TODO,  ``         ``text``  ``}``}

\

解释 Reducer 的作用

\

Reducers 是纯函数,它规定应用程序的状态怎样因响应 ACTION 而改变。Reducers 通过接受先前的状态和 action 来工作,然后它返回一个新的状态。它根据操作的类型确定需要执行哪种更新,然后返回新的值。如果不需要完成任务,它会返回原来的状态。

\

Store 在 Redux 中的意义是什么?

\

Store 是一个 JavaScript 对象,它可以保存程序的状态,并提供一些方法来访问状态、调度操作和注册侦听器。应用程序的整个状态/对象树保存在单一存储中。因此,Redux 非常简单且是可预测的。我们可以将中间件传递到 store 来处理数据,并记录改变存储状态的各种操作。所有操作都通过 reducer 返回一个新状态。

\

Redux 有哪些优点?

\

  • 结果的可预测性 - 由于总是存在一个真实来源,即 store ,因此不存在如何将当前状态与动作和应用的其他部分同步的问题。
  • 可维护性 - 代码变得更容易维护,具有可预测的结果和严格的结构。
  • 服务器端渲染 - 你只需将服务器上创建的 store 传到客户端即可。这对初始渲染非常有用,并且可以优化应用性能,从而提供更好的用户体验。
  • 开发人员工具 - 从操作到状态更改,开发人员可以实时跟踪应用中发生的所有事情。
  • 社区和生态系统 - Redux 背后有一个巨大的社区,这使得它更加迷人。一个由才华横溢的人组成的大型社区为库的改进做出了贡献,并开发了各种应用。
  • 易于测试 - Redux 的代码主要是小巧、纯粹和独立的功能。这使代码可测试且独立。
  • 组织 - Redux 准确地说明了代码的组织方式,这使得代码在团队使用时更加一致和简单。

\

3.5 React组件生命周期的阶段是什么?

\

参考答案:

\

React 组件的生命周期有三个不同的阶段:

\

  1. 初始渲染阶段:这是组件即将开始其生命之旅并进入 DOM 的阶段。
  1. 更新阶段:一旦组件被添加到 DOM,它只有在 prop 或状态发生变化时才可能更新和重新渲染。这些只发生在这个阶段。
  1. 卸载阶段:这是组件生命周期的最后阶段,组件被销毁并从 DOM 中删除。

\

3.6 详细解释 React 组件的生命周期方法

\

参考答案:

\

目前React 16.8 +的生命周期分为三个阶段,分别是挂载阶段、更新阶段、卸载阶段

\

挂载阶段:

\

  • constructor: 构造函数,最先被执行,我们通常在构造函数里初始化state对象或者给自定义方法绑定this
  • getDerivedStateFromProps:static getDerivedStateFromProps(nextProps, prevState),这是个静态方法,当我们接收到新的属性想去修改我们state,可以使用getDerivedStateFromProps
  • render: render函数是纯函数,只返回需要渲染的东西,不应该包含其它的业务逻辑,可以返回原生的DOM、React组件、Fragment、Portals、字符串和数字、Boolean和null等内容
  • componentDidMount: 组件装载之后调用,此时我们可以获取到DOM节点并操作,比如对canvas,svg的操作,服务器请求,订阅都可以写在这个里面,但是记得在componentWillUnmount中取消订阅

\

更新阶段:

\

  • getDerivedStateFromProps: 此方法在更新个挂载阶段都可能会调用
  • shouldComponentUpdate:shouldComponentUpdate(nextProps, nextState),有两个参数nextProps和nextState,表示新的属性和变化之后的state,返回一个布尔值,true表示会触发重新渲染,false表示不会触发重新渲染,默认返回true,我们通常利用此生命周期来优化React程序性能
  • render: 更新阶段也会触发此生命周期
  • getSnapshotBeforeUpdate:getSnapshotBeforeUpdate(prevProps, prevState),这个方法在render之后,componentDidUpdate之前调用,有两个参数prevProps和prevState,表示之前的属性和之前的state,这个函数有一个返回值,会作为第三个参数传给componentDidUpdate,如果你不想要返回值,可以返回null,此生命周期必须与componentDidUpdate搭配使用
  • componentDidUpdate:componentDidUpdate(prevProps, prevState, snapshot),该方法在getSnapshotBeforeUpdate方法之后被调用,有三个参数prevProps,prevState,snapshot,表示之前的props,之前的state,和snapshot。第三个参数是getSnapshotBeforeUpdate返回的,如果触发某些回调函数时需要用到 DOM 元素的状态,则将对比或计算的过程迁移至 getSnapshotBeforeUpdate,然后在 componentDidUpdate 中统一触发回调或更新状态。

\

卸载阶段:

\

  • componentWillUnmount: 当我们的组件被卸载或者销毁了就会调用,我们可以在这个函数里去清除一些定时器,取消网络请求,清理无效的DOM元素等垃圾清理工作

\

扩展:

\

React 16之后有三个生命周期被废弃(但并未删除)

\

  • componentWillMount
  • componentWillReceiveProps
  • componentWillUpdate

\

官方计划在17版本完全删除这三个函数,只保留UNSAVE_前缀的三个函数,目的是为了向下兼容,但是对于开发者而言应该尽量避免使用他们,而是使用新增的生命周期函数替代它们

\

3.7 react rounter

\

参考答案:

\

  1. 什么是React 路由?
    React 路由是一个构建在 React 之上的强大的路由库,它有助于向应用程序添加新的屏幕和流。这使 URL 与网页上显示的数据保持同步。它负责维护标准化的结构和行为,并用于开发单页 Web 应用。 React 路由有一个简单的API。
    <``switch``>`` ``<``route exact``=``"" path``=``"’/’" component``=``"{Home}/"``>`` ``<``route path``=``"’/posts/:id’" component``=``"{Newpost}/"``>`` ``<``route path``=``"’/posts’" component``=``"{Post}/"``>``</``route``></``route``></``route``></``switch``>
  1. 为什么需要 React 中的路由?
    Router 用于定义多个路由,当用户定义特定的 URL 时,如果此 URL 与 Router 内定义的任何 “路由” 的路径匹配,则用户将重定向到该特定路由。所以基本上我们需要在自己的应用中添加一个 Router 库,允许创建多个路由,每个路由都会向我们提供一个独特的视图
  1. 为什么React Router v4中使用 switch 关键字 ?
    虽然
  1. 列出 React Router 的优点
    4.1 就像 React 基于组件一样,在 React Router v4 中,API 是 'All About Components'。可以将 Router 可视化为单个根组件(),其中我们将特定的子路由()包起来。
    4.2 无需手动设置历史值:在 React Router v4 中,我们要做的就是将路由包装在 组件中。
    4.3 包是分开的:共有三个包,分别用于 Web、Native 和 Core。这使我们应用更加紧凑。基于类似的编码风格很容易进行切换。

\

3.8 hooks的优缺点

\

参考答案

\

优点

\

更容易复用代码

\

这点应该是react hooks最大的优点,它通过自定义hooks来复用状态,从而解决了类组件有些时候难以复用逻辑的问题。类组件的逻辑复用方式是高阶组件和renderProps。hooks是怎么解决这个复用的问题呢,具体如下:

\

  1. 每调用useHook一次都会生成一份独立的状态,这个没有什么黑魔法,函数每次调用都会有一份独立的作用域。
  1. 虽然状态(from useState)和副作用(useEffect)的存在依赖于组件,但它们可以在组件外部进行定义。这点是class component做不到的,你无法在外部声明state和副作用(如componentDidMount)。

\

清爽的代码风格

\

函数式编程风格,函数式组件、状态保存在运行环境、每个功能都包裹在函数中,整体风格更清爽,更优雅

\

代码量更少

\

  1. 向props或状态取值更加方便,函数组件的取值都从父级作用域直接取,而类组件需要先访问实例引用this,再访问其属性state和props,多了一步
  1. 更改状态也变得更加简单, this.setState({ count:xxx })变成 setCount(xxx)。

\

更容易发现无用的状态和函数

\

对比类组件,函数组件里面的unused状态和函数更容易被发现

\

更容易拆分组件

\

写函数组件的时候,你会更愿意去拆分组件,因为函数组件写起小组件比类组件要省事。

\

缺点

\

部分代码从主动式变成响应式

\

写函数组件时,你不得不改变一些写法习惯。你必须把深入理解useEffect和useCallback这些api的第二个参数的作用。其次,还有下面几点:

\

  1. useEffect的依赖参数并不好写,你需要花时间去判断该把什么变量加入到依赖数组,幸运的是eslint-plugin-react-hooks很多场景可以帮你解决这点,但有时得靠你自己加以判断
  1. useEffect很容易出错,它是响应式的,当某个依赖项变化时它才会被调用。有时,useEffect会发生比你预期更多的调用次数。你必须去理解它的调用时机、调用时的状态老旧问题,这不直观,也难以维护,这点在团队协作中很明显,你的队友有时会难以理解你useEffect的触发时机以及其作用。

\

状态不同步

\

不好用的useEffect,

\

这绝对可以成为摒弃react hooks的理由。函数的运行是独立的,每个函数都有一份独立的作用域。当我们处理复杂逻辑的时候,经常会碰到“引用不是最新”的问题。

\

3.9 为什么浏览器无法读取JSX?

\

参考答案

\

浏览器只能处理 JavaScript 对象,而不能读取常规 JavaScript 对象中的 JSX。所以为了使浏览器能够读取 JSX,首先,需要用像 Babel 这样的 JSX 转换器将 JSX 文件转换为 JavaScript 对象,然后再将其传给浏览器。

\

3.10 你对 React 的 refs 有什么了解?

\

参考答案

\

Refs 是 React 中引用的简写。它是一个有助于存储对特定的 React 元素或组件的引用的属性,它将由组件渲染配置函数返回。用于对 render() 返回的特定元素或组件的引用。当需要进行 DOM 测量或向组件添加方法时,它们会派上用场。

\

class ReferenceDemo extends React.Component{``   ``display() {``     ``const name = ``this``.inputDemo.value;``     ``document.getElementById(``'disp'``).innerHTML = name;``   ``}``render() {``  ``return``(    ``     ``<div>``      ``Name: <input type=``"text"` `ref={input => ``this``.inputDemo = input} />``      ``<button name=``"Click"` `onClick={``this``.display}>Click</button>      ``      ``<h2>Hello <span id=``"disp"``></span> !!!</h2>``     ``</div>``  ``);``  ``}`` ``}

\

3.11 列出一些应该使用 Refs 的情况

\

参考答案

\

以下是应该使用 refs 的情况:

\

  • 需要管理焦点、选择文本或媒体播放时
  • 触发式动画
  • 与第三方 DOM 库集成

\

3.12 React 事件绑定原理

\

参考答案

\

React并不是将click事件绑在该div的真实DOM上,而是在document处监听所有支持的事件,当事件发生并冒泡至document处时,React将事件内容封装并交由真正的处理函数运行。这样的方式不仅减少了内存消耗,还能在组件挂载销毁时统一订阅和移除事件。
另外冒泡到 document 上的事件也不是原生浏览器事件,而是 React 自己实现的合成事件(SyntheticEvent)。因此我们如果不想要事件冒泡的话,调用 event.stopPropagation 是无效的,而应该调用event.preventDefault。

\

3.15 redux-saga 和 mobx 的比较

\

参考答案

\

  1. 状态管理

\

  • redux-sage 是 redux 的一个异步处理的中间件。
  • mobx 是数据管理库,和 redux 一样。

\

  1. 设计思想

\

  • redux-sage 属于 flux 体系, 函数式编程思想。
  • mobx 不属于 flux 体系,面向对象编程和响应式编程。

\

  1. 主要特点

\

  • redux-sage 因为是中间件,更关注异步处理的,通过 Generator 函数来将异步变为同步,使代码可读性高,结构清晰。action 也不是 action creator 而是 pure action,
  • 在 Generator 函数中通过 call 或者 put 方法直接声明式调用,并自带一些方法,如 takeEvery,takeLast,race等,控制多个异步操作,让多个异步更简单。
  • mobx 是更简单更方便更灵活的处理数据。 Store 是包含了 state 和 action。state 包装成一个可被观察的对象, action 可以直接修改 state,之后通过 Computed values 将依赖 state 的计算属性更新 ,之后触发 Reactions 响应依赖 state 的变更,输出相应的副作用 ,但不生成新的 state。

\

  1. 数据可变性

\

  • redux-sage 强调 state 不可变,不能直接操作 state,通过 action 和 reducer 在原来的 state 的基础上返回一个新的 state 达到改变 state 的目的。
  • mobx 直接在方法中更改 state,同时所有使用的 state 都发生变化,不生成新的 state。

\

  1. 写法难易度

\

  • redux-sage 比 redux 在 action 和 reducer 上要简单一些。需要用 dispatch 触发 state 的改变,需要 mapStateToProps 订阅 state。
  • mobx 在非严格模式下不用 action 和 reducer,在严格模式下需要在 action 中修改 state,并且自动触发相关依赖的更新。

\

  1. 使用场景

\

  • redux-sage 很好的解决了 redux 关于异步处理时的复杂度和代码冗余的问题,数据流向比较好追踪。但是 redux 的学习成本比 较高,代码比较冗余,不是特别需要状态管理,最好用别
    的方式代替。
  • mobx 学习成本低,能快速上手,代码比较简洁。但是可能因为代码编写的原因和数据更新时相对黑盒,导致数据流向不利于追踪。

\

3.16 简述一下 React 的源码实现

\

参考答案

\

  1. React 的实现主要分为Component和Element;
  1. Component属于 React 实例,在创建实例的过程中会在实例中注册state和props属性,还会依次调用内置的生命周期函数;
  1. Component中有一个render函数,render函数要求返回一个Element对象(或null);
  1. Element对象分为原生Element对象和组件式对象,原生Element+ 组件式对象会被一起解析成虚拟 DOM 树,并且内部使用的state和props也以 AST 的形式注入到这棵虚拟 DOM 树之中;
  1. 在渲染虚拟 DOM 树的前后,会触发 React Component 的一些生命周期钩子函数,比如componentWillMount和componentDidMount,在虚拟 DOM 树解析完成后将被渲染成真实 DOM 树;
  1. 调用setState时,会调用更新函数更新Component的state,并且触发内部的一个updater,调用render生成新的虚拟 DOM 树,利用 diff 算法与旧的虚拟 DOM 树进行比对,比对以后利用最优的方案进行 DOM 节点的更新,这也是 React 单向数据流的原理(与 Vue 的 MVVM 不同之处)。

\

3.17 setState到底是异步还是同步?

\

参考答案

\

有时表现出异步,有时表现出同步

\

  1. setState只在合成事件和钩子函数中是“异步”的,在原生事件和setTimeout中都是同步的。
  1. setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形成了所谓的“异步”,当然可以通过第二个参数setState(partialState, callback)中的callback拿到更新后的结果。
  1. setState的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次setState,setState的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时setState多个不同的值,在更新时会对其进行合并批量更新。**

\

3.18 redux异步中间件之间的优劣?

\

参考答案

\

redux-thunk优点:

\

  • 体积小: redux-thunk的实现方式很简单,只有不到20行代码
  • 使用简单: redux-thunk没有引入像redux-saga或者redux-observable额外的范式,上手简单

\

redux-thunk缺陷:

\

  • 样板代码过多: 与redux本身一样,通常一个请求需要大量的代码,而且很多都是重复性质的
  • 耦合严重: 异步操作与redux的action偶合在一起,不方便管理
  • 功能孱弱: 有一些实际开发中常用的功能需要自己进行封装

\

redux-saga优点:

\

  • 异步解耦: 异步操作被被转移到单独 saga.js 中,不再是掺杂在 action.js 或 component.js 中
  • action摆脱thunk function: dispatch 的参数依然是一个纯粹的 action (FSA),而不是充满 “黑魔法” thunk function
  • 异常处理: 受益于 generator function 的 saga 实现,代码异常/请求失败 都可以直接通过 try/catch 语法直接捕获处理
  • 功能强大: redux-saga提供了大量的Saga 辅助函数和Effect 创建器供开发者使用,开发者无须封装或者简单封装即可使用
  • 灵活: redux-saga可以将多个Saga可以串行/并行组合起来,形成一个非常实用的异步flow
  • 易测试,提供了各种case的测试方案,包括mock task,分支覆盖等等

\

redux-saga缺陷:

\

  • 额外的学习成本: redux-saga不仅在使用难以理解的 generator function,而且有数十个API,学习成本远超redux-thunk,最重要的是你的额外学习成本是只服务于这个库的,与redux-observable不同,redux-observable虽然也有额外学习成本但是背后是rxjs和一整套思想
  • 体积庞大: 体积略大,代码近2000行,min版25KB左右
  • 功能过剩: 实际上并发控制等功能很难用到,但是我们依然需要引入这些代码
  • ts支持不友好: yield无法返回TS类型

\

redux-observable优点:

\

  • 功能最强: 由于背靠rxjs这个强大的响应式编程的库,借助rxjs的操作符,你可以几乎做任何你能想到的异步处理
  • 背靠rxjs: 由于有rxjs的加持,如果你已经学习了rxjs,redux-observable的学习成本并不高,而且随着rxjs的升级redux-observable也会变得更强大

\

redux-observable缺陷:

\

  • 学习成本奇高: 如果你不会rxjs,则需要额外学习两个复杂的库
  • 社区一般: redux-observable的下载量只有redux-saga的1/5,社区也不够活跃,在复杂异步流中间件这个层面redux-saga仍处于领导地位

\

3.19 state 和 props 区别是啥?

\

参考答案

\

props和state是普通的 JS 对象。虽然它们都包含影响渲染输出的信息,但是它们在组件方面的功能是不同的。即

\

  • state 是组件自己管理数据,控制自己的状态,可变;
  • props 是外部传入的数据参数,不可变;
  • 没有state的叫做无状态组件,有state的叫做有状态组件;
  • 多用 props,少用 state,也就是多写无状态组件。

\

3.20 当调用setState时,React render 是如何工作的?

\

参考答案

\

虚拟 DOM 渲染:当render方法被调用时,它返回一个新的组件的虚拟 DOM 结构。当调用setState()时,render会被再次调用,因为默认情况下shouldComponentUpdate总是返回true,所以默认情况下 React 是没有优化的。
原生 DOM 渲染:React 只会在虚拟DOM中修改真实DOM节点,而且修改的次数非常少——这是很棒的React特性,它优化了真实DOM的变化,使React变得更快。

\

context实现通信

\

2、创建组件

\

创建 GrandpaFatherSon1Son2Son3 组件

\

const Grandpa = () => {
  return (
    <>
      <div>我是 Grandpa</div>
      <Father />
    </>
  )
}

const Father = () => {
  return (
    <>
      <div>我是 Father</div>
      <Son1 />
      <Son2 />
    </>
  )
}

const Son1 = () => {
  return (
    <>
      <div>我是Son1</div>
    </>
  )
}

class Son2 extends React.Component {
  render(){
    return <div>我是 Son2</div>
  }
}

const Son3 = () => {
  return (
    <>
      <div>我是 Son3</div>
    </>
  )
}

function App() {
  return (
      <div className="App">
        <Grandpa />
      </div>
  )
}

\

3、创建 context 对象

\

引入 createContext 函数,用于构造context对象

\

import { createContext } from 'react'
const context = createContext()

\

context 中结构出 ProviderConsumer 两个组件

\

const { Provider, Consumer } = context

\

4、注入数据

\

在 App 中使用 Provider 组件进行注入,并把对应数据和方法传递下去

\

function App() {
  const [name, setName] = useState('梁又文')
  const data = {
    name,
    age: '20',
    setName,
  }
  return (
    <Provider value={data}>
      <div className="App">
        <Grandpa />
      </div>
    </Provider>
  )
}

\

5、子孙组件获取数据

\

5.1 函数组件

\

通过 Consumer 获取数据

\

Son1

\

const Son1 = () => {
  return (
    <Consumer>
      {({ name, setName }) => (
        <div>
          我是Son1,我拿到的数据是:{name} <button onClick={() => setName('我是被Son1修改的名字')}>修改名字</button>
        </div>
      )}
    </Consumer>
  )
}

\

5.2 类组件

\

通过 contextType 挂载 context 的属性

\

Son2

\

class Son2 extends Component {
  static contextType = context
  render() {
    const { name, setName } = this.context
    return (
      <>
        我是Son2,我拿到的数据是:{name} <button onClick={() => setName('我是被Son2修改的名字')}>修改名字</button>
      </>
    )
  }
}

\

5.3 useContext 方式

\

通过把 context 对象传入到 useContext 中,返回所有数据。

const Son3 = () => {
  const { name, setName, age } = useContext(context)
  return (
    <div>
      我是Son1,Grandpa今年{age}岁了。我拿到的数据是:{name}
      <button onClick={() => setName('我是被Son1修改的名字')}>修改名字</button>
    </div>
  )
}

\

6、最终代码

import { Component, createContext, useState, useContext } from 'react'
const context = createContext()
const { Provider, Consumer } = context
export { Consumer, context }

const Grandpa = () => {
  return (
    <>
      <div>我是 Grandpa</div>
      <Father />
    </>
  )
}

const Father = () => {
  return (
    <>
      <div>我是 Father</div>
      <Son1 />
      <Son2 />
      <Son3 />
    </>
  )
}

const Son1 = () => {
  return (
    <Consumer>
      {({ name, setName }) => (
        <div>
          我是Son1,我拿到的数据是:{name} <button onClick={() => setName('我是被Son1修改的名字')}>修改名字</button>
        </div>
      )}
    </Consumer>
  )
}

class Son2 extends Component {
  static contextType = context
  render() {
    const { name, setName } = this.context
    return (
      <>
        我是Son2,我拿到的数据是:{name} <button onClick={() => setName('我是被Son2修改的名字')}>修改名字</button>
      </>
    )
  }
}

const Son3 = () => {
  const { name, setName, age } = useContext(context)
  return (
    <div>
      我是Son1,Grandpa今年{age}岁了。我拿到的数据是:{name}
      <button onClick={() => setName('我是被Son1修改的名字')}>修改名字</button>
    </div>
  )
}

function App() {
  const [name, setName] = useState('梁又文')
  const data = {
    name,
    age: '20',
    setName,
  }
  return (
    <Provider value={data}>
      <div className="App">
        <Grandpa />
      </div>
    </Provider>
  )
}

export default App