开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情
一起来看看这周前端21问你都会了嘛?😂😂😂
1. 事件循环机制你知道吗?
下面回答的思维导图如下,先预览一下思维导图
-
浏览器中的 Event Loop
如下图,在浏览器中js执行代码的顺序就是按照左边这个执行栈的顺序来执行,Event Loop会帮我们把不同的代码(任务)按照预先设定好的机制放入执行栈中。 那预先设定的机制是什么样的呢?代码按什么先后顺序放进去呢?
宏任务包括
script
、setTimeout
、setInterval
、setImmediate
、I/O
及UI rendering
微任务包括
process.nextTick
、queueMicrotask
、promise.then
、MutationObserver
执行机制
- 首先执行宏任务(如script中的console.log)
- 然后执行该宏任务产生的微任务,若微任务在执行过程中产生了新的微任务,则继续执行微任务
- 微任务执行完毕后,再回到宏任务中进行下一轮循环(
setTimeout
中的回调函数)
搞懂这段代码的输出顺序,看看掌握情况
console.log('script start') async function async1() { await async2() console.log('async1 end') } async function async2() { console.log('async2 end') } async1() setTimeout(function() { console.log('setTimeout') }, 0) new Promise(resolve => { console.log('Promise') resolve() }) .then(function() { console.log('promise1') }) console.log('script end') // script start => async2 end => Promise => script end // => async1 end => promise1 => setTimeout
注意:
新版的chrome浏览器优化了,await变得更快了,具体看下面的推荐文章。
-
Node 中的 Event Loop
Node 的 Event Loop 分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。
Node 的事件循环的执行顺序为
输入数据阶段(incoming data)->轮询阶段(poll)->检查阶段(check)->关闭事件回调阶段(close callback)->定时器检测阶段(timers)->I/O事件回调阶段(I/O callbacks)->闲置阶段(idle, prepare)->轮询阶段...
六个阶段
- 定时器检测阶段(timers):本阶段执行 timer 的回调,即 setTimeout、setInterval 里面的回调函数。
- I/O事件回调阶段(I/O callbacks):执行延迟到下一个循环迭代的 I/O 回调,即上一轮循环中未被执行的一些I/O回调。
- 闲置阶段(idle, prepare):仅系统内部使用。
- 轮询阶段(poll):检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞。
- 检查阶段(check):setImmediate() 回调函数在这里执行
- 关闭事件回调阶段(close callback):一些关闭的回调函数,如:socket.on('close', ...)
特殊的process.nextTick
这个函数其实是独立于 Event Loop 之外的,它有一个自己的队列,当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行。
看一个例子:
setImmediate(() => { console.log('timeout1') Promise.resolve().then(() => console.log('promise resolve')) process.nextTick(() => console.log('next tick1')) }); setImmediate(() => { console.log('timeout2') process.nextTick(() => console.log('next tick2')) }); setImmediate(() => console.log('timeout3')); setImmediate(() => console.log('timeout4'));
在 node11 之前,因为每一个 eventLoop 阶段完成后会去检查 nextTick 队列,如果里面有任务,会让这部分任务优先于微任务执行,因此上述代码是先进入 check 阶段,执行所有 setImmediate,完成之后执行 nextTick 队列,最后执行微任务队列,因此输出为
timeout1=>timeout2=>timeout3=>timeout4=>next tick1=>next tick2=>promise resolve
在 node11 之后,process.nextTick 是微任务的一种,因此上述代码是先进入 check 阶段,执行一个 setImmediate 宏任务,然后执行其微任务队列,再执行下一个宏任务及其微任务,因此输出为
timeout1=>next tick1=>promise resolve=>timeout2=>next tick2=>timeout3=>timeout4
2. 防抖和节流你清楚吗?场景?手写?
这张图可以帮大家快速记忆,下面也有具体文字供大家阅读😀
-
函数防抖(debounce):在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。
场景:输入框输入文字触发请求、调整浏览器窗口大小时resize、文本编辑器实时保存
实现:
function debounce(fn, wait) { let timer; return function () { let _this = this; let args = arguments; if (timer) { clearTimeout(timer); } timer = setTimeout(function () { fn.apply(_this, args); }, wait); }; }
特点:防抖重在清零
clearTimeout(timer)
-
函数节流(throttle):规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。
场景:输入框输入文字触发请求、浏览器播放事件每隔一秒计算一次进度信息、
scroll
事件每隔一秒计算一次位置信息等实现:
function thorttle2(fn, wait) { let timer; return function () { let _this = this; let args = arguments; if (!timer) { timer = setTimeout(function () { timer = null; fn.apply(_this, args); }, wait); } }; }
特点:节流重在加锁
timer=timeout
时将timer = null
3. React有哪些hooks及使用限制是什么?
十种 hooks
思维导图预览
- 10种hooks详细梳理
-
useState
,纯函数组件没有状态,用于为函数组件引入state状态, 并进行状态数据的读写操作。用法:
const [state, setState] = useState(initialState);
setState()2种写法:
- setState(newValue): 参数为非函数值,直接指定新的状态值,内部用其覆盖原来的状态值
- setState(value => newValue): 参数为函数,接收原本的状态值,返回新的状态值,内部用其覆盖原来的状态值
-
useEffect
,来更好的执行副作用操作,如异步请求、设置订阅 / 启动定时器、手动更改真实DOM等,在类组件中会把请求放在componentDidMount里面,在函数组件中可以使用useEffect()。用于模拟类组件中的生命周期钩子,可以把 useEffect Hook 看做如下三个函数的组合 :componentDidMount()、componentDidUpdate()、componentWillUnmount()。用法:
useEffect(() => { // 在此可以执行任何带副作用操作 return () => { // 在组件卸载前执行 // 在此做一些收尾工作, 比如清除定时器/取消订阅等 } }, [stateValue]) // 如果指定的是[], 回调函数只会在第一次render()后执行
参数、返回值说明:
- useEffect()接受两个参数,第一个参数是要进行的异步操作,第二个参数是一个数组,用来给出Effect的依赖项,只要这个数组发生变化,useEffect()就会执行。
- 当第二项省略不填时。useEffect()会在每次组件渲染时都会执行useEffect,只要更新就会执行。
- 当第二项传 空数组[ ] 时,只会在组件挂载后运行一次。
- useEffect()返回值可以是一个函数,在组件销毁的时候会被调用。清理这些副作用可以进行如取消订阅、清除定时器操作,类似于componentWillUnmount。
-
useContext
,作用就是可以做状态的分发,在React16.X以后支持,避免了react逐层通过Props传递数据。用法:
const value = useContext(MyContext); MyContext为`React.createContext` 的返回值
注意:
useContext(MyContext)
只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用<MyContext.Provider>
来为下层组件提供 context。 -
useReducer
,在使用React的过程中,如遇到状态管理,一般会用到Redux。而React本身是不提供状态管理的。而useReducer() 提供了状态管理。但useReducer无法提供中间件等功能,假如有这些需求,还是需要用到redux。 用法:const initialState = {count: 0}; function reducer(state, action) { switch (action.type) { case 'increment': return {count: state.count + 1}; case 'decrement': return {count: state.count - 1}; default: throw new Error(); } } function Counter() { const [state, dispatch] = useReducer(reducer, initialState); return ( <> Count: {state.count} <button onClick={() => dispatch({type: 'decrement'})}>-</button> <button onClick={() => dispatch({type: 'increment'})}>+</button> </> ); }
-
useCallback
,主要是为了性能的优化,useCallback(fn, deps) 相当于 useMemo(() => fn, deps)用法:
const memoizedCallback = useCallback( () => { doSomething(a, b); }, [a, b], );
返回一个 memoized 回调函数。
-
useMemo
,主要用来解决使用React hooks产生的无用渲染的性能问题。用法:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
返回一个 memoized 值。
原理:useMemo 会记录上一次执行 create 的返回值,并把它绑定在函数组件对应的 fiber 对象上,只要组件不销毁,缓存值就一直存在,但是 deps 中如果有一项改变,就会重新执行 create ,返回值作为新的值记录到 fiber 对象上。
场景:
- 可以缓存 element 对象,从而达到按条件渲染组件,优化性能的作用。
- 如果组件中不期望每次 render 都重新计算一些值,可以利用 useMemo 把它缓存起来。
- 可以把函数和属性缓存起来,作为 PureComponent 的绑定方法,或者配合其他Hooks一起使用
useRef
,可以在函数组件中存储、查找组件内的标签或任意其它数据,useRef返回一个可变的ref对象,useRef接受一个参数绑定在返回的ref对象的current属性上,返回的ref对象在整个生命周期中保持不变。作用:保存标签对象,功能与React.createRef()一样。useImperativeHandle
,可以在使用 ref 时自定义暴露给父组件的实例值。 这个Hook允许在子组件中把自定义实例附加到父组件传过来的ref上,有利于父组件控制子组件。useLayoutEffect
,和useEffect相同,都是用来执行副作用,但是它会在所有的DOM变更之后同步调用effect。useLayoutEffect和useEffect最大的区别就是一个是同步,一个是异步。useLayoutEffect主要用来读取DOM布局并触发同步渲染,在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。useDebugValue
,可用于在 React 开发者工具中显示自定义 hooks 的标签。
-
- 使用限制
- 只能在函数最外层调用 hooks。不要在循环、条件判断或者子函数中调用。
- 只能在 React 的函数组件中调用 hooks。不要在其他 JavaScript 函数中调用。
4. React如何自定义hooks?
-
自定义hooks解决了什么问题? 👉👉在 React 中有两种流行的方式来共享组件之间的状态逻辑: render props 和高阶组件,自定义hooks让我们不增加组件的情况下解决相同问题的。
-
如何自定义hooks?👉👉自定义 hooks 是一个函数,其名称以 “
use
” 开头,函数内部可以调用其他的 hooks。例子:
import { useState, useEffect } from 'react'; function useFriendStatus(friendID) { const [isOnline, setIsOnline] = useState(null); useEffect(() => { function handleStatusChange(status) { setIsOnline(status.isOnline); } ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange); }; }); return isOnline; }
function FriendStatus(props) { const isOnline = useFriendStatus(props.friend.id); if (isOnline === null) { return 'Loading...'; } return isOnline ? 'Online' : 'Offline'; }
5. React 16.8.0之后推荐函数式组件,原因是什么?
6. 深浅拷贝了解吗?
-
因为我们了解对象类型在赋值的过程中其实是复制了地址,从而会导致改变了一方时其它也都被改变的情况,在开发中我们不希望出现这样的问题,所以我们可以使用浅拷贝来解决这个情况。
-
浅拷贝会拷贝对象第一层属性值到新的对象中,即只能解决一层的问题(对象里的属性值全是简单数据类型),实现方法有:
Object.assign
、运算符...
、手写for循环遍历复制 -
深拷贝会拷贝对象所有属性值到新的对象中,即可以解决两层以上的问题(对象里面嵌套对象,再嵌套...),实现方法有:
JSON.parse(JSON.stringify(object))
、手写实现function clone(target, map = new Map()) { if (typeof target === 'object') { let cloneTarget = Array.isArray(target) ? [] : {}; if (map.get(target)) { return target; } map.set(target, cloneTarget); for (const key in target) { cloneTarget[key] = clone(target[key], map); } return cloneTarget; } else { return target; } };
7. 哪些数组方法会改变数组,哪些不会?
下图仅列出大家容易辨认错的数组方法,还有部分数组方法都是查找类方法未列出,具体请参考MDN Array
8. 三栏布局,你有几种实现方法?
9. 箭头函数和普通函数的区别?可以用bind重定向箭头函数的指针吗?
- 语法更加简洁、清晰,声明方式不同,箭头函数只能声明成匿名函数
this
指向不同,普通函数来说,内部的this
指向函数运行时所在的对象,箭头函数没有自己的this
对象,内部的this
就是定义时上层作用域中的this
- 箭头函数的
this
永远不会变,call、apply、bind
也无法改变 - 箭头函数没有原型
prototype
- 箭头函数不能当成一个构造函数
- 箭头函数没有自己的
arguments
,没有new.target
10. async await 怎么捕获await结果的异常?
- 使用
.catch
链式调用(使得异常处理变得非常简洁) - 使用
Go
的语法(使用if(err)
来处理异常,代码极度重复,每一个地方都少不了if (err != null)
,函数中的非异步的错误也无法处理) - 使用
try/catch
(多个promise
无法判断是哪个的异常,无法继续链式调用)
11. script标签放在 header 中和放在 body 底部有什么区别
因为将script标签放在 header 中脚本会阻塞页面的渲染,所以推荐将其放在 body 底部,当解析到 script 标签时,通常页面的大部分内容都已经渲染完成,让用户马上能看到一个非空白页面。
12. CSS有哪几种定位?
13. BFC是什么?如何触发BFC?应用场景有什么?
14. 盒模型介绍一下?
- 盒模型: 盒模型又称框模型(Box Model),包含了元素内容(content)、内边距(padding)、边框(border)、外边距(margin)几个要素。如图:
-
盒模型又分为标准模型和IE模型,标准模型和IE模型的区别是元素内容(content)计算方式的不同
标准模型
标准模型元素宽度width=content,高度计算相同,如图:
IE模型
IE模型元素宽度width=content+padding,高度计算相同
-
css如何设置这两种模型的宽和高呢?以及javascript如何设置获取盒模型对应的宽和高呢?
通过css3新增的属性
box-sizing: content-box | border-box
分别设置盒模型为标准模型(content-box
)和IE模型(border-box
)。javascript获取盒模型对应的宽和高:
dom.style.width/height
只能取到行内样式的宽和高,style标签中和link外链的样式取不到。dom.currentStyle.width/height
取到的是最终渲染后的宽和高,只有IE支持此属性。window.getComputedStyle(dom).width/height
取到的是最终渲染后的宽和高,但是多浏览器支持,IE9以上支持。dom.getBoundingClientRect().width/height
也是得到渲染后的宽和高,大多浏览器支持。IE9以上支持,除此外还可以取到相对于视窗的上下左右的距离
15. null和undefined有什么区别?
null:特指对象的值未设置。
undefined:一个声明未定义的变量的初始值,或没有实际参数的形式参数。
所以,在实际使用过程中,为了保证变量所代表的语义,不要对一个变量显式的赋值 undefined,当需要释放一个对象时,直接赋值为 null 即可。16. 数组有哪些遍历方法?它们的性能对比如何?
遍历方法
for循环
for in
:以任意顺序迭代一个对象的除Symbol以外的可枚举属性,包括继承的可枚举属性。for of
:在可迭代对象(包括 Array,Map,Set,String,TypedArray,arguments 对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句。forEach
:对数组的每个元素执行一次给定的函数。map
:创建一个新数组,这个新数组由原数组中的每个元素都调用一次提供的函数后的返回值组成。reduce
:对数组中的每个元素按序执行一个由您提供的 reducer 函数,每一次运行 reducer 会将先前元素的计算结果作为参数传入,最后将其结果汇总为单个返回值。filter
:创建给定数组一部分的浅拷贝,其包含通过所提供函数实现的测试的所有元素。every
:测试一个数组内的所有元素是否都能通过某个指定函数的测试。它返回一个布尔值。some
:测试数组中是不是至少有 1 个元素通过了被提供的函数测试。它返回的是一个 Boolean 类型的值。
性能对比
for
> for-of
> forEach
> filter
> map
> for-in
17. call apply bind的异同?
-
call:使用一个指定的
this
值和单独给出的一个或多个参数来调用一个函数。 语法function.call(thisArg, arg1, arg2, ...)
-
apply:调用一个具有给定
this
值的函数,以及以一个数组(或一个类数组对象)的形式提供的参数。语法apply(thisArg)
、apply(thisArg, argsArray)
-
bind:创建一个新的函数,在
bind()
被调用时,这个新函数的this
被指定为bind()
的第一个参数,而其余参数将作为新函数的参数,供调用时使用。语法function.bind(thisArg[, arg1[, arg2[, ...]]])
总结
call/apply/bind
都是用来改变函数执行上下文的。call/apply
直接执行函数,call
的参数为this+参数列表,apply
的参数为this+数组。bind
返回函数,bind
的参数为this+参数列表。
18. 如何隐藏一个dom元素?
19. js数据类型,如何判断
-
typeof
typeof
对于原始类型来说,除了null
都可以显示正确的类型,如果你想判断null
的话可以使用variable === null
。typeof 1 // 'number' typeof '1' // 'string' typeof undefined // 'undefined' typeof true // 'boolean' typeof Symbol() // 'symbol' typeof 1n // bigint typeof null // 'object' ❌
typeof
对于对象来说,除了函数都会显示object
,所以说typeof
并不能准确判断变量到底是什么类型。typeof {} // 'object' typeof console.log // 'function' typeof [] // 'object' ❌
-
instanceof
instanceof
通过原型链的方式来判断是否为构建函数的实例,常用于判断具体的对象类型,对于原始类型来说,你想直接通过instanceof
来判断类型是不行的。[] instanceof Array // true ({}) instanceof Object // true 加个()防止编译器将{}识别为空代码报错 console.log instanceof Function // true const Person = function() {} const p1 = new Person() p1 instanceof Person // true var str = 'hello world' str instanceof String // false var str1 = new String('hello world') str1 instanceof String // true
-
Object.prototype.toString.call
20. Map、Set、Object区别是什么?WeakMap与WeakSet呢?
-
Map:
Map
对象是键值对的集合。Map
中的一个键只能出现一次,它在Map
的集合中是独一无二的。Map
对象按键值对迭代——一个for...of
循环在每次迭代后会返回一个形式为[key,value]
的数组。迭代按插入顺序进行,即键值对按set()
方法首次插入到集合中的顺序(也就是说,当调用set()
时,map 中没有具有相同值的键)进行迭代。常见操作:
-
Set:
Set
对象是值的集合,你可以按照插入的顺序迭代它的元素。Set 中的元素只会出现一次,即 Set 中的元素是唯一的。常见操作:
-
Object 是 JavaScript 的一种 数据类型 。它用于存储各种键值集合和更复杂的实体。Objects 可以通过
Object()
构造函数或者使用 对象字面量 的方式创建Object与Map的区别:
-
WeakMap:
WeakMap
对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。 -
WeakSet:
WeakSet
对象是一些对象值的集合。且其与Set
类似,WeakSet
中的每个对象值都只能出现一次。在WeakSet
的集合中,所有对象都是唯一的。常见操作:
21. 垂直居中定位有几种办法实现?你只会3种?
结尾
这些回答都是笔者通过阅读博客以及技术书籍,总结得出的回答。其中难免会有回答的不对或者有遗漏的地方。欢迎各位掘金友友在评论区发表你们的回答,一起来将每一问完善成更好的回答~
这是我最近搭建部署的博客--增肌超人的博客,本文以及其他文章都收录在其中,在博客中应该还会分享一些在看的书籍以及健身经验,欢迎来看看呀😁😁😁