React组件设计指南:从类组件到函数组件,从受控组件到非受控组件

251 阅读7分钟

类组件和函数组件的区别是什么?

一、 代码实现

  1. 类组件
class ClassComponent extends React.Component {
  state = { count: 0 };
  increment = () => this.setState({ count: this.state.count + 1 });
  render() {
   return <button onClick={this.increment}>{this.state.count}</button>;
  }
}
  1. 函数组件
function FunctionComponent() {
  const [count, setCount] = React.useState(0);
  const increment = () => setCount(count + 1);
  return <button onClick={increment}>{count}</button>;
}

二、 调用机制

  1. 类组件
  • React 通过 new ClassComponent() 创建实例
  • 实例保存在 Fiber 节点的 stateNode 属性中
  • 每次更新时调用实例的 render() 方法
  • 生命周期方法通过实例直接调用
<ClassComponent />
  1. 函数组件
  • React 直接调用函数 FunctionComponent(props)
  • 无实例创建,状态存储在 Fiber 节点的 memoizedState 中
  • Hooks 通过链表结构按顺序存储
  • 每次渲染都是全新函数调用(但状态被保留)
<FunctionComponent />

三、 生命周期

  1. 类组件
  • 有生命周期函数
class LifecycleDemo extends React.Component {
  constructor(props) {
    super(props);
    console.log("Constructor called");
  }
  
  componentDidMount() {
    console.log("Component did mount");
  }
  
  componentDidUpdate() {
    console.log("Component did update");
  }
  
  componentWillUnmount() {
    console.log("Component will unmount");
  }
  
  render() {
    console.log("Render called");
    return <div>Lifecycle Demo</div>;
  }
}
  1. 函数组件
  • 无生命周期函数
function LifecycleDemo() {
  console.log("Function body executed");
  
  React.useEffect(() => {
    console.log("Component did mount (useEffect empty dep)");
    
    return () => {
      console.log("Component will unmount");
    };
  }, []);
  
  React.useEffect(() => {
    console.log("Component did update (useEffect no dep)");
  });
  
  return <div>Lifecycle Demo</div>;
}

生命周期对应关系

类组件生命周期函数组件等价实现触发时机
constructoruseState初始化组件创建时
componentDidMountuseEffect(..., [])DOM挂载后
componentDidUpdateuseEffect(..., [deps])依赖项变化后
componentWillUnmountuseEffect清理函数组件卸载前
shouldComponentUpdateReact.memo + useMemo渲染前决定是否更新

四、 状态管理

  1. 类组件
  • 使用setState方法
class ClassUpdate extends React.Component {
  state = { count: 0 };

   handleClick = () => {
    this.setState({ count: this.state.count + 1 });
    this.setState({ count: this.state.count + 1 });
    // 结果:只增加1(合并更新)
  };
  
	 incrementTwice = () => {
    this.setState(prev => ({ count: prev.count + 1 }));
    this.setState(prev => ({ count: prev.count + 1 }));
    // 结果:增加2
  };
}
  1. 函数组件
  • 使用状态更新函数
function FunctionUpdate() {
  const [count, setCount] = React.useState(0);

  const handleClick = () => {
    setCount(count + 1);
    setCount(count + 1);
    // React 17及以前:增加2次(无批量处理)
    // React 18及之后:增加1次(自动批量处理)
  };

	const incrementTwice = () => {
    setCount(prev => prev + 1);
    setCount(prev => prev + 1);
    // 结果:增加2
  };
}

五、 Props获取

  1. 类组件
  • 通过this.props访问
  • 父组件重新渲染时,React 会更新子组件实例的 this.props
  • 整个组件生命周期中,同一个实例的 this.props 会被更新
class ClassProps extends React.Component {
  render() {
    return <div>{this.props.message}</div>;
  }
}
  1. 函数组件
  • 通过函数参数直接访问或解构访问
  • 函数内部捕获的是当次渲染时的 props 值
  • props 变化会导致函数组件重新执行
function FunctionProps(props) {
  return <div>{props.message}</div>;
}

// 或解构(通常使用这个方法)
function FunctionPropsDestructured({ message }) {
  return <div>{message}</div>;
}

函数组件的闭包陷阱

function Counter({ initialCount }) {
  const [count, setCount] = useState(initialCount);
  
  const increment = () => {
    // 该闭包捕获了初始的 initialCount,即第一次渲染时传过来的start(0) 
    setCount(count + 1);
  };
  
  return (
    <div>
      <button onClick={increment}>Increment</button>
      <span>Count: {count}</span>
    </div>
  );
}
// 父组件
function Parent() {
  const [start, setStart] = useState(0);
  
  return (
    <div>
      <button onClick={() => setStart(10)}>设置为10</button>
      <Counter initialCount={start} />
    </div>
  );
}
  • 当点击 "设置为10" 后,Counter 组件的 initialCount 变为 10
  • 但 increment 函数仍然使用闭包中捕获的旧值(0)
  • 导致点击 Increment 按钮从 0 开始计数
  • 解决方法
  1. 函数式更新
const increment = () => {
  setCount(c => c + 1); // 基于最新状态更新
};
  1. 使用ref保存最新值
const latestInitial = useRef(initialCount);
useEffect(() => {
  latestInitial.current = initialCount;
}, [initialCount]);

const increment = () => {
  setCount(latestInitial.current + 1);
};

受控组件和非受控组件的区别是什么?

一、实现方式

  1. 受控组件(React完全控制数据)
  • 创建合成事件对象(SyntheticEvent)
  • 调用事件处理函数
  • 调度状态更新
  • 重新渲染组件树
  • 协调器(Reconciler)比较虚拟DOM
  • 渲染器(Renderer)更新实际DOM
function ControlledInput() {
  const [value, setValue] = useState('');
  
  // 用户输入触发事件
  const handleChange = (e) => {
    // 更新React状态
    setValue(e.target.value);
  };
  
  // React重新渲染组件
  return (
    <input
      // 新值传递到DOM
      value={value}
      onChange={handleChange}
    />
  );
}

2. 非受控组件(DOM拥有数据)

  • 初始渲染时设置 defaultValue
  • 创建DOM节点引用
  • 用户输入直接修改DOM
  • React完全不知道变化发生
  • 需要时通过ref访问DOM节点值
function UncontrolledInput() {
  const inputRef = useRef();
  
  const handleSubmit = () => {
    // 直接访问DOM节点获取值
    console.log(inputRef.current.value);
  };
  
  return (
    <input
      // React不管理值
      defaultValue="initial"
      ref={inputRef}
    />
  );
}

二、优化策略

  1. 受控组件
  • 防抖处理
    • 问题背景:搜索框等高频输入场景导致过多API请求
    • 优化原理:防抖减少API调用频率,使用ref保持函数稳定性
function DebouncedSearch() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  
  // 使用useRef保持防抖函数稳定性
  const debouncedSearch = useRef(
    _.debounce(searchTerm => {
      fetchResults(searchTerm).then(setResults);
    }, 300)
  ).current;

  useEffect(() => {
    debouncedSearch(query);
    
    // 清理函数
    return () => debouncedSearch.cancel();
  }, [query, debouncedSearch]);

  return (
    <div>
      <input 
        value={query} 
        onChange={e => setQuery(e.target.value)}
        placeholder="Search..."
      />
      <SearchResults results={results} />
    </div>
  );
}
  • 批量更新
    • 问题背景:在大型表单中,每次输入都会触发完整的状态更新和重新渲染,导致性能瓶颈
    • 优化原理:使用函数式更新确保基于最新状态,避免直接修改状态对象,减少渲染次数
function BatchUpdateForm() {
  const [form, setForm] = useState({
    firstName: '',
    lastName: '',
    email: ''
  });

  // 优化前:每次输入都触发完整状态更新
  const handleChange = (e) => {
    const { name, value } = e.target;
    
    // 优化后:使用函数式更新避免依赖当前状态
    setForm(prev => ({
      ...prev,
      [name]: value
    }));
  };

  return (
    <form>
      <input name="firstName" value={form.firstName} onChange={handleChange} />
      <input name="lastName" value={form.lastName} onChange={handleChange} />
      <input name="email" value={form.email} onChange={handleChange} />
    </form>
  );
}
  • Web Workers处理复杂计算
    • 问题背景:受控组件的状态更新会引发重新渲染,若同步执行复杂计算(如大数据排序/过滤),会导致界面卡顿;用户输入(如搜索框)需实时反馈,但计算可能耗时。Web Workers确保输入事件不被阻塞,维持流畅交互
    • 优化原理:将计算逻辑移至Web Worker,后台线程完成计算后通过postMessage返回结果,主线程仅负责UI更新
function ComplexCalculationForm() {
  const [input, setInput] = useState('');
  const [result, setResult] = useState(null);
  const workerRef = useRef();

  useEffect(() => {
    // 初始化 Web Worker
    workerRef.current = new Worker('./calculation.worker.js');
    
    workerRef.current.onmessage = (e) => {
      setResult(e.data);
    };
    
    return () => {
      workerRef.current.terminate();
    };
  }, []);

  const handleChange = (e) => {
    const value = e.target.value;
    setInput(value);
    
    // 将计算任务交给 Worker
    workerRef.current.postMessage(value);
  };

  return (
    <div>
      <input value={input} onChange={handleChange} />
      {result && <div>Result: {result}</div>}
    </div>
  );
}
  1. 非受控组件
  • 部分更新策略
    • 问题背景:超大型表单中提交所有数据性能低下
    • 优化原理:仅收集修改过的字段数据
function LargeFormOptimization() {
  const formRef = useRef();
  const modifiedFields = useRef(new Set());
  
  const handleChange = (e) => {
    // 标记修改过的字段
    modifiedFields.current.add(e.target.name);
  };
  
  const handleSubmit = () => {
    const formData = new FormData(formRef.current);
    const data = {};
    
    // 只处理修改过的字段
    modifiedFields.current.forEach(name => {
      data[name] = formData.get(name);
    });
    
    console.log('Modified data:', data);
  };

  return (
    <form ref={formRef}>
      {Array.from({ length: 1000 }).map((_, i) => (
        <input
          key={i}
          name={`field-${i}`}
          defaultValue=""
          onChange={handleChange}
        />
      ))}
      <button type="button" onClick={handleSubmit}>Submit</button>
    </form>
  );
}
  • Ref回调优化
    • 问题背景:动态生成的表单字段难以管理ref
    • 优化原理:使用回调ref动态管理字段引用
function DynamicForm() {
  const inputsRef = useRef({});
  
  // 动态注册输入字段
  const registerInput = useCallback((name) => (element) => {
    if (element) {
      inputsRef.current[name] = element;
    } else {
      delete inputsRef.current[name];
    }
  }, []);

  const handleSubmit = () => {
    const data = {};
    Object.entries(inputsRef.current).forEach(([name, input]) => {
      data[name] = input.value;
    });
    console.log('表单数据:', data);
  };

  // 动态生成表单字段
  const fields = ['name', 'email', 'phone', 'address'];
  
  return (
    <div>
      {fields.map(field => (
        <div key={field}>
          <label>
            {field.charAt(0).toUpperCase() + field.slice(1)}:
            <input 
              ref={registerInput(field)} 
              defaultValue="" 
            />
          </label>
        </div>
      ))}
      
      <button onClick={handleSubmit}>提交</button>
    </div>
  );
}

三、受控组件适用场景

  1. 需要实时验证的表单
  • 背景:当用户输入需要即时验证(如密码强度、邮箱格式)时,受控组件能实时访问状态并更新UI反馈
function EmailForm() {
  const [email, setEmail] = useState('');
  const [isValid, setIsValid] = useState(true);

  const handleChange = (e) => {
    const value = e.target.value;
    setEmail(value);
    // 实时验证邮箱格式
    setIsValid(/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value));
  };

  return (
    <div>
      <input
        type="email"
        value={email}
        onChange={handleChange}
        style={{ borderColor: isValid ? 'green' : 'red' }}
      />
      {!isValid && <p style={{ color: 'red' }}>邮箱格式不正确</p>}
    </div>
  );
}
  1. 表单字段间存在依赖关系
  • 背景:当表单项需要基于其他字段动态变化时,受控组件能轻松管理状态间依赖
function ShippingForm() {
  const [country, setCountry] = useState('US');
  const [cities, setCities] = useState(['New York', 'Los Angeles']);
  
  const handleCountryChange = (e) => {
    const selectedCountry = e.target.value;
    setCountry(selectedCountry);
    
    // 需要根据国家更新城市选项
    if (selectedCountry === 'CN') {
      setCities(['Beijing', 'Shanghai', 'Guangzhou']);
    } else if (selectedCountry === 'JP') {
      setCities(['Tokyo', 'Osaka']);
    } else {
      setCities(['New York', 'Los Angeles']);
    }
  };

  return (
    <div>
      <select value={country} onChange={handleCountryChange}>
        <option value="US">美国</option>
        <option value="CN">中国</option>
        <option value="JP">日本</option>
      </select>
      
      <select>
        {cities.map(city => (
          <option key={city} value={city}>{city}</option>
        ))}
      </select>
    </div>
  );
}
  1. 需要动态增减表单项
  • 背景:当用户可添加/删除多个同类表单项时,受控组件能有效管理动态表单状态
function MultiEmailInput() {
  const [emails, setEmails] = useState(['']);

  const addEmail = () => {
    setEmails([...emails, '']);
  };

  const removeEmail = (index) => {
    const newEmails = [...emails];
    newEmails.splice(index, 1);
    setEmails(newEmails);
  };

  const handleEmailChange = (index, value) => {
    const newEmails = [...emails];
    newEmails[index] = value;
    setEmails(newEmails);
  };

  return (
    <div>
      {emails.map((email, index) => (
        <div key={index}>
          <input
            type="email"
            value={email}
            onChange={(e) => handleEmailChange(index, e.target.value)}
          />
          {emails.length > 1 && (
            <button type="button" onClick={() => removeEmail(index)}>
              删除
            </button>
          )}
        </div>
      ))}
      <button type="button" onClick={addEmail}>
        添加邮箱
      </button>
    </div>
  );
}

四、非受控组件的适用场景

  1. 文件上传输入
  • 背景:文件输入必须使用非受控方式,因为文件数据无法通过JavaScript直接设置
function FileUpload() {
  const fileInputRef = useRef();

  const handleSubmit = async () => {
    const file = fileInputRef.current.files[0];
    if (!file) return;
    
    const formData = new FormData();
    formData.append('file', file);
    
    try {
      const response = await fetch('/upload', {
        method: 'POST',
        body: formData
      });
      console.log('上传成功', await response.json());
    } catch (error) {
      console.error('上传失败', error);
    }
  };

  return (
    <div>
      <input type="file" ref={fileInputRef} />
      <button onClick={handleSubmit}>上传文件</button>
    </div>
  );
}
  1. 大型表单性能优化
  • 背景:表单包含大量输入字段时,非受控组件可避免每次输入都触发渲染
function SurveyForm() {
  const formRef = useRef();
  const [isSubmitting, setIsSubmitting] = useState(false);

  const handleSubmit = async (e) => {
    e.preventDefault();
    setIsSubmitting(true);
    
    try {
      // 使用FormData一次性获取所有数据
      const formData = new FormData(formRef.current);
      const data = Object.fromEntries(formData.entries());
      
      // 处理多选值
      data.interests = formData.getAll('interests');
      
      await submitSurvey(data);
      alert('提交成功!');
    } catch (error) {
      console.error('提交失败', error);
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <form ref={formRef} onSubmit={handleSubmit}>
      {/* 50+ 输入字段... */}
      <div>
        <label>兴趣:</label>
        <input type="checkbox" name="interests" value="sports" /> 体育
        <input type="checkbox" name="interests" value="music" /> 音乐
        <input type="checkbox" name="interests" value="tech" /> 科技
      </div>
      
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? '提交中...' : '提交问卷'}
      </button>
    </form>
  );
}
  1. 集成第三方DOM库
  • 背景:当需要集成非React库(如地图、图表、富文本编辑器)时,非受控组件提供DOM访问能力
function RichTextEditor() {
  const editorRef = useRef();
  
  useEffect(() => {
    // 初始化第三方富文本编辑器
    const editor = new ThirdPartyEditor(editorRef.current, {
      toolbar: ['bold', 'italic', 'link'],
      onChange: (content) => {
        console.log('内容更新:', content);
      }
    });
    
    return () => {
      // 清理资源
      editor.destroy();
    };
  }, []);

  return <div ref={editorRef} style={{ height: '300px', border: '1px solid #ccc' }}></div>;
}
  1. 简单表单且只需要最终值
  • 背景:当不需要中间状态验证,只需提交时获取最终值
function SimpleSearch() {
  const inputRef = useRef();

  const handleSearch = (e) => {
    e.preventDefault();
    const query = inputRef.current.value.trim();
    if (query) {
      performSearch(query);
    }
  };

  return (
    <form onSubmit={handleSearch}>
      <input 
        type="text" 
        ref={inputRef} 
        defaultValue="" 
        placeholder="搜索..." 
      />
      <button type="submit">搜索</button>
    </form>
  );
}