1. React 与前端
VDOM
- 使得一端多用以及服务端渲染成为了可能:只要替换掉底层的渲染引擎即可
- 最大限度的保证 React 应用的性能,不直接更新真实的 DOM 节点,而是通过 diff 算法触发更新
2. 深入浅出 React
PureComponent
- 是 React 15.3 引入的组件基类
- 继承自 Component,并将 isPureReactComponent 属性设置为 true
- shouldComponentUpdate 不会直接返回 true,而是会对属性和状态进行浅层比较,也就是仅比较直接属性是否相等
总结
- ES5 语法,用 createClass
- ES6 语法,用 component
- 组件没有自身状态,用 Function Component
- 组件是纯组件,用 PureComponent
实例化组件
- createElement
- JSX
setState
不要在 render 中使用 setState,因为 setState 会触发 render, 如果在 render 中再调用 setState,那么就会出现死循环
只要数据不会影响到 UI 的变化,能放到文法作用域里的,能放到 this 里的,都不要放到 state 中, 以避免不必要的浪费
React 事件
- React 的事件名字是驼峰式的,而 HTML 中的事件名字全是小写字母
- React 的事件处理器是一个函数,而 HTML 中的事件处理器是一个字符串
React 事件的优点:
- React 事件是声明式的,彻底绕过了选择器,绑定事件的过程变得非常简单
- React 事件是天生的事件代理,看起来事件散落在元素上,其实 React 仅仅在根元素绑定事件,所有事件都通过事件代理响应
- e 事件对象是 React 基于 W3C 规范封装过的,屏蔽了浏览器差异
- 访问原生对象:e.nativeEvent
把列表索引传给事件处理函数
- bind
- 包裹一层函数
class Demo extends Component {
onClick(index) {
console.log('index')
}
render() {
return (
<ul>
{list.map((item, key) => (<li onClick={this.onClick.bind(key)}>item.name</li>)}
</ul>
)
}
}
class Demo extends Component {
onClick(index) {
console.log('index')
}
render() {
return (
<ul>
{list.map((item, key) => (<li onClick={() => this.onClick(key)}>item.name</li>)}
</ul>
)
}
}
通信
父子组件
- props
- 回调函数:必须在初始化时传入,且不可撤回,且只能传入一个函数
- 部署消息接口:随处订阅,且可以多次订阅,还可以取消订阅,但是需要引入消息基类
- 需要一个可以发布和订阅消息的基类
- 子组件继承消息基类,就有了发布消息的能力,然后父组件订阅子组件的消息
class EventEmitter {
constructor() {
this.eventMap = {};
}
sub(name, cb) {
const eventList = this.eventMap[name] || [];
eventList.push(cb);
}
pub(name, ...data) {
(this.eventMap[name] || []).forEach(cb => cb(...data));
}
}
class Child extends EventEmitter {
constructor() {
super();
// 通过消息接口发布消息
setTimeout(() => {this.pub('update')}, 2000);
}
}
class Parent {
constructor() {
// 初始化阶段,传入回调函数
this.child = new Child();
}
// 订阅子组件的消息
this.child.sub('update', function () {
console.log('child update');
})
}
爷孙组件
- context: 可以跨越任意层次向后代组件传递消息
- 省去了层层传递的麻烦,并且通过双向声明控制了数据的可见性,当层数很多时,不失为一种方案
- 缺点:像全局变量一样,如果不加衣节制很容易造成混乱,也容易出现重名覆盖的问题
- 建议:对所有组件共享的一些只读信息可以采用 context 来传递
- React Router 就是通过 context 来传递路由属性的
兄弟组件
主模块模式:解耦,把两个子组件之间的耦合,解耦成子组件和父组件之间的耦合,把分散的东西搜集在一起,能带来更好的可维护性和可拓展性
任意组件
- 共同祖先
- 消息中间件:
- 引入全局消息工具,利用观察者模式将两个组件之间的耦合解耦成组件和消息中心+消息名称的耦合。
- 但为了解耦,却引入了全局消息中心和消息名称,消息中心对组件的侵入性很强,和第三方组件通信不能使用这种方式
- 规模很大的项目,对消息名称的维护比较棘手,重名概率极大,没有人敢随便删除消息信息,消息发布者找不到消息订阅者的信息等
- 制定命名规范,消息命名空间
- 通过把消息名称统一维护到一个文件中,通过对消息的中心化管理来解决
- 状态管理
- 通过状态管理工具把组件间的关系和关系的处理逻辑从组件中抽象出来,并集中化到统一的地方来处理;如Redux
组件的抽象与复用
总结
- 组件应该只通过属性输入,避免通过 context,更要避免读取全局变量、系统 I/O 等
- 组件的属性应该有默认值
- 组件的属性应该使用简单值,尽量避免使用对象等复杂的数据结构,简单的属性值更容易理解和维护
- 组件要足够健壮,考虑边界异常情况,要做好属性的类型验证,不可缺省
- 组件要有灵活的适用能力,不要限制使用环境,而要适用于一切环境,比如组件不要给自己设置宽度,要适用于所有的宽度
继承(A is B)
抽象父类通过继承的方式来解决重复的问题,如 PureComponent 的实现;
但是继承强调的是子类必须是父类,子类是父类的一个更狭隘的定义,如果设计的不好,会变得非常脆弱,不好维护
组合(A has B)
mixin:
- ES5 语法,通过prototype 来实现复用
- 会存在重名覆盖的问题,即后面混入的重名方法会覆盖前面混入的方法,当多人维护项目时,或者大量引用第三方 mixin 时,这个问题会被放大
高阶组件(A has B)
- 调用传入的组件
- 继承传入的组件
- 通过super,可以解决mixin 重名覆盖的问题,可多次定义重名方法
命令式与DOM
ref
- 输入框获取焦点
- 弹窗组件
- 复用非 React 的第三方库
findDOMNode
- 获取整个组件的 DOM
dangerouslySetInnerHTML
React 对输出的内容都会进行 XSS 过滤,但如过接口返回 HTML 片段,那dangerouslySetInnerHTML可以将 HTML 片段直接设置到 DOM 上
function User() {
return <div dangerouslySetInnerHTML={__html: '<a>lulustyle.net<a>'}></div>
}
3. Redux 应用架构基础
函数式编程
编程范型是指一种编程风格
函数式编程是一种典型的声明式编程,与命令式编程相对立,它更看中程序的执行目标而不是执行过程
函数是“一等公民”
- 赋值给其他变量
- 作为参数传递
- 作为返回值返回
纯函数
- 对于指定输出,返回指定结果
- 不可变性
- 共享数据
- 不存在副作用
- 调用系统 I/O的 API、Date.now()、Math.random()
- 发送网络请求
- 在函数体内修改外部变量的值
- 使用 console.log() 输出信息
- 调用存在副作用的函数等
可预测、可追溯
- 出现任何问题一定是因为组件接收了不正确的 state
- 先看产生错误state 的 reducer 接收到的 action 是否正确
- 再看是否是 reducer 内部处理出现了错误
Redux
- 一个严格规定了使用模式的库
- 借鉴了函数式编程的思想
- 采用单向数据流理念
- 使状态可预测、可追溯,即“时间旅行”,使得编程体验、代码维护、Bug 排查变得容易
Redux 的哲学理念:
- Single source of truth
- State is read-only
- Changes are made with pure functions called reducer
“只读“并不是”保护一个对象不受改变“,而是当页面需要新的数据状态时再生成一颗全新的状态数据树,使得 store.getState 返回一个全新的 JS 对象
store
- dispatch(action): 派发 action
- subscribe(listener): 订阅页面数据状态,即 store 中 state 的变化
- getState():获取当前页面状态数据树,即 store 中的 state
- replaceReducer(nextReducer): 一般开发用不到,社区一些热更新或者代码分离技术中可能会使用到
createStore()
- reducer: 为开发者编写的 reducer 函数
- preloadedState:页面状态数据树的初始状态
- enhancer:增强器,函数类型
action creator
action 每次携带的数据都不同,类似于工厂模式的生产工具
(data) => {
type: actionType,
payload: data,
}
dispatch
store.dispatch(action)
reducer
(state={}, action) => {
switch (action.type) {
case 'case':
return newState;
default:
return state;
}
}
combineReducers
combineReducers({reducers})
ES6 中通常让子 reducer 函数名称与数据状态命名一致
subscribe
store.subscribe(render)
总结:
当 Redux 的 createStore 创建了一个 store 实例后,使用 store.dispatch 一个 action,Redux 会“自动”执行处理变化并更新数据的 reducer 函数。通过 store.subscribe(callbackFunction) 订阅数据的更新,并在callbackFunction 中使用 store.getState() 获取最新数据,完成 UI 更新
保证不可变性
- 基本类型存储在堆内存中
- 引用类型存储在栈内存中
如果使用 slice、filter、map、reduce 等函数式 API 再结合 ES Next 新特性已经完全可以满足开发需求,那么就没有必要使用 immutable.js、mori.js 等第三方库了
通用深拷贝工具函数:
const type = obj => {
var toString = Object.prototype.toString;
var map = {
'[Object Array]' : 'array',
'[Object Object]' : 'object'
}
return map[toString.call(obj)];
}
const deepClone = data => {
// 先使用 type 函数进行数据类型判断
var t = type(data)
var o, i, length;
if (t === 'array') {
// 数组类型,新建空数组
o = [];
} else if (t === 'object') {
// 对象类型,新建空对象
o = {};
} else {
// 基本数据类型的值是不可变的,直接返回
return data;
}
if (t === 'array') {
for (i = 0, length = data.length; i < length; i++) {
o.push(deepClone(data[i]));
}
return o;
} else if (t === 'object') {
for (i in data) {
o[i] = deepClone(data[i])
}
return o;
}
}
Redux 中间件
中间件:作为中间设备、中间桥梁可以连接两种事务或服务
Redux 中间件:提供的是位于 action 被派发之后,到达 reducer 之前的扩展点,因此可以完成日志记录、调用异步接口、路由、中断 action 触发,甚至修改 action等
const enhancer = applyMiddleware(...middlewares)
React-redux
展示组件 | 容器组件 | |
---|---|---|
目的 | 展示页面内容 | 处理数据和逻辑 |
是否感知 Redux | 不感知 | 感知 |
数据来源 | 从 props 获取 | 从 Redux state 订阅获取 |
改变数据 | 通过回调 props 派发 action | 直接派发 action |
由谁编写 | 开发者 | 由 react-redux 库生产 |
<Provider store={store}>
<WrappedComponent />
</Provider>
4. 深入理解 Redux
6. 深入理解 React 技术内幕与生态社区
createClass && Mixins
场景:跟踪鼠标的位置信息,并将其坐标显示在页面上
import React from 'react'
import ReactDOM from 'react-dom'
const App = React.createClass({
getInitialState() {
return { x: 0, y: 0 }
},
handleMouseMove(e) {
this.setState({
x: e.clientX,
y: e.clientY
})
},
render() {
const { x, y } = this.state
return (
<div onMouseMove={this.handleMouseMove}>
<h1>The mouse position is ({x}, {y})</h1>
</div>
)
},
})
import React from 'react'
import ReactDOM from 'react-dom'
const MouseMixin = {
getInitialState() {
return { x: 0, y: 0 }
},
handleMouseMove(e) {
this.setState({
x: e.clientX,
y: e.clientY
})
}
}
const App = React.createClass({
// 使用 MouseMixin
miixins: [ MouseMixin ],
render() {
const { x, y } = this.state
return (
<div onMouseMove={this.handleMouseMove}>
<h1>The mouse position is ({x}, {y})</h1>
</div>
)
}
})
Mixins 的弊端:
-
HOC
使用高阶组件需要注意的事项和遵守的原则:
高阶组件不可以直接修改接收到的组件的自身行为,只能进行功能组合
- 高阶组件是纯函数,需要保证没有副作用
- 在进行功能组合时,一般通过增加不相关的 props 的形式给原有组件传递信息
- 不要在 render 方法中使用高阶组件
- 高阶组件不会传递 refs
7. 单页面应用代码分割
代码分割:将打包后的代码按照某种方式进行分割,即分割成切片,也就是实现不同逻辑模块的脚本文件,再按照相应逻辑对所需切片进行懒加载或按需加载。这样,某些从初始母本中被分割出来的切片脚本,也许在用户浏览周期中永远不会被加载。
第三方库代码的分割
基于业务的代码分割
分割纬度
- 按照业务逻辑和依赖库分割
- 按照路由分割:react-router 有支持
按照组件分割
消极加载(Passive Preloading)
- 指不需要用户额外的交互便进行加载,加载往往由组件的某个生命周期函数或者相关组件的某些行为触发。核心特征是触发时机不依赖用户的动作
- react-loadable
- 积极加载(Active Preloading)
- 依赖用户的动作和页面进行交互
按需加载
require.ensure
动态倒入
syntax-dynamic-import
抽象一个 Async 组件,用于动态导入:
export default class Async extends React.Component {
componentWillMount = () => {
this.cancelUpdate = false;
this.props.load.then((c) => {
this.C = c;
if (!this.cancelUpdate) {
this.forceUpdate();
}
})
}
componentWillUnmount = () => {
this.cancelUpdate = true;
}
render = () => {
const {componentProps} = this.props;
return this.C
? this.C.default
? <this.C.default {...componentProps} />
: <this.C {...componentProps} />
: null;
}
}
- load: Promise 类型,用于动态导入其他组件
8. React 应用性能优化
8.1 React 自身的优化保证
不要过早地做优化:高效的 DOM diff 算法 + 先进的 React 内部引擎
性能瓶颈:
- 图形处理应用
- DNA 检测实验应用
- 富文本编辑器
- 功能丰富的表单型应用
页面每一帧的变化都是由浏览器绘制出来的,并且这个绘制频率受限于显示器的刷新频率,所以一个重要的性能数据指标是 60帧/s 的绘制频率(对应于显示器的60HZ),即每一帧只有 16.6ms 的绘制时间。
DOM diff 可以决策出每次更新的最小化 DOM Batch 操作,即最大限度地避免了开发者对 DOM 的直接操作
在不合适的时间进行 DOM 操作,从而引发强制刷新或者布局震荡
使用 React 能完成的性能优化,使用原生 JS 都能做到,甚至做到更好,但经过 React 统一处理后,大大节省了开发成本,同时也降低了应用性能对开发者优化技能的依赖。
React 性能保证:
- 高效的 diff 算法
- Batch 操作
- 摈弃脏检测更新方式
diff 算法
- React 始终维护两套虚拟的 DOM,更新后的和前一个状态的。
- 通过对这两套虚拟 DOM 运用 diff 算法,找到需要变化的最小单元集,然后把这个最小单元集应用在真实的 DOM 中
- 找到最小单元集,采用的是启发式的思路,将复杂度从O(n)缩减到 O(n)
高效的diff 策略
- 对组件树进行分层比较,两棵树只会对同层级节点进行比较:DOM 节点跨层级移动忽略不计
- 当对同一层级的节点进行比较时,对于不同的组件类型,直接将整个组件替换为新类型组件:拥有相同类的连个组件生成相似的树形结构,拥有不同类的两个组件生成不同的树形结构
- 对于同层级同类型,如果组件的 state 或 props 发生变化,则直接重新渲染组件本身。开发者可以用 shouldComponentUpdate 来规避不必要的渲染
- 同层级的节点比较时,可以使用 key 属性来 “声明” 同层级节点的更新方式
setState 的 Batch 操作
- 积攒归并 一批变化后,再统一进行更新
8.2 性能优化调试工具
在开发环境下,可以直接在浏览器控制面板中获取 React.addons.Perf 对象
React.addons.Perf.start()
启动调试,然后对应用进行正常的操作,操作结束后,再用React.addons.Perf.stop()
关闭调试- React.addons.Perf.printWasted() : 打印可以优化的时间,即花费了多少时间在 VDOM 的创建和 diff 上,但最终并没有对真实的 DOM 节点进行更新或改动
- React.addons.Perf.printInclusive() : 打印话费的总时间
- React.addons.Perf.printExclusive(): 打印 Exclusive 时间,这个时间不包含加载组件的时间,即不包含处理 props、getInitialState, 调用 componentWillMount 和 componentDidMount 等的时间
- React.addons.Perf.printDOM(): 打印所有的 DOM 操作,例如 “设置 innerHTML” 和 “移除节点“
8.3 提升 React 应用性能
React 渲染真实的 DOM 节点的过程:
- 对 VDOM 进行更新
对比两个 VDOM,并将 diff 的结果应用于真实的 DOM 中
shouldComponentUpdate or PureComponent
- recompose 库
- reselect 库 or 利用 connect 的 options 参数
Redux 中的 connect
- 是一个高阶组件
- 从 store 中提取信息,利用 React context 特性进行传递,并在 state 发生变化时调用 mapStateToProps 来进行响应,最终触发相关组件的渲染
- 判定“发生变化”采用的是浅比较,由于对于复杂类型比较的是内存地址,而computedData总会是一个新的对象,因此会发生一些不必要的重复渲染
- 解决方案
- reselect 库
- 利用 connect 的 options 参数
connect(
[mapStateToProps],
[mapDIspatchToProps],
[mergeProps],
[options]
)
[options] = {
pure = true,
areStatesEqual = strictEqual,// (prevState, nextState) => (true:mapStateToProps则不执行)
areOwnPropsEqual = shallowEqual,
areStatePropsEqual = shallowEqual,
areMergedPropsEqual = shallowEqual,
...extraOptions
}
[mergeProps] 会将 […ownProps, …stateProps, …dispatchProps] 这三个来源进行合并,得到的对象将作为最终注入目标组件的 props,如果对 mergeProps 进行重写,则可以自定义组合最终的 props 结果。
[options] 是对象类型,包括5个布尔值,它们共同的作用是决定是否触发组件重新渲染。
Redux 中间件和 Web Worker
Web Worker:
- 能够长时间运行(响应)
- 理想的启动性能
- 理想的内存消耗
N 皇后算法(N-Queen Solver)
- 计算耗时时长和 n 值相关, n 值越大,计算成本越高