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 相比。