1.React
中 keys
的作用是什么?
Keys 是 React 用于追踪哪些列表中元素被修改、被添加或者被移除的辅助标识。
render () { return ( ‹ul› { this.state.list.map(({item, key}) =› { return ‹li key={key}›{item}‹/li› }) } ‹/ul› ) }
在开发过程中,我们需要保证某个元素的 key 在其同级元素中具有唯一性。在 React Diff
算法中 React
会借助元素的 Key
值来判断该元素是新近创建的还是被移动而来的元素,从而减少不必要的元素重渲染。此外,React
还需要借助 Key
值来判断元素与本地状态的关联关系,因此我们绝不可忽视转换函数中 Key
的重要性。
2.当你调用 setState
的时候,发生了什么事?
将传递给 setState
的对象合并到组件的当前状态,这将启动一个和解的过程,构建一个新的 react
元素树,与上一个元素树进行对比( diff
),从而进行最小化的重渲染。
3.为什么setState
的参数是一个 callback
而不是一个对
因为 this.props
和 this.state
的更新可能是异步的,不能依赖它们的值去计算下一个 state
。
4.状态(state
)和属性(props
)之间有何区别
State
是一种数据结构,用于组件挂载时所需数据的默认值。
State
可能会随着时间的推移而发生突变,但多数时候是作为用户事件行为的结果。
Props
(properties 的简写)则是组件的配置。props
由父组件传递给子组件,并且就子组件而言,props
是不可变的(immutable)。
组件不能改变自身的 props
,但是可以把其子组件的 props 放在一起(统一管理)。Props
也不仅仅是数据--回调函数也可以通过 props 传递。
5.应该在 React
组件的何处发起 Ajax
请求?
在 React
组件中,应该在 componentDidMount
中发起网络请求。这个方法会在组件第一次“挂载”(被添加到 DOM
)时执行,在组件的生命周期中仅会执行一次。
更重要的是,你不能保证在组件挂载之前 Ajax
请求已经完成,如果是这样,也就意味着你将尝试在一个未挂载的组件上调用 setState
,这将不起作用。 在 componentDidMount
中发起网络请求将保证这有一个组件可以更新了。
6.React
中的三种构建组件的方式?
React.createClass()
、ES6 class
和无状态函数。
7.React
中 refs
的作用是什么?
Refs 是
React提供给我们的安全访问
DOM` 元素或者某个组件实例的句柄。
我们可以为元素添加 ref 属性然后在回调函数中接受该元素在 DOM 树中的句柄,该值会作为回调函数的第一个参数返回:
class CustomForm extends Component { handleSubmit = () =› { console.log('Input Value: ', this.input.value); }; render() { return ( ‹form onSubmit={this.handleSubmit}› ‹input type='text' ref={input =› (this.input = input)} /› ‹button type='submit'›Submit‹/button› ‹/form› ); } }
上述代码中的 input
域包含了一个 ref
属性,该属性声明的回调函数会接收 input
对应的 DOM
元素,我们将其绑定到 this指针以便在其他的类函数中使用。
另外值得一提的是,refs
并不是类组件的专属,函数式组件同样能够利用闭包暂存其值:
function CustomForm({ handleSubmit }) { let inputElement; return ( ‹form onSubmit={() =› handleSubmit(inputElement.value)}› ‹input type='text' ref={input =› (inputElement = input)} /› ‹button type='submit'›Submit‹/button› ‹/form› ); }
8.请描述下react diff
原理(常考,大厂必考)?
把树形结构按照层级分解,只比较同级元素。
给列表结构的每个单元添加唯一的 key
属性,方便比较。
React
只会匹配相同 class
的 component(这里面的 class
指的是组件的名字) 合并操作,调用 component
的 setState
方法的时候, React
将其标记为 dirty
. 到每一个事件循环结束, React
检查所有标记 dirty
的 component
重新绘制. 选择性子树渲染。开发人员可以重写 shouldComponentUpdate
提高 diff
的性能。
9.说说React
优势?
1、React
速度很快:它并不直接对 DOM
进行操作,引入了一个叫做虚拟 DOM
的概念,安插在 javascript
逻辑和实际的 DOM
之间,性能好。
2、跨浏览器兼容:虚拟 DOM
帮助我们解决了跨浏览器问题,它为我们提供了标准化的 API
,甚至在 IE8
中都是没问题的。
3、一切都是 component
:代码更加模块化,重用代码更容易,可维护性高。
4、单向数据流:Flux
是一个用于在 JavaScript
应用中创建单向数据层的架构,它随着 React
视图库的开发而被 Facebook 概念化。
5、同构、纯粹的 javascript
:因为搜索引擎的爬虫程序依赖的是服务端响应而不是 JavaScript
的执行,预渲染你的应用有助于搜索引擎优化。
6、兼容性好:比如使用 RequireJS
来加载和打包,而 Browserify
和 Webpack
适用于构建大型应用。它们使得那些艰难的任务不再让人望而生畏。
10.说说 react
生命周期函数?
初始化阶段:
getDefaultProps
:获取实例的默认属性
getInitialState
:获取每个实例的初始化状态
componentWillMount
:组件即将被装载、渲染到页面上
render
:组件在这里生成虚拟的 DOM
节点
componentDidMount
:组件真正在被装载之后 运行中状态:
componentWillReceiveProps
:组件将要接收到属性的时候调用
shouldComponentUpdate
:组件接受到新属性或者新状态的时候(可以返回 false
,接收数据后不更新,阻止 render
调用,后面的函数不会被继续执行了) componentWillUpdate
:组件即将更新不能修改属性和状态
render
:组件重新描绘
componentDidUpdate
:组件已经更新
销毁阶段:
componentWillUnmount
:组件即将销毁
11.React
组件中怎么做事件代理理
虽然很多资料都说 React
的事件是会被代理到 document
上,但是我翻遍了官网,也没有找到相应的说明。
那么,有什么办法能够证明它吗?我想到了一个方法 → 通过 Chrome
浏览器的 Event Listeners
面板查看元素的绑定事件,具体的使用方法请参考官网文档。
从图中我们可以看到:
1.#child
元素绑定了两个点击事件,一个是通过 React
绑定的,一个是通过 addEventListener
绑定的。 2.通过 addEventListener
绑定的事件是真的绑定到 #child
元素上。 3.通过 React
绑定的事件,其实是代理绑定到 document
上。
React 模拟 DOM 事件冒泡机制
观察下面这个例子:#child
和 #parent
都绑定了一个点击事件。
由图中可以看出:点击 #child
的同时,也触发了 #parent
的点击事件,看起来“很像” DOM 的事件冒泡机制。然而,实际原理并非如此,因为按照 React
的事件代理,#child
和 #parent
绑定的事件本来就是代理到 document
上的。也就是说,只有当事件流冒泡到 document
上时,才会依次触发 document
上绑定的两个事件。
到此为止,我以为我终于搞明白这块了,后来我发现我还是错了。如果说 #child
和 #parent
的事件都代理到 document
上的话,那么在 Event Listeners
面板中,我们应该能看到 2 个绑定在 document
上的事件,但实际上只有 1 个,如下图所示。
因此,我们可以得出结论:并非 #child
和 #parent
的事件分别代理到 document
上,而是 React
在 document
上绑定了一个 dispatchEvent
函数(事件),在执行 dispatchEvent
的过程中,其内部会依次执行 #child
和 #parent
上绑定的事件。请注意,虽然 dispatchEvent
和代理到 document
上这两种方式的表现结果一样,但是其本质是有很大差别的,后边我们结合到 stopImmediatePropagation
的时候便会讲到。
那么这个 dispatchEvent
函数又是如何做到依次触发 #child
和 #parent
的事件的呢?我无力研究 React
这部分的源码,只好自己猜想了一下,其伪代码可能是这样子:
function dispatchEvent(event) { let target = event.target; target.click && target.click(); // 触发点击元素的事件 while (target.parentNode) { // 沿 DOM 向上回溯,遍历父节点,触发其 click 事件 target.parentNode.click && target.parentNode.click(); target = target.parentNode; } }
这应该便是 React
模拟 DOM
事件冒泡的大致原理。
React 禁止事件冒泡
既然有“事件冒泡”,就得有相应的禁止它的方法,这一点 React
的官网中便有提到:通过 React
绑定的事件,其回调函数中的 event
对象,是经过 React
合成的 SyntheticEvent
,与原生的 DOM 事件的 event 不是一回事。准确地说,在 React
中,e.nativeEvent
才是原生 DOM 事件的那个 event
,虽然 React
的合成事件对象也同样实现了 stopPropagation
接口。
因此,在 React
中,想要阻止“事件冒泡”(再强调一次,React
只是模拟事件冒泡,并非真正的 DOM
事件冒泡),只需要在回调函数中调用 e.stopPropagation
。请注意,这时候的 e.stopPropagation
非原生事件对象的 stopPropagation
。
以上这些都是官网中已经有的,那本文又有什么新意呢?请看下面的例子:#child
、#parent
和 document
上都绑定了事件,如何做到只触发 #child
上的事件?
我们来尝试解释一下上图中的现象:
事件流首先进入到 #child
,然后触发直接绑定在 #child
上的事件;
事件流沿着 DOM 结构向上冒泡到 document
,触发 React
绑定的 dispatchEvent
函数,从而调用了 #child
子元素上绑定的 clickChild
方法。
在 clickChild
方法的最后,我调用了 e.stopPropagation
,成功地阻止了 React
模拟的事件冒泡,因此,成功地没有触发 #parent
上的事件。
然后,最后出现了问题,还是触发了 document 上直接绑定的事件。我想要的是:”点击 #child
,只触发 #child
上的事件,不要触发任何其他元素的事件,包括 document
,我应该怎么做呢? → 答案是:调用e.nativeEvent.stopImmediatePropagation
上述过程用图解的方式来分析,我们能理解得清楚一些。
React
合成事件对象的e.stopPropagation
,只能阻止 React
模拟的事件冒泡,并不能阻止真实的 DOM
事件冒泡,更加不能阻止已经触发元素的多个事件的依次执行。在这种情况下,只有原生事件对象的 stopImmediatePropagation
能做到。
你可能会说:”既然 React
在合成事件对象中封装了 stopPropagation
,为什么不把 stopImmediatePropagation
也一并封装了呢?“
我的猜测是:”因为在 React
中不允许给同一个组件绑定多个相同类型的事件,如果非要重复绑定,那么后绑定的会覆盖前绑定的,这是它的设计思路。在这种设计思路下,不会存在某个组件有多个同类型的事件会依次触发,自然便不需要 stopImmediatePropagation
了。
总结
对于 React
的合成事件对象 e
来说:
e.stopPropagation
→ 用来阻止 React 模拟的事件冒泡
e.stopImmediatePropagation
→ 没有这个函数
e.nativeEvent.stopPropagation
→ 原生事件对象的用于阻止 DOM
事件的进一步捕获或者冒泡
e.nativeEvent.stopImmediatePropagation
→ 原生事件对象的用于阻止 DOM
事件的进一步捕获或者冒泡,且该元素的后续绑定的相同事件类型的事件也被一并阻止。
12.this
的各种情况
call apply bind指的this是谁就是谁(bind不会调用,只会将当前的函数返回)
fun.call(obj,a,b)
fun.apply(obj,[ ])
fun.bind(obj,a,b)()
this的情况:
1.以函数形式调用时,this
永远都是window
2.以方法的形式调用时,this
是调用方法的对象
3.以构造函数的形式调用时,this
是新创建的那个对象
4.使用call
和apply
调用时,this
是指定的那个对象
5.箭头函数:箭头函数的this
看外层是否有函数
1)如果有,外层函数的this
就是内部箭头函数的this
2)如果没有,就是window
6.特殊情况:通常意义上this
指针指向为最后调用它的对象。这里需要注意的一点就是如果返回值是一个对象,那么this
指向的就是那个返回的对象,如果返回值不是一个对象那么this
还是指向函数的实例
13.介绍Promise
,异常捕获,如何进行异常处理?
Promise
是解决回调地狱的好工具,比起直接使用回调函数promise
的语法结构更加清晰,代码的可读性大大增加。
但是想要在真是的项目中恰当的运用promise
可不是随便写个Demo这个简单的,如果运用不当反而会增加代码的复杂性。
使用Promise
经常遇到的问题
老旧浏览器没有Promise全局对象增么办?
可以使用es6-promise-polyfill
。es6-promise-polyfill
可以使用页面标签直接引入,当然也可以通过es6
的import
方法引入。
引入这个polyfill
之后,它会在window
对象中加入Promise
对象。这样我们就可以全局使用Promise
了。
如何进行异常处理?
参照promise
的文档我们可以在reject
回调和catch
中处理异常。但是promise
规定如果一个错误在reject
函数中被处理,那么promise
将从异常常态中恢复过来。这意味着接下来的the
n方法将接收到一个resolve
回调。
大多数时候我们希望发生错误的时候,promise
处理当前的异常并中断后续的then
操作。 我们先来看一个使用reject
处理异常的例子
var promiseStart = new Promise(function(resolve, reject){ reject('promise is rejected'); }); promiseStart.then(function(response) { console.log('resolved'); return new Promise(function(resolve, reject){ resolve('promise is resolved'); }); }) .then(function (response){ console.log('resolved:', response); }) .catch(function(error) { console.log('catched:', error); })
输出: catched: promise is rejected
14.React
怎么做数据的检查和变化
props
:组件属性,专门用来连接父子组件间通信,父组件传输父类成员,子组件可以利用但不能编辑父类成员;
state
:专门负责保存和改变组件内部的状态;
数据传递
在React
中,父组件给子组件传递数据时,通过给子组件设置props
的方式,子组件取得props
中的值,即可完成数据传递.被传递数据的格式可以是任何js
可识别的数据结构
props
一般只作为父组件给子组件传递数据用,不要试图去修改自己的props
数据改变
props
不能被自身修改,如果组建内部的属性发生变化使用state
this.setState({ ... })
React
会实时监听每个组件的props
和state
的值,一旦有变化,会立刻更新组件,将结果重新渲染到页面上,state
,props
15.介绍常见的react
优化方式
React
渲染性能优化的三个方向,其实也适用于其他软件开发领域,这三个方向分别是:
1.减少计算的量。 -> 对应到 React
中就是减少渲染的节点 或者 降低组件渲染的复杂度
2.利用缓存。-> 对应到 React
中就是如何避免重新渲染,利用函数式编程的 memo
方式来避免组件重新渲染
3.精确重新计算的范围。 对应到 React 中就是绑定组件和状态关系, 精确判断更新的'时机'和'范围'. 只重新渲染'脏'的组件,或者说降低渲染范围
目录
减少渲染的节点/降低渲染计算量(复杂度)
0️⃣ 不要在渲染函数都进行不必要的计算
1️⃣ 减少不必要的嵌套
2️⃣ 虚拟列表
3️⃣ 惰性渲染
4️⃣ 选择合适的样式方案
避免重新渲染
0️⃣ 简化 props
1️⃣ 不变的事件处理器
2️⃣ 不可变数据
3️⃣ 简化 state
4️⃣ 使用 recompose 精细化比对
精细化渲染
0️⃣ 响应式数据的精细化渲染
1️⃣ 不要滥用 Context
扩展
减少渲染的节点/降低渲染计算量(复杂度) 首先从计算的量上下功夫,减少节点渲染的数量或者降低渲染的计算量可以显著的提高组件渲染性能。
0️⃣ 不要在渲染函数都进行不必要的计算
比如不要在渲染函数(render)中进行数组排序、数据转换、订阅事件、创建事件处理器等等. 渲染函数中不应该放置太多副作用
1️⃣ 减少不必要的嵌套
所以我们需要理性地选择一些工具,比如使用原生的 CSS,减少 React 运行时的负担.
一般不必要的节点嵌套都是滥用高阶组件/RenderProps 导致的。所以还是那句话‘只有在必要时才使用 xxx’。 有很多种方式来代替高阶组件/RenderProps,例如优先使用 props、React Hooks
2️⃣ 虚拟列表
虚拟列表是常见的‘长列表'和'复杂组件树'优化方式,它优化的本质就是减少渲染的节点。
虚拟列表常用于以下组件场景:
无限滚动列表,grid, 表格,下拉列表,spreadsheets 无限切换的日历或轮播图 大数据量或无限嵌套的树 聊天窗,数据流(feed), 时间轴 等等
3️⃣ 惰性渲染
惰性渲染的初衷本质上和虚表一样,也就是说我们只在必要时才去渲染对应的节点。
举个典型的例子,我们常用 Tab 组件,我们没有必要一开始就将所有 Tab 的 panel 都渲染出来,而是等到该 Tab 被激活时才去惰性渲染。
还有很多场景会用到惰性渲染,例如树形选择器,模态弹窗,下拉列表,折叠组件等等。
这里就不举具体的代码例子了,留给读者去思考.
4️⃣ 选择合适的样式方案
所以在样式运行时性能方面大概可以总结为:CSS
> 大部分CSS-in-js
> inline style
避免重新渲染 减少不必要的重新渲染也是 React
组件性能优化的重要方向. 为了避免不必要的组件重新渲染需要在做到两点:
保证组件纯粹性。即控制组件的副作用,如果组件有副作用则无法安全地缓存渲染结果 通过shouldComponentUpdate
生命周期函数来比对 state
和 props
, 确定是否要重新渲染。对于函数组件可以使用React.memo
包装 另外这些措施也可以帮助你更容易地优化组件重新渲染:
0️⃣ 简化 props ① 如果一个组件的 props 太复杂一般意味着这个组件已经违背了‘单一职责’,首先应该尝试对组件进行拆解. ② 另外复杂的 props 也会变得难以维护, 比如会影响shallowCompare效率, 还会让组件的变动变得难以预测和调试.
下面是一个典型的例子, 为了判断列表项是否处于激活状态,这里传入了一个当前激活的 id:
这是一个非常糟糕的设计,一旦激活 id 变动,所有列表项都会重新刷新. 更好的解决办法是使用类似actived这样的布尔值 prop. actived 现在只有两种变动情况,也就是说激活 id 的变动,最多只有两个组件需要重新渲染.
简化的 props 更容易理解, 且可以提高组件缓存的命中率
1️⃣ 不变的事件处理器 ①避免使用箭头函数形式的事件处理器, 例如:
<ComplexComponent onClick={evt => onClick(evt.id)} otherProps={values} />
假设 ComplexComponent
是一个复杂的 PureComponent
, 这里使用箭头函数,其实每次渲染时都会创建一个新的事件处理器,这会导致 ComplexComponent
始终会被重新渲染.
更好的方式是使用实例方法:
class MyComponent extends Component { render() { <ComplexComponent onClick={this.handleClick} otherProps={values} />; } handleClick = () => { /*...*/ }; }
② 即使现在使用hooks
,我依然会使用useCallback
来包装事件处理器,尽量给下级组件暴露一个静态的函数:
const handleClick = useCallback(() => { /*...*/ }, []); return <ComplexComponent onClick={handleClick} otherProps={values} />;
但是如果useCallback
依赖于很多状态,你的useCallback
可能会变成这样:
const handleClick = useCallback(() => { /*...*/ // }, [foo, bar, baz, bazz, bazzzz]);
这种写法实在让人难以接受,这时候谁还管什么函数式非函数式的。我是这样处理的:
function useRefProps<T>(props: T) { const ref = useRef < T > props; // 每次渲染更新props useEffect(() => { ref.current = props; }); } function MyComp(props) { const propsRef = useRefProps(props); // 现在handleClick是始终不变的 const handleClick = useCallback(() => { const { foo, bar, baz, bazz, bazzzz } = propsRef.current; // do something }, []); }
③设计更方便处理的 Event Props
. 有时候我们会被逼的不得不使用箭头函数来作为事件处理器:
<List> {list.map(i => ( <Item key={i.id} onClick={() => handleDelete(i.id)} value={i.value} /> ))} </List>
上面的 onClick
是一个糟糕的实现,它没有携带任何信息来标识事件来源,所以这里只能使用闭包形式,更好的设计可能是这样的:
// onClick传递事件来源信息 const handleDelete = useCallback((id: string) => { /*删除操作*/ }, []); return ( <List> {list.map(i => ( <Item key={i.id} id={i.id} onClick={handleDelete} value={i.value} /> ))} </List> );
如果是第三方组件或者 DOM
组件呢? 实在不行,看能不能传递data-*
属性:
const handleDelete = useCallback(event => { const id = event.dataset.id; /*删除操作*/ }, []); return ( <ul> {list.map(i => ( <li key={i.id} data-id={i.id} onClick={handleDelete} value={i.value} /> ))} </ul> );
2️⃣ 不可变数据 不可变数据可以让状态变得可预测,也让 shouldComponentUpdate
'浅比较'变得更可靠和高效.
相关的工具有Immutable.js
、Immer
、immutability-helper
以及 seamless-immutable
。
3️⃣ 简化 state
不是所有状态都应该放在组件的 state
中. 例如缓存数据。按照我的原则是:如果需要组件响应它的变动, 或者需要渲染到视图中的数据才应该放到 state
中。这样可以避免不必要的数据变动导致组件重新渲染.
4️⃣ 使用 recompose 精细化比对
尽管 hooks
出来后,recompose
宣称不再更新了,但还是不影响我们使用 recompose
来控制shouldComponentUpdate
方法, 比如它提供了以下方法来精细控制应该比较哪些 props:
/* 相当于React.memo */ pure() /* 自定义比较 */ shouldUpdate(tes