前端框架面試之 React 的使用、 React 的原理和 React 的面試真題

一、 React 的使用

  1. React 的基本知識點,如下所示:
  • JSX 基本使用,變量、表達式、class style、子元素和組件等等,代碼如下所示:
import React from 'react'
import './style.css'
import List from '../List'

class JSXBaseDemo extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            name: '張三',
            imgUrl: 'https://img1.mukewang.com/5a9fc8070001a82402060220-140-140.jpg',
            flag: true
        }
    }
    render() {
        // // 獲取變量 插值
        // const pElem = <p>{this.state.name}</p>
        // return pElem

        // // 表達式
        // const exprElem = <p>{this.state.flag ? 'yes' : 'no'}</p>
        // return exprElem

        // // 子元素
        // const imgElem = <div>
        //     <p>我的頭像</p>
        //     <img src="xxxx.png"/>
        //     <img src={this.state.imgUrl}/>
        // </div>
        // return imgElem

        // // class
        // const classElem = <p className="title">設置 css class</p>
        // return classElem

        // // style
        // const styleData = { fontSize: '30px',  color: 'blue' }
        // const styleElem = <p style={styleData}>設置 style</p>
        // // 內聯寫法,注意 {{ 和 }}
        // // const styleElem = <p style={{ fontSize: '30px',  color: 'blue' }}>設置 style</p>
        // return styleElem

        // 原生 html
        const rawHtml = '<span>富文本內容<i>斜體</i><b>加粗</b></span>'
        const rawHtmlData = {
            __html: rawHtml // 注意,必須是這種格式
        }
        const rawHtmlElem = <div>
            <p dangerouslySetInnerHTML={rawHtmlData}></p>
            <p>{rawHtml}</p>
        </div>
        return rawHtmlElem

        // // 加載組件
        // const componentElem = <div>
        //     <p>JSX 中加載一個組件</p>
        //     <hr/>
        //     <List/>
        // </div>
        // return componentElem
    }
}

export default JSXBaseDemo

  • 條件判斷,if else、三元表達式、邏輯運算符 && ||,代碼如下所示:
import React from 'react'
import './style.css'

class ConditionDemo extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            theme: 'black'
        }
    }
    render() {
        const blackBtn = <button className="btn-black">black btn</button>
        const whiteBtn = <button className="btn-white">white btn</button>

        // // if else
        // if (this.state.theme === 'black') {
        //     return blackBtn
        // } else {
        //     return whiteBtn
        // }

        // // 三元運算符
        // return <div>
        //     { this.state.theme === 'black' ? blackBtn : whiteBtn }
        // </div>

        // &&
        return <div>
            { this.state.theme === 'black' && blackBtn }
        </div>
    }
}

export default ConditionDemo

  • 渲染列表,mapkey,代碼如下所示:
import React from 'react'

class ListDemo extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            list: [
                {
                    id: 'id-1',
                    title: '標題1'
                },
                {
                    id: 'id-2',
                    title: '標題2'
                },
                {
                    id: 'id-3',
                    title: '標題3'
                }
            ]
        }
    }
    render() {
        return <ul>
            { /* vue v-for */
                this.state.list.map(
                    (item, index) => {
                        // 這裏的 key 和 Vue 的 key 類似,必填,不能是 index 或 random
                        return <li key={item.id}>
                            index {index}; id {item.id}; title {item.title}
                        </li>
                    }
                )
            }
        </ul>
    }
}

export default ListDemo

  • 事件,bind this,關於 event 參數,傳遞自定義參數,代碼如下所示:
import React from 'react'

class EventDemo extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            name: 'zhangsan',
            list: [
                {
                    id: 'id-1',
                    title: '標題1'
                },
                {
                    id: 'id-2',
                    title: '標題2'
                },
                {
                    id: 'id-3',
                    title: '標題3'
                }
            ]
        }

        // 修改方法的 this 指向
        this.clickHandler1 = this.clickHandler1.bind(this)
    }
    render() {
        // // this - 使用 bind
        // return <p onClick={this.clickHandler1}>
        //     {this.state.name}
        // </p>

        // // this - 使用靜態方法
        // return <p onClick={this.clickHandler2}>
        //     clickHandler2 {this.state.name}
        // </p>

        // // event
        // return <a href="https://imooc.com/" onClick={this.clickHandler3}>
        //     click me
        // </a>

        // 傳遞參數 - 用 bind(this, a, b)
        return <ul>{this.state.list.map((item, index) => {
            return <li key={item.id} onClick={this.clickHandler4.bind(this, item.id, item.title)}>
                index {index}; title {item.title}
            </li>
        })}</ul>
    }
    clickHandler1() {
        // console.log('this....', this) // this 默認是 undefined
        this.setState({
            name: 'lisi'
        })
    }
    // 靜態方法,this 指向當前實例
    clickHandler2 = () => {
        this.setState({
            name: 'lisi'
        })
    }
    // 獲取 event
    clickHandler3 = (event) => {
        event.preventDefault() // 阻止默認行爲
        event.stopPropagation() // 阻止冒泡
        console.log('target', event.target) // 指向當前元素,即當前元素觸發
        console.log('current target', event.currentTarget) // 指向當前元素,假象!!!

        // 注意,event 其實是 React 封裝的。可以看 __proto__.constructor 是 SyntheticEvent 組合事件
        console.log('event', event) // 不是原生的 Event ,原生的 MouseEvent
        console.log('event.__proto__.constructor', event.__proto__.constructor)

        // 原生 event 如下。其 __proto__.constructor 是 MouseEvent
        console.log('nativeEvent', event.nativeEvent)
        console.log('nativeEvent target', event.nativeEvent.target)  // 指向當前元素,即當前元素觸發
        console.log('nativeEvent current target', event.nativeEvent.currentTarget) // 指向 document !!!

        // 1. event 是 SyntheticEvent ,模擬出來 DOM 事件所有能力
        // 2. event.nativeEvent 是原生事件對象
        // 3. 所有的事件,都被掛載到 document 上
        // 4. 和 DOM 事件不一樣,和 Vue 事件也不一樣
    }
    // 傳遞參數
    clickHandler4(id, title, event) {
        console.log(id, title)
        console.log('event', event) // 最後追加一個參數,即可接收 event
    }
}

export default EventDemo

  • 表單,受控組件,input、textarea、 selectvaluecheckbox、 radiochecked,代碼如下所示:
import React from 'react'

class FormDemo extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            name: '張三',
            info: '個人信息',
            city: 'shanghai',
            flag: true,
            gender: 'male'
        }
    }
    render() {

        // // 受控組件(非受控組件,後面再講)
        // return <div>
        //     <p>{this.state.name}</p>
        //     <label htmlFor="inputName">姓名:</label> {/* 用 htmlFor 代替 for */}
        //     <input id="inputName" value={this.state.name} onChange={this.onInputChange}/>
        // </div>

        // textarea - 使用 value
        return <div>
            <textarea value={this.state.info} onChange={this.onTextareaChange}/>
            <p>{this.state.info}</p>
        </div>

        // // select - 使用 value
        // return <div>
        //     <select value={this.state.city} onChange={this.onSelectChange}>
        //         <option value="beijing">北京</option>
        //         <option value="shanghai">上海</option>
        //         <option value="shenzhen">深圳</option>
        //     </select>
        //     <p>{this.state.city}</p>
        // </div>

        // // checkbox
        // return <div>
        //     <input type="checkbox" checked={this.state.flag} onChange={this.onCheckboxChange}/>
        //     <p>{this.state.flag.toString()}</p>
        // </div>

        // // radio
        // return <div>
        //     male <input type="radio" name="gender" value="male" checked={this.state.gender === 'male'} onChange={this.onRadioChange}/>
        //     female <input type="radio" name="gender" value="female" checked={this.state.gender === 'female'} onChange={this.onRadioChange}/>
        //     <p>{this.state.gender}</p>
        // </div>

        // 非受控組件 
    }
    onInputChange = (e) => {
        this.setState({
            name: e.target.value
        })
    }
    onTextareaChange = (e) => {
        this.setState({
            info: e.target.value
        })
    }
    onSelectChange = (e) => {
        this.setState({
            city: e.target.value
        })
    }
    onCheckboxChange = () => {
        this.setState({
            flag: !this.state.flag
        })
    }
    onRadioChange = (e) => {
        this.setState({
            gender: e.target.value
        })
    }
}

export default FormDemo

  1. 對於組件的使用,父子組件通訊,props 傳遞數據,props 傳遞函數,props 類型檢查,代碼如下所示:

import React from 'react'
import PropTypes from 'prop-types'

class Input extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            title: ''
        }
    }
    render() {
        return <div>
            <input value={this.state.title} onChange={this.onTitleChange}/>
            <button onClick={this.onSubmit}>提交</button>
        </div>
    }
    onTitleChange = (e) => {
        this.setState({
            title: e.target.value
        })
    }
    onSubmit = () => {
        const { submitTitle } = this.props
        submitTitle(this.state.title) // 'abc'

        this.setState({
            title: ''
        })
    }
}
// props 類型檢查
Input.propTypes = {
    submitTitle: PropTypes.func.isRequired
}

class List extends React.Component {
    constructor(props) {
        super(props)
    }
    render() {
        const { list } = this.props

        return <ul>{list.map((item, index) => {
            return <li key={item.id}>
                <span>{item.title}</span>
            </li>
        })}</ul>
    }
}
// props 類型檢查
List.propTypes = {
    list: PropTypes.arrayOf(PropTypes.object).isRequired
}

class Footer extends React.Component {
    constructor(props) {
        super(props)
    }
    render() {
        return <p>
            {this.props.text}
            {this.props.length}
        </p>
    }
    componentDidUpdate() {
        console.log('footer did update')
    }
    shouldComponentUpdate(nextProps, nextState) {
        if (nextProps.text !== this.props.text
            || nextProps.length !== this.props.length) {
            return true // 可以渲染
        }
        return false // 不重複渲染
    }

    // React 默認:父組件有更新,子組件則無條件也更新!!!
    // 性能優化對於 React 更加重要!
    // SCU 一定要每次都用嗎?—— 需要的時候才優化
}

class TodoListDemo extends React.Component {
    constructor(props) {
        super(props)
        // 狀態(數據)提升
        this.state = {
            list: [
                {
                    id: 'id-1',
                    title: '標題1'
                },
                {
                    id: 'id-2',
                    title: '標題2'
                },
                {
                    id: 'id-3',
                    title: '標題3'
                }
            ],
            footerInfo: '底部文字'
        }
    }
    render() {
        return <div>
            <Input submitTitle={this.onSubmitTitle}/>
            <List list={this.state.list}/>
            <Footer text={this.state.footerInfo} length={this.state.list.length}/>
        </div>
    }
    onSubmitTitle = (title) => {
        this.setState({
            list: this.state.list.concat({
                id: `id-${Date.now()}`,
                title
            })
        })
    }
}

export default TodoListDemo

  1. setState 是不可變值,可能是異步更新,可能會被合併,代碼如下所示:
import React from 'react'

// 函數組件(後面會講),默認沒有 state
class StateDemo extends React.Component {
    constructor(props) {
        super(props)

        // 第一,state 要在構造函數中定義
        this.state = {
            count: 0
        }
    }
    render() {
        return <div>
            <p>{this.state.count}</p>
            <button onClick={this.increase}>累加</button>
        </div>
    }
    increase = () => {
        // // 第二,不要直接修改 state ,使用不可變值 ----------------------------
        // // this.state.count++ // 錯誤
        // this.setState({
        //     count: this.state.count + 1 // SCU
        // })
        // 操作數組、對象的的常用形式

        // 第三,setState 可能是異步更新(有可能是同步更新) ----------------------------
        
        // this.setState({
        //     count: this.state.count + 1
        // }, () => {
        //     // 聯想 Vue $nextTick - DOM
        //     console.log('count by callback', this.state.count) // 回調函數中可以拿到最新的 state
        // })
        // console.log('count', this.state.count) // 異步的,拿不到最新值

        // // setTimeout 中 setState 是同步的
        // setTimeout(() => {
        //     this.setState({
        //         count: this.state.count + 1
        //     })
        //     console.log('count in setTimeout', this.state.count)
        // }, 0)

        // 自己定義的 DOM 事件,setState 是同步的。再 componentDidMount 中

        // 第四,state 異步更新的話,更新前會被合併 ----------------------------
        
        // // 傳入對象,會被合併(類似 Object.assign )。執行結果只一次 +1
        // this.setState({
        //     count: this.state.count + 1
        // })
        // this.setState({
        //     count: this.state.count + 1
        // })
        // this.setState({
        //     count: this.state.count + 1
        // })
        
        // 傳入函數,不會被合併。執行結果是 +3
        this.setState((prevState, props) => {
            return {
                count: prevState.count + 1
            }
        })
        this.setState((prevState, props) => {
            return {
                count: prevState.count + 1
            }
        })
        this.setState((prevState, props) => {
            return {
                count: prevState.count + 1
            }
        })
    }
    // bodyClickHandler = () => {
    //     this.setState({
    //         count: this.state.count + 1
    //     })
    //     console.log('count in body event', this.state.count)
    // }
    // componentDidMount() {
    //     // 自己定義的 DOM 事件,setState 是同步的
    //     document.body.addEventListener('click', this.bodyClickHandler)
    // }
    // componentWillUnmount() {
    //     // 及時銷燬自定義 DOM 事件
    //     document.body.removeEventListener('click', this.bodyClickHandler)
    //     // clearTimeout
    // }
}

export default StateDemo


// // 不可變值(函數式編程,純函數) - 數組
// const list5Copy = this.state.list5.slice()
// list5Copy.splice(2, 0, 'a') // 中間插入/刪除
// this.setState({
//     list1: this.state.list1.concat(100), // 追加
//     list2: [...this.state.list2, 100], // 追加
//     list3: this.state.list3.slice(0, 3), // 截取
//     list4: this.state.list4.filter(item => item > 100), // 篩選
//     list5: list5Copy // 其他操作
// })
// // 注意,不能直接對 this.state.list 進行 push pop splice 等,這樣違反不可變值

// // 不可變值 - 對象
// this.setState({
//     obj1: Object.assign({}, this.state.obj1, {a: 100}),
//     obj2: {...this.state.obj2, a: 100}
// })
// // 注意,不能直接對 this.state.obj 進行屬性設置,這樣違反不可變值

  1. setState 有的是同步的,有的是異步的,代碼如下所示:
import React from 'react'

class ListDemo extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            count: 0
        }
    }
    render() {
        return <p>{this.state.count}</p>
    }
    componentDidMount() {
        // count 初始值爲 0
        this.setState({ count: this.state.count + 1 })
        console.log('1', this.state.count) // 0
        this.setState({ count: this.state.count + 1 })
        console.log('2', this.state.count) // 0
        setTimeout(() => {
            this.setState({ count: this.state.count + 1 })
            console.log('3', this.state.count) // 2
        })
        setTimeout(() => {
            this.setState({ count: this.state.count + 1 })
            console.log('4', this.state.count) // 3
        })
    }
}

export default ListDemo

  1. React 的組件生命週期,單組件生命週期,父子組件生命週期,和 Vue 的一樣。

  2. 對於 React 的高級特性,分爲函數組件、非受控組件、Portalscontext 、異步組件、性能優化、高階組件 HOCRender Props,如下所示:

  • 函數組件,純函數,輸入 props,輸出 JSX。沒有實例,沒有生命週期,沒有 state,不能擴展其它辦法
  • 非受控組件,refdefaultValuedefaultChecked,手動操作 DOM 元素。對於非受控組件的使用場景,必須手動操作 DOM 元素,setState 實現不了。文件上傳 <input type=file>, 某些富文本編輯器,需要傳入 DOM 元素。對於受控組件和非受控組件的比較,優先使用受控組件,符合 React 的設計原則。必須操作 DOM 時,再使用非受控組件,代碼如下所示:
import React from 'react'

class App extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            name: '張三',
            flag: true,
        }
        this.nameInputRef = React.createRef() // 創建 ref
        this.fileInputRef = React.createRef()
    }
    render() {
        // // input defaultValue
        // return <div>
        //     {/* 使用 defaultValue 而不是 value ,使用 ref */}
        //     <input defaultValue={this.state.name} ref={this.nameInputRef}/>
        //     {/* state 並不會隨着改變 */}
        //     <span>state.name: {this.state.name}</span>
        //     <br/>
        //     <button onClick={this.alertName}>alert name</button>
        // </div>

        // // checkbox defaultChecked
        // return <div>
        //     <input
        //         type="checkbox"
        //         defaultChecked={this.state.flag}
        //     />
        // </div>

        // file
        return <div>
            <input type="file" ref={this.fileInputRef}/>
            <button onClick={this.alertFile}>alert file</button>
        </div>

    }
    alertName = () => {
        const elem = this.nameInputRef.current // 通過 ref 獲取 DOM 節點
        alert(elem.value) // 不是 this.state.name
    }
    alertFile = () => {
        const elem = this.fileInputRef.current // 通過 ref 獲取 DOM 節點
        alert(elem.files[0].name)
    }
}

export default App

  • React Portals,組件默認會按照既定層次嵌套渲染,讓組件渲染到父組件之外。對於 Portals 的使用場景,overflow: hidden,父組件 z-index 值太小,flexd 需要放在 body 第一層級,代碼如下所示:
import React from 'react'
import ReactDOM from 'react-dom'
import './style.css'

class App extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
        }
    }
    render() {
        // // 正常渲染
        // return <div className="modal">
        //     {this.props.children} {/* vue slot */}
        // </div>

        // 使用 Portals 渲染到 body 上。
        // fixed 元素要放在 body 上,有更好的瀏覽器兼容性。
        return ReactDOM.createPortal(
            <div className="modal">{this.props.children}</div>,
            document.body // DOM 節點
        )
    }
}

export default App

  • context,上下文環境,公共信息傳遞給每個組件,用 props 太繁瑣,用 redux 小題大做,代碼如下所示:
import React from 'react'

// 創建 Context 填入默認值(任何一個 js 變量)
const ThemeContext = React.createContext('light')

// 底層組件 - 函數是組件
function ThemeLink (props) {
    // const theme = this.context // 會報錯。函數式組件沒有實例,即沒有 this

    // 函數式組件可以使用 Consumer
    return <ThemeContext.Consumer>
        { value => <p>link's theme is {value}</p> }
    </ThemeContext.Consumer>
}

// 底層組件 - class 組件
class ThemedButton extends React.Component {
    // 指定 contextType 讀取當前的 theme context。
    // static contextType = ThemeContext // 也可以用 ThemedButton.contextType = ThemeContext
    render() {
        const theme = this.context // React 會往上找到最近的 theme Provider,然後使用它的值。
        return <div>
            <p>button's theme is {theme}</p>
        </div>
    }
}
ThemedButton.contextType = ThemeContext // 指定 contextType 讀取當前的 theme context。

// 中間的組件再也不必指明往下傳遞 theme 了。
function Toolbar(props) {
    return (
        <div>
            <ThemedButton />
            <ThemeLink />
        </div>
    )
}

class App extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            theme: 'light'
        }
    }
    render() {
        return <ThemeContext.Provider value={this.state.theme}>
            <Toolbar />
            <hr/>
            <button onClick={this.changeTheme}>change theme</button>
        </ThemeContext.Provider>
    }
    changeTheme = () => {
        this.setState({
            theme: this.state.theme === 'light' ? 'dark' : 'light'
        })
    }
}

export default App

  • 異步組件,import()、React.lazy、React.Suspense,代碼如下所示:
import React from 'react'

const ContextDemo = React.lazy(() => import('./ContextDemo'))

class App extends React.Component {
    constructor(props) {
        super(props)
    }
    render() {
        return <div>
            <p>引入一個動態組件</p>
            <hr />
            <React.Suspense fallback={<div>Loading...</div>}>
                <ContextDemo/>
            </React.Suspense>
        </div>

        // 1. 強制刷新,可看到 loading (看不到就限制一下 chrome 網速)
        // 2. 看 network 的 js 加載
    }
}

export default App

  1. 對於 React 的性能優化,shouldComponentUpdate,簡稱 SCUPureComponentReact.memo,不可變值 immutable.js,如下所示:
  • SCUSCU 默認返回 true,即 React 默認重新渲染所有子組件,必須配合 不可變值 一起使用。可先不用 SCU,有性能問題時再考慮使用,代碼如下所示:
import React from 'react'
import PropTypes from 'prop-types'
import _ from 'lodash'

class Input extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            title: ''
        }
    }
    render() {
        return <div>
            <input value={this.state.title} onChange={this.onTitleChange}/>
            <button onClick={this.onSubmit}>提交</button>
        </div>
    }
    onTitleChange = (e) => {
        this.setState({
            title: e.target.value
        })
    }
    onSubmit = () => {
        const { submitTitle } = this.props
        submitTitle(this.state.title)

        this.setState({
            title: ''
        })
    }
}
// props 類型檢查
Input.propTypes = {
    submitTitle: PropTypes.func.isRequired
}

class List extends React.Component {
    constructor(props) {
        super(props)
    }
    render() {
        const { list } = this.props

        return <ul>{list.map((item, index) => {
            return <li key={item.id}>
                <span>{item.title}</span>
            </li>
        })}</ul>
    }

    // 增加 shouldComponentUpdate
    shouldComponentUpdate(nextProps, nextState) {
        // _.isEqual 做對象或者數組的深度比較(一次性遞歸到底)
        if (_.isEqual(nextProps.list, this.props.list)) {
            // 相等,則不重複渲染
            return false
        }
        return true // 不相等,則渲染
    }
}
// props 類型檢查
List.propTypes = {
    list: PropTypes.arrayOf(PropTypes.object).isRequired
}

class TodoListDemo extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            list: [
                {
                    id: 'id-1',
                    title: '標題1'
                },
                {
                    id: 'id-2',
                    title: '標題2'
                },
                {
                    id: 'id-3',
                    title: '標題3'
                }
            ]
        }
    }
    render() {
        return <div>
            <Input submitTitle={this.onSubmitTitle}/>
            <List list={this.state.list}/>
        </div>
    }
    onSubmitTitle = (title) => {
        // 正確的用法
        this.setState({
            list: this.state.list.concat({
                id: `id-${Date.now()}`,
                title
            })
        })

        // // 爲了演示 SCU ,故意寫的錯誤用法
        // this.state.list.push({
        //     id: `id-${Date.now()}`,
        //     title
        // })
        // this.setState({
        //     list: this.state.list
        // })
    }
}

export default TodoListDemo

  • PureComponentmemoPureComponentSCU 中實現了淺比較。memo,函數組件中的 PureComponent,淺比較已使用大部分情況,儘量不要做深度比較

  • immutable.js,徹底擁抱不可變質,基於共享數據,不是深拷貝,速度好,按需使用

  1. 關於組件公共邏輯的抽離,mixin,已被 React 棄用,高階組件 HOC,Render PropsHOC 是模式簡單,但會增加組件層級,Render Props 是代碼簡潔,學習成本較高,按需使用,代碼如下所示:
  • HOC.js
    import React from 'react'
    
    // 高階組件
    const withMouse = (Component) => {
        class withMouseComponent extends React.Component {
            constructor(props) {
                super(props)
                this.state = { x: 0, y: 0 }
            }
      
            handleMouseMove = (event) => {
                this.setState({
                    x: event.clientX,
                    y: event.clientY
                })
            }
      
            render() {
                return (
                    <div style={{ height: '500px' }} onMouseMove={this.handleMouseMove}>
                        {/* 1. 透傳所有 props 2. 增加 mouse 屬性 */}
                        <Component {...this.props} mouse={this.state}/>
                    </div>
                )
            }
        }
        return withMouseComponent
    }
    
    const App = (props) => {
        const a = props.a
        const { x, y } = props.mouse // 接收 mouse 屬性
        return (
            <div style={{ height: '500px' }}>
                <h1>The mouse position is ({x}, {y})</h1>
                <p>{a}</p>
            </div>
        )
    }
    
    export default withMouse(App) // 返回高階函數
    
    
  • RenderProp.js
    import React from 'react'
    import PropTypes from 'prop-types'
    
    class Mouse extends React.Component {
        constructor(props) {
            super(props)
            this.state = { x: 0, y: 0 }
        }
      
        handleMouseMove = (event) => {
          this.setState({
            x: event.clientX,
            y: event.clientY
          })
        }
      
        render() {
          return (
            <div style={{ height: '500px' }} onMouseMove={this.handleMouseMove}>
                {/* 將當前 state 作爲 props ,傳遞給 render (render 是一個函數組件) */}
                {this.props.render(this.state)}
            </div>
          )
        }
    }
    Mouse.propTypes = {
        render: PropTypes.func.isRequired // 必須接收一個 render 屬性,而且是函數
    }
    
    const App = (props) => (
        <div style={{ height: '500px' }}>
            <p>{props.a}</p>
            <Mouse render={
                /* render 是一個函數組件 */
                ({ x, y }) => <h1>The mouse position is ({x}, {y})</h1>
            }/>
            
        </div>
    )
    
    /**
     * 即,定義了 Mouse 組件,只有獲取 x y 的能力。
     * 至於 Mouse 組件如何渲染,App 說了算,通過 render prop 的方式告訴 Mouse 。
     */
    
    export default App
    
    
  1. Redux 的使用,分爲基本概念、單項數據流、react-redux、異步 action 和中間件,如下所示:
  • redux 的單向數據流,store、state、action 和 reducer。應用中所有的 state 都以一個對象樹的形式儲存在一個單一的 store 中。 惟一改變 state 的辦法是觸發 action,一個描述發生什麼的對象。 爲了描述 action 如何改變 state 樹,你需要編寫 reducers。這是一個 reducer,形式爲(state, action) => state的純函數,描述了 action 如何把 state 轉變成下一個 statestate 的形式取決於你,可以是基本類型、數組、對象、甚至是 Immutable.js 生成的數據結構。惟一的要點是當 state 變化時需要返回全新的對象,而不是修改傳入的參數。改變內部 state 惟一方法是 dispatch 一個 actionaction 可以被序列化,用日記記錄和儲存下來,後期還可以以回放的方式執行。對於單向數據流,dispatch(action)、reducer -> newState、subscribe 觸發通知
  • 對於react-redux,<Provider> content、connect、mapStateToProps 和 mapDispatchToProps
  • 對於異步 action,有 redux-thunk、redux-promise 和 redux-saga
  • 對於 redux 中間件,在單向數據流的時候,button 進行 callback 指向 dispatchdispatch 通過 action 指向 reducerreducer 通過 state 指向 view。在有中間件以後,button 通過 callback 指向 new dispatch。在 new dispatch 中,mid1 -> mid2 -> action -> dispatch。最後 new dispatch 通過 action 指向 reducer
  1. 對於 redux 的使用,代碼如下所示:
  • action 文件夾

    • index.js
      let nextTodoId = 0
      
      // 創建一個 todo
      export const addTodo = text => {
        return {
          type: 'ADD_TODO',
          id: nextTodoId++,
          text
        }
      }
      
      // 設置完成狀態
      export const setVisibilityFilter = filter => {
        return {
          type: 'SET_VISIBILITY_FILTER',
          filter
        }
      }
      
      // 切換 todo 完成狀態
      export const toggleTodo = id => {
        return {
          type: 'TOGGLE_TODO',
          id
        }
      }
      
  • components 文件夾

    • App.js

      import React from 'react'
      import Footer from './Footer'
      import AddTodo from '../containers/AddTodo'
      import VisibleTodoList from '../containers/VisibleTodoList'
      
      const App = () => (
        <div>
          <AddTodo />
          <VisibleTodoList />
          <Footer />
        </div>
      )
      
      export default App
      
    • Footer.js

          import React from 'react'
          import FilterLink from '../containers/FilterLink'
          
          const Footer = () => (
            <p>
              Show:
              {' '}
              <FilterLink filter="SHOW_ALL">
                All
              </FilterLink>
              {', '}
              <FilterLink filter="SHOW_ACTIVE">
                Active
              </FilterLink>
              {', '}
              <FilterLink filter="SHOW_COMPLETED">
                Completed
              </FilterLink>
            </p>
          )
          
          export default Footer
          ```
      
      
    • Link.js

      import React from 'react'
      import PropTypes from 'prop-types'
      
      const Link = ({ active, children, onClick }) => {
        if (active) {
          return <span>{children}</span>
        }
      
        return (
          <a
            href=""
            onClick={e => {
              e.preventDefault()
              onClick() // 設置過濾狀態
            }}
          >
            {children}
          </a>
        )
      }
      
      Link.propTypes = {
        active: PropTypes.bool.isRequired,
        children: PropTypes.node.isRequired,
        onClick: PropTypes.func.isRequired
      }
      
      export default Link
      
    • Todo.js

      import React from 'react'
      import PropTypes from 'prop-types'
      
      // 函數組件 { onClick, completed, text } = props
      const Todo = ({ onClick, completed, text }) => (
        <li
          onClick={onClick}
          style={ {
            textDecoration: completed ? 'line-through' : 'none'
          }}
        >
          {text}
        </li>
      )
      
      Todo.propTypes = {
        onClick: PropTypes.func.isRequired,
        completed: PropTypes.bool.isRequired,
        text: PropTypes.string.isRequired
      }
      
      export default Todo
      
    • TodoList.js

          import React from 'react'
          import PropTypes from 'prop-types'
          import Todo from './Todo'
          
          // 函數組件 { todos, onTodoClick } = props
          const TodoList = ({ todos, onTodoClick }) => (
            <ul>
              {todos.map(todo => (
                <Todo key={todo.id} {...todo} onClick={() => onTodoClick(todo.id)} /> // 點擊切換完成狀態
              ))}
            </ul>
          )
          
          TodoList.propTypes = {
            todos: PropTypes.arrayOf(
              PropTypes.shape({
                id: PropTypes.number.isRequired,
                completed: PropTypes.bool.isRequired,
                text: PropTypes.string.isRequired
              }).isRequired
            ).isRequired,
            onTodoClick: PropTypes.func.isRequired
          }
          
          export default TodoList
      
  • containers 文件夾

    • AddTodo.js
          import React from 'react'
          import { connect } from 'react-redux'
          import { addTodo } from '../actions'
          
          // 函數組件,接收 props 參數
          let AddTodo = ({ dispatch }) => {
            // dispatch 即 props.dispatch
          
            let input
          
            return (
              <div>
                <form
                  onSubmit={e => {
                    e.preventDefault()
                    if (!input.value.trim()) {
                      return
                    }
                    // 創建一個 todo
                    dispatch(addTodo(input.value))
                    input.value = ''
                  }}
                >
                  <input
                    ref={node => {
                      input = node
                    }}
                  />
                  <button type="submit">
                    Add Todo
                  </button>
                </form>
              </div>
            )
          }
          
          // connect 高階組件 ,將 dispatch 作爲 props 注入到 AddTodo 組件中
          AddTodo = connect()(AddTodo)
          
          export default AddTodo
      
    • FilterLink.js
      import { connect } from 'react-redux'
      import { setVisibilityFilter } from '../actions'
      import Link from '../components/Link'
      
      const mapStateToProps = (state, ownProps) => {
        // ownProps 即父組件傳來的 `filter="SHOW_ALL"`
        return {
          active: ownProps.filter === state.visibilityFilter
        }
      }
      
      const mapDispatchToProps = (dispatch, ownProps) => {
        // ownProps 即父組件傳來的 `filter="SHOW_ALL"`
        return {
          onClick: () => {
            // 設置過濾狀態
            dispatch(setVisibilityFilter(ownProps.filter))
          }
        }
      }
      
      const FilterLink = connect(
        mapStateToProps,
        mapDispatchToProps
      )(Link)
      
      export default FilterLink
      
    • VisibleTodoList.js
      import { connect } from 'react-redux'
      import { toggleTodo } from '../actions'
      import TodoList from '../components/TodoList'
      
      // 不同類型的 todo 列表
      const getVisibleTodos = (todos, filter) => {
        switch (filter) {
          case 'SHOW_ALL':
            return todos
          case 'SHOW_COMPLETED':
            return todos.filter(t => t.completed)
          case 'SHOW_ACTIVE':
            return todos.filter(t => !t.completed)
        }
      }
      
      const mapStateToProps = state => {
        // state 即 vuex 的總狀態,在 reducer/index.js 中定義
        return {
          // 根據完成狀態,篩選數據
          todos: getVisibleTodos(state.todos, state.visibilityFilter)
        }
      }
      
      const mapDispatchToProps = dispatch => {
        return {
          // 切換完成狀態
          onTodoClick: id => {
            dispatch(toggleTodo(id))
          }
        }
      }
      
      // connect 高階組件,將 state 和 dispatch 注入到組件 props 中
      const VisibleTodoList = connect(
        mapStateToProps,
        mapDispatchToProps
      )(TodoList)
      
      export default VisibleTodoList
      
  • reducers 文件夾

    • index.js
      import { combineReducers } from 'redux'
      import todos from './todos'
      import visibilityFilter from './visibilityFilter'
      
      const todoApp = combineReducers({
        todos,
        visibilityFilter
      })
      
      export default todoApp
      
    • todos.js
          const todos = (state = [], action) => {
          switch (action.type) {
            // 創建一個 todo
            case 'ADD_TODO':
              // 注意,返回不可變數據
              return [
                ...state,
                {
                  id: action.id,
                  text: action.text, // title
                  completed: false
                }
              ]
            
            // 切換一個 todo 的完成狀態
            case 'TOGGLE_TODO':
              // 注意,返回不可變數據
              return state.map(todo =>
                (todo.id === action.id) 
                  ? {...todo, completed: !todo.completed} // 切換完成狀態
                  : todo
              )
            default:
              return state
          }
        }
        
        export default todos
      
    • visibilityFilter.js
          const visibilityFilter = (state = 'SHOW_ALL', action) => {
          switch (action.type) {
            // 設置顯示類型(所有、完成、未完成)
            case 'SET_VISIBILITY_FILTER':
              return action.filter
      
            // 默認是 SHOW_ALL
            default:
              return state
          }
        }
        
        export default visibilityFilter
      
  • index.js

    import React from 'react'
    import { Provider } from 'react-redux'
    import { createStore } from 'redux'
    import todoApp from './reducers'
    import App from './components/App'
    
    let store = createStore(todoApp)
    
    export default function () {
        return <Provider store={store}>
            <App />
        </Provider>
    }
    
  1. React-router,路由模式 hashH5 history,路由配置 動態路由和懶加載。hash 模式是默認的,如 http://abc.com/#/user/10。H5 history 模式,如 http://abc.com/user/20,後者需要 server 端支持,因此無特殊需求可選擇前者。

二、React 的原理

  1. 對於 React 的原理,分爲函數式編程、vdomdiffJSX 本質、合成事件、setStatebatchUpdate、組件渲染過程。

  2. 對於函數式編程,一種編程範式,純函數,不可變值。不可變值數組,不能直接對 this.state.list 進行 push pop splice 等,這樣違反不可變值。不可變值對象,不能直接對 this.state.obj 進行屬性設置,這樣違反不可變值。state 要在構造函數中定義
    ;不要直接修改 state,使用不可變值;setState 可能是異步更新(有可能是同步更新);state 異步更新的話,更新前會被合併。

  3. 對於 vdomdiffh 函數、vdom 數據結構、patch 函數。只比較同一層級,不跨級比較。tag 不相同,則直接刪掉重建,不再深度比較。tagkey,兩者都相同,則認爲是相同節點,不再深度比較。

  4. 對於 JSX 的本質,JSX 等同於 Vue 模版,Vue 模版不是 htmlJSX 也不是 JS。如第一個參數是 List 組件,找到 List 組件 jsx 結構,繼續拆分。

  5. 對於合成事件,所有事件掛載到 document 上,event 不是原生的,是 SyntheticEvent 合成事件對象。它和 Vue 事件不同,和 DOM 事件也不同。需要合成事件機制的原因,如下所示:

  • 更好的兼容性和跨平臺
  • 掛載到 document,減少內存消耗,避免頻繁解綁
  • 方便事件的統一管理,如事務機制
  1. setStatebatchUpdate,如下所示:
  • 有時異步,普通使用的時候;有時同步,setTimeoutDOM 事件的時候
  • 有時合併,對象形式;有時不合並,函數形式;
  • 後者相對於好理解,像 Object.assign,前者相對於更重要
  • 對於核心要點,setState 主流程、batchUpdate機制、transition 事務機制
  • 對於 setState 主流程,this.setState(newState)newState 存入 pending 隊列,然後判斷是否處於 batch update。如果是的話,保存組件於 dirtyComponents 中。如果不是的話,遍歷所有的 dirtyComponents,調用 updateComponent,更新 pending state or props
  • 對於 setState 是同步的還是異步的,其實 setState 無所謂異步還是同步,看是否能夠命中 batchUpdate 機制,判斷 isBatchingUpdates
  • 對於哪些能夠命中 batchUpdate 機制,如下所示:
    • 生命週期和它調用的函數
    • React 中註冊的事件和它調用的函數
    • React 可以管理的入口
  • 對應哪些不能夠命中 batchUpdate 機制,如下所示:
    • setTimeout、setInterval 等和它調用的函數
    • 自定義的 DOM 事件和它調用的函數
    • React 管不到的入口
  • 對於 transition 事務機制,開始處於 batchUpdateisBatchUpdates = true,其它任何操作,結束,isBatchUpdates = false
  1. 對於組件渲染和更新過程,JSX 如何渲染爲頁面,setState 之後如何更新頁面,以及對於全流程的理解,如下所示:
  • JSXcreateElement 函數,執行生成 vnodepatch(elem, vnode)patch(vnode, newVnode)
  • 對於組件的渲染過程,props state,render() 生成 vnodepatch(elem, vnode)
  • 對於組件的更新過程,setState(newState) -> dirtyComponents,可能有子組件。render() 生成 newVnodepatch(vnode, newVnode)
  • 對於 patch 更新的兩個階段,reconciliation 階段執行 diff 算法,純 JS 計算。在 commit 階段,將 diff 結果渲染 DOM
  • 對於可能出現的性能問題,JS 是單線程的,且和 DOM 渲染共用一個線程。當組件足夠複雜的時候,組件更新時計算和渲染都壓力大,同時再有 DOM 操作需求,如動畫、鼠標拖拽等,將卡頓
  • 解決方案就是 fiber,將 reconciliation 階段進行任務拆分,但是 commit 無法拆分。DOM 需要渲染時暫停,空閒時修復,window.requestIdleCallback

三、React 的面試真題

  1. 組件之間如何通訊,答案如下所示:
  • 父子組件 props
  • 自定義事件
  • ReduxContext
  1. JSX 的本質是什麼,答案如下所示:
  • createElement
  • 執行返回 vnode
  1. Context 是什麼,如何應用,答案如下所示:
  • 父組件,向其下所有子孫組件傳遞信息
  • 一些簡單的公共信息,主題色、顏色等等
  • 複雜的公共信息,用 redux
  1. shouldComponentUpdate 的用途,答案如下所示:
  • 性能優化
  • 配合不可變值一起使用,否則會出錯
  1. 什麼是純函數,答案如下所示:
  • 返回一個新值,沒有副作用,不會修改其它值
  • 重點就是不可變值
  • arr1 = arr.slice()
  1. 對於 React 的組件生命週期,答案如下所示:
  • 單組件生命週期
  • 父子組件生命週期
  • 注意 SCU
  1. React 發起 ajax 應該在哪個生命週期,答案如下所示:
  • componentDidMount 這個生命週期函數
  1. 渲染列表,爲何使用 key,答案如下所示:
  • 必須用 key,且不能是 indexrandom
  • diff 算法中通過 tagkey 來判斷,是否是 sameNode
  • 減少渲染次數,提升渲染性能
  1. 函數組件和 class 組件的區別,答案如下所示:
  • 純函數,輸入 props,輸出 JSX
  • 沒有實例,沒有生命週期,沒有 state
  • 不能擴展其它辦法
  1. 什麼是受控組件,答案如下所示:
  • 表單的值,受 state 控制
  • 需要自行監聽 onChange,更新 state
  • 對比非受控組件
  1. 何時使用異步組件,答案如下所示:
  • 加載大組件
  • 路由懶加載
  1. 多個組件有公共邏輯,如何抽離,答案如下所示:
  • 高階組件
  • Render Props
  • mixin 已被 React 廢棄
  1. redux 如何進行異步請求,答案如下所示:
  • 使用異步 action
  • redux-thunk
  1. PureComponent 有何區別,答案如下所示:
  • 實現了淺比較的 shouldComponentUpdate
  • 優化性能
  • 需要結合不可變值使用
  1. React 事件和 DOM 事件的區別,答案如下所示:
  • 所有事件掛載到 document
  • event 不是原生的,是 SyntheticEvent 合成事件對象
  • dispatchEvent
  1. 對於 React 的性能優化,答案如下所示:
  • 渲染列表時添加 key
  • 自定義事件、DOM 事件及時銷燬
  • 合理使用異步組件
  • 減少函數 bind this 的次數
  • 合理使用 SCU、PureComponent 和 memo
  • 合理使用 Immutable.js
  • webpack 層面的優化
  • 前端通用的性能優化,如圖片懶加載
  • 使用 SSR
  1. ReactVue 的區別,答案如下所示:
  • 都支持組件化
  • 都是數據驅動視圖
  • 都使用 vdom 操作 DOM
  • React 使用 JSX 擁抱 JSVue 使用模版擁抱 html
  • React 函數式編程,Vue 聲明式編程
  • React 更多的需要是自力更生,Vue 是把想要的都給你
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章