React 基础&深入学习过程

背景

在最近上半年深入的使用了集团的Weex2方案,并将在商品搜索领域落地,一路走来可能更多是看大佬的文章,业务压力大,并没有时间对这段经理进行一个归纳或者可以称为学习总结,罗列学习React和使用React Hooks的特性帮助我编写更加优雅的代码,该篇文章系本人小白在前端学习并使用半年的基础上做了一个简单回顾,如有不对的地方望海涵,欢迎大佬们 在评论区讨论互相学习进步,废话不多说,开始正文.....

JS特点

JS循环

当你在浏览器中打开一个网页时,Javascript代码将被加载并开始执行。这些代码将执行以下操作:

  1. 如果在页面加载完成后没有其他操作需要执行,则开始执行JavaScript代码。
  2. 当代码开始执行时,浏览器将进入一个等待事件的队列,该队列将包含要执行的任务。
  3. 如果在队列中有任务可用,则将该任务取出并执行。这是阻塞的,直到任务完成。
  4. 如果队列中没有可用任务,则JavaScript将继续等待,直到有新的任务可用。
  5. 当新的任务可用时,它将被取出并执行。
  6. 如果队列中没有新的任务,则JavaScript将继续等待。
  7. 这个过程将重复执行,直到队列为空或者浏览器关闭。

这就是JavaScript事件循环的基本工作原理。它允许我们编写事件驱动的代码,并且确保我们的代码在合适的时间执行。事件循环机制使我们能够使用异步编程模型,并在异步代码完成后继续执行下一条代码。

乱象的异步

JavaScript 是一门异步编程语言,它具有许多异步操作的特性。在 JavaScript 中,异步操作可以分为以下几类:

  1. 回调函数(Callback):将一个函数作为参数传递给另一个函数,当第二个函数完成时会自动调用这个函数。
  2. Promise:是一种异步编程的解决方案,它通过链式调用方法实现异步操作的流程。
  3. async/await:是 ES2017 引入的异步编程语法,它使得异步代码的编写更加容易,同时也更加清晰易懂。
  4. Generator:是一种函数,它可以暂停执行并在需要时恢复执行,这使得它非常适合处理异步操作。

总之,异步编程是 JavaScript 的一大特性,它使得程序能够更加高效地处理复杂的任务,同时也使得程序的开发更加灵活。

JS闭包

Javascript闭包是指函数和其所在的词法环境的组合。它是一种特殊的函数,它可以访问其词法作用域中的变量,即使在函数被调用后,这些变量仍然可以被访问。闭包是一种强大的特性,它允许开发者创建私有变量和方法,并在函数外部使用它们。

闭包可以通过在函数内部创建一个内部函数,并返回该函数来实现。内部函数可以访问其父函数中的变量,即使父函数已经返回。这种内部函数和其父函数之间的关系称为闭包。

下面是一个使用闭包的例子:

js 复制代码
function outerFunction() {

  var outerVariable = 'I am from the outer function';

  function innerFunction() {
    console.log(outerVariable);
  }
  return innerFunction;

}

var innerFunc = outerFunction();

innerFunc(); // Output: "I am from the outer function"

在上面的例子中,innerFunction是在outerFunction内部定义的。它可以访问outerVariable,即使outerFunction已经返回。当outerFunction被调用时,它返回innerFunction,并将其分配给innerFunc。然后,我们可以调用innerFunc,并在控制台上输出outerVariable的值。

总之,Javascript闭包是一种强大的特性,它允许开发者创建私有变量和方法,并在函数外部使用它们。

React核心理念

React是一个用于构建用户界面的JavaScript库。其核心设计理念是将界面组件化,以便于开发人员构建可重用、可维护的UI组件。React还具有以下几个关键特性:

  1. 组件化:React将用户界面划分为多个独立的组件,每个组件都具有自己的状态和属性。
  2. 虚拟DOM:React使用虚拟DOM来高效地更新用户界面。虚拟DOM是一个轻量级的JavaScript对象,它描述了用户界面的状态,并将其与React实际渲染的DOM进行比较。
  3. 单向数据流:React中的数据流是单向的,即数据从父组件流向子组件。这种设计使得数据流变得简单易懂,并且能够防止数据的混乱。
  4. 高阶组件:React允许开发人员编写高阶组件,以便于将组件应用于其他组件或在多个地方重复使用组件。
  5. 组件生命周期:React组件具有生命周期方法,这些方法在组件创建、更新和销毁时会被调用。这些方法允许开发人员在不同的阶段对组件进行操作。

React的核心设计理念是将界面组件化,以便于开发人员构建可重用、可维护的UI组件。React使用虚拟DOM来高效地更新用户界面,并且采用单向数据流的设计来保证数据的流畅性和可控性。同时,React还支持高阶组件和组件生命周期,以便于开发人员编写复杂的UI逻辑。

React提倡组合优于继承,主要是为了提高代码的可重用性、代码结构的清晰度、避免全局状态以及更容易实现MVC等方面的优势。

  1. 可重用性更高:组合可以更好地利用代码的复用性,将相似的功能组合在一起,减少重复代码的出现。这有助于提高代码的可维护性和可扩展性。
  2. 代码结构更清晰:组合模式可以将不同的组件进行分离,使得代码的结构更加清晰明了。这有助于开发者更好地理解和修改代码。
  3. 避免全局状态:组合模式可以避免使用全局状态,使得应用程序更加灵活和健壮。
  4. 更容易实现MVC:组合模式可以更好地实现MVC模式,将应用程序的不同部分进行分离,使得开发者更容易实现复杂的应用程序。

React 单向数据流是一种在 React 应用程序中组织数据流的方式,其中状态只能从父组件向子组件传递,而不能反向传递。这意味着子组件不能直接访问父组件的状态,必须通过属性来传递数据。

在 React 单向数据流中,父组件定义了组件的状态和属性,然后通过子组件来呈现这些状态和属性。当状态或属性发生更改时,父组件会重新渲染子组件,以便子组件能够显示正确的数据。

下面是一个简单的 React 单向数据流的例子:

js 复制代码
// Parent Component
import React, { Component } from 'react';
import ChildComponent from './ChildComponent';

class ParentComponent extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
  }

  incrementCount = () => {
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    return (
      <div>
        <h1>Parent Component</h1>

        <ChildComponent count={this.state.count} />

        <button onClick={this.incrementCount}>Increment Count</button>
      </div>
    );
  }
}

export default ParentComponent;

// Child Component
import React, { Component } from 'react';

class ChildComponent extends Component {
  render() {
    return (
      <div>
        <h2>Child Component</h2>

        <p>Count: {this.props.count}</p>

      </div>
    );
  }
}

export default ChildComponent;

在这个例子中,ParentComponent 定义了一个 count 状态,并通过一个按钮调用了 incrementCount 方法来更新状态。ChildComponent 通过 props 接收到了 count 属性,并将其呈现在组件中。

当按钮被点击时,incrementCount 方法会被调用,并更新 ParentComponent 的状态。由于状态只能从父组件向子组件传递,因此 ChildComponent 无法直接访问父组件的状态,但它可以通过 props 来接收和呈现父组件的数据。

总之,React 单向数据流是一种强大的组织 React 应用程序数据流的方式,可以确保组件之间的状态传递和更新都是单向的,从而简化了代码并提高了可维护性。

PS:遇到的很多性能问题也大多因为单项数据流,顶层状态变更导致大量组件重新render,从而带来了明显的渲染延迟。优化手段就是分割状态避免不需要依赖这个状态的组件再次render

常用技术总结

推荐

All in 箭头函数,定义在使用之前 All in hooks,复用业务逻辑 Weex离不开useRef,原始事件监听处理(现阶段,之后react体系完善可能就不需要了)

状态管理

状态管理是需要好好设计的,如果直接在页面里写大量useState就可能会因为某个组件为了改变状态导致整个页面重绘。几个基本原则:

  • 最贴近消费的地方管理状态
  • 非必要不提升作用域

推荐工具github.com/pmndrs/zust...优点是:

js 复制代码
使用方法(自定义hooks)
// 项目下创建一个store文件
import { create } from 'zustand';

export interface State {
  restAttr?: any;
}

export type setAllGoodsPaneSelectionsCallback = (
  selections: Set<number>
) => Set<number>;

export interface Action {
  fetchRestAttr: (noCache?: boolean) => Promise<any>;
}

export type Store = State & Action;

export const useGlobalStore = create<Store>((set, get) => {
  return {
    // 状态+灰度数据
    restAttr: undefined,
    fetchRestAttr: async noCache => {
      const _attrRes = get().restAttr;
      if (_attrRes && !noCache) {
        return _attrRes;
      }
      return new Promise(resolve => {
        const res = { test: 'test' };
        set({ restAttr: res });
        resolve(res);
      });
    },
  };
});

使用的地方只需要

js 复制代码
const restAttr = useGlobalStore(state => state.restAttr);
const fetchRestAttr = useGlobalStore(state => state.fetchRestAttr);

useEffect(() => {
  // 模拟请求
  setTimeout(() => {
    fetchRestAttr();
  }, 1000);
}, []);

Hook使用

Hooks的目标是为了更好的管理和复用业务代码,解决组件的视图复用之外,逻辑代码的复用。

另一方面也是为了让函数式组件拥有生命周期,hooks出现之前状态管理一般使用专门的状态管理工具(如Redux),hooks之后基础的状态管理就不再依赖工具,不过hooks提供的只是基础能力,对于复杂应用也不能完全依赖react hooks管理状态,会带来大量re-render导致性能问题。

1. useState 管理一个状态 ****https://react.dev/reference/react/useState

js 复制代码
import { useState } from 'react';

export default function App() {
  const [version, setVersion] = useState(0);

  const handleReset = () => {
    setVersion(version + 1);
    // 函数式使用,注意这里是同步函数,不支持异步
    setVersion(prev => {
      const next = prev + 1;
      return next;
    });
  }

  return (
    <>
      <button onClick={handleReset}>Reset</button>
      <Form key={version} />
    </>
  );
}

执行是异步还是同步,延伸阅读juejin.cn/post/695988...

2. useEffect 生命周期 or 消除副作用react.dev/reference/r...

适用场景:当组件加载执行、组件卸载时清理、依赖变化后执行

js 复制代码
import { useEffect } from 'react';
import { createConnection } from './chat.js';

const ChatRoom = ({ roomId }) => {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');
  // 不传依赖项时会每次render都执行,比较危险,不推荐
  useEffect(() => {
    // 任何状态或props变更引起的render都会执行
  });
  
  // 依赖为空时
  useEffect(() => {
    // 组件加载时执行
    return () => {
      // 组件卸载时执行
    };
  }, []);

  // 有具体依赖项时
  useEffect(() => {
    // 每次依赖项任一变更都执行,也就会出现开始serverUrl为空,roomId有值而执行
    // 这里需要判断依赖项是否符合预期
    if(!serverUrl || !roomId) return;

    // 正常业务逻辑
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    
    return () => {
      // 依赖变更时清理之前的链接,清理脏数据,如果不清理之前的链接也会正常接受并处理事件
      connection.disconnect();
    };
  }, [serverUrl, roomId]);
  
  return null;
}

3. useRef 引用值类型,区别于useState react.dev/reference/r...

适用场景:获取原始DOM并操作(如绑定事件、获取属性等)、闭包内获取最新值、避免状态变化引起re-render

例如:当一个函数内部获取state状态时经常拿不到最新的值,大多是因为形成了闭包,可以考虑useRef临时存储变量处理

js 复制代码
import { useRef } from 'react';

export default function Counter() {
  let ref = useRef(0);

  function handleClick() {
    // 这样的值不会触发页面重绘
    ref.current = ref.current + 1;
    alert('You clicked ' + ref.current + ' times!');
  }

  return (
    <button onClick={handleClick}>
      Click me!
    </button>
  );
}

DOM操作

js 复制代码
export const Scroll: FC<ScrollProps> = (props) => {
  // 获取elementRef
  const elementRef = useRef<HTMLDivElement>(null);

  const [flag, setFlag] = useState({});
  const nextFlagRef = useRef({});
  // 引用赋值flag变量
  nextFlagRef.current = flag;

  const [scrollParent, setScrollParent] = useState<Window | Element | null | undefined>();

  useEffect(() => {
    const element = elementRef.current;
    // 需要判空
    if (!element) return;
    if (!scrollParent) return;
    
    const handleScroll = () => {
      // 滚动事件监听
      // 如果依赖了外部变量,通过ref获取最新值
      console.log(nextFlagRef.current)
      // 直接读取flag可能是上一次的值
      console.log(flag)
    }
    
    scrollParent.addEventListener('scroll', handleScroll);
    
    return () => {
      // 数据变化时清理监听事件,handleScroll在闭包内和addEventListener是同一个值才能清理
      scrollParent.removeEventListener('scroll', handleScroll);
      // 这样就无法清理,箭头函数每次执行都是新的函数
      // scrollParent.removeEventListener('scroll', () => hanldeScroll);
    };
  }, [scrollParent]);

  return <list ref={elementRef}>
      {props.children}
  </list>
};

复杂应用:对外暴露组件内部操作函数react.dev/reference/r...

4. useMemo、useCallback性能优化 react.dev/reference/r...react.dev/reference/r...

非必要不需要使用,遇到性能瓶颈(渲染卡顿、耗时过长时)再考虑,要注意依赖项需要完整(少一个都是bug),如果函数没有依赖项时可以考虑用ahooksuseMemoizedFn(无脑用它替代useCallback也没问题)

适用场景: 1. 缓存数据、函数或组件(计算较大、比较耗时),避免非要渲染 2. 主动触发渲染,让函数内部获取最新state

js 复制代码
const src = useMemo(() => {
    if (url) {
      return url;
    }
    if (file) {
      return URL.createObjectURL(file);
    }
    return '';
}, [url, file]);

const onClick = useCallback(
    (e: Action) => {
      const { onAction } = props;
      if (onAction) {
        onAction(e);
      }
      innerRef.current?.hide();
    },
    [props.onAction],
);

// 内部使用useRef和useMemo实现了函数地址永远不会变化,从而避免他作为deps依赖项时diff认为变更,从而触发re-render
const onClick = useMemoizedFn(
    (e: Action) => {
      const { onAction } = props;
      if (onAction) {
        onAction(e);
      }
      innerRef.current?.hide();
    },
    [props.onAction],
);

Hooks延伸阅读

segmentfault.com/a/119000003... react.caoweiju.com/src/hooks/r... overreacted.io/zh-hans/a-c...

相关推荐
王哲晓2 分钟前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_4116 分钟前
无网络安装ionic和运行
前端·npm
理想不理想v7 分钟前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云17 分钟前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js
微信:1379712058719 分钟前
web端手机录音
前端
齐 飞25 分钟前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
神仙别闹42 分钟前
基于tensorflow和flask的本地图片库web图片搜索引擎
前端·flask·tensorflow
GIS程序媛—椰子2 小时前
【Vue 全家桶】7、Vue UI组件库(更新中)
前端·vue.js
DogEgg_0012 小时前
前端八股文(一)HTML 持续更新中。。。
前端·html
ZL不懂前端2 小时前
Content Security Policy (CSP)
前端·javascript·面试