关于react.js:一道React面试题把我整懵了

10次阅读

共计 5214 个字符,预计需要花费 14 分钟才能阅读完成。

发问: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>
  }
})

计划二:在 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 举荐的形式,只在实例化组件的时候做一次绑定,之后传递的都是同一援用,没有计划二、三带来的负面效应。参考:前端 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 绑定计划,** 如果特地在意性能,就义一点代码量,可读性:举荐四其次,如果本人自身够仔细,二三也能够应用,然而肯定要留神新生成的函数是否会导致多余渲染;
如果想不加班:举荐五(如何传参文章中有提及)。**

正文完
 0