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 策略
- Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。
- 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。
- 对于同一层级的一组子节点,它们可以通过唯一 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 的对比
共同点
- 都使用了 Virtual DOM
- 都提供了响应式和组件化的视图组件
- 将注意力集中保持在核心库,其他功能如路由和全局状态管理交给相关的库
不同
- 优化: React 应用中,某个组件的状态发生改变时,它会以组件为根,重新渲染整个组件子树。
如果要避免不必要的子组件的渲染,需要使用 PureComponent 或者 shouldComponentUpdate 方法进行优化- 在 Vue 应用中,组件的依赖是在渲染过程中自动跟踪的,所以系统能精确知道哪个组件需要被重新渲染
虚拟 DOM
虚拟 DOM 只是一个单纯的 JS 对象。
虚拟 DOM 的优劣
使用虚拟 DOM:当我们修改我们的数据重新生成新的虚拟 DOM,新老虚拟 DOM 进行 DIFF 操作之后,框架底层内部会对我们的真实 DOM 进行操作。
使用真实 DOM:直接手动操作 DOM
优点
- 保证性能的下限
当我们使用虚拟 DOM 时,框架会帮我们完成 DOM 的操作,相当于是一个自动化的过程。- 跨平台
虚拟 DOM本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、weex 开发等等。
缺点
虚拟 DOM 的使用可以保证性能的下限,但也正是因为如此,它也无法做到极致的优化。毕竟我们操作虚拟 DOM 的最终目的是操作真实 DO,那论性能的上限自然是无法与直接操作真实 DOM 相比。