写点什么

一道 React 面试题把我整懵了

作者:beifeng1996
  • 2022 年 9 月 30 日
    浙江
  • 本文字数:13539 字

    阅读完需:约 44 分钟

提问:react 项目中的 JSX 里,onChange={this.func.bind(this)}的写法,为什么要比非 bind 的 func = () => {}的写法效率高?


声明: 由于本人水平有限,有考虑不周之处,或者出现错误的,请严格指出,小弟感激不尽。这是小弟第一篇文章,有啥潜规则不懂的,你们就告诉我。小弟明天有分享,等分享完了之后,继续完善。


之前不经意间看到这道题,据说是阿里 p5-p6 级别的题目,我们先看一下这道题目,明面上是考察对 react 的了解深度,实际上涉及的考点很多:bind,arrow function,react 各种绑定 this 的方法,优缺点,适合的场景,类的继承,原型链等等,所以综合性很强。


我们今天的主题就是由此题目,来总结一下相关的知识点,这里我会着重分析题目中第二种绑定方案

五种 this 绑定方案的差异性

方案一: React.createClass

这是老版本 React 中用来声明组件的方式,在那个版本,没有引入 class 这种概念,所以通过这种方式来创建一个组件类(constructor)ES6 的 class 相比 createClass,移除了两点:一个是 mixin 一个是 this 的自动绑定。前者可以用 HOC 替代,后者则是完完全全的没有,原因是 FB 认为这样可以避免和 JS 的语法产生混淆,所以去掉了。使用这种方法,我们不需要担心 this,它会自动绑定到组件实例身上,但是这个 API 已经废弃了,所以只需要了解。


const App = React.createClass({  handleClick() {    console.log(this)  },  render() {    return <div onClick={this.handleClick}>你好</div>  }})
复制代码


更多面试题解答参见 前端react面试题详细解答

方案二:在 render 函数中使用 bind

class Test extends Component {  handleClick() {    console.log(this)  }  render() {    return <div onClick={this.handleClick.bind(this)}></div>  }}
复制代码

方案三:在 render 函数中使用箭头函数

class Test extends Component {
handleClick() { console.log(this) } render() { return <div onClick={() => this.handleClick()}></div> }}
复制代码


这两个方案简洁明了,可以传参,但是也存在潜在的性能问题: 会引起不必要的渲染


我们常常会在代码中看到这些场景: 更多演示案例请点击


class Test extends Component {  render() {    return <div>      <Input />      <button>添加<button>      <List options={this.state.options || Immutable.Map()} data={this.state.data} onSelect={this.onSelect.bind(this)} /> // 1 pureComponent    </div>  }}
复制代码


场景一:使用空对象/数组来做兜底方案,避免 options 没有数据时运行时报错。场景二:使用箭头函数来绑定 this。


可能在一些不需要关心性能的场景下这两种写法没有什么太大的坏处,但是如果我们正在考虑性能优化,譬如我们使用了PureComponent来去优化我们的渲染性能这里面 React 有使用 shallowEqual 做第一层的比较,这个时候我们关注的可能是这个 data(数据是否有变化从而影响渲染),然而被我们忽视的 options,onSelect 却会直接导致 PureComponent 失效,然而我们找不到优化失败的原因。


而假设我们的核心 data 是Immutable的,这样其实优化了我们做 diff 相关的性能。当 data 为 null 时,此时我们期望的是不会重复渲染,然而当我们的 Test 组件有状态更新,触发了 Test 的重新渲染,此时 render 执行,List 依旧会重新渲染。原因就是我们每次执行render,传递给子组件的options,onSelect是一个新的对象/函数。这样在做 shallowEqual 时,会认为有更新,所以会更新 List 组件。


这个地方也有很多解决方案:


  1. 不要直接在 render 函数里面做兜底,或者使用同一引用的数据源

  2. 对于事件监听函数,我们可以事先做好绑定,使用方案 4 或者 5,或者最新的 hook(useCallback、useMemo)


const onSelect = useCallback(() => {  ... //和select相关的逻辑}, []) // 第二个参数是相关的依赖,只有依赖变了,onSelect才会变,设置为空数组,表示永远不变
复制代码

方案四:在构造函数中使用 bind

class Test extends Component {  constrcutor() {    this.handleClick = this.handleClick.bind(this)  }
handleClick() { console.log(this) }
render() { return <Button onClick={this.handleClick}>测试</Button> }}
复制代码


这种方案是 React 推荐的方式,只在实例化组件的时候做一次绑定,之后传递的都是同一引用,没有方案二、三带来的负面效应。


但是这种写法相对 2,3 繁琐了许多:


1. 如果我们并不需要在构造函数里做什么的话,为了做函数绑定,我们需要手动声明构造函数; 这里没有考虑到实例属性的新写法,直接在顶层赋值。感谢 @Yes 好 2012 指正。

  1. 针对一些复杂的组件(要绑定的方法过多),我们需要多次重复的去写这些方法名;

  2. 无法单独处理传参问题(这一点尤其重要,也限制了它的使用场景)。

方案五:使用箭头函数定义方法(class properties)

这种技术依赖于Class Properties提案,目前还在stage-2阶段,如果需要使用这种方案,我们需要安装@babel/plugin-proposal-class-properties


class Test extends Component {  handleClick = () => {    console.log(this)  }
render() { return <button onClick={this.handleClick}>测试</button> }}
复制代码


这也是我们面试题中提到的第二种绑定方案先总结一下优点:


  1. 自动绑定

  2. 没有方案二、三所带来的渲染性能问题(只绑定一次,没有生成新的函数);

  3. 可以再封装一下,使用params => () => {}这种写法来达到传参的目的。


我们在 babel 上做一下编译:点击 class-properties(选择 ES2016 或者更高,需要手动安装一下这个 pluginbabel-plugin-transform-class-properties相比于@babel/plugin-proposal-class-properties更直观,前者是 babel6 命名方式,后者是 babel7)



在使用 plugin 编译后的版本我们可以看到,这种方案其实就是直接在构造函数中定义了一个 change 属性,然后赋值为箭头函数,从而实现的对 this 的绑定,看起来很完美,很精妙。然而,正是因为这种写法,意味着由这个组件类实例化的所有组件实例都会分配一块内存来去存储这个箭头函数。而我们定义的普通方法,其实是定义在原型对象上的,被所有实例共享,牺牲的代价则是需要我们使用 bind 手动绑定,生成了一个新的函数。


我们看一下 bind 函数的 polyfill:


if (!Function.prototype.bind) {    ... // do sth    var fBound  = function() {          // this instanceof fBound === true时,说明返回的fBound被当做new的构造函数调用          return fToBind.apply(this instanceof fBound                 ? this                 : oThis,                 // 获取调用时(fBound)的传参.bind 返回的函数入参往往是这么传递的                 aArgs.concat(Array.prototype.slice.call(arguments)));        };    ... // do sth
return fBound; };}
复制代码


如果在不支持 bind 的浏览器上,其实编译后,也就相当于新生成的函数的函数体就一条语句: fToBind.apply(...)


我们以图片的形式看一下差距:




注: 图中,虚线框面积代表引用函数所节省的内存,实线框的面积代表消耗的内存。图一:使用箭头函数做 this 绑定。只有 render 函数定义在原型对象上,由所有实例对象共享。其他内存消耗都是基于每个实例上的。图二:在构造函数中做 this 绑定。render,handler 都定义在原型对象上,实例上的 handler 实线框代表使用 bind 生成的函数所消耗的内存大小。


如果我们的 handler 函数体本身就很小,实例数量不多,绑定的方法不多。两种方案在内存占用上的差异性不大,但是一旦我们要在handler里处理复杂的逻辑,或者该组件可能会产生大量的实例,抑或是该组件有大量的需要绑定方法,第一种的优势就突显出来了。


如果说上面这种绑定 this 的方案只用在 React 上,可能我们只需要考虑上面几点,但是如果我们使用上面的方法去创建一些工具类,可能注意的不止这些。


说到类,可能大家都会想到类的继承,如果我们需要重写某个基类的方法,运行下面,你会发现,和想象中的相差甚远。


class Base {  sayHello() {    console.log('Hello')  }
sayHey = () => { console.log('Hey') }}
class A extends Base { constructor() { super() this.name = 'Bitch' }
sayHey() { console.log('Hey', this.name) }}
new A().sayHello() // 'Hello'new A().sayHey() // 'Hey'
复制代码


注: 我们希望打印出 'Hello' 'Hey Bitch',实际打印的是:'Hello' 'Hey'


原因很简单,在 A 的构造函数内,我们调用 super 执行了 Base 的构造函数,向 A 实例上添加属性,这个时候执行 Base 构造函数后,A 实例上已经有了 sayHey 属性,它的值是一个箭头函数,打印出·Hey·而我们重写的 sayHey 其实是定义在原型对象上的。所以最终执行的是在 Base 里定义的 sayHey 方法,但不是同一个方法。据此,我们还可以推理一下假设我们要先执行 Base 的 sayHey,然后在此基础上执增加逻辑我们又该怎么做?下面这种方案肯定是行不通的。


sayHey() {  super.sayHey() // 报错  console.log('get off!')}
复制代码


多说一句: 有大佬认为这种方法的性能并不好,它考察的点是 ops/s(每秒可以实例化多少个组件,越多越好),最终得出的结论是



但是就有人提出质疑,这些方法我们最终都会通过 babel 编译成浏览器能识别的代码,那么最终运行的版本所体现的差异性是否能够代表其真实的差异性。具体的我也没细看,有需要了解更多的,可以看一下这篇文章 Arrow Functions in Class Properties Might Not Be As Great As We Think


据此,我们已经 cover 了这道题多数考点,如果下次碰到这种题,或者想出这类题不妨从下面的角度去考虑下


  1. 面试者的角度:1.1 在回答这道题之前,写解释两种方案的原理,显然,面试官想要着重考察的是第二种的了解情况,他背后到底做了什么。然后谈谈他们一些常规的优缺点 1.2 回答关于效率的问题,前者每次 bind,都会生成一个新的函数,但是函数体内代码量少,最重要的还是引用的原型上的 handler,这个是共享的。但是后面这一种,他会在每个实例上生成一个函数,如果实例数量多,或者函数体大,或者是绑定函数过多,那么占用的内存就明显要超出第一种。

  2. 面试官的角度: 考 bind 实现,考 react 的绑定策略,优缺点,考性能优化策略,考箭头函数, 考原型链,考继承。发散开来,真的很广。

总结:

每种绑定方案既然存在就有其存在的理由(除了第一种已经是过去),但是也会有相应的弊端,并没有绝对的谁好谁差,我们在使用时,可以根据实际场景做选择。这道题目答到点不难,怎样让面试官觉得你懂得全面还是挺难的。


其次针对 this 绑定方案, 如果特别在意性能,牺牲一点代码量,可读性:推荐四其次,如果自己本身够细心,二三也可以使用,但是一定要注意新生成的函数是否会导致多余渲染;如果想不加班:推荐五(如何传参文章中有提及)。


  • 增加shouldComponentUpdate钩子对新旧props进行比较,如果值相同则阻止更新,避免不必要的渲染,或者使用PureReactComponent替代Component,其内部已经封装了shouldComponentUpdate的浅比较逻辑

  • 对于列表或其他结构相同的节点,为其中的每一项增加唯一key属性,以方便Reactdiff算法中对该节点的复用,减少节点的创建和删除操作

  • render函数中减少类似onClick={() => {doSomething()}}的写法,每次调用 render 函数时均会创建一个新的函数,即使内容没有发生任何变化,也会导致节点没必要的重渲染,建议将函数保存在组件的成员对象中,这样只会创建一次

  • 组件的props如果需要经过一系列运算后才能拿到最终结果,则可以考虑使用reselect库对结果进行缓存,如果 props 值未发生变化,则结果直接从缓存中拿,避免高昂的运算代价

  • webpack-bundle-analyzer分析当前页面的依赖包,是否存在不合理性,如果存在,找到优化点并进行优化


前端react面试题详细解答

在调用 setState 之后发生了什么

  • 状态合并,触发调和:

  • setState 函数之后,会将传入的参数对象与当前的状态合并,然后出发调用过程

  • 根据新的状态构建虚拟 dom 树

  • 经过调和过程,react 会高效的根据新的状态构建虚拟 DOM 树,准备渲染整个 UI 页面

  • 计算新老树节点差异,最小化渲染

  • 得倒新的虚拟 DOM 树后,会计算出新老树的节点差异,会根据差异对界面进行最小化渲染

  • 按需更新

  • 在差异话计算中,react 可以相对准确的知道哪些位置发生了改变以及该如何改变,这保证按需更新,而不是宣布重新渲染

使用 React 有何优点

  • 只需查看 render 函数就会很容易知道一个组件是如何被渲染的

  • JSX 的引入,使得组件的代码更加可读,也更容易看懂组件的布局,或者组件之间是如何互相引用的

  • 支持服务端渲染,这可以改进 SEO 和性能

  • 易于测试

  • React 只关注 View 层,所以可以和其它任何框架(如 Backbone.js, Angular.js)一起使用

HOC 相比 mixins 有什么优点?

HOC 和 Vue 中的 mixins 作用是一致的,并且在早期 React 也是使用 mixins 的方式。但是在使用 class 的方式创建组件以后,mixins 的方式就不能使用了,并且其实 mixins 也是存在一些问题的,比如:


  • 隐含了一些依赖,比如我在组件中写了某个 state 并且在 mixin 中使用了,就这存在了一个依赖关系。万一下次别人要移除它,就得去 mixin 中查找依赖

  • 多个 mixin 中可能存在相同命名的函数,同时代码组件中也不能出现相同命名的函数,否则就是重写了,其实我一直觉得命名真的是一件麻烦事。。

  • 雪球效应,虽然我一个组件还是使用着同一个 mixin,但是一个 mixin 会被多个组件使用,可能会存在需求使得 mixin 修改原本的函数或者新增更多的函数,这样可能就会产生一个维护成本


HOC 解决了这些问题,并且它们达成的效果也是一致的,同时也更加的政治正确(毕竟更加函数式了)。

为什么使用 jsx 的组件中没有看到使用 react 却需要引入 react?

本质上来说 JSX 是React.createElement(component, props, ...children)方法的语法糖。在 React 17 之前,如果使用了 JSX,其实就是在使用 React, babel 会把组件转换为 CreateElement 形式。在 React 17 之后,就不再需要引入,因为 babel 已经可以帮我们自动引入 react。

hooks 父子传值

父传子在父组件中用useState声明数据 const [ data, setData ] = useState(false)
把数据传递给子组件<Child data={data} />
子组件接收export default function (props) { const { data } = props console.log(data)}子传父子传父可以通过事件方法传值,和父传子有点类似。在父组件中用useState声明数据 const [ data, setData ] = useState(false)
把更新数据的函数传递给子组件<Child setData={setData} />
子组件中触发函数更新数据,就会直接传递给父组件export default function (props) { const { setData } = props setData(true)}如果存在多个层级的数据传递,也可依照此方法依次传递
// 多层级用useContextconst User = () => { // 直接获取,不用回调 const { user, setUser } = useContext(UserContext); return <Avatar user={user} setUser={setUser} />;};
复制代码

componentWillReceiveProps 调用时机

  • 已经被废弃掉

  • 当 props 改变的时候才调用,子组件第二次接收到 props 的时候

为什么 React 要用 JSX?

JSX 是一个 JavaScript 的语法扩展,或者说是一个类似于 XML 的 ECMAScript 语法扩展。它本身没有太多的语法定义,也不期望引入更多的标准。


其实 React 本身并不强制使用 JSX。在没有 JSX 的时候,React 实现一个组件依赖于使用 React.createElement 函数。代码如下:


class Hello extends React.Component {  render() {    return React.createElement(        'div',        null,         `Hello ${this.props.toWhat}`      );  }}ReactDOM.render(  React.createElement(Hello, {toWhat: 'World'}, null),  document.getElementById('root'));
复制代码


而 JSX 更像是一种语法糖,通过类似 XML 的描述方式,描写函数对象。在采用 JSX 之后,这段代码会这样写:


class Hello extends React.Component {  render() {    return <div>Hello {this.props.toWhat}</div>;  }}ReactDOM.render(  <Hello toWhat="World" />,  document.getElementById('root'));
复制代码


通过对比,可以清晰地发现,代码变得更为简洁,而且代码结构层次更为清晰。


因为 React 需要将组件转化为虚拟 DOM 树,所以在编写代码时,实际上是在手写一棵结构树。而 XML 在树结构的描述上天生具有可读性强的优势。


但这样可读性强的代码仅仅是给写程序的同学看的,实际上在运行的时候,会使用 Babel 插件将 JSX 语法的代码还原为 React.createElement 的代码。


总结: JSX 是一个 JavaScript 的语法扩展,结构类似 XML。JSX 主要用于声明 React 元素,但 React 中并不强制使用 JSX。即使使用了 JSX,也会在构建过程中,通过 Babel 插件编译为 React.createElement。所以 JSX 更像是 React.createElement 的一种语法糖。


React 团队并不想引入 JavaScript 本身以外的开发体系。而是希望通过合理的关注点分离保持组件开发的纯粹性。

fetch 封装

npm install whatwg-fetch --save  // 适配其他浏览器npm install es6-promise
export const handleResponse = (response) => { if (response.status === 403 || response.status === 401) { const oauthurl = response.headers.get('locationUrl'); if (!_.isEmpty(oauthUrl)) { window.location.href = oauthurl; return; } } if (!response.ok) { return getErrorMessage(response).then(errorMessage => apiError(response.status, errorMessage)); } if (isJson(response)) { return response.json(); } if (isText(response)) { return response.text(); }
return response.blob();};
const httpRequest = { request: ({ method, headers, body, path, query, }) => { const options = {}; let url = path; if (method) { options.method = method; } if (headers) { options.headers = {...options.headers,...headers}; } if (body) { options.body = body; } if (query) { const params = Object.keys(query) .map(k => `${k}=${query[k]}`) .join('&'); url = url.concat(`?${params}`); } return fetch(url, Object.assign({}, options, { credentials: 'same-origin' })).then(handleResponse); },};
export default httpRequest;
复制代码

React Hooks 在平时开发中需要注意的问题和原因

(1)不要在循环,条件或嵌套函数中调用 Hook,必须始终在 React 函数的顶层使用 Hook


这是因为 React 需要利用调用顺序来正确更新相应的状态,以及调用相应的钩子函数。一旦在循环或条件分支语句中调用 Hook,就容易导致调用顺序的不一致性,从而产生难以预料到的后果。


(2)使用 useState 时候,使用 push,pop,splice 等直接更改数组对象的坑


使用 push 直接更改数组无法获取到新值,应该采用析构方式,但是在 class 里面不会有这个问题。代码示例:


function Indicatorfilter() {  let [num,setNums] = useState([0,1,2,3])  const test = () => {    // 这里坑是直接采用push去更新num    // setNums(num)是无法更新num的    // 必须使用num = [...num ,1]    num.push(1)    // num = [...num ,1]    setNums(num)  }return (    <div className='filter'>      <div onClick={test}>测试</div>        <div>          {num.map((item,index) => (              <div key={index}>{item}</div>          ))}      </div>    </div>  )}
class Indicatorfilter extends React.Component<any,any>{ constructor(props:any){ super(props) this.state = { nums:[1,2,3] } this.test = this.test.bind(this) }
test(){ // class采用同样的方式是没有问题的 this.state.nums.push(1) this.setState({ nums: this.state.nums }) }
render(){ let {nums} = this.state return( <div> <div onClick={this.test}>测试</div> <div> {nums.map((item:any,index:number) => ( <div key={index}>{item}</div> ))} </div> </div>
) }}
复制代码


(3)useState 设置状态的时候,只有第一次生效,后期需要更新状态,必须通过 useEffect


TableDeail 是一个公共组件,在调用它的父组件里面,我们通过 set 改变 columns 的值,以为传递给 TableDeail 的 columns 是最新的值,所以 tabColumn 每次也是最新的值,但是实际 tabColumn 是最开始的值,不会随着 columns 的更新而更新:


const TableDeail = ({    columns,}:TableData) => {    const [tabColumn, setTabColumn] = useState(columns) }
// 正确的做法是通过useEffect改变这个值const TableDeail = ({ columns,}:TableData) => { const [tabColumn, setTabColumn] = useState(columns) useEffect(() =>{setTabColumn(columns)},[columns])}
复制代码


(4)善用 useCallback


父组件传递给子组件事件句柄时,如果我们没有任何参数变动可能会选用 useMemo。但是每一次父组件渲染子组件即使没变化也会跟着渲染一次。


(5)不要滥用 useContext


可以使用基于 useContext 封装的状态管理工具。

在哪个生命周期中你会发出 Ajax 请求?为什么?

Ajax 请求应该写在组件创建期的第五个阶段,即 componentDidMount 生命周期方法中。原因如下。在创建期的其他阶段,组件尚未渲染完成。而在存在期的 5 个阶段,又不能确保生命周期方法一定会执行(如通过 shouldComponentUpdate 方法优化更新等)。在销毀期,组件即将被销毁,请求数据变得无意义。因此在这些阶段发岀 Ajax 请求显然不是最好的选择。在组件尚未挂载之前,Ajax 请求将无法执行完毕,如果此时发出请求,将意味着在组件挂载之前更新状态(如执行 setState),这通常是不起作用的。在 componentDidMount 方法中,执行 Ajax 即可保证组件已经挂载,并且能够正常更新组件。

指出(组件)生命周期方法的不同

  • componentWillMount -- 多用于根组件中的应用程序配置

  • componentDidMount -- 在这可以完成所有没有 DOM 就不能做的所有配置,并开始获取所有你需要的数据;如果需要设置事件监听,也可以在这完成

  • componentWillReceiveProps -- 这个周期函数作用于特定的 prop 改变导致的 state 转换

  • shouldComponentUpdate -- 如果你担心组件过度渲染,shouldComponentUpdate 是一个改善性能的地方,因为如果组件接收了新的 prop, 它可以阻止(组件)重新渲染。shouldComponentUpdate 应该返回一个布尔值来决定组件是否要重新渲染

  • componentWillUpdate -- 很少使用。它可以用于代替组件的 componentWillReceivePropsshouldComponentUpdate(但不能访问之前的 props)

  • componentDidUpdate -- 常用于更新 DOM,响应 prop 或 state 的改变

  • componentWillUnmount -- 在这你可以取消网络请求,或者移除所有与组件相关的事件监听器

高阶组件

高阶函数:如果一个函数接受一个或多个函数作为参数或者返回一个函数就可称之为高阶函数


高阶组件:如果一个函数 接受一个或多个组件作为参数并且返回一个组件 就可称之为 高阶组件


react 中的高阶组件


React 中的高阶组件主要有两种形式:属性代理反向继承


属性代理 Proxy


  • 操作 props

  • 抽离 state

  • 通过 ref 访问到组件实例

  • 用其他元素包裹传入的组件 WrappedComponent


反向继承


会发现其属性代理和反向继承的实现有些类似的地方,都是返回一个继承了某个父类的子类,只不过属性代理中继承的是 React.Component,反向继承中继承的是传入的组件 WrappedComponent


反向继承可以用来做什么:


1.操作 state


高阶组件中可以读取、编辑和删除WrappedComponent组件实例中的state。甚至可以增加更多的state项,但是非常不建议这么做因为这可能会导致state难以维护及管理。


function withLogging(WrappedComponent) {        return class extends WrappedComponent {            render() {                return (                    <div>;                        <h2>;Debugger Component Logging...<h2>;                        <p>;state:<p>;                        <pre>;{JSON.stringify(this.state, null, 4)}<pre>;                        <p>props:<p>;                        <pre>{JSON.stringify(this.props, null, 4)}<pre>;                        {super.render()}                    <div>;                );            }        };    }
复制代码


2.渲染劫持(Render Highjacking)


条件渲染通过 props.isLoading 这个条件来判断渲染哪个组件。


修改由 render() 输出的 React 元素树

react 最新版本解决了什么问题,增加了哪些东西

React 16.x 的三大新特性 Time Slicing、Suspense、 hooks


  • Time Slicing(解决 CPU 速度问题)使得在执行任务的期间可以随时暂停,跑去干别的事情,这个特性使得 react 能在性能极其差的机器跑时,仍然保持有良好的性能

  • Suspense (解决网络 IO 问题) 和 lazy 配合,实现异步加载组件。 能暂停当前组件的渲染, 当完成某件事以后再继续渲染,解决从 react 出生到现在都存在的「异步副作用」的问题,而且解决得非的优雅,使用的是 T 异步但是同步的写法,这是最好的解决异步问题的方式

  • 提供了一个内置函数 componentDidCatch,当有错误发生时,可以友好地展示 fallback 组件; 可以捕捉到它的子元素(包括嵌套子元素)抛出的异常; 可以复用错误组件。


(1)React16.8 加入 hooks,让 React 函数式组件更加灵活,hooks 之前,React 存在很多问题:


  • 在组件间复用状态逻辑很难

  • 复杂组件变得难以理解,高阶组件和函数组件的嵌套过深。

  • class 组件的 this 指向问题

  • 难以记忆的生命周期


hooks 很好的解决了上述问题,hooks 提供了很多方法


  • useState 返回有状态值,以及更新这个状态值的函数

  • useEffect 接受包含命令式,可能有副作用代码的函数。

  • useContext 接受上下文对象(从 React.createContext 返回的值)并返回当前上下文值,

  • useReducer useState 的替代方案。接受类型为 (state,action)=> newState 的 reducer,并返回与 dispatch 方法配对的当前状态。

  • useCalLback 返回一个回忆的 memoized 版本,该版本仅在其中一个输入发生更改时才会更改。纯函数的输入输出确定性 o useMemo 纯的一个记忆函数 o useRef 返回一个可变的 ref 对象,其 Current 属性被初始化为传递的参数,返回的 ref 对象在组件的整个生命周期内保持不变。

  • useImperativeMethods 自定义使用 ref 时公开给父组件的实例值

  • useMutationEffect 更新兄弟组件之前,它在 React 执行其 DOM 改变的同一阶段同步触发

  • useLayoutEffect DOM 改变后同步触发。使用它来从 DOM 读取布局并同步重新渲染


(2)React16.9


  • 重命名 Unsafe 的生命周期方法。新的 UNSAFE_前缀将有助于在代码 review 和 debug 期间,使这些有问题的字样更突出

  • 废弃 javascrip:形式的 URL。以 javascript:开头的 URL 非常容易遭受攻击,造成安全漏洞。

  • 废弃"Factory"组件。 工厂组件会导致 React 变大且变慢。

  • act()也支持异步函数,并且你可以在调用它时使用 await。

  • 使用 <React.ProfiLer> 进行性能评估。在较大的应用中追踪性能回归可能会很方便


(3)React16.13.0


  • 支持在渲染期间调用 setState,但仅适用于同一组件

  • 可检测冲突的样式规则并记录警告

  • 废弃 unstable_createPortal,使用 CreatePortal

  • 将组件堆栈添加到其开发警告中,使开发人员能够隔离 bug 并调试其程序,这可以清楚地说明问题所在,并更快地定位和修复错误。

使用状态要注意哪些事情?

要注意以下几点。


  • 不要直接更新状态

  • 状态更新可能是异步的

  • 状态更新要合并。

  • 数据从上向下流动


**

React 与 Vue 的 diff 算法有何不同?

diff 算法是指生成更新补丁的方式,主要应用于虚拟 DOM 树变化后,更新真实 DOM。所以 diff 算法一定存在这样一个过程:触发更新 → 生成补丁 → 应用补丁。


React 的 diff 算法,触发更新的时机主要在 state 变化与 hooks 调用之后。此时触发虚拟 DOM 树变更遍历,采用了深度优先遍历算法。但传统的遍历方式,效率较低。为了优化效率,使用了分治的方式。将单一节点比对转化为了 3 种类型节点的比对,分别是树、组件及元素,以此提升效率。


  • 树比对:由于网页视图中较少有跨层级节点移动,两株虚拟 DOM 树只对同一层次的节点进行比较。

  • 组件比对:如果组件是同一类型,则进行树比对,如果不是,则直接放入到补丁中。

  • 元素比对:主要发生在同层级中,通过标记节点操作生成补丁,节点操作对应真实的 DOM 剪裁操作。


以上是经典的 React diff 算法内容。自 React 16 起,引入了 Fiber 架构。为了使整个更新过程可随时暂停恢复,节点与树分别采用了 FiberNode 与 FiberTree 进行重构。fiberNode 使用了双链表的结构,可以直接找到兄弟节点与子节点。整个更新过程由 current 与 workInProgress 两株树双缓冲完成。workInProgress 更新完成后,再通过修改 current 相关指针指向新节点。


Vue 的整体 diff 策略与 React 对齐,虽然缺乏时间切片能力,但这并不意味着 Vue 的性能更差,因为在 Vue 3 初期引入过,后期因为收益不高移除掉了。除了高帧率动画,在 Vue 中其他的场景几乎都可以使用防抖和节流去提高响应性能。

diff 算法?

  • 把树形结构按照层级分解,只比较同级元素

  • 给列表结构的每个单元添加唯一的 key 属性,方便比较

  • React 只会匹配相同 class 的 component(这里面的 class 指的是组件的名字)

  • 合并操作,调用 component 的 setState 方法的时候, React 将其标记为 dirty.到每一个 事件循环结束, React 检查所有标记 dirty 的 component 重新绘制.

  • 选择性子树渲染。开发人员可以重写 shouldComponentUpdate 提高 diff 的性能。

diff 算法如何比较?

  • 只对同级比较,跨层级的 dom 不会进行复用

  • 不同类型节点生成的 dom 树不同,此时会直接销毁老节点及子孙节点,并新建节点

  • 可以通过 key 来对元素 diff 的过程提供复用的线索

  • 单节点 diff

  • 单点 diff 有如下几种情况:

  • key 和 type 相同表示可以复用节点

  • key 不同直接标记删除节点,然后新建节点

  • key 相同 type 不同,标记删除该节点和兄弟节点,然后新创建节点

在 Redux 中使用 Action 要注意哪些问题?

在 Redux 中使用 Action 的时候, Action 文件里尽量保持 Action 文件的纯净,传入什么数据就返回什么数据,最妤把请求的数据和 Action 方法分离开,以保持 Action 的纯净。

React Hooks 和生命周期的关系?

函数组件 的本质是函数,没有 state 的概念的,因此不存在生命周期一说,仅仅是一个 render 函数而已。但是引入 Hooks 之后就变得不同了,它能让组件在不使用 class 的情况下拥有 state,所以就有了生命周期的概念,所谓的生命周期其实就是 useStateuseEffect()useLayoutEffect()


即:Hooks 组件(使用了 Hooks 的函数组件)有生命周期,而函数组件(未使用 Hooks 的函数组件)是没有生命周期的


下面是具体的 class 与 Hooks 的生命周期对应关系


  • constructor:函数组件不需要构造函数,可以通过调用 **useState 来初始化 state**。如果计算的代价比较昂贵,也可以传一个函数给 useState


const [num, UpdateNum] = useState(0)
复制代码


  • getDerivedStateFromProps:一般情况下,我们不需要使用它,可以在渲染过程中更新 state,以达到实现 getDerivedStateFromProps 的目的。


function ScrollView({row}) {  let [isScrollingDown, setIsScrollingDown] = useState(false);  let [prevRow, setPrevRow] = useState(null);  if (row !== prevRow) {    // Row 自上次渲染以来发生过改变。更新 isScrollingDown。    setIsScrollingDown(prevRow !== null && row > prevRow);    setPrevRow(row);  }  return `Scrolling down: ${isScrollingDown}`;}
复制代码


React 会立即退出第一次渲染并用更新后的 state 重新运行组件以避免耗费太多性能。


  • shouldComponentUpdate:可以用 **React.memo** 包裹一个组件来对它的 props 进行浅比较


const Button = React.memo((props) => {  // 具体的组件});
复制代码


注意:**React.memo 等效于 **``**PureComponent**,它只浅比较 props。这里也可以使用 useMemo 优化每一个节点。


  • render:这是函数组件体本身。

  • componentDidMount, componentDidUpdateuseLayoutEffect 与它们两的调用阶段是一样的。但是,我们推荐你一开始先用 useEffect,只有当它出问题的时候再尝试使用 useLayoutEffectuseEffect 可以表达所有这些的组合。


// componentDidMountuseEffect(()=>{  // 需要在 componentDidMount 执行的内容}, [])useEffect(() => {   // 在 componentDidMount,以及 count 更改时 componentDidUpdate 执行的内容  document.title = `You clicked ${count} times`;   return () => {    // 需要在 count 更改时 componentDidUpdate(先于 document.title = ... 执行,遵守先清理后更新)    // 以及 componentWillUnmount 执行的内容         } // 当函数中 Cleanup 函数会按照在代码中定义的顺序先后执行,与函数本身的特性无关}, [count]); // 仅在 count 更改时更新
复制代码


请记得 React 会等待浏览器完成画面渲染之后才会延迟调用 ,因此会使得额外操作很方便


  • componentWillUnmount:相当于 useEffect 里面返回的 cleanup 函数


// componentDidMount/componentWillUnmountuseEffect(()=>{  // 需要在 componentDidMount 执行的内容  return function cleanup() {    // 需要在 componentWillUnmount 执行的内容        }}, [])
复制代码


  • componentDidCatch and getDerivedStateFromError:目前还没有这些方法的 Hook 等价写法,但很快会加上。



用户头像

beifeng1996

关注

还未添加个人签名 2022.09.01 加入

还未添加个人简介

评论

发布
暂无评论
一道React面试题把我整懵了_React_beifeng1996_InfoQ写作社区