虽然前端并不是我的强项,但是工作几年了一直做着以后端为主的全栈开发。所以对前端的一些技术框架也比较关注,履历中也有前端开发的经历,因此有时也会被问到一些前端的知识。现如今前端技术发展迅速,JavaScript 工具缓慢而稳定地在市场中扎根,对 React 的需求呈指数级增长。选择合适的技术来开发应用或网站变得越来越有挑战性。其中 React 被认为是增长最快的 Javascript 框架。在此总结了常见的知识点
Q1:什么是虚拟DOM?
难度:⭐️
虚拟DOM(VDOM)它是真实DOM的内存表示,一种编程概念,一种模式。它会和真实的DOM同步,比如通过ReactDOM这种库,这个同步的过程叫做调和(reconcilation)。虚拟DOM更多是一种模式,不是一种特定的技术。为什么要使用虚拟DOM,原因是真实的DOM是非常耗资源的操作,虚拟DOM可以把原本多个DOM操作在内存中简化,移除不必要的DOM操作,以最小的消耗来操作DOM节点
Q2:类组件和函数组件之间有什么区别?
难度:⭐️⭐️
类组件(Class components)
类组件允许使用更多额外的功能,如组件自身的状态和生命周期钩子
class Welcome extends React.Component { render() { return ( <h1>Welcome { this.props.name }</h1> ); } } ReactDOM.render(<Welcome name='react' />, document.getElementById('root'));
函数组件(functional component)
也叫无状态组件(stateless component),仅接收 props,没有state
function Welcome (props) { return <h1>Welcome {props.name}</h1> } ReactDOM.render(<Welcome name='react' />, document.getElementById('root'));
主要区别
函数组件和类组件当然是有区别的,而且函数组件的性能比类组件的性能要高,因为类组件使用的时候要实例化,而函数组件直接执行函数取返回结果即可。为了提高性能,尽量使用函数组件。
区别 | 函数组件 | 类组件 |
---|---|---|
是否有 this |
没有 | 有 |
是否有生命周期 | 没有 | 有 |
是否有状态 state |
没有 | 有 |
Q3:React中的refs作用是什么?
难度: ⭐️⭐️
Refs
提供了一种方式,允许我们访问DOM
节点或在render
方法中创建的React
元素。在典型的React
数据流中,props
是父组件与子组件交互的唯一方式,要修改一个子组件,你需要使用新的props
来重新渲染它,但是在某些情况下,你需要在典型数据流之外强制修改子组件,被修改的子组件可能是一个React
组件的实例,也可能是一个DOM
元素,对于这两种情况React
都提供了解决办法。避免使用refs
来做任何可以通过声明式实现来完成的事情,通常在可以使用props
与state
的情况下勿依赖refs
,下面是几个适合使用refs
的情况:
- 管理焦点、文本选择或媒体播放。
- 触发强制动画。
- 集成第三方
DOM
库。
React
提供的这个ref
属性,表示为对组件真正实例的引用,其实就是ReactDOM.render()
返回的组件实例,需要区分一下渲染组件与渲染原生DOM
元素,渲染组件时返回的是组件实例,而渲染DOM
元素时,返回是具体的DOM
节点
class UnControlledForm 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>
)
}
Q4:如何创建refs?
难度: ⭐️⭐️
React
的ref
有3
种创建方式。
字符串
ref
可以直接设置为字符串值,这种方式基本不推荐使用,或者在未来的React
版本中不会再支持该方式。这主要是因为使用字符串导致的一些问题,例如当ref
定义为string
时,需要React
追踪当前正在渲染的组件,在reconciliation
阶段,React Element
创建和更新的过程中,ref
会被封装为一个闭包函数,等待commit
阶段被执行,这会对React
的性能产生一些影响等。class InputOne extends React.Component { componentDidMount() { this.refs.inputRef.value = 1; } render() { return <input ref="inputRef" />; } }
回调
React
支持给任意组件添加特殊属性,ref
属性接受一个回调函数,其在组件被加载或卸载时会立即执行。- 当给
HTML
元素添加ref
属性时,ref
回调接收了底层的DOM
元素作为参数。 - 当给组件添加
ref
属性时,ref
回调接收当前组件实例作为参数。 - 当组件卸载的时候,会传入
null
。 ref
回调会在componentDidMount
或componentDidUpdate
等生命周期回调之前执行。
Callback Ref
我们通常会使用内联函数的形式,那么每次渲染都会重新创建,由于React
会清理旧的ref
然后设置新的,因此更新期间会调用两次,第一次为null
,如果在Callback
中带有业务逻辑的话,可能会出错,可以通过将Callback
定义成类成员函数并进行绑定的方式避免。class InputTwo extends React.Component { componentDidMount() { this.inputRef.value = 2; } render() { return <input ref={(element) =>this.inputRef = element} />; } }
- 当给
API创建
在
React v16.3
中经0017-new-create-ref
提案引入了新的React.createRef
的API
,当ref
被传递给render
中的元素时,对该节点的引用可以在ref
的current
属性中被访问,ref
的值根据节点的类型而有所不同:- 当
ref
属性用于HTML
元素时,构造函数中使用React.createRef()
创建的ref
接收底层DOM
元素作为其current
属性。 - 当
ref
属性用于自定义class
组件时,ref
对象接收组件的挂载实例作为其current
属性。 - 不能在函数组件上使用
ref
属性,因为他们没有实例。
对比新的
CreateRef
与Callback Ref
,并没有压倒性的优势,只是希望成为一个便捷的特性,在性能上会会有微小的优势,Callback Ref
采用了组件Render
过程中在闭包函数中分配ref
的模式,而CreateRef
则采用了Object Ref
。class InputThree extends React.Component { constructor(props) { super(props); this.inputRef = React.createRef(); } componentDidMount() { this.inputRef.current.value = 3; } render() { return <input ref={this.inputRef} />; } }
- 当
Q5:state 和 props有什么区别?
难度: ⭐️⭐️
state 和 props都是普通的JavaScript对象。尽管它们两者都具有影响渲染输出的信息,但它们在组件方面的功能不同。即
props
是一个从外部传进组件的参数,主要作为就是从父组件向子组件传递数据,它具有可读性和不变性,只能通过外部组件主动传入新的props
来重新渲染子组件,否则子组件的props
以及展现形式不会改变。state
的主要作用是用于组件保存、控制以及修改自己的状态,它只能在constructor
中初始化,它算是组件的私有属性,不可通过外部访问和修改,只能通过组件内部的this.setState
来修改,修改state
属性会导致组件的重新渲染。
Q7:什么是高阶组件?
难度: ⭐️⭐️
高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件。基本上,这是从React的组成性质派生的一种模式,我们称它们为“纯”组件, 因为它们可以接受任何动态提供的子组件,但它们不会修改或复制其输入组件的任何行为。下面我们来实现一个最简单的高阶组件(函数),它接受一个React组件,包裹后然后返回。
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>
);
}
}
在这里使用了ES7
里的decorator
,来提升写法上的优雅,但是实际上它只是一个语法糖,下面这种写法也是可以的。
const EnhanceDemo = withHeader(Demo);
高阶组件的进阶用法
组件参数
还是以上述例子为例,此高阶组件仅仅只是展示了我是标题这个名称,但是为了更好的抽象,此标题应当可以被参数化,如下方式调用。
// 如果传入参数,则传入的参数将作为组件的标题呈现 @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> } }
这个进阶用法使用了函数编程里的柯里化 Curry
概念:只传递函数的一部分参数来调用它,让它返回一个函数去处理剩下的参数。
函数签名:fun(params)(otherParams)
应用:在React里,通过柯里化,我们可以通过传入不同的参数来得到不同的高阶组件。
组合多个高阶组件
上述高阶组件为React组件增强了一个功能,如果需要同时增加多个功能需要怎么做?这种场景非常常见,例如我既需要增加一个组件标题,又需要在此组件未加载完成时显示Loading。
@withHeader @withLoading class Demo extends Component{ }
使用
compose
可以简化上述过程,也能体现函数式编程的思想。const enhance = compose(withHeader,withLoading); @enhance class Demo extends Component{ }
组合
Compose
compose
可以帮助我们组合任意个(包括0个)高阶函数,例如compose(a,b,c)
返回一个新的函数d
,函数d
依然接受一个函数作为入参,只不过在内部会依次调用c,b,a
,从表现层对使用者保持透明。基于这个特性,我们便可以非常便捷地为某个组件增强或减弱其特征,只需要去变更compose函数里的参数个数便可。
compose
函数实现方式有很多种,这里推荐其中一个recompact.compose
Q8:什么是受控组件?
难度:⭐️⭐️⭐️
在HTML当中,像<input>
,<textarea>
, 和 <select>
这类表单元素会维持自身状态,并根据用户输入进行更新。但在React中,可变的状态通常保存在组件的状态属性中,并且只能用 setState() 方法进行更新。
非受控组件
非受控组件,即组件的状态不受React控制的组件,例如下边这个
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class Demo1 extends Component {
render() {
return (
<input />
)
}
}
ReactDOM.render(<Demo1/>, document.getElementById('content'))
在这个最简单的输入框组件里,我们并没有干涉input中的value展示,即用户输入的内容都会展示在上面。如果我们通过props给组件设置一个初始默认值,defaultValue属性是React内部实现的一个属性,目的类似于input的placeholder属性。
ps: 此处如果使用value代替defaultValue,会发现输入框的值无法改变
受控组件
同样的,受控组件就是组件的状态受React控制。上面提到过,既然通过设置input的value属性, 无法改变输入框值,那么我们把它和state结合在一起,再绑定onChange事件,实时更新value值就行了。
class Demo1 extends Component {
constructor(props) {
super(props);
this.state = {
value: props.value
}
}
handleChange(e) {
this.setState({
value: e.target.value
})
}
render() {
return (
<input value={this.state.value} onChange={e => this.handleChange(e)}/>
)
}
}
Q9:constructor中super与props参数一起使用的目的是什么?
难度:⭐️⭐️
在调用方法之前,子类构造函数无法使用this引用super()
。在ES6中,在子类的constructor
中必须先调用super
才能引用this
。在constructor
中可以使用this.props
使用props
class MyComponent extends React.Component {
constructor(props) {
super(props);
console.log(this.props); // Prints { name: 'sudheer',age: 30 }
}
}
不使用props
class MyComponent extends React.Component {
constructor(props) {
super();
console.log(this.props); // Prints undefined
// But Props parameter is still available
console.log(props); // Prints { name: 'sudheer',age: 30 }
}
render() {
// No difference outside constructor
console.log(this.props) // Prints { name: 'sudheer',age: 30 }
}
}
上面的代码片段揭示了this.props行为仅在构造函数中有所不同。外部构造函数相同。
Q10:以下使用React.createElement的等价项是什么?
难度:⭐️⭐️⭐️
以下等同于什么使用React.createElement
?
const element = (
<h1 className="greeting">
Hello, world!
</h1>
);
答:
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);
Q11:什么是JSX?
难度:⭐️⭐️⭐️
JSX即JavaScript XML。一种在React组件内部构建标签的类XML语法。JSX为react.js开发的一套语法糖,也是react.js的使用基础。React在不使用JSX的情况下一样可以工作,然而使用JSX可以提高组件的可读性,因此推荐使用JSX。
class MyComponent extends React.Component {
render() {
let props = this.props;
return (
<div className="my-component">
<a href={props.url}>{props.name}</a>
</div>
);
}
}
JSX的优点:
- 允许使用熟悉的语法来定义 HTML 元素树
- 提供更加语义化且移动的标签
- 程序结构更容易被直观化
- 抽象了 React Element 的创建过程
- 可以随时掌控 HTML 标签以及生成这些标签的代码
- 是原生的 JavaScript
Q12:React生命周期有哪些不同阶段?
难度:⭐️⭐️⭐️
React组件的生命周期分为四个不同阶段:
- 初始化:在此阶段,react组件准备设置初始状态和默认道具。
- 挂载: react组件已准备好挂载在浏览器DOM中。此阶段涵盖componentWillMount和componentDidMount生命周期方法。
- 更新:在此阶段,组件以两种方式进行更新,即发送新道具和更新状态。此阶段涵盖了shouldComponentUpdate,componentWillUpdate和componentDidUpdate生命周期方法。
- 卸载:在最后一个阶段,不需要该组件,并且可以从浏览器DOM上卸载该组件。此阶段包括componentWillUnmount生命周期方法。
Q13:React的生命周期方法是什么?
难度:⭐️⭐️⭐️
- componentWillMount:在渲染之前执行,用于根组件中的应用程序级别配置。
- componentDidMount:仅在客户端的第一次渲染之后执行。 这是AJAX请求和DOM或状态更新应该发生的地方。此方法也用于与其他JavaScript框架以及任何延迟执行的函数(如
setTimeout
或setInterval
)进行集成,在这里使用它来更新状态,以便我们可以触发其他生命周期方法。 - componentWillReceiveProps:只要在另一个渲染被调用之前更新
props
就被调用。 当我们更新状态时,从setNewNumber
触发它。 - shouldComponentUpdate:确定是否将更新组件。默认情况下,它返回true。如果您确定组件在状态或道具更新后不需要渲染,则可以返回false值。这是提高性能的好地方,因为如果组件收到新的道具,它可以防止重新渲染。
- componentWillUpdate:在由shouldComponentUpdate确认返回正值的优点和状态更改时,在重新渲染组件之前执行。
- componentDidUpdate:通常用于更新DOM以响应属性或状态更改。
- componentWillUnmount:它将用于取消任何传出的网络请求,或删除与该组件关联的所有事件侦听器。
Q14:React Fiber是什么?
难度:⭐️⭐️⭐️⭐️
React Fiber 并不是所谓的纤程(微线程、协程),而是一种基于浏览器的单线程调度算法,背后的支持 API 是大名鼎鼎的:requestIdleCallback。
Fiberl是一种将 recocilation (递归 diff),拆分成无数个小任务的算法;它随时能够停止,恢复。停止恢复的时机取决于当前的一帧(16ms)内,还有没有足够的时间允许计算。