7. react-interview

244 阅读25分钟

参考的面试题文章

在构造函数调用 super 并将 props 作为参数传入的作用是啥

  • 在调用super方法之前,子类构造函数无法使用this引用,js不允许这么做。es6中,super指代父类构造函数
  • 将props参数传递给super()调用的主要原因是在子构造函数中能够通过this.props来获取传入的props;
  • 有时候不传props,只执行super(),或者没有设置constructor的情况下,依然可以在组件内使用this.props,为什么?
    • 因为react在组件实例化的时候,内部给实例设置了一遍props
    • 但是这并不意味着可以只写super()而不写super(props):虽然react会在组件实例化的时候设置一遍props,但在super调用一直到构造函数结束之前,this.props依然是未定义的
// React 内部
const instance = new YourComponent(props);
instance.props = props;

// 传递props
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    console.log(this.props);  // { name: 'sudheer',age: 30 }
  }
}
// 没传递props
class MyComponent extends React.Component {
  constructor(props) {
    super();
    console.log(this.props); // undefined
    // 但是 Props 参数仍然可用
    console.log(props); // Prints { name: 'sudheer',age: 30 }
  }

  render() {
    // 构造函数外部不受影响
    console.log(this.props) // { name: 'sudheer',age: 30 }
  }
}

为什么自定义的React组件必须大写

  • 用来区分自定义组件和原生dom
  • babel在编译过程中会判断JSX组件的首字母,如果是小写,则当作原生的dom标签解析,会编译成字符串。如果是大写,则认为是自定义组件,编译成对象

受控组件与非受控组件

  • 受控组件
    • 绑定change事件,每当表单的值发生变化都会被写入组件的state中,这种组件是受控组件
  • 非受控组件
    • 如果一个表单组件没有value props(单选按钮和复选按钮对应的是checked props),就可以称为非受控组件
    • 通过defaultValue/defaultChecked设置组件的默认值,只会被渲染一次,在后续的渲染中不起作用
// 受控组件
<input
    type="text"
    value={this.state.value}
    onChange={(e) => {
        this.setState({
            value: e.target.value.toUpperCase(),
        });
    }}
/>

// 非受控组件
class UnControlled extends Component {
    handleSubmit = (e) => {
        console.log(e);
        e.preventDefault();
        console.log(this.name.value);
    }
    render() {
        return (
            <form onSubmit={this.handleSubmit}>
                <input type="text" ref={i => this.name = i} defaultValue="BeiJing" />
                <button type="submit">Submit</button>
            </form>
        );
    }
}
* 非受控组件
* 1.ref
* 2.defaultValue / defaultChecked
* 3.手动操作dom元素

* 非受控组件使用场景
* 1 必须手动操作dom元素,setState实现不了
* 2 文件上传 <input type="file"/>
* 3 某些富文本编辑器,需要传入dom元素

* 受控组件 vs 非受控组件
* 1. 优先使用受控组件,符合react设计原则
* 2. 必须操作dom时再使用非受控组件

setState的原理,什么时候同步,什么时候异步

  • 函数式setState用法,才是setState的未来
function incrementMultiple() {

  this.setState(increment);

  this.setState(increment);

  this.setState({count: this.state.count + 1});

  this.setState(increment);

}

在几个函数式setState调用中插入一个传统式setState调用
(嗯,我们姑且这么称呼以前的setState使用方式),
最后得到的结果是让this.state.count增加了2,而不是增加4。

原因也很简单,因为React会依次合并所有setState产生的效果,
虽然前两个函数式setState调用产生的效果是count加2,
但是半路杀出一个传统式setState调用,一下子强行把积攒的效果清空,用count加1取代。

这么看来,传统式setState的存在,会把函数式setState拖下水啊!
只要有一个传统式的setState调用,就把其他函数式setState调用给害了。
  • 生命周期和合成事件中
    • 在react生命周期和合成事件中,react仍处于他的更新机制中(这时 isBranchUpdate为true),这时无论调用多少次setState,都不会立即执行更新,把将要更新的存入_pendingStateQueue,将要更新的组件存入dirtyComponent
    • 当上一次更新机制执行完毕,(以生命周期为例,所有组件,即最顶层组件didmount后,)会将批处理标志isBranchUpdate设置为false,这时取出dirtyComponent中的组件以及 _pendingStateQueue中的 state进行更新。这样就可以确保组件不会被重新渲染多次。
    • setState本身并不是异步的,而是 React的批处理机制给人一种异步的假象。
    • componentWillUpdate和componentDidUpdate这两个生命周期中不能使用setState
  • 异步代码和原生事件中
    • 当在异步代码中调用setState,根据javascript异步机制,会将异步代码先暂存,等所有同步代码执行完毕后再执行。这时react的批处理机制已经完成,处理标志被设置为false。这时再调用setState会立即执行更新,拿到更新后的结果
  • 最佳实践
    • setState的第二个参数接收一个函数,该函数会在 React的批处理机制完成之后调用,所以你想在调用 setState后立即获取更新后的值,请在该回调函数中获取。

生命周期

  • 16版本之前的生命周期
    • 第一次渲染
      • constructor
      • componentWillMount
      • render
      • componentDidMount
    • props更新
      • componentWillReceiveProps(nextProps)
      • shoulComponentUpdate(nextProps, nextState)
      • componentWillUpdate(nextProps, nextState) 注意不能在该函数中通过this.setstate再次改变state
      • render
      • componentDidUpdate(preProps, preState)
    • state更新
      • shouldComponentUpdate(nextProps, nextState)
      • componentWillUpdate(nextProps, nextState)
      • render
      • componentDidUpdate(preProps, preState)
    • 组件卸载
      • componentWillUnmount
  • 16版本之后的生命周期
    • 第一次渲染
    • constructor
    • getDerivedStateFromProps(nextProps, preState)
      • 组件每次被rerender的时候,包括在组件构建之后(虚拟dom之后,实际dom挂载之前),每次获取新的props或state之后;每次接受新的props之后都会返回一个对象作为新的state,返回null则说明不需要更新state。配合componentDidUpdate,可以覆盖componentWillReceiveProps的所有用法
    • render
    • componentDidMount
    • props/state 更新
    • getDerivedStateFromProps(nextProps, preState)
    • shouldComponentUpdate(nextProps, nextState)
    • render
    • getSnapshotBeforeUpdate(preProps, preState)
      • 触发时间:update发生的时候,在render之后,组件dom渲染之前。返回一个值作为componentDidUpdate的第三个参数。配合componentDidUpdate可以覆盖componentWillUpdate的所有用法
    • componentDidUpdate(preProps, preState)
    • 组件卸载
    • componentWillUnmount
  • 造成组件更新的几种情况
    • setState引起的state更新或父组件重新render引起的props更新,更新后的state和props相对之前无论是否有变化,都将引起子组件的重新render
  • componentWillReceiveProps
    • 在componentWillReceiveProps方法中,将props转为自己的state(在componentWillReceiveProps方法中调用this.setState不会引起二次渲染)
    • 由于在 componentWillReceiveProps() 中能调用 this.setState() 方法,因此为了避免进入一个死循环,在调用 this.setState() 方法更新组件时就不会触发它。
  • 生命周期函数变更的原因
    • fiber之后,如果开启async rendering, 在render之前的所有函数都有可能被执行多次
      • 在componentWillMount里发起AJAX,不管多快得到结果也赶不上首次render,在Fiber启用async render之后,更没有理由在componentWillMount里做AJAX,因为componentWillMount可能会被调用多次,谁也不会希望无谓地多次调用AJAX吧
  • getDerivedStateFromProps(nextProps, revState)
    • 废弃掉了除了shouldComponentUpdate之外的在render之前执行的所有生命周期函数。以前需要利用被deprecate的所有生命周期函数才能实现的功能,都可以通过getDerivedStateFromProps的帮助来实现。
    • static getDerivedStateFromProps(nextprops, prevstate)在组件创建和更新时的render方法之前调用,它应该返回一个对象来更新状态,或者返回null来不更新任何内容。
    • getDerivedStateFromProps前面要加上static关键字,声明是静态方法,不然会被react忽略掉
    • static静态方法只能class(构造函数)调用,而实例是不能的
    • 用一个静态函数getDerivedStateFromProps来取代被deprecate的几个生命周期函数,就是强制开发者在render之前只做无副作用的操作,而且能做的操作局限在根据props和state决定新的state,而已
  • getSnapshotBeforeUpdate(preProps, preState)
    • 这个函数在render之后执行,在执行的时候dom元素还没有被更新,给了一个机会去获取dom信息,计算得到一个snapshot,这个snapshot会作为第三个参数传入componentDidUpdate

Fiber

  • React Fiber是对核心算法的一次重新实现
  • 同步更新过程的局限:

当react决定要加载或者更新组件树的时候,会做很多事情,比如调用各个组件的生命周期函数,计算和对比virtual dom,最后更新dom树,这整个过程是同步执行的。但是当组件树较大的时候问题就来了

假如更新一个组件需要1毫秒,如果有200个组件要更新,那就需要200毫秒,在这200毫秒的更新过程中,浏览器那个唯一的主线程都在专心运行更新操作,无暇去做任何其他的事情。想象一下,在这200毫秒内,用户往一个input元素中输入点什么,敲击键盘也不会获得响应,因为渲染输入按键结果也是浏览器主线程的工作,但是浏览器主线程被React占着呢,抽不出空,最后的结果就是用户敲了按键看不到反应,等React更新过程结束之后,咔咔咔那些按键一下子出现在input元素里了。这就是所谓的界面卡顿,很不好的用户体验。

现有的React版本,当组件树很大的时候就会出现这种问题,因为更新过程是同步地一层组件套一层组件,逐渐深入的过程,在更新完所有组件之前不停止,函数的调用栈就像下图这样,调用得很深,而且很长时间不会返回。

  • react fiber的方式
    • 破解JavaScript中同步操作时间过长的方法其实很简单——分片
    • 把一个耗时长的任务分成很多小片,每一个小片的运行时间很短,虽然总时间依旧很长,但是在每个小片执行完之后,会给其他任务一个执行的机会,这样唯一的线程就不会被独占,其他任务依然有运行的机会
    • react fiber把更新过程碎片化,每执行完一段更新过程,就把控制权交还给react负责任务协调的模块,看看有没有其他紧急的任务要做,如果没有就继续更新,如果有紧急任务,就去执行紧急任务
    • 维护每一个分片的数据结构,就叫做fiber
  • react fiber对现有代码的影响
    • 在现有的React中,每个生命周期函数在一个加载或者更新过程中绝对只会被调用一次;在React Fiber中,不再是这样了,第一阶段中的生命周期函数在一次加载和更新过程中可能会被多次调用
    • 在React Fiber中,一次更新过程会分成多个分片完成,所以完全有可能一个更新任务还没有完成,就被另一个更高优先级的更新过程打断,这时候,优先级高的更新任务会优先处理完,而低优先级更新任务所做的工作则会完全作废,然后等待机会重头再来。
    • react fiber一个更新过程被分为两个阶段:第一个阶段Reconciliation Phase,第二个阶段Commit Phase。在第一个阶段,react fiber会找出需要更新那些dom,这个阶段是可以被打断的;但是到了第二个阶段,那就一鼓作气把dom更新完,绝不会被打断。
    • 这两个阶段大部分工作都是react fiber做,和我们相关的也就是生命周期函数

以render函数为界限,第一阶段可能会调用下面这些生命周期函数:

componentWillMount / componentWillReceiveProps / shouldComponentUpdate / componentWillUpdate

下面这些生命周期函数则会在第二阶段调用

componentDidMount / componentDidUpdate / componentWillUnmount

因为第一阶段的过程会被打断而且“重头再来”,就会造成意想不到的情况。

比如说,一个低优先级的任务A正在执行,已经调用了某个组件的componentWillUpdate函数,接下来发现自己的时间分片已经用完了,于是冒出水面,看看有没有紧急任务,哎呀,真的有一个紧急任务B,接下来React Fiber就会去执行这个紧急任务B,任务A虽然进行了一半,但是没办法,只能完全放弃,等到任务B全搞定之后,任务A重头来一遍,注意,是重头来一遍,不是从刚才中段的部分开始,也就是说,componentWillUpdate函数会被再调用一次。

  • 挨个看一看这些可能被重复调用的函数。
    • componentWillReceiveProps,即使当前组件不更新,只要父组件更新也会引发这个函数被调用,所以多调用几次没啥,通过!
    • shouldComponentUpdate,这函数的作用就是返回一个true或者false,不应该有任何副作用,多调用几次也无妨,通过!
    • render,应该是纯函数,多调用几次无妨,通过!
    • 只剩下componentWillMountcomponentWillUpdate这两个函数往往包含副作用,所以当使用React Fiber的时候一定要重点看这两个函数的实现。
  • 一些生命周期的应用场景
    • 当你希望只有在接收到新props时做一些逻辑时,请使用componentWillReceiveProps,如:根据父组件传入的数据初始化或重置某些内部状态。若希望无论props更改还是组件内容状态更改都能触发一些逻辑,那么请使用componentDidUpdate
    • 若要在props更改时同步更新组件状态,使用componentWillReceiveProps;否则使用componentDidUpdate。

对react中key的理解

  • key的作用
    • react中的key属性,它是一个特殊的属性,它是出现不是给开发者用的(例如你为一个组件设置key之后不能获取组件的这个key props),而是给react自己用的。
    • 简单来说,react利用key来识别组件,它是一种身份标识标识,就像我们的身份证用来辨识一个人一样。每个key对应一个组件,相同的key react认为是同一个组件,这样后续相同的key对应组件都不会被创建。有了key属性后,就可以与组件建立了一种对应关系,react根据key来决定是销毁重新创建组件还是更新组件。
    • 在项目开发中,key属性的使用场景最多的还是由数组动态创建的子组件的情况,需要为每个子组件添加唯一的key属性值。数组创建子组件的位置并不固定,动态改变的;这样有了key属性后,react就可以根据key值来判断是否为同一组件。另外,还有一种比较常见的场景:为一个有复杂繁琐逻辑的组件添加key后,后续操作可以改变该组件的key属性值,从而达到先销毁之前的组件,再重新创建该组件。
  • key的最佳实践
    • 在list数组中,用key来标识数组创建子组件时,若数组的内容只是作为纯展示,而不涉及到数组的动态变更,其实是可以使用index作为key的。但是,若涉及到数组的动态变更,例如数组新增元素、删除元素或者重新排序等,这时index作为key会导致展示错误的数据。
    • key的值要稳定唯一
      • 在理想情况下,在循环一个对象数组时,数组的每一项都会有用于区分其他项的一个键值,相当数据库中主键。这样就可以用该属性值作为key值。但是一般情况下可能是没有这个属性值的,这时就需要我们自己保证。
      • 但是,需要指出的一点是,我们在保证数组每项的唯一的标识时,还需要保证其值的稳定性,不能经常改变。例如下面代码:
      • this.state.data.map(el=> <MyComponent key={Math.random()}/>)
      • 上面代码中中MyComponent的key值是用Math.random随机生成的,虽然能够保持其唯一性,但是它的值是随机而不是稳定的,在数组动态改变时会导致数组元素中的每项都重新销毁然后重新创建,有一定的性能开销;另外可能导致一些意想不到的问题出现。所以:key的值要保持稳定且唯一,不能使用random来生成key的值。
      • 在不能使用random随机生成key时,我们可以像下面这样用一个全局的localCounter变量来添加稳定唯一的key值。
var localCounter = 1;
this.data.forEach(el=>{
    el.id = localCounter++;
});
//向数组中动态添加元素时,
function createUser(user) {
    return {
        ...user,
        id: localCounter++
    }
}
{this.state.data.map((v,idx)=><Item key={idx} v={v} />)}
// 开始时:['a','b','c']=>
<ul>
    <li key="0">a <input type="text"/></li>
    <li key="1">b <input type="text"/></li>
    <li key="2">c <input type="text"/></li>
</ul>

// 数组重排 -> ['c','b','a'] =>
<ul>
    <li key="0">c <input type="text"/></li>
    <li key="1">b <input type="text"/></li>
    <li key="2">a <input type="text"/></li>
</ul>

/*
上面实例中在数组重新排序后,key对应的实例都没有销毁,而是重新更新。
具体更新过程我们拿`key=0`的元素来说明, 数组重新排序后:
    -  组件重新render得到新的虚拟dom;
    -  新老两个虚拟dom进行diff,新老版的都有key=0的组件,react认为同一个组件,
    则只可能更新组件;
    - 然后比较其children,发现内容的文本内容不同(由a--->c),而input组件并没有变化,
    这时触发组件的componentWillReceiveProps方法,从而更新其子组件文本内容;
    - 因为组件的children中input组件没有变化,其又与父组件传入的任`props`没有关联,
    所以input组件不会更新(即其`componentWillReceiveProps`方法不会被执行),
    导致用户输入的值不会变化
这就是`index`作为key存在的问题,所以`不要使用index作为key`。
*/ 
  • 不仅仅在数组生成组件上,其他地方也可以使用key,主要是react利用key来区分组件的,相同的key表示同一个组件,react不会重新销毁创建组件实例,只可能更新;key不同,react会销毁已有的组件实例,重新创建组件新的实例。(看下面的例子)
{
  this.state.type ? 
    <div><Son_1/><Son_2/></div>
    : <div><Son_2/><Son_1/></div>
}
/*
例如上面代码中,this.state.type的值改变时,原Son_1和Son2组件的实例都将会被销毁,
并重新创建Son_1和Son_2组件新的实例,不能继承原来的状态,其实他们只是互换了位置。
为了避免这种问题,我们可以给组件加上key。
*/

{
  this.state.type ? 
    <div><Son_1 key="1"/><Son_2 key="2"/></div>
    : <div><Son_2 key="2" /><Son_1 key="1"/></div>
}
// 这样,this.state.type的值改变时,Son_1和Son2组件的实例没有重新创建,react只是将他们互换位置。

  • 推荐使用index的情况
    • 并不是任何情况使用index作为key会有缺陷,比如如下情况:
你要分页渲染一个列表,每次点击翻页会重新渲染:
使用唯一id:翻页后,三条记录的key和组件都发生了改变,因此三个子组件都会被卸载然后重新渲染。
使用index:翻页后,key不变,子组件值发生改变,组件并不会被卸载,只发生更新。

不推荐使用math.random或者其他的第三方库来生成唯一值作为key。
因为当数据变更后,相同的数据的key也有可能会发生变化,从而重新渲染,引起不必要的性能浪费。
如果数据源不满足我们这样的需求,我们可以在渲染之前为数据源手动添加唯一id,而不是在渲染时添加。

虚拟dom是什么

  • 用原生javascript对象来描述一个dom节点
  • 是真实dom在内存中的表示

react diff原理 如何从 O(n^3)变成 O(n)

  • react团队通过总结规律,为复杂度的降低确定了前提:
    • 若两个组件属于同一个类型,他们拥有相同的dom结构
    • 同一层级的节点可以通过key作为唯一标识,维护节点在不同渲染过程中的稳定性
    • dom节点之间的跨层级操作并不多,同级操作是主流
  • 基于上面的前提,得到react的优化原则
    • diff算法性能突破的关键点在于分层对比
    • 类型一致的节点才有继续diff的必要性
    • key属性的设置可以帮助尽可能的复用同一层级内的节点

虚拟dom比原生dom更快吗?

  • 直接操作dom是非常消耗性能的,但是react使用virtualdom也是无法避免操作dom的
  • 如果是首次渲染,virtualdom不具有任何优势,甚至要进行更多的计算,消耗更多的内存
  • virtualdom的优势在于react的diff算法和批处理策略
  • 在更新的时候,更新的内容比较少时,可以实现精准的定点更新,不需要把全部的dom元素删除重新添加
  • 虚拟dom的优势不是更快,而是:
    • 跨平台
    • 增量更新
    • 处理兼容性问题等
  • 缺点是:
    • 虚拟dom需要消耗额外内存
    • 首次渲染其实并不一定更快

虚拟dom中的$$typeof属性的作用是什么?

  • $$typeof是一个symbol类型的变量,这个变量可以防止XSS。
reactElement中有一个$$typeof属性,它被赋值为REACT_ELEMENT_TYPEvar REACT_ELEMENT_TYPE =(typeof Symbol === 'function' && Symbol.for && Symbol.for('react.element')) ||0xeac7;
  • jsx中插入html结构:
    • 在传统前端开发中经常使用html字符串拼接然后append的方式向页面引入html结构。但是这样的写法在react好像不可以。
    • 原因是:为了防止XSS攻击,对<>等符号做了一层转义,成为escape
    • 如果真的想插入HTML结构而非文本呢?dangerouslySetInnerHTML 是 React 为浏览器 DOM 提供 innerHTML 的替换方案。
const html = '<div>123</div>'
function App(props) {
    return (
        <div
            className="App"
            dangerouslySetInnerHTML={{ __html: html }}
        >
        </div>
    );
}
/*
风险点:
场景:假如前端期望从接口中获取一个字符串渲染页面
render() {
     <div>{serverData.text}</div>
}
然而由于服务端数据入库时存在漏洞,有用户恶意存入了这样的数据
const text = {
     key: null,
     type: 'script',
     props: {src: 'http://...'
}

有风险的节点会直接被渲染到DOM树。

如果这条数据被成功渲染,那么就是一个存在风险的第三方 script 标签入侵到了当前用户的页面,
它能做什么完全取决于它想做什么,
比如获取并发送用户的 cookie、localStorage,比较可爱的情况是给用户的页面上弹十万个弹窗。

*/

  • $$typeof在渲染节点前新增了一步校验,数据库是无法存储 Symbol 类型数据的,所以用户恶意存入的数据是无法带有合法的$$typeof字段的。React渲染时会把没有 $$typeof标识,以及规则校验不通过的组件过滤掉

react组件的渲染流程是什么?

  • 使用react.createElement或者jsx编写react组件,实际上所有jsx代码最后都会在babel的帮助下转成react.createElement
  • react.createElement函数对key,ref等特殊的props进行处理,并获取defaultProps对默认props进行赋值,并且对传入的孩子节点进行处理,最终构造成一个reactElement对象,即所谓的虚拟dom
  • reactDom.render将生成好的虚拟dom渲染到指定容器上,其中采用了批处理/事务等机制并且对特定浏览器进行了性能优化,最终转换成真是dom

为什么代码中一定要引入react?

JSX只是为 React.createElement(component,props,...children)方法提供的语法糖。

所有的 JSX代码最后都会转换成 React.createElement(...), Babel帮助我们完成了这个转换的过程。

所以使用了 JSX的代码都必须引入 React

新版本已经不用了

为什么react组件首字母必须大写?

babel在编译时会判断 JSX中组件的首字母,当首字母为小写时,其被认定为原生 DOM标签, createElement的第一个变量被编译为字符串;当首字母为大写时,其被认定为自定义组件, createElement的第一个变量被编译为对象;

实现虚拟DOM转化为真实DOM

知道HOC和render props吗,它们有什么作用?有什么弊端?

  • 深入浅出React高阶组件

  • React 中的高阶组件及其应用场景

  • 【React深入】从Mixin到HOC再到Hook(原创)

  • render props组件和高阶组件主要是用来实现抽象和可复用性。

  • 弊端就是高阶组件和render props本质上都是将复用逻辑提升到父组件中,很容易产生很多包装组件,带来嵌套地狱。由于所有抽象逻辑都被其他react组件所隐藏,应用变成了一棵没有可读性的组件树,而那些可见的组件也很难在浏览器的dom中进行追踪。

  • render props:

    • React 重温之Render Props
    • 使用场景:通用业务逻辑抽取;当两个平级组件之间需要单向依赖的时候,比如两个同级组件a/b,a组件需要跟随b组件内部状态来改变自己的内部状态,就是a依赖b
// Mouse组件提供“可变数据源”,但是它并不知道自己要渲染什么,它只是一个基础数据的提供者。
class Mouse extends React.Component {
  constructor(props) {
    super(props);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
        <p>The current mouse position is ({this.state.x}, {this.state.y})</p>
        {this.props.render(this.state)} // 这个比较关键
      </div>
    );
  }
}
// 这个Cat组件就是我们希望渲染在页面的UI

class Cat extends React.Component {
  render() {
    const mouse = this.props.mouse;
    return (
      <img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
    );
  }
}

// Cat组件就内嵌在Mouse组件的render函数里
class MouseTracker extends React.Component {
  render() {
    return (
      <div>
        <h1>Move the mouse around!</h1>
        <Mouse render={mouse => (
          <Cat mouse={mouse} />
        )}/>
      </div>
    );
  }
}

  • hoc:
    • 是一个函数,能接受一个组件返回另一个组件
    • 实现hoc的两种方式:
      • 属性代理
      • 反相继承
  • 属性代理:高阶组件通过包裹的react组件来操作props
    • 属性代理的用途
      • 更改 props,可以对传递的包裹组件的WrappedComponent的props进行控制
      • 通过 refs 获取组件实例
      • 把 WrappedComponent 与其它 elements 包装在一起
// 属性代理,把高阶组件接收到的属性传递给传进来的组件
function HOC(WrappedComponent) {
     return class PP extends React.Component {
         render() {
             return <WrappedComponent {...this.props}/>
         }
     }
}

/*
可以通过 ref 获取关键词 this(WrappedComponent 的实例)

当 WrappedComponent 被渲染后,ref 上的回调函数 proc 将会执行,
此时就有了这个 WrappedComponent 的实例的引用
*/
function refsHOC(WrappedComponent) {
     return class RefsHOC extends React.Component {
         proc(wrappedComponentInstance) {
             wrappedComponentInstance.method()
         }
         render() {
             const props = Object.assign({}, this.props, {refthis.proc.bind(this)})
             return <WrappedComponent {...props}/>
         }
     }
}
  • 反向继承:高阶组件继承于被包裹的react组件
    • 反向继承允许高阶组件通过this关键词获取WrappedComponent,意味着他可以获取到state,props,组件生命周期,以及渲染方法render,所以我们主要是用它来做渲染劫持,比如在渲染方法中读取或更改 React Elements tree,或者有条件的渲染等。
function iiHOC(WrappedComponent) {
     return class Enhancer extends WrappedComponent {
         render() { return super.render()}
     }
}
  • 实现一个高阶组件
/*
这个简单的例子里高阶组件只做了一件事,那便是为被包裹的组件添加一个标题样式。
这个高阶组件可以用到任何一个需要添加此逻辑的组件上,
只需要被此高阶组件修饰即可。
由此可以看出,高阶组件的主要功能是封装并抽离组件的通用逻辑,让此部分逻辑在组件间更好地被复用

\
*/
// 1. 在原组件的基础上增加一些东西
export default function withHeader(WrappedComponent) {
    return class HOC extends Component {
        render() {
        return <div>
            <div className="demo-header">
                我是标题
            </div>
            <WrappedComponent {...this.props}/>
            </div>
        }
    }
}
// 使用上面的高阶组件: 在其他组件里,我们引用这个高阶组件,用来强化它。
@withHeader //利用装饰器语法来进行调用
export default class Demo extends Component {
    render() {
        return (
          <div>
            我是一个普通组件
          </div>
        );
    }
}

// 2
/*
但是随之带来的问题是,如果这个高阶组件被使用了多次,那么在调试的时候,
将会看到一大堆HOC,所以这个时候需要做一点小优化,

就是在高阶组件包裹后,应当保留其原有名称。

我们改写一下上述的高阶组件代码,增加了getDisplayName函数以及静态属性displayName,
此时再去观察DOM Tree。
*/
function getDisplayName(component) {
    return component.displayName || component.name || 'Component';
}
export default function (WrappedComponent) {
    return class HOC extends Component {
        static displayName = `HOC(${getDisplayName(WrappedComponent)})`
        render() {
            return <div>
                <div className="demo-header">
                    我是标题
                </div>
                <WrappedComponent {...this.props}/>
            </div>
        }
    }
}
  • 高阶组件的进阶用法
// 1. 组件参数
// 如果传入参数,则传入的参数将作为组件的标题呈现
@withHeader('Demo')
export default class Demo extends Component {
    render() {
        return (
            //...
        );
    }
}

withHeader高阶组件需要被改写成如下形式,它接受一个参数,然后返回一个高阶组件(函数)。
//外面再套一层传入参数
export default function (title) {
    return function (WrappedComponent) {
        return class HOC extends Component {
            render() {
                return <div>
                    <div className="demo-header">
                        {title? title: '我是标题'}
                    </div>
                    <WrappedComponent {...this.props}/>
                    </div>
            }
        }
    }
}


//使用es6的写法:
export default(title) => (WrappedComponent) => class HOC extends Component {
    render() {
        return <div>
            <div className="demo-header">
                {title? title: '我是标题'}
            </div>
            <WrappedComponent {...this.props}/>
        </div>
    }
}

react中的refs

  • refs提供了一种访问render方法中创建的dom节点或者react元素的方法
  • 经常被误解的是只有在类组件中才能使用refs,但是refs也可以通过利用js中的闭包与函数组件一起使用
function CustomForm ({handleSubmit}) {
    let inputElement
    return (
        <form onSubmit={() => handleSubmit(inputElement.value)}>
            <input
                type='text'
                ref={(input) => inputElement = input} 
            />
            <button type='submit'>Submit</button>
        </form>
    )
}
  • 创建refs
    • refs是使用React.createRef()创建的,并通过ref属性附加到react元素上。在构造组件时,通常将refs分配给实例属性,以便在整个组件中引用他们。
class MyComponent extends React.Component {
    constructor(props) {
        super(props);
        this.myRef = React.createRef();
    }
    render() {
        return <div ref={this.myRef} />;
    }
}
// 或者这样用
class UserForm extends Component {
    handleSubmit = () => {
        console.log("Input Value is: ", this.input.value)
    }
    render () {
        return (
            <form onSubmit={this.handleSubmit}>
                <input
                type='text'
                ref={(input) => this.input = input} />
                <button type='submit'>Submit</button>
            </form>
        )
    }
}
  • ref在不同场景下的应用
      1. 组件内使用ref,获取dom元素
      1. ref作为子组件的属性,获取的是该子组件
// 1 组件内使用ref,获取dom元素
class Child extends Component {
    constructor(){
        super();
        this.myDivReact.createRef();
    }
    componentDidMount(){
        console.log(this.myDiv.current)
    }
    render(){
        return <div ref={this.myDiv} >ref获取的dom元素</div>
    }
}

// 2. ref作为子组件的属性,获取的是该子组件
class Child extends Component {
    constructor(){
        super();
    }
    render(){
        return <div>子组件</div>
    }
}

class Parent extends Component{
    constructor(){
        super();
        this.myChild = React.createRef();
    }
    componentDidMount(){
        console.log(this.myChild.current);//获取的是Child组件
    }
    render(){
        return <Child ref={this.myChild}/>
    }
}

// 上个例子中如果想获取子组件中的dom的话,可以做如下修改
class Child extends Component {
    constructor(){
        super();
    }
    render(){
        return <div ref={this.props.myRef}>子组件</div>
    }
}

class Parent extends Component{
    constructor(){
        super();
        this.myChild = React.createRef();
    }
    componentDidMount(){
        console.log(this.myChild.current);//获取的是Child组件里的dom(div)
    }
    render(){
        return <Child myRef={this.myChild}/>
    }
}
  • forwardRef
    • react16新增的方法,返回react组件
const Child = React.forwardRef((props, ref) => {
    return <div ref={ref}>{props.txt}</div>
});

class Parent extends Component {
    constructor(){
        super();
        this.myChild = React.creatRef();
    }
    componentDidMount(){
        console.log(this.myChild.current)//获取的是child组件的div元素
    }
    render(){
        return <Child ref={this.myChild} txt=“parent props txt"/>
    }
}

绑定事件处理函数的this问题

  • 在以类继承的方法定义的组件中,事件处理函数的this指向的并不是当前组件。为什么?
    • 这不是react的问题,是javascript本来就有的。如果传递一个函数名给一个变量,然后通过在变量后加括号来调用这个方法,此时方法内部的this的指向就会丢失
    • 在react中(或者说JSX中),传递的事件参数不是一个字符串,而是一个实实在在的函数:这样说,react中的事件名(如onClick,onChange)就相当于上面例子中的中间变量,react在事件发生时调用onClick,由于onClick只是中间变量,所以处理函数中的this指向会丢失。其实真正调用的时候并不是this.handleClick(),如果是这样调用那么this指向就不会有问题,真正调用时是onClick()。
let obj = {
    tmp: 'yes',
    testLog:function(){ console.log(this.tmp) }
};
obj.testLog();   //yes
let tmpLog = obj.testLog;
tmpLog();   //undefined

绑定事件处理函数的this到当前组件,有四种方法:

  • 通过bind方法进行原地绑定,从而改变this指向:
    • <a href="#" onClick={this.addOneClick.bind(this)}>点我啊</a>
    • 这种方式写起来比较简单,但是每次执行bind方法都会生成一个新的函数,势必会有额外的开销,所以不是一种好的方法
  • 通过箭头函数:
    • <a href="#" onClick={e => this.addOneClick(e)}>点我啊</a>
    • 写法不麻烦,但仍然会有一丢丢性能开销的问题
  • 在constructor中提前对事件进行绑定:
    • 这种写法自带尿点。虽然没有额外开销,但写起来有些复杂。以后要对每个事件都要在构造器中再进行一次绑定。
  • 将事件的写法改成箭头函数的形式
constructor(props) {
    super(props);
    this.state = {
        name:"老大",
        age: 0
    };
    //在此处提前绑定
    this.addOneClick=this.addOneClick.bind(this);
}

//在此处采用箭头函数写法
addOneClick=(e) => {
    e.preventDefault();
    this.setState({ age: this.state.age + 1 })
}

向事件处理函数传递参数

// 通过箭头函数
<a href="#" onClick={(e) => this.addOneClick(2, e)}>点我啊</a>
// 通过bind
<a href="#" onClick={this.addOneClick.bind(this,2)}>点我啊</a>

react事件机制

  • react事件并没有绑定在真实的dom节点上,而是通过事件代理,在最外层的document上对事件进行统一分发。
关于event参数
 <a onClick={this.click}>确定</a>
 click = (event) => {}
 当onClick函数什么参数都不传时,click函数中参数默认是event
 event.preventDefault() 阻止默认行为(阻止<a>的跳转行为)
 event.stopPropergation() 阻止冒泡
 event.target 指向当前元素,即当前元素触发 <a></a> 引起触发事件的元素
 event.currentTarget 事件绑定的元素 指向当前元素<a></a>,假象!!!
 event其实是react封装的,可以看__proto__.constructorSyntheticBaseEvent,不是原生的event
 原生event,__proto__.constructorMouseEvent
 event.nativeEvent 获取原生event
 event.nativeEvent.target 指向当前元素,即当前元素触发 <a></a> 引起触发事件的元素
 event.nativeEvent.currentTarget 指向document(react事件绑定 全是在document上) !!!
 总结:
 a.event是一个合成事件,模拟出来dom事件所有能力
 b.event.nativeEvent 是原生事件对象
 c.所有的事件都被挂载在document上
 d.和dom事件不一样,和vue事件也不一样

原生事件和react事件的区别

  • React 事件使用驼峰命名,而不是全部小写。
  • 通过 JSX , 你传递一个函数作为事件处理程序,而不是一个字符串。
  • 在 React 中你不能通过返回 false 来阻止默认行为。必须明确调用 preventDefault。

react和原生事件的执行顺序是什么?可以混用吗?

react的所有事件都是通过document进行分发,当真实dom触发事件后,冒泡到document,才会对react事件进行处理

所以原生事件会先执行,然后执行react合成事件,最后执行真正在document上挂载的事件

React事件和原生事件最好不要混用。原生事件中如果执行了 stopPropagation方法,则会导致其他 React事件失效。因为所有元素的事件将无法冒泡到 document上,导致所有的 React事件都将无法被触发。。

在一定场景下可以使用原生事件帮助处理事件逻辑,但应尽量减少合成事件和原生事件的混用,或者使用e.target来判断事件监听器的真实元素,来避免原生事件的冒泡。

Portals

  • Portals使用场景:
    • 1.overflow: hidden
    • 2.父组件z-index太小
    • 3.fixed需要放在body的第一层
Portals 传送门
1.组件默认会按照既定层次嵌套渲染
2.如何让组件渲染到父组件以外?

render() {
    // 使用 Portals 渲染到 body 上。
    // fixed 元素要放在 body 上,有更好的浏览器兼容性。
    return ReactDom.createPortal(
        <div className="modal">{this.props.children}</div>,
        document.body //第二个参数:dom节点
    )
}

react hooks

简单介绍下什么是hooks,hooks产生的背景?hooks的优点?

  • hooks是针对在使用react时存在以下问题而产生的:
    • 组件之间复用状态逻辑很难,在hooks之前,实现组件复用,一般采用高阶组件和render props,他们的本质是将复用逻辑提升到父组件,很容易产生很多包装组件,带来嵌套地狱
    • 组件逻辑变得越来越复杂,尤其是生命周期函数中常常包含一些不相关的逻辑。完全不相关的逻辑却在同给一个方法中组合在一起。很容易产生bug,并且导致逻辑不一致
    • 复杂的class组件,使用class组件需要理解javascript中this的工作方式,不能忘记绑定事件处理器等操作,代码复杂且冗余。除此之外,class组件也会让一些react优化措施失效
  • 针对这些问题,react团队研发了hooks, 他主要有两方面作用:
    • 用于在函数组件中引入状态管理和生命周期方法
    • 取代高阶组件和render props来实现抽象和可复用性
  • 优点:
    • 避免在被广泛使用的函数组件在后期迭代过程中,需要承担一些副作用,而必须重构成类组件,帮助函数组件引入状态管理和生命周期函数
    • hooks出现之后,我们将复用逻辑提取到组件顶层,而不是强行提升到父组件。这样就能够避免hoc和render props带来的嵌套地狱
    • 避免上面陈述的class组件带来的那些问题

hooks和hoc和render props有什么不同?

  • 最大的不同在于:后两者仅仅是一种开发模式,hooks是react提供的api模式,既能自然的融入到react的渲染过程也更加符合react的函数式编程理念

介绍下常用的hooks

  • useState:状态钩子,为函数组件提供内部状态
  • useContext:共享钩子,该钩子的作用是在组件之间共享状态,可以解决react逐层通过props传递数据,他接受React.createContext()的返回结果,使用useContext将不再需要Consumer
  • useReducer:action钩子,useReducer提供了状态管理,其原理是通过用户在页面发起action,从而通过reducer方法改变state,实现页面和状态的通信,很像redux
  • useEffect:副作用钩子,接受两个参数,第一个是进行的异步操作, 第二个是数组,用来给出Effect的依赖项
  • useRef:获取组件实例;渲染周期之间共享数据的存储(state不能存储跨渲染周期的数据,因为state的保存会触发组件重渲染)。useRef传入一个参数initValue,并创建一个对象{ current: initValue }给函数组件使用,在整个生命周期中该对象保持不变。
  • useMemo/useCallback:可缓存函数的引用或值。useMemo用在计算值的缓存,注意不用滥用。经常用在下面两种场景(要保持引用相等;对于组件内部用到的 object、array、函数等,如果用在了其他 Hook 的依赖数组中,或者作为 props 传递给了下游组件,应该使用 useMemo/useCallback)
  • useLayoutEffect:会在所有的 DOM 变更之后同步调用 effect,可以使用它来读取 DOM 布局并同步触发重渲染

描述下hooks下怎么模拟生命周期函数,模拟的生命周期和class中的生命周期有什么区别?

componentDidMount:useEffect(() =>{}, []); //必须加[],不然会默认每次渲染都执行


componentDidUpdate:
useEffect(()=>{
     document.title = `You clicked ${count} times`;
     return()=>{ // 以及 componentWillUnmount 执行的内容 } }
, [count])


// shouldComponentUpdate, 只有 Parent 组件中的 count state 更新了,Child 才会重新渲染,否则不会。
function Parent() {
     const [count, setCount] = useState(0);
     const child = useMemo(()=> <Child count={count} />, [count]);
     return <>{count}</>
}

function Child(props) {
     return <div>Count:{props.count}</div>
}

这里有一个点需要注意,就是默认的useEffect(不带[])中return的清理函数,
它和componentWillUnmount有本质区别的,默认情况下return,
在每次useEffect执行前都会执行,并不是只有组件卸载的时候执行。

useEffect在副作用结束之后,会延迟一段时间执行,并非同步执行,
和compontDidMount有本质区别。遇到dom操作,最好使用useLayoutEffect。

hooks中的坑,以及为什么?

  • 不要在循环/条件/嵌套函数中调用hook,必须始终在react函数的顶层使用hook。这是因为react需要利用调用顺序来正确更新相应的状态,以及调用相应的钩子函数。一旦在循环或者条件分支语句中调用hook,就容易导致调用顺序的不一致性,产生难以预料的后果。

  • 使用useState的时候,使用push/pop/slice等直接更改数组对象的坑,demo中使用push直接更改数组无法获取到新值,应该采用析构方式,但是在class里面不会有这个问题

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)
}

props向state传值的问题: www.jianshu.com/p/13d82810e…

  • 比如你的state依赖于props,这个时候useState无法感知props的变化,那么props更新 state并不会更新,useState相当于class组件中的constructor,只执行一次,如果state想继续同步props,那么可以使用useEffect

  • useEffect 包含了哪几个生命周期?

    • componentDidMount、componentDidUpdate 、 componentWillUnmount
  • useEffect 在什么时候执行?

    • useEffect会在第一次渲染之后和每次更新渲染之后都会执行,并且在DOM渲染结束之后执行
  • useEffect 死循环?

    • 说明useEffect在传入第二个参数时一定注意:第二个参数不能为引用类型,会造成死循环,比如 []===[] 为false,所以会造成useEffect会一直不停的渲染,因此把data的初始值改为undefined,就可以
  • 函数作为依赖的时候死循环?

    • 提到组件外面,或者用useCallback包一层。useMemo 可以做类似的事情以避免重复生成对象。
  • useEffect 里面拿不到最新的props和state?

    • 使用refs

useEffect和useLayoutEffect区别?

useEffect是render结束后,callback函数执行,但是不会阻断浏览器的渲染,算是某种异步的方式吧。但是class的componentDidMount 和componentDidUpdate是同步的,在render结束后就运行,useEffect在大部分场景下都比class的方式性能更好.

useLayoutEffect里面的callback函数会在DOM更新完成后立即执行,但是会在浏览器进行任何绘制之前运行完成,阻塞了浏览器的绘制.