React尚硅谷020-036(props、ref、受控组件),html实体字符
React尚硅谷020-036(props、ref、受控组件),html实体字符
一、背景与问题
在React开发中,组件间的通信和数据控制是核心问题。props作为最基础的通信方式,其单向数据流特性虽然保证了可维护性,但也带来了数据同步的挑战。ref的出现解决了直接访问DOM的痛点,但其滥用可能导致组件解耦性下降。受控组件通过state统一管理输入状态,但实际开发中常与非受控组件混合使用,产生数据不一致的风险。HTML实体字符的处理则关系到安全性和数据完整性,需要在输入输出两端进行双重校验。
二、基本原理
1. props的单向数据流机制
React组件通过props接收父级传递的数据,通过immutable方式传递。当父级state变化时,React会重新渲染子组件,触发props更新。这种机制保证了数据的可追溯性,但需要开发者主动管理数据流向。
// 父组件
function Parent({ children }) {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>Add</button>
{children({ count })}
</div>
);
}
// 子组件
function Child({ count }) {
return <p>当前计数:{count}</p>;
}2. ref的底层实现机制
ref本质是React提供的特殊属性,通过createRef创建的ref对象会绑定到组件实例上。在函数组件中需要通过forwardRef显式传递ref,其底层通过React的ref forwarding机制实现。
// 函数组件使用ref
const MyComponent = React.forwardRef((props, ref) => {
const inputRef = useRef();
useEffect(() => {
if (ref && inputRef.current) {
ref.current = inputRef.current;
}
}, [ref]);
return <input ref={inputRef} />;
});3. 受控组件与非受控组件
受控组件通过state统一管理输入值,所有输入事件都会更新state;非受控组件直接操作DOM,通过ref获取值。这种设计权衡了数据一致性和性能。
// 受控组件示例
function ControlledInput({ onChange }) {
const [value, setValue] = useState('');
const handleChange = (e) => {
setValue(e.target.value);
onChange(e.target.value);
};
return <input value={value} onChange={handleChange} />;
}
// 非受控组件示例
function UncontrolledInput({ onChange }) {
const inputRef = useRef();
useEffect(() => {
onChange(inputRef.current.value);
}, [inputRef]);
return <input ref={inputRef} />;
}三、环境准备
# 创建React项目
npx create-react-app react-props-ref
cd react-props-ref
npm install react-router-dom项目结构建议:
src/
├── components/
│ ├── InputField.jsx
│ ├── FormValidator.jsx
│ └── TextEditor.jsx
├── utils/
│ └── html-escape.js
├── App.js
└── index.js四、核心实现
1. props的深度应用
// 定义带默认值的props
function Greeting({ name = 'Guest', greeting = 'Hello' }) {
return <h1>{greeting}, {name}</h1>;
}
// 使用prop-types校验
import PropTypes from 'prop-types';
Greeting.propTypes = {
name: PropTypes.string,
greeting: PropTypes.string.isRequired
};
// 使用defaultProps
Greeting.defaultProps = {
greeting: 'Welcome'
};2. ref的高级用法
// 使用ref获取DOM元素
function RefDemo() {
const inputRef = useRef(null);
const focusInput = () => {
if (inputRef.current) {
inputRef.current.focus();
}
};
return (
<div>
<input ref={inputRef} />
<button onClick={focusInput}>聚焦输入框</button>
</div>
);
}3. 受控组件的实现
// 多输入表单的受控组件
function LoginForm({ onSubmit }) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
onSubmit({ email, password });
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="邮箱"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="密码"
/>
<button type="submit">登录</button>
</form>
);
}五、完整案例:富文本编辑器
// TextEditor.jsx
import React, { useState, useRef } from 'react';
import { htmlEscape } from './utils/html-escape';
function TextEditor({ onChange }) {
const [content, setContent] = useState('');
const editorRef = useRef(null);
const handleInput = (e) => {
const rawContent = e.target.value;
const escapedContent = htmlEscape(rawContent);
setContent(escapedContent);
onChange(escapedContent);
};
return (
<div>
<textarea
value={content}
onChange={handleInput}
ref={editorRef}
placeholder="输入文本..."
/>
<div dangerouslySetInnerHTML={{ __html: content }} />
</div>
);
}// utils/html-escape.js
function htmlEscape(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}六、源码解析
1. ref的实现机制
React的ref实现基于fiber树结构,通过ref属性在fiber节点上创建记录。在函数组件中,通过forwardRef将ref传递到子组件,最终在渲染阶段通过React的ref forwarding机制将ref绑定到DOM节点。
// React源码片段(简化版)
function forwardRef<Props, Ref>(Component) {
return function ForwardRef(props, ref) {
const [instance, setInstance] = useState(null);
useEffect(() => {
setInstance(Component);
}, []);
return <Component {...props} ref={ref} />;
};
}2. 受控组件的渲染机制
React在渲染受控组件时,会将value和onChange事件绑定到state,通过合成事件系统统一处理输入变化。当state变化时,会触发重新渲染,形成闭环。
// React事件系统片段(简化版)
function createEventHandle(component) {
return function handleEvent(e) {
const target = e.target;
const value = target.value;
component.setState({ value });
};
}七、进阶使用
1. ref的替代方案
对于需要直接操作DOM的场景,可以使用state+ref组合替代:
function CustomInput({ onChange }) {
const [value, setValue] = useState('');
const inputRef = useRef();
const handleChange = (e) => {
setValue(e.target.value);
onChange(e.target.value);
};
return (
<input
value={value}
onChange={handleChange}
ref={inputRef}
/>
);
}2. 受控组件优化方案
使用useCallback和useMemo减少不必要的渲染:
function OptimizedForm({ onSubmit }) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSubmit = useCallback((e) => {
e.preventDefault();
onSubmit({ email, password });
}, [onSubmit]);
return (
<form onSubmit={handleSubmit}>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="邮箱"
/>
<input
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="密码"
/>
<button type="submit">登录</button>
</form>
);
}八、性能与工程实践
1. 受控组件性能优化
频繁输入时,可以采用防抖处理:
function DebouncedInput({ onChange }) {
const [value, setValue] = useState('');
const handleChange = useCallback((e) => {
const newValue = e.target.value;
setValue(newValue);
setTimeout(() => {
onChange(newValue);
}, 300);
}, [onChange]);
return <input value={value} onChange={handleChange} />;
}2. HTML实体处理性能考量
对于大量文本处理,可以使用缓存机制:
const escapeCache = new Map();
function htmlEscape(str) {
if (escapeCache.has(str)) return escapeCache.get(str);
const result = str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
escapeCache.set(str, result);
return result;
}九、常见问题与踩坑
1. ref的常见错误
错误示例:直接操作DOM
function MyComponent() {
const inputRef = useRef();
useEffect(() => {
inputRef.current.focus(); // 这里可能报错
}, []);
return <input ref={inputRef} />;
}解决方案:使用useRef替代直接操作DOM
2. 受控组件的常见问题
错误示例:混合使用受控和非受控组件
function Form({ onSubmit }) {
const [email, setEmail] = useState('');
const inputRef = useRef();
const handleSubmit = (e) => {
e.preventDefault();
onSubmit(email);
};
return (
<form onSubmit={handleSubmit}>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
ref={inputRef}
/>
<button type="submit">提交</button>
</form>
);
}3. HTML实体处理安全风险
错误示例:直接使用用户输入
function RenderContent({ html }) {
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}解决方案:使用htmlEscape函数转义
十、最佳实践
1. props使用建议
- 使用prop-types进行类型校验
- 为props设置默认值
- 对敏感数据进行脱敏处理
- 避免传递函数作为props
2. ref使用规范
- 优先使用state替代ref
- 对于必须操作DOM的场景,使用useRef
- 避免直接操作DOM,通过API调用替代
- 在函数组件中使用forwardRef传递ref
3. 受控组件应用原则
- 表单输入统一使用受控组件
- 复杂表单使用表单库(如Formik)
- 简单输入使用非受控组件
- 处理大量文本时使用受控+ref组合
4. HTML实体处理规范
- 所有用户输入都进行转义处理
- 使用第三方库(如DOMPurify)进行安全处理
- 避免直接使用dangerouslySetInnerHTML
- 对富文本编辑器输出进行双重校验
十一、总结
React的props、ref和受控组件构成了组件通信的核心机制,其背后蕴含着React对状态管理和组件解耦的深刻思考。props的单向数据流确保了可维护性,但需要开发者主动管理数据流向;ref提供了直接操作DOM的能力,但要避免破坏组件解耦性;受控组件通过state统一管理输入状态,但需要在性能和安全性之间取得平衡。
在处理HTML实体字符时,既要考虑安全风险(如XSS攻击),也要兼顾功能需求(如富文本编辑)。通过合理使用props传递数据、ref获取DOM、受控组件管理状态,以及HTML实体的转义处理,可以构建出安全、高效、可维护的React应用。
实际开发中,建议遵循以下原则:对于需要直接操作DOM的场景优先使用ref,对于表单输入统一使用受控组件,对于用户输入内容进行双重转义处理,对于复杂交互使用第三方库辅助开发。通过这些实践,可以构建出既符合React设计思想又满足业务需求的高质量应用。
评论已关闭