1.说说你对react的理解?有哪些特性?
React,用于构建用户界面的 JavaScript 库,只提供了 UI 层面的解决方案
遵循组件设计模式、声明式编程范式和函数式编程概念,以使前端应用程序更高效
使用虚拟 DOM 来有效地操作 DOM,遵循从高阶组件到低阶组件的单向数据流
帮助我们将界面成了各个独立的小块,每一个块就是组件,这些组件之间可以组合、嵌套,构成整体页面
react 类组件使用一个名为 render() 的方法或者函数组件return,接收输入的数据并返回需要展示的内容
特性:
JSX 语法
单向数据绑定
虚拟 DOM
声明式编程
Component
优势:
高效灵活
声明式的设计,简单使用
组件式开发,提高代码复用率
单向响应的数据流会比双向绑定的更安全,速度更快
2. 说说对Diff算法理解
Diff算法作用:
由于渲染真实DOM的开销很大,有时候我们修改了某个数据,直接渲染到真实dom上会引起整个dom树的重绘和重排。我们希望只更新我们修改的那一小块dom,而不是整个dom,diff算法就帮我们实现了这点
Diff算法的本质:找出两对象(虚拟dom树)之间的差异,目的是尽可能地做到节点可复用
Diff策略:
tree diff:Web UI中DOM节点跨层级地移动操作特别少,可以忽略不计
component diff:拥有相同类地两个组件生成相似的树形结构,拥有不同类地两个组件生成不同的树形结构
element diff:对于同一层级地一组子节点,通过唯一id区分
Tree Diff
(1)React通过updateDepth对Virtual DOM树进行层级控制。
(2)对树分层比较,两棵树只对同一层次节点进行比较。如果该节点不存在时,则该节点及其子节点会被完全删除,不会再进一步比较。
(3)只需遍历一次,就能完成整棵DOM树的比较。
如果DOM 节点出现了跨层级操作,Diff会怎么办?
Tree DIFF是对树的每一层进行遍历,如果某组件不存在了,则会直接销毁。如图所示,左边是旧属,右边是新属,第一层是R组件,一模一样,不会发生变化;第二层进入Component DIFF,同一类型组件继续比较下去,发现A组件没有,所以直接删掉A、B、C组件;继续第三层,重新创建A、B、C组件。
如上图所示,以A为根节点的整棵树会被重新创建,而不是移动,因此 官方建议不要进行DOM节点跨层级操作,可以通过CSS隐藏、显示节点,而不是真正地移除、添加DOM节点。
Component Diff
React对不同的组件间的比较,有三种策略
(1)同一类型的两个组件,按原策略(层级比较)继续比较Virtual DOM树即可。
(2)同一类型的两个组件,组件A变化为组件B时,可能Virtual DOM没有任何变化,如果知道这点(变换的过程中,Virtual DOM没有改变),可节省大量计算时间,所以用户可以通过 shouldComponentUpdate() 来判断是否需要判断计算。
(3)不同类型的组件,将一个(将被改变的)组件判断为dirtycomponent(脏组件),从而替换整个组件的所有节点。
注意:如上图所示,当组件D变为组件G时,即使这两个组件结构相似,一旦React判断D和G是不用类型的组件,就不会比较两者的结构,而是直接删除组件D,重新创建组件G及其子节点。虽然当两个组件是不同类型但结构相似时,进行diff算法分析会影响性能,但是毕竟不同类型的组件存在相似DOM树的情况在实际开发过程中很少出现,因此这种极端因素很难在实际开发过程中造成重大影响。
Element Diff
当节点处于同一层级时,diff提供三种节点操作:删除、插入、移动。
插入:组件 C 不在集合(A,B)中,需要插入
删除:
(1)组件 D 在集合(A,B,D)中,但 D的节点已经更改,不能复用和更新,所以需要删除 旧的D ,再创建新的。
(2)组件D之前在集合(A,B,D)中,但集合变成新的集合(A,B)了,D 就需要被删除。
移动:组件D已经在集合(A,B,C,D)里了,且集合更新时,D没有发生更新,只是位置改变,如新集合(A,D,B,C),D在第二个,无须像传统diff,让旧集合的第二个B和新集合的第二个D 比较,并且删除第二个位置的B,再在第二个位置插入D,而是 (对同一层级的同组子节点) 添加唯一key进行区分,移动即可。
移动:新旧集合中存在相同节点但位置不同时,如何移动节点
(1)B不移动,不赘述,更新l astIndex=1
(2)新集合取得 E,发现旧不存在,故在lastIndex=1的位置 创建E,更新lastIndex=1
(3)新集合取得C,C不移动,更新lastIndex=2
(4)新集合取得A,A移动,同上,更新lastIndex=2
(5)新集合对比后,再对旧集合遍历。判断 新集合 没有,但 旧集合 有的元素(如D,新集合没有,旧集合有),发现 D,删除D,diff操作结束。
Diff开发建议:
基于tree diff:
开发组件时,注意保持DOM结构的稳定;即,尽可能少地动态操作DOM结构,尤其是移动操作。
当节点数过大或者页面更新次数过多时,页面卡顿的现象会比较明显。
这时可以通过 CSS 隐藏或显示节点,而不是真的移除或添加 DOM 节点。
基于component diff:
注意使用 shouldComponentUpdate() 来减少组件不必要的更新。
对于类似的结构应该尽量封装成组件,既减少代码量,又能减少component diff的性能消耗。
基于element diff:
对于列表结构,尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,在一定程度上会影响 React 的渲染性能。
3.说说React生命周期有哪些不同的阶段?每个阶段对应的方法是?
一共有三个阶段,分别为
挂载阶段(Mounting):已插入真实的Dom阶段
更新阶段(Updating):正在被重新渲染的阶段
卸载阶段(Unmounting):已移出真是dom阶段
挂载阶段:
- constructor() 在 React 组件挂载之前,会调用它的构造函数。
- componentWillMount: 在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。
- componentDidMount(): 在组件挂载后(插入 DOM 树中)立即调用
更新运行阶段:
* componentWillReceiveProps: 在接受父组件改变后的props需要重新渲染组件时用到的比较多,外部组件传递频繁的时候会导致效率比较低
* shouldComponentUpdate():用于控制组件重新渲染的生命周期,state发生变化,组件会进入重新渲染的流程,在这里return false可以阻止组件的更新
* render(): render() 方法是 class 组件中唯一必须实现的方法。
* *componentWillUpdate(): shouldComponentUpdate返回true以后,组件进入重新渲染完成之前进入这个函数。
* **componentDidUpdate(): 每次state改变并重新渲染页面后都会进入这个生命周期
卸载或销毁阶段
componentWillUnmount (): 在此处完成组件的卸载和数据的销毁。
4. 说说你对React中虚拟dom的理解?
虚拟 DOM 不会进行排版与重绘操作,而真实 DOM 会频繁重排与重绘
使用虚拟 DOM 的优势如下:
简单方便:如果使用手动操作真实 DOM 来完成页面,繁琐又容易出错,在大规模应用下维护起来也很困难
性能方面:使用 Virtual DOM,能够有效避免真实 DOM 数频繁更新,减少多次引起重绘与回流,提高性能
跨平台:React 借助虚拟 DOM,带来了跨平台的能力,一套代码多端运行
缺点:
在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化
首次渲染大量 DOM 时,由于多了一层虚拟 DOM 的计算,速度比正常稍慢
5.说说你对react hook的理解
React中的Hook方法:
UseState():
useState()用于为函数组件引入状态。在useState()中,数组第一项为一个变量,指向状态的当前值。类似this.state,第二项是一个函数,用来更新状态,类似setState
UseEffect():
useEffect()接受两个参数,第一个参数是你要进行的异步操作,第二个参数是一个数组,用来给出Effect的依赖项。只要这个数组发生变化,useEffect()就会执行
useRef():
相当于class组件中的createRef的作用,ref.current获取绑定的对象
UseContext():
接收context状态树传递过来的数据
UseReducer():
接受reducer函数和状态的初始值作为参数,返回一个数组,其中第一项为当前的状态值,第二项为发送action的dispatch函数
UseMemo()和useCallback():
接收的参数都是一样,第一个参数为回调,第二个参数为要依赖的数据
共同作用:仅仅依赖数据发生变化, 才会调用,也就是起到缓存的作用。useCallback缓存函数,useMemo 缓存返回值。
6.React组件之间如何通信
父传子:在父组件的子组件标签上绑定自定义属性,挂载传递的数据,然后子组件通过props接收传递的数据
子传父:在父组件中的组件间标签上绑定一个数据,传递一个方法给子组件,子组件通过props接收这个方法,直接调用即可
非父子组件通信:
可以使用context状态树进行组件间的通信
还可以使用状态提升就是将多个组件需要共享的数据提升到他们最近的负组件中,父组件改变这个状态通过props分发给子组件
7.说说你对受控组件和非受控组件的理解?应用场景?
受控组件:
由React控制输入表单的元素而改变其值的方式
例如:给表单input绑定一个onChange事件,当input状态发生变化就会触发这个事件,从而更新state的变化
非受控组件:
它的表单数据由dom本身处理,不受setState控制,和传统的html相似,输入什么就会显示最新的值
在非受控组件中可以使用ref来从dom获取表单数据
8.说说Connect组件的原理是什么?
connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])
connect有四个参数, 但是后两个参数用到的很少, 探讨前两个参数.
connect 的第一个参数是 mapStateToProps
这个函数允许我们将 store 中的数据作为 props 绑定到组件上
const mapStateToProps = (state) => { // 正常我们在react-redux中会这样书写 return { themeColor: state.themeColor } }
People = connect(mapStateToProps)(People) // connect返回来的是一个函数, 因此还要再一次调用传入组件
实现主要原理, 就是将需要绑定的props作为一个函数传过来, 在connect中传给mapStateToProps一个真实的store的数据
const connect = (mapStateToProps) => (People) => { class Connect extends Component { static contextTypes = { store: PropTypes.object } constructor () { super() this.state = { allProps: {} } } componentWillMount () { const { store } = this.context this.setProps() } setProps () { const { store } = this.context let stateProps = mapStateToProps(store.getState(), this.props) // 额外传入 props this.setState({ allProps: { // 整合普通的 props 和从 state 生成的 props ...stateProps, ...this.props } }) } render () { return <People {...this.state.allProps} /> } } return Connect }
connect 的第二个参数是 mapDispatchToProps
由于更改数据必须要触发action, 因此在这里的主要功能是将 action 作为props 绑定到 组件上
const mapDispatchToProps = (dispatch, ownProps) => { return { increase: (...args) => dispatch(actions.increase(...args)), decrease: (...args) => dispatch(actions.decrease(...args)) } } class People extends Component { render(){ const {count, increase, decrease} = this.props; return (<div> <div>计数:{this.props.count}次</div> <button onClick={increase}>增加</button> <button onClick={decrease}>减少</button> </div>) } } const NiuPeople = connect(mapStateToProps, mapDispatchToProps)(People);
这里的实现原理和上面的相差不多, 主要是将action和props一起传到组件里.
const connect = (mapStateToProps, mapDispatchToProps) => (People) => { class Connect extends Component { static contextTypes = { store: PropTypes.object } constructor () { super() this.state = { allProps: {} } } componentWillMount () { const { store } = this.context this.setProps() store.subscribe(() => this.setProps()) } setProps () { // 做了一下完整性的判断 const { store } = this.context let stateProps = mapStateToProps ? mapStateToProps(store.getState(), this.props) : {} // 防止 mapStateToProps 没有传入 let dispatchProps = mapDispatchToProps ? mapDispatchToProps(store.dispatch, this.props) : {} // 防止 mapDispatchToProps 没有传入 this.setState({ allProps: { ...stateProps, ...dispatchProps, ...this.props } }) } render () { return <People {...this.state.allProps} /> } } return Connect }
9.说说react 中jsx语法糖的本质?
jsx本质就是下面这个函数React.createElement的语法糖,所有的jsx语法都会最终经过babel.js转化成为React.createElement这个函数的函数调用
React.createElement(component,props,...children)函数
1. //jsx写法:必须引入babel并且设置script标签的type为text/bable //babel:将jsx转化为React.createElement()这种函数的调用 <script type="text/babel"> const message1 = <h2>这是采用babel编译的jsx语法糖写法</h2> ReactDOM.render(message1,document.getElementById('app')); </script>
2. //React.createElement写法 //不使用jsx这种语法糖写法,就想用React.createElement()这种方式写,那么其实babel.js这个库不需要引入,script标签的type也不用写成text/babel了 <script type="text/javascript"> const message2 = React.createElement('h2',null,'这是采用React.createElement()函数写法') ReactDOM.render(message2,document.getElementById('app')); </script>
10.说说你对redux中间件的理解?常用的中间件有哪些?实现原理?
Redux中,中间件就是放在就是在dispatch过程,在分发action进行拦截处理
前面我们了解到了Redux整个工作流程,当action发出之后,reducer立即算出state,整个过程是一个同步的操作
那么如果需要支持异步操作,或者支持错误处理、日志监控,这个过程就可以用上中间件,其本质上一个函数,对store.dispatch方法进行了改造,在发出 Action和执行 Reducer这两步之间,添加了其他功能
常用的redux中间件,如:
redux-thunk:用于异步操作
redux-logger:用于日志记录
中间件都需要通过applyMiddlewares进行注册,作用是将所有的中间件组成一个数组,依次执行然后作为第二个参数传入到createStore中
const store = createStore( reducer, applyMiddleware(thunk, logger) );