疫情期间,我通过项目重学了小程序

前言

这段时间,上海疫情爆发的很严重,3月12日起笔者就被封在小区,至今已有40多天。这期间经历了面试,抢菜,写文章,到现在又捡起了小程序

笔者近几年用 github 来记录年度计划,并每周设立计划执行

年度计划

像前端知识地图、博客、读书还好,都是日常基本功,但是独立作品今年都没有动作,按照进度,上半年怎么也要有一个,于是乎,就想着从想法里拿出一个做成项目

正文

一开始我是想弄我的几行字,因为几行字是H5版本,所以想再做一版小程序,但做完之后感觉有点简单,就打算再做一个

一个机缘巧合的情况下,我想起了三年前尝试做的独立项目——NextDay

邮箱记录

想想,这个页面只有一页,但细节多,还有自定义组件,可以从中学习到做一个小程序会碰到的坑,于是乎,我的小程序之旅有一遭没一遭的做了起来

这里记录笔者做小程序时的困难和解决方案

  • 手画原型

    • NextDay App 为原型展开
  • 小程序开发

  • 自定义导航条

    • 全屏模式下,导航条不让其出现 "navigationStyle": "custom"
    • CSS 也要配合的做,不让他上下滑动postion: fixed
  • 调试接口

    • 需要授权,拿到 Partner NamePartner Secret
    • 也正是因为它严明要开源,所以开源
    • 需要在 header 处加上 Date 和 authorization,authorization 需要加上 md5 加密,可去开源处看代码
  • 主页(主画面)设计、功能

    • swiper 滑动功能接入。接口返回数据是从前几日到今天,如何在 onload 处显示今日数据?swiper 的current设置为今天
    • 布局上采用绝对定位,适配单位上采用rpx和百分比
  • 引入 npm 包

    • 返回的接口数据和页面上的数据是不同的,需要我们从中做转换,遂引入 dayjs
  • 注册小程序

    • 小程序流程
    • 文案
  • 阴影处理

    • 前端这边的设计,text-shadow 属性
  • 上拉出现设置和分享

    • 抽屉组件,点击分享Icon 分享
  • 分享给朋友和朋友圈设置

    • onShareAppMessage 和 onShareTimeline
    • 分享给朋友时,采用的图片应该是 5:4 比例尺寸的图,使用接口返回的小图,意味着需要每次加载时需要记住当前的所在组,每次滑动时都需要改变当前的所在组
  • 手机适配

    • 接口图片有多种,有适配浏览屏的图片和普通图片
    • wxml 中进行条件判断,这里注意不能加空格
  • 接口云端化

    • 因为 NextDay 的接口是 HTTP 协议,小程序不支持 HTTP 协议的,所以就用云开发做转发
    • 主要参考资料:官网微信学堂视频
  • 代码重构

    • 文件布局,设计,云开发,小程序文件,小程序中又有组件、npm 包等功能
    • 接口
  • 设计logo

    • 不用设计,去官网找即可
  • 字体改造、字体适配

    • 需要找到和 App 中一样的字体,比较难,采用字体识别 技术,两种字体,数字日期采用华为最细字体,正文描述字体采用日文字体GothicMB(和 App 中一致)
    • 截图对比,调整大小
  • 点击显示下载按钮

    • 如下所示,找到也没用
  • 下载图片功能

    • 失败,不能下载,图片域名不是 HTTPS,不支持下载,如果把图片放在云函数上免费额度肯定不够,后续再想想需不需要加,有没有必要加
  • 设置主页 关于版本、鸣谢

    • 多一个关于页面,新版本去除,感觉没必要
  • 引入自定义组件

    • 抽屉组件,点击弹出
    • 设置
  • 上拉模糊

    • CSS 样式 backdrop-filter
  • 搜索NextDay 关键字搜索不到

    • 应该是刚上架不久,又没做推广,所以搜不到
    • 要全拼NextDay101
  • 消息提醒

    • 希望每一天能提示弹出,新的想法,不知道会不会打扰到别人,第一版就没做

如要跑通这个项目,首先要去申请 PartnerName 、 Partner Secret,这样就能跑通项目,具体的答疑已经在 NextDay 的README中回答,如还有其他疑问,可在Issuse 区提出

效果截图

GIF动态

GIF

截图

截图1

scrrenshot2

scrrenshot3

小程序

小程序码

项目地址:https://github.com/johanazhu/nextday101

芝麻开门,显示全文!

网红面试题:从输入 url 到看到页面发生了什么

帝王需要约束,所以有了帝王约束力

流程图

这题扎眼看上去没问题,无非是 HTTP 请求到浏览器渲染,但可以聊的东西很多。我想它的执行顺序是,用户输入——开始导航——HTTP 请求——浏览器渲染。其中用户输入、开始导航、浏览器渲染是浏览器方面的知识点,HTTP 请求是 HTTP 方面的知识点

以下就是从输入 url 到看到页面的整个流程图

从url输入到页面渲染

前言

了解”开始导航”之前,需要先知道浏览器架构,简单来说,现代浏览器由 1 个浏览器主进程、1 个 GPU 进程、多个渲染进程、多个插件进程、网络进程、音频进程、存储进程组成

下图是李兵在《浏览器工作原理与实践》中所示,展示 Chrome 浏览器的架构

目前的浏览器架构

以及未来现代浏览器架构示意图:

未来现代chrome浏览器架构

文章现代浏览器内部揭秘中有一张图,是这样描述的

现代浏览器内部解密

图中表明浏览器主进程包含了 UI 线程、网络线程、存储线程,与李兵的观点有所不同。那以谁为准呢?以时间为准,李兵的专栏是 19 年所写,而《现代浏览器内部解密》是 18 年的文章,站在 2022 年的背景,现代浏览器,UI、网络、存储等都已升级为进程,而非是浏览器主进程中的线程

用户输入

当用户在地址栏中输入一个字符串时,地址栏会判断输入的关键字是搜索内容,还是请求的 URL

  • 如果是搜索内容,地址栏会使用浏览器默认的搜索引擎,合成新的带搜索关键字的 URL

    • 例如在 chrome 中搜长泽雅美
    • 在chrome中搜长泽雅美
  • 如果输入内容符合 URL 规则,例如输入azhubaby.com,那么地址栏会根据规则,把这段内容加上协议合成完成的 URL,如 https://azhubaby.com

当用户输入关键字并键入回车之后,意味着当前页面将替换为新的页面,此时浏览器中有个 API——beforeunload,它允许页面在离开之前触发是否一个确认对话框。这里使用此 API,可让浏览器不再导航

// 监听离开页面前的事件
window.addEventListener("beforeunload", (event) => {
  event.preventDefault();
  event.returnValue = "";
});

可在这里看看 beforeunload 的demo

从浏览器架构分工上讲,当用户输入字符串时是 UI 进程(老一点的浏览器是浏览器主进程)在运作

开始导航

当敲下 Enter 键时,UI 进程将指挥权交接给了网络进程。网络进程接受请求指令前,会先查找本地缓存是否有缓存。如果有缓存该资源,那么直接返回资源给浏览器进程;如果在缓存中没找到该资源,那么则正式进入 HTTP 请求阶段

关于 HTTP 缓存方面的知识可以看看这篇——面试常客:HTTP 缓存

HTTP 请求

之前写过一篇TCP/IP 协议及网络分层模型,讲述了 TCP/IP 网络分层协议,它就像搭积木一样,每一层需要下一层的支撑,我们的 HTTP 请求是其 HTTP 协议的应用,需要先连接传输层(TCP)以及更底层网络互连层(IP)

TCP/IP 网络分层模型

而 IP 从哪里来,通过 DNS, 使其域名 和 IP 做映射

我们使用倒推法可以理清“路线”:

HTTP 请求 —— HTTP 协议连接 —— TCP 协议连接 —— IP 协议连接 —— 需要知道 IP——DNS 做域名/IP映射

所以进入 HTTP 请求的第一步是 DNS 解析

DNS 解析

这里对 DNS 不做过多概述,简单来说,它的作用是用域名代替 IP 地址,符合人的记忆。输入du.azhubaby.com ,表示 IP 地址 47.102.152.19 ,你可以在命令行中 ping 一个域名,来求证一下结果

ping域名

HTTP 请求之前的第一步是判断 DNS 中是否有缓存,如果有,直接返回 IP 地址;如果没有,则进行 DNS 解析,并把结果 IP 缓存到 DNS

有了 IP 地址后,IP 层连接成功,接下来就是 TCP 传输层

TCP 连接

这里要看 HTTP 协议的版本,如果是 HTTP/1.1 的话,就要考虑 TCP 队列否饱满,因为 HTTP/1.1 最多允许一个域名连接 6 条 TCP,太多了就要在等待 TCP 队列中排队;如果是 HTTP/2 的话,那就没事,它允许 TCP 并发

这里还要考虑到如果协议是 HTTPS 协议的话,还需要建立一条 TLS 连接

等真正 TCP 连接时,就联想到网红面试题:三次握手、四次挥手

三次握手、四次挥手

为什么是三次握手和四次挥手,因为只有这样才能让双方(客户端和服务端)知道彼此的接收能力和发送能力是没问题的

http-tcp-three-handshakes

步骤为:

  • 客户端提出建立连接,发出客户端 seq:seq=client_isn
  • 服务端收到消息后返回 ack=client_isn+1 和服务端 seq:seq=server_isn
  • 客户端收到后返回ack=server_isn+1 表示收到了

可以理解为男女双方确认关系,男女双方要结婚,怎么办?先见父母得到父母认同,之前听过这样一句话:得不到父母祝福的婚姻是不幸福的(当然,不见父母直接结婚的也有,但不主流)

  • 男方提出去女方家,带上见面礼 seq:seq=男方的诚意
  • 女方家收到见面礼后返回(给男方)红包 ack=我们认可你啦 以及女方去男方家也带上见面礼 seq:seq=女方的诚意
  • 男方家收到见面礼后返回(给女方的)红包 ack=server_isn+1

这个叫确定关系。所以要又来又回三次,双方都确保知道对方的诚意和自己的诚意

那什么是四次挥手呢?

在断开之前,需要进行四次挥手

http-tcp-four-handshakes

为什么要有四次挥手?

主要是为了确保双方都知道对方断开连接

具体步骤为:

  • 客户端第一次发送消息给服务端告诉它需要断开连接
  • 服务端收到消息后返回消息告诉客户端:知道了,为了确保服务端收到了之前所有的 HTTP 请求,服务端需要等一等再断开连接
  • 服务端确认所有的 HTTP 请求都收到了,主动发消息给客户端:我这边所有的请求都处理完了,我也可以断开连接了
  • 客户端收到这个请求后,返回消息告诉服务端:我知道,断开连接吧

主要是为了确认双方的接收能力和发送能力是否正常、制定自己的初始化序列号为后面的可靠性传送做准备

可以理解为一对男女要分手

  • 女方提出分手,说你对我不好,我要分手
  • 男方觉得需求合理,同意分手,但分手之前要把联系方式、合照、各种乱七八糟的的事情算清楚再分手
  • 男方理清楚后,主动发消息给女方,说这边都处理清楚了,以后你是你,我是我,我们可以分手了
  • 女方收到消息后,返回告诉男方:我知道了,分手吧

于是乎,它们就断了,分手手续完成。具体详细的信息可看猿人谷的面试官,不要再问我三次握手和四次挥手,一个字:细

发送 HTTP 请求

TCP 连接已经通了,现在正式发送 HTTP 请求,这里又有的聊了,如 HTTP 的报文内容、请求头、响应头、请求方法、状态码等知识点

首先 HTTP 的报文结构由 起始行 + 头部 + 空行 + 实体组成,简单来说就是 header+body,HTTP 的报文可以没有 body(get 方法),但必须要有 header

请求头由请求行 + 头部字段构成,响应头由状态行 + 头部字段构成

请求行有三部分:请求方法、请求目标和版本号

  • 例如 GET / HTTP/1.1

状态行也有三部分:版本号、状态码和原因字符串

  • 例如 HTTP/1.1 200 OK

在浏览器中,打开 F12,在 NetWork 中任何一个请求中,你都会看到这样的结构

报文结构

这里我们也常会遇到一些例如 GET 和 POST 请求方式的区别、HTTP 状态码等相关的衍生问题

GET 和 POST 请求方式的区别

  • 从缓存角度看,GET 会被缓存,POST 不会被缓存
  • 从参数角度看,GET 通过在 URL 的”?”后以 key=value 方式传参,数据之间以“&”相连接;POST 则要将数据封装到请求体中发送,这个过程不可见
  • 从安全角度看,GET 不安全,因为 URL 可见;POST 较 GET 安全度高
  • 从编码角度看,GET 只接受 ASCII 字符,向服务器发送中文字符可能会出现乱码;POST 支持标准字符集,可以正确传递中文
  • 从数据长度的限制看,GET 一般受 URL 长度限制(URL 的最大长度是 2048 个字符),POST 无限制

HTTP 状态码

RFC 标准把状态码分成了五类 ,用数字的第一位表示分类,而 099 不用,这样状态码的实际可用范围就大大缩小了,由 000999 变成了 100~599。

这五类的具体含义是:

  • 1××:提示信息,表示目前是协议处理的中间状态,还需要后续的操作;
  • 2××:成功,报文已经收到并被正确处理;
  • 3××:重定向,资源位置发生变动,需要客户端重新发送请求;
  • 4××:客户端错误,请求报文有误,服务器无法处理;
  • 5××:服务器错误,服务器在处理请求时内部发生了错误。

目前 RFC 标准里总共有 41 个状态码

101 - Switching Protocols,客户端使用 Upgrade 头字段

200 - 请求成功

204 - 无内容,服务器成功处理了请求,但没有返回任何内容。

206 - 一般用来做断点续传,或者是视频文件等大文件的加载

301 - 永久重定向

302 - 临时重定向

304 - 未修改协商缓存,返回缓存中的数据。它不具有通常的跳转含义,但可以理解成 重定向到缓存的文件(即缓存重定向)

400 - 请求中语法错误

401 - 未授权

403 - 服务器收到请求,但是拒绝提供服务,即资源不可用

404 - 无法找到请求资源

408 Request Timeout - 请求超时

414 - 请求 URI 过长(如图一新浪常有)

500 - 服务器内部错误

501 - 尚未实施:服务器不具备请求功能

502 - 网关错误

503 - 服务器不可用,主动用 503 响应请求或 Nginx 设置限速,超过限速,会返回 503

504 - 网关超时

这里要对 304 做一下说明,当请求头 If-Modified-SinceIf-None-Match 中判断修改时间是否一致(或唯一标识是否一致),是,则返回 304,使用浏览器内存中的本地缓存;不一致则说明要更新,继续请求资源放回给客户端,并带上 Last-ModifiedETag

请求方式

HTTP/1.1 规定了八种方法,都必须是大写形式

  • GET:获取资源,可以理解为读取或者下载数据。只有 GET 请求才能起到缓存效果
  • HEAD:获取资源的元信息
  • POST:像资源提交数据,相当于写入或上传数据
  • PUT:类似 POST
  • DELETE:删除资源
  • CONNECT:建立特殊的连接隧道
  • OPTIONS:列出可对资源实行的方式
  • TRACE:追踪请求 - 响应的传输路径

浏览器渲染

当 HTTP 请求完毕后,断开 TCP 连接,将资源返回给客户端(浏览器)。此时浏览器要判断是否与打开的网站是同一个站点。因为如果是同一个站点的话,则可使用同站点的渲染进程渲染页面,如果不是,浏览器则打开新的渲染进程解析资源

浏览器渲染的大致流程如下图所示:

浏览器渲染大致流程

我们可以将页面渲染分为三个步骤:

解析

  • HTML 被解析为 DOM 树,CSS 被解析为 CSS 规则树,JavaScript 通过 DOM API 和 CSSOM API 来操作 DOM Tree 和 CSS Rule Tree

渲染

  • 浏览器引擎通过 DOM Tree 和 CSS Rule Tree 构建 Rendering Tree(渲染树),这其中进行大量的 回流(Reflow) 和 重绘(Repaint)
  • 回流和重绘
    • 回流:意味着元件的几何尺寸变了,需要重新验证并计算 Render Tree
    • 重绘:屏幕的一部分需要重画,比如某个 CSS 的背景色变了,但元件的几何尺寸没有变
    • 回流的成本要比重绘大

绘制

  • 最后通过操作系统(浏览器)的 Native GUI 的 API 绘制

其中,衍生出重绘和回流的问题,提高性能的方法之一就是减少浏览器的渲染时间,其中的一个优化点就是减少重绘和回流

减少回流和重绘的方法

  1. 不要一条条修改 DOM 样式,与其这样,不如预定义好 CSS 的 class,然后修改 DOM 的样式
  2. 把 DOM“离线”后修改
    1. 使用 documentFragment 对象在内存里操作 DOM
    2. 先把 DOM 给 display:none(有一次 Reflow),然后你想怎么改就怎么改,再把它显示出来
    3. clone 一个 DOM 节点到内存里,然后想怎么改就怎么改,改完后和在线的那个交换一下
  3. 不要把 DOM 节点的属性值放在一个循环中当作循环的变量,不然这会导致大量地读写这个节点的属性
  4. 尽可能地修改层级比较低的 DOM
  5. 不要使用 table 布局

造成回流的属性:

width、height、padding、margin、border、position、top、left、bottom、right、float、clear、text-align、vertical-align、line-height、font-weight、font-size、font-family、overflow、white-space

造成重绘的属性:

color、border-style、border-radius、text-decoration、box-shadow、outline、background

记住一点,回流是与几何大小相关,重绘与大小无关

如此,从输入 url 到看到页面的整个流程就走完了

总结

这道题能衍生很多问题,从一题可以测试出面试者的 HTTP、浏览器相关知识。正所谓”鹏怒而飞,其翼若垂天之云;水击三千里,碧空九万丈;好风凭借力,送我上青云。“。这道题之所以能成为经典题,不是没有它的原因的

笔者这里做一个总结,把这题可以衍生的知识点逐一列出,待君思索

浏览器方面

  • 浏览器架构
    • 由什么组成?浏览器主进程、GPU 进程、多个渲染进程、多个插件进程、网络进程、音频进程、存储进程等
    • 渲染进程中有哪些进程?GUI 渲染线程、JS 引擎线程、事件触发线程、网络异步线程、定时器线程
    • 进程和线程的区别?进程是应用程序创建的实例,而线程依托于进程,它是计算机最小的运行单位
  • 浏览器渲染
    • 渲染流程?解析、渲染、绘制
    • 重绘和回流
      • 两者的区别
      • 重绘和回流的属性
      • 如何减少重绘和回流,提高渲染性能

HTTP 方面

  • HTTP 缓存

    • 强缓存

      • HTTP/1.1 Cache-Control
      • HTTP/1.0 Expires
      • Cache-Control > Expires
    • 协商缓存

      • HTTP/1.1 ETag/If-None-Match
      • HTTP/1.0 Last-Modified/If-Modified-Since
      • 精准度:ETag > Last-Modified
      • 性能:Last-Modified > ETag
  • TCP/IP 连接

    • 三次握手、四次挥手
  • 网络层面的性能优化

    • HTTP/1.1 的做法
    • HTTP/2 的做法
    • HTTP/3 的做法
    • 每个阶段采用的性能优化是有所不同的

参考资料

芝麻开门,显示全文!

TCP/IP 协议及网络分层模型

互联网要互联连接,一条消息从发送到接受,都需要走一遍网络协议。我们讲讲 TCP/IP 协议以及网络分层模型

先看网络分层模型

网络分层模型

在网络分层模型中,有两种模型,一是 TCP/IP 标准,它提出得早,1970年就被发明。它提出了分层概念,将网络通信分为四层,分别是链接层、网络层、传输层、应用层

另一种是 OSI,全称叫开放式系统互联通信参考模型,是国际标准,用来统一各种网络协议,始于上世纪70年代后期。是网络分层的”空架子老大”

OSI 模型分成了七层,分别为物理层、数据链路层、网络层、传输层、会话层、表示层、应用层

两者的关系就好比手机系统的巨头是 iOS 和 Android,它们制定了标准,因为话语强,所以国际标准以它们为核心再添加指定一些其他标准,但核心是 iOS 和 Android。其中 TCP/IP 就是网络分层模型中的老大,OSI 就像英国女王那样,是皇室象征,是国际标准

TCP/IP 网络分层模型

很有意思,TCP/IP 明明是网络通信协议的统称,却以其中两个核心协议为名字来称呼。它总共有四层,像搭积木一样,每一层需要下层的支撑,同时又支撑着上层。如图所示:

TCP/IP 网络分层模型

最底层也是第一层叫 网络访问(链接)层,Network Access(link)layer,负责在以太网、Wi-Fi 这样底层网络上发送原始数据包。使用 MAC 地址来标记网络上的设备,所以有时候也叫 MAC 层

第二层叫网络互连层,internet layer, IP 协议就处在这一层。因为 IP 协议定义了 IP 地址的概念,所以就在链接层的基础上,用 IP 地址取代 MAC 地址

第三层叫传输层,transport layer,负责保证数据在 IP 地址标记的两点之间可靠地传输,例如 TCP、UDP、SCTP等

第四层叫应用层,顾名思义,是面向具体应用的协议,如 HTTP、SSH、FTP、SMTP、DNS等

按照 TCP/IP 模型,网络通信是一层包着一层,发送端每通过一层则增加首部,接收端每通过一层则删除首部

数据传输过程

这里要注意的是:

  • MAC 层的传输单位是帧(frame)
  • IP 层的传输单位是包(packet)
  • TCP 层的传输单位是段(segment)
  • HTTP 的传输单位则是消息或报文(message)

注:我们常说的丢包丢包丢的就是 IP 包

OSI 网络分层模型

官方定的分层模型叫 OSI,全称是开放式系统互联通信参考模型。你问我支不支持,我是支持的,但它又不是个强制标准,所以一直有四层(TCP/IP模型)、七层(OSI模型)之说。

OSI 网络分层模型

第一层:物理层,physical layer,网络的物理形式,例如电缆、光纤、网卡等;

第二层:数据链路层,data link layer,它基本相当于 TCP/IP 的链接层;

第三层:网络层,network layer,相当于 TCP/IP 的网络层;

第四层:传输层,transport layer,相当于 TCP/IP 的传输层;

第五层,会话层,session layer,维护网络中的连接状态,即保持会话和同步;

第六层,表示层,presentation layer,把数据转换为合适、可理解的语法和语义;

第七层,应用层,面向具体的应用传输数据

两个分层模型的映射关系

分层模型的映射关系

第一层:物理层,TCP/IP 里无对应;

第二层:数据链路层,对应 TCP/IP 的网络访问(链接)层;

第三层:网络层,对应 TCP/IP 的网络互连层;

第四层:传输层,对应 TCP/IP 的传输层;

第五、六、七层:统一对应到 TCP/IP 的应用层。

TCP/IP 协议

上诉我们知道两个网络分层模型是什么,由什么组成,并且知道它们的映射关系。虽然有国际标准,但这个世界还是以 TCP/IP 协议为主要标准。你可以理解为先入为主,也可以理解为单极霸权,甚至可以联想一下5G/6G 标准对世界话语权的重要性,总之,现阶段还是以 TCP/IP 协议为主。

四大层

网络访问(链接)层

MAC 地址,它表示的你的物理世界地址

网络层

IP 协议定义了 IP 地址

电影《黑客军团》中有个片段,被做成了程序员段子

程序员段子

A:你的地址是什么?

B: 173.168.15.10(IP地址)

A:不,你的本地地址呢?

B: 127.0.0.1

A:我的意思是你的物理地址

B: 29:01:38:62:31:58(物理地址指的就是 MAC 地址)

你也可以通过 ipconfig /all 来查看你的所有地址

网络地址

传输层

具有代表性的是 TCP,可以说在 HTTP3 之前,传输层就是 TCP 的天下,它代表的就是传输层,可在 HTTP2 之后,发现影响性能瓶颈的地方成了 TCP 的队头阻塞,所以 UDP 开始活跃起来,真是此一时彼一时

关于 TCP 和 UDP ,这张图表能更好地对比分析

TCP 和 UDP

应用层

应用层有很多协议,例如 HTTP 协议、FTP 协议、SMTP 协议…

应用层协议应用
HTTP万维网
FTP文件传输
DNS域名转换
SMTP电子邮件
TELNET远程终端接入

参考资料

芝麻开门,显示全文!

备忘录:全局下载的npm包

最近升级 node,不小心升级坏了,于是把原先的 Node 应用程序删了之后,输入命令后说”系统找不到命令“。试了各种方法,还是不行,索性把原先的 node 全部删除,重新安装一边

按照安装指示一下把 Node 安装完

把缓存和全局包放在自定义盘下

npm config set cache "D:\Program Files (x86)\NodeJS\node_cache"
npm config set prefix "D:\Program Files (x86)\NodeJS\node_global"

再下载个人认为可以全局下载的 npm 包

cnpm:阿里出品,npm 中国站,特点是下载 npm 包的源放在国内,所以下包速度会变快

npm install -g cnpm --registry=https://registry.npmmirror.com

pm2:小卡的 node 进程管理工具

npm install pm2 -g

nodemon: node 开发者必备,服务热更新

npm i nodemon -g

vuepress:尤式文档

npm install -g vuepress

umi:云谦的脚手架

yarn add umi

pnpm:更快的软件包管理器

npm install -g pnpm

yarn:和 npm 差不多的包管理器,速度快、又稳定

npm install -g yarn

http-server:快速启动 Node 静态服务

npm install -g http-server

serve:快速启动 Node 静态服务

npm install -g serve

npm-check-updates:升级 package.json 中的 各种包

npm install -g npm-check-updates
# 运行
ncu -u

typescript:添加了类型系统的 JavaScript

npm install -g typescript

antfu/ni:自动使用正确的包管理器

npm i -g @antfu/ni

芝麻开门,显示全文!

防抖与节流

最近去面试,又遇到面试官问我防抖与节流了,而明明前几天就看过手写代码,却写不出来。有时候我在想,是不是自己太笨了

回归正题

防抖(debounce)

先不说概念,按自己的理解,在单反里,有防抖机制。因为人在拿着单反的时候会手抖(单反重),按下快门的瞬间,照片会糊,所以有防抖机制,以防止新手把照片拍糊

单反中的防抖是防止抖动,让人拍出清晰的照片,JavaScript 中的防抖是为了什么?

同理,它的作用也是防止抖动。试想当你频繁触发一个事件时,就会引起不必要的性能损失,那么让该事件在停止触发后再触发,以此减少部分性能

防抖的定义

防抖就是要延迟执行,你一直操作触发事件一直不执行,当你停止操作等待多少秒后才执行

也就是说不管事件触发频率有多高,一定在事件触发 n 秒后执行。如果在事件触发的 n 秒又触发了这个事件,那就以新事件的事件为准,n 秒后才执行。总之,要等你触发完事件 n 秒内不再触发事件,它才执行

手写防抖

根据定义,我们知道要在时间 n 秒后执行,那么我们就用定时器来实现

function debounce(event, wait) {
  let timer = null;
  return function (...args) {
    clearTimeout(timer); // 清除setTimeout,使其回调函数不执行
    timer = setTimeout(() => {
      event.apply(this, args);
    }, wait);
  };
}

代码很简单,即当还在触发事件时,就清除 timer,使其在 n 秒后执行,但此写法首次不会立即执行,为其健壮性,需加上判断是否第一次执行的第三个参数 flag,判断其是否立即执行

function debounce(event, wait, flag) {
  let timer = null;
  return function (...args) {
    clearTimeout(timer);
    if (!timer && flag) {
      event.apply(this, args);
    } else {
      timer = setTimeout(() => {
        event.apply(this, args);
      }, wait);
    }
  };
}

防抖场景

窗口大小变化,调整样式

window.addEventListener("resize", debounce(handleResize, 200));

搜索框,输入后 1000 毫秒搜索

debounce(fetchSelectData, 300);

表单验证,输入 1000 毫秒后验证

debounce(validator, 1000);

防抖帝王库

两大工具库都有防抖源码,可供参考

lodash-debounce

underscore-debounce

节流(throttle)

顾名思义,一节一节的流,就好似控制水阀,在事件不断触发的过程中,固定时间内执行一次事件

手写节流

因为是固定时间内执行一次时间,所以我们有两种实现方法,一用时间戳,二用定时器

时间戳

function throttle(event, wait) {
  let pre = 0;
  return function (...args) {
    if (new Date() - pre > wait) {
      // 当 n 秒内不重复执行
      pre = new Date();
      event.apply(this, args);
    }
  };
}

使用时间戳虽然能实现节流,但是最后一次事件不会执行

定时器

function throttle(event, wait) {
  let timer = null;
  return function (...args) {
    if (!timer) {
      timer = setTimeout(() => {
        timer = null;
        event.apply(this, args);
      }, wait);
    }
  };
}

使用定时器实现节流,虽然最后一次能触发,但是第一次不会触发

时间戳 + 定时器

为解决第一次和最后一次都可以触发,把两者结合起来

function throttle(event, wait) {
  let pre = 0,
    timer = null;
  return function (...args) {
    if (new Date() - pre > wait) {
      clearTimeout(timer);
      timer = null;
      pre = new Date();
      event.apply(this, args);
    } else {
      timer = setTimeout(() => {
        event.apply(this, args);
      }, wait);
    }
  };
}

节流场景

scroll 滚动

window.addEventListener("scroll", throttle(handleScroll, 200));

input 动态搜索

throttle(fetchInput, 300);

节流帝王库

lodash-throttle

underscore-throttle

总结

防抖:只执行最后一次。事件持续触发,但只有等事件停止触发后 n 秒后才执行函数

节流:控制执行频率。持续触发,每 n 秒执行一次函数

对比图:

防抖节流对比图

线上 demo(司徒正美的 demo):防抖节流

参考资料

芝麻开门,显示全文!

神人操作符:可选链与空值合并

分享两个工作中常用的操作符:可选连和空值合并

?. 操作符

中文翻译为 可选链操作符 ,允许开发者读取深嵌在对象链中的属性值,而不必显示验证每个引用。当引用为空时,表达式停止计算并返回一个未定义的值

let johan = {
  name: "johan",
  age: 28,
  lover: {
    name: "masami",
    age: 34,
  },
};

// 没有 ?. 操作符之前
const johanLover = johan.lover && johan.lover.name;
console.log(johanLover);
// 使用 ?. 操作符
const johanLover = johan.lover?.name;
console.log(johanLover); // masami
// 如果没有值则返回 undefined
const johanLover = johan.lover?.sex;
console.log(sex); // undefined

?? 操作符

另一个会用的是空值合并 ??

控制合并可以真正的检查 nullish 值,而不是 falsely 值。什么是 nullish 值,什么又是 falsely 值?

falsely 值:空字符串、数字0、undefined、null、false、NaN 等

然后,很多情况下你指向检测一个变量是否为空值——undefined 或者 null,就像变量可以是一个空字符串甚至是一个假值

640

参考资料

芝麻开门,显示全文!

Promise面试题思考延伸

最近想起之前在V2EX上看到的一个问题:一个 async function 数组, 怎样一个一个顺序执行?

想做到的是,每过一秒,分别打印: this is 0 > this is 1 > this is 2 ~ this is 8

下面的代码结果是过一秒后全部执行。

是不是哪里写的不对呢,多谢指教

var jobs = [];

for (let i = 0; i < 8; i++) {
  jobs.push(async function () {
    setTimeout(function () {
      console.log("this is " + i);
    }, 1000);
  });
}

(async function () {
  for (const job of jobs) {
    await job();
  }
})();

这题考察对 Promise 、async/await 的理解。而这题又能让人联想到一个类似的setTimeout 循环问题——破解前端面试(80% 应聘者不及格系列):从 闭包说起

请问以下代码打印出什么数据

for (var i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(new Date(), i);
  }, 1000);
}

console.log(new Date(), i);

如何修改成每隔1秒打印一个数,以0、1、2、3、4、5 的顺序排列,并要求原有的代码块中的循环和两处 console.log 不变,该怎么改造代码?

此题面试官想考察循环、闭包、闭包的解决(IIFE)、ES6 知识点(let、Promise)、ES7 的Async/await,以及setTimeout 的第三个参数等等知识点,具体看文章能明白一二

这里我们讲第一道题目,如果要实现题主所说的效果,缺少了什么?

已知:let形成块级作用域,意味着每次循环,jobs就 push 一个 async 函数,这些都是同步执行

但是注意,async 中的函数也是同步执行,只有等到 await 时才会进入微任务中,所以当

  • i=0时,jobs 塞入一个 setTimeout(function() { console.log(“this is 0” )})
  • i=1时,jobs 塞入一个 setTimeout(function() { console.log(“this is 1” )})
  • i=2时,jobs 塞入一个 setTimeout(function() { console.log(“this is 2” )})
  • i=3时,jobs 塞入一个 setTimeout(function() { console.log(“this is 3” )})
  • i=7时,jobs 塞入一个 setTimeout(function() { console.log(“this is 7” )})

继续往下执行

(async function () {
  for (const job of jobs) {
    await job();
  }
})();

这里题主了解到 await 需要 async 配合使用,就写了立即执行匿名函数,执行数组 jobs,但问题是 jobs 中的每个子项都是执行 async function(){setTimeout},这里的 async 有意义吗?

jobs.push(async function () {
  setTimeout(function () {
    console.log("this is " + i);
  }, 1000);
});

如果要让 await 暂停进程并恢复进程(即await job()),我们需要的是什么?

去掉 async,使其变成一个普通的函数,结果执行结果一致

jobs.push(function () {
  setTimeout(function () {
    console.log("this is " + i);
  }, 1000);
});

同样,将普通函数改成箭头函数也是如此,一秒之后打印还是0~7

根据网友总结:

  1. 对于 promise 对象,await 会阻塞函数执行,等待 promise 的 resolve 返回值,作为 await 的结果,然后再执行下一个表达式
  2. 对于非 promise 对象,比如 箭头函数、同步表达式等等,await 等待函数或者直接量的返回,而不是等待其执行结果

所以如果要让 await 每隔1秒执行一个 job,那就需返回一个 promise 实例,基于此逻辑进行改造

...
jobs.push(function () {
    return new Promise((resolve, reject) => {
        setTimeout(function () {
            resolve()
        	console.log("this is " + i)
    	}, 1000)
    })
});
...

这样,就解决了这个问题

我们的逻辑是在循环中,每次向 jobs 中塞入一个函数,这个函数返回的是一个实例 Promise(即 await 遇到后会暂停等异步结束再继续后续代码)。当执行 await job() 时,我们知道是循环 jobs,await 让其等待执行,执行完第一个后,再执行第二个,循序执行,每一个等待1秒钟,就达到题目的要求

这里我们了解到 await 等待的是一个 promise 实例(如果非 promise 实例,就不用等待),既然说到 Promise,我们就延伸一下,then 的链式调用

Promise 的 then 方法支持链式调用,它有哪几种情况?

  • 不 return(返回)值, 值延续上一个 resolved
  • return
    • return 非 Promise 实例
    • return Promise 实例

不 return

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("johan");
  }, 2000);
});

promise
  .then((data) => {
    console.log(data);
    // 不返回任何值
  })
  .then((data) => {
    console.log("第二次", data);
  });
点击展开答案

johan

第二次 undefined

return 非 Promise 实例

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("johan");
  }, 2000);
});

promise
  .then((data) => {
    console.log(data);
    return "帝王johan";
  })
  .then((data) => {
    console.log("第二次", data);
  });
点击展开答案

johan

第二次 帝王johan

return Promise 实例

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("johan");
  }, 2000);
});

promise
  .then((data) => {
    console.log(data);
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve(`${data} next`);
      }, 4000);
    });
  })
  .then((data) => {
    console.log("第二次", data);
  });
点击展开答案

johan

第二次 johan next

以上三个例子可以得知,在 then 方法中的 onfulfilled 函数和 onrejected 函数,不仅支持不返回,而且支持非 Promise 实例的普通值,而且支持一个 Promise 实例。并且返回的这个 Promise 实例或非 Promise 实例的普通值将会传给下一个 then 方法的 onfulfilled 函数或者 onrejected 函数中

因为我们知道它是 Generator 函数的语法糖async 函数返回的是一个 Promise 对象,当函数执行时,一旦遇到 await 就会先返回,等到异步操作完成,再接着执行函数体内后面的语句

我们来一道题来测试一下

const myPromise = (val) => Promise.resolve(val);
const delay = (duration) => {
  /**/
};
myPromise(`hello`)
  .then(delay(1000))
  .then((val) => console.log(val)); // hello

myPromise 是个 Promise 实例,传入值 hello,经过 Promise.resolve 传到then 中,然后经 delay 再传递给下一个 then,打印出val,所以 delay(1000) 会返回一个 Promise 实例,这样,第二个 then 才能打印出 hello

const delay = (duration) => (val) =>
  new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(val);
    }, duration);
  });

芝麻开门,显示全文!

一文概述:从状态复用到Hooks

学习一项知识,必须问自己三个重要问题:1. 它的本质是什么。2. 它的第一原则是什么。3. 它的知识结构是怎样的

测试一下 Hooks 的熟练程度

为什么不能在 for 循环、if 语句里使用 Hooks

React.memo、React.useCallback、React.usememo 的作用,以及对比

useState 中的值是个对象,改变对象中的值,组件会渲染吗?如果用 React.memo() 包裹住呢

Hooks 的(实现)原理是什么?

Hooks 的本质是什么?为什么?

React Hooks,它带来了哪些便利?

React Hooks 当中的 useEffect 是如何区分生命周期钩子的

useEffect(fn, []) 和 componentDidMount 有什么差异


回答得如何?在了解一个概念前,疑惑越多,理解就越深

是什么

React Hooks 是 React 16.8 推出的新特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性

为什么会有 Hooks

我们一定要有个概念,即 React 的本质是什么?它的特征是 UI=f(data)、一切皆组件、声明式编程。那么,既然是 UI=f(data),data(数据)通过 function 来驱动 UI 视图变化。在业务中,你不能简单只展示,也需交互,交互就会更新状态,React 是通过 setState 来改变状态。但这仅限于类组件,所以在Hooks出现之前,函数式组件用来渲染组件(也称它为木偶组件),类组件用来控制状态

而后,为了让状态能更好的复用,提出了Mixinsrender props高阶组件。诚然,render props、高阶组件能虽然能解决,但是会带来副作用——组件会形成“嵌套地狱”

以及类组件本身的生命周期会使得复杂的组件变得难以理解、class 语法的学习成本等等,构成了React 团队提出 hooks——让函数式组件拥有状态管理

官网也阐述过设计Hooks的三大动机:

  1. 在组件之间复用状态逻辑很难
  2. 复杂组件变得难以理解
  3. 难以理解的 class

状态复用的实验

Mixins时代

在笔者尚未使用 React 之前就存在,现已被淘汰

Mixins(混入)是一种通过扩展收集功能的方式,它本质上是将一个对象的属性拷贝到另一个对象上,不过你可以拷贝任意多个对象的任意个方法到一个新对象上去,这是继承所不能实现的。它的出现主要就是为了解决代码复用问题

这里不对其做分析,React官方文档在 Mixins Considered Harmful 一文中提到了 Mixins 带来的危害:

  • Mixins 可能会相互依赖,相互耦合,不利于代码维护
  • 不同的 Mixins 中的方法可能会相互冲突
  • Mixins 非常多时,组件是可以感知到的,甚至还要为其做相关处理,这样会给代码造成滚雪球的复杂性

Render Props

指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术

具有 render prop 的组件接受一个返回 React 元素的函数,并在组件内部通过调用此函数来实现自己的渲染逻辑

<DataProvider render={data=> (
    <h1>Hello, {data.target}</h1>
)}>

具体可在官网了解

HOC(高阶组件)

HOC的原理其实很简单,它就是一个函数,并且它接受一个组件作为参数,并返回一个新的组件,把复用的地方放在高阶组件中,你在使用的时候,只需要做不同用处

打个比方:就好像给你一瓶水,你在渴的时候就会喝它;你在耍帅的时候拿它摆POSE;你在别人需要的时候给他喝帮助人…

Writing is cheap. Show me code

function Wrapper(WrappedComponent) {
  return class extends React.Component {
    componentDidMount() {
      console.log("我是一瓶水");
    }
    render() {
      return (
        <div>
          <div className="title">{this.props.title}</div>
          <WrappedComponent {...this.props} />
        </div>
      );
    }
  };
}
import "./styles.css";
import React from "react";
import Wrapper from "./Wrapper";

class A extends React.Component {
  render() {
    return <div>喝它</div>;
  }
}

class B extends React.Component {
  render() {
    return <div>耍帅摆POSE</div>;
  }
}

class C extends React.Component {
  render() {
    return <div>帮助别人</div>;
  }
}

const AA = Wrapper(A);
const BB = Wrapper(B);
const CC = Wrapper(C);

export default function App() {
  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>
      <AA title="我是普通人" />
      <BB />
      <CC />
    </div>
  );
}

这样就很明显的看出 HOC 的好处,”一瓶水“是共同代码,A、B、C处理业务代码,然后将A、B、C传入HOC(一瓶水)中,返回了一个新的组件 AA、BB、CC。相同的代码得到了公用

HOC-demo

各位可以前往这里查看 demo

HOC 的用处不单单是代码复用,还可以做权限控制、打印日志等。但它的缺陷也没明显,当大量使用 HOC 后,会产生大量的嵌套,使得嵌套变得困难;并且 HOC 会劫持 props,在不遵守约定的情况下可能会造成冲突

总结下 HOC:

  • 用法:创建一个函数,该函数接收一个组件作为输入,除了组件还可以传递其他的参数,基于该组件返回一个不同的组件
  • 优点:代码复用,逻辑复用
  • 缺点:因为嵌套使得调试难度变高;会劫持props,或许造成冲突

Hooks 的出世

前有状态复用的不给力( Mixins 被淘汰,render props、HOC 的副作用又大),后有类组件的复杂组件难以理解、维护(过多的生命周期),class 属性造成的 this 指向又麻烦。于是乎,Hooks 大喊一声:我来也

它起码有三个好处

  • 逻辑复用
    • 秒杀render props、hoc
  • 业务代码更聚合
    • 秒杀类组件
  • 写法简洁
    • 秒杀类组件

useState

作用:让函数组件具有维持状态的能力,替代类组件的constructor初始化状态

例子:

const Counter = () => {
  const [count, setCount] = useState(0);
  return <div onClick={() => setCount(count + 1)}>{count}</div>;
};

特点:逻辑复用

在使用 useState 时,会出现两个衍生问题:

一:Capture Value 特性

函数式组件与类组件有何不同中曾介绍过,函数式组件能捕获渲染时所用的值。并举例组件中点三下加, setTimeout 3秒后弹出数字,在点两次加,3秒后展示3,而不是5。而类组件却能获得最新的数据,这是为什么?

因为函数式组件有 Capture Value 的特性。而从源码的角度看,每次调用 setXX 会引发 re-render 从而重渲染组件

如果想获得最新值,可以通过 useRef 来将值保存在内存中

二:useState 中的值是个对象,改变对象中的值,组件会渲染吗?怎么优化?

一般我们用 useState 尽量遵守单一值,但难免会遇到一些特殊情况,如果值是个对象,改变对象中的其中一个属性,其他属性不变,那么引用其他属性的组件是否会渲染呢?

const DemoSon = (props) => {
  console.log("render", props);
  return <div>{props.name}</div>;
};

const Demo = () => {
  const [data, setData] = useState({ foo: { name: "johan", bar: { baz: 1 } } });
  const handleClick = () => {
    setData({
      ...data,
      foo: {
        ...data.foo,
        bar: {
          baz: 2,
        },
      },
    });
  };

  return (
    <div onClick={handleClick}>
      {data.foo.bar.baz}
      <DemoSon name={data.foo.name} />
    </div>
  );
};

点击 div,修改 baz 的值,DemoSon 是否会渲染呢?答案是会的,为什么会渲染?因为你的引用值发生了变化,生成了新的虚拟DOM,渲染到视图上时,子组件就会渲染。如何优化,让数据不变的组件不重复渲染?我觉得有两种方式,一拆分 data,拆分成 foo 对象和name,因为 setData 并不改变 name,所以DemoSon 不会渲染,还有一种是通过 memo 包裹住 DemoSon,因为 memo 能避免重新渲染

可查看线上 demo

useEffect

作用:处理副作用,替代类组件的componentDidMount、componentDidUpdate、componentWillUnmount

使用方式:

// 没有第二个参数
// mount 阶段和 update 阶段都执行
useEffect(fn);

// 第二个参数为空数组
// 当 mount 阶段会执行
useEffect(fn, []);

// 第二个参数为依赖项
// 当依赖项(deps)数据更新时会执行
useEffect(fn, [deps]);

// 清除副作用
useEffect(() => {
  const subscription = props.source.subscribe();
  return () => {
    // 清除订阅
    subscription.unsubscribe();
  };
});

PS:以上注释中的 mount 阶段,即组件加载时;update 指数据(包括props、state)变化时

在使用 useEffect 时,会面临几个问题:

1. useEffect(fn, []) 和 componentDidMount 有什么区别?

虽然 useEffect(fn, []) 和 componentDidMount 都可以表示组件加载时执行,但从细节上两者有所不同。要谈起细节需从源码中聊起,具体可看 React 源码魔术师卡颂的这篇——useEffect(fn, [])和cDM有什么区别? 了解,这里我讲下我的理解

源码中把虚拟DOM和虚拟DOM渲染到真实DOM分为两个阶段。虚拟DOM存在内存中,在 JSX 中对数据增删改,虚拟DOM会对对应的数据打上标签,这个阶段称为 render 阶段;把虚拟DOM映射到真实DOM的操作被称为 commit 阶段,它负责把这些标签转换为具体的DOM操作

在 render 阶段

  • 插入 DOM 元素被打上 Placement 标签;
  • 更新 DOM 元素被打上 Update 标签;
  • 删除 DOM 元素被打上 Deletion 标签;
  • 更新 Ref 属性被打上 Ref 标签
  • useEffect 回调被打上 Passive 标签

而 commit 阶段分为三个子阶段

  • 渲染视图前(before mutation 阶段)
  • 渲染视图时(mutation 阶段)
  • 渲染视图后(layout 阶段)

被打上 Placement 标签的,会在 mutation 阶段时执行对应的 appendChild 操作,意味着 DOM 节点被插入到视图中,接着在 layout 阶段调用 componentDidMount

而被打上 Passive 标签的,它会在 commit 阶段的三个子阶段执行完成后再异步调用 useEffect 的回调函数

由此可见,它们的调用调用时机是不同的,useEffect(fn,[]) 是在 commit 阶段执行完以后异步调用回调函数,而 componentDidMount 会在 commit 阶段完成视图更新(mutation阶段)后再 layout 阶段同步调用

hooks 中也有一个和 componentDidMount 调用时机相同的 hooks——useLayoutEffect

其次useEffect(fn, []) 会捕获 props 和state,而 componentDidMount 并不会。使用 useEffect(fn, []) 的会第哦啊函数会拿到初始的 props 和 state,这个道理和 capture value 是一个道理

总结:两点不同,一、执行时机不同;二、useEffect(fn, []) 会对 props 和 state 进行捕获

下文会用demo说明 capture value 特性

2. 每一次渲染都有它自己的 props 和 state

先讨论一下渲染(rendering),我们来看一个计数器组件 Counter

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>点击 {count} 次</p>
      <button onClick={() => setCount(count + 1)}>点击</button>
    </div>
  );
}

第一次渲染时,count 的初始值从 useState(0) 中获取。当调用 setCount(count + 1) ,React 重新渲染组件,此时 count 的值就成 1。如下所示:

// Mount 第一次渲染
function Counter() {
  const count = 0; // 默认从useState 中获得
  // ...
  <p>点击 {count} 次</p>;
  // ...
}

// Update 点击 1 次
function Counter() {
  const count = 1; // 通过 setCount 修改 count
  // ...
  <p>点击 {count} 次</p>;
  // ...
}

// Update 点击 2 次
function Counter() {
  const count = 2; //  通过 setCount 修改 count
  // ...
  <p>点击 {count} 次</p>;
  // ...
}

每当我们更新状态时,React 会重新渲染组件。每次渲染获得此刻(快照)的 count 状态

而在类组件中并不是捕获值

举个例子:

class ClassDemo extends React.Component {
  state = {
    count: 0,
  };
  componentDidMount() {
    setInterval(() => {
      this.setState({ count: this.state.count + 1 });
    }, 1000);
  }
  render() {
    return <div>我是Class Component, {this.state.count}</div>;
  }
}

页面上的 count 会每隔一秒钟加1,而换成函数式组件

const FunctionDemo = () => {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);

  const handleClick = () => {
    setCount(count + 1);
  };

  return <div onClick={handleClick}>我是Function Component, {count}</div>;
};

永远是1

这就是 hooks 的capture value,类似例子在函数式组件与类组件有何不同介绍过

可前往线上demo查看

useLayoutEffect

作用:同步执行副作用

大部分情况下,使用 useEffect 就可以帮我们处理副作用,但是如果想要同步调用一些副作用,比如对 DOM 的操作,就需要使用 useLayoutEffect,useLayoutEffect 中的副作用会在 DOM 更新之后同步执行

与类组件中的 componentDidMount 效果一致,都是在 commit 阶段完成视图更新(mutation阶段)后在 layout阶段同步调用

useCallback

作用:记忆函数,避免函数重新生成。在函数传递给子组件时,可以避免子组件重复渲染

例子:

const memoizedCallback = useCallback(() => {
  doSomething(a, b);
}, [a, b]);

可缓存的引用

在类组件中常困扰人的是 this 绑定问题

  1. render 方法中使用bind
class App extends React.Component {
  handleClick() {
    console.log("this > ", this);
  }
  render() {
    return <div onClick={this.handleClick.bind(this)}>test</div>;
  }
}
  1. render方法中使用箭头函数
class App extends React.Component {
  handleClick() {
    console.log("this > ", this);
  }
  render() {
    return <div onClick={(e) => this.handleClick(e)}>test</div>;
  }
}
  1. 构造函数中bind
class App extends React.Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
  }
  handleClick() {
    console.log("this > ", this);
  }
  render() {
    return <div onClick={this.handleClick}>test</div>;
  }
}

4.在定义阶段使用箭头函数绑定

class App extends React.Component {
  handleClick = () => {
    console.log("this > ", this);
  };
  render() {
    return <div onClick={this.handleClick}>test</div>;
  }
}

前三种都会因 App 组件的props或state 变化而重新触发渲染,使其渲染新的handleClick。第四种将handleClick抽离出赋值为变量,通过 this 指向存储函数,起到了缓存作用

而函数式组件一定会渲染

function App() {
  const handleClick = () => {
    console.log("Click");
  };
  return <div onClick={handleClick}>test</div>;
}

但是 useCallback 能缓存函数,让它”记住“

function App() {
  const handleClick = useCallback(() => {
    console.log("Click");
  }, []);
  return (
    <div className="App">
      <Demo handleClick={handleClick} />
    </div>
  );
}

但使用 useCallback 必须使用 shouldComponentUpdate 或者 React.memo 来忽略同样的参数重复渲染

所以单独使用 useCallback 是不能的,它需要和 React.memo 配合

function Demo(props) {
  return <div onClick={props.handleClick}>test</div>;
}

const MemoDemo = memo(Demo);

function App() {
  const handleClick = useCallback(() => {
    console.log("Click");
  }, []);
  return (
    <div className="App">
      <Demo handleClick={handleClick} />
    </div>
  );
}

但是 useCallback 会使代码可读性变差,所以尽量不用 useCallback

不用 useCallback ,那怎么提高性能呢?

useMemo

作用:记忆组件。替代类组件的shouldComponentUpdate

useCallback 的功能完全可以由 useMemo 所取代,如果你想通过 useMemo 返回一个记忆函数也是完全可以的

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

例子:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

默认情况下,如果 React 父组件重新渲染,它包含的所有子组件都会重新渲染,即使子组件没有任何变化

useMemo 和 useCallback 接受的参数都是一样,都是在其依赖项发生变化后执行,都是返回缓存的值,区别在于 useMemo 返回的是函数运行的结果,useCallback 返回的是函数

useMemo 返回的是一个值,用于避免在每次渲染时都进行高开销的计算

useCallback VS useMemo

相同点:useCallback 和 useMemo 都是性能优化的手段,类似于类组件的 shouldComponentUpdate,在子组件中使用 shouldComponentUpdate,判断该组件的 props 和 state 有没有变化,从而避免每次父组件 render 时重新渲染子组件

区别:useCallback 和 useMemo 的区别是 useCallback 返回一个函数,当把它返回的这个函数作为子组件使用时,可以避免每次父组件更新时重新渲染这个子组件

memo

作用:避免重新渲染

只有当 props 改变时会重新渲染子组件

被 memo 包裹住后,当 props 不变时,子组件就不会渲染

React.memo() 方法可以防止子组件不必要渲染,从而提供组件性能。

关于性能优化 Dan 曾写过一篇文章:在你写memo()之前,其实在我们使用 useCallback、useMemo、memo前不妨试试 state 下移和内容提升。目的就是让不用渲染的组件不重复渲染

useRef

作用:

保存引用值,跟 createRef 类似。我们习惯用 ref 保存 DOM

使用 useRef 保存和更新一些数据是有一定好处的,它可以不通过内存来保存数据,使得这些数据再重渲染时不会被清除掉

它不仅仅是用来管理DOM ref 的,它还相当于 this,可以存放任何变量,很好的解决闭包带来的不方便性

如果我们想利用普通的变量再重渲染过程中追踪数据变化是不可行的,因为每次组件渲染时它都会被重新初始化。然而,如果使用 ref 的话,其中的数据能在每次组件渲染时保持不变。

例子:

const [count, setCount] = useState < number > 0;
const countRef = useRef < number > count;

函数式组件与类组件有何不同介绍过使用方法

其他

useContext:减少组件层级

useReducer: 类 redux 的方法,useState 是基于它扩张的

ForwardRef:转发 ref

useImperativeHandle :透传 Ref,父组件获取子组件内的方法

自定义 Hooks

由于 useState 和 useEffect 是函数调用,因为我们可以将其组合成自己的 Hooks

function MyResponsiveComponent() {
  const width = useWindowWidth();
  return <p> Window width is {width}</p>;
}

function useWindowWidth() {
  const [width, setWidth] = useState(window, innerWidth);
  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener("resize", handleResize);
    return () => {
      window.removeEventListener("resize", handleResize);
    };
  });
  return width;
}

自定义 Hooks 让不同的组件共享可重用的状态逻辑。注意状态本身是不共享的。每次调用 Hook 都只声明了其自身的独立状态

React Hooks的不足

虽然实现了大多数类组件的功能,但是还无法实现 getSnapshotBeforeUpdate 和 componentDidCatch 这两个 API

附录:使用规则

Hooks 的本质就是 JavaScript 函数,在使用它时需要遵守两条规则

只在最顶层使用 Hook

不要在循环,条件或嵌套函数中调用 Hook,确保总是在你的 React 函数的最顶层以及任何 return 之前调用他们。遵守这条规则,你就能确保 Hook 在每次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确

只在 React 函数中调用 Hook

不要再普通的 JavaScript 函数中调用 Hook,你可以:

  • 在 React 的函数组件中调用 Hook
  • 在自定义 Hook 中调用其他 Hook

参考资料

芝麻开门,显示全文!

函数式组件与类组件有何不同

前言

React 中最关键的知识点就是 组件,在 React 16.8 之前(还没有 Hooks 前),我们的应用大多写成 Class 组件,因为 Class 组件有生命周期,能控制状态(state)。但函数式组件只能默默站在后面,说自己是木偶组件(也叫无状态组件),传来 props,展示UI

以下文字都基于有了 Hooks 后

正文

函数式组件和类组件之间是否有什么根本上的区别?

函数式组件捕获渲染时的值

具体可以看这篇文章:函数式组件与类组件有何不同?

因为在 React 中 props 是不可变(immutable)的,它们永远不会改变。然而,this 是可变(mutable)的

事实上,这就是类组件 this 存在的意义。React 本身会随着时间的推移而改变,以便你可以在渲染方法以及生命周期方法中得到最新的实例

函数式组件会捕获当前状态下的值,如果你使用定时器改变当前值的状态,那函数式组件显示的还是原来的值,而不是最新值。而类组件会一直获取最新值

只要一渲染,函数式组件就会捕获当前的值。而类组件即使渲染了,但是它的 this 会指向最新的实例

类组件

可以看线上Demo

class ClassDemo extends React.Component {
  state = {
    value: "",
  };

  showMessage = () => {
    alert("最新值为 " + this.state.value);
  };

  handleMessageChange = (e) => {
    this.setState({ value: e.target.value });
  };

  handleClick = () => {
    setTimeout(this.showMessage, 3000);
  };

  render() {
    return (
      <div>
        <input value={this.state.value} onChange={this.handleMessageChange} />
        <button onClick={this.handleClick}>点击</button>
      </div>
    );
  }
}

这样的结果是点击后获取到最新的值,而不是 3 秒前的值。为什么?因为 this 可变,3 秒之后执行 alert("最新值为 " + this.state.value)。 this.state.value 指向最新的值

如果类组件如果想保存原来的值该怎么做?

一、调用事件之前读取this.props

可以看线上Demo

showMessage = (value) => {
  alert("最新值为 " + value);
};

handleClick = () => {
  const { value } = this.state;
  setTimeout(() => this.showMessage(value), 3000);
};

可以解决,但点击时获取到当前的 user,再传递给 this.showMessage,这样,即使 3 秒之后也是原来的值

缺点:每次都要从 this.props 中拿值,如果数据一多,写起来不符合人性

二、在构造函数中绑定方法

可以看线上Demo

constructor(props) {
    super(props);
    this.showMessage = this.showMessage.bind(this);
    this.handleClick = this.handleClick.bind(this);
}

这个方法解决不了问题。我们的问题是我们从 this.props 中读取数据太迟了—— 读取时已经不是我们所需要使用的上下文

三、利用闭包

把方法写进 render 中,这样每次渲染时就能捕获住当时所用的 props 或者 state

可以看线上Demo

class ClassDemo extends React.Component {
  state = {
    value: "",
  };

  render() {
    const { value } = this.state;

    const showMessage = () => {
      alert("最新值为 " + value);
    };

    const handleMessageChange = (e) => {
      this.setState({ value: e.target.value });
    };

    const handleClick = () => {
      setTimeout(showMessage, 3000);
    };
    return (
      <div>
        <input value={this.state.value} onChange={handleMessageChange} />
        <button onClick={handleClick}>点击</button>
      </div>
    );
  }
}

但是这个方法很蠢,这个写法和函数式组件有什么区别呢?还不如用函数式组件呢

函数式组件如果想保存最新的值呢

使用 useRef 保存最新的值,让组件获得最新的值

function MyComponent() {
  const ref = useRef(null);
}

首先,ref 与实例都扮演同样的角色,ref 对象是一个有 current 属性的一个容器

上次的例子我们用函数式组件就可以这样写:

const FunctionDemo = () => {
  const [value, setValue] = useState("");

  const refValue = useRef("");

  const showMessage = () => {
    alert("最新值为 " + refValue.current);
  };

  const handleMessageChange = (e) => {
    setValue(e.target.value);
    refValue.current = e.target.value;
  };

  const handleClick = () => {
    setTimeout(showMessage, 3000);
  };

  return (
    <div>
      <input value={value} onChange={handleMessageChange} />
      <button onClick={handleClick}>点击</button>
    </div>
  );
};

可以看线上Demo

这里笔者提出两个疑问:

  • 为什么 ref 能保存住最新的值?

  • 为什么函数式组件会捕获,类组件不会呢?

后续文章会给出笔者的回答

参考资料

芝麻开门,显示全文!

从一道面试题引申到N道面试题

昨天分享了深入浅出 setState 原理篇 ,其中讲到 setState 是同步还是异步的问题?这不,引起了古老的回忆,翻开笔记,想起曾经有一个体验良好的面试,面试官从一道面试题出发,循序渐进,引出了各种知识点,这些知识点能检测出面试者的React知识点、ES6知识点、JS基础等。我在此基础上,加上自己的理解,整理一个个人认为考点较充足的面试分享

双方客套,面试正式开始,面试官正手来一个面试题

如下的代码, a 的值是多少

class A extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      a: 0,
    };
  }
  componentDidMount() {
    this.setState({ a: 1 });
    setTimeout(() => {
      this.setState({ a: 2 });
    }, 0);
    new Promise((resolve) => {
      resolve(this.setState({ a: 3 }));
    }).then(() => {
      this.setState({ a: 4 });
    });
  }
  render() {
    console.log("state", this.state.a);
    return <div>{this.state.a}</div>;
  }
}

这题考察到 “React 渲染生命周期” 以及 “setState 是同步还是异步” 知识点

答案是:0、3、4、2

首先是 React 渲染生命周期,当挂载时,其生命周期调用顺序为:

  • constructor
  • static getDerivedStateFromProps()
  • render
  • componentDidMount

所以先 render 一次 state.a,值为 0 ,接着进入 componentDidMount 生命周期,this.setState({ a: 1 })this.setState({ a: 3 }) 为同步操作,setTimeout 会将其中的回调函数(即() => { this.setState({ a: 2 })}) 放入宏任务中,then 之后的回调函数(即() =>{this.setState({ a: 4 })})会放入微任务中

因为(legacy 模式下)setState 的同周期内的 setState 会批处理合成为一个 setState,并以后者为主,所以 this.setState({a: 1}) 会被覆盖。因为调用了 setState ,触发了更新,意味着又 render 一次,此时的 state.a 就显示为 3。当此宏任务调用完后去查看微任务队列,发现有未执行的回调函数,执行它 this.setState({ a: 4 }) ,又一次调用 setState,触发更新,state.a 显示 4。微任务队列为空后,查看宏任务队列,发现回调函数 this.setState({ a: 2 }),执行,触发更新,state.a 显示 2

所以其结果为:0、0、4、2

不知道你对否~~

我们接着改造一个这个题,变成

class A extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      a: 0,
    };
  }
  componentDidMount() {
    this.setState({ a: 1 });
    console.log("a", this.state.a);
    setTimeout(() => {
      this.setState({ a: 2 });
      console.log("a", this.state.a);
    }, 0);
    new Promise((resolve) => {
      resolve(this.setState({ a: 3 }));
      console.log("a", this.state.a);
    }).then(() => {
      this.setState({ a: 4 });
      console.log("a", this.state.a);
    });
  }
  render() {
    return <div>{this.state.a}</div>;
  }
}

不在 render 展示 state.a ,而是在调用完 setState 后查看 state.a 的值,结果会如何呢?

改编后的题主要考察组件的数据更新和视图的更新是两码事

答案是:0、0、3、2

首先都是在 componentDidMount 中,其次,与上个案例一样,调用依次是

  • this.setState({ a: 1 });
  • this.setState({ a: 3 });
  • this.setState({ a: 4 });
  • this.setState({ a: 2 });

其区别在于调用 this.setState({ a: 1 }) 和 this.setState({ a: 3 }) 后,数据不会马上更新,调用 setState 后,会将调用压入队列中,到最后一并执行(批处理),所以此时查看 state.a 的值,会看到还是 0,因为它还没触发批处理。而 Promise、setTimeout 之类原生事件会同步执行,值就显示为你 setState 什么,我就显示什么

我们在上述两个例子中谈到了 Event Loop,在 React 中会因为性能优化而对 setState 做处理,如果在浏览器环境中,上述的例子会怎么展示呢?

console.log("0");
setTimeout(() => {
  console.log("1");
}, 0);
new Promise((resolve) => {
  resolve();
  console.log("2");
}).then(() => {
  console.log("3");
});
console.log("4");

这题主要考验了浏览器的 Event Loop 机制

答案:0、2、4、3、1

第一个值为 0 没有疑问

遇到 setTimeout,所以 console.log(“1”) 排入 宏任务队列

因为 new Promise 中的执行函数会同步执行,而 then 中的”console.log(“3”)“会进入微任务,所以第二个值为 2,

接着就是第三个值 4,再因为 Event Loop 机制(宏任务-执行全部微任务-再去找宏任务队列第一个),所以先执行微任务,第四个值为3

最后执行宏任务(setTimeout),第五个值为 5

既然说到了 Promise,不妨考考 Promise,手写一个太麻烦,没必要考课本。那来说说为什么 promise 中能 .then,它的链式调用的原理是什么?

每次 new Promise() 后能 .then().then().then(),因为它每次调用完 then 后,返回了 Promise 实例,所以才能一直调用下去

这样理解下来,链式调用的核心,就是调用完方法后返回对象本身(return this)

那我们出一道题,题目是

class Operator {...}
var op = new Operator(1)
op.add(3).minus(2).multi(2).divide(1)

写出 Operator 构造函数中的 add、minus、multi、divide

我的答案是:

class Operator {
  constructor(initNum) {
    this.num = initNum;
  }
  add(value) {
    this.num = this.num + value;
    return this;
  }
  minus(value) {
    this.num = this.num - value;
    return this;
  }
  multi(value) {
    this.num = this.num * value;
    return this;
  }
  divide(value) {
    this.num = this.num / value;
    return this;
  }
}

从这题可以引申出 class、ES6还有有哪些特性和柯里化等等

总结

从 this.setState 的一道面试题延申出各种问题,即考察了面试者对 this.setState 的理解,又考了浏览器的 Event Loop,并引申出 Promise 的链式调用,并用一道题目考察面试者的 JS 基础能力,再之后,还可以问 ES6 的特性和柯里化,知识广度就变大了,也能更好的考察面试者

芝麻开门,显示全文!