mirror of
https://github.com/NohamR/knowledge-kit.git
synced 2026-05-25 12:27:15 +00:00
238 lines
13 KiB
Markdown
238 lines
13 KiB
Markdown
# React核心技术剖析
|
||
|
||
|
||
|
||
## 虚拟 Dom
|
||
|
||
传统的开发方式的是关心接口请求后的数据处理以及 Dom 操作。在推出 React 和 Vue 之流后,开发者无须直接操作 Dom,而是去关心数据流动。因为在 MVVM 框架中很重要,但是在名字上没有体现出其重要性的一个角色:**binder** 层帮我们做好了 view 到 model 的绑定关系(底层实现类似对属性的 getter、setter 进行拦截,然后设置 watcher。然后通过指令去绑定到 Dom 和 数据。setter 的时候发现数据变动了则发送通知,然后 watcher 监听到后去自动刷新 Dom)。你只需要关心数据。
|
||
|
||
那么如何实现的呢?我们知道 Dom 操作很耗费性能,但是到底消耗在哪里,重排、重绘、渲染算一个,可以看看这篇[文章](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.31.md)。
|
||
结合实际开发步骤想想看,先声明数据,再写 jsx 模版,然后数据驱动(setState),拿到第一份的 Virtual Dom。在数据变动后 setState 拿到最新的数据和样式模版,生成最新的 Virtual Dom。然后根据 diff 算法找出变动的地方,然后该组件重新渲染其子组件也会跟着一起渲染。
|
||
|
||
虚拟 Dom 本质上来看就是 JS 对象(将一个 Dom 节点用 js 对象模拟表示出来)。直接操作 Dom 成本高,采用一种方式将 Dom 的树形结构表示出来,那么就采用了 js 对象去描述一个 Dom 结构。 然后再每次数据变动之后,根据数据和样式模版渲染生成新的虚拟 Dom。再利用 Diff 算法计算出差异部分,再去渲染。
|
||
|
||
React 性能高效的一个原因就是 Virtual Dom 的应用和 diff 之后的 Batch Update(批量处理,类比 Vue 中的 $nextTick。有 Native 开发经验的同学对于这里应该有似曾相识的感觉,和 RunLoop 很像。任何 UI 层变动的东西提交给系统,系统再下一次的运行循环到来的时候统一去渲染。)
|
||
|
||
|
||
|
||
## Diff 算法
|
||
|
||
diff 算法大体上做的事情就是拿到前后2个状态的 Virtual Dom ,然后按照同层级节点去比较,发现当前的节点有差异,则不向下进行比较,直接将当前节点重新渲染。
|
||
|
||
|
||
|
||
## JSX 的原理
|
||
|
||
JSX 做的事情是为了告诉 React 样式模版是什么。本质上来说 JSX 就是 `React.createElement` 的可读性更强的版本。`React.createElement` 接收三个参数。参数1:标签类型;参数2:属性;参数3:子元素。
|
||
|
||
|
||
```Javascript
|
||
render () {
|
||
const { content } = this.props
|
||
return <div><span>item-testing</span></div>
|
||
}
|
||
// 等价于下面的写法
|
||
render () {
|
||
const { content } = this.props
|
||
return React.createElement('div', {}, React.createElement('span', {}, 'item-testing'))
|
||
}
|
||
```
|
||
|
||
|
||
|
||
## 生命周期
|
||
|
||

|
||
|
||
|
||
|
||
## 状态管理
|
||
|
||
Redux 设计上是对 Flux 的改进,增加了 reducer。Flux 就不再介绍了。解决了各个组件之间数据传递的复杂问题。先看看 Redux 进行状态管理的一个流程吧。
|
||
|
||

|
||
|
||
### 开发步骤
|
||
|
||
- 各个组件在需要修改传递数据的时候创建一个 Action
|
||
- 利用 dispatch 提交给 store
|
||
- store 本身不处理 state,所以将 action 转发给 reducer
|
||
- reducer 根据 action 的 type 判断具体如何处理数据。 reducer 中返回的函数是纯函数(输入给定的时候,输出的结果也是恒定的。且不改变输入值),函数的返回值就是 state,state 返回给 store,store 可以通过 `getState()` 拿到最新的 state 数据。
|
||
- 各个组件如果需要知道 state 的数据变化,那么可以在组件的 constructor 中设置监听订阅(subscribe) store(代码:store.subscribe(this.handleStateChange))。订阅的地方设置一个处理函数,然后在处理函数里面根据 store 获取到最新的 state(this.setState(store.getState()))。
|
||
|
||
### 开发经验
|
||
|
||
- Redux 中每次创建 action 都需要设置 type,type 为字符串,所以很容易写错,且各个组件都直接用字符串的方式创建 action 的 type 会比较分散,字符串拼写错误造成的 bug 难以排查,所以需要一个地方集中统一处理 action type。思路为在 `src/store` 文件夹下面创建 actionTypes.js 文件夹,创建全部大写的变量,然后导出。
|
||
<details>
|
||
<summary>展开示例代码</summary>
|
||
|
||
```javascript
|
||
export const CHANGE_INPUT_VALUE = 'change_input_value';
|
||
export const ADD_TODO_ITEM = 'add_todo_item';
|
||
export const DELETE_TODO_ITEM = 'delete_todo_item';
|
||
export const INIT_TODO_DATA = 'init_todo_data';
|
||
export const GET_INIT_LIST = 'get_init_list'
|
||
```
|
||
</details>
|
||
|
||
- Redux 使用的时候全局工程会创建很多 action,所以和上面的思想一样,需要集中统一处理,符合“收口原则”、“单一原则”。做法就是在 `src/store` 目录下创建一个 actionCreators.js 文件。然后在里面引入 actionType.js,根据业务导出几个产生 action 的函数。
|
||
<details>
|
||
<summary>展开示例代码</summary>
|
||
|
||
```javascript
|
||
import { CHANGE_INPUT_VALUE, ADD_TODO_ITEM, DELETE_TODO_ITEM } from './actionTypes'
|
||
|
||
export const getInputChangeAction = (value) => {
|
||
return {
|
||
type: CHANGE_INPUT_VALUE,
|
||
value
|
||
}
|
||
};
|
||
|
||
export const getAddTodoItemAction = () => ({
|
||
type: ADD_TODO_ITEM
|
||
});
|
||
|
||
export const getDeleteTodoItemAction = (value) => ({
|
||
type: DELETE_TODO_ITEM,
|
||
value
|
||
});
|
||
```
|
||
</details>
|
||
- store 发现 action 提交的数据是函数类型的时候,会自动执行函数
|
||
|
||
|
||
### 核心思想
|
||
|
||
- 单一数据源:整个应用的 state 被存储在一棵 Object tree 中,并且这个 object tree 只存在于唯一一个 store 中
|
||
- State 是只读的:唯一改变 state 的方式就是触发 action,action 是描述已发生时间的普通对象
|
||
- 使用纯函数来执行修改:为了描述 action 如何改变 state tree,你需要根据业务编写 reducer
|
||
|
||
### Redux-thunk
|
||
|
||
Redux-thunk 是 redux 里面常用的一个中间件。中间件?针对谁和谁的中间?对 action 和 store 的中间件。本来 action 只可以返回一个对象,灵活性较低,但是采用了 redux-thunk 之后,action 不仅可以传递对象,还可以传递函数。 action 通过 dispatch 传递给 store。 dispatch 判断 action 的类型,如果是对象则直接传递;如果是函数则直接执行。
|
||
|
||

|
||
|
||
- 异步函数不应该放在组件的生命周期函数里面。复杂的业务逻辑和异步函数适合拆分。目前主流的解决方案有2种中间件:redux-thunk、redux-saga。采用不同的策略
|
||
- redux-thunk:将异步任务分离到 action 中。
|
||
- redux-saga:将异步任务拆分到单独的文件中,而不是 action 里面。
|
||
相比较而言,redux-saga 比 redux-thunk 功能更加强大,提供的有用的功能更多。
|
||
|
||
- react-redux:网上经常说的 react-redux 里面既有 UI 组件、也有容器组件。connect 方法将一个 UI 组件(傻瓜组件) 和 store、dispatch 联合在一起后,connect 函数的返回结果就是一个容器组件
|
||
```javascript
|
||
export default connect(mapStateToProps, mapDispatchToProps)(TodoListReactRedux)
|
||
```
|
||
|
||
|
||
## 组件的写法
|
||
|
||
```javascript
|
||
// 功能组件
|
||
function Welcome (props) {
|
||
return <h2>Hello, {props.name}</h2>;
|
||
}
|
||
// 等价于下面的写法。ES6 类
|
||
class Welcome extends React.Component {
|
||
render () {
|
||
return <h2>hello, {this.props.name}</h2>;
|
||
}
|
||
}
|
||
|
||
```
|
||
|
||
###
|
||
|
||
## 开发tips
|
||
|
||
- 不要直接操作 state,只能通过 setState 操作数据;props 是只读的
|
||
|
||
- setState 的时候如果依赖之前的 state 数据,那么 setState 第一个参数可以更改为函数方式,这个函数有2个参数
|
||
```javascript
|
||
setState((state, props) => ({ count: state.count + props.increment }));
|
||
```
|
||
- 路由设置的时候,我们经常会设置路径。每个路径匹配到具体的页面资源会呈现出来。但是在一开始的时候会遇到疑问,为什么我在浏览器里面输入了 “/home”。但是出来的内容还是 “/” 皮配到的页面,后来知道了还可以设置 **exact** 属性可以精确控制。
|
||
```javascript
|
||
<Provider store={store}>
|
||
<BrowserRouter>
|
||
<div>
|
||
<Header />
|
||
<Route path='/' exact component={Home}></Route>
|
||
<Route path='/login' exact component={Login}></Route>
|
||
<Route path='/write' exact component={Write}></Route>
|
||
<Route path='/detail/:id' exact component={Detail}></Route>
|
||
</div>
|
||
</BrowserRouter>
|
||
</Provider>
|
||
```
|
||
- 在 React 的开发中,有2个名字会很熟悉:傻瓜组件、容器组件。假如一个 TodoList 的 UI 部分和逻辑处理部分都在 一个 TodoList 组件里面进行解决,那么代码将会冗余且不易测试,为了解决此问题,我们常常会将 UI 部分单独抽离出去,只负责显示出 UI,这种组件叫做傻瓜组件(UI组件)。页面需要的数据或者点击事件的处理函数都通过 **props** 的形式由父组件传递下来。父组件在渲染的时候只负责逻辑的展示,在自身的 render 函数里面调用之前分离出去的傻瓜组件(UI组件)。为了保证代码的健壮性和安全性,UI 组件需要的数据和函数都通过 props 传递,且加一个 propTypes 安全校验。
|
||
|
||
- 无状态组件:当一个组件只有 render 函数的时候,这样的组件被叫做**无状态组件**。做法就是将 `class TodoList extends React.Dom` 修改成一个函数。函数形式的无状态组件效率比较高。因为类形式的组件,会有生命周期等函数,效率会低一些。
|
||
|
||
```javascript
|
||
const TodoListUI = (props) => {
|
||
return <div>props.name</div>;
|
||
}
|
||
```
|
||
|
||
- export default 在一个模块里面只可以存在一个,使用的时候不需要 `{}`;export 可以存在多个,使用的时候需要使用 `{}`
|
||
```javascript
|
||
export const person = {
|
||
name: 'lbp',
|
||
age: 22
|
||
}
|
||
export const testing = 'testing'
|
||
export default store;
|
||
|
||
import store from './store'
|
||
import { person, testing } from './store'
|
||
```
|
||
|
||
|
||
## React 和 Vue 的对比
|
||
|
||
- React 是单向数据流,数据是不可变的。Vue 是双向数据流,数据是可以变的。什么意思?看下面的例子
|
||
Vue.js
|
||
```javascript
|
||
<input type="text" maxlength="11" autocomplete="off" class="form-control input-lg input-flat input-flat-user" placeholder="请输入手机号码" name="resetmobile" v-validate="'required|resetmobile'" v-model="resetmobile">
|
||
```
|
||
React.js
|
||
```javascript
|
||
constructor(props) {
|
||
super(props);
|
||
this.state = store.getState()
|
||
// 函数的 this 绑定放在顶部对 React 性能提升有好处
|
||
this.handleInputChange = this.handleInputChange.bind(this)
|
||
store.subscribe(this.handleStateChange);
|
||
}
|
||
|
||
<input placeholder="things to do..."
|
||
style={ { width: 300, marginRight: 20 } }
|
||
value={props.inputValue}
|
||
onChange={props.handleInputChange}
|
||
/>
|
||
handleInputChange(e) {
|
||
/*
|
||
// 方式1
|
||
this.setState({
|
||
inputValue: this.inputRef.value
|
||
})
|
||
*/
|
||
//方式2:redux:先创建 action,然后 dispatch 给 store,然后 store 转发给 reducer、reducer处理完后给 store、依赖的组件设置监听,然后在监听的回调里面拿到最新的数据去 render UI
|
||
const action = getInputChangeAction(e.target.value);
|
||
store.dispatch(action);
|
||
}
|
||
// 负责订阅 store 里面 state 的改变;感知到 store 里面的 state 改变,则在当前组件里面 setState
|
||
handleStateChange () {
|
||
this.setState(store.getState());
|
||
}
|
||
```
|
||
可以看出来,Vue 通过简单的一个内置命令 `v-model` 将 model 和 view 双向绑定了起来,数据是双向的。model 改变 view 自动刷新;用户在 input 写了文字,model 的值也自动改变。React 先设置一个 input 组件,监听用户的输入事件(onChange),然后在 onChange 里面拿到当前输入框里面的数据,然后你可以直接 setState 去操作数据,setState 后才会触发 render 函数,dom 才会跟着更新。
|
||
|
||
- React 比 Vue 更加适合构建大型项目。
|
||
什么是大型项目?这句话没什么意义,那就说一些区别吧。通过上面的说明知道 Vue 在内部做了双向绑定,对于数据的处理更加方便。React 对于数据变动还需要自己手动去调用 setState。假如有多个数据,按照 Vue 的原理会启动 n 个 watcher 去监听,所以性能会有一些问题。React 性能相关都需要开发者去处理。
|
||
|
||
- Vue 设计思想:How easy it can be。React:How corrct it can be 和 all in js(css写法也在用 js 控制,比如 styled-component)
|
||
|
||
|
||
|