0%

面经

Class组件和函数组件的区别

Class组件

  • 有组件实例
  • 有生命周期
  • 有state和setState

函数组件

  • 没有组件实例
  • 没有生命周期
  • 没有state和setState,只能接收props
  • 函数组件是一个纯函数,执行完立即销毁,无法存储state

Class组件存在的问题

  • 大型组件很难拆分和重构,变得难以测试
  • 相同业务逻辑分散到各个方法中,可能会变得混乱
  • 复用逻辑可能变得复杂,比如 HOC、Render Props

所以 react 中更提倡函数式编程,因为函数更灵活,更易拆分,但函数组件太简单,所以出现了hook,hook就是用来增强函数组件功能的。

useState为什么不能放到条件语句里面?

react通过单链表来管理hooks。update阶段,hooks函数执行的顺序是不变的,就可以根据这个链表拿到当前hooks对应的Hook对象。如果将useState写在条件判断中,可能会导致顺序错乱,导致当前hooks拿到的不是自己对应的Hook对象。

实现一个Promise.all

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function promiseAll(promises){
return new Promise((resolved, rejected) => {
let resultCount = 0;
let results = new Array(promises.length);

for(let i = 0; i < promises.length; i++){
promises[i].then(value => {
resultCount++
results[i] = value
if(resultCount === promises.length){
return resolved(results)
}
}, error => {
rejected(error)
})
}
})
}

使用Redux的好处,以及和Mobx的区别

Redux的三大优势:

  1. 单一数据源
  2. 状态是只读的
  3. 状态的改变只能通过纯函数改变

Redux和Mobx区别:

  1. Redux将数据保存在单一的store中;而Mobx将数据保存在分散的多个store中
  2. Redux使用简单对象保存数据,需要手动处理变化后的操作;Mobx使用observable保存数据,数据变化后自动处理响应的操作。
  3. Redux使用的是不可变状态,意味着状态只是只读的,不能直接去修改它,而是应该通过纯函数改变返回一个新的状态;Mobx中的状态是可变的,可以直接对其进行修改
  4. Redux比较复杂,因为其中的函数式编程思想掌握起来不是那么容易,同时需要借助一系列的中间件来处理异步和副作用;Mobx相对比较简单,在其中有很多的抽象,使用的更多是面向对象的思维
  5. Redux提供可以进行时间回溯的开发工具,同时其纯函数以及更少的抽象,调试比较容易;Mobx中有更多的抽象和封装,调试起来比较复杂,同时结果也更难以预测

React SSR是怎么实现的?

所谓同构,通俗的讲,就是一套 React 代码在服务器上运行一遍,到达浏览器又运行一遍。 服务端渲染完成页面结构,客户端渲染绑定事件。

  • 服务端执行流程:在服务端使用react-dom/server下的renderToString将React组件转化为string,拼接在html中进行返回。此时html中不包含元素对应的事件。打包时把react-dom下的hydrate的逻辑打包到js中,拼接在html中作为script标签返回,提供给客户端运行使用
  • 浏览器执行流程:请求html,渲染html返回的页面内容并下载js文件,此时页面显示元素但不可交互,运行js中的ReactDom.hydrate给页面元素绑定事件,页面可交互。

有用过代码规范相关的吗?Eslint 和 Prettier 冲突怎么解决?

https://www.jianshu.com/p/b3a693cdcee9

实现一个数组转树形结构的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const data = [
{ id: 1, text: 't1', parentId: 0 },
{ id: 11, text: 't11', parentId: 1 },
{ id: 12, text: 't12', parentId: 1 },
{ id: 2, text: 't2', parentId: 0 },
{ id: 21, text: 't21', parentId: 2 },
{ id: 3, text: 't3', parentId: 0 }
]

function arrToTree(data, id, parentId, children){
let cloneData = JSON.parse(JSON.stringify(data))
return cloneData.filter(father => {
let newArr = cloneData.filter(child => {
return father[id] === child[parentId]
})
father[children] = newArr
return father[parentId] === 0
})
}

const treeData = arrToTree(data, 'id', 'parentId', 'children')

React性能优化

  • 使用React.Memo来缓存组件

    提升应用程序性能的一种方法是实现memoization。Memoization是一种优化技术,主要通过存储昂贵的函数调用的结果,并在再次发生相同的输入时返回缓存的结果,以此来加速程序。父组件的每次状态更新,都会导致子组件重新渲染,即使传入子组件的状态没有变化,为了减少重复渲染,我们可以使用React.memo来缓存组件,这样只有当传入组件的状态值发生变化时才会重新渲染。如果传入相同的值,则返回缓存的组件。示例如下:

    1
    2
    3
    4
    5
    export default React.memo((props) => {
    return (
    <div>{props.value}</div>
    )
    });
  • 使用useMemo缓存大量的计算

    有时渲染是不可避免的,但如果您的组件是一个功能组件,重新渲染会导致每次都调用大型计算函数,这是非常消耗性能的,我们可以使用新的useMemo钩子来“记忆”这个计算函数的计算结果。这样只有传入的参数发生变化后,该计算函数才会重新调用计算新的结果。通过这种方式,您可以使用从先前渲染计算的结果来挽救昂贵的计算耗时。总体目标是减少JavaScript在呈现组件期间必须执行的工作量,以便主线程被阻塞的时间更短。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 避免这样做
    function Component(props) {
    const someProp = heavyCalculation(props.item);
    return <AnotherComponent someProp={someProp} />
    }

    // 只有 `props.item` 改变时someProp的值才会被重新计算
    function Component(props) {
    const someProp = useMemo(() => heavyCalculation(props.item), [props.item]);
    return <AnotherComponent someProp={someProp} />
    }
  • 避免使用内联对象

    使用内联对象时,react会在每次渲染时重新创建对此对象的引用,这会导致接收此对象的组件将其视为不同的对象,因此,该组件对于prop的浅层比较始终返回false,导致组件一直重新渲染。许多人使用的内联样式的间接引用,就会使组件重新渲染,可能会导致性能问题。为了解决这个问题,我们可以保证该对象只初始化一次,指向相同引用。另外一种情况是传递一个对象,同样会在渲染时创建不同的引用,也有可能导致性能问题,我们可以利用ES6扩展运算符将传递的对象解构。这样组件接收到的便是基本类型的props,组件通过浅层比较发现接受的prop没有变化,则不会重新渲染。示例如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // Don't do this!
    function Component(props) {
    const aProp = { someProp: 'someValue' }
    return <AnotherComponent style={{ margin: 0 }} aProp={aProp} />
    }

    // Do this instead :)
    const styles = { margin: 0 };
    function Component(props) {
    const aProp = { someProp: 'someValue' }
    return <AnotherComponent style={styles} {...aProp} />
    }
  • 避免使用匿名函数

    虽然匿名函数是传递函数的好方法(特别是需要用另一个prop作为参数调用的函数),但它们在每次渲染上都有不同的引用。这类似于上面描述的内联对象。为了保持对作为prop传递给React组件的函数的相同引用,您可以将其声明为类方法(如果您使用的是基于类的组件)或使用useCallback钩子来帮助您保持相同的引用(如果您使用功能组件)。当然,有时内联匿名函数是最简单的方法,实际上并不会导致应用程序出现性能问题。这可能是因为在一个非常“轻量级”的组件上使用它,或者因为父组件实际上必须在每次props更改时重新渲染其所有内容。因此不用关心该函数是否是不同的引用,因为无论如何,组件都会重新渲染。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // 避免这样做
    function Component(props) {
    return <AnotherComponent onChange={() => props.callback(props.id)} />
    }

    // 优化方法一
    function Component(props) {
    const handleChange = useCallback(() => props.callback(props.id), [props.id]);
    return <AnotherComponent onChange={handleChange} />
    }

    // 优化方法二
    class Component extends React.Component {
    handleChange = () => {
    this.props.callback(this.props.id)
    }
    render() {
    return <AnotherComponent onChange={this.handleChange} />
    }
    }
  • 延迟加载不是立即需要的组件

    延迟加载实际上不可见(或不是立即需要)的组件,React加载的组件越少,加载组件的速度就越快。因此,如果您的初始渲染感觉相当粗糙,则可以在初始安装完成后通过在需要时加载组件来减少加载的组件数量。同时,这将允许用户更快地加载您的平台/应用程序。最后,通过拆分初始渲染,您将JS工作负载拆分为较小的任务,这将为您的页面提供响应的时间。这可以使用新的React.Lazy和React.Suspense轻松完成。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 延迟加载不是立即需要的组件
    const MUITooltip = React.lazy(() => import('@material-ui/core/Tooltip'));
    function Tooltip({ children, title }) {
    return (
    <React.Suspense fallback={children}>
    <MUITooltip title={title}>
    {children}
    </MUITooltip>
    </React.Suspense>
    );
    }

    function Component(props) {
    return (
    <Tooltip title={props.title}>
    <AnotherComponent />
    </Tooltip>
    )
    }
  • 调整CSS而不是强制组件加载和卸载

    渲染成本很高,尤其是在需要更改DOM时。每当你有某种手风琴或标签功能,例如想要一次只能看到一个项目时,你可能想要卸载不可见的组件,并在它变得可见时将其重新加载。如果加载/卸载的组件“很重”,则此操作可能非常消耗性能并可能导致延迟。在这些情况下,最好通过CSS隐藏它,同时将内容保存到DOM。尽管这种方法并不是万能的,因为安装这些组件可能会导致问题(即组件与窗口上的无限分页竞争),但我们应该选择在不是这种情况下使用调整CSS的方法。另外一点,将不透明度调整为0对浏览器的成本消耗几乎为0(因为它不会导致重排),并且应尽可能优先于更该visibility 和 display。有时在保持组件加载的同时通过CSS隐藏可能是有益的,而不是通过卸载来隐藏。对于具有显著的加载/卸载时序的重型组件而言,这是有效的性能优化手段。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 避免对大型的组件频繁对加载和卸载
    function Component(props) {
    const [view, setView] = useState('view1');
    return view === 'view1' ? <SomeComponent /> : <AnotherComponent />
    }

    // 使用该方式提升性能和速度
    const visibleStyles = { opacity: 1 };
    const hiddenStyles = { opacity: 0 };
    function Component(props) {
    const [view, setView] = useState('view1');
    return (
    <React.Fragment>
    <SomeComponent style={view === 'view1' ? visibleStyles : hiddenStyles}>
    <AnotherComponent style={view !== 'view1' ? visibleStyles : hiddenStyles}>
    </React.Fragment>
    )
    }
  • 使用React.Fragment避免添加额外的DOM

  • 虚拟化长列表

    react-window 或 react-virtualized

  • 不可变数据

    Immer 或 immutability-helper

  • 使用生产版本

实现一个深拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function deepClone(obj){
function isObject(o){
return (typeof o === 'object' || typeof o === 'function') && o !== null
}

if(!isObject(obj)){
throw new Error('非对象')
}

let isArray = Array.isArray(obj)
let newObj = isArray ? [...obj] : {...obj}
Reflect.ownKeys(newObj).forEach(key => {
newObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key]
})

return newObj
}

微前端是怎么实现的?怎么独立部署?子应用通信怎么做?

Webpack构建流程

  1. 初始化:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler
  2. 编译:从 Entry 出发,针对每个 Module 串行调用对应的 Loader 去翻译文件的内容,再找到该 Module 依赖的 Module,递归地进行编译处理
  3. 输出:将编译后的 Module 组合成 Chunk,将 Chunk 转换成文件,输出到文件系统中

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

Loader 和 Plugin 的原理和区别

  • Loader用于对模块文件进行编译转换和加载处理,在module.rules数组中进行配置,它用于告诉Webpack在遇到哪些文件时使用哪些Loader去加载和转换。
  • Plugin用于扩展Webpack功能,实现原理是在构建流程里注入钩子函数,在合适的时机通过webpack提供的api改变输出结果。在plugins数组中进行配置。

webpack 怎么做分包?

webpack 性能优化

react diff的复杂度,以及react diff的原理?

react hooks的优缺点?

从输入url到页面渲染经过了哪些步骤?

知道BFC吗?使用场景有哪些?

BFC 即 Block Formatting Contexts (块级格式化上下文)。
具有 BFC 特性的元素可以看作是隔离了的独立容器,容器里面的元素不会在布局上影响到外面的元素,并且 BFC 具有普通容器所没有的一些特性。通俗一点来讲,可以把 BFC 理解为一个封闭的大箱子,箱子内部的元素无论如何翻江倒海,都不会影响到外部。

怎么判断是否是数组?

用instanceof判断

1
2
const a = [];
console.log(a instanceof Array); //true

用Object的toString方法判断

1
2
3
const a = [];
Object.prototype.toString.call(a); // "[object Array]"
Object.prototype.toString.apply(a); // "[object Array]"

用Array对象的isArray方法判断

1
2
const a = [];
Array.isArray(a); //true

页面卡顿怎么去定位?

数组有10万个数据,取第一个和取第10万个的耗时多久?

工作中遇到最难的问题?

防抖和节流

防抖定义

防抖就是要延迟执行,你一直操作触发事件一直不执行,当你停止操作等待多少秒后才执行。

也就是说不管事件触发频率有多高,一定在事件触发 n 秒后执行。如果在事件触发的 n 秒又触发了这个事件,那就以新事件的事件为准,n 秒后才执行。总之,要等你触发完事件 n 秒内不再触发事件,它才执行。

手写防抖

根据定义,我们知道要在时间 n 秒后执行,那么我们就用定时器来实现:

1
2
3
4
5
6
7
8
9
function debounce(func, wait) {
let timer = null;
return function (...args){
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args)
}, wait)
}
}

代码很简单,即当还在触发事件时,就清除 timer,使其在 n 秒后执行,但此写法首次不会立即执行,为其健壮性,需加上判断是否第一次执行的第三个参数 flag,判断其是否立即执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
function debounce(event, wait, flag) {
let timer = null;
return function (...args) {
clearTimeout(timer)
if (!timer && flag) {
event.apply(this, args)
} else {
timer = setTimeout(() => {
event.apply(this, args)
}, wait)
}
}
}

防抖场景

窗口大小变化,调整样式

1
window.addEventListener('resize', debounce(handleResize, 200))

搜索框,输入后300毫秒搜索

1
debounce(fetchSelectData, 300)

表单验证,输入 1000 毫秒后验证

1
debounce(validator, 1000)

节流定义
顾名思义,一节一节的流,就好似控制水阀,在事件不断触发的过程中,固定时间内执行一次事件。

手写节流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function throttle(func, wait) {
let pre = 0, timer = null;
return function (...args) {
if (new Date() - pre > wait) {
clearTimeout(timer);
timer = null;
pre = new Date();
func.apply(this, args)
} else {
timer = setTimeout(() => {
func.apply(this, args)
}, wait)
}
}
}

节流场景

scroll 滚动

1
window.addEventListener('scroll', throttle(handleScroll, 200))

input 动态搜索

1
throttle(fetchInput, 300)

http2的相关特性?

  • 二进制分帧(HTTP/2 将请求和响应数据分割为更小的帧,并且它们采用二进制编码)
  • 多路复用
  • 服务器推送
  • 头部压缩

viewport和移动端布局方案?

实现一个compose函数

1
2
3
4
5
6
7
8
9
function compose(...fns) { // fns是传入的函数
const fn = fns.pop();
return (...args) => {
fn(...args);
if (fns.length > 0) {
compose(...fns);
}
};
}

React Fiber?

React内部实现的一套状态更新机制。支持任务不同优先级,可中断与恢复,并且恢复后可以复用之前的中间状态。

怎么优化h5加载速度?怎么实现h5页面秒开?

https://segmentfault.com/a/1190000041701111

js bridge通信原理?

useReducer比redux好在哪里?

HTTP 和 HTTPS 的区别

https://www.51cto.com/article/701195.html

HTTP 常见的状态码

1 表示消息
2 表示成功
3 表示重定向
4 表示请求错误
5 表示服务器错误

1xx(代表请求已被接受,需要继续处理。这类响应是临时响应,只包含状态行和某些可选的响应头信息,并以空行结束)

  • 100(客户端继续发送请求,这是临时响应):这个临时响应是用来通知客户端它的部分请求已经被服务器接收,且仍未被拒绝。客户端应当继续发送请求的剩余部分,或者如果请求已经完成,忽略这个响应。服务器必须在请求完成后向客户端发送一个最终响应
  • 101:服务器根据客户端的请求切换协议,主要用于websocket或http2升级

2xx(代表请求已成功被服务器接收、理解、并接受)

  • 200(成功):请求已成功,请求所希望的响应头或数据体将随此响应返回
  • 204(无内容):服务器成功处理请求,但没有返回任何内容

3xx(表示要完成请求,需要进一步操作。 通常,这些状态代码用来重定向)

  • 301(永久移动):请求的网页已永久移动到新位置。 服务器返回此响应(对 GET 或 HEAD 请求的响应)时,会自动将请求者转到新位置
  • 302(临时移动): 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求
  • 304:协商缓存,告诉客户端有缓存,直接使用缓存中的数据,返回页面的只有头部信息,是没有内容部分

4xx(代表了客户端看起来可能发生了错误,妨碍了服务器的处理)

  • 400(错误请求): 服务器不理解请求的语法
  • 401(未授权): 请求要求身份验证。 对于需要登录的网页,服务器可能返回此响应
  • 403(禁止): 服务器拒绝请求
  • 404(未找到): 服务器找不到请求的网页
  • 405(方法禁用): 禁用请求中指定的方法
  • 408(请求超时): 服务器等候请求时发生超时

5xx(表示服务器无法完成明显有效的请求。这类状态码代表了服务器在处理请求的过程中有错误或者异常状态发生)

  • 500(服务器内部错误):服务器遇到错误,无法完成请求
  • 501(尚未实施):服务器不具备完成请求的功能。 例如,服务器无法识别请求方法时可能会返回此代码
  • 502(错误网关): 服务器作为网关或代理,从上游服务器收到无效响应
  • 503(服务不可用): 服务器目前无法使用(由于超载或停机维护)
  • 504(网关超时): 服务器作为网关或代理,但是没有及时从上游服务器收到请求