Files
knowledge-kit/Chapter2 - Web FrontEnd/2.32.md
2025-06-23 01:18:55 +08:00

13 KiB
Raw Blame History

React核心技术剖析

虚拟 Dom

传统的开发方式的是关心接口请求后的数据处理以及 Dom 操作。在推出 React 和 Vue 之流后,开发者无须直接操作 Dom而是去关心数据流动。因为在 MVVM 框架中很重要,但是在名字上没有体现出其重要性的一个角色:binder 层帮我们做好了 view 到 model 的绑定关系(底层实现类似对属性的 getter、setter 进行拦截,然后设置 watcher。然后通过指令去绑定到 Dom 和 数据。setter 的时候发现数据变动了则发送通知,然后 watcher 监听到后去自动刷新 Dom。你只需要关心数据。

那么如何实现的呢?我们知道 Dom 操作很耗费性能,但是到底消耗在哪里,重排、重绘、渲染算一个,可以看看这篇文章。 结合实际开发步骤想想看,先声明数据,再写 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:子元素。

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生命周期

状态管理

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

Redux-数据流动

开发步骤

  • 各个组件在需要修改传递数据的时候创建一个 Action
  • 利用 dispatch 提交给 store
  • store 本身不处理 state所以将 action 转发给 reducer
  • reducer 根据 action 的 type 判断具体如何处理数据。 reducer 中返回的函数是纯函数(输入给定的时候,输出的结果也是恒定的。且不改变输入值),函数的返回值就是 statestate 返回给 storestore 可以通过 getState() 拿到最新的 state 数据。
  • 各个组件如果需要知道 state 的数据变化,那么可以在组件的 constructor 中设置监听订阅subscribe store代码store.subscribe(this.handleStateChange))。订阅的地方设置一个处理函数,然后在处理函数里面根据 store 获取到最新的 statethis.setState(store.getState()))。

开发经验

  • Redux 中每次创建 action 都需要设置 typetype 为字符串,所以很容易写错,且各个组件都直接用字符串的方式创建 action 的 type 会比较分散,字符串拼写错误造成的 bug 难以排查,所以需要一个地方集中统一处理 action type。思路为在 src/store 文件夹下面创建 actionTypes.js 文件夹,创建全部大写的变量,然后导出。

    展开示例代码
    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'
    
  • Redux 使用的时候全局工程会创建很多 action所以和上面的思想一样需要集中统一处理符合“收口原则”、“单一原则”。做法就是在 src/store 目录下创建一个 actionCreators.js 文件。然后在里面引入 actionType.js根据业务导出几个产生 action 的函数。

    展开示例代码
    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
    }); 
    
  • store 发现 action 提交的数据是函数类型的时候,会自动执行函数

核心思想

  • 单一数据源:整个应用的 state 被存储在一棵 Object tree 中,并且这个 object tree 只存在于唯一一个 store 中
  • State 是只读的:唯一改变 state 的方式就是触发 actionaction 是描述已发生时间的普通对象
  • 使用纯函数来执行修改:为了描述 action 如何改变 state tree你需要根据业务编写 reducer

Redux-thunk

Redux-thunk 是 redux 里面常用的一个中间件。中间件?针对谁和谁的中间?对 action 和 store 的中间件。本来 action 只可以返回一个对象,灵活性较低,但是采用了 redux-thunk 之后action 不仅可以传递对象,还可以传递函数。 action 通过 dispatch 传递给 store。 dispatch 判断 action 的类型,如果是对象则直接传递;如果是函数则直接执行。

不使用redux-thunk时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 函数的返回结果就是一个容器组件

    export default connect(mapStateToProps, mapDispatchToProps)(TodoListReactRedux)
    

组件的写法

// 功能组件
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个参数

    setState((state, props) => ({ count: state.count + props.increment }));
    
  • 路由设置的时候,我们经常会设置路径。每个路径匹配到具体的页面资源会呈现出来。但是在一开始的时候会遇到疑问,为什么我在浏览器里面输入了 “/home”。但是出来的内容还是 “/” 皮配到的页面,后来知道了还可以设置 exact 属性可以精确控制。

    <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 修改成一个函数。函数形式的无状态组件效率比较高。因为类形式的组件,会有生命周期等函数,效率会低一些。

    const TodoListUI = (props) => {
      return <div>props.name</div>;
    }
    
  • export default 在一个模块里面只可以存在一个,使用的时候不需要 {}export 可以存在多个,使用的时候需要使用 {}

    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

    <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

    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。ReactHow corrct it can be 和 all in jscss写法也在用 js 控制,比如 styled-component

在 React、React Native、Vue、Weex、Flutter 等声明式开发思想的框架下UI = F(state)。一个状态唯一对应一个 UI但一个 UI 不一定对应一个 state关心 state 即可