#  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'))
}
```



## 生命周期

![React生命周期](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-06-17-ReactLifecycle.PNG)



## 状态管理

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

![Redux-数据流动](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-06-26-Redux-Structures.png)

### 开发步骤

- 各个组件在需要修改传递数据的时候创建一个 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 的类型，如果是对象则直接传递；如果是函数则直接执行。

![不使用redux-thunk时action返回函数报错](https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/assets/2019-06-24-reduxThunk.png)

- 异步函数不应该放在组件的生命周期函数里面。复杂的业务逻辑和异步函数适合拆分。目前主流的解决方案有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）


  