随着应用变得复杂,前端状态的管理也变得复杂,异步状态、同步状态、谁修改的,怎么修改的,变得分不清了,所以需要一种规范将这些操作和状态统一起来,做到可以追溯,便于调试。
比如我们在 React 应用中,要处理组件间的通信、通过 this.setState 设置和通过 this.state 访问状态,而 Redux 提供全局唯一的状态树,只能通过 dispatch 修改状态,然后修改状态之后会触发组件的自动更新,所以使得上面的效果变成可能。
而 Redux 就是为了解决上面的问题而生的,它提供一个可预测的状态管理容器,让操作和状态都有序进行。
现在有很多状态管理库,如 Mobx、Redux 还有 DVA 、Rematch等,根据业务的规模、抽象程度、团队的喜好可以选取对应的库。
- 函数式和面向对象
- 单一 Store 和 多个 Store
- JavaScript 对象和可观察对象
- 不可变和可变
Redux 更多的是纯函数式编程,纯函数 Reducer 等 Mobx 是面向对象的函数响应式编程:将状态包装成可观察对象,一旦发生变化,就自动更新
- redux 一般只有一个 store
- mobx 比较灵活有多个 Store
Redux 是不可变对象 Mobx 可以直接操作对象,更新新值,然后自动触发更新
- Mobx 灵活,API 少,学习成本低
- Redux 严谨、调试方便、可时间旅行
mobx的流程图如上,通常是:触发action,在action中修改state,通过computed拿到state的计算值,自动触发对应的reactions,这里包含autorun,渲染视图等。有一点需要注意:**相对于react来说,mobx没有一个全局的状态树,状态分散在各个独立的store中。**mobx的工作原理非常简单,使用Object.defineProperty来拦截对数据的访问,一旦值发生变化,将会调用react的render方法来实现重新渲染视图的功能或者触发autorun等。 ** 特点: **
- 相较于redux中使用middleware来处理异步,mobx中不需要那么复杂,只需要使用async/await来优雅的处理异步的逻辑
- mobx中推荐使用单例模式来管理各种数据流,就像上面的Apple类,封装了可观察值的属性以及对应的操作逻辑,然后new一个实例并导出
- 在react中使用mobx,可以不需要操作state或props,直接赋值给类的属性即可
上面这段代码很简单:
- 通过observable装饰器装饰一个属性,这个属性变动之后,就会自动触发响应的动作,这里的属性可以是string,boolean,array,object等
- autorun,when,observer都是reactions,当observable装饰的可观察属性发生变化时,会触发它们自动执行
- observer的作用是,当可观察属性发生变化时,调用react组件的render方法重新渲染视图
- autorun,当autorun函数中依赖的可观察属性发生变化时,就会自动触发autorun函数的执行,同时,autorun函数返回一个函数,调用该函数将会在执行期间清理 autorun。
- when,该函数接受两个函数作为参数,第一个函数返回一个判断条件;在该判断条件满足时,将会执行第二个函数;when函数只会执行一次。
- computed的行为跟vue中的computed行为一直,它与autorun的区别简单的说,computed需要被使用才会触发自动计算,被使用可以是在视图中渲染这个值,也可以是其他reactions
- 单向数据流
- 基于中间件
一个单向数据流的状态管理库,可用于 React/Vue/Angular 等框架。
状态类型:
- Domain State:服务器端的状态,与数据库保持同步
- UI State:交互有关的状态,模态框的开关、页面的 loading、选中状态等
Redux 是 JavaScript 状态容器,提供可预测化的状态管理。让每个 State 变化都可预测,将应用中所有的动作与状态统一管理,让一切有迹可循。
View 中通过 Connect 订阅 Store 的修改 一旦 Store 修改,就会通知所有的订阅者, View 接收到通知之后会使用新的状态重新渲染
它任务:
- Web 应用是一个状态机,试图与状态一一对应
- 所有的状态,保存在一个全局的状态对象里面
- 单一数据源,一个应用一般只有一个 Store
- State 是只读的,唯一改变 State 的方式是 dispatch 一个 action,action 描述了修改状态的相关信息。便于调试和进行重做(也就是 time traveling)
- 通过纯函数来修改,需要编写 Reducer 来接收 Action 进行实际状态的修改,Reducer 接收上一次的 State 和 Action,返回新的 State。只要传入的 Action 相同,那么返回的新状态一定是一样的结果。
- 用户触发页面上的操作,dispatch 发送一个 action
- Redux 接收到这个 Action 后通过 Reducer 函数计算出下一个状态
- 将新的状态更新进 Store,Store 更新之后通知页面进行重新渲染
- 想要修改 Store 状态的唯一方法,就是 dispatch 一个 action
- dispatch 主要就是通知 Store 调用 Reducers 函数,接收对于的 action,然后进行状态的改变,返回下一个状态。
- dispatch 的返回值时它 dispatch 的 action 本身,这使得 Redux 基于 dispatch 引入了一个 middlewares 中间件的概念,可以通过一系列中间件去增强 dispatch 的功能,比如 redux-logger,就是在 dispatch 修改状态的前后打印对于的 dispatch 方法、前后状态,方便调试
- 中间件的执行是一个洋葱圈,即从最外层的前半部分一个个执行,执行到最内层函数,然后从最内层函数的后半部分执行,执行到最外层的后半部分,这种执行逻辑类似一个洋葱圈,同样应用洋葱圈模型的还有 koa、vuex 等
- 甚至可以通过异步中间件,来增强 dispatch 来处理异步流程
- https://segmentfault.com/a/1190000017140200
- https://zhuanlan.zhihu.com/p/82217557
- brickspert/blog#22 (comment)
- http://www.imooc.com/read/72/article/1679
有四个 API
- createStore
- 生成的 store,里面有三个 API
- getState
- subscribe
- dispatch
- replaceReducer
- 生成的 store,里面有三个 API
- bindActionCreators
- applyMiddlewares
- combineReducers
React 16 之前:
- 挂载:
- constructor:初始化 state,绑定方法
- componentWillMount:不能 setState,因为还没渲染
- render:纯函数
- componentDidMount:可以操作 DOM、发起网络请求、挂在定时器
- 更新:
- componentWillReceiveProps:从父组件收到新属性
- shouldComponentUpdate:是否更新
- componentWillUpdate
- render
- componentDidUpdate
- 卸载
- componentWillUnmount:清除定时器,删除无用的 DOM,
React 16 及之后:
- 挂载:
- constructor
- getDerviedStateFromProps
- render
- componentDidMount
- 更新
- getDerivedStateFromProps
- shouldComponentUpdate
- render
- getSnapshotBeforeUpdate
- componentDidUpdate
- 卸载
- componentWillUnmount
说一下 React 的算法查找和比较过程,具体如何比较,用了什么算法?怎么查找?时间复杂度为多少?一直问,问的很深。那么 React 是如何进行局部更新的了?怎么找到这个局部的节点,并比较它们的不同?
是用了简单的深度优先搜索,但是应用了三种剪枝策略,使得时间复杂度从 O(n^3) 降到 O(n),因为之前每比较一个节点,都要开两重循环去找另一颗树中的节点:
- Tree Diff:逐层比较,假设跨层级操作节点的概率不大,那么如果遇到在比较新旧的 Virtual DOM 的时候,如果此节点在旧的里面有,在新的里面没有,那么就删除这个节点,而不会去跨层级找到这个节点。
- Component Diff:两个相同类型的组件具有相同的 DOM 结构,两个不同类型的组件具有不同的 DOM 结构,如果组件不同,那么直接进行删除和插入操作。
- Element Diff:对于同一父节点下的一组节点,通过 key 值来标志,如果 key 不一样那么进行删除和插入操作,如果 key 一样,那么进行移动操作。(具体通过 lastIndex/nextIndex/_mountIndex 进行对比)
通过上面的三种优化策略比较之后,会生成一个 patchObj(差异对象),然后在根据这个 patchObj 去更新 DOM 对象,这里的 patchObj 主要分两类:
- 一类 nodePatchTypes,主要有 CREATE/UPDATE/REMOVE/REPLACE,对应着节点的创建、更新、删除、替换。
- 一类是 propsPatchTypes,主要有 REMOVE/UPDATE,对应着节点属性的删除和更新。
可以看到,这里我们比较不同的算法通过三种优化策略,优化到了 O(n),然后我们只用生成的差异对象,去做 DOM 的更新,而不是完全去渲染整个 DOM。
还可以结合框架的事务机制,将多次更新合并成一次,这样子能近一步减少重排的次数,提高渲染的效率和用户体验。
优点:
- VD 最大的特点是将页面的状态抽象为 JS 对象的形式,配合不同的渲染工具,使得跨平台成为可能,如 React 借助 VD 实现了服务器端渲染、浏览器渲染和移动端渲染等功能
- 在进行页面更新的时候,借助 VD,DOM 元素的改变可以在内存中进行比较,然后借助框架的事务机制,将多次改变结果合并后一次更新到页面,从而有效的减少页面的渲染次数,提高渲染效率。
使用 VD:UI = f(state) UI = render(state)
- 使用 JS 的计算时间换渲染时间
- JSX
- Babel 编译成函数形式
- h 函数
- 变成 VD
- 遍历 VD,生成真实 DOM
如何更新
- setState
- 事件触发
- 合成事件
- 钩子函数
- 原生事件
- 原生 API
更新算法
- Diff 算法
- Tree Diff
- Component Diff
- Element Diff
- https://segmentfault.com/a/1190000016328371
- https://juejin.im/post/5b0638a9f265da0db53bbb6d
- creeperyang/blog#33
- creeperyang/blog#30
- https://zhuanlan.zhihu.com/p/20346379
- 至少包含三个值:标签名(tag),属性(props),子元素对象(children)
传统的 diff 算法的复杂度为 O(n^3),而 React 制定大胆的策略,将 O(n^3) 复杂度的问题转换为 O(n) 复杂度的问题。
- Tree Diff:Web UI 中 DOM 节点跨层级移动操作特别是,可以忽略不计
- 只会对同一父节点下的所有子节点比较,如果子节点不存在,就删除,不会进一步比较。这样只需要一次遍历,就能完成整个 DOM 树的比较。
- 建议:在开发组件时,保持稳定的 DOM 结构有助于性能提升
- Component Diff:两个相同组件产生类似的 DOM 结构,不同的组件产生不同的 DOM 结构;
- 同一类型组件,按原策略继续进行 Virtual DOM 比较
- 如果不是, 则判断此组件为 dirty component,则会替换该组件下的所有节点
- 对于同一类型的组件,有可能其 Virtual DOM 没有变化,如果能够确切的知道这一点可以节省大量的 Diff 运行时间,因此 React 通过在组件内允许用户通过 shouldComponentUpdate 来判断是否需要 Diff,也可以直接继承 PureComponent,然后自动帮你完成
- Element Diff:对于同一层次的一组子节点,它们可以通过唯一的 id 进行区分。所以只需要给同一组的元素给与不同的 key 值就可以了
- 提供三种操作:INSERT_MARKUP、MOVING_EXISTING、REMOVE_NODE
- 添加唯一的 Key,Key 变化就进行插入和删除,否则移动
- 在开发过程中,尽量减少将最后一个节点移动到列表首部的操作,当节点的数量过大或者更新过于频繁时,在一定程度上会影响 React 的渲染性能。
因此通过上面的三个策略,可以让用户无需顾忌性能问题而 “任性自由” 的刷新界面,同时也无需关系 Virtual DOM 背后运作的实际原理,因为 React Diff 会帮助我们计算出实际变化的 Virtual DOM 部分,然后进行实际的 DOM 操作 。所以说 React Diff 和 Virtual DOM 是保证 React 性能口碑的幕后推手。
react 事件中 setState 浅合并
setState 看似异步的,但是实际上不是真正意义上的异步,只是模拟了异步的行为。
- 在原生 DOM 事件和 setTimeout 等原生里面,setState 是同步更新的
- 在合成事件或者钩子函数里面会遵循一定的策略,不会马上更新,因为合成事件和钩子函数在更新之前就运行了
- React 会去维护一个 isBatchingUpdates 标志,如果为 false 直接更新,如果是 true,那么会暂存状态进队列
- 且 setState 本身不是异步的,本身执行过程和代码时同步的,不会马上更新,因为合成事件和钩子函数在更新之前就运行了,导致没法马上拿到更新的值,所以形成了所谓的 “异步”,当然 setState 的第二个参数可以拿到更新之后的结果,或者接收函数调用,在函数调用里面可以拿到更新之后的结果
- 而且 React 的批量更新也有一定的规则,如果在合成事件或者钩子函数中,多次 setState 同一个属性会用最新的覆盖,如果多次 setState 不同的属性,会进行合并批量更新
11.手写体:Vue 的基本实现原理
如果你有 express
,koa
, redux
的使用经验,就会发现他们都有 中间件(middlewares)
的概念,中间件
是一种拦截器的思想,用于在某个特定的输入输出之间添加一些额外处理,同时不影响原有操作。
本质上是一个中间件管理器,一般是 ”尾递归“,中间件一个接一个运行,习惯将 response 响应写作最后一个中间件里面。
虽然也可以写成 ”洋葱圈“ 模型,但是 response 在最后,next 之后的内容已经不影响最后的结果了
中间件支持 generator,koa2 async/await ,一般是洋葱圈模型
13.问题:小程序 WebView 渲染,是如何实现的?
14.问:简历中提到了three.js,都做了些啥
使用 TypeScript,babel-plugin-import,在导入是编译导入去导入对应的包
- Vue 逻辑层到渲染层怎么显示的?
虽然Vue和React两者在定位上有一些交集,但差异也是很明显的。
- Vue 使用的是 web 开发者更熟悉的模板与特性,Vue的API跟传统web开发者熟悉的模板契合度更高,比如Vue的单文件组件是以模板+JavaScript+CSS的组合模式呈现,它跟web现有的HTML、JavaScript、CSS能够更好地配合。React 的特色在于函数式编程的理念和丰富的技术选型。Vue 比起 React 更容易被前端工程师接受,这是一个直观的感受;React 则更容易吸引在 FP 上持续走下去的开发者。
- 从使用习惯和思维模式上考虑,对于一个没有任何Vue和React基础的web开发者来说, Vue会更友好,更符合他的思维模式。React对于拥有函数式编程背景的开发者以及一些并不是以web为主要开发平台的开发人员而言,React更容易接受。这并不意味着他们不能接受Vue,Vue和React之间的差异对他们来说就没有web开发者那么明显。可以说,Vue更加注重web开发者的习惯。
**
- 实现上,Vue跟React的最大区别在于数据的reactivity,就是反应式系统上。Vue提供反应式的数据,当数据改动时,界面就会自动更新,而React里面需要调用方法SetState。我把两者分别称为Push-based和Pull-based。所谓Push-based就是说,改动数据之后,数据本身会把这个改动推送出去,告知渲染系统自动进行渲染。在React里面,它是一个Pull的形式,用户要给系统一个明确的信号说明现在需要重新渲染了,这个系统才会重新渲染。两者并没有绝对的优劣之分,更多的也是思维模式和开发习惯的不同。
- 两者不是完全互斥的,比如说在React里面,你也可以用一些第三方的库像MobX实现Push-based的系统,同时你也可以在Vue2.0里面,通过一些手段,比如把数据freeze起来,让数据不再具有反应式特点,或者通过手动调用组件更新的方法来做一个pull-based系统。所以两者并没有一个绝对的界限,只是默认的倾向性不同而已。
- React 推崇不可变性和 lifting-state-up,和函数式编程风格;Vue 属于 MVVM 格式,属于面向对象的风格
- 慎用 setState
- PureComponent 减少更新次数
- useMemo 缓存组件
- 长列表使用虚拟化技术
- Code Spliting 代码懒加载
- 服务器端渲染
四种:
- 父组件通过 props 给子组件传值
- 父组件通过 props 给子组件传递函数,然后在子组件中调用回调函数影响父组件
- 如果子组件嵌套很深,可以使用 Context API,通过 createContext 然后生成 context之后通过 Provider 提供 value 属性和函数来获取和修改,类似 props 的传值和传函数
- Context 也可以用在同级组件,非父子,但是对于非父子且跨级非常深的组件通信一般使用自定义事件的方式,借助发布事件,订阅事件的方式,在一个组件上注册一个事件,然后再另外一个组件上触发这个事件来进行数据的传递。
再后来就可以用一系列状态管理库了 Redux、Mobx