React 面试题

JSX

JSX 是 JS 的一种语法扩展,JSX 可以生成 React 元素。

// 实际上 Babel 会把 JSX 转译为 React.createElement() 函数调用
const element = (
    <h1 className="greeting">
        Hello React!
    </h1>
);

// 这就是为什么你必须引入 React 库,因为使用 JSX 就需要 React 库
const element = React.createElement(
    'h1',
    {className: 'greeting'},
    'Hello React!'
);

// React.createElement() 会创建这样的对象,也叫做 React 元素,就是虚拟 DOM
const element = {
    type: 'h1',
    props: {
        className: 'greeting',
        children: 'Hello React!'
    }
}

组件

组件名一定要大写,因为在 JSX 中小写的会被当成 html 标签

  • 编译为 React.createElement('todo')
  • 编译为 React.createElement(Todo);

React 的组件分为 函数组件 和 class 组件

// 函数组件没有内部的状态,也没有生命周期
function Hello(props) {
    return (
        <div>
            // 函数组件使用 props
            Hello React {props.name}
        </div>
    )
}

// class 组件则拥有状态、生命周期
class Count extends React.Component {
    constructor(props) {
        super(props);
    }
    render(){
        return(
            <div>
                // class 组件使用 props
                Hello React {this.props.name}
            </div>
        )
    }
}

事件处理

传统的 HTML 使用纯小写 onclick,React 使用驼峰式 onClick

// html
<button onclick='func'></button>

// react
<button onClick={activateLasers}>
    Activate Lasers
</button>

// 传统的 HTML 可以通过 return false 来阻止默认行为,React 不行,必须使用 event.preventDefault
<a href='#' onclick="console.log('The link was clicked'); return false">
    Click me
</a>

// react
function ActionLink(){
    function handleClick(e){
        e.preventDefault();
        console.log('The link was clicked.');
    }

    return (
        <a href='#' onClick={handleClick}>
            Click me
        </a>
    )
}
// React 中的 e 为合成事件,因此无需担心浏览器兼容性的问题。

组件生命周期

挂载

  • constructor()
  • static getDerivedStateFromProps()
  • render()
  • componentDidMount()

更新
static getDerivedStateFromProps()
shouldComponentUpdate()
render()
getSnapshotBeforeUpdate()
componentDidUpdate()

卸载
componentWillUnmount()

setState

// 不要直接修改 state
// Wrong: 此代码不会重新渲染组件
this.state.comment = 'Hello';

// 应该使用 setState
// Correct
this.setState({
    comment: 'hello'
})
// state 的更新可能/通常是异步的
class App extends React.Component {
    state = {count: 0}
    ...(){
        this.setState({count: 1});
        console.log(this.state.count) // 输出 0
    }
}

// 如果你想要获得修改后的值
this.setState({
    count: 1
}, () => {
    console.log(this.state.count) // 输出 1
})
// 如果第二个 setState 依赖于第一个 setState 之后的值,则可以将第二个 setState 的参数设置为函数
this.setState({
    count: 1
}, () => {
    console.log(this.state.count);
})

this.setState((state, props) => {
    return {
        count: state.count + 1;
    }
})

setState 何时是异步的

在合成事件和组件的生命周期中 setState 是异步的,在原生事件和定时器中 setState 是同步的。
React 内部维护了一个标识 isBatchingUpdates,当这个值为 true 表示将 setState 缓存进入队列,最后进行批量更新;当这个值为 false 时表示直接更新。
合成事件和组件的生命周期中,会把 isBatchingUpdates 设置为 true;
原生事件和定时器中,会把 isBatchingUpdates 设置为 false;
当你调用 setState() 的时候, React 会把你提供的对象合并到当前的 state;

列表渲染

{
props.todos.map((todo) => {
return
})
}

表单

class App extends React.Component{
constructor(props){
super(props);
this.state = {
value: ''
}
}

handleChange = (e) => {
    this.setState({
        value: e.target.value
    })
}

render(){
    return (
        <div>
            {this.state.value}
            <input type="text" value={this.state.value} onChange={this.handleChange} />
        </div>
    )
}

}

受控组件

通常表单内部拥有自己的状态,状态会被用户的输入所改变。而 React 中,用户的输入会被劫持,实际上的数据源完全由 React 提供
React 的 state 称为组件/表单的唯一数据源,渲染表单的组件还控制着用户输入过程中表单发生的操作。被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”。当然,与之对应的称为“非受控组件”。

Refs

何时使用 Refs

  • 管理焦点,文本选择或媒体交换
  • 继承第三方 DOM 库
  • 触发强制动画
class App extends React.Component {
    constructor(props) {
        super(props);
        this.myRef = React.createRef();
    }

    handleClick = () => {
        this.myRef.current.focus();
    }

    render() {
        return (
            <div>
                // 绑定
                <input type="text" ref={this.myRef} />
                <button onClick={this.handleClick}>点我</button>
            </div>
        )
    }

}

组件通信

父子组件通信

父组件通过 props 传递数据给子组件。
父组件通过 props 把自己的函数传递给子组件,子组件内部可以直接调用,实现子组件向父组件通信。

非父子组件通信

可以通过 events 实现发布-订阅,也可以借助于 context。
复杂的情况可以考虑用 Redux。

Context

Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。

// Context 让我们可以无需明确地传遍每一个组件,就能将值深入传递进组件树
// 为当前地 theme 创建一个 context
const ThemeContext = React.createContext('light');

class App extends React.Component {
    render(){
        // 使用一个 Provider 来将当前地 theme 传递给以下地组件树
        // 无论多深,任何组件都能读取这个值
        //  在这个例子中,我们将 “dark” 作为当前的值传递下去
        return (
            <ThemeContext.Provider value="dark">
                <Toolbar />
            </ThemeContext.Provider>
        );
    }
}

// 中间的组件再也不必指明往下传递 theme 了
function Toolbar(props){
    return (
        <div>
            <ThemeButton />
        </div>
    );
}

class ThemeButton extends Component {
    // 指定 contentType 读取当前的 theme context
    // React 会向上找到最近的 theme Provider,然后使用它的值
    // 在这个例子中,当前的 theme 值为 “dark”
    static contextType = ThemeContext;
    render() {
        return <Button theme={this.context} />;
    }
}

Fiber 架构

React 在它的 V16 版本推出了 Fiber 架构。
浏览器是多线程的,这些线程包括 JS 引擎线程(主线程),以及 GUI 渲染线程,定时器线程,事件线程等工作线程。其中,JS 引擎线程和 GUI 渲染线程是互斥的。又因为绝大多数的浏览器页面的刷新频率取决于显示器的刷新频率,即每16.6毫秒就会通过 GUI 渲染引擎刷新一次。所以,如果 JS 引擎线程一次性执行了一个长时间(大于16.6毫秒)的同步任务,就可能出现掉帧的现象,影响用户的体验。
在旧版本的 React 中,对于一个庞大的组件,无论组件的创建还是更新都可能需要较长的时间。而 Fiber 的思路是将原本耗时较长的同步任务分片为多个任务单元,执行完一个任务单元后可以保存当前的状态,切换到 GUI 渲染线程去刷新页面,接下来再回到主线程并从上个断点继续执行任务。
React 中的 Fiber,将原本耗时很长的同步任务分成多个耗时短的分片,从而实现了浏览器中互斥的主线程与 GUI 渲染线程之间的调度。
除此之外,对于每一个 Fiber 的同步任务来说,都拥有一个优先级(总共定义了6中优先级)。
当主线程执行完一个任务 A 的一个分片,若此时出现了一个优先级更高的任务 B, React 就可能会把任务 A 废弃掉,待之后重新执行一次任务 A。

Diff 策略

    1. Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。
    1. 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。
    1. 对于同一层级的一组子节点,它们可以通过唯一 ID 进行区分。

Hook

Hook 是一种特殊的函数,它可以让你“勾入” React 的特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 class 组件的特性。

useState

import React, { useState } from 'react';

function Example(props) {
    let [count, setCount] = useState(0);

    return (
        <div>
            { count }
            <button onClick={() => setCount(count + 1)}>
                Click Me
            </button>
        </div>
    )
}

useEffect

Effect Hook 可以让你在函数组件中执行副作用操作

import React, {
    useState,
    useEffect,
} from 'react';

function App(){
    const [count, setCount] = useState(0);

    useEffect(() => {
        document.title = `YOU clicked ${count} times`;
    });

    return (
        <div>
            <p>You clicked {count} times</p>
            <button onClick={() => setCount(count+1)}>
                Click Me
            </button>
        </div>
    );
}

useEffect 会在每次渲染后都执行,包括初次渲染和每次数据更新之后。
组件挂载时,运行副作用;组件更新时,先清除上一个 effect,再运行下一个 effect;组件卸载时,清除最后一个 effect;

function FriendStatus(props){
    useEffect(() => {
        ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
        return () => {
            ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
        }
    })
}

useRef

import React, { useRef } from 'react';

function App(props){
    let refs = useRef(null);
    return (
        <input ref = {refs}>
    )
}

useReducer

function useReducer(reducer, initialState){
    const [state, setState] = useState(initialState);

    function dispatch(action) {
        const nextState = reducer(state, action);
        setState(nextState);
    }

    return [state, dispatch];
}


function Todos() {
    const [todos, dispatch] = useReducer(todosReducer, []);

    function handleAddClick(text) {
        dispatch({ type: 'ADD', text});
    }
    // ...
}

function todosReducer(state, action) {
    switch (action.type) {
        case 'ADD':
            return [...state, {
                text: action.text,
                completed: false,
            }];
        // other actions
        default:
            return state;
    }
}

useMemo 和 useCallback

Vue 和 React 有一个比较明显的差异。
由于 Vue 使用了对数据的劫持,知道具体数据的变化。因此当父组件数据改变,而子组件数据没有改变时,只有父组件会重新渲染。
而 React 的方式很粗暴,只要父组件的数据改变,强制会让子组件也重新渲染。
React 的类组件分为 React.Component 和 React.PureComponent, React 会浅对比该组件的前后 state 和 props,如果没有变化,则不会重新渲染组件。
而对于函数组件来说,默认情况下也是会重新渲染的,我们可以通过 React.memo 包裹函数组件来实现类似 PureComponent 的效果。
总的来说,类组件我们可以使用 PureComponent,函数组件我们可以使用 React.memo 来提高性能。

useMemo

// 返回一个 memoized 值
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

把“创建”函数和依赖数组项作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。

import React, {useState, useEffect} from 'react';

const Child = (props) => {
    useEffect(() => {
        console.log("子组件渲染");
    });
    return (
        <div>
            <div>子组件</div>
            <button onClick={props.onClick}>子组件按钮</button>
        </div>
    )
}

const MemoChild = React.memo(Child);

const Father = () => {
    const [count, setCount] = useState(0);
    return (
        <div>
            <span>父组件</span>
            <span>计时器:{count}</span>
            <button onClick={() => setCount(count + 1)}>父组件按钮</button>
            <MemoChild onClick={() => setCount(count + 1)}/>
        </div>
    )
}

上面的代码,即使我们使用了 React.memo()包住子组件,当父组件的 count 数据变化时,子组件也会重新渲染。

问题出在:

<MemoChild onClick={() => setCount(count + 1)} />

每次父组件重现渲染时,传给子组件的 props 的地址就发生了变化(也就是说只要是引用类型,都会存在这个问题),因此子组件也会重新渲染。
在类组件中,组件的重新渲染不会影响函数的地址,因此不会影响子组件。
所以我们需要在函数组件中保存某个函数的地址

const cb = useCallback(() => {
    setCount(count => count + 1)
}, [])

<MemoChild onClick={cb} />

React-Router

import React from 'react';
import {
    BrowserRouter as Router,
    Link,
    Switch,
    Route
} from 'react-router-dom'; // web 端调用 react-router-dom

function App(){
    return (
        <Router>
            <Link to="/">首页</Link>
            <Link to="/blog">博客</Link>

            <Switch>
                <Route path="/about">
                    <About />
                </Route>
                <Route path="/">
                    <Home />
                </Route>              
            </Switch>
        </Router>
    )
}

动态路由匹配

import {
    ...
    useParams,
} from 'react-router-dom'

<Switch>
    <Route path='/user/:id'>
        <User />
    </Route>
</Switch>

function User(){
    let { id } = useParams();
    return (
        <div>
            user: { id }
        </div>
    );
}

React 和 Vue 的对比

共同点

  1. 都使用了 Virtual DOM
  2. 都提供了响应式和组件化的视图组件
  3. 将注意力集中保持在核心库,其他功能如路由和全局状态管理交给相关的库

不同

  1. 优化: React 应用中,某个组件的状态发生改变时,它会以组件为根,重新渲染整个组件子树。
    如果要避免不必要的子组件的渲染,需要使用 PureComponent 或者 shouldComponentUpdate 方法进行优化
  2. 在 Vue 应用中,组件的依赖是在渲染过程中自动跟踪的,所以系统能精确知道哪个组件需要被重新渲染

虚拟 DOM

虚拟 DOM 只是一个单纯的 JS 对象。

虚拟 DOM 的优劣

使用虚拟 DOM:当我们修改我们的数据重新生成新的虚拟 DOM,新老虚拟 DOM 进行 DIFF 操作之后,框架底层内部会对我们的真实 DOM 进行操作。
使用真实 DOM:直接手动操作 DOM

优点

  1. 保证性能的下限
    当我们使用虚拟 DOM 时,框架会帮我们完成 DOM 的操作,相当于是一个自动化的过程。
  2. 跨平台
    虚拟 DOM本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、weex 开发等等。

缺点

虚拟 DOM 的使用可以保证性能的下限,但也正是因为如此,它也无法做到极致的优化。毕竟我们操作虚拟 DOM 的最终目的是操作真实 DO,那论性能的上限自然是无法与直接操作真实 DOM 相比。

全部评论

相关推荐

1 1 评论
分享
牛客网
牛客企业服务