DOM Api-insertAdjacent 系列

起因

看朋友圈时候,有看到一个自己从来没有发现过的Api。

名字是 insertAdjacentHTML。

觉得很好奇,就试着看了看这个Api,发现在某些场景下,出奇的好用。

概述

insertAdjacentElement() 方法将一个给定的元素节点插入到相对于被调用的元素的给定的一个位置。

其他几个 Api 效果类似

语法与使用

element.insertAdjacentHTML

直接看这个就行了。

兼容性


Can I Use insert-adjacent? Data on support for the insert-adjacent feature across the major browsers from caniuse.com.

不难看出这是一套基础的 Api。

相应的其他 Api

  • Element.insertAdjacentHTML()
  • Element.insertAdjacentText()
  • Node.insertBefore()
  • Node.appendChild() (same effect as beforeend)

Intersection Observer 实现哨兵元素

起因

在自己之前写的博客《JavaScript实现列表无限加载》中,有提到使用 getBoundingClientRect 实现一个哨兵元素,从而实现无限加载等功能。

然而 getBoundingClientRect 的方法,用起来其实挺别扭的,因为每次滚动都要调用与检测,且必须自己书写检测函数,并不是很方便。
所幸的是,浏览器给我们添加了新的 Api,Intersection Observer

Intersection Observer

参照 MDN 上的解释,这个 API 的作用是:

Intersection observer API提供了一种方法,可以异步观察目标元素的交集变化与祖先元素或顶层文件。

使用场景

  • 当页面滚动时,懒加载图片或其他内容。
  • 实现“可无限滚动”网站,也就是当用户滚动网页时直接加载更多内容,无需翻页。
  • 为计算广告收益,检测其广告元素的曝光情况。
  • 根据用户是否已滚动到相应区域来灵活开始执行任务或动画。

简单来说,就是实现一个哨兵功能,当它出现在视图窗口,或者指定区域时,触发相应的回调。

Why?

关于为什么使用,MDN 也给出了详实的解释:

过去,交集检测通常需要涉及到事件监听,以及对每个目标元素执行Element.getBoundingClientRect() 方法以获取所需信息。可是这些代码都在主线程上运行,所以任何一点都可能造成性能问题。当网页遍布这些代码时就显得比较丑陋了。

兼容性

兼容性如下:


Can I Use intersectionobserver? Data on support for the intersectionobserver feature across the major browsers from caniuse.com.

只能说不容乐观

后续

这篇文章这儿不会讲具体的用法什么的,MDN 已经讲解的很详细了,搬运二手知识是没有意义的。
MDN 的教程,可以在文章最后的参考资料看到。

总结

这是个很强大的 API,很好用。
但是兼容性不容乐观也是它的缺点。

参考资料

React Native 性能优化

起因

自己使用 RN 已经很长时间了,但是对于 RN 的了解却不够深入,接下来一段时间会多关注性能优化相关的知识

课程:https://www.youtube.com/watch?v=NdUg_hjI30w

加载方式

Native端:Native init 与 JS init
JS端:Fetch Data, JS Render
Native: Native Render

启动流程

问题:

  1. ViewList

Native端 优化

预加载

具体实现:

增量更新

分包

具体架构:

JS端优化

加载速度快

本地缓存

缓存同时也能实现数据复用

非首屏异步加载

通过 Hack 的方式,使得原有需要5个节点的轮播图,改为只创建3个节点。
通过3个节点,模拟出轮播图的效果

滚动优化

ListView配置

体验优化

目的是为了,点击时按钮先出现点击效果,然后在下一帧执行一些耗时的操作。
视觉优先

性能与可控性强

组件划分

其他优化

了解网络请求的生命周期 - Resource Timing

起因

自己在学习性能优化的相关知识。
准备从网络请求这儿出发,配合着对 Chrome Dev Tools 的学习与使用,从而深入性能优化的相关知识。

关于 Resource Timing

所有网络请求都被视为资源。通过网络对它们进行检索时,资源具有不同生命周期,以 Resource Timing 表示。

下面是一个资源的生命周期

参考资料:了解 Resource Timing

解读 DOMContentLoaded 与 load 事件

起因

自己在最近的学习过程中,发现一些之前记住的知识点因为太久没有用到,而产生了模糊感。
所以在回学校的空隙时间,准备好好的理一下这些基础知识。
而自己又对性能优化比较感兴趣,所以这次从性能优化的角度出发。

两种加载事件

一般在加载的时候,会触发两种事件,一种是 DOMContentLoaded,一种是 Load 事件。
两种看起来差不多,其实内部还是有很大的区别的。

查看 MDN 的解释是:

当初始HTML文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,而无需等待样式表、图像和子框架完成加载。另一个不同的事件 load 应该仅用于检测一个完全加载的页面。

然而解释终究只是解释,中间其实还有很多谜题。

问题

接下来,就要抛出问题,然后一个个的去解决。

  1. 浏览器解析时的加载顺序是怎么样的?
  2. 什么时候触发 DOMContentLoaded,什么时候触发 Load
  3. 浏览器的脚本是否会影响 DOMContentLoaded,defer 和 async 属性的脚本呢?

解析时的加载与执行顺序

加载顺序

关于加载顺序,比较容易理解:

首先浏览器会解析 HTML,获取需要加载的外部脚本与样式表文件,并行下载。
同时,加载资源的顺序是有优先级的,比如 CSS/JS 文件就是高优先级,图片则是低优先级。
保证在连接数有限的情况下,能尽可能快的加载必要资源。

但,并不是下载完成就会直接执行的。执行顺序不等于加载完成的数据。

说到加载顺序,这儿还踩了一个坑。
就是在做实验的时候,加载了两个JS,但是第二个 JS 的文件的加载时间后面。

一开始也是有点百思不得其解,后面看了看 Network 面板,资源的加载时间。

发现这个资源大部分时间花在了 Queueing 上,于是翻 Chrome dev tools 的文档,得到如下解释:

然后发现自己碰见了经典的浏览器连接数限制问题。

具体的问题与解释可以看:浏览器允许的并发请求资源数是什么意思?

执行顺序

经过反复的实验与调试,还有资料的查找。这儿确定了浏览器资源的执行顺序,也明白了为什么人们常说性能优化时,CSS 文件要放在头部,JS 文件要放在 Body 底部,什么是阻塞渲染。

关于 CSS 与 JS 执行顺序,浏览器会严格按资源出现的顺序而执行。即使后续有资源提前完成了下载,也得等待之前的资源下载并执行完成才能执行。

关于 JS 是有些不同的, JS的执行,分为 CSS 之前的 JS 与 CSS 之后的 JS。
在CSS之前的JS立刻得到了执行,而在CSS之后的JS,需要等待CSS加载完后才执行。

阻塞渲染

这个其实也是一个老生常谈的问题,只是说之前的自己只是听过,谈过做过,但没有真正的去检测过实际效果。想想也是有些惭愧的。

CSS的阻塞渲染

默认情况下,CSS 被视为阻塞渲染的资源,这意味着浏览器将不会渲染任何已处理的内容,直至 CSSOM 构建完毕。

也就是说,在CSS没有加载和解析完成之前,浏览器是不会渲染任何内容的。

JS 的阻塞渲染

JS 有可能会修改 DOM。比如说 document.write ,所以在浏览器遇到 JS 文件时,会停止后面的解析,等待 JS 完成后,再去解析后面的文档。这样也就造成了阻塞渲染的效果。

加入在头部放一个超级大,运算超级复杂的 JS 文件,由于阻塞渲染,所以页面将会有非常长的白屏时间,带来的体验也非常的差。

结论

浏览器会并发加载 CSS/JS,但执行顺序还是按原来的依赖顺序来,比如 JS 的执行需要等待位于其前面的 CSS/JS 加载并执行完。

DOMContentLoaded 与 Load 事件

DOMContentLoaded

关于 DOMContentLoaded 事件
这儿可以用这个来做总结:

DOMContentLoaded 事件的触发时机是,当浏览器加载完页面,解析完所有标签,执行完 script 标签中的 JS 脚本,就会触发。
需要注意的点就在于,JS 的执行,需要等待它前面的CSS加载与执行,因为 JS 可能会依赖位于它前面的 CSS 计算出来的样式。

Load事件

而 Load 事件,MDN 上给的解释是:

load 应该仅用于检测一个完全加载的页面
当一个资源及其依赖资源已完成加载时,将触发load事件

浏览器这儿,则是:

当onload事件触发时,页面上所有的DOM,样式表,脚本,图片,flash都已经加载完成了。

脚本对 DOMContentLoaded 的影响

问题:是否会造成影响?
答案:会,原因上面已经说过了。只有执行完成JS后,DOMContentLoaded 事件才会被触发。

defer 与 async

JS 脚本加载时,可以选择 defer 与 async 属性。
这儿选择

>


  1. 没有 defer 或 async,浏览器会立即加载并执行指定的脚本,“立即”指的是在渲染该 script 标签之下的文档元素之前,也就是说不等待后续载入的文档元素,读到就加载并执行。

  2. 有 async,加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步)。

  3. 有 defer,加载后续文档元素的过程将和 script.js 的加载并行进行(异步),但是 script.js 的执行要在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成。

同时关于具体的表现,这儿有张图:

参考资料:

TypeScript - 不止稳,而且快

前言

关于 TypeScript 是什么,应该大部分人都已经知道了,但是在这儿,还是摘抄一下知乎的回答:

TypeScript 是 JavaScript 的强类型版本。然后在编译期去掉类型和特有语法,生成纯粹的 JavaScript 代码。由于最终在浏览器中运行的仍然是 JavaScript,所以 TypeScript 并不依赖于浏览器的支持,也并不会带来兼容性问题。

对于我个人而言, 使用 TypeScript 写项目已经有半年多了,中间有被 TypeScript 的配置与升级折腾到想砸电脑的时候,也有提前发现错误时的暗自庆幸,同时也有因为找不到类型定义文件而自己手写,提PR补全的时候。
总的来说使用 TypeScript 的这一年,什么感觉都有。但最后还是依然坚持使用 TypeScript ,因为其带来的效率提升是远远大于环境升级所带来的开销的。

稳定压倒一切

作为程序员,自然希望代码上线之后能安安稳稳的跑着,而不是突然报错崩溃啥的。
所以 TypeScript 之前最被看重的就是静态类型检查功能。

至于静态类型检查的作用,在知乎的另一个回答中有相关的回答:

静态类型检查可以避免很多不必要的错误, 不用在调试的时候才发现问题 (其实有的时候根本调试不出问题, 只是默默地把坑挖了, 说不定埋的就是个炸弹, 之前用 TypeScript 重写应用的服务器端程序, 写完之后就发现了不少暂时没有影响到运行的严重问题).

懒人的自我救赎

然而,我是个很“懒”的人,不愿在重复的事情上花上很多时间,也不喜欢像背书一下,背下来 Api 文档。更希望自己的时间能专注于核心业务的开发,而非边边角角的事情。

去年十月,在因为实际学习需要,接触越来越多前端框架时候,感觉整天的开发,大半的时间都浪费在了查文档上,特别是一些 React 的组件,props又多又长……每次写的时候,都得回去翻文档,简直绝望。

在这种每天近乎绝望的重复劳动下,我开始尝试去找解决方法,再到后来有一天接触了 TypeScript ,感觉到这就是自己想要的功能。
嗯……看中的不是 TypeScript 的稳定性,而是 TypeScript 的代码提示。

比如写 Node.js,使用 TypeScript 与 不使用的区别是这样的:

不仅不用手动翻阅 Api, 而且参数是什么也都一清二楚了。

且TypeScript 的代码提示是基于类型文件工作的,而相比于各个编辑器自己定义的代码片段来说,不仅有大量的志愿者去维护,更新及时,而且种类繁多,基本现有的流行框架类库,都有相应的类型定义文件。

所以自打用上 TypeScript 后,就过上了再也不用去费脑子记 Api 和参数的日子,开发效率与幸福感都得到了大大的提升。

不止稳,更要快

而 TypeScript 的快,不仅体现在代码提示上,同时也体现于重构、可读性和配套的编辑器上。

代码重构

在重构上,这个自己是有实际体会的,如果写JS,重构时候不小心改了啥,除了运行时候能发现,其他时候往往难以察觉,且 ESLint 也只能是排查简单的问题,所以出了BUG会非常麻烦。
而 TypeScript 不一样,重构了,重新编译一下就知道,哪里错了,哪里改动了不应该改的。对于我自己这种时不时就会重构的人来说,省时又省力。

可读性

可读性上,TypeScript 明显占优,查看开源代码时,如果注释不是很完善,往往会看的云里雾里,而 TypeScript 在同等条件下,至少有个类型,能让自己更容易明白代码的参数、返回值和意图。

编辑器

这个是不得不提的部分,因为 VSCode 实在是太方便了,性能也高,且编辑器自身保持着一个高速的开发与迭代状态,时不时就能感受到 VSCode 开发团队的诚意和其所带来的惊喜。

因为都是微软家产品的原因,VSCode 对 TypeScript 的支持也相当完善。各种插件也层出不穷,基于 TypeScript 做的 Automatic Type Acquisition 功能使得 JavaScript 的用户也能享受到详细的代码提示功能,这一点上比 Sublime 等编辑器方便了很多。

关于 VSCode 编辑器的上手与配置,可以看阎王发表的这篇文章:如何快速上手一款 IDE - VSC 配置指南和插件推荐

解放自己,专注业务核心开发:TypeScript 编辑器插件推荐

当然,每次写 TypeScript 时,依然会遇到一些烦心的问题和重复的劳动。
比如说,TypeScript的类型定义文件是需要手动下载相应的 @types 包的,虽然相比于之前的方式已经进化了很多,但是每次还要重复,依然会觉得繁琐。
所以下面会推荐自己常用的几个插件,把自己从繁琐无趣零成长的工作中解放出来。

TypeScript Importer-告别手动重复写import的日子

插件地址:TypeScript Importer

这个是我最喜欢的插件,具体的作用,一图胜千言:

在长长的路径里,导入另一个文件夹深处的模块,那种感觉是绝望……
每次都要重复的import,每次都要重复的判断路径,每次都要重新写一遍import……

虽然工作量也不大,但是确实会影响心情和效率。

Types auto installer - 自动安装相应的类型定义文件

插件地址:Types auto installer

在之前,你安装一个模块并在 TypeScript 运行两段命令。
以lodash为例:

1
2
npm i lodash --save
npm i @types/lodash --save

当然,你也可以合并到一句话去写。
虽然工作量不大……但是架不住量多啊……每开一个项目都得来这么一次,简直绝望……

所以当时就想着自己写一个自动安装类型定义文件的小工具,后面确实也写出来了,只是再后来又发现 VSCode 有这个插件,功能也很完善,就用它的了。
插件的作用很简单,就是当你运行:

1
npm install --save lodash

它会自动执行:

1
npm install --save @types/lodash

与此同时还有一键下载安装所有 package.json 依赖类型定义文件的功能,可以说是非常方便了。

Sort Typescript Imports-给你import的模块们排序

插件地址:Sort Typescript Imports

同样,话不多说,一图胜千言:

这是一个看起来没什么作用的插件……因为其实 import顺序是否整洁有序好像对开发效率啥的并没有很大的提升。
但这是一个你接受了它的设定,就可能会觉得十分有趣的插件……

具体的作用就是,让你的 imports 更有顺序,相近文件夹的排列在一起。看起来会更好看一些。

Emmm……如果一定要说作用的话,就是更好看一些吧……很符合我这种有轻微代码洁癖的人的心态……

为什么要关注这个?

自己在知乎上有回答过一道问题:《最近一年前端技术栈哪些技术点最困扰你?》
我的回答是:

开发环境的搭建。
没有官方的cli,或者自己要做一些拓展(比如用ts)真的非常烦人。
各种报错,而这种在开发环境上积累的经验和踩的坑是价值非常低的。(因为基本最后翻官方配置文档都能解决)
耗时长,学习价值低,更新速度快。

在这儿,也是同理。
用 TypeScript 和相关插件所解决的问题,都是一些繁琐、无趣、零成长的工作,而且还影响心情。
有这个时间,为什么不多陪陪女朋友,多学点东西,多解决一些有意思的问题呢?
所以这种可以让计算机解决的问题,就让计算机去解决吧~

参考资料

状态决定视图——基于状态的前端开发思考

前提

在现在的前端社区,关于MVVM、Model driven view 之类的概念,已经算是非常普及了。React/Vue 这类框架可以算是代表。
而自己虽然有 React/Vue 的使用经验,也了解 MVVM,状态机等核心概念,但是却一直没有很好的应用。

直到前几天接手一个组件开发的需求,写之前尝试细细分析时,才突然想通这之间的联系。
Emmm……内容比较浅,并不是什么了不得的神兵利器。更多的是个人的感悟。

个人困惑

自己在前一段时间里,陷入了如何写好代码的困惑之中,在学习了《重构》、《代码整洁之道》等知识之后,确实有一些好转。但是写代码总是要重构才能好一些些,也是很麻烦的事情,于是就有了如下的思考。

前端与状态

现在的前端开发中,对于状态的管理是重中之重。
而使用 React/Vue 这类 MVVM 框架,通过组件化、自动绑定等方式,能有效降低前端开发时的复杂度。

MVVM

提到状态就不得不提到MVVM框架,而MVVM的框架的核心,并不是双向绑定或者依赖收集什么的,而是:状态决定视图
用代码描述就是:

1
View = ViewModel(Model)

理想情况下,ViewModel是纯函数,给定相同的Model,产出相同的View。

随着前端的发展,Web应用的状态管理愈发复杂,然而由于前端的一些特性:

  • 代码开源
  • 请求透明
  • 不保存用户数据

也决定了前端只负责整个Web应用上的视觉和交互层,凡是涉及到数据的,后端必然要做严谨的校验,不相信任何前端的请求。
所以前端的核心工作,就是提供一个友善的人机交互的操作界面。当然,这也符合广义上的前端定义。

而 MVVM 的出现,能有效的提高前端开发的效率和品质,从而得到了大规模的发展与应用。

复杂度

在《代码大全2》这本书中,有句让我印象深刻的话:

软件工程的本质即是管理复杂度。

细细想来,也确实是如此。
前端开发自然也属于软件开发,管理复杂度恰恰也是前端目前的核心问题。

有限状态机

那么如何更好的管理前端软件的复杂度? React 的状态机思想给出了自己的答案。
状态机是我在学习计算机中,时常听到的一个概念,比如学 React 时,会提到 React 就是个状态机,听团队关于编译原理的分享时,也会听到状态机。所以就去专门补习了这个概念。

有限状态机在维基百科上的描述如下:

有限状态机(英语:finite-state machine,缩写:FSM)又称有限状态自动机,简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。

有限状态机并不是一个复杂的概念

简单说,它有三个特征:

  • 状态总数(state)是有限的。
  • 任一时刻,只处在一种状态之中。
  • 某种条件下,会从一种状态转变(transition)到另一种状态。
    它对JavaScript的意义在于,很多对象可以写成有限状态机。

启示

随着对状态决定视图与状态机两个概念的学习与思考,于是有了新的思路:

状态决定视图,Action则负责完成状态间的转移,那么写好代码的核心在于,用最恰当的状态去描述界面,用最恰当的动作去完成状态间的转移。

Emmm……很简单的概念,但是自己之前一直没有想的很清楚。

总结

随着对这个概念的了解,自己在开发时的思路也愈发的清晰化。
自己现在写代码之前,会思考一系列问题,想清楚再下手:

  • 这个页面有几种状态(初始化状态?成功状态?失败状态?出错状态?)
  • 描述这些状态需要什么参数
  • 在什么时候转变状态,需要改变哪些部分

把这些问题想清楚了,剩下的工作就是跟着思路,完成数据与UI部分。
以上就是自己的思路了,如果各位有什么建议的话,欢迎和我交流呀 😊 ~

参考资料

浅析Node与Element

起因

起因有二:

  1. 在看winter老师的分享:《一个前端的自我修养》时,有注意到这么一幅图,里面有写childNodechildren属性。
    node和element

  2. 昨天有学弟问起我,能否自己定义一个所有元素节点通用的方法,就像数组可以用 Array.prototype.xxx 来添加一个所有数组的方法。
    于是发现自己对于Node和Element的概念其实还不太清晰,所以上MDN看了看,写篇博客沉淀一下。

Node

Node类继承于EventTarget,下面是MDN给的解释。

Node在这儿指DOM节点,其中包括了我们最常见的元素节点,比如 div/p/span 之类的。除此之外还包括了 Document/Comment 之类的节点。
一个节点的类型,可以通过其nodeType类型查看到,具体的类型则可以看下图:

高频的属性与方法

Node定义了一些经典的节点操作方法,我这儿画了个简单的图,并没有列出全部属性

写前端的同学,日常应该都会频繁的用到这些方法。

当然,也有可能会遇到踩坑的现象。比如说在使用nextSibling完成遍历操作的时候,nextSibling有可能会返回的是文本节点而非元素节点,那么在调用一些元素节点的属性或方法时(如 innerHTML),就会出错。这也是为什么一定要区分清楚两种节点的原因。

Element

至于说Element, 大家肯定就熟悉多了。学前端入门的时候,就用过的 document.getElementBy* 的 Api,取出来的就是Element。
Element在MDN的解释如下:

这个其实大家日常的使用也非常多,就不多做解释了。

Node与Element的关系

至于Node与Element的关系,从继承方面讲可能为清晰很多。

Element 继承于 Node,具有Node的方法,同时又拓展了很多自己的特有方法。
所以在Element的一些方法里,是明确区分了Node和Element的,比如说:childNodes, children, parentNode, parentElement等方法。

而Node的一些方法,返回值为Node,比如说文本节点,注释节点之类的,而Element的一些方法,返回值则一定是Element。
区分清楚这点了,也能避免很多低级问题。

如何给所有DOM元素添加方法

由于JavaScript原型的特点,我们只要给原型添加方法,就可以在所有继承的子元素中调用此方法。

当然,在这儿你选择污染Element的原型也好,Node的原型也罢,都是可行的。
具体看你要调用这个方法的元素,是纯元素节点还是会有别的一些节点。
按需取用就行。

DEMO:

1
2
3
4
HTMLElement.prototype.sayHi = () => alert('hi')
const p = document.querySelector('p')
p.sayHi()

总结:

  1. Node是节点,其中包含不同类型的节点,Element只是Node节点的一种。
  2. Element继承与Node,可以调用Node的方法。
  3. 给所有DOM元素添加方法,只需要污染Node或者Element的原型链就行。

参考资料:

Redux源码阅读笔记(2) - Redux的原理与适配

范式

与其说Redux是一个工具类库,我更想说它是一套处理状态与数据变更的范式。官方有明确的一些规则,社区也累积了很多最佳实践。

从谷歌搜索来看,这是个很有趣的现象。
至于个人看法则是根据项目和团队实际情况来选用Redux方案,而非强制上马,避免未来可能会背的技术债。

Redux Flow

Redux是单向数据流,其工作流如下图所示(看不懂或者没用过的小伙伴可以先看看Redux文档,用一用):

在Redux中,Store中state的改变只能由action触发,经过reducer,从而改变state,因为在Redux中,是不允许直接改变state的。至于这一点,下一篇博客会谈到为什么。

这样的特性也就决定了,我们需要编写大量的Action和Reducer代码,从而给人一种很繁琐的感觉。从而带来一些不必要的开销与麻烦。因此是不怎么适合小项目的。

取舍

至于这一点,社区则有一篇文章讲述了这个:You Might Not Need Redux

而Redux与Flux的作者也表达过:

我想修正一个观点:当你在使用 React 遇到问题时,才使用 Redux。
你应当清楚何时需要 Flux。如果你不确定是否需要它,那么其实你并不需要它。

在Redux的文档中,则有:

在打算使用 Redux 的时候进行权衡是非常重要的。它从设计之初就不是为了编写最短、最快的代码,他是为了解决 “当有确定的状态发生改变时,数据从哪里来” 这种可预测行为的问题的。它要求你在应用程序中遵循特定的约定:应用的状态需要存储为纯数据的格式、用普通的对象描述状态的改变、用不可更新的纯函数式方式来处理状态变化。这也成了抱怨是“样板代码”的来源。这些约束需要开发人员一起来努力维护,但也打开了一扇扇可能的大门(比如:数据持久性、同步)。

原理

至于Redux实现的原理,就得看它的源代码了,就像上一篇博客所说的,Redux的源代码简洁且有力。
今天则从Redux最重要的方法createStore()说起,来看看Redux的内部实现。

下面是简化后的代码,提取了Redux的createStore()的主干部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function createStore (reducer, preloadedState, enhancer) {
let currentReducer = reducer
// Store state
let currentState = preloadedState
let currentListeners = []
let nextListeners = currentListeners
let isDispatching = false
function getState () {}
function ensureCanMutateNextListeners () {}
function subscribe (listener) {}
function dispatch (action) {}
function replaceReducer () {}
return {
dispatch,
subscribe,
getState,
replaceReducer
}
}

下面则针对各个问题,来解读这份源代码。

1. 为什么不能直接修改state,怎么做到的

createStore函数中,初始化state为preloadedState,如果preloadedState不存在则为undefined。
而我们要拿到store的state,只能通过store的getState函数做到。函数内容如下:

1
2
3
4
5
6
7
8
/**
* Reads the state tree managed by the store.
*
* @returns {any} The current state tree of your application.
*/
function getState () {
return currentState
}

至于不可直接更改的原因则是currentState作为私有变量,只能被内部访问,没有暴露在公共接口中。
这也算是闭包的一种应用吧。

这儿也解决了一个我的小疑惑,就是频繁调用getState()开销如何?
现在看来,也只是和直接读取 state 的开销差不多,微乎其微。

2. 怎么通过subscribe来Redux state的更新

这点的话,当时看了看源代码,便了然了。
Redux内部实现了个订阅者模式,subscribe则是添加订阅。
而在dispatch函数中,底部有这么一段:

1
2
3
4
5
const listeners = currentListeners = nextListeners
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}

3. state是怎么更新的

这个问题则涉及到了 dispatch函数,在 dispatch 函数内部有这么一段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function dispatch(action) {
try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
const listeners = currentListeners = nextListeners
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
return action
}

也就是说当使用dispatch函数触发action时,currentReducer计算并返回新的state,完成这次更新。

令我感到注目的是,这儿用了 try..catch..finally这种方式,但是并没有catch部分,于是自己实践了一下:

1
2
3
4
5
try {
throw new Error('Error')
} finally {
console.log('Finally')
}

对此控制台的结果如下:

也就是说在reducer计算新的state时,错误会抛出,但是函数却能继续运行,不至于直接崩溃。
感觉这个方法对于某些特定场景还是很有用的。

4. ensureCanMutateNextListeners是什么

createStore的源代码中,有一个函数是 ensureCanMutateNextListeners,内容是:

1
2
3
4
5
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}

很多函数在执行时都调用了它,开始还不是很理解为什么,然后找了找别人的解释:

同时有一个辅助方法ensureCanMutateNextListeners()。这是考虑到,在执行某个监听函数的时候,可能会添加新的监听函数,或者取消某个监听函数。为了让这些改变不影响当前的监听函数列表的执行,因此在改变之前,先拷贝一份副本(即nextListeners),然后对该副本进行操作,从而所有的改变会在下一次dispatch(action)的时候生效。 – Redux入门

Redux的适配

Redux从来就不想只做React的工具,他的目标是和各大框架配合使用。而事实上各大框架也有Redux的适配库。
那么自己的问题就在于Redux怎么和别的工具去做一个适配。

一开始不理解,后面看了源代码,想了想这种观察者模式,与 createStore 返回的store对象所具有的内容,心里便了然了。

框架通过Redux的subscribe方法,订阅state更新,并通过dispatch方法,触发state更新。
那么具体框架如何应用数据,就是框架内部业务层面的事情了。每个框架根据自身特点和需求处理的方式都不一样。有兴趣的可以看看 react-reduxrevue

Redux源码阅读笔记(1) - Why Redux?

cover

起因

日常的习惯里,就有阅读开源项目源代码的习惯。
之前的项目中则大量的应用了Redux作为状态容器,也积累了一些问题,所以今天试着阅读Redux源代码,去解决自己的困惑。

在网络上Why ReduxRedux原理分析Redux源代码分析之类的文章层出不穷,之所以写这篇文章的原因是为了解决自己在使用Redux时说产生的一些好奇与困惑。并非科普文,如果你有遇到类似的问题,不妨看看~

问题

一直认为,带着问题去阅读源代码,比漫无边际的阅读要有效的多。就像自己很喜欢的那句话一样:

“师必有名。” – 《礼记·檀弓下》

下面则是自己在使用Redux时遇到的一些问题。

  • 为什么要用Redux,解决了什么问题?
  • Redux的原理,如何与各类框架去做一个适配
  • 为什么每次都要返回全新的State?而不能直接在原来的State做修改?
  • Redux中间件相关的问题,Redux-saga,Redux-thunk等经典中间件是怎么实现的

阅读的Redux版本是Github拉取的最新版,commit号是:27ab1697d82175e00f34508b3e76d2f17ed894bd

初见

Redux的源代码,严密,简洁且有力,而且注释超级多,所以读起来还是很舒服的。
当然在解释源代码之前,需要先解决第一个问题:Why Redux?

1. Why Redux?

对于Why Redux?这个问题,很多人都有自己的理解。
对于我而言,这个问题的答案是如下两点:

  1. 前端应用的复杂化,需要一个可靠的状态管理器来管理前端状态
  2. React/Vue等MVVM框架的兴起,状态决定视图,组件之间数据共享,跨级交流的困难

这两点互相作用互相影响,我们需要一个可靠的全局状态管理器,管理全局和共享状态,解决组件数据共享,跨级交流困难的问题。

状态的复杂化

至于前端状态的复杂化,在Redux官方的文档中,有这么一段话:

随着 JavaScript 单页应用开发日趋复杂,JavaScript 需要管理比任何时候都要多的 state (状态),管理不断变化的 state 非常困难。如果一个 model 的变化会引起另一个 model 变化,那么当 view 变化时,就可能引起对应 model 以及另一个 model 的变化,依次地,可能会引起另一个 view 的变化。直至你搞不清楚到底发生了什么。state 在什么时候,由于什么原因,如何变化已然不受控制。 当系统变得错综复杂的时候,想重现问题或者添加新功能就会变得举步维艰。

具体有多复杂,每个人的业务场景都不一样,这里就不赘述了。

React/Vue等MVVM框架的兴起

React/Vue等MVVM框架发展这么久了,早已为大部分前端工程师所熟知。而Redux/Flux/Vuex/Mobx等全局状态管理器,则是配合React/Vue等MVVM框架来完成大中型应用的开发。

这儿的标题是React/Vue等MVVM框架的兴起,也就是我认为Why Redux这个问题的答案之一就是这个。

世间万物都是有联系的,对重型Web Application开发/更简便的前端开发的需要促进了各类前端框架的出现,而各类前端框架自己也存在一些痛点,如React中组件层级嵌套过深,组件交互麻烦等问题则推进了Redux这类状态管理容器的出现。

如果前端每天的需求只是做一些简单的展示页,那么React/Vue这类框架很有可能只是昙花一现或是束之高阁,因为没有实际的需求。此时引进Vue/React的成本是大于收益的。


React/Vue等MVVM框架的核心特点,那就是状态决定视图,翻译成伪代码就是:

1
f(state) => view

使用React/Vue时候,在数据管理方面是有痛点的。React为View层,本身不具有好的数据管理方式,而Vue则相似,均需要配合Redux/Vuex来实现数据管理。

具体的痛点,在这儿,Vue的官方文档给了详尽的解释:

一个状态自管理应用包含以下几个部分

  • state,驱动应用的数据源;
  • view,以声明方式将state映射到视图;
  • actions,响应在view上的用户输入导致的状态变化。

以下是一个表示“单向数据流”理念的极简示意:

但是,当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:

  • 多个视图依赖于同一状态。
  • 来自不同视图的行为需要变更同一状态。

对于问题一,传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。对于问题二,我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致无法维护的代码。

来源:Vuex 是什么?

而Redux,就是为了解决上述问题而来。