那些年,自己沒回答上來的react面試題

lio-mengxiang發表於2019-04-11

1、React中元素與元件的區別

那個時候剛學react,不知道面試官說的元素是什麼,現在知道了,就是虛擬dom嘛。。。

React 元素(React element)

它是 React 中最小基本單位,我們可以使用 JSX 語法輕鬆地建立一個 React 元素:

const element = <div className="element">I'm element</div>
複製程式碼

React 元素不是真實的 DOM 元素,它僅僅是 js 的普通物件(plain objects),所以也沒辦法直接呼叫 DOM 原生的 API。上面的 JSX 轉譯後的物件大概是這樣的:


{
    _context: Object,
    _owner: null,
    key: null,
    props: {
    className: 'element',
    children: 'I'm element'
  },
    ref: null,
    type: "div"
}
複製程式碼

除了使用 JSX 語法,我們還可以使用 React.createElement() 和 React.cloneElement() 來構建 React 元素。

React 元件

React 中有三種構建元件的方式。React.createClass()、ES6 class和無狀態函式。

1、React.createClass()

var Greeting = React.createClass({
  render: function() {
    return <h1>Hello, {this.props.name}</h1>;
  }
});
複製程式碼

2、ES6 class

class Greeting extends React.Component{
  render: function() {
    return <h1>Hello, {this.props.name}</h1>;
  }
};
複製程式碼

3、無狀態函式

無狀態函式是使用函式構建的無狀態元件,無狀態元件傳入props和context兩個引數,它沒有state,除了render(),沒有其它生命週期方法。

function Greeting (props) {
  return <h1>Hello, {props.name}</h1>;
}
複製程式碼

4、PureComponent

除了為你提供了一個具有淺比較的shouldComponentUpdate方法,PureComponent和Component基本上完全相同。

元素與元件的區別

元件是由元素構成的。元素資料結構是普通物件,而元件資料結構是類或純函式。

2、請說一說Forwarding Refs有什麼用

是父元件用來獲取子元件的dom元素的,為什麼有這個API,原因如下

// 例如有一個子元件和父元件,程式碼如下
子元件為:
class Child extends React.Component{
    constructor(props){
      super(props);
    }
    render(){
      return <input />
    }
 }

// 父元件中,ref:
class Father extends React.Component{
  constructor(props){
    super(props);
    this.myRef=React.createRef();
  }
  componentDidMount(){
    console.log(this.myRef.current);
  }
  render(){
    return <Child ref={this.myRef}/>
  }
}
複製程式碼

此時父元件的this.myRef.current的值是Child元件,也就是一個物件,如果用了React.forwardRef,也就是如下

// 子元件
const Child = React.forwardRef((props, ref) => (
    <input ref={ref} />
));
// 父元件
class Father extends React.Component {
    constructor(props) {
        super(props);
        this.myRef = React.createRef();
    }
    componentDidMount() {
        console.log(this.myRef.current);
    }
    render() {
        return <Child ref={this.myRef} />
    }
}
複製程式碼

此時父元件的this.myRef.current的值是input這個DOM元素

3、簡述一下virtual DOM 如何工作?

  • 當資料發生變化,比如setState時,會引起元件重新渲染,整個UI都會以virtual dom的形式重新渲染
  • 然後收集差異也就是diff新的virtual dom和老的virtual dom的差異
  • 最後把差異佇列裡的差異,比如增加節點、刪除節點、移動節點更新到真實的DOM上

4、你對Fiber架構瞭解嗎

注:答案參考司徒正美大神的文章

4.1、Fiber架構相對於以前的遞迴更新元件有有什麼優勢

  • 原因是遞迴更新元件會讓JS呼叫棧佔用很長時間。
  • 因為瀏覽器是單執行緒的,它將GUI渲染,事件處理,js執行等等放在了一起,只有將它做完才能做下一件事,如果有足夠的時間,瀏覽器是會對我們的程式碼進行編譯優化(JIT)及進行熱程式碼優化。
  • Fiber架構正是利用這個原理將元件渲染分段執行,提高這樣瀏覽器就有時間優化JS程式碼與修正reflow!

4.1、既然你說Fiber是將元件分段渲染,那第一段渲染之後,怎麼知道下一段從哪個元件開始渲染呢?

  • Fiber節點擁有return, child, sibling三個屬性,分別對應父節點, 第一個孩子, 它右邊的兄弟, 有了它們就足夠將一棵樹變成一個連結串列, 實現深度優化遍歷。

4.2 怎麼決定每次更新的數量

  • React16則是需要將虛擬DOM轉換為Fiber節點,首先它規定一個時間段內,然後在這個時間段能轉換多少個FiberNode,就更新多少個。
  • 因此我們需要將我們的更新邏輯分成兩個階段,第一個階段是將虛擬DOM轉換成Fiber, Fiber轉換成元件例項或真實DOM(不插入DOM樹,插入DOM樹會reflow)。Fiber轉換成後兩者明顯會耗時,需要計算還剩下多少時間。
  • 比如,可以記錄開始更新檢視的時間var now = new Date - 0,假如我們更新試圖自定義需要100毫秒,那麼定義結束時間是var deadline = new Date + 100 ,所以每次更新一部分檢視,就去拿當前時間new Date<deadline做判斷,如果沒有超過deadline就更新檢視,超過了,就進入下一個更新階段

4.3 如何排程時間才能保證流暢

  • 使用瀏覽器自帶的api - requestIdleCallback,
  • 它的第一個引數是一個回撥,回撥有一個引數物件,物件有一個timeRemaining方法,就相當於new Date - deadline,並且它是一個高精度資料, 比毫秒更準確
  • 這個因為瀏覽器相容性問題,react團隊自己實現了requestIdleCallback

4.4 fiber帶來的新的生命週期是什麼

那些年,自己沒回答上來的react面試題

建立時

  • constructor ->
  • getDerivedStateFromProps(引數nextProps, prevState,注意裡面this不指向元件的例項)->
  • render ->
  • componentDidMount

更新時

  • getDerivedStateFromProps(這個時props更新才呼叫,setState時不呼叫這個生命週期, 引數nextProps, prevState) ->
  • shouldComponentUpdate(setState時呼叫引數nextProps, nextState)->
  • render->
  • getSnapsshotBeforeUpdate(替換 componentWillUpdate)
  • componentDidUpdate(引數prevProps, prevState, snapshot)

5、什麼是受控元件和非受控元件

  • 受狀態控制的元件,必須要有onChange方法,否則不能使用 受控元件可以賦予預設值(官方推薦使用 受控元件) 實現雙向資料繫結
class Input extends Component{
    constructor(){
        super();
        this.state = {val:'100'}
    }
    handleChange = (e) =>{ //e是事件源
        let val = e.target.value;
        this.setState({val});
    };
    render(){
        return (<div>
            <input type="text" value={this.state.val} onChange={this.handleChange}/>
            {this.state.val}
        </div>)
    }
}
複製程式碼
  • 非受控也就意味著我可以不需要設定它的state屬性,而通過ref來操作真實的DOM
class Sum extends Component{
    constructor(){
        super();
        this.state =  {result:''}
    }
    //通過ref設定的屬性 可以通過this.refs獲取到對應的dom元素
    handleChange = () =>{
        let result = this.refs.a.value + this.b.value;
        this.setState({result});
    };
    render(){
        return (
            <div onChange={this.handleChange}>
                <input type="number" ref="a"/>
                {/*x代表的真實的dom,把元素掛載在了當前例項上*/}
                <input type="number" ref={(x)=>{
                    this.b = x;
                }}/>
                {this.state.result}
            </div>
        )
    }
}
複製程式碼

6、什麼是狀態提升

使用 react 經常會遇到幾個元件需要共用狀態資料的情況。這種情況下,我們最好將這部分共享的狀態提升至他們最近的父元件當中進行管理。我們來看一下具體如何操作吧。

import React from 'react'
class Child_1 extends React.Component{
    constructor(props){
        super(props)
    }
    render(){
        return (
            <div>
                <h1>{this.props.value+2}</h1>
            </div> 
        )
    }
}
class Child_2 extends React.Component{
    constructor(props){
        super(props)
    }
    render(){
        return (
            <div>
                <h1>{this.props.value+1}</h1>
            </div> 
        )
    }
}
class Three extends React.Component {
    constructor(props){
        super(props)
        this.state = {
            txt:"牛逼"
        }
        this.handleChange = this.handleChange.bind(this)
    }
    handleChange(e){
        this.setState({
            txt:e.target.value
        })
    }
    render(){
       return (
            <div>
                <input type="text" value={this.state.txt} onChange={this.handleChange}/>
                <p>{this.state.txt}</p>
                <Child_1 value={this.state.txt}/>
                <Child_2 value={this.state.txt}/>
            </div>
       )
    }
}
export default Three
複製程式碼

7、什麼是高階元件

高階元件不是元件,是 增強函式,可以輸入一個元元件,返回出一個新的增強元件

  • 屬性代理 (Props Proxy) 在我看來屬性代理就是提取公共的資料和方法到父元件,子元件只負責渲染資料,相當於設計模式裡的模板模式,這樣元件的重用性就更高了
function proxyHoc(WrappedComponent) {
	return class extends React.Component {
		render() {
			const newProps = {
				count: 1
			}
			return <WrappedComponent {...this.props} {...newProps} />
		}
	}
}
複製程式碼
  • 反向繼承
const MyContainer = (WrappedComponent)=>{
    return class extends WrappedComponent {
        render(){
            return super.render();
        }
    }
}
複製程式碼

8、什麼是上下文Context

Context 通過元件樹提供了一個傳遞資料的方法,從而避免了在每一個層級手動的傳遞 props 屬性。

  • 用法:在父元件上定義getChildContext方法,返回一個物件,然後它的子元件就可以通過this.context屬性來獲取
import React,{Component} from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
class Header extends Component{
    render() {
        return (
            <div>
                <Title/>
            </div>
        )
    }
}
class Title extends Component{
    static contextTypes={
        color:PropTypes.string
    }
    render() {
        return (
            <div style={{color:this.context.color}}>
                Title
            </div>
        )
    }
}
class Main extends Component{
    render() {
        return (
            <div>
                <Content>
                </Content>
            </div>
        )
    }
}
class Content extends Component{
    static contextTypes={
        color: PropTypes.string,
        changeColor:PropTypes.func
    }
    render() {
        return (
            <div style={{color:this.context.color}}>
                Content
                <button onClick={()=>this.context.changeColor('green')}>綠色</button>
                <button onClick={()=>this.context.changeColor('orange')}>橙色</button>
            </div>
        )
    }
}
class Page extends Component{
    constructor() {
        super();
        this.state={color:'red'};
    }
    static childContextTypes={
        color: PropTypes.string,
        changeColor:PropTypes.func
    }
    getChildContext() {
        return {
            color: this.state.color,
            changeColor:(color)=>{
                this.setState({color})
            }
        }
    }
    render() {
        return (
            <div>
                <Header/>
                <Main/>
            </div>
        )
    }
}
ReactDOM.render(<Page/>,document.querySelector('#root'));
複製程式碼

9、react中的Portal是什麼?

Portals 提供了一種很好的將子節點渲染到父元件以外的 DOM 節點的方式。

ReactDOM.createPortal(child, container)
複製程式碼

第一個引數(child)是任何可渲染的 React 子元素,例如一個元素,字串或碎片。第二個引數(container)則是一個 DOM 元素。

10、react16的錯誤邊界(Error Boundaries)是什麼

部分 UI 中的 JavaScript 錯誤不應該破壞整個應用程式。 為了解決 React 使用者的這個問題,React 16引入了一個 “錯誤邊界(Error Boundaries)” 的新概念。

import React from 'react';
import ReactDOM from 'react-dom';
class ErrorBoundary extends React.Component{
    constructor(props) {
        super(props);
        this.state={hasError:false};
    }
    componentDidCatch(err,info) {
        this.setState({hasError: true});
    }
    render() {
        if (this.state.hasError) {
            return <h1>Something Went Wrong</h1>
        }
        return this.props.children;
    }
}

class Page extends React.Component{
    render() {
        return (
            <ErrorBoundary>
                <Clock/>
            </ErrorBoundary>
        )
    }
}
class Clock extends React.Component{
    render() {
        return (
            <div>hello{null.toString()}</div>
        )
    }
}

ReactDOM.render(<Page/>,document.querySelector('#root'));
複製程式碼

11、如何在React中使用innerHTML

增加dangerouslySetInnerHTML屬性,並且傳入物件的屬性名叫_html

function Component(props){
    return <div dangerouslySetInnerHTML={{_html:'<span>你好</span>'}}>
            </div>
}
複製程式碼

12、react16版本的reconciliation階段和commit階段是什麼

  • reconciliation階段包含的主要工作是對current tree 和 new tree 做diff計算,找出變化部分。進行遍歷、對比等是可以中斷,歇一會兒接著再來。
  • commit階段是對上一階段獲取到的變化部分應用到真實的DOM樹中,是一系列的DOM操作。不僅要維護更復雜的DOM狀態,而且中斷後再繼續,會對使用者體驗造成影響。在普遍的應用場景下,此階段的耗時比diff計算等耗時相對短。

13、請簡單談一下react的事件機制

  • 當使用者在為onClick新增函式時,React並沒有將Click時間繫結在DOM上面。
  • 而是在document處監聽所有支援的事件,當事件發生並冒泡至document處時,React將事件內容封裝交給中間層SyntheticEvent(負責所有事件合成)
  • 所以當事件觸發的時候,對使用統一的分發函式dispatchEvent將指定函式執行。

14、為什麼列表迴圈渲染的key最好不要用index

舉例說明

變化前陣列的值是[1,2,3,4],key就是對應的下標:0,1,2,3
變化後陣列的值是[4,3,2,1],key對應的下標也是:0,1,2,3
複製程式碼
  • 那麼diff演算法在變化前的陣列找到key =0的值是1,在變化後陣列裡找到的key=0的值是4
  • 因為子元素不一樣就重新刪除並更新
  • 但是如果加了唯一的key,如下
變化前陣列的值是[1,2,3,4],key就是對應的下標:id0,id1,id2,id3
變化後陣列的值是[4,3,2,1],key對應的下標也是:id3,id2,id1,id0
複製程式碼
  • 那麼diff演算法在變化前的陣列找到key =id0的值是1,在變化後陣列裡找到的key=id0的值也是1
  • 因為子元素相同,就不刪除並更新,只做移動操作,這就提升了效能

相關文章