🌈React的优点

学习简单

学完ES6+或TS基本语法再过一遍React官方文档即可进入简单的开发

语法简洁

React对TS有着近乎完美的支持度和Eslint的编辑器友好.与Vue需要记一大堆API不同的是React几乎没有API也没有类似Vue3中的setup等啰嗦的东西,更加极客,适合治疗代码强迫症.

生态丰富

无论国内外大厂,React的采样率都是三大框架中最高的,所以其生态也是非常强大的,与Vue的生态基本不相上下,远超Angular,比如支持小程序开发的Taro,支持移动端开发的React Native等,基本开发中要用到的东西都有开源实现的方案

社区庞大

因为是三大框架中全球用户最多的,所以社区庞大,爬坑最容易,一般找生态,查问题,基本谷歌一下,各种stackoverflow,github issue,medium,dev.to等各种社区会报出最准确的解决方案.

🌧React缺点

React最大的缺点在于过于灵活,一大堆杂七杂八的状态管理,有时候会导致选择障碍,路由也是组件实现,需要配置式必须要自己封装,并且工程化团队开发困难,NG虽然工程化比较好,但生态的落差基本直接弃.React还有一个缺点就是移动端好用的UI库并不多,而上述问题Vue都解决的不错,比如拥有官方的vuex和配置式的vue router以及类似Vant这种现象级别的移动组件库.所以站长建议最好有时间把两者都学习一下,那么在开发的时候就可以灵活选择.

✍️目标学者

本教程适合以下朋友学习

👉 前端入门者: 已经学习过TS和ES6的基础知识,需要快速学习一个前端框架

👉 React初学者: 已经学会了各种React知识希望有一套能进行项目实战练习的教程

👉 Vue开发者: 觉得Vue3太啰嗦不够Geek或者对TS和编辑器支持度有强迫症想尝试一下新东西

👉 Angular开发者: 想开发一下小程序或者移动APP

👉 Jquery为主的传统MVC开发者: Jquery+PHP/ROR写腻了? 那么就来追一下潮流哈

👉 Gopher,Javaer等职业后端: 想成为全栈开发者或者至少不再与前端争论对错和扯皮,自己能看懂前端代码,那么本教程正适合你

👀教程内容

本教程是一套从React入门到项目实战的全解析教程.内容涉及React的几乎所有知识以及Spa后台和SSR网站两个实战案例的开发等,非常全面地讲解React+TS项目开发中的方方面面,使大家能全面深入的掌握React及其周边的生态.

Spa应用的脚手架选择更好更快的vitejs,SSR应用的脚手架选择next.js.为了教程更轻量易懂,我们直接略过传统的Class组件,而直接全部使用函数组件讲解,后续所有React系列的教程也全部使用函数组件.并且为了与本站其它教程相对应,本站所有的前端/Node教程全部使用Typescript讲解.

🌒知识点

  • React+TS+Vitejs的开发环境构建
  • 掌握React开发的绝大部分技术
  • 灵活的使用Hooks以及编写自定义的Hooks
  • React Router的深度配置化封装以及懒加载实现
  • 学会使用Reducer+Context封装轻量级的状态管理
  • 掌握Redux与Mobx两种模式来进行全局的状态管理
  • 全面掌握React Router的使用和配置化封装
  • 学会css in js等多种css模式的使用
  • 使用Antd Components快速构建布局和表单
  • 使用localforage构建本地化数据缓存
  • 使用umi request进行与后端的数据互交
  • 动态暗黑模式和实时皮肤切换实现
  • 登录手机认证,社会化认证和验证码实现
  • 动态权限路由与菜单的生成
  • 图片上传以及区块拖动等常用组件的使用
  • Antd Charts实现数据可视化
  • TailWindCss与Material ui的使用
  • SSR原理以及React手动构建SSR应用
  • 完全掌握Nextjs框架的使用
  • 初步学会使用Mobx进行状态管理
  • 学会使用swr.js来和后端进行数据互交
  • Monorepo和同构架构的实现

🔥后续可期待

后面我们还会围绕着《精通React》这个系列制作其它教程,大概有以下几个内容

  • React+Electron桌面应用开发
  • Taro+RN开发跨端应用
  • React原理与实现

希望大家喜欢

[]

状态与副作用

useState

用于改变组件内的状态,例

const StateDemo: FC = () => {
    const [count, setCount] = useState(1);
    const [isShow, toggleShow] = useState(true);

    return (
        <div>
            <p>{count}</p>
            <Button onClick={() => setCount(count + 1)}>增加</Button>
            <p className="pt-5">{isShow ? <span> I'm show now </span> : null}</p>
            <Button onClick={() => toggleShow(!isShow)}>{isShow ? '显示' : '隐藏'}</Button>
        </div>
    );
};

export default StateDemo;

useEffect

在状态不同的生命周期执行副作用

简单用法

每次状态更新都执行所有没有依赖的useEffect,以下代码'toggle ghost'这一条在resize浏览器时也会触发

const EffectDemo: FC = () => {
    const [ghost, setGhost] = useState<boolean>(false);
    const [width, setWidth] = useState(window.innerWidth);
    const toggleGhostBtn = () => setGhost(!ghost);
    const resizeHandle = () => setWidth(window.innerWidth);
    useEffect(() => {
        window.addEventListener('resize', resizeHandle);
    });
    useEffect(() => {
        console.log('toggle ghost');
    });
    return (
        <div>
            <p>{ghost ? 'ghost' : '普通'}按钮</p>
            <Button type="primary" onClick={toggleGhostBtn} ghost={ghost}>
                切换按钮样式
            </Button>
            <p className="pt-5">宽度为: {width}</p>
        </div>
    );
};

依赖更新

通过useEffect的第二个参数,可以指定其依赖的变量,只有此变量的状态更改时才会执行副作用函数,如果第二个参数为空,则只在第一次渲染和重新渲染时触发

const EffectDemo: FC = () => {
    ...
    useEffect(() => {
        // changeWidth
    }, [width]);
    useEffect(() => {
        console.log('toggle ghost');
    }, [ghost]);
   useEffect(() => {
        console.log('只在第一次或重新渲染组件时触发');
    }, []);
};

清理监听

在监听widthuseEffect中,每次改变width的状态,都会添加一个resize事件,这会极大的耗费浏览器占用的内存,通过一个返回值的方式,即可在下一次width状态改变后与添加新的resize监听前,取消上次添加的resize监听事件

const EffectDemo: FC = () => {
    ...
    useEffect(() => {
        window.addEventListener('resize', resizeHandle);
        return () => {
            window.removeEventListener('resize', resizeHandle);
        };
    }, [width]);
};

异步执行

useEffect中执行异步函数的语法如下,其实就是在原函数里调用一个async打头的立即函数

useEffect(() => {
    (async () => {})();
});

以下示例代码让按钮在变成ghost之后1s再变红色

const EffectDemo: FC = () => {
    const [red, setRed] = useState<boolean>(false);
    useEffect(() => {
        (async () => {
            await new Promise((resolve, reject) => setTimeout(() => resolve(true), 1000));
            setRed(ghost);
        })();
    }, [ghost]);
    return (
        <div>
            <Button type="primary" onClick={toggleGhostBtn} ghost={ghost} danger={red}>
                切换按钮样式
            </Button>
        </div>
    );
};

useLayoutEffect

useEffect几乎一样,但是具有防闪烁作用,比如下面的代码

const StateDemo: FC = () => {
    const [count, setCount] = useState(1);
    const [isShow, toggleShow] = useState(true);
    useLayoutEffect(() => {
        if (count === 0) setCount(Math.floor(Math.random() * 100));
    }, [count]);
    return (
        <div>
            <p>{count}</p>
            <Button onClick={() => setCount(0)}>随机数</Button>
...
        </div>
    );
};

export default StateDemo;

如果使用useEffect在点击按钮时,它会在渲染屏幕后异步调用,而useLayoutEffect则会把所有逻辑先计算完毕最后一次性渲染,所以它会阻塞渲染,所以除非必须要用,一般情况下基本用useEffect

useContext

用于向后代组件透传一个值,以创建一个语言选择器为例

定义一个语言列表变量

const langs: LangType[] = [
    { name: 'en', label: 'english' },
    { name: 'zh-CN', label: '简体中文' },
];

创建一个context

const localContext = createContext<LangStateProps>({
    lang: langs[0],
    setLang: (lang: LangType) => {},
});

创建provider包装器

const LocalProvider: FC<LangStateProps> = ({ lang, setLang, children }) => {
    useEffect(() => {
        setLang(lang);
    }, [lang]);
    return (
        <>
            <localContext.Provider value={{ lang, setLang }}>{children}</localContext.Provider>
        </>
    );
};

创建Local组件

const Local: FC = ({ children }) => {
    const [lang, setLang] = useState(langs[0]);
    return (
        <LocalProvider lang={lang} setLang={setLang}>
            {children}
        </LocalProvider>
    );
};

App.tsx中的所有节点包含于Local组件

const App: FC = () => {
    return (
        <Local>
        ...
        </Local>
    );
};
export default App;

语言选择组件

const Lang: FC = () => {
    const { lang, setLang } = useContext(localContext);
    const changeLang = (value: string) => {
        const current = langs.find((item) => item.name === value);
        current && setLang(current);
    };
    return (
        <>
            <Select defaultValue={lang.name} style={{ width: 120 }} onChange={changeLang}>
                {langs.map(({ name, label }) => (
                    <Option key={name} value={name}>
                        {label || name}
                    </Option>
                ))}
            </Select>
        </>
    );
};

显示当前语言

const CurrentLang: FC = () => {
    const { lang } = useContext(localContext);
    return <div>当前语言: {lang.label || lang.name}</div>;
};

App.tsx中使用以上两个组件

const App: FC = () => {
    return (
        <Local>
                ...
                <LangSelector />
                <CurrentLang />
        </Local>
    );
};
export default App;

useReducer

使用Context+useReducer可以实现轻量级的全局状态管理

以实现一个简单的应用配置功能为例(包含标题设置和暗黑模式切换)

编写类型

// 可选择的主题模式
export type ThemeMode = 'light' | 'dark';
// 初始化应用配置
export type ConfigType = {
    title?: string;
    theme?: ThemeMode;
};
// 合并默认配置后的最终应用配置状态
export type ConfigStateType = Required<ConfigType>;
// 可用的reducer操作
export enum ConfigureActionType {
    SET_TITLE = 'change_title',
    CHANGE_THEME = 'change_theme',
}
// 传入给dispatch触发器的数据
export type ConfigureAction =
    | { type: ConfigureActionType.SET_TITLE; value: string }
    | { type: ConfigureActionType.CHANGE_THEME; value: ThemeMode };
// 透传给子组件的context
export interface ConfigureContextType {
    state: ConfigStateType;
    dispatch: Dispatch<ConfigureAction>;
}

创建Context

// 透传配置状态与dispatch
export const ConfigureContext = createContext<ConfigureContextType | null>(null);

状态操作

为了确保数据的唯一性不被污染,使用immer.js操作数据

export const configReducer: Reducer<ConfigStateType, ConfigureAction> = produce((draft, action) => {
    switch (action.type) {
        // 设置标题
        case ConfigureActionType.SET_TITLE:
            draft.title = action.value;
            break;
        // 设置主题
        case ConfigureActionType.CHANGE_THEME:
            draft.theme = action.value;
            break;
        default:
            break;
    }
});

包装器组件

  • 合并默认配置和初始化配置
  • 使用useEffect创建在标题或主题状态改变时引发的副作用钩子
  • 把配置状态和dispatch传给ConfigureContext
const Configure: FC<{ config?: ConfigType }> = ({ config = {}, children }) => {
    const [state, dispatch] = useReducer(
        configReducer,
        config,
        (c) =>
            ({
                title: 'react app',
                theme: 'light',
                ...c,
            } as ConfigStateType),
    );
    useEffect(() => {
        const html = document.getElementsByTagName('html')[0];
        if (state.theme === 'dark') {
            html.classList.add('dark');
        } else {
            html.classList.remove('dark');
        }
    }, [state.theme]);
    useEffect(() => {
        document.title = state.title;
    }, [state.title]);
    return (
        <ConfigureContext.Provider value={{ state, dispatch }}>
            {children}
        </ConfigureContext.Provider>
    );
};

主题选择组件

const Theme = () => {
    const context = useContext(ConfigureContext);
    if (!context) return null;
    const { state, dispatch } = context;
    const toggleTheme = () =>
        dispatch({
            type: ConfigureActionType.CHANGE_THEME,
            value: state.theme === 'light' ? 'dark' : 'light',
        });
    return (
        <div>
            <span>切换主题</span>
            <Switch
                checkedChildren="🌛"
                unCheckedChildren="☀️"
                onChange={toggleTheme}
                checked={state.theme === 'dark'}
                defaultChecked={state.theme === 'dark'}
            />
        </div>
    );
};

标题设置组件

const Title: FC = () => {
    const context = useContext(ConfigureContext);
    if (!context) return null;
    const { state, dispatch } = context;
    const changeTitle = (e: React.ChangeEvent<HTMLInputElement>) =>
        dispatch({
            type: ConfigureActionType.SET_TITLE,
            value: e.target.value,
        });
    return (
        <div>
            <span>设置标题</span>
            <Input placeholder="标题" value={state.title} onChange={changeTitle} />
        </div>
    );
};

App.tsx中使用

<Configure>
  ...
  <Theme />
  <Title />
</Configure>

自定义Hooks

为了更加便捷的使用dispatch,可以通过自定义一个hooks的方式来封装一些方法

const useConfig = () => {
    const context = useContext(ConfigureContext);
    const { state = defaultConfig, dispatch } = context ?? {};
    const toggleTheme = () =>
        dispatch &&
        dispatch({
            type: ConfigureActionType.CHANGE_THEME,
            value: state.theme === 'light' ? 'dark' : 'light',
        });
    const changeTitle = (value: string) =>
        dispatch &&
        dispatch({
            type: ConfigureActionType.SET_TITLE,
            value,
        });
    return { config: state, toggleTheme, changeTitle };
};

有了自定的hooks之后就可以直接在组件中使用了

const Theme: FC = () => {
    const {
        config: { theme },
        toggleTheme,
    } = useConfig();
    return (
        <div>
            <span>切换主题</span>
            <Switch
                checkedChildren="🌛"
                unCheckedChildren="☀️"
                onChange={toggleTheme}
                checked={theme === 'dark'}
                defaultChecked={theme === 'dark'}
            />
        </div>
    );
};
const Title: FC = () => {
    const {
        config: { title },
        changeTitle,
    } = useConfig();
    return (
        <div>
            <span>设置标题</span>
            <Input placeholder="标题" value={title} onChange={(e) => changeTitle(e.target.value)} />
        </div>
    );
};

性能优化

useMemo

useMemo拥有个两个参数,一个回调函数和一个依赖项数组,回调函数必须返回一个值,只有在依赖项发生改变的时候,才会重新调用此函数,返回一个新的值.

回调函数的返回值可以是一个普通类型的值(例如字符串,布尔值,数组,对象等)也可以是一个函数,甚至是一个react组件,如果返回值是一个函数,则其作用就与useCallback一样

以下代码在每次config发生改变时,另一个组件的console.log也会调用

const Theme: FC = () => {
    const {
        config: { theme },
        toggleTheme,
    } = useConfig();
    console.log('render theme component');
    return (...组件代码);
};
const Title: FC = () => {
    const {
        config: { title },
        changeTitle,
    } = useConfig();
    console.log('render title component');
   return (...组件代码);
};

这样会在每次的input敲入一个字符时就会导致Theme组件重新渲染,极大的浪费了性能,可以通过useMemo做一下优化

const Theme: FC = () => {
    const {
        config: { theme },
        toggleTheme,
    } = useConfig();
    return useMemo(() => {
        console.log('render theme component');
        return (...组件代码);
    }, [theme]);
};
const Title: FC = () => {
    const {
        config: { title },
        changeTitle,
    } = useConfig();
    return useMemo(() => {
        console.log('render title component');
        return (...组件代码);
    }, [title]);
};

现在更改其中一个组件只会执行自己组件里的console.log

useCallback

现在把ThemeTitle两个组件放在一起作为ConfigPanel的子组件,并取消原来的useMemo包装,而改用memo包装

React.memo包装的组件,只有当props改变之后才会重新渲染,memo是浅对比

const Theme: FC<{ theme: ThemeMode; toggleTheme: () => void }> = memo(({ theme, toggleTheme }) => {
    console.log('render theme component');
    return (...组件代码);
});
const Title: FC<{ title: string; changeTitle: (value: string) => void }> = memo(
    ({ title, changeTitle }) => {
        console.log('render title component');
        return (...组件代码);
    },
);
const ConfigPanel: FC = () => {
    const { config, toggleTheme, changeTitle } = useConfig();
    return (
        <>
            <Theme theme={config.theme} toggleTheme={toggleTheme} />
            <Title title={config.title} changeTitle={changeTitle} />
        </>
    );
};

这时会发现Theme子组件中执行toggleTheme也会导致Title组件重新渲染,原因是changeTitle函数不是固定的,父组件重选渲染后会导致产生新的changeTitle变量,现在尝试使用useCallback包装,是其只在title改变时才产生新值,toggleTheme也一样

const useConfig = () => {
    const context = useContext(ConfigureContext);
    const { state = defaultConfig, dispatch } = context ?? {};
    const toggleTheme = useCallback(
        () =>
            dispatch &&
            dispatch({
                type: ConfigureActionType.CHANGE_THEME,
                value: state.theme === 'light' ? 'dark' : 'light',
            }),
        [state.theme],
    );

    const changeTitle = useCallback(
        (value: string) =>
            dispatch &&
            dispatch({
                type: ConfigureActionType.SET_TITLE,
                value,
            }),
        [state.title],
    );
    return { config: state, toggleTheme, changeTitle };
};

现在执行toggleTheme并不会导致Title组件重新渲染了,反之亦然

组件引用

useRef

创建ref对象,其.current属性被初始化为传入的参数,其current属性是可以通过赋值主动改变,而 ref 对象本身在组件的整个生命周期内保持不变

生命周期不变对象

以下代码通过使用useRef保存上一次的变量,无论count如何改变都不会执行console.log,因为ref对象本身是不变的.而由于useEffectuseLayoutEffect都是生命周期钩子,与外部是异步的,所以ref.current虽然会在钩子中被赋值为最新值,而其外部则保持上一次的值.

const StateDemo: FC = () => {
    ...
    const ref = useRef(count);
    useLayoutEffect(() => {
        ref.current = count;
        if (count === 0) setCount(Math.floor(Math.random() * 100));
    }, [count]);
    useEffect(() => {
        console.log('ref has changed');
    }, [ref]);
    return (
        <div>
            <p>{count}</p>
            <p>{ref.current}</p>
            <Button onClick={() => setCount(count + 1)}>增加</Button>
            ...
        </div>
    );
};

forwardRef结合

通过forwardRef可以把useRef的值与dom节点绑定,从而可以操控原生的dom节点

const CustomInput = forwardRef((props = {}, ref: Ref<any>) => (
    <input ref={ref} type="text" {...props} />
));
const RefDemo: FC = () => {
    const inputRef = useRef<HTMLInputElement | null>(null);

    useEffect(() => {
        if (inputRef.current) {
            inputRef.current.focus();
            inputRef.current.value = 'useRef';
        }
    });
    return (
        <>
            <CustomInput ref={inputRef} />
        </>
    );
};
export default RefDemo;

useImperativeHandle

因为视频教程的制作耗费站长大量的业余时间,有着巨大的工作量.每一集教程,需要编写代码,编写文档,录制视频,剪辑视频等多个流程,并且还提供了问答服务,如果全部免费的话本站将很难持续发展下去为大家提供更优质的内容.所以不得不收取一定费用,望大家谅解.

订阅本站后可以使用本站的一切服务,包括视频教程的学习,下载,问答以及本站发布的其它任何资源都可以随意使用.本站保证不再对订阅者收取二次费用.需要注意的是,后续推送的站长直播服务只有终身订阅者才可以享受.

本站目前提供各类编程开发相关的技术视频教程以及针对这些视频教程的问答服务和这些技术周边生态的导航,文档的翻译,开源项目的推荐,技巧性文章的发布等等.并且也为订阅者提供QQ群和discord问答服务.对于视频教程中的代码,站长专门搭建了一个代码托管平台方便大家下载.

本站在编程语言方面专注于Javascript/Typescript,Golang,PHP等几种站长擅长以及工作中常用的语言.技术栈涉及React,Vue等前端生态以及,Node.js,各种golang和php技术等后端技能,同时也会涉及一些包括Linux,Devops,Docker,K8S等在内与编程相关的技术,甚至还会讲解一些硬件方面的东西,总之用"杂七杂八"形容最为贴切.

本站绝大多数收费属于原创的虚拟商品,具有可复制性,可传播性,一旦授予,不接受任何形式的退款、换货要求。请您在购买获取或订阅之前确认好 ,避免引起不必要的纠纷