Better

Ethan的博客,欢迎访问交流

阅读笔记:Hooks 原理、Lint 工具、离线化、垃圾回收、React 性能技巧

本篇是阅读笔记,最近阅读到的一些值得记录的东西,主要内容如题

React Hooks

React Hooks 是为了解决 Class Component 的一些问题引入的

  • 类组件间的逻辑难以复用,因为不像 Go 或 C++ 一样,Class 可以多重继承,类的逻辑的复用就成了一个问题
  • 复杂组件难以理解。类组件经常会在生命周期做一些数据获取事件监听的副作用函数,这样很难把组件拆分为更小的力度
  • Class 令人迷惑,比如 this 问题

Why Hooks

  • mixins 存在的问题
    • 命名空间耦合
    • 运行时才能知道具体有什么参数,无法做静态检查
    • 组件参数不清晰,比如 props 定义位置
  • HOC 存下的问题
    • 命名空间解耦
    • 静态检查
    • 依然无法解决组件 props 的问题,props 可能在高阶组件中被修改
    • 组件实例增加
  • Hooks
    • Hooks 跑在普通函数式组件中,所以肯定没有命名空间问题
    • TypeScript 能对普通函数做很好的静态检查
    • Hooks 不能更改组件的 Props
    • 不会生成新的组件实例

React 社区 react-redux@7 中新引入了三个 API

  • useSelector:等同于 mapStateToProps,把数据从 state 中取出来
  • useStore:返回 store 本身
  • useDispatch:返回 store.dispatch

Hooks 的限制一定程度上透露出了 Hooks 的实现原理

  • Hooks 必须是一个按顺序执行的函数,也就是说,不管整个组件执行多少次,渲染多少次,组件中 Hooks 的顺序都是不会变的。
  • Hooks 是 React 函数内部的函数

因此要实现 Hooks 最关键的问题在于两个:

  • 找到正在执行的 React 函数
  • 找到正在执行的 Hooks 的顺序

设置一个全局对象叫 CurrentOwner,拥有两个属性,第一个值是 current,表示正在执行的组件函数,可以在组件加载和更新时设置它的值,加载或更新完毕之后再设置为 null;第二个属性是 index,它就是 CurrentOwner.current 中 Hooks 的顺序,每次我们执行一个 Hook 函数就自增 1。

const CurrentOwner: {
  current: null | Component<any, any>,
  index: number
} = {
  // 正在执行的组件函数,
  // 在组件加载和重新渲染前设置它的值
  current: null,
  // 组件中 hooks 的顺序
  // 每执行一个 Hook 自增
  index: 0
}

实现 getHook 函数,如果 CurrenOwner.current 是 null,那这就不是一个合法的 hook 函数,我们直接报错。如果满足条件,我们就把 hook 的 index + 1,接下来我们把组件的 Hooks 都保存在一个数组里,如果 index 大于 Hooks 的长度,说明 Hooks 没有被创造,我们就 push 一个空对象,避免之后取值发生 runtime error。然后我们直接返回我们的 Hook。

function getHook (): Hook {
  if (CurrentOwner.current === null) {
    throw new Error(`invalid hooks call: hooks can only be called in a component.`)
  }
  const index = CurrentOwner.index++ // hook 在该组件函数中的 index
  const hooks: Hook[] = CurrentOwner.current.hooks // 所有的 hooks
  if (index >= hooks.length) { // 如果 hook 还没有创建
    hooks.push({} as Hook) // 对象就是 hook 的内部状态
  }
  return hooks[index] // 返回正在执行的 hook 状态
}

实现 useState 钩子,首先如果 initState 是函数,直接执行它。调用我们我们之前写好的 getHook 函数,它返回的就是 Hook 的状态。接下来就是 useState 的主逻辑,如果 hook 还没有状态的话,我们就先把正在执行的组件缓存起来,然后 useState 返回的就是我们的 hook.state, 其实就是一个数组,第一个值当然就是我们 initState,第一个参数是一个函数,它如果是一个函数,我们就执行它,否则就直接把参数赋值给我们的 hook.state 第一个值,赋值完毕之后我们把当前的组件加入到更新队列,等待更新。


function useState<S> (initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>] {
  if (isFunction(initialState)) {
    initialState = initialState() // 如果 initialState 是函数就直接执行
  }
  const hook = getHook() as HookState<S> // 找到该函数中对应的 hook
  if (isUndefined(hook.state)) { // 如果 hook 还没有状态
    hook.component = CurrentOwner.current // 正在执行的组件函数,缓存起来
    hook.state = [ // hook.state 就是我们要返回的元组
      initialState,
      (action) => {
        hook.state[0] = isFunction(action) ? action(hook.state[0]) : action
        enqueueRender(hook.component) // 加入更新队列
      }
    ]
  }
  return hook.state // 已经创建 hook 就直接返回
}

Lint 工具

lint 工具简史

  • JSLint
    • 最早的JS lint 工具
    • lint 规则不可自定义
  • JSHint
    • 基础自 JSLint
    • 增加了可配置性
  • ESLint
    • 可自定义 rules
    • 提供了完善的插件机制
    • 可定位到具体的 rules

ESLint 将源代码解析成 AST,然后检测 AST 来判断代码是否符合规则。ESLint 使用 esprima 将源代码解析成 AST,然后就可以使用任意规则来检测 AST 是否符合预期,这也是 ESLint 高可扩展性的原因

那个时候 ESLint 并没有大火,因为需要将源代码转成 AST,运行速度上输给了 JSHint ,并且 JSHint 当时已经有完善的生态(编辑器的支持)。真正让 ESLint 大火是因为 ES6 的出现。

ES6 发布后,因为新增了很多语法,JSHint 短期内无法提供支持,而 ESLint 只需要有合适的解析器就能够进行 lint 检查。这时 babel 为 ESLint 提供了支持,开发了 babel-eslint,让ESLint 成为最快支持 ES6 语法的 lint 工具。

Lint 工具的意义主要表现在 JS 语言的独特性,对大多数编程语言来说都会有代码检查,一般来说编译程序会内置检查工具。JavaScript 是一个动态的弱类型语言,在开发中比较容易出错。因为没有编译程序,为了寻找 JavaScript 代码错误通常需要在执行过程中不断调试。像 ESLint 这样的可以让程序员在编码的过程中发现问题而不是在执行的过程中。

Lint工具的优势:

  • 避免低级bug,找出可能发生的语法错误。使用未声明变量、修改 const 变量……
  • 提示删除多余的代码。声明而未使用的变量、重复的 case ……
  • 确保代码遵循最佳实践。可参考 airbnb style、javascript standard
  • 统一团队的代码风格。加不加分号?使用 tab 还是空格?

使用方式

全局安装 eslint,项目下运行 eslint --init,得到 .eslintrc.js 配置文件

配置方式

  • 使用注释把 lint 规则直接嵌入到源代码中,当然我们一般使用注释是为了临时禁止某些严格的 lint 规则出现的警告
  • 使用配置文件进行 lint 规则配置

配置文件的优先级

.eslintrc.js > .eslintrc.yaml  > .eslintrc.yml > .eslintrc.json > .eslintrc > package.json

配置参数

  • 解析器配置 parse
  • 环境与全局变量 globals env
  • 规则设置 rules
    • “off” 或 0:关闭规则
    • “warn” 或 1:开启规则,warn 级别的错误 (不会导致程序退出)
    • “error” 或 2:开启规则,error级别的错误(当被触发的时候,程序会退出)
  • 扩展:直接使用别人已经写好的 lint 规则,方便快捷。扩展一般支持三种类型
    • eslint: 开头的是 ESLint 官方的扩展,一共有两个:eslint:recommended 、eslint:all。
    • plugin: 开头的是扩展是插件类型,也可以直接在 plugins 属性中进行设置
    • 最后一种扩展来自 npm 包,官方规定 npm 包的扩展必须以 eslint-config- 开头
  • 插件:因为官方的规则只能检查标准的 JavaScript 语法,如果你写的是 JSX 或者 Vue 单文件组件,ESLint 的规则就开始束手无策了。这个时候就需要安装 ESLint 的插件,来定制一些特定的规则进行检查。

ESLint 的规则不仅只有关闭和开启这么简单,每一条规则还有自己的配置项。如果需要对某个规则进行配置,就需要使用数组形式的参数。

  • 第一个参数为是否启用规则
  • 后面的参数才是规则的配置项

关于解析器:对于 @typescript-eslint/parse 这个解析器,主要是为了替代之前存在的 TSLint,TS 团队因为 ESLint 生态的繁荣,且 ESLint 具有更多的配置项,不得不抛弃 TSLint 转而实现一个 ESLint 的解析器。

文中还还讲到了如何开发插件,如有需要,自行看原文

最佳配置,业界有许多 JavaScript 的推荐编码规范,较为出名的就是下面两个

  • airbnb style
  • javascript standard

将 ESLint 和 Prettier 结合使用,不仅统一编码规范,也能统一代码风格。

离线化探索

乐观 UI

谈及改善用户焦虑情绪,很有必要介绍下乐观 UI。乐观 UI 是一种界面的响应模式,它推荐前端在服务端接收响应之前,先更新 UI,一旦服务器返回,再变更为实际结果。

前端离线化几种常用的方案

  • Application Cache
    • HTML5 最早提供一种了一种缓存机制,可以使web的应用程序离线运行。
    • 使用 Application Cache 接口设置浏览器应该缓存的资源,即配置 manifest 文件
  • service-worker
    • Application Cache 一直无法有效的解决离线资源精细化控制
    • 独立的后台JS线程,是一种特殊的 worker 上下文访问环境。
    • 借助CacheStorage,我们可以在 sw 安装激活的生命周期中,按需填充缓存资源,然后在fetch 事件中,拦截 http 请求,将缓存资源或者自定义消息返回给页面。
  • 离线数据:借助客户端能力,我们可以把 web 代码包提前内置到客户端之中,然后使用一套代码更新机制,前端代码缓存问题可以得到解决。

service-worker 实现了真正的可用性及安全性。首先,相对于原有web 应用逻辑是不可见,它类似于一个中间拦截服务,中间发生任

离线数据前端方案

  • PouchDB:内部封装了IndexDB、WebSql兼容前端处理
  • Redux-Offline:基本思路是通过redux middleware 监听每次 acton 数据变化,然后将需要离线的数据序列化到本地(对于 web 浏览而言存储兼容顺序是indexdb—websql—localstorage)
  • Redux 与 IndexDB 结合

垃圾回收

这其实是一个非常不新鲜的话题了,这里有一点点新的知识

JS 的垃圾回收方法被称为 mark-and-sweep

  • 垃圾收集器找到所有的根,并标记
  • 遍历并标记所有来自它们的所有参考
  • 遍历到标记的对象并标记他们的引用
  • 一直这样,直到有未访问的引用
  • 没有被标记的所有对象都被删除

值得注意的是,JavaScript 引擎做了很多优化,使其运行速度更快,并且不会影响代码运行

  • 分代收集 —— 对象被分成两组:『新的』和『旧的』。许多对象出现,完成他们的工作并快速释放,他们可以很快被清理。那些长期存活下来的对象会变得『老旧』,而且检查的次数也会减少。
  • 增量收集 —— 如果有许多对象,并且我们试图一次遍历并标记整个对象集,则可能需要一些时间并在执行过程中带来明显的延迟。所以引擎试图将垃圾收集工作分成几部分来做,然后将这几部分逐一处理。这需要他们之间额外的标记来追踪变化,但是会有许多微小的延迟而不是大的延迟。
  • 闲时收集 —— 垃圾收集器只会在 CPU 空闲时尝试运行,以减少可能对代码执行的影响。

React 性能优化技巧

21 项优化 React App 性能的优化

  • 使用不可变数据
    • 应该将 React 的状态视为不可变
    • 对于数组,使用 concat 或 es6 的扩展符
    • 对于对象:使用 Object.assign 或 es6 的扩展符
    • Immutability Helper
    • Immutable.js
    • Seamless-immutable
    • React-copy-write
    • immer
  • 函数无状态组件/React.PureComponent
    • React.PureComponent/React.memo 对状态进行浅层比较
    • 比较原始数据类型的值,比较对象的引用
  • 生成多个快文件
    • SplitChunksPlugin
  • Webpack 使用 Production 标识生产环境
  • 依赖优化,比如
    • Moment.js 如果不需要支持多语言,使用 moment-locales-webpack-plugin 删除不需要的语言环境
    • lodash 使用 lodash-webpack-plugin 删除未使用的函数
  • React.Fragment 避免额外的 HTML 元素包裹
  • 避免在渲染函数中使用内联函数定义:会导致 prop diff 失败
  • 防抖与节流
  • 避免在 map 中使用 index 作为组件
    • 建议始终使用唯一属性作为 key,如果数据不支持,可以考虑使用 shortid 模块
  • 避免使用 props 来初始化 state
  • 在 DOM 元素上传递 props
  • 在使用 Redux Connect 时,同时使用 Reselect 避免组件频繁重新渲染
    • 创建自动缓存的数据
    • createSelector Api
    • 应对 React 开发原则:所有能计算得到的数据一定是通过计算得到的,Store 中只存储最原始的数据
  • 记忆化 React 组件
  • 使用 CSS 动画代替 JS 动画
    • CSS transitions
    • CSS animations
    • JavaScript
  • 使用 CDN
  • CPU 扩展任务中使用 Web Worker
  • 虚拟化长列表
    • react-window
    • react-virtualized
  • 分析打包体积
  • 考虑服务端渲染
  • Gzip 压缩

来源



留言