Hi 加菲

每个人都在负重前行


  • 首页

  • 标签

  • 分类

  • 归档

  • 搜索

Vue3 原理篇之 Block Tree

发表于 2021-05-25 | 更新于: 2021-05-31 | 分类于 Vue

前言

安装 npm install @vue/cli -g
构建项目 vue create vue3-compiler

用vite安装vue3项目

1
2
3
4
5
npm i -g create-vite-app
create-vite-app demo3
cd demo3
npm instal
npm run dev

vue3一样需要把我们写的template最终变成render方法,看一下vue3的转换器。

编译过程

  • 先将模板进行分析 生成对应的ast树(对象来描述语法);
  • 做转化流程 transform -> 对动态节点做一些标记 指令、插槽、事件、属性… patchFlag
  • 代码生成 codegen -> 生成最终的代码

Block的概念 -> Block Tree

  • diff算法的特点是递归遍历,每次比较同一层,比完自己比儿子、孙子…性能比较差,不停的递归,之前写的都是全量比对,所有的属性、方法都要比对。
  • block的作用就是收集动态节点(它下面所有的);将树的递归拍平成了一个数组,以前要diff就要diff【children】 children下的children,现在只要diff【dynamicChildren】
  • 在createVNode的时候就会判断这个节点是否是动态的,是的话就让外层的block收集起来
  • 目的是为了diff的时候只diff动态的节点

举例:

1】

1
2
<div></div>
<div></div>

vue3里面可以有多个跟节点,编译会生成_Fragment包裹;

2】

添加动态节点

1
2
<div>{{name}}</div>
<p><span><a>{{age}}</a></span></p>

编译后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { toDisplayString as _toDisplayString, createVNode as _createVNode, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock(_Fragment, null, [
_createVNode("div", null, _toDisplayString(_ctx.name), 1 /* TEXT */),
_createVNode("p", null, [
_createVNode("span", null, [
_createVNode("a", null, _toDisplayString(_ctx.age), 1 /* TEXT */)
])
])
], 64 /* STABLE_FRAGMENT */))
}

console.log(render({name: 'jf', age: 18}));
// Check the console for the AST

生成的ast树:

block的作用就是收集动态节点

3】

1
2
3
4
5
6
7
8
9
10
<div>
<template v-if="flag">
<p>hello word</p>
<span>{{title}}</span>
</template>
<template v-else>
<span>{{title}}</span>
<p>hello word</p>
</template>
</div>
1
2
3
4
block -> div 父亲更新 会找到dynamicChildren => 子的block和动态节点
block(v-if key="0") <div>xxx</div>
block -> div
block(v-if key="1") <div>xxx</div>

编译后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { createVNode as _createVNode, toDisplayString as _toDisplayString, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock, createCommentVNode as _createCommentVNode } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
(_ctx.flag)
? (_openBlock(), _createBlock(_Fragment, { key: 0 }, [
_createVNode("p", null, "hello word"),
_createVNode("span", null, _toDisplayString(_ctx.title), 1 /* TEXT */)
], 64 /* STABLE_FRAGMENT */))
: (_openBlock(), _createBlock(_Fragment, { key: 1 }, [
_createVNode("span", null, _toDisplayString(_ctx.title), 1 /* TEXT */),
_createVNode("p", null, "hello word")
], 64 /* STABLE_FRAGMENT */))
]))
}

console.log(render({flag: true}));
// console.log(render({flag: false}));

可以看到key从 0->1 需要全量比对

如果会影响dom结构的,都会被标记成block节点: v-if v-else
动态内容自己和父节点都会标记为block,父亲会收集儿子的block -> Block Tree;

4】

1
2
3
<div>
<span v-for="item in count">{{item}}</span>
</div>
1
2
3
4
5
block -> div
block -> v-for 不收集动态节点了 2个节点

block -> div
block -> v-for 3个节点

v-for 序列不稳定,改变结构的也要封装到block中,期望的更新方式是拿以前的和现在的做对比,靶向更新,如果前后节点不一致,只能全量比对
两个儿子(children)的全量比对,全量diff就是用之前的递归方法去比

Block Tree做的就是保证需要更新的时候能知道哪些节点需要更新

patchFlag

描述不同的动态节点,以此来做相对应的比对

源码:packages/shared/src/patchFlags

render方法
源码:packages/runtime-core/src/renderer

日常 CSS

发表于 2020-08-09 | 更新于: 2020-08-20 | 分类于 CSS

1. loading动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
.loadingImg{
animation: loadingCircle 1s infinite;
-webkit-animation: loadingCircle 1s infinite;
}

.myTransform(@deg: 0deg){
transform: rotate(@deg);
-webkit-transform: rotate(@deg);
-moz-transform: rotate(@deg);
-o-transform: rotate(@deg);
-ms-transform: rotate(@deg);
}

@keyframes loadingCircle{
from{
.myTransform(@deg: 0deg)
}
to{
.myTransform(@deg: -360deg)
}
}
@-moz-keyframes loadingCircle{
from{
.myTransform(@deg: 0deg)
}
to{
.myTransform(@deg: -360deg)
}
}
@-webkit-keyframes loadingCircle{
from{
.myTransform(@deg: 0deg)
}
to{
.myTransform(@deg: -360deg)
}
}

@-o-keyframes loadingCircle{
from{
.myTransform(@deg: 0deg)
}
to{
.myTransform(@deg: -360deg)
}
}

2. 文本溢出

单行

1
2
3
overflow: hidden;
text-overflow:ellipsis;
white-space: nowrap;

多行

1
2
3
4
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;

3. 取消边框

表单元素在手机中点击时会出现边框,取消边框的写法如下:

1
input, textarea, button, a{ -webkit-tap-highlight-color:rgba(0,0,0,0); }

webkit是苹果浏览器引擎,tap点击,highlight背景高亮,color颜色,颜色用数值调节。

每日一问

发表于 2020-08-06 | 更新于: 2021-05-25 | 分类于 面试题

[Q1] Loader和Plugin的区别是什么?

[A1]

  1. loader 用于加载某些资源文件,它是一个转换器,将A文件进行编译成B文件,比如:将A.less转换为A.css,单纯的文件转换过程。 因为webpack 本身只能打包commonjs规范的js文件,对于其他资源例如 css,图片,或者其他的语法集,比如 jsx,是没有办法加载的。 这就需要对应的loader将资源转化,加载进来。从字面意思也能看出,loader是用于加载的,它作用于一个个文件上,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中。

  2. 而plugin 用于扩展webpack的功能,是一个扩展器。它直接作用于 webpack,扩展了webpack本身的功能,针对的是loader结束后,webpack打包的整个过程,它并不直接操作文件,而是基于事件机制工作(在webpack运行的生命周期中会广播出许多事件,plugin可以监听这些事件,在合适的时机通过webpack提供的API改变输出结果。)。当然loader也时变相的扩展了 webpack ,但是它只专注于转化文件这一个领域。而plugin的功能更加的丰富,而不仅局限于资源的加载。

[参考答案]

  1. 作用不同:
    Loader直译为”加载器”。Webpack将一切文件视为模块,但是webpack原生是只能解析js文件,如果想将其他文件也打包的话,就会用到loader。 所以Loader的作用是让webpack拥有了加载和解析非JavaScript文件的能力。
    Plugin直译为”插件”。Plugin可以扩展webpack的功能,让webpack具有更多的灵活性。 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
  2. 用法不同:
    Loader在module.rules中配置,也就是说作为模块的解析规则而存在。 类型为数组,每一项都是一个Object,里面描述了对于什么类型的文件(test),使用什么加载(loader)和使用的参数(options)
    Plugin在plugins中单独配置。 类型为数组,每一项是一个plugin的实例,参数都通过构造函数传入。

[Q2] 请解释React中props和state的区别?

[A2]

  1. 共同点: props 和 state 都属于 react 数据接口,他们都可以决定组件的行为和状态
  2. 不同点:
    props 属于对外接口,state 属于对内接口,props 主要是父组件向子组件传递参数配置该组件,组件内部无法控制也无法修改。想要获取父组件的数据只能通过回调函数的形式(即在自组件中调用父组件的方法)。
    state 主要用于保存,控制,修改组件自身状态,你可以理解state是一个局部的,只能被组件自身控制的数据源。不能被外界获取和修改。我们可以通过this.setState更新数据,setState导致组件重新渲染。
    总之props是让外部对组件进行配置,state是让组件控制自己的状态

[Q3] 浏览器的本地存储(1)的cookie了解多少?

[A3]

  1. 含义:Cookie指某些网站为了辨别用户身份而储存在用户本地终端上的数据(通常经过加密)。 cookie是服务端生成,客户端进行维护和存储。通过cookie,可以让服务器知道请求是来源哪个客户端,就可以进行客户端状态的维护,比如登陆后刷新,请求头就会携带登陆时response header中的set-cookie,Web服务器接到请求时也能读出cookie的值,根据cookie值的内容就可以判断和恢复一些用户的信息状态。也可以理解为,就是客户端用来存储数据的一种,以键值对的形式存在,既可以在客户端设置,也可以在服务器端设置,cookie会跟随任意HTTP请求一起发送。
  2. 缺点:
    • 大小最多只能存储4kb的数据(各浏览器之间略有不同),各浏览器的cookie每一个name=value的value值大概在4k,所以4k并不是一个域名下所有的cookie共享的,而是一个name的大小,此外很多浏览器对一个站点的cookie个数也是有限制的。
    • 不安全。
  3. 分类:
    • 会话cookie:没有设置有效时间的 cookie。只要关闭了浏览器,cookie 就会被销毁。
    • 永久cookie:cookie 被保存在文件中,在有效时间内可长期存在,浏览器重启或机器重启都可以再次读取到cookie。
  4. 应用场景:记住密码,下次自动登录;购物车功能;保存用户的偏好,比如网页的字体大小、背景色等;记录用户浏览数据,进行商品(广告)推荐;多个站点共享登录状态(同一住域名下多个子站点)

[参考答案]

Cookie最开始被设计出来其实并不是做本地存储的,而是为了弥补http在状态管理上的不足
http协议是一个无状态协议,客户端向服务器发请求,服务器返回响应,这次事件就完成了,但是下次发请求如何让服务端直到客户端是谁呢?在这个需求下就产生了Cookie

Cookie本质上就是浏览器里面存储的一个很小的文本文件,内部以键值对的方式来存储(在chrome开发者面版的Application这一栏可以看到)都会携带相同的Cookie,服务器拿Cookie进行解析,便能拿到客户端的状态 Cookie的作用就是用来做状态存储的,但是也有很多缺陷:

  1. 容量缺陷。Cookie的体积上限只有4KB,只能用来存储少量的信息
  2. 性能缺陷。Cookie紧跟域名,不管域名下面的某一个地址需不需要这个Cookie,请求都会携带上完整的Cookie,这样随着请求数的增多,其实会造成巨大的性能浪费的,因为请求携带了很多不必要的内容
  3. 安全缺陷。由于Cookie以纯文本的形式在浏览器和服务器中传递,很容易被非法用户截取,然后进行一系列的篡改,在Cookie的有限期内重新发送给服务器,这是很危险的。另外,在HttpOnly为false的情况下,Cookie信息能直接通过js脚本来读取。

[Q4] 浏览器的本地存储(2)的WebStorage了解多少

[A4]

html5中的Web Storage包括会话存储(sessionStorage)和本地存储(localStorage)。

localStore特点:

  • 保存的数据长期存在,下一次访问该网站的时候,网页可以直接读取以前保存的数据。
  • 大小为5M左右
  • 仅在客户端使用,不和服务端进行通信
  • 接口封装较好

sessionStore特点:

  • 会话级别的浏览器存储
  • 大小为5M左右
  • 仅在客户端使用,不和服务端进行通信
  • 接口封装较好

作用域:localStorage只要在相同的协议、相同的主机名、相同的端口下,就能读取/修改到同一份localStorage数据。sessionStorage比localStorage更严苛一点,除了协议、主机名、端口外,还要求在同一窗口(也就是浏览器的标签页)下。
应用场景:1.可以用它对表单信息进行维护,将表单信息存储存储在里面,可以保证页面即使刷新也不会让之前的表单信息丢失。2.可以用它存储本次浏览记录。如果关闭页面后不需要这些记录,用sessionStorage就再合适不过了。事实上微博采取这样的存储方式。

[Q5] 在css中link和@import的区别是什么?

[A5]

  1. 从属关系区别
    @import是 CSS 提供的语法规则,只有导入样式表的作用,import在html使用时候需要<style type="text/css">标签,同时可以直接@import url(CSS文件路径地址);放如css文件或css代码里引入其它css文件。;link是HTML提供的标签,不仅可以加载 CSS 文件,还可以定义 RSS、rel 连接属性等。
  2. 加载顺序区别
    加载页面时,link标签引入的 CSS 被同时加载;@import引入的 CSS 将在页面加载完毕后被加载。
  3. 兼容性区别
    @import是 CSS2.1 才有的语法,故只可在 IE5+ 才能识别;link标签作为 HTML 元素,不存在兼容性问题。
  4. DOM可控性区别
    可以通过 JS 操作 DOM ,插入link标签来改变样式;由于 DOM 方法是基于文档的,无法使用@import的方式插入样式。
  5. 权重区别
    link和@import,谁写在后面,谁的样式就被应用,后面的样式覆盖前面的样式

[Q6] 常见的loader以及作用的总结?

[A6]

  1. babel-loader: 将ECMAScript2015+ 版本的代码转换为向后兼容的Javascript语法,以便运行在当前和旧版本的浏览器中。
  2. less-loader、sass-loader、stylus-loader:css预处理器,将他们编译为css语法;postcss-loader:自动配置样式前缀,兼容浏览器;css-loader: 解析css语法,处理css中的 @import 和 url 这样的外部资源;style-loader: 将样式插入到 DOM中,方法是在head中插入一个style标签,并把样式写入到这个标签的 innerHTML里。
  3. file-loader、url-loader: url-loader可以将满足条件(将比较小的图片,一般是小于8k)的图片转换为base64,缺点是转换后比之前大,好处是不用发送http请求;不满足条件的url-loader会自动调用file-loader来处理图片,会对图片进行打包,返回打包后的路径。

[参考答案]

  1. raw-loader:加载文件原始内容(utf-8)
  2. file-loader:把文件输出到一个文件夹中,在代码中通过相对URL去引用输出的文件
  3. url-loader:和file-loader类似,但是能在文件很小的情况下以base64的方式把文件内容注入到代码中
  4. source-map-loader:加载额外的Source Map文件,以方便断点调试
  5. svg-inline-loader:将压缩后的 SVG 内容注入代码中
  6. image-loader:加载并且压缩图片文件
  7. json-loader 加载 JSON 文件(默认包含)
  8. handlebars-loader: 将 Handlebars 模版编译成函数并返回
  9. babel-loader:把ES6转化成ES5
  10. ts-loader: 将 TypeScript 转换成 JavaScript
  11. awesome-typescript-loader:将 TypeScript 转换成 JavaScript,性能优于 ts-loader
  12. css-loader:加载css,支持模块化、压缩、文件导入等特性
  13. style-loader:把css代码注入到js中,通过DOM操作去加载css
  14. eslint-loader:通过ESLint检查JS代码
  15. tslint-loader:通过 TSLint检查 TypeScript 代码
  16. postcss-loader:扩展 CSS 语法,使用下一代 CSS,可以配合 autoprefixer 插件自动补齐 CSS3 前缀
  17. vue-loader:加载 Vue.js 单文件组件
  18. cache-loader: 可以在一些性能开销较大的 Loader 之前添加,目的是将结果缓存到磁盘里

[Q7] webpack中source map是什么?生产环境怎么用?

[A7]
概念:通常,js代码出错,控制台会提示第几行第几列代码出错。但是webpack打包压缩后的代码,都被压缩到了一行,变量也变成了a,b,c,d。代码出错,控制台就没法正确的提示错误位置。sourceMap就可以解决这个问题。sourceMap就是一个信息文件,里面储存着打包前的位置信息。也就是说,sourceMap 提供了源代码到最终生成代码间的映射关系,能够帮助开发者方便的定位源代码。出错的时候,浏览器控制台将直接显示原始代码出错的位置,而不是转换后的代码,点击出错信息将直接跳转到原始代码位置。
生成环境:webpack打包仍然生成sourceMap,但是将map文件挑出放到本地服务器,将不含有map文件的部署到服务器,借助第三方软件(例如fiddler),将浏览器对map文件的请求拦截到本地服务器,就可以实现本地sourceMap调试。

[基础理解SourceMap]
[相关文章2:]

对于生产环境的配置……….

[参考答案] source map是将编译、打包、压缩后的代码映射回源代码的过程。打包压缩后的代码不具备良好的可读性,想要调试源码就需要 soucre map。
map文件只要不打开开发者工具,浏览器是不会加载的

线上环境一般有三种处理方案:
hidden-source-map:借助第三方错误监控平台Sentry使用
nosources-source-map:只会显示具体行数以及查看源代码的错误栈。安全性比source map高
source:通过nginx设置将.map文件只对白名单开放(公司内网)
注意的是:避免在生产中使用 inline- 和 eval- ,因为它们会增加bundle体积大小,并降低整体性能。

[Q8] 浏览器缓存机制(1)对于开发很重要,强缓存的内容能了解多少呢?

[A8]
浏览器缓存是浏览器对之前请求过的文件进行缓存,以便下一次访问时重复使用,节省带宽,提高访问速度,降低服务器压力。
http缓存机制主要在http响应头中设定,响应头中相关字段为Expires、Cache-Control、Last-Modified、Etag。
强缓存:强缓存通过Expires和Cache-Control两种响应头实现,浏览器不会向服务器发送任何请求,直接从本地缓存中读取文件并返回Status Code: 200 OK

https://github.com/amandakelake/blog/issues/41

[参考答案]
强缓存: 浏览器中的缓存作用分为两个情况,一种是需要发送HTTP请求,一种是不需要发送 首先是检查强缓存,这个阶段不需要发送http请求 如何来检查呢?通过相应的字段来进行,但是说起这个字段就有意思啦 在http/1.0和http/1.1当中,这个字段是不一样的。在早期,也就是http/1.0时期,使用的是Expires,而http/1.1使用的是Cache-Contronl

Expires
Expries即过期的时间,存在于服务端返回的响应头中,告诉浏览器在这个过期时间之前可以直接从缓存里面获取数据,无需再次请求

Expires: Wed, 22 Nov 2020 08:30:00 GMT
表示资源在2020年11月22号8点30分过期,过期了就得向服务端发送请求
这个方式看上去没什么问题,合情合理,但其实潜藏了一个坑,那就是服务器的时间和浏览器的时间可能并不一致,那服务器返回的这个过期时间可能就是不准确的,因此这种方式很快在后来的http/1.1版本就抛弃了。
Cache-Control
在http1.1中,采用了一个非常关键的字段:Cache-Control。 它和Expires本质的不同在于它并没有采用具体的过期时间点这个方式,而是采用过期时长来控制缓存,对应的字段是max-age,比如这个例子:

Cache-Control:max-age=3600
代表这个响应返回后在3600秒,也就是一个小时之内可以直接使用缓存
如果你觉得它只有max-age一个属性的话,那就想错了
它其实可以组合非常多的指令,完成更多的场景的缓存判断,将一些关键的属性列举如下:
public:客户端和代理服务器都可以缓存。因为一个请求可能要经过不同的代理服务器最后才到达目标服务器,那么结果就是不仅仅浏览器可以缓存数据,中间的任何代理节点都可以进行缓存
private:只有浏览器能缓存,中间的代理服务器不能缓存
no-cache:跳过当前的强缓存,发送http请求,即直接进入协商缓存阶段
no-store:非常简单粗暴,不进行任何形式的缓存
s-maxage:这和max-age长得非常像,但是区别在于s-maxage是针对代理服务器的缓存时间。
值得注意的是:当Expires和Cache-Control同时存在的话,Cache-Control会优先考虑 还存在一种情况是:当资源缓存时间超时了,也就是强缓存失效了,接下就要进入第二部分–协商缓存

[Q9] react-router里的Link标签和a标签有什么区别?

[参考答案]
区别
从最终渲染的DOM来看,这两者都是链接,都是a标签,区别是: Link标签是react-router里实现路由跳转的链接,一般配合Route使用,react-router接下了其默认的链接跳转行为,区别于传统的页面跳转,Link标签的”跳转”行为只会触发相匹配的Route对应的页面内容更新,而不会刷新整个页面

Link标签做的三件事情:

1.有onclick那就执行onclick
2.click的时候阻止a标签默认事件
3.根据跳转href(即使是to),用history(web前端路由两种方式之一,history&hash)跳转,此时只是链接变了,并没有刷新页面
而标签就是普通的超链接了,用于从当前页面跳转到href指向的里一个页面(非锚点情况)

a标签默认事件禁掉之后做了什么才实现了跳转?

1
2
3
4
5
6
let domArr=document.getElementByTagName('a');
[...domArr].forEach(item=>{
item.addEventListener('click',function(){
location.href=this.href
})
})

[Q10] 能不能说一说XSS攻击?

[A10]
XSS的原理是:恶意攻击者在web页面中会插入一些恶意的script代码。当用户浏览该页面的时候,那么嵌入到web页面中script代码会执行,因此会达到恶意攻击用户的目的。
XSS攻击的危害包括
1、盗取各类用户帐号,如机器登录帐号、用户网银帐号、各类管理员帐号
2、控制企业数据,包括读取、篡改、添加、删除企业敏感数据的能力
3、盗窃企业重要的具有商业价值的资料
4、非法转账
5、强制发送电子邮件
6、网站挂马
7、控制受害者机器向其它网站发起攻击

[参考答案]
XSS全称是Cross Site Scripting[跨站脚本],为了和css区分,故叫它xss。XSS攻击是指浏览器中执行恶意脚本(无论是跨域还是同域),从而拿到用户的信息进行操作。

这些操作一般可以完成下面这些事情 1.窃取Cookie 2.监听用户行为,比如输入账号密码后直接发送到黑客服务器 3.修改DOM伪造登录表单 4.在页面中生成浮窗广告

通常情况下,XSS攻击的实现有三种方式 — 存储型、反射型和文档型。原理比较简单,一一介绍

存储型
存储型,将恶意脚本存储了起来,确实,存储型的XSS将脚本存储到了服务端的数据库,然后在客户端执行这些脚本,从而达到攻击的效果

常见的场景是留言评论区提交一段代码,如果前后端没有做好转义的工作,那评论内容存到了数据库,在页面渲染过程中直接执行,相当于执行一段位置逻辑的js代码,是非常恐怖的。这就是存储型的xss攻击

反射型
反射型xss指的是恶意脚本作为网络请求的一部分 比如我输入

http://baidu.com?q=<script>alert("你完蛋了")</script>
这样在服务端会拿到q参数,然后将内容返回给浏览器端,浏览器将这些内容作为HTML的一部分解析,发现是一个脚本,直接执行,这样被攻击了

之所以叫它反射型,是因为恶意脚本是通过作为网络请求的参数,经过服务器,然后在反射到HTML文档中,执行解析。和存储型不一样的是:服务器并不会存储这些恶意脚本

文档型
文档型的XSS攻击并不会经过服务端,而是作为中间人的角色,在数据传输过程劫持到网络数据包,然后修改里面的html文档 这样的劫持方式包括wifi路由劫持或者本地恶意软件等

防范措施
明白三种xss攻击的原理,发现一个共同点:都是让恶意脚本直接能在浏览器中执行 那么要防范它,就是要避免这些脚本代码的执行 为了完成这一点,必须做到一个信念,两个利用。

一个信念
千万不要相信任何用户的输入! 无论是在前端和服务端,都要对用户的输入进行转码或过滤

如

<script>alert('你完了!')</script>
转码后变为:

<script>alert('你完蛋了')</script>
这样的代码在html解析的过程中是无法执行的 当然也可以利用关键词过滤的方式,将script标签给删除。那么现在的内容只剩下

什么都没有

利用CSP
CSP,即浏览器中的内容安全策略,它的核心思想就是服务器决定浏览器加载哪些资源,具体来说可以完成以下功能: 1.限制其他域下的资源加载 2.禁止向其他域提交数据 3.提供上报机制,能帮助我们及时发现XSS攻击

利用HttpOnly
很多XSS攻击脚本都是用来窃取Cookie,而设置Cookie的HttpOnly属性后,JavaScript便无法读取Cookie的值,这样也很好的防范XSS攻击。

总结
xss攻击是指浏览器中执行恶意脚本,然后拿到用户的信息进行操作。主要分为存储型、反射型和文档型。防范的措施包括:

一个信念:不要相信用户的输入,对输入的内容转码或者过滤,让其不可执行 两个利用:利用CSP,利用Cookie的HttpOnly属性

[Q11] 谈谈你对重绘和回流的理解?

[A11]

回流也叫重排。有以下的操作会触发回流:

  1. 一个 DOM 元素的几何属性变化,常见的几何属性有width、height、padding、margin、left、top、border 等等, 这个很好理解。
  2. 使 DOM 节点发生增减或者移动。
  3. 读写 offset族、scroll族和client族属性的时候,浏览器为了获取这些值,需要进行回流操作。
  4. 调用 window.getComputedStyle 方法。

当 DOM 的修改导致了样式的变化,并且没有影响几何属性的时候,会导致重绘(repaint)。

优化:减少重绘和重排:   

  1. 不要一条一条地修改 DOM 的样式。可以先定义好 css 的 class,然后修改 DOM 的 className。
  2. 不要把 DOM 结点的属性值放在一个循环里当成循环里的变量。
  3. 为动画的 HTML 元件使用 fixed 或 absoult 的 position,那么修改他们的 CSS 是不会 reflow 的。
  4. 千万不要使用 table 布局。因为可能很小的一个小改动会造成整个 table 的重新布局。table及其内部元素除外,它可能需要多次计算才能确定好其在渲染树中节点的属性,通常要花3倍于同等元素的时间。这也是为什么我们要避免使用table做布局的一个原因。
  5. 不要在布局信息改变的时候做查询(会导致渲染队列强制刷新)  

[参考答案]
http://www.mamicode.com/info-detail-2890451.html

[Q12] 简单说下你理解的语义化,怎样来保证你写的符合语义化?HTML5语义化标签了解下?

[A12]

  1. HTML语义化:就是页面去掉样式或者加载失败的时候能够让页面呈现出清晰的结构。HTML5新增了好多语义化的标签,例如:header、footer、nav、menu、section、article等等,单单从字面上理解,就知道标签的含义。在写页面的时候,我们可以直接引用这些标签,不需要再用没有任何含义的div标签了,对于机器可以识别,对于开发人员很容易明白。这就是HTML语义化。

  2. 语义化的好处:
    1)为了在没有CSS的情况下,页面也能呈现出很好地内容结构、代码结构
    2)用户体验:例如title、alt用于解释名词
    3)有利于SEO:利于被搜索引擎收录,更便于搜索引擎的爬虫程序来识别
    4)方便其他设备解析(如屏幕阅读器、盲人阅读器、移动设备)以意义的方式来渲染网页
    5)便于项目的开发及维护,使HTML代码更具有可读性

[参考答案]
很多时候我们写HTML,为了方便都会直接使用div和span标签,在通过class来确定具体样式。网站哪一部分为标题,哪一部分为导航,哪一部分为头部和尾部,都只能通过class来进行确定。 但是class命名规范却又没有一套统一的标准,依次导致很多时候无法确定整体网站的结构 因此,在HTML5出现后,添加了关于页面布局结构的新标签。而在html书写过程中,根据不同的内容使用合适的标签进行开发,即为语义化。

在编程中,语义指的是一段代码的含义(这个HTML的元素有什么作用,扮演了什么样的角色)。HTML语义元素清楚地向浏览器和开发者描述其意义,例如form、table以及img等

1.优点:对搜索引擎友好,有了良好的结构和语义,网页内容自然容易被搜索引擎抓取
2.HTML5新增语义元素 article aside details figcaption figure footer> header main mark nav section summary time
3.为什么要语义化?
语义化的优势主要在于以下几点:
其他开发者便于阅读代码,通过不同标签明白每个模块的作用和区别
结构明确、语义清晰的页面能有更好的用户体验,在样式(css)没有加载前也有较为明确的结构,更如img这一类的,在图片无法加载的情况下有alt标签告知用户此处图片的具体内容;
利于seo,语义化便于搜索引擎爬虫理解,和搜索引擎建立良好的沟通,能让爬虫爬取更多关键有效的信息
方便其他设备阅读(如屏幕阅读器,盲人设备和移动设备等)
4.如何语义化?
一般的网站分为头部、导航、文章(或其他模块)、侧栏、底部,根据不同的部位,使用不同的标签进行书写。
表示页面不同位置的标签:header、nav、article、section、footer、aside
表示具体元素的作用或者意义的标签:a、abbr、address、audio、blockquote、caption、code、datalist、del、detail、ol、ul、figure、figuration、img、input、mark、p等

  • 尽可能少的使用无语义的标签div和span
  • 在语义不明显时,既可以使用div或者p时,尽量用p,因为p在默认情况下有上下间距,对兼容特殊终端有利;
  • 不要使用纯样式标签,如b、font、u等,改用css设置
  • 需要强调的文本,可以包含在strong或者em标签中(浏览器预设样式,能用css指定就不用他们),strong默认样式是加粗(不要加b),em是斜体(不用i)
  • 使用表格时,标题要用caption,表头用thead,主体部分用tbody包围,尾部用tfoot包围。表头和一般单元格要区分开,表头用th,,单元格用td
  • 表单域要用fieldset标签包起来,并用legend标签说明表单的用途
  • 每个input标签对应的说明文件都需要使用label标签。并且通过input设置为id属性,在label标签中设置for=someld来让说明文本和相对应的input关联起来

5.注意点: em、strong、dfn、code、samp、kbd、var、cite等,虽然这些标签定义的文本大多会呈现处特殊的样式,但实际上,这些标签都拥有确切的语义 我们并不反对使用它们,但是如果您只是为了达到某种视觉效果而使用这些标签的话,我们建议您可以使用样式表,那么做会达到更加丰富的效果。

[Q13] 常见的plugin以及作用的总结

[A13]

  1. clean-webpack-plugin 每次打包之前都清空dist目录下的文件
  2. html-webpack-plugin 配置打包生成的html,以哪个文件为模板、生成的html文件名、压缩配置等。
  3. mini-css-extract-plugin 抽离css样式,压缩配置
  4. speed-measure-webpack-plugin 费时分析,可以查看打包、构建速度,很好的优化分析工具
  5. optimize-css-assets-webpack-plugin css压缩
  6. terser-webpack-plugin js压缩,配置drop_console: true 去掉所有console.log输出
  7. @sentry/webpack-plugin 配置Sentry服务
  8. new webpack.HotModuleReplacementPlugin() 模块热替换
  9. webpack-bundle-analyzer: 可视化 Webpack 输出文件的体积 (业务组件、依赖第三方模块) 分析工具

[参考答案]

  1. define-plugin:定义环境变量(Webpack4 之后指定 mode 会自动配置)
  2. ignore-plugin:忽略部分文件
  3. commons-chunk-plugin:提取公共代码
  4. webpack-parallel-uglify-plugin: 多进程执行代码压缩,提升构建速度
  5. serviceworker-webpack-plugin:为网页应用增加离线缓存功能
  6. ModuleConcatenationPlugin: 开启 Scope Hoisting

前端错误监控 Sentry

发表于 2020-08-01 | 更新于: 2020-08-21 | 分类于 Sentry

1.Linux安装Sentry

我这里用的是阿里云服务器:CenterOS7.8,CUP1核,内存4G。

步骤:
1、 [CenterOS安装Docker]
2、[docker配置阿里镜像] 这步很重要,速度不知道快了多少
3、安装docker-compose 官网地址:https://docs.docker.com/compose/install/
太慢了,我这里用phyon装的
看这里https://blog.csdn.net/u010381752/article/details/90295010

1
2
yum install -y python-pip
pip install docker-compose

4、看 github上的文档 onpremise,如下图(【这步很重要】,就是这里看到的最低需要2400MB RAM):

5、执行:

1
2
git clone https://github.com/getsentry/onpremise.git
cd onpremise

6、 创建配置文件

1
2
cp -n config.example.yml config.yml
cp -n sentry.conf.example.py sentry.conf.py

7、配置邮箱

在onpremise/sentry下,找到conf.yml配置如下:

[注意]

  1. 这里的mail.from和mail.username一样,都是邮箱地址

  2. mail.password是邮箱的授权密码,登录邮箱去POP3/SMTP/IMAP设置下开启服务获取密码即可。

8、执行:

1
./install.sh  # 这步比较慢,耐心等待

创建成功,中间会让你输入是否创建新账号,输入邮箱和密码。
成功之后会出现

1
2
3
You're all done! Run the following command to get Sentry running:

docker-compose up -d

最后

1
docker-compose up -d

Sentry服务器跑起来了。

9、去阿里配置可访问端口:
进入到当前实例-安全组配置-右上角选择旧版,因为新版我没有找到可以配置端口范围的地方,配置内容如下:

好了之后就可以直接用浏览器打开 ip:9000 ,登录之后需要配置邮件服务的账号密码登录(上面配置过的应该不用了),如果之前config中没有配置,这里会显示,就配置一下。
完成~

2. 玩转Sentry

基础配置完成,进入到Sentry界面。

2.1 接入邮件|钉钉服务

在onpremise/sentry/config.yml已经配置了邮件信息的基础上,打开Sentry界面,manage/status/mail/下 按钮Send a test email to XXX,报

1
Connection unexpectedly closed: timed out

安装插件django_smtp_ssl

1
pip install django-smtp-ssl

配置onpremise/sentry/config.yml

1
mail.backend: 'django_smtp_ssl.SSLEmailBackend'

配置onpremise/sentry/requirements.txt

1
2
3
# Add plugins here
sentry-dingtalk-new # 钉钉通知插件
django-smtp-ssl~=1.0 # 发邮件支持SSL协议

重新启动

1
2
./install.sh
docker-compose up -d

点击测试按钮,再发送测试邮件就能发送成功了。

测试邮箱如上图能够发送成功,但是我本地测试了一下,当项目中报错时,错误日志信息已经发送到Sentry服务,但是并没有发送邮件。(我又点了发送测试邮件的按钮,能够发送邮件)

配置钉钉:看这个步骤https://my.oschina.net/u/4290244/blog/3349141
获取自定义钉钉webhook https://ding-doc.dingtalk.com/doc#/serverapi2/qf2nxq

【邮箱、钉钉一样,测试都能发送消息,实际项目中报错的时候都不能发送。】

邮箱一直能收到测试邮件….

1
[This is an example Python exception] This is an example Python exception http://xxx.xxx.xxx.xxx:9000/organizations/sentry/issues/7/

点钉钉的测试按钮,机器人也能自动发送消息….

针对测试邮件能够发送,报错邮件不能发送问题https://blog.csdn.net/github_38281308/article/details/106905367,查看这个发型可能跟docker有关,待研究docker…

2.2 性能分析

2.3 SourceMap

生产环境打包后的代码,快速定位到源码。

1、 安装插件
yarn add @sentry/webpack-plugin --dev

2、根目录下新建 .sentryclirc,配置如下

1
2
3
4
5
6
[defaults]
url = http://xxx.xxx.xxx.xxx:9000/
org = sentry
project = react
[auth]
token = xxxxxxxxxxxxxx

① url是sentry上报的网址
② ③如下图

④ token就是 API keys,Auth Tokens 新建一个令牌(注意生成token的时候要勾选 project:write 选项)

3、配置插件参数
生产打包webpack配置:

1
2
3
4
5
6
7
new SentryPlugin ({
release: "production@1.0.1",//版本号
include: path.join(__dirname,'../dist/'), //需要上传到sentry服务器的资源目录,会自动匹配js 以及map文件
ignore: ['node_modules'], //忽略文件目录,当然我们在inlcude中制定了文件路径,这个忽略目录可以不加
configFile :'.sentryclirc',
urlPrefix : "~/" // 线上对应的url资源的相对路径 比如我的域名是 http://XXX .com/,静态资源都在 static文件夹里面,
})

webpack打包之后可以在Sentry服务端->版本 一栏查看到版本号 1.0.1

4、配置Sentry dns参数

1
2
3
4
Sentry.init({
dsn: "http://73779960f3a74f4b893f54b81b386020@121.40.178.235:9000/2",
release: "production@1.0.1" // 这个版本号与上一步一致
});

5、 配置devtool

1
devtool:isDev?'eval-cheap-module-source-map' : 'nosources-source-map', // 'source-map'


从打包时间来看,nosources-source-map 耗时 58s左右,source-map 耗时 20s左右,可看具体需求配置。

这样的话基本 source map配置就结束了,有几个问:

1) @sentry/wepback-plugin 配置之后,打包之后的代码带.map文件,不能自动删除,部署到服务器时的时候需要手动删除。
2) 生产打包时同一个版本传到Sentry,.map和.js文件会一直追加,文件越来越多,需要手动删除。

解决方案:可以尝试用webpack-sentry-plugin插件代替,上传完之后可自动删除.map文件且可以过滤.js文件不上传到Sentry服务器上,但是我没配成功….

5、 配置 environment (可选)

入口文件配置更改如下:

1
2
3
4
5
Sentry.init({
dsn: "http://xxxxx@xxx.xxx.xxx.xxx:9000/x",
release: "production@1.0.3",
environment: 'env_react1', // process.env.NODE_ENV 可以在Sentry->版本->环境区分
});

配置了environment之后,可以在Sentry服务后台左侧版本菜单下查看,同一版本的还是放在一个版本下面,然后可以用环境进行区分

3. 旧版报错方案

这部分的报错过程都是按照 [Linux下搭建Sentry] 这种方式安装的,没有仔细看 onpremise github文档导致的~记录下,毕竟装的时候太折磨了…

  1. [dial tcp: lookup production.cloudflare.docker.com]
  2. [docker启动失败报错:Failed to start docker.service: Unit is not loaded properly: Invalid argument]
  3. [ERROR: Service ‘sentry-cleanup’ failed to build: pull access denied for sentry-onpremise-local, repository does not exist or may require ‘docker login’: denied: requested access to the resource is denied],执行docker-compose build --pull --force-rm web
  4. [FAIL: Expected minimum RAM available to Docker to be 2400 MB but found 1991 MB]

安装完成之后 docker login登录一下,之后可能会有一些权限报错。

Babel 7

发表于 2020-07-31 | 更新于: 2020-08-06 | 分类于 Babel

1. babel配置

Babel 是一个工具链,主要用于将 ECMAScript 2015+ (又可称为ES6,ES7,ES8等)版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中(如低版本的 node 环境中)。

1.1 @babel/cli @babel/core

create-react-app myapp 新建项目

[Install]
yarn add @babel/cli @babel/core --dev

@babel/cli 是babel提供的命令行工具,主要是提供babel这个命令。 官网推荐安装在项目中,而不是安装在全局环境,因为每个项目用的babel的版本不一样。可以单独管理和升级。

Babel 的核心功能包含在 @babel/core 模块中。不安装 @babel/core,无法使用 babel 进行编译。

修改 package.json, 在 script 里面新增

1
"babel": "babel app.js -o ./dist/app.js"

App.js

1
2
3
4
5
6
7
8
9
10
11
12
13
import React from 'react';

let func = () => {
return 1;
};

function App() {
return <div>
1
{func()}
</div>;
}
export default App;

执行 yarn babel,报错:

1
2
3
{ SyntaxError: E:\hjf\test\babel-app\src\App.js: Support for the experimental syntax 'jsx' isn't currently enabled 

Add @babel/preset-react (https://git.io/JfeDR) to the 'presets' section of your Babel config to en able transformation.

1.2 @babel/plugin-syntax-jsx @babel/preset-react

[Install]

yarn add @babel/plugin-syntax-jsx --dev

新建 babel.config.js

1
2
3
4
5
module.exports = {
plugins: [
"@babel/plugin-syntax-jsx"
]
}

执行 yarn babel,生成/dist/app.js文件

1
2
3
4
5
6
7
8
9
10
11
import React from 'react';
let func = () => {
return 1;
};
function App() {
return <div>
1
{func()}
</div>;
}
export default App;

没有任何变化…

[Install]

yarn add @babel/preset-react --dev

babel.config.js

1
2
3
4
5
6
7
8
module.exports = {
+ presets: [
+ "@babel/preset-react"
+ ],
plugins: [
"@babel/plugin-syntax-jsx"
]
}

执行 yarn babel,生成/dist/app.js文件

1
2
3
4
5
6
7
8
9
10
11
import React from 'react';

let func = () => {
return 1;
};

function App() {
return /*#__PURE__*/React.createElement("div", null, "1", func());
}

export default App;

转换 JSX 语法

1.3 @babel/plugin-transform-arrow-functions

[Install]
yarn add @babel/plugin-transform-arrow-functions --dev

执行 yarn babel,生成/dist/app.js文件

1
2
3
4
5
6
7
8
9
10
import React from 'react';

let func = function () {
return 1;
};

function App() {
return /*#__PURE__*/React.createElement("div", null, "1", func());
}
export default App;

转化箭头函数,这里let没有转化

1.4 preset(预设的插件集合)

官网
预设的插件包,不需要一个一个引入

【官方 Preset】:针对常用环境编写了一些 preset:

  • @babel/preset-env
  • @babel/preset-flow
  • @babel/preset-react
  • @babel/preset-typescript
  1. Babel 可以删除类型注释!Babel 不做类型检查,但仍然需要安装 Flow 或 TypeScript 来执行类型检查的工作。
  2. @babel/preset-env 官方描述:
    1
    @babel/preset-env is a smart preset that allows you to use the latest JavaScript without needing to micromanage which syntax transforms (and optionally, browser polyfills) are needed by your target environment(s). This both makes your life easier and JavaScript bundles smaller!

@babel/preset env是一个智能预置,它允许您使用最新的JavaScript,而无需对目标环境所需的语法转换(以及可选的浏览器polyfill)进行微观管理。这不仅让您的生活更轻松,而且JavaScript捆绑包更小!

同时使用多个Plugin和Preset时的执行顺序:

  1. 先执行完所有Plugin,再执行Preset。
  2. 多个Plugin,按照声明次序顺序执行。
  3. 多个Preset,按照声明次序逆序执行。

1.4.1 @babel/preset-env

[Install]
yarn add @babel/preset-env --dev

babel.config.js

1
2
3
4
5
6
7
8
9
module.exports = {
presets: [
"@babel/preset-react",
+ "@babel/preset-env"
],
plugins: [
"@babel/plugin-syntax-jsx"
]
}

执行 yarn babel,生成/dist/app.js文件,转化成功

1
2
3
4
5
"use strict";

var func = function func() {
return 1;
};

1.4.2 @babel/preset-flow @babel/preset-typescript

[Install]
yarn add @babel/preset-flow @babel/preset-typescript --dev

babel.config.js

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
presets: [
"@babel/preset-react",
"@babel/preset-env",
+ "@babel/preset-flow",
+ "@babel/preset-typescript"
],
plugins: [
"@babel/plugin-syntax-jsx"
]
}

安装官网推荐的预设插件包

1.5 @babel/polyfill

App.js 新增代码

1
2
let arr = [1, 2, 4]
arr.includes(3);

执行 yarn babel,生成/dist/app.js文件,includes 并没有转化

1
2
3
4
"use strict";

var arr = [1, 2, 4];
arr.includes(3);

[Install]
yarn add @babel/polyfill

接下去阅读相关文章

2. 错误

2.1 [报错]

1
2
3
var _ref = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee(k) {
^
ReferenceError: regeneratorRuntime is not defined

[解决]
在 babel.config.js中加入plugin

1
2
3
plugins:[
"@babel/plugin-transform-runtime"
]

3. 相关文章

  1. https://www.babeljs.cn/docs/
  2. https://juejin.im/post/6844904008679686152
  3. https://juejin.im/post/6844904132294213639#heading-0
  4. https://www.jianshu.com/p/cbd48919a0cc

脚手架 CLI

发表于 2020-07-29 | 更新于: 2020-07-31 | 分类于 脚手架

1. 创建模板

我的模板:https://api.github.com/users/jiafei2333/repos 选择 react-template

2. 搭建脚手架

代码地址:https://github.com/jiafei2333/cli

3. READEME

3.1 介绍

前端开发CLI工具

3.2 包使用步骤

  1. 全局安装:npm install -g jiafei2333-cli

  2. 查看命令:jiafei2333-cli

  3. 安装本地配置文件:jiafei2333-cli config set a 1

  4. 下载模板:jiafei2333-cli i | install 这里模板选择 react-template,也可以配置自己的模板

3.3 脚手架使用步骤

  1. yarn compile 编译

  2. jiafei2333-cli 查看命令行信息

  3. 更改模板配置(二选一):

    • 3.1 修改 utils/constants 中 DEFAULTS 的配置
    • 3.2 执行 jiafei2333-cli config set registry xxxx | jiafei2333-cli config set type xxxx 即可
  4. 发布流程

    • 4.1 nrm use npm 切换npm源 (不是淘宝镜像可以跳过)
    • 4.2 npm adduser (第一次发包 npm login 先登录)
    • 4.3 npm publish (发布、更新包)

React-Redux Hooks

发表于 2020-07-24 | 更新于: 2020-07-24 | 分类于 Redux , React

1. useSelector

1
2
3
4
5
6
7
import React from 'react'
import { useSelector } from 'react-redux'

export const TodoListItem = props => {
const todo = useSelector(state => state.todos[props.id])
return <div>{todo.text}</div>
}

2. 计算衍生数据

2.1 创建可记忆的 Selector

我们需要一个可记忆的 selector 来替代这个 getVisibleTodos,只在 state.todos or state.visibilityFilter 变化时重新计算 todos,而在其它部分(非相关)变化时不做计算。

Reselect 提供 createSelector 函数来创建可记忆的 selector。createSelector 接收一个 input-selectors 数组和一个转换函数作为参数。如果 state tree 的改变会引起 input-selector 值变化,那么 selector 会调用转换函数,传入 input-selectors 作为参数,并返回结果。如果 input-selectors 的值和前一次的一样,它将会直接返回前一次计算的数据,而不会再调用一次转换函数。

定义一个可记忆的 selector getVisibleTodos:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { createSelector } from 'reselect'

const getVisibilityFilter = state => state.visibilityFilter
const getTodos = state => state.todos

export const getVisibleTodos = createSelector(
[getVisibilityFilter, getTodos],
(visibilityFilter, todos) => {
switch (visibilityFilter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
}
}
)

在上例中,getVisibilityFilter 和 getTodos 是 input-selector。因为他们并不转换数据,所以被创建成普通的非记忆的 selector 函数。但是,getVisibleTodos 是一个可记忆的 selector。他接收 getVisibilityFilter 和 getTodos 为 input-selector,还有一个转换函数来计算过滤的 todos 列表。

3. useDispatch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from 'react'
import { useDispatch } from 'react-redux'

export const CounterComponent = ({ value }) => {
const dispatch = useDispatch()

return (
<div>
<span>{value}</span>
<button onClick={() => dispatch({ type: 'increment-counter' })}>
Increment counter
</button>
</div>
)
}

4. useStore

1
2
3
4
5
6
7
8
9
10
import React from 'react'
import { useStore } from 'react-redux'

export const CounterComponent = ({ value }) => {
const store = useStore()

// EXAMPLE ONLY! Do not do this in a real app.
// The component will not automatically update if the store state changes
return <div>{store.getState()}</div>
}

相关文章

  1. https://react-redux.js.org/api/hooks
  2. https://cn.redux.js.org/docs/recipes/ComputingDerivedData.html

前端监控

发表于 2020-06-18 | 更新于: 2021-05-25 | 分类于 前端监控

1.准备工作

【为什么要做前端监控】

初始化项目

1
npm init -y

新建 webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const path = require('path');
//HtmlWebpackPlugin自动产出HTML文件 user-agent 把浏览器的UserAgent变成一个对象
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',//入口文件
context: process.cwd(),//上下文目录
mode: 'development',//开发模式
output: {
path: path.resolve(__dirname, 'dist'),//输出目录
filename: 'monitor.js'//文件名
},
devServer: {
contentBase: path.resolve(__dirname, 'dist')//devServer静态文件根目录
},
plugins: [
new HtmlWebpackPlugin({//自动打包出HTML文件的
template: './src/index.html',
inject: 'head'
})
]
}

新建 src/index.html

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>前端监控SDK</title>
</head>
<body>

</body>
</html>

新建 src/index.js

安装包

1
npm install webpack webpack-cli webpack-dev-server html-webpack-plugin user-agent -D

修改package.json

1
2
3
4
"scripts": {
"build": "webpack",
"dev": "webpack-dev-server"
}

2.编写监控采集脚本

2.1 开通日志服务

  • 日志服务(Log Service,简称 SLS)是针对日志类数据一站式服务,用户无需开发就能快捷完成数据采集、消费、投递以及查询分析等功能,帮助提升运维、运营效率,建立 DT 时代海量日志处理能力
  • 日志服务帮助文档
  • Web Tracking

2.1.1 步骤

1、【创建Project】 : 名称(front-end-monitor-test)、所属地域(杭州) 确定;
2、【创建logStore】:名称(font-end-monitor-store)、开启WebTracking 确定;
3、【创建成功,立即接入数据】:搜索WebTracking SDK、下一步、开启包含中文、下一步
4、【完成数据接入配置】:查询日志->立即尝试
5、【有数据之后设置索引】:https://sls.console.aliyun.com/lognext/profile 找到下面的Project下的项目,进入,查询分析属性-设置-开启日志聚类-自动生成索引-确定-确定

2.2 监控错误

2.2.1 错误分类

  • JS错误
    • JS错误
    • Promise异常
  • 资源异常
    -监听error

2.2.2 数据结构设计

1.jsError

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"title": "前端监控系统",//页面标题
"url": "http://localhost:8080/",//页面URL
"timestamp": "1590815288710",//访问时间戳
"userAgent": "Chrome",//用户浏览器类型
"kind": "stability",//大类
"type": "error",//小类
"errorType": "jsError",//错误类型
"message": "Uncaught TypeError: Cannot set property 'error' of undefined",//类型详情
"filename": "http://localhost:8080/",//访问的文件名
"position": "0:0",//行列信息
"stack": "btnClick (http://localhost:8080/:20:39)^HTMLInputElement.onclick (http://localhost:8080/:14:72)",//堆栈信息
"selector": "HTML BODY #container .content INPUT"//选择器
}

2.promiseError

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"title": "前端监控系统",//页面标题
"url": "http://localhost:8080/",//页面URL
"timestamp": "1590815290600",//访问时间戳
"userAgent": "Chrome",//用户浏览器类型
"kind": "stability",//大类
"type": "error",//小类
"errorType": "promiseError",//错误类型
"message": "someVar is not defined",//类型详情
"filename": "http://localhost:8080/",//访问的文件名
"position": "24:29",//行列信息
"stack": "http://localhost:8080/:24:29^new Promise (<anonymous>)^btnPromiseClick (http://localhost:8080/:23:13)^HTMLInputElement.onclick (http://localhost:8080/:15:86)",//堆栈信息
"selector": "HTML BODY #container .content INPUT"//选择器
}

3.resourceError

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"title": "前端监控系统",//页面标题
"url": "http://localhost:8080/",//页面URL
"timestamp": "1590816168643",//访问时间戳
"userAgent": "Chrome",//用户浏览器类型
"kind": "stability",//大类
"type": "error",//小类
"errorType": "resourceError",//错误类型
"filename": "http://localhost:8080/error.js",//访问的文件名
"tagName": "SCRIPT",//标签名
"timeStamp": "76",//时间
"selector": "HTML BODY SCRIPT"//选择器
}

2.2.3 实现

2.2.3.1 收集log信息

src/index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<body>
<div id="container">
<div class="content">
<input id="errorBtn" type="button" value="点击抛出错误" onclick="errorClick()" />
<input id="promiseBtn" type="button" value="点击抛出Promise错误" onclick="promiseErrorClick()" />
</div>
</div>
<script>
// JS错误
function errorClick() {
window.someVar.error = 'error';
}
// Promise错误
function promiseErrorClick() {
new Promise(function (resolve, reject) {
//window.someVar.error = 'error'; // 或者
reject('error --');
}).then(result=>{
console.log("result:", result);
});
}
</script>
<!-- 加载资源错误 -->
<script src="./someError.js"></script>
</body>

src/index.js

1
import './monitor/index';

新建 src/monitor/index.js

1
2
import {injectJsError} from './lib/jsError';
injectJsError();

新建 src/monitor/lib/jsError.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import getLastEvent from '../util/getLastEvent';
import getSelector from '../util/getSelector';
import tracker from '../util/tracker';
export function injectJsError(){
// 监听全局未捕获的错误
window.addEventListener('error', function(event){ // 错误实践对象
console.log("error: ",event);
let lastEvent = getLastEvent(); // 监听到错误之后,获取到最后一个交互事件
// console.log("lastEvent:", lastEvent);
// 这是一个脚本加载错误
if (event.target && (event.target.src || event.target.href)) {
tracker.send({//资源加载错误
kind: 'stability',//稳定性指标
type: 'error',//resource
errorType: 'resourceError', // js或css资源加载错误
filename: event.target.src || event.target.href,//加载失败的资源
tagName: event.target.tagName,//标签名
// timeStamp: formatTime(event.timeStamp),//时间
selector: getSelector(event.path || event.target),//选择器
})
}else{
tracker.send({
kind: 'stability',//监控指标的大类:stability'稳定性指标
type: 'error', // 小类型,这是一个错误
errorType: 'jsError', //JS执行错误
url: '', // 访问哪个路径报错了s
message:event.message, // 报错信息
filename: event.filename, // 哪个文件报错了
position: `${event.lineno}:${event.colno}`,//行列号
stack: getLines(event.error.stack), // 错误堆栈
selector: lastEvent ? getSelector(lastEvent.path) : '', // 选择器,代表最后一个操作的元素
});
}
}, true); // true代表在捕获阶段调用,false代表在冒泡阶段捕获,使用true或false都可以 (这里是捕获阶段去出发,冒泡阶段有可能阻止冒泡就捕获不到了)
//当Promise 被 reject 且没有 reject 处理器的时候,会触发 unhandledrejection 事件
window.addEventListener('unhandledrejection', function (event) {
console.log("unhandledrejection:",event);

let lastEvent = getLastEvent();
let message = '';
let line = 0;
let column = 0;
let file = '';
let stack = '';
if (typeof event.reason === 'string') {
message = event.reason;
} else if (typeof event.reason === 'object') {
message = event.reason.message;
}
let reason = event.reason;
if (typeof reason === 'object') {
if (reason.stack) {
var matchResult = reason.stack.match(/at\s+(.+):(\d+):(\d+)/);
if (matchResult) {
file = matchResult[1];
line = matchResult[2];
column = matchResult[3];
}
stack = getLines(reason.stack);
}
}
tracker.send({//未捕获的promise错误
kind: 'stability',//稳定性指标
type: 'error',//jsError
errorType: 'promiseError',//unhandledrejection
message: message,//标签名
filename: file,
position: line + ':' + column,//行列
stack,
selector: lastEvent ? getSelector(lastEvent.path || lastEvent.target) : ''
})
}, true);// true代表在捕获阶段调用,false代表在冒泡阶段捕获,使用true或false都可以

function getLines(stack){
if (!stack) {
return '';
}
return stack.split('\n').slice(1).map(item => item.replace(/^\s+at\s+/g, '')).join('^');
}
}

新建 src/monitor/util/getLastEvent.js

1
2
3
4
5
6
7
8
9
10
11
12
13
let lastEvent;
// 每次在用户进行下面这些事件交互时,都会把最新的event赋值给lastEvent
['click','pointerdown', 'touchstart', 'mousedown', 'keydown', 'mouseover'].forEach(event => {
document.addEventListener(event, (event) => {
lastEvent = event;
}, {
capture: true,//capture 控制监听器是在捕获阶段执行还是在冒泡阶段执行
passive: true //passive 的意思是顺从的,表示它不会对事件的默认行为说 no
});
});
export default function () {
return lastEvent;
};

新建 src/monitor/util/getSelector.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const getSelector = function (path) {
return path.reverse().filter(function (element) {
return element !== window && element !== document;
}).map(function (element) {
var selector;
if (element.id) {
selector = `#${element.id}`;
} else if (element.className && typeof element.className === 'string') {
selector = '.' + element.className.split(' ').filter(function (item) { return !!item }).join('.');
} else {
selector = element.nodeName;
}
return selector;
}).join(' ');
}
// pathsOrTarget [input#errorBtn, div.content, div#container, body, html, document, Window]
export default function (pathsOrTarget){
if (Array.isArray(pathsOrTarget)) { // 有可能是数组
return getSelector(pathsOrTarget);
} else { // 也有可能是对象
var paths = [];
var element = pathsOrTarget;
while (element) {
paths.push(element);
element = element.parentNode;
}
return getSelector(paths);
}
}

2.2.3.2 上报

新建 src/util/tracker.js

PutWebTracking

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
const project = 'front-end-monitor-test';
const endpoint = 'cn-hangzhou.log.aliyuncs.com';
const logstoreName = 'font-end-monitor-store';
var userAgent = require('user-agent');
function getExtraData() {
return {
title: document.title,
url: location.href,
timestamp: Date.now(),
userAgent: userAgent.parse(navigator.userAgent).name
// 用户ID 等等等 获取额外数据
};
}
class SendTracker {
constructor() {
this.url = `http://${project}.${endpoint}/logstores/${logstoreName}/track`; // 上报的路径 文档里有参数说明
this.xhr = new XMLHttpRequest();
}
send(data = {}, callback) {
let extraData = getExtraData();
let logs = { ...extraData, ...data };
// 阿里云传值的要求 对象的值不能是数字
for (let key in logs) {
if (typeof logs[key] === 'number') {
logs[key] = "" + logs[key];
}
}
console.log("log:",logs);
let body = JSON.stringify({
__logs__: [logs]
});
console.log("body:",body);
this.xhr.open("POST", this.url, true);
this.xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); // 请求体类型
this.xhr.setRequestHeader('x-log-apiversion', '0.6.0'); // 版本号
this.xhr.setRequestHeader('x-log-bodyrawsize', body.length); // 请求体大小
this.xhr.onload = function () {
if ((this.status >= 200 && this.status <= 300) || this.status == 304) {
callback && callback();
}
}
this.xhr.onerror = function (error) {
console.log('error', error);
}
this.xhr.send(body);
}
}
export default new SendTracker();

2.3 接口异常采集脚本

2.3.1 数据结构设计

成功的请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"title": "前端监控系统", //标题
"url": "http://localhost:8080/", //url
"timestamp": "1590817024490", //timestamp
"userAgent": "Chrome", //浏览器版本
"kind": "stability", //大类
"type": "xhr", //小类
"eventType": "load", //事件类型
"pathname": "/success", //路径
"status": "200-OK", //状态码
"duration": "7", //持续时间
"response": "{\"id\":1}", //响应内容
"params": "" //参数
}

失败的请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"title": "前端监控系统",
"url": "http://localhost:8080/",
"timestamp": "1590817025617"
"userAgent": "Chrome",
"kind": "stability",
"type": "xhr",
"eventType": "load",
"pathname": "/error",
"status": "500-Internal Server Error",
"duration": "7",
"response": "",
"params": "name=jiafei"
}

2.3.2 实现

src/index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<body>
<div id="container">
<div class="content">
<input type="button" value="发起ajax成功请求" onclick="sendAjaxSuccess()" />
<input type="button" value="发起ajax失败请求" onclick="sendAjaxError()" />
</div>
</div>

<script>
function sendAjaxSuccess() {
let xhr = new XMLHttpRequest;
xhr.open('GET', '/success', true);
xhr.responseType = 'json';
xhr.onload = function () {
console.log(xhr.response);
}
xhr.send();
}
function sendAjaxError() {
let xhr = new XMLHttpRequest;
xhr.open('POST', '/error', true);
xhr.responseType = 'json';
xhr.onload = function () {
console.log(xhr.response);
}
xhr.onerror = function (error) {
console.log(error);
}
xhr.send("name=test");
}
</script>
</body>

webpack.config.js 添加before方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
context: process.cwd(),
mode: 'development',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'monitor.js'
},
devServer: {
contentBase: path.resolve(__dirname, 'dist'),
// before是用来配置路由的 内部是起了express服务 webpack-dev-server内置express,所以不用再安装express
before(router){
router.get('/success',function(req, res){
res.json({id:1}); // 200
});
router.post('/error',function(req, res){
res.sendStatus(500); // 500
})
}
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
inject: 'head'
})
]
}

新建 src/monitor/lib/xhr.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import tracker from '../util/tracker';
export function injectXHR() {
let XMLHttpRequest = window.XMLHttpRequest;
let oldOpen = XMLHttpRequest.prototype.open;
// 重写open方法 增强
XMLHttpRequest.prototype.open = function (method, url, async) {
if (!url.match(/logstores/) && !url.match(/sockjs/)) { // 把tracker上报的请求排除
this.logData = {method, url, async}; // 这个this指XMLHttpRequest的实例,给这个实例上加一个logData的属性
}
return oldOpen.apply(this, arguments);
}

let oldSend = XMLHttpRequest.prototype.send;
// 重写send方法 增强
XMLHttpRequest.prototype.send = function (body) { // body是请求体
if (this.logData) {
let start = Date.now(); // 在发送前记录下开始的时间
// XMLHttpRequest readyState 0 1 2 3 4 当状态变为4的时候就是load了
// 之后 status 2xx 304 就是成功 其他就是失败
let handler = (type) => (event) => { // 这里的type 就是 load、error、abort
let duration = Date.now() - start;
let status = this.status;
let statusText = this.statusText;
tracker.send({//未捕获的promise错误
kind: 'stability',//稳定性指标
type: 'xhr',//xhr
eventType: type,//load error abort
pathname: this.logData.url,//接口的url地址
status: status + "-" + statusText,
duration: "" + duration,//接口耗时
response: this.response ? JSON.stringify(this.response) : "", // 响应体
params: body || ''
})
}
this.addEventListener('load', handler('load'), false);
this.addEventListener('error', handler('error'), false);
this.addEventListener('abort', handler('abort'), false); // 终止请求 abort
}
oldSend.apply(this, arguments);
};
}

/src/monitor/index.js

1
2
3
4
import {injectJsError} from './lib/jsError';
import {injectXHR} from './lib/xhr';
injectJsError();
injectXHR();

2.4 白屏

2.4.1 数据设计

1
2
3
4
5
6
7
8
9
10
11
12
{
"title": "前端监控系统",
"url": "http://localhost:8080/",
"timestamp": "1590822618759",
"userAgent": "chrome",
"kind": "stability", //大类
"type": "blank", //小类
"emptyPoints": "0", //空白点
"screen": "2049x1152", //分辨率
"viewPoint": "2048x994", //视口
"selector": "HTML BODY #container" //选择器
}

2.4.2 实现

  • elementsFromPoint方法可以获取到当前视口内指定坐标处,由里到外排列的所有元素

src/index.html

1
2
3
4
5
6
7
<body>
<div id="container">
<div class="content">

</div>
</div>
</body>

新建 src/monitor/util/onload.js

1
2
3
4
5
6
7
export default function (callback) {
if (document.readyState === 'complete') {
callback();
} else {
window.addEventListener('load', callback);
}
};

新建 src/monitor/lib/blankScreen.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import tracker from '../util/tracker';
import onload from '../util/onload';
function getSelector(element) {
var selector;
if (element.id) {
selector = `#${element.id}`;
} else if (element.className && typeof element.className === 'string') {
selector = '.' + element.className.split(' ').filter(function (item) { return !!item }).join('.');
} else {
selector = element.nodeName.toLowerCase();
}
return selector;
}
export function blankScreen() {
// 不需要穷举所有的,像react 的话就只有id=root
const wrapperSelectors = ['body', 'html', '#container', '.content'];
let emptyPoints = 0;
function isWrapper(element) {
let selector = getSelector(element);
if (wrapperSelectors.indexOf(selector) >= 0) { // 匹配上了 空白点 ++
emptyPoints++;
}
}
onload(function () {
let xElements, yElements;
for (let i = 1; i <= 9; i++) {
// 根据具体页面布局风格定位点的坐标
xElements = document.elementsFromPoint(window.innerWidth * i / 10, window.innerHeight / 2); // 纵坐标都是高度的一半,横坐标宽度分成10份
yElements = document.elementsFromPoint(window.innerWidth / 2, window.innerHeight * i / 10); // 横坐标是宽度的一半,纵坐标高度分成10份
isWrapper(xElements[0]); // xElements 返回包含多个元素的数组,这里取第0个
isWrapper(yElements[0]);
}
if (emptyPoints >0) { // 一共有18个点 假设大于16个点就是白屏 这里写0做测试
let centerElements = document.elementsFromPoint(window.innerWidth / 2, window.innerHeight / 2); // 中间点
tracker.send({
kind: 'stability',
type: 'blank',
emptyPoints: "" + emptyPoints,
screen: window.screen.width + "x" + window.screen.height,
viewPoint: window.innerWidth + 'x' + window.innerHeight, // 布局视口大小
selector: getSelector(centerElements[0]),
})
}
});
}
//screen.width 屏幕的宽度 screen.height 屏幕的高度
//window.innerWidth 去除工具条与滚动条的窗口宽度 window.innerHeight 去除工具条与滚动条的窗口高度

src/monitor/index.js

1
2
3
4
5
6
import {injectJsError} from './lib/jsError';
import {injectXHR} from './lib/xhr';
import {blankScreen} from './lib/blankScreen';
injectJsError();
injectXHR();
blankScreen();

2.4.2.1 举例

src/index.html

1
2
3
4
5
6
7
8
9
10
11
<body>
<div id="container">
<div class="content" style="width:200px;word-wrap:break-word;">

</div>
</div>
<script>
let content = document.getElementsByClassName('content')[0];
content.innerHTML = '<span>@</span>'.repeat(1000);
</script>
</body>

可以依次拉伸显示不同宽度尺寸的屏幕,页面上定位到的emptyPoints个数随之变化,来设置最大白屏点数去发送白屏tracker

2.5 加载时间

  • PerformanceTiming
  • DOMContentLoaded
  • FMP

2.5.1 阶段含义

2.5.2 阶段计算

【注意】计算阶段下面第二张图注意:执行javascript 依赖css解析完毕,构建DOM 依赖JS解析完毕。
css没有解析完毕会阻塞js,这里涉及async、defer

2.5.3 数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"title": "前端监控系统",
"url": "http://localhost:8080/",
"timestamp": "1590828364183",
"userAgent": "chrome",
"kind": "experience",
"type": "timing",
"connectTime": "0",
"ttfbTime": "1",
"responseTime": "1",
"parseDOMTime": "80",
"domContentLoadedTime": "0",
"timeToInteractive": "88",
"loadTime": "89"
}

2.5.4 实现

新建 /src/monitor/lib/timing.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import tracker from '../util/tracker';
import onload from '../util/onload';
export function timing() {
onload(function () {
// 一般延迟3秒后再去拿时间,这样更加准确
setTimeout(() => {
const {
fetchStart,
connectStart,
connectEnd,
requestStart,
responseStart,
responseEnd,
domLoading,
domInteractive,
domContentLoadedEventStart,
domContentLoadedEventEnd,
loadEventStart } = performance.timing;
tracker.send({
kind: 'experience', // 用户体验指标
type: 'timing', // 每个阶段的时间
connectTime: connectEnd - connectStart,//TCP连接耗时
ttfbTime: responseStart - requestStart,//ttfb 首字节到达时间
responseTime: responseEnd - responseStart,//Response响应耗时
parseDOMTime: loadEventStart - domLoading,//DOM解析渲染耗时
domContentLoadedTime: domContentLoadedEventEnd - domContentLoadedEventStart,//DOMContentLoaded事件回调耗时
timeToInteractive: domInteractive - fetchStart,//首次可交互时间
loadTime: loadEventStart - fetchStart//完整的加载时间
});

}, 3000);
});
}

src/monitor/index.js

1
2
3
4
5
6
7
8
import {injectJsError} from './lib/jsError';
import {injectXHR} from './lib/xhr';
import {blankScreen} from './lib/blankScreen';
import {timing} from './lib/timing';
injectJsError();
injectXHR();
blankScreen();
timing();

刷新页面,可以看到向阿里云服务器发送track请求, 可以看到事件回调耗时0毫秒

2.5.4.1 举例

src/index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<body>
<div id="container">
<div class="content" style="width:600px;word-wrap:break-word;">

</div>
</div>
<script>
// 当初始的 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,而无需等待样式表、图像和子框架的完全加载。
// DOM解析完成,即使依赖的资源没有加载完成也会触发这个事件
document.addEventListener('DOMContentLoaded', function(){
let start = Date.now();
while((Date.now() - start) < 1000){}
})
</script>
</body>

刷新页面,可以看到发送track请求 domContentLoadedTime: 1001

2.6 性能指标

  • PerformanceObserver.observe方法用于观察传入的参数中指定的性能条目类型的集合。当记录一个指定类型的性能条目时,性能监测对象的回调函数将会被调用
  • entryType
  • paint-timing
  • event-timing
  • LCP
  • FMP
  • time-to-interactive

2.6.1 字段描述

【注意】字段下面的Performance图片中的Rendering、Painting,它们的区别是:渲染引擎会在内部将页面画好Rendering,画好之后一次性的放到浏览器中Painting,始终有两张图,这个叫双缓冲。多久更新一次:比如设备的屏幕刷新率为 60 次/秒,1000ms/60,大概16.6ms绘制一帧(交换一次图片)。

2.6.2 数据结构设计

1.paint

1
2
3
4
5
6
7
8
9
10
11
12
{
"title": "前端监控系统",
"url": "http://localhost:8080/",
"timestamp": "1590828364186",
"userAgent": "chrome",
"kind": "experience",
"type": "paint",
"firstPaint": "102",
"firstContentPaint": "2130",
"firstMeaningfulPaint": "2130",
"largestContentfulPaint": "2130"
}

2.firstInputDelay
{
“title”: “前端监控系统”,
“url”: “http://localhost:8080/",
“timestamp”: “1590828477284”,
“userAgent”: “chrome”,
“kind”: “experience”,
“type”: “firstInputDelay”,
“inputDelay”: “3”,
“duration”: “8”,
“startTime”: “4812.344999983907”,
“selector”: “HTML BODY #container .content H1”
}

2.6.3 实现

src/index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<body>
<div id="container">
<div class="content" style="width:600px;word-wrap:break-word;">
</div>
</div>
<script>
let content = document.getElementsByClassName('content')[0];
setTimeout(() => {
let h1 = document.createElement('h1');
h1.innerHTML = '我是最有重要的内容';
// 加这个elementtiming属性,来表示这个是有意义的内容 FCP
h1.setAttribute('elementtiming', 'meaningful');
content.appendChild(h1);
}, 2000);
</script>
</body>

/src/monitor/lib/timing.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import tracker from '../util/tracker';
import onload from '../util/onload';
import getSelector from '../util/getSelector';
import getLastEvent from '../util/getLastEvent';
export function timing() {
onload(function () {
let FMP, LCP;
// 增加一个性能条目的观察者
// 捕获页面中出现有意义的元素
new PerformanceObserver((entryList, observer) => {
let perfEntries = entryList.getEntries();
console.log("perfEntries:",perfEntries);
FMP = perfEntries[0];
observer.disconnect(); // 不在观察了
}).observe({ entryTypes: ['element'] }); // 观察页面中有意义的元素

// 捕获页面中的large content paint 代表在viewport中最大的页面元素加载的时间
new PerformanceObserver((entryList, observer) => {
const perfEntries = entryList.getEntries();
const lastEntry = perfEntries[perfEntries.length - 1];
LCP = lastEntry;
observer.disconnect();
}).observe({ entryTypes: ['largest-contentful-paint'] });

// 用户首次和页面交互(单击链接,点击按钮等)到页面响应交互的时间 直接在页面上点击就能触发
new PerformanceObserver(function (entryList, observer) {
let lastEvent = getLastEvent();
const firstInput = entryList.getEntries()[0];
console.log("FID:", firstInput);
if (firstInput) {
// processingStart 开始处理的时间 startTime 开始点击的时间
let inputDelay = firstInput.processingStart - firstInput.startTime;//处理延迟
let duration = firstInput.duration;//处理耗时
if (firstInput > 0 || duration > 0) {
tracker.send({
kind: 'experience', // 用户体验指标
type: 'firstInputDelay', // 首次输入延迟
inputDelay: inputDelay, // 延时的时间
duration: duration, // 处理的时间
startTime: firstInput.startTime,
selector: lastEvent ? getSelector(lastEvent.path || lastEvent.target) : ''
});
}
}
observer.disconnect();
}).observe({ type: 'first-input', buffered: true }); // 用户的第一次交互,可以是点击页面,也可以是第一次输入

setTimeout(() => {
const {
fetchStart,
connectStart,
connectEnd,
requestStart,
responseStart,
responseEnd,
domLoading,
domInteractive,
domContentLoadedEventStart,
domContentLoadedEventEnd,
loadEventStart } = performance.timing;
tracker.send({
kind: 'experience', // 用户体验指标
type: 'timing', // 每个阶段的时间
connectTime: connectEnd - connectStart,//TCP连接耗时
ttfbTime: responseStart - requestStart,//ttfb 首字节到达时间
responseTime: responseEnd - responseStart,//Response响应耗时
parseDOMTime: loadEventStart - domLoading,//DOM解析渲染耗时
domContentLoadedTime: domContentLoadedEventEnd - domContentLoadedEventStart,//DOMContentLoaded事件回调耗时
timeToInteractive: domInteractive - fetchStart,//首次可交互时间
loadTime: loadEventStart - fetchStart//完整的加载时间
});

// 开始发送性能指标
const FP = performance.getEntriesByName('first-paint')[0];
const FCP = performance.getEntriesByName('first-contentful-paint')[0];
console.log('FP', FP);
console.log('FCP', FCP);
console.log('FMP', FMP);
console.log('LCP', LCP);
tracker.send({
kind: 'experience',
type: 'paint',
firstPaint: FP.startTime,
firstContentPaint: FCP.startTime,
firstMeaningfulPaint: FMP.startTime,
largestContentfulPaint: LCP.startTime
});

}, 3000);
});
}

相关文章

http://www.jiafeipeixun.cn/2020/html/101.1.monitor.html

React源码理解之Fiber

发表于 2020-06-10 | 更新于: 2020-06-18 | 分类于 React

react16就是用fiber写的
fiber解决了执行栈不能中断的问题

1.屏幕刷新率

  • 目前大多数设备的屏幕刷新率为 60 次/秒
  • 浏览器渲染动画或页面的每一帧的速率也需要跟设备屏幕的刷新率保持一致
  • 页面是一帧一帧绘制出来的,当每秒绘制的帧数(FPS)达到 60 时,页面是流畅的,小于这个值时,用户会感觉到卡顿
  • 每个帧的预算时间是16.66 毫秒 (1秒/60)
  • 1s 60帧,所以每一帧分到的时间是 1000/60 ≈ 16 ms。所以我们书写代码时力求不让一帧的工作量超过 16ms

2.帧

  • 每个帧的开头包括样式计算、布局和绘制
  • JavaScript执行 Javascript引擎和页面渲染引擎在同一个渲染线程,GUI渲染和Javascript执行两者是互斥的
  • 如果某个任务执行时间过长,浏览器会推迟渲染

2.1 rAF

requestAnimationFrame 回调函数会在绘制之前执行

window.requestAnimationFrame() 告诉浏览器你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行
即在上图布局、绘制之前的浅蓝色区域执行。
requestIdleCallback就是上图最右侧的绿色区域,如果在这个区域操作DOM就会重新渲染。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<body>
<div style="background: lightblue;width: 0;height: 20px;"></div>
<button>开始</button>
<script>
/**
* requestAnimationFrame(callback) 由浏览器专门为动画提供的API
* cancelAnimationFrame(返回值) 清除动画
* <16.7 丢帧
* >16.7 跳跃 卡顿
*/
const div = document.querySelector('div');
const button = document.querySelector('button');
let start;
function progress() {
// div的宽 = div之前的宽 + 1px
div.style.width = div.offsetWidth + 1 + 'px';
div.innerHTML = (div.offsetWidth) + '%';
if (div.offsetWidth < 100) {
let current = Date.now();
// 打印的是line28开始准备执行的时间到真正执行的时间差
console.log(current - start);
start = current;
timer = requestAnimationFrame(progress);
}
}
button.addEventListener('click',function(){
div.style.width = 0;
start = Date.now();// 先获取到当前时间 current是毫秒数
requestAnimationFrame(progress);
})
</script>
</body>

点击button之后,可以看到打印出的时间差都是16、17秒,基本上在16.7左右

2.2 requestIdleCallback

  • 我们希望快速响应用户,让用户觉得够快,不能阻塞用户的交互
  • requestIdleCallback使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应
  • 正常帧任务完成后没超过16 ms,说明时间有富余,此时就会执行 requestIdleCallback 里注册的任务


上图,绿色的框中就是一帧。
首先,用户代码向浏览器申请时间片,当浏览器执行完之后如果有剩余时间,就分配时间片给用户代码执行,在约定时间内再归还控制器。

1
2
3
4
// 这是一个全局属性
// 告诉浏览器,我现在执行callback函数,但是它优先级比较低,告诉浏览器可以空闲的时候执行
// 但是如果到了超时时间,就必须马上执行
requestIdleCallback(callback, {timeout: 1000});
1
2
3
4
5
6
7
8
9
window.requestIdleCallback(
callback: (deaLine: IdleDeadline) => void,
option?: {timeout: number}
)

interface IdleDeadline {
didTimeout: boolean // 表示任务执行是否超过约定时间
timeRemaining(): DOMHighResTimeStamp // 任务可供执行的剩余时间
}
  • callback:回调即空闲时需要执行的任务,该回调函数接收一个IdleDeadline对象作为入参。其中IdleDeadline对象包含:
    • didTimeout,布尔值,表示任务是否超时,结合 timeRemaining 使用
    • timeRemaining(),表示当前帧剩余的时间,也可理解为留给任务的时间还有多少
  • options:目前 options 只有一个参数
    • timeout。表示超过这个时间后,如果任务还没执行,则强制执行,不必等待空闲
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      <body>
      <script>
      function sleep(delay) {
      for (var time = Date.now(); Date.now() - time <= delay;);
      }
      const works = [
      () => {
      console.log("第1个任务开始");
      //sleep(20); // 一帧16.6 因为此任务的执行时间超过了16.6毫秒,所以把控制权交给浏览器
      //while(true){}
      console.log("第1个任务结束");
      },
      () => {
      console.log("第2个任务开始");
      //sleep(20);
      console.log("第2个任务结束");
      },
      () => {
      console.log("第3个任务开始");
      //sleep(20);
      console.log("第3个任务结束");
      },
      ];

      requestIdleCallback(workLoop, { timeout: 1000 });
      function workLoop(deadline) {
      console.log('本帧剩余时间', parseInt(deadline.timeRemaining()));
      // 如果此帧的剩余时间超过0 或者 已经超时了(timeout)
      while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && works.length > 0) {
      performUnitOfWork();
      } // 如果说没有剩余时间了,就需要放弃执行任务控制权,执行权交还给浏览器

      if (works.length > 0) { // 说明还有未完成的任务
      console.log(`只剩下${parseInt(deadline.timeRemaining())}ms,时间片到了等待下次空闲时间的调度`);
      requestIdleCallback(workLoop, { timeout: 1000 });
      }
      }
      function performUnitOfWork() {
      //console.log("performUnitOfWork:",works.length);
      works.shift()();
      }
      </script>
      </body>

这里的line29 while,每完成一个任务(works),都会判断是否还有剩余时间,有的话再去执行下一个任务,即line30

如果把上面的sleep(20)注释都打开,打印如下:

1
2
3
4
5
6
7
8
9
10
11
本帧剩余时间 14
第1个任务开始
第1个任务结束
只剩下0ms,时间片到了等待下次空闲时间的调度
本帧剩余时间 9
第2个任务开始
第2个任务结束
只剩下0ms,时间片到了等待下次空闲时间的调度
本帧剩余时间 15
第3个任务开始
第3个任务结束

如果line10的while(true){}那执行任务就会卡在这里
如果把sleep都打开,上述任务总共执行时间为60ms+,具体时间值不固定

这里的一帧16.6毫秒只是大多数情况,显示器越强,刷新频率越高,一帧的时间越短。一秒多少帧跟显示器的刷新频率有关。只有当显示器的刷新频率是60HZ的时候,一帧才是16.6毫秒。

fiber是把整个任务分成很多小任务,每次执行一个任务,执行完成后会看看有没有剩余时间,如果有继续下一个任务,如果没有放弃执行,交给浏览器调度。

2.3 MessageChannel

3. 单链表

在fiber中很多地方都用到链表

  • 单链表是一种链式存取的数据结构
  • 链表中的数据是以节点来表示的,每个节点的构成:元素 + 指针(指示后继元素存储位置),元素就是存储数据的存储单元,指针就是连接每个节点的地址

如上图,firstUpdate 头指针、lastUpdate 尾指针、nextUpdate 指向下一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class Update {
constructor(payload) {
this.payload = payload; // payload指数据或者元素
this.nextUpdate = null; // 指向下一个节点的指针
}
}
class UpdateQueue {
constructor() {
this.baseState = null; // 原状态
this.firstUpdate = null; // 第一个 更新
this.lastUpdate = null; // 最后一个更新
}
clear() {
this.firstUpdate = null;
this.lastUpdate = null;
}
enqueueUpdate(update) {
if (this.firstUpdate === null) {
this.firstUpdate = this.lastUpdate = update;
} else {
this.lastUpdate.nextUpdate = update; // 最后一个元素的next指向新插入的这个元素
this.lastUpdate = update; // 尾指针指向新插入的这个元素
}
}
// 获取老状态,然后遍历这个链表,进行更新 更新状态
forceUpdate() {
let currentState = this.baseState || {}; // 初始状态
let currentUpdate = this.firstUpdate;
while (currentUpdate) {
let nexState = typeof currentUpdate.payload == 'function' ? currentUpdate.payload(currentState) : currentUpdate.payload;
currentState = { ...currentState, ...nexState }; // 解构老状态 合并新状态
currentUpdate = currentUpdate.nextUpdate;
}
this.firstUpdate = this.lastUpdate = null; // 更新完成之后清空列表
this.baseState = currentState;
return currentState;// 返回新状态
}
}


let queue = new UpdateQueue();
queue.enqueueUpdate(new Update({ name: 'test1' }));
queue.enqueueUpdate(new Update({ number: 0 }));
queue.enqueueUpdate(new Update(state => ({ number: state.number + 1 })));
queue.enqueueUpdate(new Update(state => ({ number: state.number + 1 })));
queue.enqueueUpdate(new Update({ name: 'test2' }));
queue.enqueueUpdate(new Update({ age: '18' }));
queue.forceUpdate();
console.log(queue.baseState);

打印: { name: ‘test2’, number: 2, age: ‘18’ }

一个虚拟DOM是一个链表节点,也是一个工作单元。一个虚拟DOM就是一个最小单元。

4. fiber历史

  1. fiber之前是什么样的?为什么需要fiber
  2. 看一下fiber之后的代码是怎么样的?

4.1 fiber之前的协调

  • React 会递归比对VirtualDOM树,找出需要变动的节点,然后同步更新它们。这个过程 React 称为Reconcilation(协调)
  • 在Reconcilation期间,React 会一直占用着浏览器资源,一则会导致用户触发的事件得不到响应, 二则会导致掉帧,用户可能会感觉到卡顿
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
let root = {
key: 'A1',
children: [
{
key: 'B1',
children: [
{
key: 'C1',
children: []
},
{
key: 'C2',
children: []
}
]
},
{
key: 'B2',
children: []
}
]
}
function walk(element) {
doWork(element);
element.children.forEach(child=>{
walk(child)
});
}

function doWork(element) {
console.log(element.key);
}
walk(root);

如上数据结构,A1下有B1、B2,B1下有C1、C2
打印:A1 B1 C1 C2 B2 深度优先遍历

这种遍历是递归调用,执行栈会越来越深,不能中断,因为中断后想恢复就非常难了。
1.不能中断 2.执行栈太深

4.2 fiber是什么

  • 我们可以通过某些调度策略合理分配CPU资源,从而提高用户的响应速度
  • 通过Fiber架构,让自己的Reconcilation过程变成可被中断。 适时地让出CPU执行权,除了可以让浏览器及时地响应用户的交互

4.2.1 fiber是一个执行单位

  • Fiber是一个执行单元,每次执行完一个执行单元, React 就会检查现在还剩多少时间,如果没有时间就将控制权让出去

4.2.2 fiber是一种数据结构

  • React目前的做法是使用链表, 每个 VirtualDOM 节点内部表示为一个Fiber

1
2
3
4
5
6
7
8
9
10
type Fiber = {
//类型
type: any,
//父节点
return: Fiber,
// 指向第一个子节点
child: Fiber,
// 指向下一个弟弟
sibling: Fiber
}

5. fiber执行阶段

  • 每次渲染有两个阶段:Reconciliation(协调\render阶段)和Commit(提交阶段)
  • 协调阶段: 可以认为是 Diff 阶段, 这个阶段可以被中断, 这个阶段会找出所有节点变更,例如节点新增、删除、属性变更等等, 这些变更React 称之为副作用(Effect)
  • 提交阶段: 将上一个阶段计算出来的需要处理的副作用(Effects)一次性执行了。这个阶段必须同步执行,不能被打断

5.1 render阶段

render阶段会构建fiber树

element.js

1
2
3
4
5
6
7
8
9
10
let A1 = { type: 'div', key: 'A1' };
let B1 = { type: 'div', key: 'B1', return: A1 };
let B2 = { type: 'div', key: 'B2', return: A1 };
let C1 = { type: 'div', key: 'C1', return: B1 };
let C2 = { type: 'div', key: 'C2', return: B1 };
A1.child = B1;
B1.sibling = B2;
B1.child = C1;
C1.sibling = C2;
module.exports = A1;

render.js 遍历fiber树

  • 从顶点开始遍历
  • 如果有第一个儿子,先遍历第一个儿子
  • 如果没有第一个儿子,标志着此节点遍历完成
  • 如果有弟弟遍历弟弟
  • 如果有没有下一个弟弟,返回父节点标识完成父节点遍历,如果有叔叔遍历叔叔
  • 没有父节点遍历结束

如上图:
遍历顺序:A1,有儿子,B1,有儿子,C1,没有儿子,有弟弟,C2,没有儿子,没有弟弟,有叔叔,即B1的弟弟,B2,没儿子,没弟弟,没叔叔,回到A1。

完成顺序:c1、C2、B1、B2、A1。 这里C1没有儿子,里面完成了,接着是C2没有儿子,完成,C1、C2都完成了,B1完成一次类推。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
let rootFiber = require('./element');
//下一个执行工作单元
let nextUnitOfWork = null;
//render工作循环
function workLoop() {
while (nextUnitOfWork) {// 如果有待执行的执行单元就执行
//执行一个任务并返回下一个任务
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
//render阶段结束
}
// 执行单个工作单元
function performUnitOfWork(fiber) { // A1 B1
beginWork(fiber); // 处理次fiber
if (fiber.child) {//如果有子节点就返回第一个子节点
// 这里A1有child返回B1;B1有儿子返回C1
return fiber.child;
}
while (fiber) {//如果没有子节点说明当前节点已经完成了渲染工作 C1
completeUnitOfWork(fiber);//可以结束此fiber的渲染了
if (fiber.sibling) {//如果它有弟弟就返回弟弟
return fiber.sibling;
}
fiber = fiber.return;//如果没有弟弟让爸爸完成,然后找叔叔
}
}
function beginWork(fiber) {
console.log('beginWork', fiber.key); // A1
//fiber.stateNode = document.createElement(fiber.type);
}
function completeUnitOfWork(fiber) {
console.log('completeUnitOfWork', fiber.key);
}
nextUnitOfWork = rootFiber;
workLoop();

打印:

1
2
3
4
5
6
7
8
9
10
beginWork A1
beginWork B1
beginWork C1
completeUnitOfWork C1
beginWork C2
completeUnitOfWork C2
completeUnitOfWork B1
beginWork B2
completeUnitOfWork B2
completeUnitOfWork A1

现在把它和之前的requestIdleCallback连起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function workLoop(deadline) {
// while (nextUnitOfWork) {
while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && nextUnitOfWork) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
if (nextUnitOfWork) { // 说明还有未完成的任务
console.log(`只剩下${parseInt(deadline.timeRemaining())}ms,时间片到了等待下次空闲时间的调度`);
requestIdleCallback(workLoop, { timeout: 1000 });
}
}
function performUnitOfWork(fiber) {
beginWork(fiber);
if (fiber.child) {
return fiber.child;
}
while (fiber) {
completeUnitOfWork(fiber);
if (fiber.sibling) {
return fiber.sibling;
}
fiber = fiber.return;
}
}
function beginWork(fiber) {
console.log('beginWork', fiber.key);
}
function completeUnitOfWork(fiber) {
console.log('completeUnitOfWork', fiber.key);
}
nextUnitOfWork = rootFiber;
requestIdleCallback(workLoop, {timeout: 1000});

相当于用链实现了一个类似于可中断的递归的功能

vue中不是fiber,在vue中是把每个更新任务分割的足够小,它是组件级别的更新,更新范围小。vue3采用了模板标记更新,能很快速的定位到更新的地方进行更新。
react是不管在哪里调setState,都是从根节点(指整个项目的根节点)比较更新的。在react中是任务还是很大,但是分割成多个小任务,可以中断和恢复,不阻塞主进程执行高级优先任务。

5.2 commit阶段

6. 手动实现React

创建一个空项目 create-react-app myApp

6.1 实现虚拟DOM

1】

src/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import ReactDOM from 'react-dom';
// JSX其实是一种特殊语法,在babel编译的时候会编译成js
let element = (
<div id="A1">
<div id="B1">
<div id="C1"></div>
<div id="C2"></div>
</div>
<div id="B2"></div>
</div>
);

ReactDOM.render(
element,
document.getElementById('root')
);

将上述line4-ling12放到https://www.babeljs.cn/中编译(勾选左边的react),结果:

1
2
3
4
5
6
// React.createElement(type,props,...children);
var element = React.createElement("div", {id: "A1"},
React.createElement("div", {id: "B1"},
React.createElement("div", {id: "C1"}),
React.createElement("div", {id: "C2"})),
React.createElement("div", {id: "B2"}));
1
2
3
4
5
6
7
8
9
10
11
12
13
import React from 'react';
import ReactDOM from 'react-dom';

var element = React.createElement("div", {id: "A1"},
React.createElement("div", {id: "B1"},
React.createElement("div", {id: "C1"}),
React.createElement("div", {id: "C2"})),
React.createElement("div", {id: "B2"}));
console.log(element);
ReactDOM.render(
element,
document.getElementById('root')
);

React.createElement 返回一个元素,也就是虚拟DOM,虚拟DOM就是一个JS对象,以JS对象的方式描述界面上DOM的样子

React.createElement替换jsx格式在页面上显示是一样的,还是以A1为开头的div 的数据格式,console.log()打印出来如下:

新建 src/react.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 创建元素(虚拟DOM)的方法
* @param {*} type 元素的类型
* @param {*} config 配置对象 属性、key、ref
* @param {...any} children 放置所有的儿子 这里做成数组,正在在react中可能是数组也可能是对象
*/
function createElement(type, config, ...children){
delete config.__self;
delete config.__source; // 表示这个文件是在哪行哪列哪个文件生成的
return {
type,
props:{
...config,
children
}
}
}

2】

当C1中的内容是文本时,children里面不是虚拟DOM,是文本值
src/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import ReactDOM from 'react-dom';
let element = (
<div id="A1">
<div id="B1">
<div id="C1">C1内容</div>
<div id="C2">C2内容</div>
</div>
<div id="B2">B2内容</div>
</div>
);

console.log(element);
ReactDOM.render(
element,
document.getElementById('root')
);

新建src/constants.js

1
2
// 表示这是一个文本元素
export const ELEMENT_TEXT = Symbol.for('ELEMENT_TEXT');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import {ELEMENT_TEXT} from './constants';
/**
* 创建元素(虚拟DOM)的方法
* @param {*} type 元素的类型
* @param {*} config 配置对象 属性、key、ref
* @param {...any} children 放置所有的儿子 这里做成数组,正在在react中可能是数组也可能是对象
*/
function createElement(type, config, ...children){
delete config.__self;
delete config.__source; // 表示这个文件是在哪行哪列哪个文件生成的
return {
type,
props:{
...config,
children: children.map(child=>{
// 做了一个兼容处理,如果是react元素就返回自己,如果是一个字符串的话就返回元素对象
return typeof child === 'object' ? child : { // 源码里面没有转,就是字符串
type: ELEMENT_TEXT,
props: {text:child, children: []}
}
})
}
}
}
const React = {
createElement
};
export default React;

调用自己写的react看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from './react';
import ReactDOM from 'react-dom';
let element = (
<div id="A1">
<div id="B1">
<div id="C1">C1内容</div>
<div id="C2">C2内容</div>
</div>
<div id="B2">B2内容</div>
</div>
);

console.log(element);
// ReactDOM.render(
// element,
// document.getElementById('root')
// );

打印出的虚拟DOM结构如下:

6.2 实现初次渲染

6.2.1 根据虚拟DOM生成fiber树

1】

原生的效果
src/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React from 'react';
import ReactDOM from 'react-dom';
let style = { border: '3px solid red', margin: '5px' };
let element = (
<div id="A1" style={style}>A1内容
<div id="B1" style={style}>B1内容
<div id="C1" style={style}>C1内容</div>
<div id="C2" style={style}>C2内容</div>
</div>
<div id="B2" style={style}>B2内容</div>
</div>
);

console.log(element);
ReactDOM.render(
element,
document.getElementById('root')
);

src/constants.js

1
2
3
4
5
6
7
8
// 表示这是一个文本元素
export const ELEMENT_TEXT = Symbol.for('ELEMENT_TEXT');
// React应用需要一个跟fiber
export const TAG_ROOT = Symbol.for('TAG_ROOT');
// 原生的节点 span div 函数组件 类组件
export const TAG_HOST = Symbol.for('TAG_HOST');
// 这是文本节点
export const TAG_TEXT = Symbol.for('TAG_TEXT');

新建 src/react-dom.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import {TAG_ROOT} from './constants';
/**
*render是要把一个元素渲染到一个容器内部
*每个虚拟DOM节点对应一个fiber
* @param {*} element 虚拟DOM
* @param {*} container 容器
*/
function render(element, container){ // ReactDOM.render(element,document.getElementById('root')); 这里 container 对应的是<div id="root"></div>这个真实的DOM元素
let rootFiber = {
tag: TAG_ROOT, // 每个fiber都会有一个tag标识,
stateNode: container, // 一般情况下如果这个元素是一个原生节点的话,stateNode指向真实DOM元素
// props.children 是一个数组,里面放的是React元素 即虚拟DOM ,后面会根据每个React元素创建对应的fiber
props:{ children: [element] } // 这个fiber的属性对象children属性,里面放的是要渲染的元素
}
scheduleRoot(rootFiber); // 开始调度,从根节点开始遍历
}
const ReactDOM = {
render
};
export default ReactDOM;

新建 src/schedule.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
*从根节点开始渲染和调度
*两个阶段:
一、diff阶段,对比新旧的虚拟DOM,进行增量 更新或创建
render阶段:这个阶段比较花时间,可以对任务进行拆分,拆分的维度就是虚拟DOM节点,一个虚拟DOM节点对应一个任务,此阶段可以暂停,成果是effect list知道哪些节点删除了 增加了
二、commit阶段: 进行DOM更新创建阶段,此阶段不能暂停,要一气呵成
*/
let nextUnitOfWork = null//下一个工作单元
let workInProgressRoot = null;//正在渲染中的根Fiber
function scheduleRoot(rootFiber){ // {tag: TAG_ROOT, stateNode: container, props:{ children: [element] } }
// 大致原理:根据 element虚拟DOM 创建真实DOM插入到容器 container 中
//把当前树设置为nextUnitOfWork开始进行调度
workInProgressRoot = rootFiber;
nextUnitOfWork = rootFiber;
}
function workLoop(deadline){
let shouldYield = false; // 是否要让出时间片或控制权
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);//执行一个任务并返回下一个任务
shouldYield = deadline.timeRemaining() < 1;//如果剩余时间小于1毫秒就说明没有时间了,需要把控制权让给浏览器
}
//如果没有下一个执行单元了,并且当前渲染树存在,则进行提交阶段
if (nextUnitOfWork) { // 如果时间片到期后还有任务没完成,就需要请求浏览器再次调度
requestIdleCallback(workLoop, {timeout: 500});
}else{
console.log("渲染阶段结束");
}
}
// react告诉浏览器,我现在有任务,请你在闲的时候执行
// 这里有一个优先级的概念 expirationTime
requestIdleCallback(workLoop, {timeout: 500});
2】跟节点A1

src/schedule.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
function performUnitOfWork(currentFiber) {
beginWork(currentFiber);//开始渲染前的Fiber,就是把子元素变成子fiber

if (currentFiber.child) {//如果有子节点就返回第一个子节点 做下一个任务
return currentFiber.child;
}
while (currentFiber) {//如果没有子节点说明当前节点已经完成了渲染工作 开始找弟弟
completeUnitOfWork(currentFiber);//可以结束此fiber的渲染了
if (currentFiber.sibling) {//如果它有弟弟就返回弟弟
return currentFiber.sibling;
}
currentFiber = currentFiber.return;//如果没有弟弟让爸爸完成,然后找叔叔
}
}
/**
*beginwork 开始收下线的钱
*completeUnitOfWork 把下线的钱收完了
* @param {*} currentFiber
* 1. 创建真实DOM元素
* 2. 创建子fiber
*/
function beginWork(currentFiber) {
if (currentFiber.tag === TAG_ROOT) {//如果是根节点
updateHostRoot(currentFiber);
}
}
function updateHostRoot(currentFiber) {//如果是根节点
const newChildren = currentFiber.props.children;//直接渲染子节点
reconcileChildren(currentFiber, newChildren);
}
function reconcileChildren(currentFiber, newChildren){// 根据虚拟DOM节点 创建子fiber
let newChildIndex = 0;// 新子节点的索引
let prevSibling; // 上一个新的子fiber
// 遍历子虚拟DOM数组 虚拟DOM转化为fiber
while (newChildIndex < newChildren.length) {
const newChild = newChildren[newChildIndex]; // 取出虚拟DOM节点
let tag;
if (newChild && newChild.type === ELEMENT_TEXT) {
tag = TAG_TEXT;//文本
} else if (newChild && typeof newChild.type === 'string') {
tag = TAG_HOST;//如果type是字符串,那他是原生DOM节点
}
let newFiber = {
tag,//原生DOM组件 TAG_HOST
type: newChild.type,//具体的元素类型
props: newChild.props,//新的属性对象
stateNode: null,//stateNode肯定是空的,因为这个时候div还没有创建DOM元素
return: currentFiber,//父Fiber
effectTag: PLACEMENT,//副作用标识 增加节点 副作用标识,在render阶段我们会收集副作用,有增加 删除 更新
nextEffect: null // effect list也是一个单链表 nextEffect指向下一个节点
// effect list的顺序和完成顺序是一样的 但是只放出钱的fiber即有副作用的fiber
}// 在beginWork创建fiber 在completeUnitOfWork的时候收集effect
if (newFiber) {
if (newChildIndex === 0) {
currentFiber.child = newFiber;//第一个子节点挂到父节点的child属性上
} else {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;//然后newFiber变成了上一个哥哥了
}
newChildIndex++;
}
}

constants.js

1
2
3
4
5
6
7
// 副作用
// 增加节点
export const PLACEMENT = Symbol.for('PLACEMENT');
// 更新节点
export const UPDATE = Symbol.for('UPDATE');
// 删除节点
export const DELETION = Symbol.for('DELETION');
3】如果是原生文本节点

新建 src/utils.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
    if (/^on/.test(key)) {
dom[key.toLowerCase()] = value;
} else if (key === 'style') {
if (value) {
for (let styleName in value) {
if (value.hasOwnProperty(styleName)) {
dom.style[styleName] = value[styleName];
}
}
}
} else {
dom.setAttribute(key, value);
}
return dom;
}
export function setProps(elem, oldProps, newProps) {
for (let key in oldProps) {
if (key !== 'children') {
if (newProps.hasOwnProperty(key)) {
setProp(elem, key, newProps[key]);
} else {
elem.removeAttribute(key);
}
}
}
for (let key in newProps) {
if (key !== 'children') {
setProp(elem, key, newProps[key])
}
}
}

schedule.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function beginWork(currentFiber) {
if (currentFiber.tag === TAG_ROOT) {
updateHostRoot(currentFiber);
} else if (currentFiber.tag === TAG_TEXT) {//如果是原生文本节点
updateHostText(currentFiber);
}
}
//如果是原生文本节点
function updateHostText(currentFiber) {
if (!currentFiber.stateNode) {
currentFiber.stateNode = createDOM(currentFiber);//先创建真实的DOM节点
}
}
function createDOM(currentFiber) {
if (currentFiber.type === ELEMENT_TEXT) { // 如果是文本节点创建文本节点
return document.createTextNode(currentFiber.props.text);
}else if(currentFiber.type === TAG_HOST){
const stateNode = document.createElement(currentFiber.type);
updateDOM(stateNode, {}, currentFiber.props);
return stateNode;
}

}
function updateDOM(stateNode, oldProps, newProps) {
setProps(stateNode, oldProps, newProps);
}
4】如果是原生DOM节点

schedule.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function beginWork(currentFiber) {
if (currentFiber.tag === TAG_ROOT) {
updateHostRoot(currentFiber);
} else if (currentFiber.tag === TAG_TEXT) {
updateHostText(currentFiber);
} else if (currentFiber.tag === TAG_HOST) {//如果是原生DOM节点
updateHostComponent(currentFiber);
}
}
//如果是原生DOM节点
function updateHostComponent(currentFiber) {//如果是原生DOM节点
if (!currentFiber.stateNode) {// 如果此fiber没有创建DOM节点
currentFiber.stateNode = createDOM(currentFiber);//先创建真实的DOM节点
}
const newChildren = currentFiber.props.children;
reconcileChildren(currentFiber, newChildren);
}

6.2.2 收集effect list

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 在完成的时候要收集有副作用的fiber,然后组成effect list
// 每个fiber有两个属性,一个firstEffect指向第一个有副作用的子fiber lastEffect指向最后一个有副作用的子fiber
// 中间的用nextEffect做成一个单链表 firstEffect->大儿子,大儿子.nextEffect->二儿子,二儿子.nextEffect->三儿子,lastEffect也指向三儿子
function completeUnitOfWork(currentFiber) {// 第一个完成的A1(TEXT)
const returnFiber = currentFiber.return; //找它爹A1
if (returnFiber) {
////////// 这段是把自己儿子的链挂到父亲身上
if (!returnFiber.firstEffect) {
returnFiber.firstEffect = currentFiber.firstEffect;
}
if (!!currentFiber.lastEffect) {
if (!!returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = currentFiber.firstEffect;
}
returnFiber.lastEffect = currentFiber.lastEffect;
}
////////// 这段是把自己挂到儿子身上
const effectTag = currentFiber.effectTag;
if (effectTag) { // 如果有值,有副作用
if (!!returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = currentFiber;
} else {
returnFiber.firstEffect = currentFiber;
}
returnFiber.lastEffect = currentFiber;
}
}
}

A1:B1,B2
B1:C1,C2
B2:D1,D2

B1.firstEffect->C1
B1.lastEffect->C2

A1.firstEffect->C1
A1.lastEffect->C2

C1.nextEffect ->C2
C2.nextEffect ->B1

A1.lastEffect ->B1

完成顺序:C1,C2,B1,D1,D2,B2,A1

6.2.3 commit阶段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function workLoop(deadline){
let shouldYield = false;
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
shouldYield = deadline.timeRemaining() < 1;
}
if (nextUnitOfWork && workInProgressRoot) {
requestIdleCallback(workLoop, {timeout: 500});
}else{
console.log("渲染阶段结束");
// 提交阶段
commitRoot();
}
}
function commitRoot() {
let currentFiber = workInProgressRoot.firstEffect;
while (currentFiber) {
commitWork(currentFiber);
currentFiber = currentFiber.nextEffect;
}
workInProgressRoot = null;
}
function commitWork(currentFiber) {
if (!currentFiber) {
return;
}
let returnFiber = currentFiber.return;//先获取父Fiber
const domReturn = returnFiber.stateNode;//获取父的DOM节点
if (currentFiber.effectTag === PLACEMENT && currentFiber.stateNode != null) {//如果是新增DOM节点
let nextFiber = currentFiber;
domReturn.appendChild(nextFiber.stateNode);
}
currentFiber.effectTag = null;
}

完整版schedule.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
import {PLACEMENT, ELEMENT_TEXT, TAG_TEXT, TAG_HOST} from './constants';
/**
*从根节点开始渲染和调度
*两个阶段:
一、diff阶段,对比新旧的虚拟DOM,进行增量 更新或创建
render阶段:这个阶段比较花时间,可以对任务进行拆分,拆分的维度就是虚拟DOM节点,一个虚拟DOM节点对应一个任务,此阶段可以暂停,成果是effect list知道哪些节点删除了 增加了
render阶段有两个任务:1.根据虚拟DOM生成fiber树;2.收集effect list
二、commit阶段: 进行DOM更新创建阶段,此阶段不能暂停,要一气呵成
*/
let nextUnitOfWork = null//下一个工作单元
let workInProgressRoot = null;//正在渲染中的根Fiber
export function scheduleRoot(rootFiber){ // {tag: TAG_ROOT, stateNode: container, props:{ children: [element] } }
// 大致原理:根据 element虚拟DOM 创建真实DOM插入到容器 container 中
//把当前树设置为nextUnitOfWork开始进行调度
workInProgressRoot = rootFiber;
nextUnitOfWork = rootFiber;
}
function performUnitOfWork(currentFiber) {
beginWork(currentFiber);//开始渲染前的Fiber,就是把子元素变成子fiber

if (currentFiber.child) {//如果子节点就返回第一个子节点
return currentFiber.child;
}
while (currentFiber) {//如果没有子节点说明当前节点已经完成了渲染工作
completeUnitOfWork(currentFiber);//可以结束此fiber的渲染了
if (currentFiber.sibling) {//如果它有弟弟就返回弟弟
return currentFiber.sibling;
}
currentFiber = currentFiber.return;//如果没有弟弟让爸爸完成,然后找叔叔
}
}
// 在完成的时候要收集有副作用的fiber,然后组成effect list
// 每个fiber有两个属性,一个firstEffect指向第一个有副作用的子fiber lastEffect指向最后一个有副作用的子fiber
// 中间的用nextEffect做成一个单链表 firstEffect->大儿子,大儿子.nextEffect->二儿子,二儿子.nextEffect->三儿子,lastEffect也指向三儿子
function completeUnitOfWork(currentFiber) {// 第一个完成的A1(TEXT)
const returnFiber = currentFiber.return; //找它爹A1
if (returnFiber) {
if (!returnFiber.firstEffect) {
returnFiber.firstEffect = currentFiber.firstEffect;
}
if (!!currentFiber.lastEffect) {
if (!!returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = currentFiber.firstEffect;
}
returnFiber.lastEffect = currentFiber.lastEffect;
}

const effectTag = currentFiber.effectTag;
if (effectTag) { // 如果有值,有副作用
if (!!returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = currentFiber;
} else {
returnFiber.firstEffect = currentFiber;
}
returnFiber.lastEffect = currentFiber;
}
}
}

/**
*beginwork 开始收下线的钱
*completeUnitOfWork 把下线的钱收完了
* @param {*} currentFiber
* 1. 创建真实DOM元素
* 2. 创建子fiber
*/
function beginWork(currentFiber) {
if (currentFiber.tag === TAG_ROOT) {//如果是根节点
updateHostRoot(currentFiber);
} else if (currentFiber.tag === TAG_TEXT) {//如果是原生文本节点
updateHostText(currentFiber);
} else if (currentFiber.tag === TAG_HOST) {//如果是原生DOM节点
updateHostComponent(currentFiber);
}
}
//如果是原生DOM节点
function updateHostComponent(currentFiber) {//如果是原生DOM节点
if (!currentFiber.stateNode) {// 如果此fiber没有创建DOM节点
currentFiber.stateNode = createDOM(currentFiber);//先创建真实的DOM节点
}
const newChildren = currentFiber.props.children;
reconcileChildren(currentFiber, newChildren);
}
//如果是原生文本节点
function updateHostText(currentFiber) {
if (!currentFiber.stateNode) {
currentFiber.stateNode = createDOM(currentFiber);//先创建真实的DOM节点
}
}
function createDOM(currentFiber) {
if (currentFiber.type === ELEMENT_TEXT) { // 如果是文本节点创建文本节点
return document.createTextNode(currentFiber.props.text);
}else if(currentFiber.type === TAG_HOST){
const stateNode = document.createElement(currentFiber.type);
updateDOM(stateNode, {}, currentFiber.props);
return stateNode;
}

}
function updateDOM(stateNode, oldProps, newProps) {
setProps(stateNode, oldProps, newProps);
}

function updateHostRoot(currentFiber) {//如果是根节点
const newChildren = currentFiber.props.children;//直接渲染子节点
reconcileChildren(currentFiber, newChildren);
}
function reconcileChildren(currentFiber, newChildren){// 根据虚拟DOM节点 创建子fiber
let newChildIndex = 0;// 新子节点的索引
let prevSibling; // 上一个新的子fiber
// 遍历子虚拟DOM数组 虚拟DOM转化为fiber
while (newChildIndex < newChildren.length) {
const newChild = newChildren[newChildIndex]; // 取出虚拟DOM节点
let tag;
if (newChild && newChild.type === ELEMENT_TEXT) {
tag = TAG_TEXT;//文本
} else if (newChild && typeof newChild.type === 'string') {
tag = TAG_HOST;//如果type是字符串,那他是原生DOM节点
}
let newFiber = {
tag,//原生DOM组件 TAG_HOST
type: newChild.type,//具体的元素类型
props: newChild.props,//新的属性对象
stateNode: null,//stateNode肯定是空的,因为这个时候div还没有创建DOM元素
return: currentFiber,//父Fiber
effectTag: PLACEMENT,//副作用标识 增加节点 副作用标识,在render阶段我们会收集副作用,有增加 删除 更新
nextEffect: null // effect list也是一个单链表 nextEffect指向下一个节点
// effect list的顺序和完成顺序是一样的 但是只放出钱的fiber即有副作用的fiber
}// 在beginWork创建fiber 在completeUnitOfWork的时候收集effect
if (newFiber) {
if (newChildIndex === 0) {
currentFiber.child = newFiber;//第一个子节点挂到父节点的child属性上
} else {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;//然后newFiber变成了上一个哥哥了
}
newChildIndex++;
}
}
function workLoop(deadline){
let shouldYield = false; // 是否要让出时间片或控制权
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);//执行一个任务并返回下一个任务
shouldYield = deadline.timeRemaining() < 1;//如果剩余时间小于1毫秒就说明没有时间了,需要把控制权让给浏览器
}
//如果没有下一个执行单元了,并且当前渲染树存在,则进行提交阶段
if (nextUnitOfWork && workInProgressRoot) { // 如果时间片到期后还有任务没完成,就需要请求浏览器再次调度
requestIdleCallback(workLoop, {timeout: 500});
}else{
console.log("渲染阶段结束");
// 提交阶段
commitRoot();
}
}
function commitRoot() {
let currentFiber = workInProgressRoot.firstEffect;
while (currentFiber) {
commitWork(currentFiber);
currentFiber = currentFiber.nextEffect;
}
workInProgressRoot = null;
}
function commitWork(currentFiber) {
if (!currentFiber) {
return;
}
let returnFiber = currentFiber.return;//先获取父Fiber
const domReturn = returnFiber.stateNode;//获取父的DOM节点
if (currentFiber.effectTag === PLACEMENT && currentFiber.stateNode != null) {//如果是新增DOM节点
let nextFiber = currentFiber;
domReturn.appendChild(nextFiber.stateNode);
}
currentFiber.effectTag = null;
}

// react告诉浏览器,我现在有任务,请你在闲的时候执行
// 这里有一个优先级的概念 expirationTime
requestIdleCallback(workLoop, {timeout: 500});

6.3 实现元素的更新

currentRoot 当前根
workInProgressRoot 正在工作的根

public\index.html

1
2
3
4
5
<body>
<div id="root"></div>
<button id="reRender1">reRender1</button>
<button id="reRender2">reRender2</button>
</body>

src/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import React from './react';
import ReactDOM from './react-dom';
let style = { border: '3px solid red', margin: '5px' };
let element = (
<div id="A1" style={style}>
A1
<div id="B1" style={style}>
B1
<div id="C1" style={style}>C1</div>
<div id="C2" style={style}>C2</div>
</div>
<div id="B2" style={style}>B2</div>
</div>
)
console.log(element);
ReactDOM.render(
element,
document.getElementById('root')
);

+let reRender2 = document.getElementById('reRender1');
+reRender2.addEventListener('click', () => {
+ let element2 = (
+ <div id="A1-new" style={style}>
+ A1-new
+ <div id="B1-new" style={style}>
+ B1-new
+ <div id="C1-new" style={style}>C1-new</div>
+ <div id="C2-new" style={style}>C2-new</div>
+ </div>
+ <div id="B2" style={style}>B2</div>
+ <div id="B3" style={style}>B3</div>
+ </div>
+ )
+ ReactDOM.render(
+ element2,
+ document.getElementById('root')
+ );
+});
+
+let reRender3 = document.getElementById('reRender2');
+reRender3.addEventListener('click', () => {
+ let element3 = (
+ <div id="A1-new2" style={style}>
+ A1-new2
+ <div id="B1-new2" style={style}>
+ B1-new2
+ <div id="C1-new2" style={style}>C1-new2</div>
+ <div id="C2-new2" style={style}>C2-new2</div>
+ </div>
+ <div id="B2" style={style}>B2</div>
+ </div>
+ )
+ ReactDOM.render(
+ element3,
+ document.getElementById('root')
+ );
+});

原理解析

第一次渲染 workInProgressRoot,渲染完成

1
2
3
4
5
6
7
8
9
10
// schedule.js
function commitRoot() {
let currentFiber = workInProgressRoot.firstEffect;
while (currentFiber) {
commitWork(currentFiber);
currentFiber = currentFiber.nextEffect;
}
+ currentRoot = workInProgressRoot; // 把当前渲染成功的根fiber给 currentRoot
workInProgressRoot = null;
}

将 workInProgressRoot 置为空

每次更新的时候都拿虚拟DOM节点和上次的fiber节点进行对比

奇数更新

1
2
3
4
5
6
7
8
9
export function scheduleRoot(rootFiber){
if (currentRoot) {//奇数次更新 说明有值,已经渲染过了
+ rootFiber.alternate = currentRoot;
+ workInProgressRoot = rootFiber;
+ } else {
+ workInProgressRoot = rootFiber;//第一次渲染
+ }
nextUnitOfWork = rootFiber;
}

完整代码schedule.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
import { setProps } from './utils';
import {
ELEMENT_TEXT, TAG_ROOT, TAG_HOST, TAG_TEXT, PLACEMENT, DELETION, UPDATE
} from './constants';
+let currentRoot = null;//当前的根Fiber
let workInProgressRoot = null;//正在渲染中的根Fiber
let nextUnitOfWork = null//下一个工作单元
+let deletions = [];//要删除的fiber节点

export function scheduleRoot(rootFiber) {
//{tag:TAG_ROOT,stateNode:container,props: { children: [element] }}
+ if (currentRoot && currentRoot.alternate) {//偶数次更新
+ workInProgressRoot = currentRoot.alternate;
+ workInProgressRoot.firstEffect = workInProgressRoot.lastEffect = workInProgressRoot.nextEffect = null;
+ workInProgressRoot.props = rootFiber.props;
+ workInProgressRoot.alternate = currentRoot;
+ } else if (currentRoot) {//奇数次更新
+ rootFiber.alternate = currentRoot;
+ workInProgressRoot = rootFiber;
+ } else {
+ workInProgressRoot = rootFiber;//第一次渲染
+ }
nextUnitOfWork = workInProgressRoot;
}

function commitRoot() {
+ deletions.forEach(commitWork);
let currentFiber = workInProgressRoot.firstEffect;
while (currentFiber) {
commitWork(currentFiber);
currentFiber = currentFiber.nextEffect;
}
+ deletions.length = 0;//先把要删除的节点清空掉
+ currentRoot = workInProgressRoot;
workInProgressRoot = null;
}
function commitWork(currentFiber) {
if (!currentFiber) {
return;
}
let returnFiber = currentFiber.return;//先获取父Fiber
const domReturn = returnFiber.stateNode;//获取父的DOM节点
if (currentFiber.effectTag === PLACEMENT && currentFiber.stateNode != null) {//如果是新增DOM节点
let nextFiber = currentFiber;
domReturn.appendChild(nextFiber.stateNode);
+ } else if (currentFiber.effectTag === DELETION) {//如果是删除则删除并返回
+ domReturn.removeChild(currentFiber.stateNode);
+ } else if (currentFiber.effectTag === UPDATE && currentFiber.stateNode != null) {//如果是更新
+ if (currentFiber.type === ELEMENT_TEXT) {
+ if (currentFiber.alternate.props.text != currentFiber.props.text) {
+ currentFiber.stateNode.textContent = currentFiber.props.text;
+ }
+ } else {
+ updateDOM(currentFiber.stateNode, currentFiber.alternate.props, currentFiber.props);
+ }
+ }
currentFiber.effectTag = null;
}

function performUnitOfWork(currentFiber) {
beginWork(currentFiber);//开始渲染前的Fiber,就是把子元素变成子fiber

if (currentFiber.child) {//如果子节点就返回第一个子节点
return currentFiber.child;
}

while (currentFiber) {//如果没有子节点说明当前节点已经完成了渲染工作
completeUnitOfWork(currentFiber);//可以结束此fiber的渲染了
if (currentFiber.sibling) {//如果它有弟弟就返回弟弟
return currentFiber.sibling;
}
currentFiber = currentFiber.return;//如果没有弟弟让爸爸完成,然后找叔叔
}
}

function beginWork(currentFiber) {
if (currentFiber.tag === TAG_ROOT) {//如果是根节点
updateHostRoot(currentFiber);
} else if (currentFiber.tag === TAG_TEXT) {//如果是原生文本节点
updateHostText(currentFiber);
} else if (currentFiber.tag === TAG_HOST) {//如果是原生DOM节点
updateHostComponent(currentFiber);
}
}
function updateHostRoot(currentFiber) {//如果是根节点
const newChildren = currentFiber.props.children;//直接渲染子节点
reconcileChildren(currentFiber, newChildren);
}
function updateHostText(currentFiber) {
if (!currentFiber.stateNode) {
currentFiber.stateNode = createDOM(currentFiber);//先创建真实的DOM节点
}
}
function updateHostComponent(currentFiber) {//如果是原生DOM节点
if (!currentFiber.stateNode) {
currentFiber.stateNode = createDOM(currentFiber);//先创建真实的DOM节点
}
const newChildren = currentFiber.props.children;
reconcileChildren(currentFiber, newChildren);
}
function createDOM(currentFiber) {
if (currentFiber.type === ELEMENT_TEXT) {
return document.createTextNode(currentFiber.props.text);
}
const stateNode = document.createElement(currentFiber.type);
updateDOM(stateNode, {}, currentFiber.props);
return stateNode;
}

function reconcileChildren(currentFiber, newChildren) {
let newChildIndex = 0;//新虚拟DOM数组中的索引
+ let oldFiber = currentFiber.alternate && currentFiber.alternate.child;//父Fiber中的第一个子Fiber
+ let prevSibling;
+ while (newChildIndex < newChildren.length || oldFiber) {
+ const newChild = newChildren[newChildIndex];
+ let newFiber;
+ const sameType = oldFiber && newChild && newChild.type === oldFiber.type;//新旧都有,并且元素类型一样
+ let tag;
+ if (newChild && newChild.type === ELEMENT_TEXT) {
+ tag = TAG_TEXT;//文本
+ } else if (newChild && typeof newChild.type === 'string') {
+ tag = TAG_HOST;//原生DOM组件
+ }
+ if (sameType) {
+ if (oldFiber.alternate) {
+ newFiber = oldFiber.alternate;
+ newFiber.props = newChild.props;
+ newFiber.alternate = oldFiber;
+ newFiber.effectTag = UPDATE;
+ newFiber.nextEffect = null;
+ } else {
+ newFiber = {
+ tag:oldFiber.tag,//标记Fiber类型,例如是函数组件或者原生组件
+ type: oldFiber.type,//具体的元素类型
+ props: newChild.props,//新的属性对象
+ stateNode: oldFiber.stateNode,//原生组件的话就存放DOM节点,类组件的话是类组件实例,函数组件的话为空,因为没有实例
+ return: currentFiber,//父Fiber
+ alternate: oldFiber,//上一个Fiber 指向旧树中的节点
+ effectTag: UPDATE,//副作用标识
+ nextEffect: null //React 同样使用链表来将所有有副作用的Fiber连接起来
+ }
+ }
+ } else {
+ if (newChild) {//类型不一样,创建新的Fiber,旧的不复用了
+ newFiber = {
+ tag,//原生DOM组件
+ type: newChild.type,//具体的元素类型
+ props: newChild.props,//新的属性对象
+ stateNode: null,//stateNode肯定是空的
+ return: currentFiber,//父Fiber
+ effectTag: PLACEMENT//副作用标识
+ }
+ }
+ if (oldFiber) {
+ oldFiber.effectTag = DELETION;
+ deletions.push(oldFiber);
+ }
+ }
+ if (oldFiber) { //比较完一个元素了,老Fiber向后移动1位
+ oldFiber = oldFiber.sibling;
+ }
if (newFiber) {
if (newChildIndex === 0) {
currentFiber.child = newFiber;//第一个子节点挂到父节点的child属性上
} else {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;//然后newFiber变成了上一个哥哥了
}
prevSibling = newFiber;//然后newFiber变成了上一个哥哥了
newChildIndex++;
}
}

function updateDOM(stateNode, oldProps, newProps) {
setProps(stateNode, oldProps, newProps);
}
function completeUnitOfWork(currentFiber) {
const returnFiber = currentFiber.return;
if (returnFiber) {
if (!returnFiber.firstEffect) {
returnFiber.firstEffect = currentFiber.firstEffect;
}
if (!!currentFiber.lastEffect) {
if (!!returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = currentFiber.firstEffect;
}
returnFiber.lastEffect = currentFiber.lastEffect;
}

const effectTag = currentFiber.effectTag;
if (effectTag) {
if (!!returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = currentFiber;
} else {
returnFiber.firstEffect = currentFiber;
}
returnFiber.lastEffect = currentFiber;
}
}
}

function workLoop(deadline) {
let shouldYield = false;
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);//执行一个任务并返回下一个任务
shouldYield = deadline.timeRemaining() < 1;//如果剩余时间小于1毫秒就说明没有时间了,需要把控制权让给浏览器
}
//如果没有下一个执行单元了,并且当前渲染树存在,则进行提交阶段
if (!nextUnitOfWork && workInProgressRoot) {
commitRoot();
}
requestIdleCallback(workLoop);
}
//开始在空闲时间执行workLoop
requestIdleCallback(workLoop);

6.4 实现类组件

src/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React from './react';
import ReactDOM from './react-dom';
class ClassCounter extends React.Component {
constructor(props) {
super(props);
this.state = { number: 0 };
}
onClick = () => {
this.setState(state => ({ number: state.number 1 }));
}
render() {
return (
<div id="counter">
<span>{this.state.number}</span>
<button onClick={this.onClick}>加1</button>
</div >
)
}
}
ReactDOM.render(
<ClassCounter />,
document.getElementById('root')
);

src/react.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { ELEMENT_TEXT } from './constants';
+import { Update, UpdateQueue } from './updateQueue';
+import { scheduleRoot } from './scheduler';
function createElement(type, config, ...children) {
delete config.__self;
delete config.__source;
return {
type,
props: {
...config,
children: children.map(
child => typeof child === "object" ?
child :
{ type: ELEMENT_TEXT, props: { text: child, children: [] } })
}
}
}
+class Component {
+ constructor(props) {
+ this.props = props;
+ this.updateQueue = new UpdateQueue();
+ }
+ setState(payload) {// 可能是对象,也可能是一个函数
// 源码中updateQueue其实是此类组件对应的fiber节点的
+ // this.internalFiber.updateQueue.enqueueUpdate(new Update(payload));
+ this.updateQueue.enqueueUpdate(new Update(payload));
+ scheduleRoot();// 从根节点开始调度
+ }
+}
+Component.prototype.isReactComponent = {}; // 说明是类组件
let React = {
createElement,
+ Component
}
export default React;

新建src/updateQueue.js 更新队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
export class Update {
constructor(payload) {
this.payload = payload;
}
}
// 数据结构是一个单链表
export class UpdateQueue {
constructor() {
this.firstUpdate = null;
this.lastUpdate = null;
}
enqueueUpdate(update) {
if (this.lastUpdate === null) {
this.firstUpdate = this.lastUpdate = update;
} else {
this.lastUpdate.nextUpdate = update;
this.lastUpdate = update;
}
}
forceUpdate(state) {
let currentUpdate = this.firstUpdate;
while (currentUpdate) {
state = typeof currentUpdate.payload == 'function' ? currentUpdate.payload(state) : currentUpdate.payload;
currentUpdate = currentUpdate.nextUpdate;
}
this.firstUpdate = this.lastUpdate = null;
return state;
}
}

6.5 实现hooks

useState是语法糖,是基于useReducer实现的

6.5.1 useReducer

src/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import React from './react';
import ReactDOM from './react-dom';

function reducer(state, action) {
switch (action.type) {
case 'ADD':
return { count: state.count + 1 };
default:
return state;
}
}
function FunctionCounter() {
const [countState, dispatch] = React.useReducer(reducer, { count: 0 }); // hooks索引为0
// const [countState, dispatch] = React.useReducer(reducer, { count: 0 }); // hooks索引为1
return (
<div>
<h1 onClick={() => dispatch({ type: 'ADD' })}>
Count: {countState.count}
</h1 >
</div>
)
}
ReactDOM.render(
<FunctionCounter />,
document.getElementById('root')
);

src/schedule.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import { setProps, deepEquals } from './utils';
+import { UpdateQueue, Update } from './updateQueue';
import {
ELEMENT_TEXT, TAG_ROOT, TAG_HOST, TAG_TEXT, TAG_CLASS, TAG_FUNCTION, PLACEMENT, DELETION, UPDATE
} from './constants';
let currentRoot = null; //当前的根Fiber
let workInProgressRoot = null; //正在渲染中的根Fiber
let nextUnitOfWork = null; //下一个工作单元
let deletions = []; //要删除的fiber节点
+let workInProgressFiber = null; //正在工作中的fiber
+let hookIndex = 0; //hook索引


function updateFunctionComponent(currentFiber) {
+ workInProgressFiber = currentFiber;
+ hookIndex = 0;
+ workInProgressFiber.hooks = [];
const newChildren = [currentFiber.type(currentFiber.props)];
reconcileChildren(currentFiber, newChildren);
}
+export function useReducer(reducer, initialValue) {
// 为什么函数组件重新渲染 const [countState, dispatch] = React.useReducer(reducer, { count: 0 }); 里的状态拿的是上一次的状态
// workInProgressFiber.alternate.hooks[hookIndex] 拿到的是当前工作中的fiber的上一个fiber的当前这个索引hookIndex对应的hook
+ let newHook =
+ workInProgressFiber.alternate &&
+ workInProgressFiber.alternate.hooks &&
+ workInProgressFiber.alternate.hooks[hookIndex];
+ if (newHook) {// 拿上一个forceUpdate(newHook.state)hook的状态去更新 得到新的状态
+ newHook.state = newHook.updateQueue.forceUpdate(newHook.state);
+ } else { // 第一次渲染
+ newHook = {
+ state: initialValue,
+ updateQueue: new UpdateQueue() // 空的更新队列
+ };
+ }
+ const dispatch = action => { // {type:'ADD'}
+ let payload = reducer ? reducer(newHook.state, action) : action;// reducer有值,返回reducer()后的新对象,没有返回action
+ newHook.updateQueue.enqueueUpdate( // 增加update对象
+ new Update(payload)
+ );
+ scheduleRoot();
+ }
+ workInProgressFiber.hooks[hookIndex++] = newHook;
+ return [newHook.state, dispatch];
+}

每个fiber都有自己的hook,每个hook都有自己的state,updateQueue
这里hook是通过索引hookIndex来取上一个hook的,所以hook的数量和顺序不能改变,这样前后两次比较就会出错

6.5.2 useState

src/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function FunctionCounter() {
const [countState, useStateCount] = React.useState({ count: 0 });
return (
<div>
<h1 onClick={useStateCount(count+1)}>
Count: {countState.count}
</h1 >
</div>
)
}
ReactDOM.render(
<FunctionCounter />,
document.getElementById('root')
);

src/schedule.js

1
2
3
+export function useState(initState) {
+ return useReducer(null, initState)
+}

相关文章

http://www.zhufengpeixun.cn/2020/html/96.2.fiber.html

《深入浅出 React和Redux》第一遍

发表于 2020-05-25 | 更新于: 2020-08-06 | 分类于 其他

前言

目的:第一遍摘抄书中大部分内容,方便过第二、三遍时可以随时随地通过手机查看。

1.React

1、我们在JSX中看到一个组件使用了onClick,但是并没有产生直接使用onclick的HTML,而是使用事件委托的方式处理点击事件,无论有多少个onClick出现,其实最后都只在DOM树上添加了一个事件处理函数,挂在最高层的DOM节点上。所有的点击事件都被这个事件处理函数捕获,然后根据具体组件分配给特定函数。
因为React控制了组件的生命周期,在unmount的时候自然能够清除相关的所有事件处理函数,内存泄漏也不再是一个问题。

2、试试

1
2
使用 ` create-react-app ` 命令创建项目
执行 ` npm run eject` 将隐藏在 ` react-scripts ` 中的一系列技术栈配置都显示出来 (不可逆)

3、纯函数,指的是没有任何副作用,输出完全依赖输入的函数,两次函数调用如果输入相同,得到的结果也绝对相同。

4、web前端开发关于性能优化有一个原则:尽量减少DOM操作。虽然DOM操作也只是一些简单的JavaScript语句,但是DOM操作会引起浏览器对网页进行重新布局,重新绘制,这就是一个比JavaScript语句执行慢很多的过程。

5、分而治之 把问题分解为多个小问题,拆分组件。

6、prop是组件的对外接口,state是组件的内部状态,对外用prop,对内用state。

7、

1
2
3
4
5
6
7
8
class Counter extends React.Component{
super(props);
this.onClickIncrementButton = this.onClickIncrementButton.bind(this);
this.onClickInDecrementButton = this.onClickDecrementButton.bind(this);
this.state = {
count: props.initValue || 0
}
}

(1) 如果一个组件需要定义自己的构造函数,一定要在构造函数的第一行通过super调用父类也就是React.Component的构造函数。如果在构造函数中没有调用super(props),那么组件实例被构造之后,类实例的所有成员函数就无法通过this.props访问到父组件传递过来的props值。
(2) 在Count的构造函数中还给两个成员函数绑定了当前this的执行环境,因为ES6方法创建的React组件类并不自动给我们绑定this到当前实例对象。
(3) 在构造函数中可以通过参数props获得传入prop值,在其他函数中比如render中,可以通过this.props访问传入prop的值。

8、在开发环境中定义 propTypes, 在开发过程中避免犯错,但是生产环境做propTypes检查没什么帮助,还要消耗CPU计算资源。在生产环境用插件babel-react-optimize,生产环境安装可以去掉propTypes。

9、组件的生命周期的三个阶段:

  • 装载过程(Mount),也就是把组件第一次在DOM树中渲染的过程;
  • 更新过程(Update),当组件被重新渲染的过程;
  • 卸载过程(Unmount),组件从DOM中删除的过程。

9.1、装载过程

  • constructor
  • getInitialState
  • getDefaultProps
  • componentWillMount
  • render
  • componentDidMount

【constructor】目的:

  • 初始化state

  • 绑定成员函数的this环境
    在ES6语法下,类的每个成员函数在执行时的this并不是和类实例自动绑定的。而在构造函数中,this就是当前组件实例,为了方便调用,往往在构造函数中将这个实例的特定函数绑定this为当前函数。

    1
    this.onClickIncrementButton = this.onClickIncrementButton.bind(this);

    表示通过bind方法让当前实例中onClickIncrementButton函数被调用时,this始终指向当前组件实例。

【getInitialState、 getDefaultProps】只有在React.createClass方法创造的组件类才有作用。

【类组件名.defaultProps】在ES6中可以指定props 的初始值

【render】
函数应该是一个纯函数,完全根据this.state和this.props来决定返回的结果,而且不要产生任何副作用。

【componentDidMount】
render函数被调用完之后,componentDidMount函数并不是会被立刻调用,componentDidMount被调用的时候,render函数返回的东西已经引发了渲染,组件已经被”装载“到了DOM树上。

举例:

1
2
3
4
5
<App>
<Counter1></Counter1>
<Counter2></Counter2>
<Counter3></Counter3>
</App>

父组件包含三个子组件,在这4个组件中各自生命周期函数中打印文字,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
constructor App
componentWillMount App
render App
constructor Counter1
componentWillMount Counter1
render Counter1
constructor Counter2
componentWillMount Counter2
render Counter2
constructor Counter3
componentWillMount Counter3
render Counter3
componentDidMount Counter1
componentDidMount Counter2
componentDidMount Counter3
componentDidMount App
可以看到,虽然componentWillMount都是紧贴自己组价的render函数之前被调用,componentDidMount可不是跟着render函数被调用,当所有三个组件的render都被调用之后,三个组件的componentDidMount才连在一起被调用。
之所以会出现上面的现象是因为render函数本身并不往DOM树上渲染或者装载内容,它只是返回一个JSX的对象,然后由React库来根据返回对象决定如何渲染。而React库肯定是要把所有组件返回的结果综合起来,才能知道该如何产生对应的DOM修改。所以,只有React库调用三个Counter组件的render函数之后,才能有可能完成装载,这时候才会依次调用各个组件的componentDidMount函数作为装载过程的收尾。
componentDidMount只在浏览器端执行,在componentDidMount被调用的时候,组件已经被装载到DOM树上了,可以放心获取渲染出来的任何DOM。

【componentDidUpdate】
当props或者state被修改的时候,就会引发组件的更新过程。

【shouldComponentUpdate】
我们知道render函数应该是一个纯函数,这个纯函数的逻辑输入就是组件的props和state。所以,shouldComponentUpdate的参数就是接下来的props和state值。

【componentWillUnMount】
当React组件要从DOM树上删除掉之前,对应的componentWillUnMount函数就会被调用

10、第二章小结:React利用props来定义组件的对外接口,用state来代表内部的状态,某个数据选择用props还是state表示,取决于这个数据是对外还是对内。

2. Redux

  • 唯一数据源
  • 保持状态只读
  • 数据改变只能通过纯函数完成

2.1.1唯一数据源

指的是应用的状态数据应该只存储在唯一的一个store上。
这个唯一Store的状态,是一个树形的对象,每个组件往往只是用树形对象上一部分的数据。

2.1.2保持状态只读

就是说不能直接修改状态,要修改Store的状态,必须要通过派发一个action对象完成。

2.1.3数据改变只能通过纯函数完成

2.2 容器组件和展示组件

拆分容器组件和展示组件,是设计React组件的一种模式,和Redux没有直接关系。

2.1 react-redux

2.1.1 connect

connect(mapStateToProps, mapDispatchToProps)(Counter)

这里有两次函数执行,第一次是connect函数,第二次是把connect函数返回的函数再次执行,最后产生的就是容器组件。

2.1.2 Provider

react-redux要求store必须包含三个函数:subscribe、dispatch、getState。

3. 模块化 React 和 Redux 应用

  • 代码文件的组织结构;
  • 确定模块的边界;
  • Store的状态树设计。

3.1 代码文件的组织方式

Redux应用适合于“按功能组织”,也就是把完成同一应用功能的代码放在一个目录下,一个应用功能包含多个角色的代码。
拿Todo应用为例,那个应用的两个基本功能就是TodoList和Filter,所以代码文件目录如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
todoList/
actions.js
actionTypes.js
index.js
reducer.js
views/
component.js
container.js
filter/
actions.js
actionTypes.js
index.js // 这个文件把所有的角色导入,统一导出
reducer.js
views/
component.js
container.js

每个基本功能对应的其实就是一个功能模块,每个功能模块对应一个目录。
在这种组织方式下,当你要修改某个功能模块代码的时候,只要关注对应的目录就行了。

3.2 确定模块的边界

在理想的情况下,我们应该通过增加代码就能增加系统的功能,而不是通过对现有代码的修改来增加功能。

不同功能模块之间的依赖关系应该简单而且清晰,也就是所谓的保持模块之间低耦合性;一个模块应该把自己的功能封装的很好,让外界不要太依赖与自己内部的结构,这样不会因为内部的变化而影响外部模块的功能,就是所谓高内聚性。

React组件本身应该具有低耦合性和高内聚性的特点,不过,在Redux的游乐场中,React组件扮演的就是一个视图的角色,还有reducer、actions这些角色参与这个游戏。对于整个Redux应用而言,整体由模块构成,但是模块不再是React组件,而是由React组件加上相关reducer和actions构成的一个小整体。

可以预期每个模块之间会有依赖关系,比如filter模块想要todoList的action构造函数和视图,那么我们希望对方如何导入?一种写法是像下面的代码这样:

1
2
import * as actions from '../todoList/actions';
import container as TodoList from '../todoList/views/container';

todoList和filter中的文件名几乎一样,但是这毕竟是模块内部的事情,不应该假设所有模块都按照这样的文件夹名命名。

现在我们既然把一个目录看做一个模块,那我们要做的就是明确这个模块对外的接口,而这个接口应该实现把内部封装起来。todoList和filter模块目录下的index.js就是我们的模块接口。

比如,在todoList/index.js中,代码如下:

1
2
3
4
import * as actions from '../todoList/actions';
import reducer from './reducer';
import views as TodoList from '../todoList/views/container';
export {actions, reducer, views};

如果filter中的组件想要使用todoList中的功能,应该导入todoList这个目录,如下:

1
import {actions, reducer, views} from '../todoList';

3.3 状态树的设计

  • 一个模块控制一个状态节点
  • 避免冗余数据
  • 树形结构扁平

3.4 辅助开发工具

  • React Devtools:可以检视React组件的树形结构。
  • Redux Devtools:可以检视Redux数据流,可以将Store状态跳跃到任何一个历史状态,也就是所谓的“时间旅行”功能。
  • React Perf:可以发现React组件的渲染问题。

redux-immutable-state-invariant辅助包:每个reducer函数都必须是一个纯函数,不能修改传入的参数state和action,这个包可以在开发环境中使用,当不小心在reducer中修改了参数state,会给以错误警告。

4. 组件的性能优化

4.1 单个组件的性能优化

React Hooks

发表于 2020-05-25 | 更新于: 2020-07-24 | 分类于 React

解决的问题

  • 在组件之间复用状态逻辑很难,可能要用到render props和高阶组件,React 需要为共享状态逻辑提供更好的原生途径,Hook 使你在无需修改组件结构的情况下复用状态逻辑
  • 复杂组件变得难以理解,Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据)

注意事项

  • 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
  • 只能在 React 的函数组件中调用 Hook。

useState

  • useState 就是一个 Hook
  • 通过在函数组件里调用它来给组件添加一些内部 state,React 会在重复渲染时保留这个 state
  • useState 会返回一对值:当前状态和一个让你更新它的函数,你可以在事件处理函数中或其他一些地方调用这个函数。它类似 class 组件的 this.setState,但是它不会把新的 state 和旧的 state 进行合并
  • useState 唯一的参数就是初始 state
    1
    const [state, setState] = useState(initialState);

1】 alert会“捕获”点击按钮时候的状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React, {useState} from 'react';

function Counter(){
const [number,setNumber] = useState(0);
function alertNumber(){
setTimeout(()=>{
alert(number);
},3000);
}
return (
<>
<p>{number}</p>
<button onClick={()=>setNumber(number+1)}>+</button>
<button onClick={alertNumber}>alertNumber</button>
</>
)
}
export default Counter;

点击 alertNumber按钮一次, 再点击 +按钮两次,number变为 2, 3秒钟后alert 0

2】 函数式更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React, {useState} from 'react';

function Counter(){
const [number,setNumber] = useState(0);
function alertNumber(){
setTimeout(()=>{
setNumber(number=>number+1) // 函数式更新
},3000);
}
return (
<>
<p>{number}</p>
<button onClick={()=>setNumber(number+1)}>+</button>
<button onClick={alertNumber}>alertNumber</button>
</>
)
}
export default Counter;

如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setNumber。该函数将接收先前的 state,并返回一个更新后的值。

3】惰性初始state

initialState初始状态参数只会在组件初始渲染的时候调用,后续渲染会被忽略
与 class 组件中的 setState 方法不同,useState 不会自动合并更新对象。你可以用函数式的 setState 结合展开运算符来达到合并更新对象的效果

4】性能优化

Object.is

调用 State Hook 的更新函数并传入当前的 state 时,React 将跳过子组件的渲染及 effect 的执行。(React 使用 Object.is 比较算法 来比较 state。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, {useState} from 'react';

function Counter(){
const [state,setNumber] = useState(function(){
return {number: 0, name: '计数器'}
});
console.log("Counter render");
return (
<>
<p>{state.name}:{state.number}</p>
<button onClick={()=>setNumber({...state, number: state.number + 1})}>+</button>
<button onClick={()=>setNumber(state)}>old state +</button>
</>
)
}
export default Counter;

点击 + 按钮会输出 Counter render 组件刷新
点击 old state + 不更改数据,会刷新一次,之后都不会刷新组件

减少渲染次数

useCallback

减少渲染次数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 情况一
import React, {useState} from 'react';

function Counter(){
const [number,setNumber] = useState(0);
const [name,setName] = useState('test');
// 会在每次渲染的时候都会生成一个新的函数
const addClick = ()=> setNumber(number+1);
return (
<>
<p>{name}:{number}</p>
<button onClick={addClick}>+</button>
</>
)
}
export default Counter;

添加了useCallback之后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React, {useState, useCallback} from 'react';
let lastAddClick;
function Counter(){
const [number,setNumber] = useState(0);
const [name,setName] = useState('test');
// 添加了 useCallback,后面置了一个空数组
const addClick = useCallback(()=> setNumber(number+1), []);
// 只有在依赖的变量发生变化的时候才会重新生成
// const addClick = useCallback(()=> setNumber(number+1), [number]);

console.log(lastAddClick === addClick);
lastAddClick = addClick;
return (
<>
<p>{name}:{number}</p>
<button onClick={addClick}>+</button>
</>
)
}
export default Counter;

第一次是false,点击按钮可以看到后面都是 true
将line27注释打开,这里的number就是依赖的变量,所以这里当number改变的时候,都会重新生成 addClick

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 情况一
import React, {useState} from 'react';

function Child(props){
console.log("Child render");
return (
<button onClick={props.addClick}>{props.data.number}</button>
)
}

function App(){
const [number, setNumber] = useState(0);
const [name, setName] = useState('test');

const data = {number}
const addClick = ()=>setNumber(number+1);

return (
<div>
<input type="text" value={name} onChange={e => setName(e.target.value)} />
<Child addClick={addClick} data={data} />
</div>
)
}
export default App;

input框中改变input 的值触发onChange,App 和 Child 都会渲染
点击Child的button,改变number的值,App 和 Child 都会渲染

子组件Child是一个纯函数,希望只有当props改变的时候才会渲染Child组件

1】 memo
1
2
import React, {useState, memo} from 'react';
Child = memo(Child);
2】 useCallback

这里每次App组件渲染的时候都会重新生成addClick,改变如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
+ import React, {useState, useCallback, memo} from 'react';

let lastAddClick;
function App(){
const [number, setNumber] = useState(0);
const [name, setName] = useState('test');

const data = {number}
+ // 第二个参数表示此函数缓存依赖的变量,如果变量变了,会生成新的函数
+ const addClick = useCallback(()=>setNumber(number+1), [number]);
+ console.log("addClick:", addClick === lastAddClick);
+ lastAddClick = addClick;

return (
<div>
<input type="text" value={name} onChange={e => setName(e.target.value)} />
<Child addClick={addClick} data={data} />
</div>
)
}
function Child(props){
console.log("Child render");
return (
<button onClick={props.addClick}>{props.data.number}</button>
)
}
Child = memo(Child);
export default App;

第一次进来的时候 addClick: false,之后改变name的值时 都是true,只有改变 addClick函数缓存依赖的变量number时,才会重新生成,输出true

这里 const data = {number}每次App函数渲染的时候也都重新生成了

3】useMemo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
+ import React, {useState, useCallback, useMemo} from 'react';

let lastAddClick;
+ let lastData;
function App(){
const [number, setNumber] = useState(0);
const [name, setName] = useState('test');

const data = useMemo(()=>({number}),[number]);
const addClick = useCallback(()=>setNumber(number+1),[number]);
console.log("addClick:", lastAddClick === addClick);
lastAddClick = addClick;

+ console.log("data:", lastData === data); // 可以看到,一开始进来false,改变inpu中的值为true,点击按钮改变number 的值为false
+ lastData = data;

return (
<div>
<input type="text" value={name} onChange={e => setName(e.target.value)} />
<Child addClick={addClick} data={data} />
</div>
)
};

function Child(props){
console.log("Child render");
return (
<button onClick={props.addClick}>{props.data.number}</button>
)
};
Child = memo(Child);
export default App;

5】注意事项

只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。

因为react内部hook是以链表的形式一个一个按顺序存储的,如下情况,一次if判断,存储 ① ② ③,第二次if判断只存储 ① ② ,它是通过前后存储的hook按顺序判断的,少了一个就会出错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React, {useState, useEffect} from 'react';
function App() {
const [number, setNumber] = useState(0);
const [visible, setVisible] = useState(false);
if (number % 2 == 0) {
useEffect(() => {
setVisible(true);
}, [number]);
} else {
useEffect(() => {
setVisible(false);
}, [number]);
}
return (
<div>
<p>{number}</p>
<p>{visible && <div>visible</div>}</p>
<button onClick={() => setNumber(number + 1)}>+</button>
</div>
)
}
export default App;

useReducer

hooks的作者就是redux的作者

  • useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法
  • 在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import React, {useReducer} from 'react';

let initialState = {number: 0};
const INCREMENT = 'increment';
const DECREMENT = 'decrement';

function reducer(state, action) {
switch (action.type) {
case INCREMENT:
return {number: state.number + 1};
case DECREMENT:
return {number: state.number - 1};
default:
return state;
}
}

function App(){
const [state, dispatch] = useReducer(reducer, initialState);
// 这里的 initialState 就是 state的初始值
return (
<div>
<p>{state.number}</p>
<button onClick={()=>dispatch({type: INCREMENT})}>+</button>
<button onClick={()=>dispatch({type: DECREMENT})}>-</button>
</div>
)
};

export default App;

如上,点+加一,点-减一

useState是基于useReducer实现的,基于上面的useReducer自己实现useState

原生useState

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, {useState} from 'react';
let initialState = {number: 0};

function App(){
const [state, setState] = useState(initialState);
return (
<div>
<p>{state.number}</p>
<button onClick={()=>setState({number: state.number+1})}>+</button>
<button onClick={()=>setState({number: state.number-1})}>-</button>
</div>
)
};

export default App;

自定义useState

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import React, {useCallback, useReducer} from 'react';
let initialState = {number: 0};

// 自定义hooks
function useState(initialState){ // 内部就是用的useReducer
const reducer = useCallback((state,action)=>action.payload);
// 不用useCallback也可以,优化而已
// const reducer = (state,action)=>action.payload;
const [state, dispatch] = useReducer(reducer, initialState); // 这里的initialState就是上一行 (state,action) 中的state
function setState(payload){
// 调dispatch方法 派发action
dispatch({payload});
}
return [state, setState]; // 这里state有,setState需要自己实现
}

function App(){
const [state, setState] = useState(initialState);
return (
<div>
<p>{state.number}</p>
<button onClick={()=>setState({number: state.number+1})}>+</button>
<button onClick={()=>setState({number: state.number-1})}>-</button>
</div>
)
};

export default App;

useContext

  • 接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值
  • 当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定
  • 当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值
  • useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者 <MyContext.Consumer>
  • useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件提供 context

不使用useContext时的用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import React, {useState, useContext} from 'react';
let MyContext = React.createContext();

function Counter(){
return (
<MyContext.Consumer>
{
value => (
<div>
<p>{value.state.number}</p>
<button onClick={()=>value.setState({number: value.state.number+1})}>+</button>
</div>
)
}
</MyContext.Consumer>
);
}
function App(){
const [state, setState] = useState({number: 0});
return (
<MyContext.Provider value={{state, setState}}>
<Counter />
</MyContext.Provider>
)
};

export default App;

使用useContext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React, {useState, useContext} from 'react';
let MyContext = React.createContext();

function Counter(){
let {state, setState} = useContext(MyContext);
return (
<div>
<p>{state.number}</p>
<button onClick={()=>setState({number: state.number+1})}>+</button>
</div>
);
}
function App(){
const [state, setState] = useState({number: 0});
return (
<MyContext.Provider value={{state, setState}}>
<Counter />
</MyContext.Provider>
)
};

export default App;

effect

  • 在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性
  • 使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。你可以把 effect 看作从 React 的纯函数式世界通往命令式世界的逃生通道
  • useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 具有相同的用途,只不过被合并成了一个 API
  • 该 Hook 接收一个包含命令式、且可能有副作用代码的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, {useState, useEffect} from 'react';

function App(){
const [state, setState] = useState({number: 0});
// useEffect里的函数会在 componentDidMount、componentDidUpdate后进行调用
useEffect(()=>{
document.title = state.number;
})
return (
<div>
<p>{state.number}</p>
<button onClick={()=>setState({number: state.number+1})}>+</button>
</div>
)
};

export default App;

点击+页面数字变化,title也跟着变化

1】调过effect进行性能优化

  • 如果某些特定值在两次重渲染之间没有发生变化,你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数即可
  • 如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, {useState, useEffect} from 'react';

function App(){
const [state, setState] = useState({number: 0});
// 如果没有给第二个参数,函数会在每次执行渲染后调用
useEffect(()=>{
document.title = state.number;
}, [])
return (
<div>
<p>{state.number}</p>
<button onClick={()=>setState({number: state.number+1})}>+</button>
</div>
)
};

export default App;

如果没有第二个参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, {useState, useEffect} from 'react';

function App(){
const [state, setState] = useState({number: 0});

useEffect(()=>{
let timer = setInterval(()=>{
setState(state=>({number: state.number+1}));
},1000)
}, [])
return (
<div>
<p>{state.number}</p>
<button onClick={()=>setState({number: state.number+1})}>+</button>
</div>
)
};

export default App;

有第二个参数,它是每隔1s加一,没有,就会各种叠加,因为没渲染一次,就会生成一个新的定时器。

2】 清除副作用

  • 副作用函数还可以通过返回一个函数来指定如何清除副作用
  • 为防止内存泄漏,清除函数会在组件卸载前执行。另外,如果组件多次渲染,则在执行下一个 effect 之前,上一个 effect 就已被清除
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import React, {useState, useEffect} from 'react';

function App(){
const [state, setState] = useState({number: 0});

useEffect(()=>{
let timer = setInterval(()=>{
setState(state=>({number: state.number+1}));
},1000)
}, [])
return (
<div>
<p>{state.number}</p>
<button onClick={()=>setState({number: state.number+1})}>+</button>
</div>
)
};

function Counter(){
const [visible, setVisible] = useState(true);
return (
<div>
{
visible && <App />
}
<button onClick={()=>setVisible(false)}>hide</button>
</div>
)
}

export default App;

点击hide按钮后,组件App不显示,页面报错

1
Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

不能在一个已经卸载的组件上执行状态更新。会出现内存泄露。为了修复,需要在useEffect清理函数中取消所有的订阅和异步任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import React, {useState, useEffect} from 'react';

function App(){
const [state, setState] = useState({number: 0});

useEffect(()=>{
let timer = setInterval(()=>{
setState(state=>({number: state.number+1}));
},1000)

// useEffect会返回一个清理函数,当组件将要卸载的时候会执行清理函数
return ()=>{
clearInterval(timer);
}
}, [])
return (
<div>
<p>{state.number}</p>
<button onClick={()=>setState({number: state.number+1})}>+</button>
</div>
)
};

function Counter(){
const [visible, setVisible] = useState(true);
return (
<div>
{
visible && <App />
}
<button onClick={()=>setVisible(false)}>hide</button>
</div>
)
}
export default Counter;

3】useRef

  • useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)
  • 返回的 ref 对象在组件的整个生命周期内保持不变
1
const refContainer = useRef(initialValue);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import React, {useState, useRef} from 'react';

let lastRef;
function Child(){
// let refObj = React.createRef();
let refObj = useRef();
console.log(lastRef === refObj);
lastRef = refObj;

function getFocus(){
refObj.current.focus()
}
return (
<div>
<input ref={refObj} />
<button onClick={getFocus}>focus</button>
</div>
)
}
function App(){
const [state, setState] = useState({number: 0});
return (
<div>
<p>{state.number}</p>
<button onClick={()=>setState({number: state.number+1})}>+</button>
<Child />
</div>
)
};
export default App;

用 React.createRef时, 每次渲染Child组件都会重新生成 refObj
用 useRef时,返回的ref对象在组件的整个生命周期内保持不变。

实现useRef

1
2
3
4
5
6
7
let currentRefObject;
function useRef(){
if(!currentRefObject){
currentRefObject = {current: null};
}
return currentRefObject;
}

4】forwardRef

现在想把获取焦点按钮放到App组件中,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, {useRef} from 'react';

function Child(props){
return (
<div>
<input ref={props.ref} />
</div>
)
}
function App(){
let refObj = useRef();
return (
<div>
<Child ref={refObj} />
<button onClick={()=>refObj.current.focus()}>focus</button>
</div>
)
};
export default App;

页面会报错

1
Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

使用React.forwardRef

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
+import React, {useRef} from 'react';

+function Child(props,ref){
return (
<div>
+ <input ref={ref} />
</div>
)
}
+let ForwardRefChild = React.forwardRef(Child);
function App(){
let refObj = useRef();
function getFocus(){
refObj.current.focus()
}
return (
<div>
<ForwardRefChild ref={refObj} />
<button onClick={getFocus}>focus</button>
</div>
)
};
export default App;

直接这样传递的话,Child组件的输入框对象直接暴露给了父组件,也破坏了封装的原则,很危险

useImperativeHandle

  • useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值
  • 在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import React, {useRef, useImperativeHandle} from 'react';

function Child(props,ref){
let refObj = useRef();
useImperativeHandle(ref, () => ({
focus(){
refObj.current.focus();
}
}),
)
return (
<div>
<input ref={refObj} />
</div>
)
}
let ForwardRefChild = React.forwardRef(Child);
function App(){
let refObj = useRef();
function getFocus(){
refObj.current.focus();
refObj.current.value = 'xxxx';
}
return (
<div>
<ForwardRefChild ref={refObj} />
<button onClick={getFocus}>focus</button>
</div>
)
};
export default App;

如果不用useImperativeHandle,子组件直接用父组件传递过来的<input ref={ref} />,当父组件App执行到line22时,就会直接取修改子组件input中的value,现在父组件中getFocus方法只有line21是生效的,因为它调用的focus方法是子组件有暴露给外部的。

useLayoutEffect

  • 其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect
  • 可以使用它来读取 DOM 布局并同步触发重渲染
  • 在浏览器执行绘制之前useLayoutEffect内部的更新计划将被同步刷新
  • 尽可能使用标准的 useEffect 以避免阻塞视图更新

如图是浏览器呈现一张页面的过程。
在红色分割线左边界面并没有绘制,绿色圈中是布局,蓝色圈中是绘制,页面上能看到效果。
如图可以看到 useLayoutEffect 和 useEffect 的执行顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React, {useState, useEffect, useLayoutEffect} from 'react';

function App(){
const [color, setColor] = useState('red');
useEffect(()=>{
console.log("useEffect", color);
})
useLayoutEffect(()=>{
alert("useLayoutEffect");
})
return (
<div>
<p style={{backgroundColor: color}}>背景色</p>
<button onClick={()=>setColor('red')}>红</button>
<button onClick={()=>setColor('yellow')}>黄</button>
<button onClick={()=>setColor('blue')}>蓝</button>
</div>
)
};
export default App;

点击按钮颜色切换时,先alert useLayoutEffect,界面变色,再console.log出 useEffect

自定义hooks

  • 有时候我们会想要在组件之间重用一些状态逻辑
  • 自定义 Hook 可以让你在不增加组件的情况下达到同样的目的
  • Hook 是一种复用状态逻辑的方式,它不复用 state 本身
  • 事实上 Hook 的每次调用都有一个完全独立的 state
  • 自定义 Hook 更像是一种约定,而不是一种功能。如果函数的名字以 use 开头,并且调用了其他的 Hook,则就称其为一个自定义 Hook

示例

当有2个逻辑相同的组件时,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import React, {useState, useEffect} from 'react';
import ReactDOM from 'react-dom';
function Counter1(){
const [number, setNumber] = useState(0);
useEffect(()=>{
let timer = setInterval(()=>{
setNumber(number+1);
}, 1000);
return ()=>{
clearInterval(timer);
}
})
return (
<div>
{number}
</div>
)
}
function Counter2(){
const [number, setNumber] = useState(0);
useEffect(()=>{
let timer = setInterval(()=>{
setNumber(number+1);
}, 1000);
return ()=>{
clearInterval(timer);
}
})
return (
<div>
{number}
</div>
)
}

ReactDOM.render(
<>
<Counter1 />
<Counter2 />
</>,
document.getElementById('root')
);

页面上同时展示 每隔1秒 number + 1

使用自定义hooks(函数的名字以 use 开头,并且调用了其他的 Hook),如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import React, {useState, useEffect} from 'react';
import ReactDOM from 'react-dom';

function useCounter(){
const [number, setNumber] = useState(0);
useEffect(()=>{
let timer = setInterval(()=>{
setNumber(Math.random());
}, 1000);
return ()=>{
clearInterval(timer);
}
})
return number;
}
function Counter1(){
let number = useCounter();
return (
<div>
{number}
</div>
)
}
function Counter2(){
let number = useCounter();
return (
<div>
{number}
</div>
)
}

ReactDOM.render(
<>
<Counter1 />
<Counter2 />
</>,
document.getElementById('root')
);

这里line8,将number生成了随机数来展示说明,Hook是一种复用状态逻辑的方式,它不复用state本身,事实上Hook的每次调用都有一个完全独立的state

中间件

logger

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import React, {useReducer, useEffect} from 'react';

let initialState = {number: 0};
const INCREMENT = 'increment';
const DECREMENT = 'decrement';

function reducer(state, action) {
switch (action.type) {
case INCREMENT:
return {number: state.number + 1};
case DECREMENT:
return {number: state.number - 1};
default:
return state;
}
}
+// 实现redux-logger 在每次状态变更后打印新的状态值;
+// redux中间件,是用新的dispatch替换老的dispatch
+function useLogger(reducer, initialState){
+ const [state, dispatch] = useReducer(reducer, initialState);
+ function loggerDispatch(action){
+ console.log("老状态:", state);
+ dispatch(action);
+ }
+ useEffect(()=>console.log("新状态", state));
+ return [state, loggerDispatch]
+}
function App(){
+ const [state, dispatch] = useLogger(reducer, initialState); // 这里调用 useLogger
return (
<div>
<p>{state.number}</p>
<button onClick={()=>dispatch({type: INCREMENT})}>+</button>
<button onClick={()=>dispatch({type: DECREMENT})}>-</button>
</div>
)
};

export default App;

thunk

thunk里面派发的是函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import React, {useReducer, useEffect} from 'react';

let initialState = {number: 0};
const INCREMENT = 'increment';
const DECREMENT = 'decrement';

function reducer(state, action) {
switch (action.type) {
case INCREMENT:
return {number: state.number + 1};
case DECREMENT:
return {number: state.number - 1};
default:
return state;
}
}

+function useThunk(reducer, initialState){
+ const [state, dispatch] = useReducer(reducer, initialState);
+ function thunkDispatch(action){
+ // 判断是否为函数,这里模拟的是redux-logger,line23的参数是根据line35中的来,这里传入新的dispatch,也就是thunkDispatch
+ if(typeof action === 'function'){
+ action(thunkDispatch, ()=>state);
+ }else{
+ dispatch(action);
+ }
+ }
+ return [state, thunkDispatch]
+}
function App(){
+ const [state, dispatch] = useThunk(reducer, initialState);
return (
<div>
<p>{state.number}</p>
+ <button onClick={()=>dispatch(function(dispatch, getState){
+ dispatch({type: INCREMENT})
+ })}>+</button>
<button onClick={()=>dispatch({type: DECREMENT})}>-</button>
</div>
)
};

export default App;

promise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import React, {useReducer, useEffect} from 'react';

let initialState = {number: 0};
const INCREMENT = 'increment';
const DECREMENT = 'decrement';

function reducer(state, action) {
switch (action.type) {
case INCREMENT:
return {number: state.number + 1};
case DECREMENT:
return {number: state.number - 1};
default:
return state;
}
}

+function usePromise(reducer, initialState){
+ const [state, dispatch] = useReducer(reducer, initialState);
+ function promiseDispatch(action){
+ if(typeof action.then === 'function'){
+ action.then(promiseDispatch); // 点击按钮 1秒 后加1
+ }else{
+ dispatch(action);
+ }
+ }
+ return [state, promiseDispatch]
+}
function App(){
+ const [state, dispatch] = usePromise(reducer, initialState);
return (
<div>
<p>{state.number}</p>
+ <button onClick={()=>dispatch(new Promise(function(resolve){
+ setTimeout(()=>{
+ resolve({type: INCREMENT})
+ },1000)
+ }))}>+</button>
<button onClick={()=>dispatch({type: DECREMENT})}>-</button>
</div>
)
};

export default App;

ajax

新建api.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let express = require('express');
let app = express();
app.use(function (req, res, next) {
res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
next();
});
app.get('/api/users', function (req, res) {
let offset = parseInt(req.query.offset);// 偏移量
let limit = parseInt(req.query.limit); // 每页的条数
let result = [];
for (let i = offset; i < offset + limit; i++) {
result.push({ id: i + 1, name: 'name' + (i + 1) });
}
res.json(result);
});
app.listen(8000);

npm install express
node ./api.js 运行后台

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import React, { useState, useEffect, useLayoutEffect } from 'react';
import ReactDOM from 'react-dom';
function useRequest(url) {
let limit = 5;
let [offset, setOffset] = useState(0);
let [data, setData] = useState([]);
async function loadMore() {
setData(null);
let pageData = await fetch(`${url}?offset=${offset}&limit=${limit}`)
.then(response => response.json());
setData([...data, ...pageData]);
setOffset(offset + pageData.length);
}
useEffect(loadMore, []);
return [data, loadMore];
}

function App() {
const [users, loadMore] = useRequest('http://localhost:8000/api/users');
if (users === null) {
return <div>正在加载中....</div>
}
return (
<>
<ul>
{
users.map((item, index) => <li key={index}>{item.id}:{item.name}</li>)
}
</ul>
<button onClick={loadMore}>加载更多</button>
</>
)
}
ReactDOM.render(<App />, document.getElementById('root'));

小结

useMemo、useCallback、useRef
本质上都是为了缓存,这些东西在没有hooks之前,在以前我们都用类组件,一但创建就有类的实例,上面的属性也可以存在。但是现在我们用hooks,hooks只能用在函数组件里,函数组件没有this,就没有实例,就没有办法在实例上挂属性和状态。现在就要靠 useMemo、useCallback、useRef 实现缓存。

官方文档

https://zh-hans.reactjs.org/docs/hooks-intro.html

1、React 需要为共享状态逻辑提供更好的原生途径。
2、你可以使用 Hook 从组件中提取状态逻辑,使得这些逻辑可以单独测试并复用。Hook 使你在无需修改组件结构的情况下复用状态逻辑。 这使得在组件间或社区内共享 Hook 变得更便捷。

相关文章

https://zh-hans.reactjs.org/docs/hooks-intro.html
http://www.zhufengpeixun.cn/2020/html/62.5.react-hooks.html

React+Redux+Antd

发表于 2020-05-23 | 更新于: 2020-09-09 | 分类于 React , Redux

1. 环境搭建

npx create-react-app myapp

版本:

1
2
3
4
5
6
"react": "^16.13.1",
"redux": "^4.0.5",
"react-dom": "^16.13.1",
"react-router-dom": "^5.2.0",
"@babel/core": "^7.9.6",
"webpack": "^4.43.0",

1.1 安装webpack

npm install --save-dev webpack-cli webpack webpack-merge webpack-dev-server clean-webpack-plugin

具体步骤见 https://jiafei2333.github.io/2019/10/12/Webpack-base/

1.2 安装loader

npm install style-loader css-loader --save-dev
npm install less less-loader --save-dev
npm install file-loader --save-dev

1.3 安装babel

npm install @babel/core @babel/preset-env babel-loader --save-dev
npm i @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators --save-dev
npm install core-js@2 --save
npm install --save-dev @babel/plugin-transform-runtime
npm install --save @babel/runtime
npm install --save-dev @babel/plugin-syntax-dynamic-import

具体步骤见 https://jiafei2333.github.io/2019/11/13/Webpack-js/

1.4 安装开发环境

npm install --save redux
npm install --save react-redux
npm install --save redux-saga
npm install --save react-router-dom
npm install redux-thunk redux-logger --save
npm install mini-css-extract-plugin --save-dev // 抽离css文件

1.5 配置node环境

新建 nodejs/server.js
nodejs server.js启动服务

可以安装nodemon node的监视器 监视文件变化:
npm install nodemon -g 使用: nodemon 文件名(可以增加配置文件)
yarn add express
yarn add cors

1.6 基础目录创建

1.7 UI组件库

npm install antd --save

1.8 错误处理

2. 基础功能

2.1 将redux与react-router 连接

2.1.1 前情

src/redux/actions/home.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import {fetchLoginAdd} from '../actionServer/home';
import {LOGIN_ADD} from '../action-types';
function LoginPost(data){ // redux-thunk
return async function(dispatch, getState){
let all = await fetchLoginAdd(data);
if(all.Code === 0){
dispatch({type: LOGIN_ADD})
// 当这里登陆成功需要跳转到首页,直接切换路由时,需要用到dispatch(push('/xxx'));
// 引入push import { push } from 'connected-react-router';
}
}
}
export {
LoginPost
}

在redux actions中可以用dispatch(push(‘/xxxx’))切换路由
通过 connected-react-router 和 history 两个库将 react-router 与 redux 进行深度整合实现。

npm install connect-react-router history --save

然后给 store 添加如下配置:

  • 创建history对象,因为我们的应用是浏览器端,所以使用createBrowserHistory创建
  • 使用connectRouter包裹 root reducer 并且提供我们创建的history对象,获得新的 root reducer
  • 使用routerMiddleware(history)实现使用 dispatch history actions,这样就可以使用push(‘/path/to/somewhere’)去改变路由(这里的 push 是来自 connected-react-router 的)

store.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import {createStore, applyMiddleware} from 'redux';
import thunk from 'redux-thunk';
import { createBrowserHistory } from 'history';
import { routerMiddleware } from 'connected-react-router';
import rootReducers from '../reducers/reducer';

export const history = createBrowserHistory();

const initialState = {};

const store = createStore(
rootReducers(history),
initialState,
applyMiddleware(thunk, routerMiddleware(history)));
export default store;

src/reducers/reducers.js

1
2
3
4
5
6
7
8
9
10
11
import {combineReducers} from 'redux';
import { connectRouter } from 'connected-react-router';
import appReduce from './appReduce';

const createRootReducer = history => {
return combineReducers({
router: connectRouter(history),
appReduce
})
}
export default createRootReducer;

根组件中,添加如下配置

  • 使用ConnectedRouter包裹路由,并且将 store 中创建的history对象引入,作为 props 传入应用
  • ConnectedRouter组件要作为Provider的子组件

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React from 'react';
import ReactDOM from 'react-dom';
import {Provider} from 'react-redux';
import {ConnectedRouter} from 'connected-react-router';
import store,{history} from 'Redux/store/store';
import Router from './router';

ReactDOM.render(
<Provider store={store}>
<ConnectedRouter history={history}>
<Router />
</ConnectedRouter>
</Provider>,
document.getElementById('root'));

这样就将 redux 与 react-router 整合完毕了。

2.2 顶部加载动画 NProgress

npm install --save nprogress

2.3 路由登录判断

src/router.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import React from 'react';
import {Route, Switch, Redirect } from 'react-router-dom';
import {ConnectedRouter} from 'connected-react-router';
import Home from 'Pages/Home/Home';
import Page404 from 'Pages/404';
import LoginIndex from 'Pages/Home/LoginIndex';
import PrivateRoute from 'Components/Base/PrivateRoute';

const RouteApp = ({history})=>{
const Routes = [
{
path: '/',
needAuth: true, // true 需要判断是否登录
component: Home,
},
{
path: '/login',
needAuth: false,
component: LoginIndex,
},
]
return (
<ConnectedRouter history={history} >
<Switch>
{
Routes.map(({path, needAuth, component}, index)=>{
if(needAuth === true){ // 需要做登录判断
return <PrivateRoute key={index} exact path={path} component={component} history={history} />

}else{
return <Route key={index} exact path={path} component={component} />
}
})
}
<Redirect to="/" render={Page404} />
</Switch>
</ConnectedRouter>
)
};
export default RouteApp;

src/components/Base/PrivateRoute.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import {Route, Redirect} from 'react-router-dom';
import {E} from 'Config/E';

export const PrivateRoute = ({path, component, history}) => {
let isLoginIn = window.localStorage.getItem(`${E.SERVER_TOKEN}token`);
if(!isLoginIn){ // 没有登录 重定向到登录页面
return (
<Redirect to="/login" />
)
}else{
return (
<Route exact path={path} component={component} />
)
}
}
export default PrivateRoute;

2.4 使用redux-saga

关于为什么将redux-thunk换成redux-saga,及redux-saga的知识点查看 #5.1 #5.2

src/redux/sagas/rootSaga.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 登录
function * watchPostLoginIn(){
while(true){
const action = yield take(types.POST_LOGIN_IN);
// 这里action返回的就是页面上dispatch的内容
const all = yield call(homeServer.getLoginAdd, action.payload);
if(all.Code === 0){ // 登录成功跳转到首页
// 存储token
setToken(all.Data.Token);
// 登录成功跳转到首页
yield put(push('/'));
// 如果要存储数据 这里走reducers
yield put({type: SET_PARAMS, payload:{...}});
}
}
}
export default function* rootSaga() {
yield fork(watchPostLoginIn);
}

/src/store/store.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import {createStore, applyMiddleware, compose} from 'redux';
+import createSagaMiddleware from 'redux-saga';
import logger from 'redux-logger';
import { createBrowserHistory } from 'history';
import { routerMiddleware } from 'connected-react-router';
import rootReducers from 'Redux/reducers/reducer';
+import rootSagas from 'Redux/sagas/rootSaga';

// 创建一个saga中间件
+const sagaMiddleware = createSagaMiddleware()

export const history = createBrowserHistory();

const initialState = {};

const store = createStore(
rootReducers(history),
initialState,
compose(
applyMiddleware(
routerMiddleware(history),
+ sagaMiddleware, // 将sagaMiddleware 中间件传入到 applyMiddleware 函数中
logger
)
)
);

// 动态执行saga,注意:run函数只能在store创建好之后调用
+sagaMiddleware.run(rootSagas)

export default store;

src/pages/Home/LoginIndex.js

1
2
3
4
5
6
7
8
9
onSubmit(value){
this.props.postLoginIn({userName: value.username, password: value.password});
}
...
const mapDispatchToProps = dispatch => {
return {
postLoginIn: (data) => dispatch({type: POST_LOGIN_IN,payload:data})
}
}

3. 疑问

3.1 push跳转

在redux/actions中dispatch(push("/home"))做了路由跳转,url地址栏改变了,但是页面没有切换
src/router.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React from 'react';
import {BrowserRouter, Route, Switch, Redirect, Link } from 'react-router-dom';
import {ConnectedRouter} from 'connected-react-router';
import Home from './pages/Home/Home';
import LoginIndex from './pages/Home/LoginIndex';

const RouteApp = ({history})=>{
return (
<ConnectedRouter history={history} >
{/* <BrowserRouter> */}
<Switch>
<Route path="/home" exact={true} component={Home} />
<Route path="/Login" exact={true} component={LoginIndex} />
<Redirect to="/" ></Redirect>
</Switch>
{/* </BrowserRouter> */}
</ConnectedRouter>

)
};
export default RouteApp;

现在将line10、line16注释,可以跳转,url和页面都切换。

3.2 引入less文件

直接import 引入不了 ??
import Style from './style.less'; Style打印出一个空对象 ??

改成
import './style.less'; 直接引入
className='class的名字'

3.3 全局下的菜单栏数据

我的想法是没有做数据交互的组件都写成函数组件,所以这里写顶部菜单栏组件一开始写的是函数组件,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import React from 'react';
import { Layout, Menu } from 'antd';
import {E} from 'Config/E';

const { Header, Content, Footer } = Layout;

const MainLayout = ({store, history, children}) =>{
const appReducers = store.getState();
let isLoginIn = window.localStorage.getItem(`${E.SERVER_TOKEN}token`);
return (
<>
{
!isLoginIn ?
<>{children}</>
:
<Layout className="layout">
{
appReducers.mainMenu ?
<Header>
<div className="logo" />
<Menu theme="dark" mode="horizontal" defaultSelectedKeys={['2']}>
<Menu.Item key="1">nav 1</Menu.Item>
<Menu.Item key="2">nav 2</Menu.Item>
<Menu.Item key="3">nav 3</Menu.Item>
</Menu>
</Header> : ""
}
<Content style={{ padding: '0 50px' }}>
{children}
</Content>
<Footer style={{ textAlign: 'center' }}>Ant Design ©2018 Created by Ant UED</Footer>
</Layout>
}
</>

)
}
export default MainLayout;

但是当首页componentDidMount中获取数据,redux reducer数据改变,页面没有更新,所以只能改为Class Component,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import React, { Component } from 'react';
import { connect } from 'react-redux';
import {Link} from 'react-router-dom';
import { Layout, Menu } from 'antd';
import {E} from 'Config/E';

const { Header, Content, Footer } = Layout;

export class MainLayout extends Component {
constructor(props){
super(props);
this.isLoginIn = window.localStorage.getItem(`${E.SERVER_TOKEN}token`);
}
render() {
const {mainMenu, children} = this.props;
return (
<>
{
!this.isLoginIn ?
<>{children}</>
:
<Layout className="layout">
{
mainMenu ?
<Header>
<div className="logo" />
<Menu theme="dark" mode="horizontal" defaultSelectedKeys={['2']}>
{
mainMenu.map((item)=>{
return (
<Menu.Item key={item.ModeCode}><Link key={item.ModeCode} to={item.ModeUrl}>{item.ModeName}</Link></Menu.Item>
)
})
}
</Menu>
</Header> : ""
}
<Content style={{ padding: '0 50px' }}>
{children}
</Content>
<Footer style={{ textAlign: 'center' }}>Ant Design ©2018 Created by Ant UED</Footer>
</Layout>
}
</>
)
}
}

const mapStateToProps = (state) => ({
mainMenu: state.appReduce.mainMenu
})

const mapDispatchToProps = {

}
export default connect(mapStateToProps, mapDispatchToProps)(MainLayout)

当mainMenu数据变化的时候页面菜单自动刷新显示。

3.4 路由问题

3.4.1 路由保护

PrivateRoute.js 文件 这样写组件加载失败,之前好几周都是好好的,不知道为什么突然不行了??

3.4.2 刷新问题

【问题】
三级路由localhost:3000/editorialCenter/auditing/auditPending刷新界面又 http://localhost:3000/editorialCenter/auditing/bundle.js 404 (Not Found)

【解决】
webpack.base.js

1
2
3
output:{
publicPath: '/'
}

3.4.3 BrowserRouter 重定向问题

和 HashRouter 不同的是,BrowserRouter路由跳转会根据 /xxx 后面的具体页面去服务器请求,在开发模式下可以配置如下:

1
2
3
devServer:{
historyApiFallback: true,
}

在生产环境下,我这里的后台是.net环境,远程桌面是用IIS配置的站点环境,只要在服务器端下载安装 https://www.iis.net/downloads/microsoft/url-rewrite,然后在打包好的跟目录下添加项目中的 web.config文件即可。

3.5 优化问题

3.5.1 babel

babel-import-plugin

在.babelrc中配置

1
2
3
4
5
6
7
8
9
"plugins": [
["import",
{
"libraryName": "antd",
"libraryDirectory": "es",
"style": true // or 'css'
}
]
]

跟没有配置对比,多出了将近300k的css文件,其他文件的大小都是一样的 ???为啥

4. 报错

4.1 ant 引入.less后缀的样式文件

1
2
// import 'antd/dist/antd.css';
import 'antd/dist/antd.less';

将 build/webpack.base.js

1
2
3
4
{
test: /\.less$/,
use: ["style-loader", "css-loader", "less-loader"]
}

修改为

1
2
3
4
5
6
7
8
9
{
test: /\.less$/,
use: ["style-loader", "css-loader", {
loader: "less-loader",
options: {
javascriptEnabled: true
}
}]
}

报错

当前版本 "less-loader": "^6.1.0",查了之后都说v6有兼容性问题,所以将版本改为"less-loader": "5.0.0",,这样就可以了,没有报错。

4.2 speed-measure-webpack-plugin

speed-measure-webpack-plugin 和 HotModuleReplacementPlugin 不能同时使用,否则会报错。所以在开发环境中先把费时分析插件注释,见[webpack.base.js]。

5. 知识点

5.1 redux-thunk

redux-thunk的原理就是判别action的类型,如果action是函数,就调用这个函数。thunk仅仅做了执行这个函数,并不在乎函数主体内是什么,也就是说thunk使得redux可以接受函数作为action,但是函数的内部可以多种多样。比如下面是一个获取商品列表的异步操作所对应的action:

1
2
3
4
5
6
7
8
9
10
11
12
13
export default ()=>(dispatch)=>{
fetch('/api/goodList',{ //fecth返回的是一个promise
method: 'get',
dataType: 'json',
}).then(function(json){
var json=JSON.parse(json);
if(json.msg==200){
dispatch({type:'init',data:json.data});
}
},function(error){
console.log(error);
});
};

从这个具有副作用的action中,我们可以看出,函数内部极为复杂。如果需要为每一个异步操作都如此定义一个action,显然action不易维护。

action不易维护的原因:

  • action的形式不统一
  • 就是异步操作太为分散,分散在了各个action中

5.2 redux-saga

从 Saga 内触发异步操作(Side Effect)总是由 yield 一些声明式的 Effect 来完成的。

5.2.1 call

https://redux-saga-in-chinese.js.org/docs/basics/DeclarativeEffects.html

redux-saga 提供了一个不一样的方式来执行异步调用

1
2
3
4
5
6
import { call } from 'redux-saga/effects'

function* fetchProducts() {
const products = yield call(Api.fetch, '/products')
// ...
}

call 是一个会阻塞的 Effect。即 Generator 在调用结束之前不能执行或处理任何其他事情。
call 不仅可以用来调用返回 Promise 的函数。我们也可以用它来调用其他 Generator 函数。
fork表示无阻塞调用。

5.2.2 take

1
2
3
4
5
6
7
8
9
10
11
function* loginFlow() {
while(true) {
const {user, password} = yield take('LOGIN_REQUEST')
const token = yield call(authorize, user, password)
if(token) {
yield call(Api.storeItem({token}))
yield take('LOGOUT')
yield call(Api.clearItem('token'))
}
}
}

loginFlow 首先等待一个 LOGIN_REQUEST action。 然后在 action 的 payload 中获取有效凭据(即 user 和 password)并调用一个 call 到 authorize 任务。

5.2.3 put

redux-saga做为中间件,工作流是这样的:

UI——>action1————>redux-saga中间件————>action2————>reducer..

从工作流中,我们发现redux-saga执行完副作用函数后,必须发出action,然后这个action被reducer监听,从而达到更新state的目的。相应的这里的put对应与redux中的dispatch,工作流程图如下:

从图中可以看出redux-saga执行副作用方法转化action时,put这个Effect方法跟redux原始的dispatch相似,都是可以发出action,且发出的action都会被reducer监听到。

1
yield put({type:'login'})

5.2.4 select

put方法与redux中的dispatch相对应,同样的如果我们想在中间件中获取state,那么需要使用select。select方法对应的是redux中的getState,用户获取store中的state,使用方法:

1
const state= yield select()

5.2.5 fork

fork方法相当于web work,fork方法不会阻塞主线程,在非阻塞调用中十分有用。
当我们 fork 一个 任务,任务会在后台启动,调用者也可以继续它自己的流程,而不用等待被 fork 的任务结束。

5.2.6 takeEvery和takeLatest

takeEvery和takeLatest用于监听相应的动作并执行相应的方法,是构建在take和fork上面的高阶api,比如要监听login动作,takeEvery方法可以:

1
yield takeEvery('login',loginFunc)

takeEvery监听到login的动作,就会执行loginFunc方法,除此之外,takeEvery可以同时监听到多个相同的action。在上面的例子中,takeEvery 允许多个 loginFunc 实例同时启动

takeLatest方法跟takeEvery是相同方式调用:

1
takeLatest('login',loginFunc)

与takeLatest不同的是,takeLatest是会监听执行最近的那个被触发的action。
在任何时刻 takeLatest 只允许执行一个 loginFunc 任务,并且这个任务是最后被启动的那个,如果之前已经有一个任务在执行,那之前的这个任务会自动被取消。

5.3 webpack

5.3.1 Source Map

SourceMap是一种映射关系。当项目运行后,如果出现错误,我们可以利用sourceMap反向定位到源码。
sourceMap就是一个信息文件,里面储存着打包前的位置信息。也就是说,转换后的代码的每一个位置,所对应的转换前的位置。

相关文章:https://juejin.im/post/6844903971648372743

6. 功能模块

新增独立的功能模块

6.1 采编中心

src/pages/EditorialCenter/Auditing 审模块

自定义useRequest hook 封装统一列表逻辑,包括获取列表数据,分页;其中每个页面组件顶部的搜索条件不唯一,所以这里只将 请求列表数据的接口 + PageIndex + PageSize,进行了封装,各个页面的参数以Object.assign 拼接的方式传入。

6.1.1 自定义hook

1. 列表数据请求 + 分页

src/pages/EditorialCenter/Auditing/auditPending.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import React, {useMemo} from 'react';
import { Table } from 'antd';
import get from 'lodash/get';
import {getEditorialCenterListJson} from 'Redux/actionServer/content'; // 数据请求接口
import useRequest from '../hooks/useRequest'; // 自定义hook

function getColumns(){
const columns = [
... 这里是table的column
];
return columns;
}

const AuditPending = () =>{
// 本页面组件的参数项(不唯一,所以不封装)
const params = useMemo(()=>({
Keyword: "",
SSubmitTime: "",
ESubmitTime: "",
FlowType: 0,
ReviewStatus: 99,
Sort: 0,
}));
const {data, loading, PageIndex, PageSize, setPagination} = useRequest(()=>{
let new_params = Object.assign({}, params, {PageIndex: PageIndex, PageSize: PageSize}); // 拼接列表参数
return getEditorialCenterListJson(new_params)
}, []);


return (
<>
<Table
loading={loading}
columns={getColumns()}
rowKey={'ArticleID'}
dataSource={get(data,"Items") ?get(data,"Items") : []}
style={{backgroundColor:'#fff',borderRadius:'5px'}}
pagination={{
total:get(data,"Count") ? get(data,"Count") : 0,
showTotal:(total) => `共 ${total} 条记录 第${PageIndex}页`,
pageSize:PageSize,
current:PageIndex,
defaultCurrent:1,
showSizeChanger :true,
showQuickJumper:true,
}}
onChange={setPagination}
/>
</>

);
}
export default AuditPending;

/redux/hooks/useRequest.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import {useState, useEffect} from 'react';

const useRequest = (fn, dependence) =>{
const [data, setData] = useState({Items:[], Count: 0});
const [loading, setLoading] = useState(false);
const [PageIndex, setPageIndex] = useState(1);
const [PageSize, setPageSize] = useState(10);

// 依赖项 = 各个组件自定义的筛选条件参数 + PageIndex + PageSize
dependence = [...dependence, PageIndex, PageSize];

const request = useCallback(() =>{
setLoading(true); // 设置loading
fn()
.then(res=>{
setData(res.Data);
})
.finally(()=>{
setLoading(false);
})
}, dependence);

// 分页
const setPagination = (pagination) =>{
setPageIndex(pagination.current);
setPageSize(pagination.pageSize);
};

useEffect(() => {
request()

return () => { // 销毁
// request()
}
}, dependence);

return {
data,
loading,
PageIndex,
PageSize,
setPagination
}
}
export default useRequest;

2. 列表参数条件查询,添加了日期控件、搜索框

src/pages/EditorialCenter/Auditing/auditPending.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
function getColumns(){
const columns = [
... 这里是table的column
];
return columns;
}
const AuditPending = () =>{
const [timeV, setTime] = useState("");
const [Keyword, setKeyword] = useState("");
// 本页面组件的参数项(不唯一,所以不封装)
const [params, setParams] = useState({
Keyword: "",
SSubmitTime: "",
ESubmitTime: "",
FlowType: 0,
ReviewStatus: 99,
Sort: 0,
});

// 时间控件
const setRangePicker = useCallback((value) => {
setTime(value);
}, [timeV]);
// 关键词搜索
const searchInput = useCallback((e) => {
setKeyword(e.target.value);
}, [Keyword])
// 清空操作
const clearState = () => {
setTime("");
setKeyword("");
setParams(Object.assign({}, params, {Keyword: "", SSubmitTime: "", ESubmitTime: ""}));
};
// 筛选操作
const searchFun = () => {
setParams(Object.assign({}, params, {Keyword: Keyword, SSubmitTime: timeV ? moment(timeV[0]).format('YYYY-MM-DD') : "", ESubmitTime: timeV ? moment(timeV[1]).format('YYYY-MM-DD') : ""}));
};

// 自定义hook
const {data, loading, PageIndex, PageSize, setPagination} = useRequest(()=>{
let new_params = Object.assign({}, params, {PageIndex: PageIndex, PageSize: PageSize}); // 拼接列表参数
return getEditorialCenterListJson(new_params);
}, [params]);

return (
<>
<div className={'searchBox'}>
<span>提交时间:</span>
<RangePicker onChange={setRangePicker} value={timeV} className={'marR20'}/>
<span>关键词:</span>
<Input placeholder="请输入关键词" value={Keyword} onChange={searchInput} style={{width: 160}} className={'marR20'}/>
<Button onClick={searchFun}type="primary" className={'marR20'} >筛选</Button>
<Button onClick={clearState}>清空</Button>
</div>
<Table
loading={loading}
columns={getColumns()}
rowKey={'ArticleID'}
dataSource={get(data,"Items") ?get(data,"Items") : []}
style={{backgroundColor:'9#fff',borderRadius:'5px'}}
pagination={{
total:get(data,"Count") ? get(data,"Count") : 0,
showTotal:(total) => `共 ${total} 条记录 第${PageIndex}页`,
pageSize:PageSize,
current:PageIndex,
defaultCurrent:1,
showSizeChanger :true,
showQuickJumper:true,
}}
onChange={setPagination}
/>
</>
);
}
export default AuditPending;

image

3. 这里的state太多,使用 useReducer 改进

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import React, {useState, useEffect, useCallback, useReducer} from 'react';
import { useSelector } from 'react-redux';
import { Checkbox, DatePicker, Button, Input, Select } from 'antd';
import { PictureOutlined, VideoCameraOutlined } from '@ant-design/icons';
import moment from 'moment';
import EditorialCenterMenu from '../components/EditorialCenterMenu';
import TableFunction from '../components/TableFunction';
import {getEditorialCenterListJson, getArticleGetReviewStatusJson} from 'Redux/actionServer/content';
import useRequest from '../hooks/useRequest';
import {pageButton} from 'Util/commonFun';

const { RangePicker } = DatePicker;
const { Option } = Select;

let initialReviewStatus = {"Text": "全部", "Value": "99"};
let initialState = {
timeV: "",
Keyword: "",
ReviewStatusData: [],
ReviewStatus: initialReviewStatus
}

function reducer(state, action){
switch (action.type) {
case 'SetParams':
return {
...state,
[`${action.paramsN}`]: action.paramsV
};
default:
return state;
}
}

function getColumns(){
const columns = [
... 这里是table的column
];
return columns;
}

const AuditPending = () =>{
const [{timeV, Keyword, ReviewStatusData, ReviewStatus }, dispatch] = useReducer(reducer, initialState);

// 本页面组件的参数项(不唯一,所以不封装)
const [params, setParams] = useState({
Keyword: "",
SSubmitTime: "",
ESubmitTime: "",
FlowType: 0,
ReviewStatus: ReviewStatus.Value,
Sort: 0,
});
// 自定义hook
const {data, loading, PageIndex, PageSize, setPagination} = useRequest(()=>{
let new_params = Object.assign({}, params, {PageIndex: PageIndex, PageSize: PageSize}); // 拼接列表参数
return getEditorialCenterListJson(new_params);
}, [params]);
// 顶部tab切换数据
const mainMenu = useSelector(state => state.appReduce.mainMenu);

/* --------------- 筛选操作 ---------------------------------------------------------------------------*/
// 时间控件
const setRangePicker = useCallback((value) => {
dispatch({type: 'SetParams', paramsN: 'timeV', paramsV: value});
}, [timeV]);
// 关键词搜索
const searchInput = useCallback((e) => {
dispatch({type: 'SetParams', paramsN: 'Keyword', paramsV: e.target.value});
}, [Keyword])
// 状态
// 获取审核状态数据
useEffect(()=>{
getArticleGetReviewStatusJson()
.then(res=>{
dispatch({type: 'SetParams', paramsN: 'ReviewStatusData', paramsV: res.Data});
})
}, []);
// 状态change
const selectChange = useCallback((value) => {
let _temp = ReviewStatusData.find( item=>{
return item.Value === value
});
dispatch({type: 'SetParams', paramsN: 'ReviewStatus', paramsV: {"Text": _temp.Text, "Value": _temp.Value}});
}, [ReviewStatusData]);
// 清空操作
const clearState = useCallback(() => {
dispatch({type: 'SetParams', paramsN: 'timeV', paramsV: ''});
dispatch({type: 'SetParams', paramsN: 'Keyword', paramsV: ''});
setParams(Object.assign({}, params, {Keyword: "", SSubmitTime: "", ESubmitTime: "", ReviewStatus: initialReviewStatus.Value}));
dispatch({type: 'SetParams', paramsN: 'ReviewStatus', paramsV: initialReviewStatus});
}, []);
// 筛选操作
const searchFun = () => {
if(PageIndex !== 1) setPagination({current: 1, pageSize: PageSize}); // 添加搜索条件,PageIndex 初始化
setParams(Object.assign({}, params, {Keyword: Keyword, SSubmitTime: timeV ? moment(timeV[0]).format('YYYY-MM-DD') : "", ESubmitTime: timeV ? moment(timeV[1]).format('YYYY-MM-DD') : "", ReviewStatus: ReviewStatus.Value,}));
};

return (
<>
<EditorialCenterMenu mainMenu={mainMenu} />
{/* 筛选条件 */}
<div className={'searchBox'}>
<span>提交时间:</span>
<RangePicker onChange={setRangePicker} value={timeV} className={'marR20'}/>
<span >审核状态:</span>
<Select value={ReviewStatus.Text} onChange={selectChange} style={{ width: 120 }} className={'marR20'}>
{
ReviewStatusData &&
ReviewStatusData.map( (item, index)=>{
return (
<Option value={item.Value} key={index}>{item.Text}</Option>
)
})
}
</Select>
<span>关键词:</span>
<Input placeholder="请输入关键词" value={Keyword} onChange={searchInput} style={{width: 160}} className={'marR20'}/>
<Button onClick={searchFun}type="primary" className={'marR20'} >筛选</Button>
<Button onClick={clearState}>清空</Button>
</div>
{/* 列表通用 table 组件 */}
<TableFunction loading={loading} getColumns={getColumns()} data={data} PageIndex={PageIndex} PageSize={PageSize} setPagination={setPagination}/>
</>
);
}

export default AuditPending;

将列表页面中的table单独封装如下:

/src/pages/EditorialCenter/components/TableFunction.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import React from 'react';
import PropTypes from 'prop-types';
import get from 'lodash/get';
import { Table } from 'antd';

const TableFunction = ({loading, getColumns, data, PageIndex, PageSize, setPagination}) =>{
return (
<Table
loading={loading}
columns={getColumns}
rowKey={'ArticleID'}
dataSource={get(data,"Items") ?get(data,"Items") : []}
style={{backgroundColor:'#fff',borderRadius:'5px'}}
pagination={{
total:get(data,"Count") ? get(data,"Count") : 0,
showTotal:(total) => `共 ${total} 条记录 第${PageIndex}页`,
pageSize:PageSize,
current:PageIndex,
defaultCurrent:1,
showSizeChanger :true,
showQuickJumper:true,
}}
onChange={setPagination}
/>
)
}

TableFunction.propTypes = {
loading: PropTypes.bool,
getColumns: PropTypes.array,
data: PropTypes.object,
PageIndex: PropTypes.number,
PageSize: PropTypes.number,
setPagination: PropTypes.func
}
export default TableFunction;

6.1.2 封装全局按钮loading

暂时,发模块:列表、条件筛选,获取数据都走redux流程

6.2 接入Sentry

这里的内容直接登录的Sentry服务,创建Project即可看到:

  1. Add the Sentry SDK as a dependency using yarn or npm:
    yarn add @sentry/react | npm install @sentry/react

  2. Connecting the SDK to Sentry

    1
    2
    3
    4
    5
    6
    7
    8
    import React from 'react';
    import ReactDOM from 'react-dom';
    import * as Sentry from '@sentry/react';
    import App from './App';

    Sentry.init({dsn: "http://XXXXXXXX"});

    ReactDOM.render(<App />, document.getElementById('root'));

接入完成,当项目中发送错误时,会发送信息到Sentry服务,具体请见 [跳转]。

7. Webpack 优化

Webpack 优化 详解

见webpack.prod.js文件,查看 new BundleAnalyzerPlugin()打包分析结果

7.1 按需加载antd

npm install --save-dev babel-import-plugin

打包结果分析:失败,见 3.5

7.2 优化构建速度

7.2.1 Dll

https://jiafei2333.github.io/2019/11/14/Webpack-majorization/ 见 5.DllPlugin && DllReferencePlugin

webpack.dll.js

1
General output time took 38.24 secs

构建时能够看到delegated证明是用引入的dll/下提前生成的文件,但是看了耗时,时间并没有缩短,反而还增加了……待解决。

7.2.2 include/exclude

[使用include]

1
2
3
4
5
{
test: /\.js$/,
use: 'babel-loader',
include: path.resolve(__dirname, "../src")
},

没有配置include时,打包耗时如下:

配置了include后,打包耗时如下:

可以看出构建时间大大缩短了,特别是babel-loader的时间,打包时的babel-loader耗时同样由20s+缩短为3s+,大大加快了打包时间。

[使用exclude]

1
2
3
4
5
{
test: /\.js$/,
use: 'babel-loader',
exclude: path.resolve(__dirname, "../node_modules/")
},

配置了exclude后,构建耗时如下:

7.3 SplitChunks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
optimization:{
splitChunks: {
// initial 只操作同步的,all 所有的,async异步的(默认)
chunks: 'async', // 默认支持异步的代码分割import()
minSize: 30000, // 文件超过30k 就会抽离
maxSize: 0, // 没有最大上限
minChunks: 1, // 最少模块引用一次才抽离
maxAsyncRequests: 5, // 最大异步请求数,最多5个
maxInitialRequests: 3, // 最大初始化请求数,即最多首屏加载3个请求
automaticNameDelimiter: '~', // 抽离的命名分隔符 xxx~a~b (如果是a、b文件引用)
automaticNameMaxLength: 30, // 名字最大长度
name: true,
cacheGroups: { // 缓存组 这里面也可以配置上面的配置
vendors: { // 先抽离第三方
test: /[\\/]node_modules[\\/](jquery)|(lodash)/,
priority: -1
},
react:{
test: /[\\/]node_modules[\\/](react|react-dom)/,
priority: -2,
},
default: {
minChunks: 2,
priority: -20, // 优先级, -2比 -20大
reuseExistingChunk: true
}
}
}
}

打包之后发现效果不明显,可能是这个项目代码太少了….待解释。

8. 配置文件

[webpack.base.js]

[webpack.dev.js]

[webpack.prod.js]

[webpack.dll.js]

[.babelrc]

8. 相关文章

  1. https://juejin.im/post/5b4de4496fb9a04fc226a7af
  2. https://medium.com/stashaway-engineering/react-redux-tips-better-way-to-handle-loading-flags-in-your-reducers-afda42a804c6
  3. https://juejin.im/post/5b440f7ae51d45195759f345
  4. https://juejin.im/post/5d6771375188257573636cf9
  5. [Webpack 优化]

9. 未完成功能

  • 1.loading
    • 列表页loading(完成,封装的自定义hook)
    • 组件动态加载loading(完成)
    • 按钮loading
  • 2.nprogress
  • 3.saga代替thunk(完成)
  • 4.配置node后台服务(完成)
  • 5.antd中局部修改默认样式(完成 不需要global包裹)
  • 6.react-hot-loader(完成)
  • 7.强制刷新时获取基本信息(菜单权限、配置信息等等)(完成)
  • 8.路由监听,拼接路由(完成)
  • 9.自定义hooks封装(完成)
  • 10.动态加载组件(react-loadable)(完成)
  • 11.打包配置-打包分析-打包优化-查看bundle大小 (完成)
  • 12.主题色配置
  • 13.打包问题、引入样式问题(完成,开启css module)
  • 14.Source Map配置+生产环境下配合Sentry(完成)
  • 15.生产环境打包时拷贝web.config文件(完成)
  • 16.登录判断 PrivateRoute
  • 17.图片打包优化,小图片生成base64等(完成,将图片放到样式中用url-loader)
  • 18.生产环境下去掉state状态输出

github项目入口: https://github.com/jiafei2333/React-Redux-Antd

tips:
react-template添加配置

  1. webpack.prod.js
  2. webpack.base.js
  3. Loading组件和loading图
  4. babel 和 package.json
  5. webpack.dll.js

roadhog-umi-dva

发表于 2020-04-28 | 更新于: 2020-05-08 | 分类于 框架

1. roadhog

  • roadhog 是基于 webpack 的封装工具,目的是简化 webpack 的配置
  • 提供 server、 build 和 test 三个命令,分别用于本地调试和构建
  • 提供了特别易用的 mock 功能
  • 命令行体验和 create-react-app 一致,配置略有不同,比如默认开启 css modules
  • 还提供了 JSON 格式的配置方式。

根目录新建.roadhogrc.mock.js文件 mock本地数据

1.1 内部是如何实现的

1、本地全局安装npm install roadhog -g
2、npm root -g 查看全局安装的路径,找到roadhog/package.json
可以看到

1
2
3
"bin":{
"roadhog": "./bin/roadhog.js"
}

3、查看这个roadhog.js文件,部分源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/env node

const spawn = require('cross-spawn');
const chalk = require('chalk'); // 可以在命令行显示颜色
const script = process.argv[2];
//node bin/roadhog.js -v
switch (script) {
case "-v":
case "--version":
console.log(require("../package.json").version);
break;
case "build":
case "server":
result = spawn.sync(
"node",
[require.resolve(`../lib/${script}`)],
{ stdio: "inherit" }
);
process.exit(result.status);
break;
default:
console.log(`Unknown script ${chalk.cyan(script)}.`);
break;
}

获取到命令行输入的参数 process.argv 根据不同的参数执行相应的文件
例如这里执行的是build,在node环境中,读取的是../lib/build.js文件
lib/build.js 缩减版如下:

1
2
3
4
5
6
7
8
9
10
11
const webpack = require("webpack");
const chalk = require("chalk");
function doneHandler(err, stats) {
console.log(stats.toJson().assets);
}
function build() {
let config = require("lib/config/webpack.config.prod");
var compiler = webpack(config);
compiler.run(doneHandler);
}
build();

会去读取 webpack.config.prod 配置文件,再交给webpack编译打包
4、lib\config\webpack.config.prod.js 简单配置如下:

1
2
3
4
5
6
7
8
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
path: path.resolve('dist'),
filename: "[name].js"
}
};

5、配置自定义的打包配置
根目录新建.roadhogrc 或 .roadhogrc.js

2. UmiJs

  • UmiJS 是一个类 Next.JS 的 react 开发框架。
  • 他基于一个约定,即 pages 目录下的文件即路由,而文件则导出 react 组件
  • 然后打通从源码到产物的每个阶段,并配以完善的插件体系,让我们能把 umi 的产物部署到各种场景里。

约定 src/layouts/index.js 为全局路由,返回一个 React 组件,所以编译顺序是先去编译/layouts/下的index.js,读取到React组件,然后去编译pages下的页面内容。

核心工作:
1、动态检测项目,生成配置文件(pages/.umi/history.js|router.js)

会监控pages文件夹,有新的文件生成就改变.umi/文件夹下的配置

1
2
3
4
let fs = require('fs');
fs.watchFile('**',function(){
...监控文件变化,执行重新生成配置文件的操作
})

3. dva

  • 基于 redux、redux-saga 和 react-router 的轻量级前端框架。
  • dva是基于react+redux最佳实践上实现的封装方案,简化了redux和redux-saga使用上的诸多繁琐操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import React from 'react';
import dva,{connect} from 'dva';
import keymaster from 'keymaster';
import { Router, Route } from 'dva/router';
//dva react react-dom redux redux-saga react-router react-router-dom history
const app = dva();
//redux combineReducers reducer都有自己的状态
//combineReducers({counter:counterReducer})
//总的状态树 state={counter:0,counter2:0}
const delay = (millseconds)=>{
return new Promise(function(resolve,reject){
setTimeout(function(){
resolve();
},millseconds);
});
}
app.model({
namespace:'counter',
state:{number:0},
reducers:{//接收老状态,返回新状态
add(state){ //dispatch({type:'add'});
return {number:state.number+1};
},
minus(state){//dispatch({type:'minus'})
return {number:state.number-1};
}
},
// 延时操作 调用接口 等待
effects:{
*asyncAdd(action,{put,call}){ //redux-saga/effects {put,call}
yield call(delay,1000);//把100传给delay并调用,yield会等待promise完成
yield put({type:'add'});
}
},
subscriptions:{
keyboard({dispatch}){ // 监听键盘输入,是空格键 就执行add方法
keymaster('space',()=>{
dispatch({type:'add'});
});
},
changeTitle({history}){ // 当路径发生变化的时候执行回调函数
setTimeout(function(){
history.listen(({pathname})=>{
document.title = pathname;
});
},1000);

}
}
});
app.model({
namespace:'counter2',
state:{number:0},
reducers:{//接收老状态,返回新状态
add(state){ //dispatch({type:'add'});
return {number:state.number+1};
},
minus(state){//dispatch({type:'minus'})
return {number:state.number-1};
}
}
});
const Counter = (props)=>{
return (
<div>
<p>{props.number}</p>
<button onClick={()=>props.dispatch({type:'counter/add'})}>add</button>
<button onClick={()=>props.dispatch({type:'counter/asyncAdd'})}>asyncAdd</button>
<button onClick={()=>props.dispatch({type:'counter/minus'})}>-</button>
</div>
)
}
const Counter2 = (props)=>{
return (
<div>
<p>{props.number}</p>
<button onClick={()=>props.dispatch({type:'counter2/add'})}>+</button>
<button onClick={()=>props.dispatch({type:'counter2/minus'})}>-</button>
</div>
)
}
//{counter1:{number:0},counter2:{number:0}}
const ConnectedCounter = connect(
state=>state.counter
)(Counter);
const ConnectedCounter2 = connect(
state=>state.counter2
)(Counter2);
app.router(
({app,history})=>(
<Router history={history}>
<>
<Route path="/counter1" component={ConnectedCounter}/>
<Route path="/counter2" component={ConnectedCounter2}/>
</>
</Router>
)
);
app.start('#root');

[官网]https://dvajs.com/guide/

注意: 这里最后的手写dva可以看一下,都是基于redux react-redux react-router-dom redux-saga connected-react-router history的封装:http://www.zhufengpeixun.cn/2020/html/31.dva.html#t43.1%20src\index.js

1
import { routerMiddleware, connectRouter, ConnectedRouter } from "connected-react-router";

小结

  • roadhog 是基于 webpack 的封装工具,目的是简化 webpack 的配置
  • umi 可以简单地理解为 roadhog + 路由,思路类似 next.js/nuxt.js,辅以一套插件机制,目的是通过框架的方式简化 React 开发
  • dva 目前是纯粹的数据流,和 umi 以及 roadhog 之间并没有相互的依赖关系,可以分开使用也可以一起使用,个人觉得 umi + dva 是比较搭的

参考文章

http://www.zhufengpeixun.cn/2020/html/30.cms-8-roadhog.html
http://www.zhufengpeixun.cn/2020/html/30.cms-10-umi.html
https://v2.umijs.org/zh/guide/
http://www.zhufengpeixun.cn/2020/html/30.cms-12-dva.html

react-router-dom

发表于 2020-04-25 | 更新于: 2020-05-25 | 分类于 React
  • 不同的路径渲染不同的组件
  • 有两种实现方式
    • HashRouter:利用hash实现路由切换
    • BrowserRouter: 利用h5 Api实现路由的切换

h5中的路由

hash

默认地址栏后面会加hash,如 http://www.a.com/index.html#

hash 模式 前端路由的特点,不刷新,但是可以根据路径显示不同的内容
hash 不是浏览器的规范,优点:兼容性比较好,缺点是不美观 (#/…)

1
2
3
<a href="#/home">首页</a>
<a href="#/abort">关于</a>
<div id="container"></div>

点击a标签,可以看到路由变化了,但是页面没有改变

1
2
3
4
5
1)初始化路径
一般启动服务的时候初始进来是index.html,需要跳转到/页面或者本身的页面
```javascript
window.location.hash = window.location.hash.slice(1) || '/';
// window.location.hash.slice(1) 如果进来是#/user就把值加过去 或者 '/' ,‘#’ hash是不用写的,默认就有

2)监听
根据路径渲染div里面的内容

1
2
3
4
container.innerHTML = window.location.hash;
window.addEventListener('hashchange',function{
container.innerHTML = window.location.hash;
})

history

window.history
window.history.pushState({},null,’/‘)

默认我们的h5 history api 它不支持强制刷新,会找不到对应的路径(比如当前是/页面,通过window.history.pushState({},null,’/user’),路由改变为/user,这时候刷新,页面会显示 Cannot GET /user)
如果页面找不到对应的路径,可以跳转到首页,在通过window.history.pushState跳到这个/user页面上

1
2
3
4
5
6
7
8
9
<a onclick="push('/')">首页</a>
<a onclick="push('/abort')">关于</a>
<div id="container"></div>
<script>
function push(path){
window.history.pushState({},null,path);
container.innerHTML = path;
}
</script>

点击a标签可以看到页面和路径都变化,且浏览器最上角的前进后退按钮历史数据一直在叠加,可以通过window.history.length看到

1)默认前进、后退是不会调用pushState方法的

1
2
3
4
5
window.addEventListener('popstate',function(){ // 只有在点击[前进][后退]按钮时才触发
console.log(window.location); // 路径相关的信息
console.log(window.history); // 历史相关的方法
container.innerHTML = window.location.pathname;
})

React中的路由

HashRouter | BrowserRouter

npm install –save react-router-dom

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React from 'react';
import ReactDOM from 'react-dom';
import Home from './pages/Home';
import User from './pages/User';
import Profile from './pages/Profile';
// react路由需要引入 内部提供了很多组件
// 提供一个路由容器,里面放着一条条的路由

// 如果用BrowserRouter webpack 插件 history-fallback-api 如果路径找不到会跳转到首页
// 这里用HashRouter
import {HashRouter as Router, Route} from 'react-router-dom';

ReactDOM.render(
// 和react-redux一样 主要提供一些属性集合方法,Router提供属性和方法
<Router>
<Route path="/" exact component={Home} />
<Route path="/user" component={User} />
<Route path="/profile" component={Profile} />
</Router>,
document.getElementById('root')
);
// exact 精确匹配

实现

Router提供属性和方法,父组件提供,子组件(Route)拿
在Home组件中打印

1
2
render() {
console.log(this.props);

打印如下:

这里的 history、location 不是原生windows上的,是自己封装的 math指匹配到的信息:路径、路径参数、是否是精确匹配

步骤

1) HashRouter 要提供一个对象 history location match, 传递给后代组件 ,更新视图(setState)
2) Route 路由 主要的工作是拿到父组件中路径 和自己身上的path比较是否相等,如果相等渲染自己的component即可

1.

index.js

1
import {HashRouter as Router, Route} from './react-router-dom';

新建 ./src/react-router-dom/index.js

1
2
3
4
5
6
7
import HashRouter from './HashRouter';
import Route from './Route';

export {
HashRouter,
Route
}

新建 ./src/react-router-dom/HashRouter.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { Component } from 'react';
import Context from './context';

// 提供一个对象
class HashRouter extends Component {
render() {
let value = {

}
return (
<Context.Provider value={value}>
{this.props.children}
</Context.Provider>
);
}
}
export default HashRouter;

父组件提供值

新建 ./src/react-router-dom/Route.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React, { Component } from 'react';
import context from './context';

class Route extends Component {
static contextType = context;
render() {
return (
<div>
Route
</div>
);
}
}
export default Route;

子组件拿到值去匹配

新建 ./src/react-router-dom/context.js

1
2
3
4
// 提供一个上下文环境
import React from 'react';

export default React.createContext();

2.

HashRouter.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import React, { Component } from 'react';
import Context from './context';

// 提供一个对象
class HashRouter extends Component {
state = {
location: {
// 如果地址是#/user就直接赋过去,如果是起始localhost:3000,就加一个'#/',‘#’因为是hash会自动添加
pathname: window.location.hash.slice(1) || '/',
state: null
}
}
componentDidMount() {
// 组件加载完毕 默认跳转到首页,如果有就采用默认的
window.location.hash = window.location.hash.slice(1) || '/';
window.addEventListener('hashchange',()=>{
this.setState({
location:{
...this.state.location,
pathname: window.location.hash.slice(1)
}
})
})
}
render() {
let value = {
location: this.state.location
}
return (
<Context.Provider value={value}>
{this.props.children}
</Context.Provider>
);
}
}
export default HashRouter;

Route.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, { Component } from 'react';
import context from './context';

class Route extends Component {
static contextType = context;
render() {
console.log(this.context); // 获取到的是父组件(Router)提供的value的值
console.log(this.context.location.path);
return (
<div>
Route
</div>
);
}
}
export default Route;

浏览器地址改为 http://localhost:3000/#/user console.log输出: 3个location的对象和/user

3.

在Route中匹配组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React, { Component } from 'react';
import context from './context';

class Route extends Component {
static contextType = context;
render() {
// 这个path属性是当前真正的请求路径
let pathname = this.context.location.pathname;
// 获取我们在<Route></Route> 定义的路径和组件
// exact 表示是否严格匹配
let { path, component: Component, exact=false } = this.props;
let props = { // 将context中的属性传递下去,之前在Home组件中 this.props能打印出来
...this.context // context中有location
}
// 如果路径渲染成功,就渲染对应的组件
if(path === pathname){
return <Component {...props} />
}
// 否则就不进行渲染操作
return null;
}
}
export default Route;

将地址栏地址改为http://localhost:3000/#/user | http://localhost:3000/#/ | http://localhost:3000/#/profile 都能匹配到相应的组件

4. Link

4.1 原生

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React from 'react';
import ReactDOM from 'react-dom';
import Home from './pages/Home';
import User from './pages/User';
import Profile from './pages/Profile';
// Link
import {HashRouter as Router, Route, Link} from 'react-router-dom';

ReactDOM.render(
<Router>
<>
<nav>
<Link to={{pathname:'/', state:{title:'哈哈'}}}>首页</Link>
<Link to="/user">用户</Link>
<Link to="/profile">个人中心</Link>
</nav>
<Route path="/" exact component={Home} />
<Route path="/user" component={User} />
<Route path="/profile" component={Profile} />
</>
</Router>,
document.getElementById('root')
);

这里Link在页面上hash路由解析成如下:

1
2
3
4
5
<nav>
<a href="#/">首页</a>
<a href="#/user">用户</a>
<a href="#/profile">个人中心</a>
</nav>

4.2 手写

新建./src/react-router-dom/Link.js

1
2
3
4
5
6
7
8
9
10
11
12
import React from 'react';
import context from './context';
export default class Link extends React.Component{
static contextType = context;
render(){
let {to} = this.props;
// 这里有可能是hash也有可能是browser的,所以都采用方法点击的方式传递事件(hash的话直接href=”#/“跳转)
return <a onClick={()=>{
this.context.history.push(to) // 调用父组件的history的push方法,把写在Link上的to属性传过去
}}>{this.props.children}</a>
}
}

HashRouter.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import React, { Component } from 'react';
import Context from './context';

class HashRouter extends Component {
state = {
location: {
pathname: window.location.hash.slice(1) || '/',
state: null // 初始为null
}
}
componentDidMount() {
window.location.hash = window.location.hash.slice(1) || '/';
window.addEventListener('hashchange',()=>{
this.setState({
location:{
...this.state.location,
pathname: window.location.hash.slice(1),
state: this.locationState, // 只有跳转的时候才会携带状态,如果用户刷新页面就会丢失,比如有一个列表,点击跳转详情页
}
})
})
}
locationState = null;
render() {
let value = {
location: this.state.location,
history: {
push: (to)=>{
if(typeof to === 'object'){
let {pathname, state} = to;
this.locationState = state; // 跳转时先存起来当前的状态
window.location.hash = pathname;
}else{
// 跳转路径,其实就是改变hash值
window.location.hash = to; // 改变hash上面line16就会监听到从而改变state
}
}
}
}
return (
<Context.Provider value={value}>
{this.props.children}
</Context.Provider>
);
}
}
export default HashRouter;

5. exact

5.1 path-to-regexp

将路径转化为正则

这个包react默认引用,不用安装

1
2
3
4
5
6
7
// 将路径转化成正则
const pathToRegExp = require("path-to-regexp");

let url = '/user';

let reg = pathToRegExp(url,[],{end: false});
console.log(reg); // /^\/user\/?(?=\/|$)/i 打印匹配出的路径

5.2

Route.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React, { Component } from 'react';
import context from './context';
import pathToRegExp from 'path-to-regexp';

class Route extends Component {
static contextType = context;
render() {
let pathname = this.context.location.pathname;

let { path, component: Component, exact=false } = this.props;
let reg = pathToRegExp(path,[],{end: exact}); // 获取每个Route中path上的路径正则
let props = {
...this.context
}
// 正则匹配路径
if(reg.test(pathname)){
return <Component {...props} />
}
return null;
}
}
export default Route;

6. Switch

这里使用原生的,当如下:

1
2
3
4
<Route path="/" exact={false} component={Home} /> 
<Route path="/user" component={User} />
<Route path="/user" component={User} />
<Route path="/profile" component={Profile} />

这里匹配到一个/user之后会继续向下匹配,当有多个匹配成功则页面上显示多个组件,使用 Switch之后

1
2
3
4
5
6
7
8
9
import {HashRouter as Router, Route, Link, Switch} from 'react-router-dom';
<Router>
<Switch>
<Route path="/" exact={true} component={Home} />
<Route path="/user" component={User} />
<Route path="/user" component={User} />
<Route path="/profile" component={Profile} />
</Switch>
</Router>

只匹配一次,匹配到了就不走下面了

6.1 手写

新建 ./src/react-router-dom/Switch.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 循环所有的孩子,如果一个路径匹配到就停止循环
import React, { PureComponent } from 'react';
import context from './context';
import pathToRegExp from 'path-to-regexp';
class Switch extends PureComponent {
static contextType = context;
render() {
let child = Array.isArray(this.props.children) ? this.props.children : [this.props.children];
for(let i = 0; i < child.length; i++){
let child = children[i]; // 这里已经是解析后的虚拟dom了,不是Route那个组件
let {path='/', exact=false} = child.props; // 虚拟DOM的机构里,属性就是在props里
let {pathname} = this.context.location;
let reg = pathToRegExp(path,[],{end: exact});
if(reg.test(pathname)){
return child;
}
}
return null;
}
}
export default Switch;

7. Redirect

当路径匹配不到就重定向到首页

1
2
3
4
5
6
7
8
import {HashRouter as Router, Route, Link, Switch, Redirect} from 'react-router-dom';
<Switch>
<Route path="/" exact={true} component={Home} />
<Route path="/user" component={User} />
<Route path="/user" component={User} />
<Route path="/profile" component={Profile} />
<Redirect to="/"></Redirect>
</Switch>

注意,这里Redirect必须要和Switch一起使用,不然导致死循环,因为/ 会一直向下匹配

7.1 手写

新建./src/react-router-dom/Redirect.js

1
2
3
4
5
6
7
8
9
10
11
import React, { Component } from 'react';
import context from './context';

class Redirect extends Component {
static contextType = context;
render() {
this.context.history.push(this.props.to); // 就是一个重定向的跳转
return null;
}
}
export default Redirect;

8. render

Route进行渲染的时候,有三种方式:
1、 第一种是Component
2、 第二种是render,会渲染render这个函数返回的内容
3、第三种是children

权限判断,如果没有登录就去登录页面
index.js

1
2
3
4
5
6
7
8
<Switch>
<Route path="/" exact={true} component={Home} />
<Route path="/user" component={User} />
<Route path="/user" component={User} />
{/* <Route path="/profile" component={Profile} /> */}
<Protected path="/profile" component={Profile} />
<Redirect to="/" />
</Switch>

新建 ./src/pages/Protected.js

1
2
3
4
5
6
7
8
9
10
11
12
13
import React from 'react';
import { Route, Redirect } from '../react-router-dom';
// 在渲染这个Protected组件的时候要做判断,判断当前用户是否登录,如果登录了,直接渲染Profile组件
// 如果没有登录,则直接跳转到登录页,等登录后再跳转回来

export default ({component:Component, ...rest})=>(
<Route {...rest} render={
props=>( // props={location, history, match} 当前页的location.pathname:'/profile' 登录成功之后还要跳回来
localStorage.getItem('isLogin') ? <Component {...props} /> : <Redirect to={{pathname:'/login',state:{from: props.location.pathname}}} />
)
}
/>
)

Route.js

1
2
3
4
5
6
7
8
9
10
11
12
let props = {
location: this.context.location,
history: this.context.history,
match
}
if(Component){
return <Component {...props} />;
}else if(render){
return render(props);
}else{
return null;
}

新建 ./src/pages/Login.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React, { Component } from 'react'

export default class Login extends Component {
handleClick = ()=>{
localStorage.setItem('logined','true');
if(this.props.location.state)
this.props.history.push(this.props.location.state.from);
}
render() {
return (
<button className="btn btn-primary" onClick={this.handleClick}>登录</button>
)
}
}

9. withRouter

把一个组件当成Route渲染出来,具有Route上的属性(history,location…等)

新建./src/react-router-dom/withRouter.js

1
2
3
4
5
6
import {Route} from '../react-router-dom';
export default function (Component) {
return props=>(
<Route component={Component}>}/>
)
}

新建一个NavHeader.js组件

1
2
3
4
5
6
7
8
9
10
11
12
13
import React, { Component } from 'react'
import {withRouter} from '../react-router-dom';
class NavHeader extends Component {
render() {
return (
<div className="navbar-heading">
<div className="navbar-brand" onClick={()=>this.props.history.push('/')}>首页</div>
</div>
)
}
}
export default withRouter(NavHeader);
// 用了withRouter,所以this.props.history.push有这个方法,这个history是Route上的属性

10. BrowserRouter

BrowserRouter 用的history对象来实现, history.pushState()

新建./src/react-router-dom/BrowserRouter.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import React, { Component } from 'react'
import Context from './context';
(function (history) {
var pushState = history.pushState;
history.pushState = function (state,title,pathname) {
if (typeof window.onpushstate == "function") {
window.onpushstate(state,pathname);
}
return pushState.apply(history, arguments);
};
})(window.history);
export default class BrowserRouter extends Component {
state = {
location: { pathname: '/' }
}
getMessage = null
componentDidMount() {
// 这个事件是原生支持的
window.onpopstate = (event) => {
this.setState({
location: {
...this.state.location,
pathname:document.location.pathname,
state:event.state
}
});
};
// 自己实现调用history.pushState(state,title,path)
window.onpushstate = (state,pathname) => {
this.setState({
location: {
...this.state.location,
pathname,
state
}
});
};
}
render() {
let that = this;
let value = {
location: that.state.location,
history: {
push(to) {
if (that.block) {
let allow = window.confirm(that.getMessage(that.state.location));
if (!allow) return;
}
if (typeof to === 'object') {
let { pathname, state } = to;
window.history.pushState(state, '', pathname);
} else {
window.history.pushState('', '', to);
}
},
block(getMessage) {
that.block = getMessage;
},
unblock() {
that.block = null;
}
}
}
return (
<Context.Provider value={value}>
{this.props.children}
</Context.Provider>
)
}
}

相关文档

http://www.zhufengpeixun.cn/2020/html/64.1.router.html

React源码理解

发表于 2020-04-13 | 更新于: 2020-06-10 | 分类于 React

不做具体代码分析,只是作为源码思想的解读

虚拟DOM的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React from 'react';
import ReactDOM from 'react-dom';
class App extends React.Component {
render() {
return (
<div>
<p>1</p>
<button>+</button>
</div>
);
}
}
let element = <App />
console.log(element);

ReactDOM.render(element, document.getElementById('root'));

打印出来的这个element对象就是虚拟DOM


属性解析:
$$typeof:
Symbol(react.element) 表示这是一个react元素;
标识元素类型的,有:div,Class,Function….
如果说$$typeof是Symbol的话可以仿XSS攻击,因为Symbol是唯一值,如果后台直接返虚拟DOM直接显示在页面上,带Symbol就可以防止后台返回的数据有恶意的虚拟DOM类型,可以避免这个问题,Symbol只有前端有,后台没有。
key: 就是唯一标识
props: 属性,这里没有所以为空
ref:
type: 类型就是这里的这个App Class 如果是函数组件就是函数App
_owner: 它的所有者是谁,谁创建了它 (带下划线的都是内部属性,不是核心属性)
_source: 是源代码哪个文件里的第几行

这就是React元素,它就是一个普通的对象,描述了真实DOM的样子

将上面line3 ~ line13 的代码放到babel中编译,如下:

1
2
3
4
5
6
7
8
9
10
11
12
class App extends React.Component {
render() {
return
React.createElement("div",
null,
React.createElement("p", null, "1"),
React.createElement("button", null, "+")
);
}

}
let element = React.createElement(App, null);

新建./react/ReactElement.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import ReactCurrentOwner from './ReactCurrentOwner';
import { REACT_ELEMENT_TYPE } from '../shared/ReactSymbols';
function hasValidRef(config) {
return config.ref !== undefined;
}
function hasValidKey(config) {
return config.key !== undefined;
}
const RESERVED_PROPS = {
key: true,
ref: true,
__self: true,
__source: true
}
//是react-babel 将<span>A<span><span>A<span>变成数组了吗?
//createElement(type,config,spanA,spanB);
export function createElement(type, config, children) {
let propName;//定义一个变量叫属性名
const props = {};//定义一个元素的props对象
let key = null;//在兄弟节点中唯一标识自己的唯一性的,在同一个的不同兄弟之间key要求不同
let ref = null;//ref=React.createRef() "username" this.refs.username {input=>this.username = input} 从而得到真实的DOM元素
let self = null;//用来获取真实的this指针
let source = null;//用来定位创建此虚拟DOM元素在源码的位置 哪个文件 哪一行 哪一列
if (config !== null) {
if (hasValidRef(config)) {
ref = config.ref;
}
if (hasValidKey(config)) {
key = config.key;
}
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
for (propName in config) {
if (!RESERVED_PROPS.hasOwnProperty(propName)) {
props[propName] = config[propName]
}
}
}
const childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;//如果说是独生子的话children是一个对象
} else if (childrenLength > 1) {
const childArray = Array(childrenLength);
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
props.children = childArray;//如果说是有多个儿子的话,props.children就是一个数组了
}
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
//只有当属性对象没有此属性对应的值的时候,默认属性才会生效,否则直接忽略
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName]
}
}
}
//ReactCurrentOwner此元素的拥有者
return ReactElement(
type, key, ref, self, source, ReactCurrentOwner.current, props
)
}
function ReactElement(type, key, ref, _self, _source, _owner, props) {
const element = {
$$typeof: REACT_ELEMENT_TYPE,
type,
key,
ref,
props,
_owner,
_self,
_source
}
return element; // 这个最后生成的就是react元素即那个虚拟DOM
}

将上述babel编辑的代码转换成表示虚拟DOM的那个对象

1
Component.prototype.isReactComponent = {};

在React内部是凭借这个变量来判断是不是一个React组件的
因为组件定义有两种方式,一是类组件,一是函数组件,都被babel编译成函数

虚拟DOM的渲染

1
2
3
4
5
import ReactDOM from 'react-dom';
ReactDOM.render(
element,
document.getElementById('root')
)

新建./react-dom/index.js

1
2
3
4
5
6
7
8
9
10
11
12
import { createDOM } from '../react/vdom';
// element 就是那个虚拟DOM的对象,
// container 挂载的容器
function render(element, container) {
//1.要把虚拟 DOM变成真实DOM
let dom = createDOM(element);
//2.把直实DOM挂载到container上
container.appendChild(dom);
}
export default {
render
}

新建./react/vdom.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import { TEXT, ELEMENT, CLASS_COMPONENT, FUNCTION_COMPONENT } from './constants';
import { onlyOne, setProps, flatten } from './utils';

export function createDOM(element) {
element = onlyOne(element);//为什么要这么写? children是一个数组
let { $$typeof } = element;
let dom = null;
if (!$$typeof) {// element 是一个字符串或者数字
dom = document.createTextNode(element);
} else if ($$typeof == TEXT) {//对象{$$typeof:TEXT}
dom = document.createTextNode(element.content);
} else if ($$typeof == ELEMENT) {
//如果此虚拟DOM是一个原生DOM节点
dom = createNativeDOM(element);
} else if ($$typeof == FUNCTION_COMPONENT) {
//如果此虚拟DOM是一个函数组件,就渲染此函数组件
dom = createFunctionComponentDOM(element);
} else if ($$typeof == CLASS_COMPONENT) {
//如果此虚拟DOM是一个类组件,就渲染此类组件
dom = createClassComponentDOM(element);
}
element.dom = dom;//不管是什么类型的元素,都让它的dom属性指向他创建出来的直实DOM元素
return dom;
}
//创建函数组件对应的真实的DOM对象
function createFunctionComponentDOM(element) {
let { type: FunctionCounter, props } = element;//type = FunctionCounter
let renderElement = FunctionCounter(props);//返回要渲染的react元素
element.renderElement = renderElement;//需要缓存,方便下次对比
let newDOM = createDOM(renderElement);
//虚拟DOM的dom属性指向它创建出来的真实DOM
renderElement.dom = newDOM;//我们从虚拟DOMReact元素创建出真实DOM,创建出来以后会把真实DOM添加到虚拟DOM的dom属性上
return newDOM;
//element.renderElement.dom=DIV真实DOM元素
}
function createClassComponentDOM(element) {
let { type: ClassCounter, props } = element;
let componentInstance = new ClassCounter(props);//创建一个ClassCounter组件的实例
//当创建类组件实例 后,会在类组件的虚拟DOM对象上添一个属性componentInstance,指向类组件实例
element.componentInstance = componentInstance;//以后组件运行当中componentInstance是不变的
let renderElement = componentInstance.render();
//在类组件实例上添加renderElement,指向上一次要渲染的虚拟DOM节点
//因为后面组件更新的,我们会重新render,然后跟上一次的renderElement进行dom diff
componentInstance.renderElement = renderElement;
let newDOM = createDOM(renderElement);
renderElement.dom = newDOM;
// element.componentInstance.renderElement.dom=DIV真实DOM元素
return newDOM;
}
/**
let element = React.createElement('button',
{ id: 'sayHello', onClick },
'say', React.createElement('span', { color: 'red' }, 'Hello')
);
*/
function createNativeDOM(element) {
let { type, props } = element;// span button div
let dom = document.createElement(type);// 真实的BUTTON DOM对象
//1.创建此虚拟DOM节点的子节点
createDOMChildren(dom, element.props.children);
setProps(dom, props);
//2.给此DOM元素添加属性
return dom;
}
function createDOMChildren(parentNode, children) {
children && flatten(children).forEach((child, index) => {
//child其实是虚拟DOM,我们会在虚拟DOM加一个属性_mountIndex,指向此虚拟DOM节点在父节点中的索引
//在后面我们做dom-diff的时候会变得非常非常重要
child._mountIndex = index;
let childDOM = createDOM(child);//创建子虚拟DOM节点的真实DOM元素
parentNode.appendChild(childDOM);
});
}


export function ReactElement($$typeof, type, key, ref, props) {
let element = {
$$typeof, type, key, ref, props
}
return element;
}

对$$typeof的判断,来创建真实DOM元素

合成事件

react本身模拟了一套事件机制

index.js

1
2
3
4
5
6
7
8
9
import React from 'react';
import ReactDOM from 'react-dom';

let onClick = (event)=>{alert( event + 'hello')};
let element = React.createElement('button',
{id: 'sayHello', onClick},
'say', React.createElement('span',{ style: { color: 'red' } }, 'hello');
)
ReactDOM.render(element, document.getElementById('root'));

分析:一个button按钮,id为sayHello,上面有一个onClick事件,包含say内容和一个span标签,span标签内容为hello,颜色为红色

新建./react/event.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import { updateQueue } from './component';

/*
* 在React中并不是把事件绑在要绑定的DOM节点上,而是绑定到document上,类似于事件委托
* 1、因为合成事件可以屏蔽浏览器的差异,不同浏览器绑定事件和触发的方式不一样
* 2、合成事件可以实现事件对象复用,重用,减少垃圾回收,提高性能
* 3、因为默认我们要实现批量更新,setState 两个setState会合并成一次更新
*
* @params dom 要绑定事件的DOM节点
* @params eventType 事件的类型 onClick onChange
* @params listener 事件处理函数
*
*/
export function addEvent(dom, eventType, listener) {
eventType = eventType.toLowerCase(); // onClick =>onclick
// 在要绑定的DOM节点上挂载一个对象,准备存放监听函数
let eventStore = dom.eventStore || (dom.eventStore = {});
// eventStore.onclick = ()=>{ alert('hello') };
eventStore[eventType] = listener;
// document.addEventListener('click', dispatchEvent);
// dispatchEvent 事件处理,向上冒泡,最终是document处理事件
// 第一阶段是捕获,第二阶段是冒泡,false冒泡阶段,true捕获阶段
document.addEventListener(eventType.slice(2), dispatchEvent, false);
// 磨平差异
if('chrome'){
document.addEventListener();
}else if('ie6'){
document.attachEvent();
}
}
let syntheticEvent;

// 真正事件触发的回调统一是这个dispatchEvent方法
// event是原生DOM事件对象,但是传递给我们的监听函数并不是它
// 所有的事件处理函数都会进入dispatchEvent
function dispatchEvent(event) {
let { type, target } = event; // type=click , target=button
let eventType = 'on' + type;
syntheticEvent = getSyntheticEvent(event);
//在事件监听函数执行前先进入批量更新模式
updateQueue.isPending = true;
// 模拟事件冒泡
while (target) {
let { eventStore } = target;
let listener = eventStore && eventStore[eventType];
if (listener) {
listener.call(target, syntheticEvent);
}
target = target.parentNode;
}
// 等所有的监听函数都执行完了,就可以清掉所有的属性了,供下次复用次syntheticEvent对象
for (let key in syntheticEvent) {
if (syntheticEvent.hasOwnProperty(key))
delete syntheticEvent[key];
}
//当事件处理函数执行完成后,把批量更新模式改为false
updateQueue.isPending = false;
//执行批量更新,就是把缓存的那个updater全部执行了
updateQueue.batchUpdate();
}
// 持久化 异步调用事件会有问题,事件被清空了,用 事件对象.persist(); 持久化
// 如果执行了persist,就让syntheticEvent指向了新对象,while循环结束之后再清除的是新对象的属性
function persist() {
syntheticEvent = {};
Object.setPrototypeOf(syntheticEvent, {
persist
});
}
function getSyntheticEvent(nativeEvent) {
// 第一次才会创建,以后不再创建,始终会用同一个
if (!syntheticEvent) {
persist();
}
syntheticEvent.nativeEvent = nativeEvent;
syntheticEvent.currentTarget = nativeEvent.target;
// 把原生事件上的方法和属性都拷贝到了合成对象上
for (let key in nativeEvent) {
if (typeof nativeEvent[key] == 'function') {
syntheticEvent[key] = nativeEvent[key].bind(nativeEvent);
} else {
syntheticEvent[key] = nativeEvent[key];
}
}
return syntheticEvent;
}

unstable_batchedUpdates 批量强制更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 情况一
setTimeout(()=>{
this.setState({number: this.state.number+1});
console.log(this.state.number); // 1
this.setState({number: this.state.number+1});
console.log(this.state.number); // 2
})

// 情况二 批量强制更新
import ReactDOM, {unstable_batchedUpdates} from 'react-dom';
setTimeout(()=>{
unstable_batchedUpdates(()=>{
this.setState({number: this.state.number+1});
console.log(this.state.number); // 0
this.setState({number: this.state.number+1});
console.log(this.state.number); // 0
})

})

原理

1
2
3
4
5
6
7
import { updateQueue } from './component';
function unstable_batchedUpdates(fn){
updateQueue.isPending = true; // 强行处理批量更新模式
fn();
updateQueue.isPending = false;
updateQueue.batchUpdate();
}

补充一下updateQueue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export let updateQueue = {
updaters: [], // 这里面放着将要执行的更新器对象
isPending: false, // 是否批量更新,如果 isPending=true 则处于批量更新模式
add(updater) { // 放进去就完事,不进行真正的更新
this.updaters.push(updater);
},
// 需要有人调用batchUpdate方法才会真正更新
batchUpdate() {// 强行全部更新 执行真正的更新
if (this.isPending) {
return;
}
this.isPending = true; // 进入批量更新模式
let { updaters } = this;
let updater;
while ((updater = updaters.pop())) {
updater.updateComponent(); // 更新所有脏 dirty 组件
}
this.isPending = false; // 改为非批量更新
},
};

DOM-DIFF

  • DOM 节点跨层级的移动操作特别少,可以忽略不计
  • 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构
  • 对于同一层级的一组子节点,它们可以通过唯一key进行区分
  • DIFF算法在执行时有三个维度,分别是Tree DIFF、Component DIFF和Element DIFF,执行时按顺序依次执行,它们的差异仅仅因为DIFF粒度不同、执行先后顺序不同

Tree DIFF

  • Tree DIFF是对树的每一层进行遍历,如果某组件不存在了,则会直接销毁

  • 当出现节点跨层级移动时,并不会出现想象中的移动操作,而是以 A 为根节点的树被整个重新创建

Component DIFF

  • 如果是同一类型的组件,按照原策略继续比较
  • 类型不同则直接替换

原理分析

老节点 A B C D
新节点 A C B E F

(1) 针对老节点做一个map映射,属性是老节点的key,值是老节点(即React元素节点)(A:A元素,B:B元素,C:C元素,D:D元素)
(2) lastIndex 指向老节点中可复用,上一个不需要移动的元素的索引
(3) _mountIndex 挂载索引,表示这个节点在兄弟节点中的位置 (0 1 2 3)

1、开始遍历新节点的数组
1.1、在map里找有没有可复用的老节点,通过key去查找,如果新老两个元素key一样,type类型也是一样的,那就可以复用

【比较A节点】
通过key去查找老节点,key、type都一样,找到A
可复用的A节点 _mountIndex < lastIndex 则需要移动,否则不用移动可复用,现在对于A节点来说,_mountIndex 为0,lastIndex 为0,所以DOM节点A不用移动,复用老的DOM节点,用新属性更新这个DOM节点


如图,此时lastIndex还是指向0,_mountIndex 指向新的A节点的下标 0

【比较C节点】
继续,通过key去查找老节点,key、type都一样,找到C
可复用的老节点的_mountIndex:2, lastIndex:0 不移动,可复用,复用老的DOM节点,用新属性更新这个DOM节点,下标改为1
并且lastIndex = Math.max(lastIndex,老节点的挂载点_mountIndex 即2),这里lastIndex为2
每次比较都会取最大值给lastIndex,并且更新挂载点的索引

【比较B节点】
继续,通过key去查找老节点,key、type都一样,找到B
老的B节点的_mountIndex: 1,现在lastIndex: 2, _mountIndex < lastIndex ,移动节点,一定到新节点B的同下标位置 2

【比较节点E】
继续,通过key去查找老节点,没有找到,新节点E插入,挂载点是 3

【比较节点F】
继续,通过key去查找老节点,没有找到,新节点E插入,挂载点是 4

新数组遍历完了,将老节点中没用到的节点删除

【小结】
1、把老节点放在map里,属性为key,值为虚拟DOM
2、遍历新子元素数组,先去map里找有没有能复用的,如果找到了就更新DOM属性,并且判断_mountIndex是否小于lastIndex,如果小于则需要移动,从老节点原来的位置删除,并插入新数组的当前位置,然后把老节点的挂载点_mountIndex等于此节点在新数组中的索引,另外更新lastIndex为lastIndex和老挂载点的较大值
3、如果找不到可以复用的老节点,则直接在当前索引位置插入新节点
4、新数组遍历完成后,如果还有没有用到的老节点,删除。

深度优先遍历,如果节点下还有子节点,会先去遍历子节点
这里的类型比较是type,$$type大类型,可能都是element,而type则是比较的是div、span这种

【示例】
如果 A 下面有节点 B C,改成 A C B,会先去更新B C 下面的子节点,再整体移动 B 放到 C 的后边



部分代码实现

更新子节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let diffQueue = [];// 这是一个补丁包,记录了哪些节点需要删除,哪些节点需要添加
function updateChildren(dom, oldChildrenElements, newChildrenElements){
updateDepth++; // 每进入一个新的子层级,就让updateDepth++
diff(dom, oldChildrenElements, newChildrenElements, diffQueue);
updateDepth--; // 每比较完一层,返回上一级的时候,就updateDepth--
if(updateDepth == 0){ // updateDepth = 0,就说明到最上面一层了,整个更新对比就完事了
path(diffQueue); // 把收集到的差异补丁传给path方法进行更新
diffQueue.length = 0;
}
}
// 这里diffQueue的结构
diffQueue.push({
parentNode: parentNode, // 我要移动哪个父节点下的元素
type: 'MOVE', // INSERT REMOVE
fromIndex: oldChildrenElement._mountIndex, // 从哪里移动
toIndex: i,// 移动到哪里
})

开始打包,这里操作的是真实的DOM元素

  • 第一步,把该删除的删除 MOVE REMOVE 移动的和删除的
  • 第二步,插入移动的

这里指的是移动的B 和 删除的 D,其中要把移动的B放在一个Map里面缓存起来后面复用

先更新父节点的属性,再更新子节点的属性
先移动子节点,再移动父节点

相关文章:
http://www.zhufengpeixun.cn/2020/html/96.1.react16.html

生命周期

context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function createContext(defaultValue){
Provider.value = defaultValue;
function Provider(props){
Provider.value = props.value;
return props.children;
}
function Consumer(props){
// return props.children(Provider.value);
return onlyOne(props.children)(Provider.value);
}
return {Provider, Consumer};
}

const React = {
createElement,
Component,
createRef,
createContext
}
export React;
1
2
3
export function onlyOne(obj){
return Array.isArray(obj) ? obj[0] : obj;
}

设计模式

发表于 2020-04-08 | 更新于: 2020-05-14 | 分类于 js基础

装饰器模式

  • 在不改变原有的结构和功能 为对象添加功能
  • 装饰模式有时候比继承更加灵活

在不改变原有的结构和功能 为对象添加功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Duck{
eat(food) {
console.log(`吃${food}`);
}
}

class TangDuck{
constructor() {
this.duck=new Duck();
}
eat(food) {
this.duck.eat(food);
console.log('谢谢');
}
}
let t = new TangDuck();
t.eat('苹果');

打印: 吃苹果 谢谢

解析:Duck原组件没有改变,TangDuck对它做了增强,除了原有的之外添加了额外的逻辑

装饰模式有时候比继承更加灵活

装饰器模式是将一个对象嵌入另一个对象之中,实际上相当于这个对象被另一个对象包装起来,形成一条包装链。请求随着这条链条依次传递到所有的对象,每个对象有处理这个请求的机会。

下面是链式的水+咖啡+奶+糖=13的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Coffee{
make(water){
return `${water}+咖啡`;
}
cost(){
return 10;
}
}

class MilkCoffee{
constructor(parent){
this.parent = parent;
}
make(water){
return `${this.parent.make(water)}+牛奶`;
}
cost(){
return this.parent.cost()+1;
}
}

class SugerCoffee{
constructor(parent){
this.parent = parent;
}
make(water){
return `${this.parent.make(water)}+糖`;
}
cost(){
return this.parent.cost()+2;
}
}
let coffee = new Coffee();
let milkCoffee = new MilkCoffee(coffee);
let milksugerCoffee = new SugerCoffee(milkCoffee);
console.log(milksugerCoffee.make('水')+'='+milksugerCoffee.cost());

AOP

面向切面编程
就是在函数执行之前或之后添加一些额外的逻辑,而不需要函数的功能。

应用场景

埋点

埋点分析,是网站分析的一种常用的数据采集方法

埋点方式

  • 服务器层面的:主要是通过客户端的请求进行分析
  • 客户端层面的:通过埋点进行相应的分析
    • 代码埋点
    • 自动化埋点:通过AOP思想对相应的方法进行统计
    • 第三方实现 百度、友盟等…
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<body>
<button data-name="西瓜" id="watermelon">西瓜</button>
<button data-name="苹果" id="apple">苹果</button>
<script>
let watermelon = document.getElementById('watermelon');
let apple = document.getElementById('apple');
Function.prototype.after = function(afterFn){
let _this = this;
return function(){
_this.apply(this,arguments); // 先执行15-17
afterFn.apply(this,arguments); // 再执行19-22
}
}
// 这是正常逻辑
function click(){
console.log('点击'+this.dataset.name);
}
// 这是埋点逻辑
click = click.after(function(){
let img = new Image();
img.src = `http://localhost:3000?name=${this.dataset.name}`; // 当前按钮点击的名字通过src发送给服务器端
});
Array.from(document.querySelectorAll('button')).forEach(function(button){ // 给每个button上都增加一个事件
button.addEventListener('click',click);
});
1
2
3
4
5
6
7
let express = require('express');
let app = express();
app.get('/',function(req,res){
console.log('name',req.query.name);
res.end('ok');
});
app.listen(3000);

表单校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<body>
<form action="">
用户名<input type="text" name="username" id="username">
密码<input type="text" name="password" id="password">
<button id="submit-btn" >提交</button>
</form>
<script>
Function.prototype.before = function(beforeFn){
let _this = this;
return function(){
let ret = beforeFn.apply(this,arguments);
if(ret)
_this.apply(this,arguments);
}
}
function submit(){ // 业务逻辑
alert('提交表单');
}
let checkUserNameNotNull= submit.before(function(){ // 校验逻辑 和 业务逻辑分开
let username = document.getElementById('username').value;
if(username.length<6){
return alert('用户名不能少于6位');
}
return true;
});
let checkUserNameMoreThanSix = checkUserNameNotNull.before(function(){ // 校验逻辑 和 业务逻辑分开
let username = document.getElementById('username').value;
if(!username){
return alert('用户名不能为空');
}
return true;
});
document.getElementById('submit-btn').addEventListener('click',checkUserNameMoreThanSix);

express 不太一样,但是koa和这个原理是一样的
axios的interceptor就是依据这个写的

类装饰器 | 方法装饰器

1
2
3
4
@testable 
class Person{

}

观察者模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Star{
constructor(name) {
this.name=name;
this.state='';
this.observers=[]; // 观察者数组
}
getState() { // 获取状态
return this.state;
}
setState(state) {// 改变状态
this.state=state;
this.notifyAllObservers();
}
attach(observer) { // 绑定一个新的观察者
this.observers.push(observer);
}
notifyAllObservers() { // 通知所有的观察者更新自己
this.observers.forEach(observer=>observer.update());
}
}
class Fan{
constructor(name,subject) {
this.name=name;
this.subject=subject;
this.subject.attach(this);
}
update() {
console.log(`${this.subject.name}有新的状态-${this.subject.getState()},${this.name}正在更新`);
}
}
let star=new Star('明星');
let fan1=new Fan('粉丝',star);
star.setState('结婚');

观察者模式区别于发布订阅模式:

  • 被观察者和观察者是耦合的(被观察者内部保存观察者的引用地址的数组用来通知观察者的)
  • 观察者的update动作是由被观察者调用的

发布订阅模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class Agent{ // 中介
constructor(){
this._events = {};
}
// on addEventListener
subscribe(type,listener){
let listeners = this._events[type];
if(listeners){
this._events[type].push(listener);
}else{
this._events[type] = [listener];
}
}
// emit
publish(type){
let listeners = this._events[type];
let args = Array.prototype.slice.call(arguments,1);
if(listeners){
listeners.forEach(listener=>listener(...args));
}
}
}
class LandLord{ // 房东
constructor(name){this.name=name;}
lend(agent,area,money){ // 向外出租
agent.publish('house',area,money);
}
}
class Tenant{ // 租房的人
constructor(name){this.name=name;}
rent(agent,area,money){ //租房
agent.subscribe('house',(area,money)=>{
console.log(`我看到中介的新房源了 ${area}平 ${money}元`);
});
}
}

let agent = new Agent();
let t1 = new Tenant('张三');
let t2 = new Tenant('李四');
t1.rent(agent);
t2.rent(agent);

let landLord = new LandLord();
landLord.lend(agent,60,4000); // 发布房源

打印

1
2
我看到中介的新房源了 60平 4000元
我看到中介的新房源了 60平 4000元

应用场景:

微信公众号,你关注了微信公众号,微信公众号就会给粉丝推消息,微信公众号就是一个主题,粉丝就是观察者。
微博,观察者模式的精髓就是,主题持有观察者的引用,所以才能实现 主题发生改变,观察者能响应到主题发生的变化。一个动作发生了改变,其他几个动作也要发生改变。业务场景就这样。

100道前端面试题

发表于 2020-04-01 | 更新于: 2020-08-25 | 分类于 面试题

CSS

[Q1] 已知如下代码,如何修改才能让图片宽度为 300px ?注意下面代码不可修改。

1
<img src="1.jpg" style="width:480px!important;”>

[A1]
没有顺序,置前置后都生效
1、max-width: 300px;
2、transform: scale(0.625,0.625);
3、zoom: 0.5; (zoom的缩放是相对于左上角的;而scale默认是居中缩放。)
4、padding: 0 90px; box-sizing: border-box;
5、用js document.getElementsByTagName("img")[0].setAttribute("style","width:300px!important;");
6、给图片设置动画

1
2
3
4
5
6
7
8
9
10
11
img {
animation: test 0s forwards;
}
@keyframes test {
from {
width: 300px;
}
to {
width: 300px;
}
}

原理是CSS动画的样式优先级高于!important的特性

[Q2] 介绍下 BFC 及其应用

[A2]

块级格式化上下文 (Block Formatting Context)

怎样才能形成BFC:
1、根元素
2、float的值不能为none
3、overflow的值不能为visible
4、display的值为table-cell, table-caption, inline-block,flex中的任何一个
5、position的值不为relative和static 

[BFC] https://juejin.im/post/5a4dbe026fb9a0452207ebe6
[BFC] https://zhuanlan.zhihu.com/p/25321647
[清除浮动] https://www.cnblogs.com/dolphinX/p/3508869.html
[清除浮动详解] https://www.cnblogs.com/iyangyuan/archive/2013/03/27/2983813.html

[Q3] vw vh 是什么意思

[A3]
vw:相对于视口的宽度,视口被均分为100单位的vw
vh:相当于视口的宽度

[Q4] 用css画出三角形

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html>
<head>
<title></title>
<style type="text/css">
/* css3绘制三角形 */
.triangle{
width: 0px; /*设置宽高为0,所以div的内容为空,从才能形成三角形尖角*/
height: 0px;
border-bottom: 200px solid #00a3af;
border-left: 200px solid transparent; /*transparent 表示透明*/
border-right: 200px solid transparent;
}
</style>
</head>
<body>
<div class="triangle"></div>
</body>
</html>

https://blog.csdn.net/weixin_36270908/article/details/98947183

[Q5] 三栏布局

当中间内容超出时 https://jiafei2333.github.io/interview/layout_overflow.html 中float中间部分超出 若希望中间内容不到左边去 可以给中间设为bfc 因为bfc不与float重叠 例如 overflow:hidden | overflow:auto;

box-sizing: content-box; 默认

React/Vue

[Q1] 写 React / Vue 项目时为什么要在列表组件中写 key,其作用是什么?

https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/1
在交叉对比中,当新节点跟旧节点头尾交叉对比没有结果时,会根据新节点的key去对比旧节点数组中的key,从而找到相应旧节点(这里对应的是一个key => index 的map映射)。如果没找到就认为是一个新增节点。而如果没有key,那么就会采用遍历查找的方式去找到对应的旧节点。一种一个map映射,另一种是遍历查找。相比而言。map映射的速度更快。

key的作用是为了在diff算法执行时更快的找到对应的节点,提高diff速度。

【拓展】

React/Vue Diff算法

[Q2] React 中 setState 什么时候是同步的,什么时候是异步的?

在React中,如果是由React引发的事件处理(比如通过onClick引发的事件处理),调用setState不会同步更新this.state,除此之外的setState调用会同步执行this.state。所谓“除此之外”,指的是绕过React通过addEventListener直接添加的事件处理函数,还有通过setTimeout/setInterval产生的异步调用。

原因:在React的setState函数实现中,会根据一个变量isBatchingUpdates判断是直接更新this.state还是放到队列中回头再说,而isBatchingUpdates默认是false,也就表示setState会同步更新this.state,但是,有一个函数batchedUpdates,这个函数会把isBatchingUpdates修改为true,而当React在调用事件处理函数之前就会调用这个batchedUpdates,造成的后果,就是由React控制的事件处理过程setState不会同步更新this.state。

[Q] 聊聊 Redux 和 Vuex 的设计思想

[Q] 聊聊 Vue 的双向数据绑定,Model 如何改变 View,View 又是如何改变 Model 的?

[Q] Virtual DOM 真的比操作原生 DOM 快吗?谈谈你的想法。

[Q] react的constructor中为什么要写super(prop

【解答】

1、我们为什么要调用super?能不能不调用它?

在 JavaScript 中,super 指代父类的构造函数。
在你调用父类构造函数之前,你无法在构造函数中使用 this。JavaScript 不会允许你这么做。

1
2
3
4
5
6
7
8
9
class Checkbox extends React.Component {
constructor(props) {
// 这时候还不能使用 `this`
super(props);
// 现在开始可以了
this.state = { isOn: true };
}
// ...
}

JavaScript 强制你在使用 this 前运行父类构造函数有一个很好的理由。考虑这样一个类结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person {
constructor(name) {
this.name = name;
}
}

class PolitePerson extends Person {
constructor(name) {
this.greetColleagues(); // 这是不允许的,下面会解释原因
super(name);
}

greetColleagues() {
alert('Good morning folks!');
}
}

想象一下如果在调用 super 前使用 this 是被允许的。一个月之后。我们或许会改变 greetColleagues 把 person 的 name 加到消息中。

1
2
3
4
greetColleagues() {
alert('Good morning folks!');
alert('My name is ' + this.name + ', nice to meet you!');
}

但我们忘了 this.greetColleagues() 是在 super() 设置 this.name 之前被调用的。this.name 甚至还没被定义!如你所见,像这样的代码理解起来会很困难。

为了避免这样的陷阱,JavaScript 强制规定,如果你想在构造函数中用this,就必须先调用 super。

1
2
3
4
5
constructor(props) {
super(props);
// ✅ 现在可以使用 `this` 了
this.state = { isOn: true };
}

2、另一个问题:为什么要传 props?
事实上即便在调用 super() 时没有传入 props 参数,你依然能够在 render 和其它方法中访问 this.props
但在 super() 调用一直到构造函数结束之前,this.props 依然是未定义的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// React 内部
class Component {
constructor(props) {
this.props = props;
// ...
}
}

// 你的代码
class Button extends React.Component {
constructor(props) {
super(); // 我们忘了传入 props
console.log(props); // {}
console.log(this.props); // undefined
}
// ...
}

这也是推荐总是使用 super(props) 的写法,即便这是非必要的。

1、调用super的原因:在ES6中,在子类的constructor中必须先调用super才能引用this;根本原因是constructor会覆盖父类的constructor,导致你父类构造函数没执行,所以手动执行下。
2、super(props)的目的:在constructor中可以使用this.props。

参看文章:
https://juejin.im/post/5c04fea5f265da6133565696

JS

[Q] [‘1’, ‘2’, ‘3’].map(parseInt) what & why ?

parseInt() 参数:
string 必需。要被解析的字符串。
radix 可选。表示要解析的数字的基数。该值介于 2 ~ 36 之间。如果省略该参数或其值为 0,则数字将以 10 为基础来解析。如果它以 “0x” 或 “0X” 开头,将以 16 为基数。如果该参数小于 2 或者大于 36,则 parseInt() 将返回 NaN。

解析:https://github.com/sisterAn/blog/issues/19

【拓展】

1
2
3
4
5
6
7
8
9
10
11
1、
['10','10','10','10','10','10','10','10','10','10','10','10','10','10','10','10','10','10'].map(parseInt);
[10, NaN, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]

2、
['9','9','9','9','9','9','9','9','9','9','9','9','9',].map(parseInt);
[9, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, 9, 9, 9]

3、
['11','11','11','11','11','11','11','11','11','11','11','11','11','11','11','11','11','11'].map(parseInt);
[11, NaN, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]

【拓展】

1
2
3
let unary = fn => val => fn(val)
let parse = unary(parseInt)
console.log(['1.1', '2', '0.3'].map(parse))

[Q] 什么是防抖和节流?有什么区别?如何实现?

【时间起因】https://github.com/mqyqingfeng/Blog/issues/22 (只看原因,不看这里的原理解析)
【简洁归纳】防抖是虽然事件持续触发,但只有等事件停止触发后 n 秒才执行函数,节流是持续触发的时候,每 n 秒执行一次函数。

https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/5

防抖

1
<input type="text" id="inp" />
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function debounce(fn) {
let timeout = null; // 创建一个标记用来存放定时器的返回值
return function () {
clearTimeout(timeout); // 每当用户输入的时候把前一个 setTimeout clear 掉
timeout = setTimeout(() => { // 然后又创建一个新的 setTimeout, 这样就能保证输入字符后的 interval 间隔内如果还有字符输入的话,就不会执行 fn 函数
fn.apply(this, arguments); // line12 输出 input DOM
// fn(); // line12 输出 Window对象
}, 2000);
};
}
function sayHi() {
console.log('防抖成功',this); // 当前调用sayHi方法的this
}

var inp = document.getElementById('inp');
inp.addEventListener('input', debounce(sayHi)); // 防抖 n 之间后执行一次 最后一次触发,2000后执行
//inp.addEventListener('input', sayHi); // 每次input中输入都触发

[疑问] fn.apply(this, arguments);而不是这样 fn()
[解答] 为了确保上下文环境为当前的this,所以不能直接用fn。
如果单单为了打印那句console.log(‘防抖成功’);确实可以直接fn(),但我们得考虑实际情况,让sayHi的this指向input是必要的,例如我们需要在输入完改变字体颜色,如下:

1
function sayHi() { console.log('防抖成功'); this.style.color = 'red'; }

这个时候 fn.apply(this, arguments);的作用就显而易见了

【应用场景】

  1. 表单的连续点击,防止重复提交。比如重复发送一篇文章,提交按钮一直点,但是在一定的时间内只会触发一次。
  2. 类百度的搜索,连续输入等输入停止后再搜索。
  3. 一直拖动浏览器窗口,只想触发一次事件等。

节流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function throttle(fn) {
let canRun = true; // 通过闭包保存一个标记
return function () {
if (!canRun) return; // 在函数开头判断标记是否为true,不为true则return
canRun = false; // 立即设置为false
setTimeout(() => { // 将外部传入的函数的执行放在setTimeout中
fn.apply(this, arguments);
// 最后在setTimeout执行完毕后再把标记设置为true(关键)表示可以执行下一次循环了。当定时器没有执行的时候标记永远是false,在开头被return掉
canRun = true;
}, 500);
};
}
function sayHi(e) {
console.log(e.target.innerWidth, e.target.innerHeight);
}
window.addEventListener('resize', throttle(sayHi));

【应用场景】

  1. 节流可以是scroll,用滚动条计算高度的时候原先会实时计算,用节流,可以每隔例如300ms计算一次。
  2. 自动保存草稿功能,当用户在输入的时候(一直触发事件),单位时间内只保存一次草稿。
  3. 游戏中的刷新率

[Q] 介绍下 Set、Map、WeakSet 和 WeakMap 的区别?

[Q] 介绍下深度优先遍历和广度优先遍历,如何实现?

[A]

[Q] 有以下 3 个判断数组的方法,请分别介绍它们之间的区别和优劣Object.prototype.toString.call() 、 instanceof 以及 Array.isArray() ?

[A]

  1. Object.prototype.toString.call()

每一个继承 Object 的对象都有 toString 方法,如果 toString 方法没有重写的话,会返回 [Object type],其中 type 为对象的类型。但当除了 Object 类型的对象外,其他类型直接使用 toString 方法时,会直接返回都是内容的字符串,所以我们需要使用call或者apply方法来改变toString方法的执行上下文。

1
2
3
const an = ['Hello','An'];
an.toString(); // "Hello,An"
Object.prototype.toString.call(an); // "[object Array]"

这种方法对于所有基本的数据类型都能进行判断,即使是 null 和 undefined 。

1
2
3
4
5
6
7
Object.prototype.toString.call('An') // "[object String]"
Object.prototype.toString.call(1) // "[object Number]"
Object.prototype.toString.call(Symbol(1)) // "[object Symbol]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call(function(){}) // "[object Function]"
Object.prototype.toString.call({name: 'An'}) // "[object Object]"
  1. instanceof

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

1
2
3
4
5
6
7
8
[]  instanceof Array; // true


function M(){};
let obj = new M();
console.log(obj instanceof M); // true
console.log(obj instanceof Object); // true
console.log(M instanceof Object); // true
  1. Array.isArray()

Array.isArray() 用于确定传递的值是否是一个 Array。

  • 当检测Array实例时, Array.isArray 优于 instanceof,因为Array.isArray能检测iframes.
1
2
3
4
5
6
7
8
9
var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
xArray = window.frames[window.frames.length-1].Array;
var arr = new xArray(1,2,3); // [1,2,3]

// Correctly checking for Array
Array.isArray(arr); // true
// Considered harmful, because doesn't work though iframes
arr instanceof Array; // false
  • Array.isArray() 与 Object.prototype.toString.call()

Array.isArray()是ES5新增的方法,当不存在 Array.isArray() ,可以用 Object.prototype.toString.call() 实现。

1
2
3
4
5
if (!Array.isArray) {
Array.isArray = function(arg) {
return Object.prototype.toString.call(arg) === '[object Array]';
};
}

[Q] 全局作用域中,用 const 和 let 声明的变量不在 window 上,那到底在哪里?如何去获取?

ES6规定,var 命令和 function 命令声明的全局变量,依旧是顶层对象的属性,但 let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。

1
2
3
4
5
6
7
8
9
var a = 12;
function f(){};
console.log(window.a); // 12
console.log(window.f); // f(){}

let aa = 1;
const bb = 2;
console.log(window.aa); // undefined
console.log(window.bb); // undefined



通过上图也可以看到,在全局作用域中,用 let 和 const 声明的全局变量并没有在全局对象中,只是一个块级作用域(Script)中

[Q] cookie 和 token 都存放在 header 中,为什么不会劫持 token?

【XSS攻击】
恶意攻击者往Web页面里插入恶意Script代码,当用户浏览该页之时,嵌入其中Web里面的Script代码会被执行,从而达到恶意攻击用户的目的。
【XSS危害】

  • 窃取网页浏览中的cookie值
    登录完毕之后服务端会返回一个cookie值。这个cookie值相当于一个令牌,拿着这张令牌就等同于证明了你是某个用户。如果你的cookie值被窃取,那么攻击者很可能能够直接利用你的这张令牌不用密码就登录你的账户。如果想要通过script脚本获得当前页面的cookie值,通常会用到document.cookie。(不过某些厂商的cookie有其他验证措施如:Http-Only保证同一cookie不能被滥用)
  • 劫持流量实现恶意跳转
    这个很简单,就是在网页中想办法插入一句像这样的语句:
    1
    <script>window.location.href="http://www.baidu.com";</script>

【CSRF攻击】
跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。
一个典型的CSRF攻击有着如下的流程:

  • 受害者登录a.com,并保留了登录凭证(Cookie)。
  • 攻击者引诱受害者访问了b.com。
  • b.com 向 a.com 发送了一个请求:a.com/act=xx。浏览器会…
  • a.com接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是受害者自己发送的请求。
  • a.com以受害者的名义执行了act=xx。
  • 攻击完成,攻击者在受害者不知情的情况下,冒充受害者,让a.com执行了自己定义的操作。

浏览器发送请求的时候不会自动带上token,而cookie在浏览器发送请求的时候会被自动带上。csrf就是利用的这一特性,所以token可以防范csrf,而cookie不能。
1、首先token不是防止XSS的,而是为了防止CSRF的;
2、CSRF攻击的原因是浏览器会自动带上cookie,而浏览器不会自动带上token

[Q] 下面的代码打印什么内容,为什么?

1
2
3
4
5
var b = 10;
(function b() {
b = 20;
console.log(b) // [Function b]
})()

【拓展】 自执行的具名函数A内部修改A的值无效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(function A() {
console.log(A); // [Function A]
A = 1;
console.log(window.A); // undefined
console.log(A); // [Function A]
}())

(function A() {
console.log(A); // undefined
var A = 1;
console.log(window.A); // undefined
console.log(A); // 1
}())

function A() {
console.log(A); // [Function A]
A = 1;
console.log(window.A); // 1
console.log(A); // 1
}
A();

【拓展】

1
2
3
4
5
6
7
8
9
var b = 10;
(function b() {
// 内部作用域,会先去查找是有已有变量b的声明,有就直接赋值20,确实有了呀。发现了具名函数 function b(){},拿此b做赋值;
// IIFE的函数无法进行赋值(内部机制,类似const定义的常量),所以无效。
// (这里说的“内部机制”,想搞清楚,需要去查阅一些资料,弄明白IIFE在JS引擎的工作方式,堆栈存储IIFE的方式等)
b = 20;
console.log(b); // [Function b]
console.log(window.b); // 10,不是20
})();

所以严格模式下能看到错误:Uncaught TypeError: Assignment to constant variable

1
2
3
4
5
6
var b = 10;
(function b() {
'use strict'
b = 20;
console.log(b)
})() // "Uncaught TypeError: Assignment to constant variable."

其他情况例子:
有window:

1
2
3
4
5
6
var b = 10;
(function b() {
window.b = 20;
console.log(b); // [Function b]
console.log(window.b); // 20是必然的
})();

有var

1
2
3
4
5
6
var b = 10;
(function b() {
var b = 20; // IIFE内部变量
console.log(b); // 20
console.log(window.b); // 10
})();

[Q] 简单改造下面的代码,使之分别打印 10 和 20。

1
2
3
4
5
var b = 10;
(function b(){
b = 20;
console.log(b);
})();

【解答】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var b = 10;
(function b(){
b = 20;
console.log(window.b);
})();
// 打印10 方法一
var b = 10;
(function b(b){
window.b = 20;
console.log(b);
})(b);
// 打印10 方法二
var b = 10;
(function b(){
var b = 20;
console.log(b);
})();
// 打印20 方法一
var b = 10;
(function b(){
let b = 20;
console.log(b);
})();
// 打印20 方法二
var b = 10;
(function b(b){
b = 20;
console.log(b);
})(b);
// 打印20 方法三

[Q] 下面代码输出什么?

1
2
3
4
5
6
7
8
var a = 10;
(function () {
console.log(a)
a = 5
console.log(window.a)
var a = 20;
console.log(a)
})()

【解答】
undefined
10
20

[Q] 实现一个 sleep 函数,比如 sleep(1000) 意味着等待1000毫秒,可从 Promise、Generator、Async/Await 等角度

[Q] Object.assign() 和 … 的区别

  • 当一个 Object 使用了 Object.defineProperty 修改了 set 方法,因为调用 Object.assign 会触发 setter 方法,会触发意想不到的错误
  • 如果将空对象作为第一个参数传递给Object.assign(),看起来 Object spread 会更快。

[Q] 为什么要用闭包 全部放在全局不好吗?

【解答】
垃圾回收:会去获取变量、方法被调用的次数,被调用的次数为0就会销毁;
把变量、方法等全部定义在全局是可以的,随着代码的执行早晚会被销毁;变量都在全局太多会重名 出现污染的情况所以要用闭包(避免全局变量污染);闭包的缺点:内存泄漏 add = 一个方法(闭包方法);add和这个方法形成一个调用栈,add不会释放,闭包里面的变量也不会释放,只能手动释放 add = null

其他

[Q] 谈谈你对 TCP 三次握手和四次挥手的理解

【三次握手】

1
2
3
4
5
6
7
三次握手之所以是三次是保证client和server均让对方知道自己的接收和发送能力没问题而保证的最小次数。

第一次client => server 只能server判断出client具备发送能力
第二次 server => client client就可以判断出server具备发送和接受能力。此时client还需让server知道自己接收能力没问题于是就有了第三次
第三次 client => server 双方均保证了自己的接收和发送能力没有问题

其中,为了保证后续的握手是为了应答上一个握手,每次握手都会带一个标识 seq,后续的ACK都会对这个seq进行加一来进行确认。

或者

1
2
3
1、客户端发送位码为syn=1,随机产生seq number=1234567的数据包到服务器,服务器由SYN=1知道客户端要求建立联机(客户端:我要连接你)
2、服务器收到请求后要确认联机信息,向A发送ack number=(客户端的seq+1),syn=1,ack=1,随机产生seq=7654321的包(服务器:好的,你来连吧)
3、客户端收到后检查ack number是否正确,即第一次发送的seq number+1,以及位码ack是否为1,若正确,客户端会再发送ack number=(服务器的seq+1),ack=1,服务器收到后确认seq值与ack=1则连接建立成功。(客户端:好的,我来了)

【拓展】
提问:为什么http建立连接需要三次握手,不是两次或四次?
回答: 三次是最少的安全次数,两次不安全,四次浪费资源

【四次挥手】

1
2
3
4
5
6
为什么要四次挥手?TCP是全双工,何为全双工就是客户端与服务端建立两条通道,通道1:客户端的输出连接服务端的输入;通道2:客户端的输入连接服务端的输出。两个通道可以同时工作:客户端向服务端发送信号的同时服务端也可以向客户端发送信号。所以关闭双通道的时候就是这样:
客户端:我要关闭输入通道了。
服务端:好的,你关闭吧,我这边也关闭这个通道。

服务端:我也要关闭输入通道了。
客户端:好的你关闭吧,我也把这个通道关闭。

[Q] 介绍下重绘和回流(Repaint & Reflow),以及如何进行优化 ?

浏览器的渲染过程

1、解析HTML,生成DOM树,解析CSS,生成CSSOM树
2、将DOM树和CSSOM树结合,生成渲染树(Render Tree)
3、根据RenderTree,我们就知道了所有节点的样式,然后计算他们在页面上的大小和位置,最后把节点绘制到页面上。
需要明白,这几个步骤并不一定一次性顺序完成。如果 DOM 或 CSSOM 被修改,以上过程需要重复执行,这样才能计算出哪些像素需要在屏幕上进行重新渲染。实际页面中,CSS 与 JavaScript 往往会多次修改 DOM 和 CSSOM。
https://github.com/chenjigeng/blog/issues/4
https://juejin.im/post/5a8e242c5188257a6b060000

[Q] 浏览器输入URL后发生了什么?

https://www.xuecaijie.com/it/157.html#1Q64p5DeC8dKFF

[Q] 请求时浏览器缓存 from memory cache 和 from disk cache 的依据是什么,哪些数据什么时候存放在 Memory Cache 和 Disk Cache中

了解下: https://juejin.im/post/5c22ee806fb9a049fb43b2c5?utm_source=gold_browser_extension

Node

[Q] 介绍下 npm 模块安装机制,为什么输入 npm install 就可以自动安装对应的模块?

  • 发出npm install命令
  • 查询node_modules目录之中是否已经存在指定模块
    • 若存在,不再重新安装
    • 若不存在
      • npm 向 registry 查询模块压缩包的网址 (https://registry.npmjs.org/react 跟模块名, 就会看到 react 模块所有版本的信息。)
      • 下载压缩包,存放在根目录下的.npm目录里
      • 解压压缩包到当前项目的node_modules目录

【拓展】
注意,一个模块安装以后,本地其实保存了两份。一份是/.npm目录下的压缩包,另一份是node_modules目录下解压后的代码。
但是,运行npm install的时候,只会检查node_modules目录,而不会检查
/.npm目录。也就是说,如果一个模块在~/.npm下有压缩包,但是没有安装在node_modules目录中,npm 依然会从远程仓库下载一次新的压缩包。

[Q] 浏览器和Node 事件循环的区别

https://jiafei2333.github.io/2019/09/16/EventLoop/

算法

[Q] 两个数组合并成一个数组

请把两个数组 [‘A1’, ‘A2’, ‘B1’, ‘B2’, ‘C1’, ‘C2’, ‘D1’, ‘D2’] 和 [‘A’, ‘B’, ‘C’, ‘D’],合并为 [‘A1’, ‘A2’, ‘A’, ‘B1’, ‘B2’, ‘B’, ‘C1’, ‘C2’, ‘C’, ‘D1’, ‘D2’, ‘D’]。

https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/39

[Q] 改造下面的代码,使之输出0 - 9,写出你能想到的所有解法。

1
2
3
4
5
for (var i = 0; i< 10; i++){
setTimeout(() => {
console.log(i);
}, 1000)
}

解法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// 1 错  10个10
for (var i = 0; i< 10; i++){
setTimeout((i) => {
console.log(i);
}, 1000)
}
// 2 对
for (var i = 0; i< 10; i++){
setTimeout((i) => {
console.log(i);
}, 1000,i)
}
// 3 对
for (let i = 0; i< 10; i++){
setTimeout(() => {
console.log(i);
}, 1000)
}
// 4 错 10个10
for (let i = 0; i< 10; i++){
setTimeout((i) => {
console.log(i);
}, 1000)
}
// 5 错 10个10
for (var i = 0; i< 10; i++){
(function(i){
setTimeout((i) => {
console.log(i);
}, 1000)
})(i)
}
// 6 对
for (var i = 0; i< 10; i++){
(function(i){
setTimeout(() => {
console.log(i);
}, 1000)
})(i)
}
// 7 对
for (var i = 0; i< 10; i++){
try{
throw i;
}catch(i){
setTimeout(() => {
console.log(i);
}, 1000)
}
}
// 8 对
for (var i = 0; i<10; i++) {
new Promise(function (resolve, reject) {
resolve(i)
}).then(function (data) {
setTimeout(() => {
console.log(data)
}, 1000)
})
}
// 9 对
for(var i = 0; i<10; i++) {
setTimeout(console.log.bind(null, i), 1000)
}

【拓展】
bind(null, …)

1
2
3
4
5
function multiply (x, y, z) {
return x * y * z;
}
var double = multiply.bind(null, 2);
console.log(double(3, 4)); // 24

例如这里第一次就传了x的值,那么yz的值就后续调用里面传入的。

bind() 的另一个最简单的用法是使一个函数拥有预设的初始参数。只要将这些参数(如果有的话)作为 bind() 的参数写在 this 后面。当绑定函数被调用时,这些参数会被插入到目标函数的参数列表的开始位置,传递给绑定函数的参数会跟在它们后面。

[Q] 使用迭代的方式实现 flatten 函数

1
var arr=[1,2,3,[4,5],[6,[7,[8]]]]

【解答】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var arr=[1,2,3,[4,5],[6,[7,[8]]]]

const flatten = (data)=>{
let _temp = [];
(function flattening(data){
data.forEach(item=>{
if(Array.isArray(item)){
return flattening(item);
}else{
_temp.push(item);
}
})
})(data)

return _temp;
}
console.log(flatten(arr));
1
2
3
4
5
6
7
8
9
var arr=[1,2,3,[4,5],[6,[7,[8]]]]

const flatten = (data) =>{
while(data.some(item=>Array.isArray(item))){
data = [].concat(...data);
}
return data;
}
console.log(flatten(arr));
1
2
3
4
var arr=[1,2,3,[4,5],[6,[7,[8]]]]

const flatten = data => data.reduce((acc,cur)=> (Array.isArray(cur) ? [...acc, ...flatten(cur)] : [...acc, cur]), [])
console.log(flatten(arr));

[Q] 下面代码中 a 在什么情况下会打印 1?

1
2
3
4
var a = ?;
if(a == 1 && a == 2 && a == 3){
console.log(1);
}

【解答】

== 会进行隐式类型转换 所以我们重写toString方法就可以了
这题考察的是类型的隐式转换,考引用类型在比较运算符时候,隐式转换会调用本类型toString或valueOf方法.

1
2
3
4
5
6
7
8
9
var a = {
num: 1,
toString: function(){
return a.num ++
}
}
if(a == 1 && a == 2 && a == 3){
console.log(1);
}
1
2
3
4
5
6
7
8
9
var a = {
num: 1,
valueOf: function(){
return a.num ++
}
}
if(a == 1 && a == 2 && a == 3){
console.log(1);
}

valueOf优先级大于toString, 同时存在,会调用valueOf

[Q] 筛选id相同和不相同的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let arr1 = [1,2,3,4,5];
let arr2 = [1,3,5,7];
[...arr1,...arr2].reduce((result,v)=>{
let index=result.diff.indexOf(v)
if(index!==-1){
result.same.push(result.diff.splice(index,1)[0]);
}else{
result.diff.push(v);
}
return result;
},{same:[],diff:[]})
// 打印结果:
{
diff:[2,4,7],
same:[1,2,5]
}

[Q] 用最短的代码实现去重

1
2
3
4
[...new Set([1,'1',2,1,1,3])

// 打印结果:
// [1, "1", 2, 3]

网站优化

1、css、js文件压缩

2、dns预解析 加载静态资源快 优先加载
3、双核浏览器 例如 360、搜狗 都是ie和chrome内核 表示渲染页面优先使用weikit

语音面试题

[Q] 什么是高阶组件,你在工作是如何应用的?

高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。高阶组件是参数为组件,返回值为新组件的函数(高阶组件时一个函数,并不是组件)。组件是将 props 转换为 UI,而高阶组件是将组件转换为另一个组件。
请注意,HOC 不会修改传入的组件,也不会使用继承来复制其行为。相反,HOC 通过将组件包装在容器组件中来组成新组件。HOC 是纯函数,没有副作用。
不要在 render 方法中使用 HOC(render中的高阶组件会在每次render时重新mount,重新挂载组件会导致该组件及其所有子组件的状态丢失。)
重要:https://zh-hans.reactjs.org/docs/higher-order-components.html
应用场景: react-redux 的connect,react-router-dom withRouter,全局loading封装,Form.create()对表单form域中的数据进行统一的管理和处理,数据埋点?logger?,
代码复用: 将组件重复部分抽出来,在通过高阶组件拓展,增删改props,达到组件可复用的目的。
条件渲染:控制组件的渲染逻辑,常见:鉴权, 判断页面权限
生命周期捕获/劫持:借助父组件子组件生命周期规则捕获子组件的生命周期,常见case:打点。

高阶组件其实是装饰者模式在react中的一种实现,它类似于高阶函数,接收react组件 作为参数,返回一个新的react组件,高阶函数的实现方法有两种,分别是属性代理和反向代理,在工作中属性代理使用的比较多,例如用户权限页面的控制,我们基本上用的属性代理,这个原理其实是跟react-redux中的connect很相似,另一种就是组件的复用。
通常都是有一个函数传入一个组件,然后返回一个被增强的组件,目标就是把通用的业务逻辑抽象到高阶组件上,然后高阶组件通过属性传递给子组件,解决通用代码逻辑复用的问题,但是他会有两个问题,就是如果嵌套层级过深的话,会造成传值的数据流难以维护,重复的属性会被覆盖,还有就是这样子组件的使用权权在高阶组件本身,不够灵活。
反向继承、劫持、渲染劫持、切面编程 有点aop面向编程,有点装饰者设计模式的感觉

useState、useEffects可以代替高阶组件

分为代理方式的高阶组件;继承方式的高阶组件

[Q] 展示组件(Presentational component)和容器组件(Container component)之间有何不同?

1、有状态【Stateful】和 无状态【Stateless】:
容器组件倾向于有状态,展示组件倾向于无状态,这不是硬性规定,因为容器组件和展示组件都可以是有状态的。
2、类【Classes】 和 函数【Functions】
3、纯粹【Pure】 和 不纯粹 【Impure】:
纯粹:输入什么就输出什么,不会再其中做相应的变动。
不管是展示组件还是容器组件都会有上面的二分特性。在我看来,展示组件往往是没有状态的纯函数,而容器组件往往是有状态的纯类。
这里把素材库的例子聚一下,中间card这块就是纯函数组件,可以视频、音频、图片多处复用解耦了逻辑和页面展示,取值、属性、方法回调什么的。

[Q] React中Refs的作用是什么?

http://react.html.cn/docs/refs-and-the-dom.html

三种创建方式:
1、直接指定ref = ‘xxx’ ref 是一个字符串,react不推荐使用了
2、通过React.createRef()创建,创建的ref 有个 current属性,函数组件如果需要使用,需要使用React.forwardRef 转发一下ref。hoc组件传递ref也存在同样的问题,需要使用React.forwardRef 转发
3、通过函数 ref={input => this.userName = input} ,获取值需要使用 this.userName.value

  • 当 ref 属性被用于一个普通的 HTML 元素时,React.createRef() 将接收底层 DOM 元素作为它的 current 属性以创建 ref 。
  • 当 ref 属性被用于一个自定义类组件时,ref 对象将接收该组件已挂载的实例作为它的 current 。
  • 你不能在函数式组件上使用 ref 属性,因为它们没有实例。如果要用就用React.forwardRef转发一下ref(…这里放函数组件)

在非受控组件中使用,获取真实dom元素的值,或者获取焦点、控制video、audio的播放状态、或者是一些动画

useRef()?

[Q] 组件的状态(state)和属性(props)之间有何不同?

共同点: props 和 state 都属于 react 数据接口,他们都可以决定组件的行为和状态
不同点:
props 属于对外接口,state 属于对内接口,props 主要是父组件传递参数配置该组件,组件内部无法控制也无法修改。想要修改父组件的数据只能通过回调函数的形式(即在子组件中调用父组件的方法)。
state 主要用于保存,控制,修改组件自身状态,你可以理解state是一个局部的,只能被组件自身控制的数据源。不能被外界获取和修改。我们可以通过this.setState更新数据,setState导致组件重新渲染。
总之props是让外部对组件进行配置,state是让组件控制自己的状态
工作中:没有state的组件叫无状态组件,反之有状态组件,工作中尽量多无状态组件,尽量少写有状态组件,增加代码的可维护性和复用性。

[Q] React context 是什么?有什么用?

1、React Context优点:能够让数据在组件树中传递,基于树形结构共享数据的方式,在某个节点组件开启提供context后,所有后代节点组件都可以获取到共享的数据。
而props或者state进行多级数据传递,则数据需要自顶下流经过每一级组件,无法跨级。
2、React Context缺点:
(1)context相当于全局变量, 难以追溯数据源
(2)耦合度高,即不利于组件复用也不利于测试
(3)当 props 改变或者 setState 被调用,生成新的 context,但是 shouldComponentUpdate 返回的 false 会 block 住 context,导致没有更新。
3、React Context使用:可以通过Provider组件的value来传递数据,也可以通过调用react.createContext()来产生context,然后在Consumer组件获得context中的数据。

[Q] 了解Redux么,说一下redux吧

Redux 本身是个状态管理框架,核心或者说目的一句话就能概括, 清晰的描述应用的状态 。
Redux 核心和原则
1.这个应用的状态是一个唯一的状态树
2.状态是只读的, 只能通过 action 来触发修改, 其实实际修改状态的是 reducer
3修改状态只能通过纯函数
Redux 中的概念
1.reducer
reducer 就是实际改变 state 的函数, 在 redux 中, 也只有 reducer 能够改变 state.
根据 redux 的原则, 整个应用只有一个唯一的状态树, 这样, 理论上只要一个 reducer 就够了. 但是, 实际编码时, 如果应用稍具规模, 只有一个 reducer 文件, 显然不利于分模块合作开发, 也不利于代码维护.
所以, reducer 一般是按模块, 或者根据你所使用的框架来组织, 会分散在多个文件夹中. 这时, 可以通过 redux 提供的 API combineReducers 来合并多个 reducer, 形成一个唯一的状态树.
reducer 的使用只要注意 2 点:1、必须是纯函数2、多个 reducer 文件时, 确保每个 reducer 处理不同的 action, 否则可能会出现后面的 reducer 被覆盖的情况
2.state
state 或者说是 store, 其实就是整个应用的状态.
3.action
redux 中的 action 其实就是一个 包含 type 字段的plain object. type 字段决定了要执行哪个 reducer, 其他字段作为 reducer 的参数.
4.action creator
action creator 本质是一个函数, 返回值是一个满足 action 的定义的 plain object. 使用 action creator 的目的就是简化 action 的定义
5.dispatch
 view层通过action来改变store从而改变当前的state,但是action只是一个对象而已,store.dispatch() 就是 view 发出 Action对象的唯一方法。
dispatch的中文意思就是派遣、发送的意思。 即将action发送到store.
6.subscribe
store允许使用 store.subscribe 方法设置监听函数,一旦 state 发生变化, 就自动执行这个函数。
7.middleware
redux 的 middleware 发生在 dispatching an action 和 reaches the reducer 之间. 在这个时间点, 除了可以实现异步操作, 还可以实现 logging等等.

[Q] 什么是受控组件和非受控组件?

由React控制值的表单元素称为受控组件。受控组件的特点:
1.由React通过JSX渲染出来
2.由React控制值的改变,也就是说想要改变元素的值,只能通过React提供的方法来修改,并且只能通过setState来设置受控组件的值。
非受控组件则是将真实数据储存在DOM节点中,由表单元素本身维持自身状态,并根据用户输入进行更新。非受控组件的特点:
1.由元素本身维持自身状态,不需要为状态更新编写数据处理;
2.在React中设置默认值需要设置defaultValue,获取值需要使用ref。
在大多数情况下,建议使用受控组件来实现表单,因为非受控组对比受控组件,有以下一些缺点:
1.无法即时校验(提前校验用户输入);
2.无法根据输入值的变化禁用提交按钮(通过禁用提交来约束用户输入);
3.无法强制输入格式;
4.无法响应值的输入(如即时获得用户的输入来改变其他状态显示);
5.无法动态输入。

[Q] 怎么实现React组件的国际化?

【解答】
一般像多语言国际化这种全局需求,我们可以使用 React 的 context 来全局共享一份相关数据,包含:“当前语言” 和一些通用词汇的语言包。

同时,React 组件本身也可以维护一套自己的语言包,比如时间选择器等,通过获取全局的 “当前语言”,然后通过映射获取指定位置的字符,进行拼接或展示。

在应用过程中,注意几点:

  1. 切换语言是一个低频需求,但语言包可能会比较大,可以按需加载
  2. 限制词语或句子的长度,在语言切换时,长度可能会变化,比如英文单词可能比中文单词长,会影响布局
  3. 注意颜色在不同语言、文化中的差异
  4. 注意日期和货币格式在不同国家和地区的差异显示

[Q] React为什么要搞Hooks,React Hooks帮我们解决了哪些问题?

【解答】

  1. class 组件需要编译成函数,增加了代码体积,hook 用在函数组件不会产生冗余代码
  2. class 复用逻辑需要借助高阶组件,高阶组件嵌套会导致不能直接获取真实 ref,嵌套多个也会比较复杂,hook 可以使用自定义 hook 来复用逻辑
  3. 函数组件中没有 this,可以避免之前事件的绑定 this 问题
  4. hook 的 useEffect 可以将订阅与取消订阅逻辑写在一处,减少重复代码
  5. useEffect useCallback useMemo 第二个参数可以优化性能,只在依赖项改变时才会更新,不用像之前写在 shouldComponentUpdate 中再比较 props 去确认是否更新

上面这部分可以参考,然后自己总结一版

[Q] 创建React动画有哪些方式?

【解答】
创建React动画有以下几种:1.基于定时器或requestAnimationFrame的间隔动画;使用定时器可能会有掉帧问题,而使用requestAnimationFrame则性能较好,完全使用js,不依赖css,帧数跟屏幕刷新率一致,页面运行到后台会自动暂停提高性能。2.基于css3中的animation和transition简单动画;有较高的性能,代码量少,但是只能依赖于css效果,对于复杂动画比较难实现跟控制。3.React动画插件CssTransitionGroup;性能比较好,但限定于入场跟出场场景。4.其他第三方动画库。

[Q] 你对immutable有了解吗?它有什么作用?

1、Immutable实现的原理
Immutable实现的原理是利用结构共享形成持久化数据结构,也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。同时为了避免 deepCopy 把所有节点都复制一遍带来的性能损耗,Immutable使用了结构共享,即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。
2、Immutable的优点
(1)节约内存
JavaScript 中的对象一般是可变的(Mutable),因为使用了引用赋值,新的对象简单的引用了原始对象,改变新的对象将影响到原始对象。为了解决这个问题,一般的做法是使用shallowCopy(浅拷贝)或 deepCopy(深拷贝)来避免被修改,但这样做造成了CPU和内存的浪费。Immutable 可以很好地解决这些问题。
(2)Undo/Redo,Copy/Paste,时间轴等功能容易实现
因为每次数据都是不一样的,只要把这些数据放到一个数组里储存起来,想回退到哪里就拿出对应数据即可,很容易开发出撤销重做这种功能。
(3)并发安全
Immutable Data一旦创建,就不能再被更改,也就不需要并发锁。
3、Immutable使用
(1)与React搭配使用,Immutable简洁高效的判断数据是否变化,提高渲染速度及性能
(2)与Redux/flux搭配使用

【或者】

immutable是实现Immutable data的库,这个库有大量的api可以产生不可被修改的数据,对Immutable对象的任何修改或添加删除操作都会返回一个新的Immutable对象。其实现原理是持久化数据结构,相对于深拷贝而言,通过旧数据来创建新数据的时候,只修改发生变化的节点及其父节点,他们节点保持共享,性能更好,处理速度更快。
跟React配合使用能提高性能:一方面如果在state中保存了一份有深层结构的引用类型的数据,如果没有Immutable.js,则需要深拷贝一份再做修改(Object.assign及react中的setState都是属于浅拷贝)。而用Immutable.js将state中的数据包装一下,不需深拷贝就可以直接修改。另一方面由于修改后返回的是新对象,React.js只需要在oldState.obj === newState.obj这一层就能判断出obj产生了变化,不需要深入obj的深层结构。

[Q] React性能优化有哪些方法?【未完】

【解答】
1.Code Splitting。利用webpack打包分离去重实现动态导入,减少重复性代码块。
React.memo包裹函数组件,进行组件记忆
如果使用Component那就需要shouldComponent进行优化,也可以使用使用pureComponent组件代替Component,前提是组件的状态是值类型,如果是引用类型则会出现异常。
使用react中的lazy,Suspense懒加载组件
使用React Fragments避免额外渲染
不要使用内联函数,不要再render方法中操作状态,render函数应该保证纯净
使用immutable数据,避免很多浑然不知的bug
组件尽可能的拆分解耦,可以让部分组件避免不必要的domdiff;给列表类组件提供key,让domdiff可以实现最少的更新操作;
在constructor中使用bind来绑定this,而不在使用时绑定或减少使用箭头函数,因为constructor只在组件初始化时执行一次,而使用时绑定是每次render都会执行,箭头函数也是如此;按需引入props,避免多余更新;使用css隐藏节点而不是强制加载和卸载;使用React.Fragment减少不必要DOM。
使用事件节流和防抖
使用CND托管文件和资源
使用Reselect避免重复计算样式:
组件少写内联样式
使用css动画代替js动画
transform: rotate(0deg) 开启css加速,屏蔽非标准webkit带来的bug
6.使用SSR,可以在服务端生成html后返回到客户端,使客服端能快速看到完整渲染的页面。
7.使用react性能查看工具了解组件加载到卸载的情况,方便优化代码。

[Q] 描述事件在React中的处理方式?

收集面试题

1、哪些html标签会阻塞浏览器渲染,除了script还有哪些?长轮询的iframe会阻塞渲染

Redux

发表于 2020-03-16 | 更新于: 2020-05-06 | 分类于 Redux

redux是个数据流,并不依赖于react,可以和任何框架结合使用

Redux 三大原则

  • 整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中
  • State 是只读的,惟一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象 使用纯函数来执行修改,为了描述action如何改变state tree ,你需要编写 reducers
  • 单一数据源的设计让React的组件之间的通信更加方便,同时也便于状态的统一管理

Redux

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
let initialState = {
title: {color:'red',text:'标题'},
content: {color:'green',text:'内容'}
}

function reducer(state = initialState, action){
switch (action.type) {
case UPDATE_TITLE_COLOR:
return {
...state,
title:{
...state.title,
color: action.payload
}
}
default:
return state;
break;
}
}

export default function createStore(reducer) {
let state;
let listener;
function getState(){
return state;
}
// action有格式要求,第一个必须是一个纯对象,new Object{},第二个必须要有一个type属性
function dispatch(action){
state = reducer(state,action);
// 发布
listener.forEach(item=>item());
}
// 订阅 当state状态改变的时候能自动去render页面
function subscribe(listener){
listener.push(listener);
return function (){ // 返回取消订阅方法
return listener.filter(item => !item === listener);
}
}
// 这步的目的是,初始reducer中的state是undefined的,派发这个动作,将initiState赋值给reducer中的state
// 所以以后调用 store.dispatch({type:'UPDATE_TITLE_COLOR',payload:'block'});这种时只需要传action的值即可
dispatch({type:'@@REDUX/INIT'});
return {
getState,
dispatch,
subscribe
}
}

let store = createStore(reducer);
store.getState();
store.dispatch({type:'...', payload: '...'});

// 假设这里有一个渲染的方法
renderApp(); // 第一次需要手动渲染
// 订阅 - 监听
let unSubscribe = store.subscribe(renderApp);
// 取消订阅
unSubscribe();

React + Redux

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import React, { Component } from 'react';
import { createStore } from '../redux';
import store from '../store';

export default class Counter extends Component {
constructor(props) {
super(props);
this.state = { value: 0 };
}
componentDidMount() {
// 监听() => this.setState({ value: store.getState() }) ,当reducer中状态改变时执行setStatet方法render页面
this.unsubscribe = store.subscribe(() => this.setState({ value: store.getState() }));
}
componentWillUnmount() {
// 组件卸载时取消监听事件,如果不取消,组件卸载之后this.setState中的this实例没有就会报错
this.unsubscribe();
}
render() {
return (
<div>
<p>{this.state.value}</p>
<button onClick={() => store.dispatch({ type: 'INCREMENT' })}>+</button>
<button onClick={() => store.dispatch({ type: 'DECREMENT' })}>-</button>
</div>
)
}
}

bindActionCreators

[1] actionCreate

() => store.dispatch({ type: 'INCREMENT' }) 如何简化这个写法

1
2
3
4
5
6
7
8
9
function increment(){ // actionCreator  action的创建函数
return{ type: 'INCREMENT' };
}
function decrement(){ // actionCreator action的创建函数
return{ type: 'DECREMENT' };
}

<button onClick={() => store.dispatch(increment())}>+</button>
<button onClick={() => store.dispatch(decrement())}>-</button>

[2] bindActionCreators

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { bindActionCreators } from 'redux';

function increment(){ // actionCreator action的创建函数
return{ type: 'INCREMENT' };
}
function decrement(){ // actionCreator action的创建函数
return{ type: 'DECREMENT' };
}
// bindActionCreators 绑定actionCreators actionCreators跟dispatch自动绑定在一起
increment = bindActionCreators(increment, store.dispatch);
decrement = bindActionCreators(decrement, store.dispatch);

<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>

[3] 手写 bindActionCreator

1
2
3
4
5
6
// bindAcrionCreators.js
export default function (actionCreator,dispatch) {
return function () {
return dispatch(actionCreator());
}
}

[4] 绑定多个 bindActionCreators

1
2
3
4
5
6
7
8
9
10
import { bindActionCreators } from './redux';

function increment(){ // actionCreator action的创建函数
return{ type: 'INCREMENT' };
}
function decrement(){ // actionCreator action的创建函数
return{ type: 'DECREMENT' };
}
let actions = {increment, decrement};
actions = bindActionCreators(actions, store.dispatch);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// bindAcrionCreators.js

// 此方法只能接受一个actionCreator actionCreator是一个函数
function bindActionCreator(actionCreator,dispatch) {
return function () {
return dispatch(actionCreator());
}
}
export default function(actionCreators, dispatch){
if(typeof actionCreators == 'function'){
return bindActionCreator(actionCreators, dispatch);
}
const bundActionCreators = {};
for(let key in actionCreators){
bundActionCreators[key] = bindActionCreator(actionCreators[key],dispatch);
}
return bundActionCreators;
}

[5] bindActionCreators 传参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function bindActionCreator(actionCreator,dispatch) {
return function (...args) {
return dispatch(actionCreator(...args));
}
}
export default function(actionCreators, dispatch){
if(typeof actionCreators == 'function'){
return bindActionCreator(actionCreators, dispatch);
}
const bundActionCreators = {};
for(let key in actionCreators){
bundActionCreators[key] = bindActionCreator(actionCreators[key],dispatch);
}
return bundActionCreators;
}


----

import { bindActionCreators } from './redux';

function increment(payload){
return{ type: 'INCREMENT', payload };
}
function decrement(payload){
return{ type: 'DECREMENT', payload};
}
let actions = {increment, decrement};
actions = bindActionCreators(actions, store.dispatch);

-----

<button onClick={()=>action.increment(2)}>+</button>
<button onClick={()=>action.decrement(3)}>-</button>

combineReducers

[1] 用法

1
2
3
4
5
6
7
import { combineReducers } from 'redux';
import conuter1 from './counter1';
let reducer = combineReducers({
conuter1,
conuter2
...
})

[2] 手写combineReducers

1
2
3
4
5
6
7
8
9
10
function combindReducers(reducers){
// 合并完了之后状态树 key是合并的状态树的属性名,值就是那个reducer
return function(state={},action){
let nextState = {};
for(let key in reducers){
nextState[key] = reducers[key](state[key],action);
}
return nextState;
}
}

react-redux

react组件和redux仓库进行自动关联的一个库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// index.js
import store from './store';
import {Provider} from './react-redux';
ReactDOM.render(<Provider store={store}> ... </Provider>,document.getElementById('root'));


// Counter.js
import {connect} from 'react-redux';

let mapStateToProps = state=>({value:state.counter});
let mapDispatchToProps = ;
export default connect(
mapStateToProps,
mapDispatchToProps
)(Counter)

[1] 手写connect

// react-redux/Provider.js

1
2
3
4
5
6
7
8
9
10
11
import React from 'react';
import Context from './Context';
export default class Provider extends React.Component{
render(){
return (
<Context.Provider value={{store: this.props.store}}>
{this.props.children}
</Context.Provider>
)
}
}

// react-redux/Context

1
2
import React from 'react';
export default React.createContext();

// react-redux/connect.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import React from "react";
import { bindActionCreators } from "../redux";
import Context from "./Context";
export default function(mapStateToProps, mapDispatchToProps) {
return function wrapWithConnect(WrappedComponent) {
return class extends React.Component {
static contextType = Context;
constructor(props, context) {
super(props);
this.state = mapStateToProps(context.store.getState());
}
componentDidMount() {
this.unsubscribe = this.context.store.subscribe(() =>
this.setState(mapStateToProps(this.context.store.getState()))
);
}
shouldComponentUpdate() {
// 判断新老状态是否一样,一样就不更新
if (this.state === mapStateToProps(this.context.store.getState())) {
return false;
}
return true;
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
let actions = bindActionCreators(
mapDispatchToProps,
this.context.store.dispatch
);
return <WrappedComponent {...this.props} {...this.state} {...actions} />;
}
};
};
}

Redux中间件

中间件派发action之后,reducer之前
原理:拦截dispatch,增强dispatch

1
2
3
4
5
6
7
8
9
// 1、缓存老的dispatch
// 2、重写dispatch方法
let oldDispatch = store.dispatch;
store.dispatch = function(action){
console.log('%c prev state','font:bold;color:gray',store.getState());
console.log('%c action', 'font:bold;color:green',action);
oldDispatch(action);
console.log('%c next state','font:bold;color:blue',store.getState());
}

可以理解为面向切面编程,AOP

redux-logger

1
2
3
4
5
6
7
8
9
10
11
12
// getState获取仓库状态
// dispatch用来重新派发动作(这个dispatch就是改造后的最终的dispatch,不是原来的dispatch)
export default function ({getState, dispatch}){
return function(next){ // 代表下一个中间件next
return function(action){ // 动作action
console.log('%c prev state','font:bold;color:gray',getState());
console.log('%c action', 'font:bold;color:green',action);
next(action);
console.log('%c next state','font:bold;color:blue',getState());
}
}
}

中间件的原理和koa是一样的

redux-thunk

1
2
3
4
5
6
7
8
9
10
export default function ({getState, dispatch}){
return function(next){ // 代表下一个中间件next
return function(action){ // 动作action
if(typeof action === 'function'){
return action(dispatch, getState);
}
next(action);
}
}
}

redux-promise

1
2
3
4
5
6
7
8
9
10
export default function ({getState, dispatch}){
return function(next){ // 代表下一个中间件next
return function(action){ // 动作action
if(action.then && typeof action.then === 'function'){
return action.then(dispatch);
}
next(action);∂
}
}
}

compose

[拓展]

1
2
3
4
5
6
7
8
let result = add3(add2(add1('ceshi')));
// 等同于
let result = compose(add3, add2, add1)('ceshi');
function compose(...fns){
if(fns.length === 0) return args=>args;
if(fns.length === 1) return fns[0];
return fns.reduce((a,b) => (...args) => (a(b(...args))));
}

applyMiddleware

1
2
3
// let store = createStore(reducer);
let store = applyMiddleware(promise, thunk, logger)(createStore)(reducer);
export default store;

【applyMiddleware 实现】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function applyMiddleware(...middlewares){
// 返回一个方法 传入 (createStore)
return function (createStore){
// 返回一个方法 传入 (reducer)
return function (reduce){
let store = createStore(reducer);
let dispatch;
let middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
middlewares = middlewares.map(middleware => middleware(middlewareAPI));
// store.dispatch传给logger的next参数,结果再传给thunk的next,结果再传给promise的next参数,最终返回pormise的dispatch方法
dispatch = compose(...middlewares)(store.dispatch);
return {
...store,
dispatch
}
}
}
}

redux-persist

redux数据缓存在内存中,页面一刷新就初始化了,可以使用redux-persist持久化缓存数据

1
npm install redux-persist -D

官网的用法 https://github.com/rt2zz/redux-persist

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
const persistConfig = { // 持久化,持久到哪里?到localStore ajax接口里 localStore.setItem('root:')
key: 'root', // 持久化的key
storage // 存储的位置,默认值
}
// 持久化reducer
const persistedReducer = persistReducer(persistConfig, reducer);
let store = applyMiddleware(promise, thunk, logger)(createStore)(persistedReducer);
// 持久化store
let persistor = persistStore(store);
export {
persistor,
store
}

页面中引入

1
2
3
4
5
6
7
8
9
10
11
import { PersistGate } from 'redux-persist/integration/react'

const App = () => {
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<RootComponent />
</PersistGate>
</Provider>
);
};

redux-saga

npm install redux react-redux redux-saga -D

在 redux-saga 的世界里,Sagas 都用 Generator 函数实现。我们从 Generator 里 yield 纯 JavaScript 对象以表达 Saga 逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'

import reducer from './reducers'
import mySaga from './sagas'

// create the saga middleware
const sagaMiddleware = createSagaMiddleware()
// mount it on the Store
const store = createStore(
reducer,
applyMiddleware(sagaMiddleware)
)

// then run the saga
sagaMiddleware.run(mySaga)

// render the application

派发动作都是同步的,要异步就使用saga

明天验证:yield call 不但可以调用一个返回promise的函数,还可以调用另一个saga
thunk比较

fork

表示开启了一个新的子进程去处理这个请求,
如果调用了fork则代码不会阻塞在此,而是会向下执行。

1
2
3
4
5
6
// let result = yield call(login, username, password); // 这个请求是阻塞的,只有请求login方法返回之后才能执行下面的代码
const task = yield fork(login, username, password); // 开辟的子进程,所以这里也不能直接拿到值了,逻辑可以放到login方法中写
// 这里task代表新的子任务本身,而非login方法返回值

// 取消任务
yield cancel(task);

[中文官网]https://redux-saga-in-chinese.js.org/
http://www.zhufengpeixun.cn/2020/html/63.4.redux-saga.html

参考文章:

http://www.zhufengpeixun.cn/2020/html/63.1.redux.html
http://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_one_basic_usages.html
http://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_two_async_operations.html
http://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_three_react-redux.html

前端面试题4天训练营

发表于 2020-02-28 | 更新于: 2021-05-25 | 分类于 面试题

第一天非常有必要和大家聊的三个话题

1. 大前端时代需要掌握的技术栈

HTML5

https://developer.mozilla.org/zh-CN/docs/Web/Guide/HTML/HTML5

  • 语义化标签类

<article></article>、<footer></footer>、<header></header>

  • 音视频处理

本次升级最大的亮点就是audio、video,新媒体解决方案代替了传统的flash。这可以说是html5最大的亮点。

  • canvas / webGL

  • history API

vue\react框架中做spa单页应用的话会用到路由,路由分为hash和browser,其中browser路由就是利用提供的histor API实现的

  • requestAnimationFrame

https://segmentfault.com/a/1190000020639465?utm_source=tag-newest

  • 地理位置

getCurrentPosition() 方法来获得用户的位置

  • web scoket

………….

CSS3

  • 常规

  • 动画

  • 盒子模型

  • 响应式布局

………….

JavaScript

  • ECMAScript 3/5/6/7/8/9
  • DOM
  • BOM
  • 设计模式
  • 底层原理
    • 堆栈内存
    • 闭包作用域 AO/VO/GO/EC/ECSTACK-
    • 面向对象OOP
    • THI
    • EventLoop
    • 浏览器渲染原理
    • 回流重绘

………….

网络通信层

  • AJAX / Fetch / axios
  • HTTP1.0 / 2.0
  • TCP
  • 跨域处理方案
  • 性能优化

Hybrid或者APP再或者小程序

  • Hybrid
  • uni-app
  • RN
  • Flutter
  • 小程序 MPVUE
  • Weex
  • PWA

…………..

工程化方面

  • webpack
  • git
  • linux / nginx

…………..

全栈方面

  • node
  • express
  • koa2
  • mongodb
  • nuxt.js / next.js

…………..

框架方面

  • Vue
    • 基础知识
    • 核心原理
    • vue-router
    • vue-cli
    • vuex
    • element ui
    • vant
    • cube
    • SSR
    • 优化
  • React
    • 基础知识
    • 核心原理
    • react-router-dom
    • redux
    • react-redux
    • dva
    • umi
    • mobix
    • antd
    • antd pro
    • SSR
    • 优化

游戏方向

可视化或者AI方向

……………

3. BAT/TMD这种大公司到底是怎样面试的

一个问题就知道你会不会CSS了,( ̄ε(# ̄)☆╰╮( ̄▽ ̄///)

  • 什么是标签语义化?

用合理的标签干合适的事情

  • 都有哪些标签,都是啥意思?

有块状标签、有行内标签、有行内块状标签,分部是 如下:

  • 块级标签和行内标签的区别?

行内元素:相邻的行内元素在同一行,行内元素的宽度、高度、内边距的 top/bottom和外边距的top/bottom都是不可改变的,但 padding 和 margin 的 left 和 right 是可以设置的。常见的行内元素有:span、a、br、em、i… 块级元素:独占一行,他们的宽度、高度、内边距和外边距都可控制。常见的块级元素有:table、form、ul li、div、p、h1-6、article、header、footer….行内块元素(inline-block):即融合了行内元素和块级元素的特性,即在一行显示,又能设置宽高。常见的行内块元素有:img、input、button…..

  • 如何转换

  • display除了这几个值还有哪些?

display: table display: flex

  • display:none
    • 让元素隐藏,你可以怎么做
    • display:none和visibility:hidden的区别?
    • opacity的兼容处理? (用 filter)
    • filter(滤镜)还能做哪些事情? (修改所有图片的颜色为黑白: filter: grayscale(100%);)
  • display:flex
    • 项目中你什么时候用到了flex
    • 除了这种方式能居中还有哪些?
    • 响应式布局还可以怎么做?
    • 都有哪些盒子模型

o(╥﹏╥)o

好了,咱们换下一个题….


说一下,你自己感觉自己擅长哪些?

(#^.^#) 没事的,在我们眼里,你擅长的点我比你更擅长,如果不是,没关系,后面还有P7/P8们兜着呢!^_^


说一下,这个需求怎么做?
还有吗 ( ̄▽ ̄)/
还有吗 ( ̄▽ ̄)/
……
那说一下,你感觉这几种方式哪个更好,各自有啥问题……


总之一句话,不把你问“死”,算我这次面试失败!!!

“ 在面试介绍的时候要突出自己精心准备的面试题:在哪些公司呆过,自己擅长哪些技术栈,平时有什么爱好,比较擅长原生js,比较擅长promise设计模式 ”

几道前端经典的面试题

1.掌握盒子水平垂直居中的五大方案

  • 定位: 三种

第一种:需要知道具体宽高,才能用margin left top设置值

1
2
3
4
5
6
7
8
9
10
11
12
body {
position: relative;
}
.box {
width: 100px;
height: 50px;
position: absolute;
top: 50%;
left: 50%;
margin-top: -25px;
margin-left: -50px;
}

第二种:有宽高,不需要考虑具体是多少

1
2
3
4
5
6
7
8
9
10
.box {
width: 100px;
height: 50px;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
}

第三种:可以不设置宽高

1
2
3
4
5
6
7
8
9
.box {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
-webkit-transform: translate(-50%, -50%);
-moz-transform: translate(-50%, -50%);
-ms-transform: translate(-50%, -50%);
}
  • display: flex
1
2
3
4
5
6
/* 父标签 */
body{
display: flex;
justify-content: center;
align-items: center;
}
  • javascript
1
2
3
4
5
6
7
8
let HTML = document.documentElement,
winW = HTML.clientWidth,
winH = HTML.clientHeight,
boxW = box.offsetWidth,
boxH = box.offsetHeight;
box.style.position = "absolute";
box.style.left = (winW - boxW) / 2 + 'px';
box.style.top = (winH - boxH) / 2 + 'px';
  • display: table-cell
1
2
3
4
5
6
7
8
9
10
11
body {
display: table-cell; /* */
vertical-align: middle;
text-align: center;
/* 父级需要固定宽高 百分比不算固定宽高*/
width: 500px;
height: 500px;
}
.box {
display: inline-block;
}

2.关于CSS3中盒模型的几道面试题

  • 标准盒子模型

  • IE盒子模型(怪异盒子模型)

border

  • FLEX盒模型

3.掌握几大经典布局方案

  • 圣杯布局
  • 双飞翼布局

即 左右固定,中间自适应
这里看有道上面的例子

4.移动端响应式布局开发的三大方案

  • media(pc端 + 移动端 用同一套代码时)
  • rem (pc端用px ,移动端用rem)
  • flex
  • vh / vw (相当于百分比,称为百分比布局,vh把视窗高度分为100分,1vh就是1%,vw同理)

课后作业

  1. 使用css,让一个div消失在视野中,发挥想象力 ?
    (display:none; visibility:hidden;它们的区别。透明度opacity又涉及到哪些内容。-margin也可以,-margin涉及到双飞翼出来了)
  2. 请说明z-index的工作原理,适用范围?
  • 文档流
  • 定位

(这个问题主要问的就是文档流,z-index的原理就是建立不同的文档流,建立文档流不同层级,脱离文档流有几种方式?float、定位、transform、animate css3的transfrom动画为什么好,帧性能的优化点,js动画是要随时改变样式的,会引发很多次回流,css3是会回到最后一帧,只引发一次回流)
3. 谈谈你对HTML5的理解?

  1. 如何使一个div里面的文字垂直居中,且该文字的大小根据屏幕大小自适应?

  2. 不考虑其它因素,下面哪种的渲染性能比较高?

1
2
3
4
5
6
7
.box a{
....
}

a{
....
}

css浏览器渲染机制,选择器从右向左查询,第二种只找所有的a,第一种先找所有a,再找box下所有a,渲染了2次,二次筛选。

第二天面试题

  1. 对象(数组)的深克隆和浅克隆(头条)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let obj = {
a: 100,
b: [10, 20, 30],
c: {
x: 10
},
d: /^\d+$/
};
// let obj2 = JSON.parse(JSON.stringify(obj)); => 弊端 正则、函数、new Date() 这些类型的值都转化成了字符串,这种深克隆的处理,大部分是没有问题的。如下:
JSON.stringify({a:/^\d+$/})
//"{"a":{}}"

JSON.stringify({a:function(){}, b:'xxx'})
// "{"b":"xxx"}"

JSON.stringify({a:new Date()})
// "{"a":"2020-02-27T13:04:50.963Z"}"

JSON.stringify(obj)
// "{"a":100,"b":[10,20,30],"c":{"x":10},"d":{}}"
//这里就是把所有的数据都变成字符串,把所有的值重新开辟新的空间,跟原来没有关系。

深克隆

1
2
3
4
5
6
7
8
9
10
11
12
13
//=>深克隆
function deepClone(obj) {
if (typeof obj !== "object") return obj;
if (obj instanceof RegExp) return new RegExp(obj);
if (obj instanceof Date) return new Date(obj);
let cloneObj = new obj.constructor;
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
cloneObj[key] = deepClone(obj[key]);
}
}
return cloneObj;
}
  1. BAT笔试题中几道关于堆栈内存和闭包作用域的题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let a={}, b='0', c=0;  
a[b]='加菲';
a[c]='培训';
console.log(a[b]);
// 对象中属性名不能重复,数字属性名=对象属性名
//衍生=> 对象和数组的区别
---------------------
//example 2
let a={}, b=Symbol('1'), c=Symbol('1');
a[b]='加菲';
a[c]='培训';
console.log(a[b]);
// 衍生 => 自己实现一个Symbol


---------------------
//example 3
let a={}, b={n:'1'}, c={m:'2'};
a[b]='加菲';
a[c]='培训';
console.log(a[b]);
//衍生=> Object.prototype.toString项目中的应用 和 valueOf的区别

如图:
obj里面存{name:’xxx’}转化为”[object Object]”这个字符串,通过toString()转化,可以知道,所有对象,不管存的是什么,toString()之后的结果都是字符串”[object Object]”
堆内存:存储引用类型值的空间;
栈内存:存储基本类型值和执行代码的环境。

1
2
3
4
5
6
var test = (function(i){
return function(){
alert(i*=2);
}
})(2);
test(5);

答案:’4’ alert弹出来的都要转化为字符串
如图:

这里test等于的是一个立即执行函数,所以函数执行,返回一个函数,返回给test的是一个引用地址(十六进制)
在右侧堆中,对象存储是键值对,函数存储的是要执行的代码(以字符串格式),作为对象还存除了prototype length等
闭包不叫内存泄露,只叫保存不销毁

1
2
3
4
5
6
7
8
9
var a=0, b=0;
function A(a){
A=function(b){
alert(a+b++);
};
alert(a++);
}
A(1);
A(2);
1
2
3
4
5
6
7
let i = 10;
console.log(5+i++);
// 15 i 11

let i = 10;
console.log(5+(++i));
// 16 i 11

如图:

‘1’ ‘4’

3.一道关于面向对象面试题所引发的血案(阿里)
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Operator_Precedence

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function Foo() {
getName = function () {
console.log(1);
};
return this;
}
Foo.getName = function () {
console.log(2);
};
Foo.prototype.getName = function () {
console.log(3);
};
var getName = function () {
console.log(4);
};
function getName() {
console.log(5);
}
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();

// 2411233

变量提升:在当前这个执行上下文中,所有代码执行之前,把所有带var 的提前声明,带function 提前声明+定义(赋值)。

4.一道面试题让你彻底掌握JS中的EventLoop(头条)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');

课后作业

第一题:写出下面代码输出的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function A(){
alert(1);
}
function Func() {
A=function(){
alert(2);
};
return this;
}
Func.A=A;
Func.prototype={
A:()=>{
alert(3);
}
};
A();
Fn.A();
Fn().A();
new Fn.A();
new Fn().A();
new new Fn().A();

第二题:写出下面代码输出的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var x=2;
var y={
x:3,
z:(function(x){
this.x*=x;
x+=2;
return function(n){
this.x*=n;
x+=3;
console.log(x);
}
})(x)
};
var m=y.z;
m(4);
y.z(5);
console.log(x, y.x);

第三题:写出下面代码输出的结果

1
2
3
4
var a = ?;
if (a == 1 && a == 2 && a == 3) {
console.log(1);
}

第四题:写出下面代码输出的结果

1
2
3
4
5
6
7
8
9
10
11
12
var x=0,
y=1;
function fn(){
x+=2;
fn=function(y){
console.log(y + (--x));
};
console.log(x, y);
}
fn(3);
fn(4);
console.log(x, y);

第五题:写出下面代码输出的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
setTimeout(() => {
console.log(1);
}, 20);
console.log(2);
setTimeout(() => {
console.log(3);
}, 10);
console.log(4);
console.time('AA');
for (let i = 0; i < 90000000; i++) {
// do soming
}
console.timeEnd('AA'); //=>AA: 79ms 左右
console.log(5);
setTimeout(() => {
console.log(6);
}, 8);
console.log(7);
setTimeout(() => {
console.log(8);
}, 15);
console.log(9);

Single-Spa

发表于 2020-01-09 | 更新于: 2020-01-21

入手demo:https://www.jdon.com/52552

H5-问题集锦

发表于 2019-12-23 | 更新于: 2019-12-24

1.【安卓手机】文本框获取焦点时导致fixed或absolute定位的按钮被手机键盘顶上去

如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//navigator.userAgent.indexOf用来判断浏览器类型
var isAndroid = navigator.userAgent.indexOf('Android') > -1 || navigator.userAgent.indexOf('Adr') > -1;
if (isAndroid){//如果是安卓手机的浏览器
var win_h = $(window).height();//关键代码
$("body").height(win_h);//关键代码
window.addEventListener('resize', function () {
// Document 对象的activeElement 属性返回文档中当前获得焦点的元素。
if (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA') {
if($('.footer').is(':visible')){
$('.footer').hide();
}else{
$('.footer').show();
}
}
});
}

页面中加入如上判断条件,可以防止定位元素上移

新问题:定位元素不上移了,但键盘上方同一位置会出现白色的占位区块

1
position: static;  // 将 fixed 换成 static 即可

微前端

发表于 2019-12-18 | 更新于: 2019-12-24 | 分类于 微前端

微前端就是后端微服务思想在前端的映射。
核心: 拆、合。 先拆后合。

  • 微前端如何在浏览器中落地?
  • single-spa

https://github.com/YataoZhang/my-single-spa/issues/4

这里的 11个状态流程图是微前端内部的状态,理解就行,single-spa 内部也是这个状态。

卸载操作

1
2
3
4
5
6
// vue
vueInstance.$destory();

// react
let el = ReactDOM.findNode(reactInstance);
ReactDOM.unmountComponentAtNode(el);

搜索:【 qiankun 微前端 】
文章: https://juejin.im/post/5d8ed64b51882516622936ff

1
2
3
4
return {...a, ...b}

// 等同于
Object.assign({}, a, b);

React

发表于 2019-12-11 | 更新于: 2020-07-13 | 分类于 React

React

文档 http://www.zhufengpeixun.cn/2020/html/62.1.react-basic.html

React当中的元素就是普通的js对象,它描述了界面上元素的样子,react-dom会在render的时候根据对象的描述生成真实的dom结构。
react元素是构成react应用的最小单位。
react中的元素也就是虚拟dom。

react 源码 github.com => 搜索react => packages => react => src => ReactElement.js

react forceUpdate | react flush

e.preventDefault(); // 阻止默认表单提交事件

jsx

JavaScript+xml 是一种把js和html混合书写的一种语法。
JSX其实只是一种语法糖,最终会通过babeljs转译成createElement语法,以下代码等价。

1
2
3
4
5
6
7
8
9
10
11
ReactDOM.render(
<h1>Hello</h1>,
document.getElementById('root')
);

React.createElement("h1", {
className: "title",
style: {
color: 'red'
}
}, "hello");

this

事件的处理 关于this的处理
一般来说我们希望在回调函数里让this=当前组件

  • 1、使用箭头函数 this就会永远指向类的实例
  • 2、如果用的是普通函数,this = undefined
    • 2.1 可以使用bind
    • 2.2 可以使用匿名函数 ()=>funName()

解决this指针的三种方法

  • this.add.bind(this); 把add方法里面的this指针绑定为组件实例
  • 使用匿名函数 ()=>this.add();
  • 1
    2
    3
    4
    // 给类的实例增加一个add的属性,而这个属性的this绑死为组件的实例
    add = () => {...}
    ...
    onClick={this.add}

ref

ref的用法 + 受控组件、非受控组件

reference = 引用 如果我们希望在代码中获取到React元素渲染到页面上的真实DOM的话

ref用法

1
2
3
this.a = React.createRef(); // {current: null}

<input ref={this.a} />

受控和非受控

非受控组件:指DOM元素的值存在于DOM元素内部,这个值跟react是相互独立的,不受react控制,这被称为非受控组件。
受控:设置 value={this….} onChange来改变value的值

生命周期

旧

父组件包裹子组件 父组件 Counter 子组件 SubCounter
当父组件改变数据时,触发时 父子组件生命周期顺序如下:

Counter shouldComponentUpdate
Counter componentWillUpdate
Counter render
SubCounter componentWillReceiveProps
SubCounter shouldComponentUpdate
SubCounter componentWillUpdate
SubCounter render
SubCounter componentDidUpdate
Counter componentDidUpdate

新

如图 props、state、forceUpdate 改变时都会触发

  • static getDerivedStateFromProps
1
2
3
4
// 从属性对象中获取派发的状态,但返回的对象将会成为新的状态对象,如果不改变状态,则可以返回null
static getDerivedStateFromProps(nextProps,prevState)){

}

getDerivedStateFromProps 的存在只有一个目的:让组件在props变化时更新state。

  • shouldComponentUpdate
1
2
3
shouldComponentUpdate(nextProps, nextState){

}
  • render

  • getSnapshotBeforeUpdate

1
2
3
4
5
6
7
// 在更新前获取DOM的快照
getSnapshotBeforeUpdate(prevProps, prevState){
    return ......xxxxx; // 此返回值会传给componentDidUpdate最后一个参数
}
componentDidUpdate(prevProps,prevState,prevXXXX){

}
  • 更新DOM

  • componentDidUpdate

1
2
3
componentDidUpdate(prevProps, prevState, prevXXXX){

}

状态

状态提升

多个组件需要共享同一个状态的话,就需要把他们的状态提升到他们共同的父组件中。

setState

1
this.setState({});

修改状态;重新render

setState 可能是异步的

出于性能方面的考虑,React 可以将多次的 setState() 调用合并为一次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
constructor(){
this.state = {number:0}
}
add = () =>{
this.setState({number:this.state.number+1});
console.log(this.state.number); // 0
this.setState({number:this.state.number+1});
console.log(this.state.number); // 0
setTimeout( ()=>{
this.setState({number:this.state.number+1});
console.log(this.state.number); // 2
this.setState({number:this.state.number+1});
console.log(this.state.number); // 3
})
}
// 页面上显示 3

源码

dirtyComponents 脏组件,就是组件的状态和界面显示不一致

1
2
3
4
5
6
7
8
// 源码
if( !batchingStrategy.isBatchingUpdates){ // 为 false 则不批量更新,立即处理
batchingStrategy.batchUpdates(enqueusUpdate, component);
return;
}

// 否则 批量更新,存储到dirtyComponent中
dirtyComponent.push(component);
1
2
3
window.trigger = function(event,method){
event.target.component[method].call(event.target.component); // 不是源码
}

实现原理和react一样,在react中会把所有的事件都委托给全局统一实现,通过事件源去区分,去调用对应的方法。
通过事件委托来实现的。

在react中,当你要开启一个事件的时候,当你要执行一个回调函数的时候,它会进入到批量更新状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
window.trigger = function(event,name){
// 当事件函数(即这里的add方法)执行的时候,先置为true,开启批量更新模式
batchingStrategy.isBatchingUpdates = true;
let component = event.target.component;
component[name].bind(component,event);
// 当事件结束的时候(这里指 add执行结束,且此时还未执行setTimeou异步方法,所以前2个皆为 0 0 ) 置为false
batchingStrategy.isBatchingUpdates = false;
// 进行批量更新,把所有的脏组件根据自己的状态和属性重新渲染
batchingStrategy.batchedUpdates();
}
add = () =>{
this.setState({number:this.state.number+1});
console.log(this.state.number); // 0
this.setState({number:this.state.number+1});
console.log(this.state.number); // 0
// setTimeout里面的代码比较特殊,不会走批量更新,会立刻进行更新
setTimeout( ()=>{
this.setState({number:this.state.number+1});
console.log(this.state.number); // 2
this.setState({number:this.state.number+1});
console.log(this.state.number); // 3
})
}

上面line12 - line15 ,将多次传入的对象进行合并处理,以产生一个新的最终的 state 对象,这种合并类似于:

1
2
3
4
5
6
const newState = Object.assign(
{},
state0,
state1,
state2
);

然后再将得到的 “newState” 通过调用 setState 方法进行更新,所以,如果多次调用 setState 方法时传入的对象有相同的 key,那么最后一次调用时所传入的对象的那个 key 的值将成为最终的更新值,在最后一次调用前的值都将被覆盖。

优化window.trigger中的方法 事务

  • 一个所谓的 Transaction 就是将需要执行的 method 使用 wrapper 封装起来,再通过 Transaction 提供的 perform 方法执行
  • 而在 perform 之前,先执行所有 wrapper 中的 initialize 方法;perform 完成之后(即 method 执行后)再执行所有的 close 方法一组 initialize 及 close 方法称为一个 wrapper
     *                       wrappers (injected at creation time)
    *                                      +        +
    *                                      |        |
    *                    +-----------------|--------|--------------+
    *                    |                 v        |              |
    *                    |      +---------------+   |              |
    *                    |   +--|    wrapper1   |---|----+         |
    *                    |   |  +---------------+   v    |         |
    *                    |   |          +-------------+  |         |
    *                    |   |     +----|   wrapper2  |--------+   |
    *                    |   |     |    +-------------+  |     |   |
    *                    |   |     |                     |     |   |
    *                    |   v     v                     v     v   | wrapper
    *                    | +---+ +---+   +---------+   +---+ +---+ | invariants
    * perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained
    * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
    *                    | |   | |   |   |         |   |   | |   | |
    *                    | |   | |   |   |         |   |   | |   | |
    *                    | |   | |   |   |         |   |   | |   | |
    *                    | +---+ +---+   +---------+   +---+ +---+ |
    *                    |  initialize                    close    |
    *                    +-----------------------------------------+
    * 
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Transaction {
constructor(wrapper){
this.wrapper = wrapper;
}
perform(func){
this.wrapper.initialize();
func.call();
this.wrapper.close();
}

}

let transaction = new Transaction({
initialize() {
batchingStrategy.isBatchingUpdates = true;
},
close() {
batchingStrategy.isBatchingUpdates = false;
batchingStrategy.batchedUpdates();
}
});
window.trigger = function(event,name){
let component = event.target.component;
transaction.perform(component[name].bind(component,event));
}

1、异步更新state,将短时间内的多个setState合并成一个
2、为了解决异步更新导致的问题,增加另一种形式的setState:接受一个函数作为参数,在函数中可以得到前一个状态并返回下一个状态

1
2
3
4
5
6
7
add = () =>{
this.setState((state)=>{number: state.number+1});
this.setState((state)=>{number: state.number+1});
this.setState((state)=>{number: state.number+1});
// 这样 页面上会输出3
// 如果想从上一个状态计算下一个状态,需要传递一个函数而非对象
}

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class App extends Component {
state = {
count: 0
};

componentDidMount() {
// 生命周期中调用
this.setState({ count: this.state.count + 1 });
console.log("lifecycle: " + this.state.count); // 0
setTimeout(() => {
// setTimeout中调用
this.setState({ count: this.state.count + 1 });
console.log("setTimeout: " + this.state.count); // 2
}, 0);
document.getElementById("div2").addEventListener("click", this.increment2);
}

increment = () => {
// 合成事件中调用
this.setState({ count: this.state.count + 1 });
console.log("react event: " + this.state.count); // 2=>2
};

increment2 = () => {
// 原生事件中调用
this.setState({ count: this.state.count + 1 });
console.log("dom event: " + this.state.count); // 2=>3
};

render() {
return (
<div className="App">
<h2>couont: {this.state.count}</h2>
<div id="div1" onClick={this.increment}>
click me and count+1
</div>
<div id="div2">click me and count+1</div>
</div>
);
}
}

探讨前,我们先简单了解下react的事件机制:react为了解决跨平台,兼容性问题,自己封装了一套事件机制,代理了原生的事件,像在jsx中常见的onClick、onChange这些都是合成事件。

那么以上4种方式调用setState(),后面紧接着去取最新的state,按之前讲的异步原理,应该是取不到的。然而,setTimeout中调用以及原生事件中调用的话,是可以立马获取到最新的state的。根本原因在于,setState并不是真正意义上的异步操作,它只是模拟了异步的行为。React中会去维护一个标识(isBatchingUpdates),判断是直接更新还是先暂存state进队列。setTimeout以及原生事件都会直接去更新state,因此可以立即得到最新state。而合成事件和React生命周期函数中,是受React控制的,其会将isBatchingUpdates设置为 true,从而走的是类似异步的那一套。

在 setTimeout 中去 setState 并不算是一个单独的场景,它是随着你外层去决定的,因为你可以在合成事件中 setTimeout ,可以在钩子函数中 setTimeout ,也可以在原生事件setTimeout,但是不管是哪个场景下,基于event loop的模型下, setTimeout 中里去 setState 总能拿到最新的state值。

小结:
1、setState 只在合成事件和钩子函数中是“异步”的,在原生事件和 setTimeout 中都是同步的。
2、setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。
3、setState 的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次 setState , setState 的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时 setState 多个不同的值,在更新时会对其进行合并批量更新。

小测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class App extends React.Component {
state = { val: 0 }

componentDidMount() {
this.setState((state) => {
console.log(this.state.val)
return { val: this.state.val + 1 }
})

this.setState((state) => {
console.log(this.state.val)
return { val: this.state.val + 1 }
})

setTimeout(_ => {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val);

this.setState({ val: this.state.val + 1 })
console.log(this.state.val)
}, 0)
}

render() {
return <div>{this.state.val}</div>
}
}
export default App;

0 0 2 3
【提问】line11 为什么在使用函数式 setState 进行状态更新后,在后一个里面还是不能通过 this.state.age 拿到最新的值?
【回答】源码这样解释,执行第二个 setState 里面的函数时,由第一个 setState 所产生的最新的 state 并没有合并到 this 对象上面去,所以此时通过 this 获取不到最新的状态,故而拿到的 this.state.val 的值为 0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class App extends React.Component {
state = { val: 0 }

componentDidMount() {
this.setState((state) => {
console.log(this.state.val)
return { val: state.val + 1 }
})

this.setState((state) => {
console.log(this.state.val)
return { val: state.val + 1 }
})

setTimeout(_ => {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val);

this.setState({ val: this.state.val + 1 })
console.log(this.state.val)
}, 0)
}

render() {
return <div>{this.state.val}</div>
}
}
export default App;

0 0 3 4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class App extends React.Component {
state = { val: 0 }

componentDidMount() {
this.setState((state) => {
console.log(state.val)
return { val: state.val + 1 }
})

this.setState((state) => {
console.log(state.val)
return { val: state.val + 1 }
})

setTimeout(_ => {
this.setState({ val: this.state.val + 1 })
console.log(this.state.val);

this.setState({ val: this.state.val + 1 })
console.log(this.state.val)
}, 0)
}

render() {
return <div>{this.state.val}</div>
}
}
export default App;

0 1 3 4

Context

Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。

1
2
let Context = React.createContext(); // 返回一个context实例
// Context.Provider 负责提供数据 Context.Consumer 负责获取数据

https://zh-hans.reactjs.org/docs/context.html

从示例 http://react.html.cn/docs/context.html#%E5%8A%A8%E6%80%81-context 可以看出没有被包裹的改变同一个值,没有改变

注意下告诫
http://react.html.cn/docs/context.html#%E5%91%8A%E8%AF%AB

等学了react-redux的 之后可以在反过来看看Context中的 childContextTypes getChildContext 等

PureComponent

  • 当一个组件的props或state变更,React 会将最新返回的元素与之前渲染的元素进行对比,以此决定是否有必要更新真实的 DOM,当它们不相同时 React 会更新该 DOM。
  • 如果渲染的组件非常多时可以通过覆盖生命周期方法 shouldComponentUpdate 来进行优化
  • shouldComponentUpdate 方法会在重新渲染前被触发。其默认实现是返回 true,如果组件不需要更新,可以在shouldComponentUpdate中返回 false 来跳过整个渲染过程。其包括该组件的 render 调用以及之后的操作
  • PureComponent通过prop和state的浅比较来实现shouldComponentUpdate
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import React, { Component } from 'react';

class Title extends Component {
render() {
console.log("render Title");
return (
<div>
{this.props.title}
</div>
)
}
}

class Counter extends Component{
render() {
console.log("render Counter");
return (
<div>
{this.props.number}
</div>
)
}
}

class App extends Component {
constructor(props){
super(props);
this.state = {
title: '这里是标题',
number:0
}
this.inputRef = React.createRef();
}
add = ()=>{
this.setState({
number: this.state.number + parseInt(this.inputRef.current.value)
})
}
render() {
console.log("render App");
return (
<div className="App">
<Title title={this.state.title}></Title>
<Counter number={this.state.number}></Counter>
<input ref={this.inputRef} />
<button onClick={this.add}>+</button>
</div>
);
}
}

export default App;

App 组件包含2个子组件 Title 组件 和 Counter 组件
当点击 + 时,三个组件都刷新,当父组件的值改变,子组件也render

1
2
3
render App
render Title
render Counter

问题

  • 当改变数字的时候 title 没有改变,<Title> 组件不应该更新
  • 当 + 0,实际数字没有改变,3个组件都不应该更新

优化

所有组件都继承 PureComponent

这个是React原生的 import React, { Component, PureComponent } from 'react'

测试

input中输入 0,3个组件都不render
input中输入 1

1
2
render App
render Counter

手写PureComponent

浅比较方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 浅比较 比较obj1 和 obj2 是否相等,相等返回true,不相等返回false,只比较第一层
function shallowEqual(obj1,obj2){
if(obj1 === obj2){
return true;
}
if(typeof obj1 != 'object' || obj1 === null ||typeof obj2 != 'object' || obj2 === null ){
return false;
}
let keys1 = Object.keys(obj1);
let keys2 = Object.keys(obj2);
if(keys1.length != keys2.length){
return false;
}
for(let key of keys1){
if(!obj2.hasOwnProperty(key) || obj1[key] !== obj2[key]){
console.log("line16~")
return false;
}
}
return true;
}

let obj1 = {name:'jiafei'};
let obj2 = {name:'jiafei'};
console.log(shallowEqual(obj1,obj2)); // 返回 true

let obj1 = {attr:{name:'jiafei'}};
let obj2 = {attr:{name:'jiafei'}};
console.log(shallowEqual(obj1,obj2));
// 返回 line16~ false
// 这里走到了line16 {name:'jiafei'}这个对象引用的地址不一样,所以不相等,这里只比较了第一层

改成深比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
for(let key of keys1){
// if(!obj2.hasOwnProperty(key) || obj1[key] !== obj2[key]){
// console.log("line30~")
// return false;
// }
// 递归改成深比较
if(obj2.hasOwnProperty(key)){
if(obj1[key] != obj2[key]){
if( typeof obj1[key] === 'object' && typeof obj2[key] === 'object'){
return shallowEqual(obj1[key],obj2[key])
}
}
}else{
return false;
}
}
let obj1 = {attr:{name:'jiafei'}};
let obj2 = {attr:{name:'jiafei'}};
console.log(shallowEqual(obj1,obj2));
// 返回 true

PureComponent 要注意的问题

当state中

1
2
3
4
this.state = {
title: '这里是标题',
number:{count: 0}
}

时,改变state时这样写

1
2
3
4
this.state.number.count = this.state.number.count + parseInt(this.inputRef.current.value);
this.setState({
number: this.state.number
})

那么,改变input中的值3个组件都不render,因为

1
2
3
if(!obj2.hasOwnProperty(key) || obj1[key] !== obj2[key]){
return false;
}

这里{count: ...} 这个对象指向的引用地址没有改变
可以这样改

1
2
3
4
this.state.number.count = this.state.number.count + parseInt(this.inputRef.current.value);
this.setState({
number: {...this.state.number,count: this.state.number.count}
})

每次都创建新的number对象,所以能够更新

函数组件的PureComponent

当<Title>组件变成函数组件时

1
2
3
4
5
6
7
8
function Title(props){
console.log("render Title Func");
return (
<div>
{props.title}
</div>
)
}

可以看到每次改变input中的值,都会刷新 render Title Func

解决:

1
2
3
4
5
6
7
8
9
function Title(props){
console.log("render Title Func");
return (
<div>
{props.title}
</div>
)
}
Title = React.memo(Title);

memo的原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function memo(Func){
return class extends PureComponent{
render(){
return <Func {...this.props} />
}
}
}
function memo(Func){
return class extends PureComponent{
render(){
return Func(this.props);
}
}
}

额外补充

1
2
3
4
5
6
7
8
9
let null1 = null;
let null2 = null;
console.log(null1 === null2);
// true

let obj1 = {};
let obj2 = {};
console.log(obj1 === obj2);
// false

react组件通信

1、属性传递;2、context redux内部也是context实现的。

看下react中文文档 http://react.html.cn/docs/context.html 这里的例子可以看官网的 好好看看官方的这个例子

代码拆分 http://react.html.cn/docs/code-splitting.html

高阶组件

  • 高阶组件就是一个函数,传给它一个组件,它返回一个新的组件
  • 高阶组件的作用其实就是为了组件之间的代码复用

示例一

下面有2个逻辑相似的组件 如下
UserNameInput.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React, { Component } from 'react';
class UserNameInput extends Component {
constructor(props){
super(props);
this.state = {
value: ''
}
}
componentDidMount() {
this.setState({
value: localStorage.getItem('username')
})
}
render() {
return (
<div>
{this.state.value}
</div>
);
}
}
export default UserNameInput;

EmailInput.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React, { Component } from 'react';
class EmailInput extends Component {
constructor(props){
super(props);
this.state = {
value: ''
}
}
componentDidMount() {
this.setState({
value: localStorage.getItem('email')
})
}
render() {
return (
<div>
{this.state.value}
</div>
);
}
}
export default EmailInput;

这2个组件都是获取一个值然后展示,其中line3 ~ 13 的逻辑可以复用

将公共逻辑部分封装成一个高阶组件
withLocal.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React from 'react';

export default function(Component,name){
return class extends React.Component{
constructor(props){
super(props);
this.state = {
value: ''
}
}
componentDidMount() {
this.setState({
value: localStorage.getItem(name)
})
}
render(){
return <Component {...this.props} value={this.state.value} />;
}
}
}

那么原来的组件,还改变成如下:
UserNameInput.js

1
2
3
4
5
6
7
8
9
10
11
12
13
import React, { Component } from 'react';
import withLocal from './withLocal';

class UserNameInput extends Component {
render() {
return (
<div>
{this.props.value}
</div>
);
}
}
export default withLocal(UserNameInput,'username');

EmailInput.js

1
2
3
4
5
6
7
8
9
10
11
12
13
import React, { Component } from 'react';
import withLocal from './withLocal';

class EmailInput extends Component {
render() {
return (
<div>
{this.props.value}
</div>
);
}
}
export default withLocal(EmailInput,'email');

可以看到页面上还是能显示之前localStore设置的数据

示例二

升级,从本地获取到英文名字再从服务器端拿取数据显示对应的中文名字

withAjax.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React from 'react';

export default function(Component){
return class extends React.Component{
constructor(props){
super(props);
this.state = {
valueAjax: ''
}
}
componentDidMount() {
fetch('http://localhost:3000/translate.json').then(response=>response.json()).then(result=>{
this.setState({valueAjax:result[this.props.value]})
}).catch(err=>{ // 错误捕获
console.log(err);
})
}
render(){
return <Component value={this.state.valueAjax} />;
}
}
}

withLocal.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React from 'react';

export default function(Component,name){
return class extends React.Component{
constructor(props){
super(props);
this.state = {
value: ''
}
}
componentDidMount() {
this.setState({
value: localStorage.getItem(name)
})
}
render(){
return <Component {...this.props} value={this.state.value} />;
}
}
}

UserNameInput.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, { Component } from 'react';
import withLocal from './withLocal';
import withAjax from './withAjax';

class UserNameInput extends Component {
render() {
return (
<div>
{this.props.value}
</div>
);
}
}
let UserNameInputWithAjax = withAjax(UserNameInput);
let UserNameInputWithLocal = withLocal(UserNameInputWithAjax,'username');
export default UserNameInputWithLocal;

顺序:withLocal (携带本地的英文名)=> withAjax (从服务器请求数据传递)=> UserNameInput

高阶组件一般就嵌套一层,超过二层就太复杂了。之后学到的hooks能解决这个问题(高阶组件的嵌套问题)
高阶组件应用场景:比如 react-redux、react路由、路由权限

  • react 高阶组件是一种react复用代码的封装、抽取方式,高阶组件是一个函数,接收组件并封装
    1、基于属性代理:操作组件的props
    2、基于反向继承:拦截生命周期、state、渲染过程
  • 使用高阶组件
    1、@修饰符
    2、直接调用函数
    https://blog.csdn.net/qq_29590623/article/details/88560805

render props

和高阶组件一样,都是解决逻辑复用问题,传值方式不同,本质没有区别。
一个组件 它的子组件是个函数。

Context.Consumer 实质上就是一个 render props

React.Fragment

片段(fragments) 可以让你将子元素列表添加到一个分组中,并且不会在DOM 中增加额外节点。

http://react.html.cn/docs/fragments.html

插槽Portal

Portals 提供了一种很好的方法,将子节点渲染到父组件 DOM 层次结构之外的 DOM 节点。

1
ReactDOM.createPortal(child, container)

第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 片段(fragment)
第二个参数(container)则是一个 DOM 元素

入口文件index.html 增加一个放置插槽的dom元素位置

1
<div id="modal-root"></div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import React,{Component} from 'react';
import ReactDOM from 'react-dom';
import './modal.css';

class Modal extends Component{
constructor() {
super();
this.modal=document.querySelector('#modal-root');
}
render() {
return ReactDOM.createPortal(this.props.children,this.modal);
}
}
class Page extends Component{
constructor() {
super();
this.state={show:false};
}
handleClick=() => {
this.setState({show:!this.state.show});
}
render() {
return (
<div>
<button onClick={this.handleClick}>显示模态窗口</button>
{
this.state.show&&
<Modal>
<div id="modal" className="modal">
<div className="modal-content" id="modal-content">
内容
<button onClick={this.handleClick}>关闭</button>
</div>
</div>
</Modal>
}
</div>
)
}
}

modal.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
.modal{
position: fixed;
left:0;
top:0;
right:0;
bottom:0;
background: rgba(0,0,0,.5);
display: block;
}

@keyframes zoom{
from{transform:scale(0);}
to{transform:scale(1);}
}

.modal .modal-content{
width:50%;
height:50%;
background: white;
border-radius: 10px;
margin:100px auto;
display:flex;
flex-direction: row;
justify-content: center;
align-items: center;
animation: zoom .6s;
}

这里的样式可以看一下,flex 、animation

React.lazy

https://zh-hans.reactjs.org/docs/code-splitting.html#reactlazy

错误边界

https://zh-hans.reactjs.org/docs/error-boundaries.html

长列表优化

未完待续

相关文章:

https://juejin.im/post/5b6f1800f265da282d45a79a
https://juejin.im/post/5aca20c96fb9a028d700e1ce
https://zh-hans.reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html

React 项目版本升级

发表于 2019-12-09 | 更新于: 2020-04-30 | 分类于 React

前言

当前项目版本 react: “16.2.0” 、dva: “2.3.1” 、 roadhog: “2.4.2”、 babel: 6.x
最新版本 react v16.12.0
暂时决定 升级至 react v16.9.0、babel v7.7.7

16.3.0生命周期更新、16.9.0 新增Hooks

已知:

  • 生命周期版本更改,废弃了 componentWillMount(可以将业务代码改至 componentDidMount)、componentWillReceiveProps(该部分牵扯大量业务逻辑状态更改,检索出来有约50处使用,耗时)、componentWillUpdate(当前项目中未使用,可忽略)

参考资料:
https://github.com/nanyang24/blog/issues/92
https://zh-hans.reactjs.org/blog/2019/08/08/react-v16.9.0.html

步骤

升级 react | react-dom

1、npm install react@16.9.0 react-dom@16.9.0 --save

2、启动项目 无异常, 使用 React.Lazy、Supense 来进行 代码分割,(主要目的)

3、按照 lazy、Suspense 的格式尝试修改了其中一个组件引入的多个弹层之后,代码报错,如下:

4、调试发现,如下方式引入的 lazy、Suspense 并没有找到源码地址,即便 react 的版本为16.9.0

1
import React,{lazy, Suspense} from 'react';

5、新起一个项目 create-react-app projectName , 同样引入如上代码 发现 /node_modules/@types/react/index.d.ts 中包含Suspense 且 @types/react 版本为 v16.9.16,同理发现公司项目中的版本为 v16.3.0

6、升级 npm install @types/react,默认升级到 v16.9.16,再运行项目,可以正常访问,且 打包查看,之前有一个 5596k大小的包,现在是 5222k,可见业务逻辑代码分割成功,现继续更改代码中组件的引入方式。
5596kk => 5222k => 5176k => 5036k 将项目中所有外部引入的 Modal 都使用lazy加载,可见 还是有 5M 多…. (接下来可能需要从代码业务层入手)

7、warning

1
Warning: React version not specified in eslint-plugin-react settings. See https://github.com/yannickcr/eslint-plugin-reat#configuration.

8、旧的生命周期方法名替换 npx react-codemod rename-unsafe-lifecycles 失败

1
2
3
4
175 errors
0 unmodified
817 skipped
9 ok

9、在路由组件的最外层包裹一个错误边界组件。

10、 《使用开发者工具中的分析器对组件进行分析》 看不懂…

https://zh-hans.reactjs.org/docs/optimizing-performance.html
中文版 https://juejin.im/post/5ba1f995f265da0a972e1657#heading-0

roadhog analyze

roadhog build --analyze

如上图所示发现是版本问题,那就先升级babel

升级babel

根目录下新增 .babelrc 文件

1
2
3
{
"plugins": ["transform-decorators-legacy"]
}

以下是之前一系列的babel插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
devDependencies:{
"babel-plugin-dva-hmr": "^0.4.1",
"babel-plugin-transform-remove-console": "^6.9.4",
}

dependencies:{
"babel-cli": "^6.26.0",
"babel-plugin-import": "^1.13.0",
"babel-plugin-transform-decorators-legacy": "^1.3.5",
"babel-polyfill": "^6.26.0",
"babel-preset-es2015": "^6.24.1",
"babel-preset-stage-2": "^6.24.1",
"babel-runtime": "^6.26.0",
}

官方提供了一个工具babel-upgrade,对于已有的项目,只需要运行这样一行命令就可以了:

1
npx babel-upgrade --write --install

升级如下:

.babelrc

1
2
3
4
5
6
7
8
9
10
{
"plugins": [
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
]
]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
dependencies:{
"@babel/cli": "^7.0.0",
"@babel/core": "^7.0.0",
"@babel/plugin-proposal-class-properties": "^7.0.0",
"@babel/plugin-proposal-decorators": "^7.0.0",
"@babel/plugin-proposal-export-namespace-from": "^7.0.0",
"@babel/plugin-proposal-function-sent": "^7.0.0",
"@babel/plugin-proposal-json-strings": "^7.0.0",
"@babel/plugin-proposal-numeric-separator": "^7.0.0",
"@babel/plugin-proposal-throw-expressions": "^7.0.0",
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/plugin-syntax-import-meta": "^7.0.0",
"@babel/polyfill": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@babel/runtime-corejs2": "^7.0.0",

"babel-plugin-import": "^1.13.0",
}

devDependencies:{
"@babel/core": "^7.0.0",

"babel-plugin-dva-hmr": "^0.4.1",
"babel-plugin-transform-remove-console": "^6.9.4",
}

继续分析—

1
roadhog build --analyze

还是失败

ES6 之 阅读理解

发表于 2019-11-21 | 更新于: 2020-05-06 | 分类于 个人

17. Iterator

https://jiafei2333.github.io/2019/09/16/Generator/

是一种接口,为各种不同的数据结构(Array、Object、Map、Set 等)提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作。
ES6 创造了一种新的遍历命令for…of循环,Iterator 接口主要供for…of消费。

一刷:
http://es6.ruanyifeng.com/#docs/iterator

18. Generator

https://jiafei2333.github.io/2019/09/16/Generator/

调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的next方法,就会返回一个有着value和done两个属性的对象。

1、一个对象如果要具备可被for…of循环调用的 Iterator 接口,就必须在Symbol.iterator的属性上部署遍历器生成方法(原型链上的对象具有该方法也可)。

1/2刷:
http://es6.ruanyifeng.com/#docs/generator

19. Generator 函数的异步应用

http://es6.ruanyifeng.com/#docs/generator-async

一刷,没怎么看明白,可以二刷继续研究,评论也看看。

20.async 函数

http://es6.ruanyifeng.com/#docs/async

async 函数就是 Generator 函数的语法糖。

1、Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。Generator 函数,需要调用next方法,或者用co模块,才能真正执行,得到最后结果。
2、任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行。
3、Promise 的回调函数( .then )都存放在一个单独的栈里面,而 await 是暂停执行,所以不完全一样。

一刷

12. Symbol

https://jiafei2333.github.io/2019/09/04/Reflect-Symbol/

Symbol函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的Symbol函数的返回值是不相等的。

1、Singleton 模式指的是调用一个类,任何时候返回的都是同一个实例。

http://es6.ruanyifeng.com/#docs/symbol
一刷:内置的 Symbol 值 这块内容太琐碎了,没怎么记得。

14. Proxy

https://jiafei2333.github.io/2019/09/04/Object.defineProperty-Proxy/

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

1、ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。
2、注意,要使得Proxy起作用,必须针对Proxy实例(上例是proxy对象)进行操作,而不是针对目标对象(上例是空对象)进行操作。
3、虽然 Proxy 可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下,也无法保证与目标对象的行为一致。主要原因就是在 Proxy 代理的情况下,目标对象内部的this关键字会指向 Proxy 代理。
4、《this 问题》 这个可以看看,其他的很多都是Proxy的属性
5、Proxy web服务的客户端。

一刷:
http://es6.ruanyifeng.com/#docs/proxy

15. Reflect

Reflect对象与Proxy对象一样,也是 ES6 为了操作对象而提供的新 API。

一刷:
http://es6.ruanyifeng.com/#docs/reflect

10. 11. 对象

10.1. 对象的拓展

1.1 属性的遍历

  • (1)for…in
  • (2)Object.keys(obj)
  • (3)Object.getOwnPropertyNames(obj)
  • (4)Object.getOwnPropertySymbols(obj)
  • (5)Reflect.ownKeys(obj)
    Reflect.ownKeys返回一个数组,包含对象自身的所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。

10.1.2 super

我们知道,this关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字super,指向当前对象的原型对象。

JavaScript 引擎内部,super.foo等同于Object.getPrototypeOf(this).foo(属性)或Object.getPrototypeOf(this).foo.call(this)(方法)。 ??
Object.getPrototypeOf() 返回指定对象的原型。

11.2. 对象的新增方法

11.2.1 Object.is()

比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。

1
2
3
4
5
6
7
Object.is({}, {}); // false

+0 === -0 //true
NaN === NaN // false

Object.is(+0, -0) // false
Object.is(NaN, NaN) // true

11.2.2 Object.assign()

Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。

1
Object.assign(target, source1, source2);
  • Object.assign拷贝的属性是有限制的,只拷贝源对象的自身属性(不拷贝继承属性),也不拷贝不可枚举的属性(enumerable: false)。属性名为 Symbol 值的属性,也会被Object.assign拷贝。

11.2.3 proto属性,Object.setPrototypeOf(),Object.getPrototypeOf()

https://jiafei2333.github.io/2019/09/09/Prototype-Extends/

11.2.4 Object.keys(),Object.values(),Object.entries() 搭配 for…of 使用

也都可以搭配数组 for (let index of ['a', 'b'].keys())

一刷:
http://es6.ruanyifeng.com/#docs/object
http://es6.ruanyifeng.com/#docs/object-methods

9. 数组

9.1. 拓展运算符的应用

http://es6.ruanyifeng.com/#docs/array#%E6%89%A9%E5%B1%95%E8%BF%90%E7%AE%97%E7%AC%A6%E7%9A%84%E5%BA%94%E7%94%A8

扩展运算符内部调用的是数据结构的 Iterator 接口,因此只要具有 Iterator 接口的对象,都可以使用扩展运算符,比如 Map 结构。
Generator 函数运行后,返回一个遍历器对象,因此也可以使用扩展运算符。

9.2. Array.from()

Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。

任何有 length 属性的对象,都可以通过 Array.from 方法转为数组,而此时扩展运算符就无法转换。

一刷:
http://es6.ruanyifeng.com/#docs/array

8. 函数的拓展

8.1. 函数的 length 属性

函数的length属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length属性将失真,这个个数不算。

8.2. 作用域

** http://es6.ruanyifeng.com/#docs/function#%E4%BD%9C%E7%94%A8%E5%9F%9F

8.3. 箭头函数

  • (1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。

箭头函数可以让this指向固定化,这种特性很有利于封装回调函数。下面是一个例子,DOM 事件的回调函数封装在一个对象里面。

1
2
3
4
5
6
7
8
9
10
11
12
var handler = {
id: '123456',

init: function() {
document.addEventListener('click',
event => this.doSomething(event.type), false);
},

doSomething: function(type) {
console.log('Handling ' + type + ' for ' + this.id);
}
};

上面代码的init方法中,使用了箭头函数,这导致这个箭头函数里面的this,总是指向handler对象。否则,回调函数运行时,this.doSomething这一行会报错,因为此时this指向document对象。

this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。

8.4. 尾调用优化

我们知道,函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到A,B的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。

尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。

一刷:
http://es6.ruanyifeng.com/#docs/function

13. Set和Map数据结构

Set

https://es6.ruanyifeng.com/#docs/set-map

Set本身是一个构造函数,用来生成Set数据结构。它类似于数组,但是成员的值都是唯一的,没有重复的值。

【去重】

1
2
3
4
5
let s = new Set([1,2,3,4,5,4,3,2,1]);
console.log([...s]);

let s = new Set([1,2,3,4,5,4,3,2,1]);
console.log(Array.from(s));

【遍历】
keys()、values()、entries() 搭配 for…of

Set 结构的实例默认可遍历,它的默认遍历器生成函数就是它的values方法。
Set 结构的实例与数组一样,也拥有forEach方法,用于对每个成员执行某种操作,没有返回值。
数组的map和filter方法也可以间接用于 Set 了。

WeakSet

WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。 ES6 规定 WeakSet 不可遍历。

  • WeakSet 的成员只能是对象,而不能是其他类型的值。
  • WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。

Map

Map也是构造函数,它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。

创建的由来不错:
https://es6.ruanyifeng.com/#docs/set-map#%E5%90%AB%E4%B9%89%E5%92%8C%E5%9F%BA%E6%9C%AC%E7%94%A8%E6%B3%95

草稿

发表于 2019-11-20 | 更新于: 2020-05-25 | 分类于 个人

【暂存死区】

【没有导致state的值发生变化的this.setState()会导致重新渲染,原理是什么?是只要调setState了,没有做shouldComponentUpdate之类的判断就回导致渲染吗?】

【长列表】

名言警句

发表于 2019-11-15 | 更新于: 2020-06-25 | 分类于 个人
  • 2019年11月15日 晴
    你对生活付出多少,它就会让你过什么样的生活。

  • 2019年11月20日 晴 冷
    哪怕是房梁和屋檐把生活压的再低,但还是有另外一片生活的希望。

  • 2019年11月21日 晴
    命运所有的馈赠,早已暗中标了价格。

  • 2019年12月09日 晴
    每个人都在负重前行。

  • 2019年12月18日 冷
    《情商》

  • 2020年05月25日 大雨
    三十而立

  • 2020年06月25日 大雨 - 晴
    放弃很容易,坚持一定很酷!
    只有不断学习,才有可能在这个抛弃你连说都不说一声的时代,慢慢进步。

Webpack 优化篇

发表于 2019-11-14 | 更新于: 2020-08-06 | 分类于 Webpack

前言

1
2
npm init -y
npm install webpack webpack-cli webpack-dev-server html-webpack-plugin css-loader style-loader mini-css-extract-plugin babel-loader @babel/core @babel/preset-env @babel/preset-react file-loader --save-dev
1
npm install react react-dom --save-dev

package.json

1
2
3
4
5
"scripts": {
"dev": "webpack-dev-server --env=development",
"dev:build": "webpack --env=development",
"build": "webpack --env=production"
},

新建webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");
module.exports = mode => {
return {
mode: mode, // 这个env就是package.json script里面=后面传过来的值
entry: "./src/index.js", // 入口
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist") // 出口
},
module: {
rules: [
{
test: /\.(png|jpe?g|gif)$/,
use: "file-loader"
},
{
test: /\.js$/,
use: {
loader: "babel-loader",
options:{ // .babelrc 也可以写在这个文件中
presets:[
"@babel/preset-env",
"@babel/preset-react"
]
}
}
},
{
test: /\.css$/,
use: [
mode !== "development"
? MiniCssExtractPlugin.loader
: "style-loader",
"css-loader"
]
}
]
},
plugins: [
new PurgecssPlugin({
paths: glob.sync(`${path.join(__dirname, "src")}/**/*`, { nodir: true }) // 不匹配目录,只匹配文件
}),
mode !== "development" &&
new MiniCssExtractPlugin({
filename: "css/[name].css"
}),
new HtmlWebpackPlugin({
template: "./src/template.html",
filename: "index.html"
})
].filter(Boolean)
};
};

1.删除无用的Css样式

先来看编写的代码

1
2
3
4
import './index.css'
import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(<div>hello</div>,document.getElementById('root'));

index.css

1
2
3
4
5
6
body{
background: red
}
.class1{
background: red
}
1
npm install purgecss-webpack-plugin glob --save-dev

这里的.class1显然是无用的,我们可以搜索src目录下的文件,删除无用的样式

1
2
3
4
5
6
7
8
9
// webpack.config.js
const glob = require('glob'); // 主要功能就是查找匹配的文件
// 主要的作用删除无意义的css,只能配合mini-css-extract-plugin使用
const PurgecssPlugin = require('purgecss-webpack-plugin');

// 需要配合mini-css-extract-plugin插件
mode !== "development" && new PurgecssPlugin({
paths: glob.sync(`${path.join(__dirname, "src")}/**/*`, { nodir: true }) // 不匹配目录,只匹配文件
}),

编译 npm run build
可以看到 dist/css/main.css 中没有.class这个无用的样式

2.图片压缩插件

将打包后的图片进行优化

1
npm install image-webpack-loader --save-dev

在file-loader之前使用压缩图片插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{ // 降低分辨率 清晰度
test: /\.(png|jpe?g|gif)$/,
use: [
{
loader: 'file-loader'
},
mode !== 'development' && {
// 可以在使用file-loader之前 对图片进行压缩
loader: 'image-webpack-loader',
options: { // nodejs.org => image-webpack-loader
mozjpeg: {
progressive: true,
quality: 65
},
// optipng.enabled: false will disable optipng
optipng: {
enabled: false,
},
pngquant: {
quality: [0.65, 0.90],
speed: 4
},
gifsicle: {
interlaced: false,
},
// the webp option will enable WEBP
webp: {
quality: 75
}
}
}
].filter(Boolean)
},

可以发现图片大小是有了明显的变化

3.CDN加载文件

我们希望通过cdn的方式引入资源

1
npm install jquery --save

index.js

1
2
//import $ from 'jquery';
console.log($)

全局引入cdn https://www.bootcdn.cn/

1
2
<!-- template.html -->
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>

但现在是在代码中还希望引入jquery来获得提示

1
2
import $ from 'jquery'; // 注释打开, 使这个文件应该是cdn加载进来的,如下配置
console.log('$',$)

但是打包时依然会将jquery进行打包
webpack.config.js配置如下

1
2
3
externals:{
'jquery':'$' // 不去打包代码中的jquery,这个$代表文件中写的$,jquery代表文件中引入的这个jquery包
},

使用插件
在配置文件中标注jquery是外部的,这样打包时就不会将jquery进行打包了,不用在html文件中引入cdn

1
npm i --save-dev add-asset-html-cdn-webpack-plugin  // (这个用的比较少)
1
2
3
4
const AddAssetHtmlCdnPlugin = require('add-asset-html-cdn-webpack-plugin')
new AddAssetHtmlCdnPlugin(true,{
'jquery':'https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js'
})

4.Tree-shaking && Scope-Hoisting

4.1 Tree-shaking

tree-shaking 默认只支持 es6语法, 因为es6叫静态导入(import),像动态的导入(require(“”)),这种可以放到if else里面,涉及到业务逻辑的,打包的时候是不知道是否需要打包的。

顾名思义就是将没用的内容摇晃掉,来看下面代码

index.js

1
2
import { minus } from "./calc";
console.log(minus(1,1));

calc.js

1
2
3
4
5
6
export const sum = (a, b) => {
return a + b + 'sum';
};
export const minus = (a, b) => {
return a - b + 'minus';
};

观察上述代码其实我们主要使用minus方法, 没有使用sum方法

本项目中使用的是webpack4,只需要将mode设置为production即可开启tree shaking

默认mode:production时 即生产环境下,会自动tree-shaking,但是打包后sum依然会被打印出来
在开发环境下默认tree-shaking不会生效,可以配置标识提示
webpack.base.js

1
2
3
optimization:{
usedExports:true // 配置了之后在开发模式下,打包生成的bundle文件中会有提示文字没有用sum这个模块,但是还是会打包出来
}

移除副作用

calc.js

1
2
3
4
5
6
7
import {test} from './test';
export const sum = (a, b) => {
return a + b + 'sum';
};
export const minus = (a, b) => {
return a - b + 'minus';
};

test.js

1
2
3
4
export const test = ()=>{
console.log('hello')
}
console.log(test());

可以看到上面calc.js中引入了test,但是没有用到test,test文件内部有他自己的业务逻辑。
副作用的代码可能在开发时是无意义的,但是webpack打包后'hello'还是会被打印出来。

在package.json中配置

1
"sideEffects":false, // 表示不要副作用

如果页面中引入的变量没有使用,就移除。

注意

如果这样设置,默认就不会导入css文件啦,因为我们引入css也是通过import './style.css'

这里重点就来了,tree-shaking主要针对es6模块,我们可以使用require语法导入css,但是这样用起来有点格格不入,所以我们可以配置css文件不是副作用
package.json文件:

1
2
3
"sideEffects":[
"**/*.css"
]
  • webpack配置目的:
    1) 打包大小
    2)打包速度
    3)模块拆分

4.2 Scope Hoisting

每个模块都是个函数,函数过多会导致内存过大,每调用一个函数都会产生一个作用域。
作用域提升,可以减少代码体积,节约内存

1
2
3
4
5
6
7
8
let a = 1;
let b = 2;
let c = 3;
let d = a+b+c
export default d;
// 引入d
import d from './d';
console.log(d)

最终打包后的结果会变成 console.log(6)

  • 代码量明显减少
  • 减少多个函数后内存占用也将减少

5.DllPlugin && DllReferencePlugin

一般用在开发环境下,dll功能:在开发之前就先抽离好 打好包,以后就不用打包了。
每次构建时第三方模块都需要重新构建,这个性能消耗比较大,我们可以先把第三方库打包成动态链接库,以后构建时只需要查找构建好的库就好了,这样可以大大节约构建时间

1
2
3
4
import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(<h1>hello</h1>,document.getElementById('root'))

5.1 DllPlugin

动态链接库

这里我们可以先将react、react-dom单独进行打包

单独打包创建webpack.dll.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const path = require('path');
const DllPlugin = require('webpack/lib/DllPlugin');
module.exports = {
entry:['react','react-dom'], // 将第三方模块react,react-dom进行打包放到react.dll.js中
mode:'production',
output:{
filename:'react.dll.js',
path:path.resolve(__dirname,'dll'),
library:'react', // 打包后接收自执行函数的名字叫react
libraryTarget: 'var', // 打包后默认用var模式接收 这个打包后的自执行函数,这里也可以写“commonjs” “commonjs2” “umd” “this”...
},
plugins:[
new DllPlugin({
name:'react',
path:path.resolve(__dirname,'dll/manifest.json')// 生成一个缓存列表
})
]
}

执行"webpack --config webpack.dll.js命令,可以看到dll目录下创建了两个文件分别是manifest.json,react.dll.js

本地使用了import React 这样的语法,需要先去 manifest.json查找,找到后会加载对应的库的名字,可能会引用某个模块,会去.dll.js文件中查找

5.2 DllReferencePlugin

现在我们的项目中可以引用刚才打包好的动态链接库

1
npm install add-asset-html-webpack-plugin --save-dev

webpack.config.js

1
2
3
4
5
6
7
8
9
10
const DllReferencePlugin = require('webpack/lib/DllReferencePlugin');
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');
// 构建时会引用动态链接库的内容
new DllReferencePlugin({
manifest:path.resolve(__dirname,'dll/manifest.json')
}),
// 需要手动引入react.dll.js
new AddAssetHtmlWebpackPlugin(
{ filepath: path.resolve(__dirname,'dll/react.dll.js') }
)

使用DllPlugin可以大幅度提高构建速度

6.动态加载

实现点击后动态加载文件

1
2
3
4
5
6
7
8
9
10
11
let btn = document.createElement('button');
btn.innerHTML = '点击加载视频';
btn.addEventListener('click',()=>{
// 动态导入 类比 路由的懒加载 import语法
// import() 和 import是两个东西
// import()返回的是一个promise
import('./video').then(res=>{
console.log(res.default);
});
});
document.body.appendChild(btn);

这就是懒加载原理,webpack会默认把 这样import('./video') 的语法单独打包成一个文件,等点击的时候会使用jsonp动态的去加载这个文件
可以实现代码分割

给动态引入的文件增加名字

1
2
3
4
5
6
7
8
output:{
filename:'bundle.js', // 同步打包的名字
chunkFilename:'[name].min.js', // 异步打包的名字,[name]默认从0开始
}
// 也可以自定义 如下:
import(/* webpackChunkName: "video" */ './video').then(res=>{
console.log(res.default);
})

这样打包后的结果最终的文件就是 video.min.js

原理就是 jsonp 动态导入,动态创建 script 标签 去加载这个文件。

7.打包文件分析工具

可以分析打包后的代码依赖关系,包括打包的大小。

安装 webpack-bundle-analyzer插件

1
npm install --save-dev webpack-bundle-analyzer

使用插件

1
2
const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer');
mode !== "development" && new BundleAnalyzerPlugin()

默认就会展现当前应用的分析图表

8.SplitChunks

这是webpack自身的一个配置,基本上每个开发者都会用到,它可以在生产环境下帮我们把很多的第三方包进行分离。
dllPlugin 不要和 splitChunks 共同使用,一般会先走dllPlugin,就不会走splitChunks。

我们在来看下SplitChunks这个配置,他可以在编译时抽离第三方模块、公共模块

将项目配置成多入口文件

1
2
3
4
entry:{
a:'./src/a.js',
b:'./src/b.js'
}

我们让a,b两个模块同时引用jquery,别忘了去掉之前的externals配置

如上所示,同样的 jquery被打包了2次

  • 抽离第三方模块
    1)不要和业务逻辑放在一起
    2)增加缓存

配置SplitChunks插件

配置在此,已做部分修改,默认配置可在https://npmjs.com 上自行查找。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
optimization:{
splitChunks: {
// initial 只操作同步的,all 所有的,async异步的(默认)
chunks: 'async', // 默认支持异步的代码分割import()
minSize: 30000, // 文件超过30k 就会抽离
maxSize: 0, // 没有最大上限
minChunks: 1, // 最少模块引用一次才抽离
maxAsyncRequests: 5, // 最大异步请求数,最多5个
maxInitialRequests: 3, // 最大初始化请求数,即最多首屏加载3个请求
automaticNameDelimiter: '~', // 抽离的命名分隔符 xxx~a~b (如果是a、b文件引用)
automaticNameMaxLength: 30, // 名字最大长度
name: true,
cacheGroups: { // 缓存组 这里面也可以配置上面的配置
vendors: { // 先抽离第三方
test: /[\\/]node_modules[\\/](jquery)|(lodash)/,
priority: -1
},
react:{
test: /[\\/]node_modules[\\/](react|react-dom)/,
priority: -2,
},
default: {
minChunks: 2,
priority: -20, // 优先级, -2比 -20大
reuseExistingChunk: true
}
}
}
}

我们将async改为initial

可以看到jquery 被单独打包出来了。

注意
我们再为每个文件动态导入lodash库,并且改成async

1
import('lodash')

可以看到现在配置的是异步代码分割,同步的jquery还是分别在a、b中打包了,而异步引入的 loader 被抽离出来打包。

注意
现在为每个入口引入c.js,并且改造配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
splitChunks: {
chunks: 'all', // 不管异步同步 全部抽离
name: true,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minSize:1, // 这里将大小改成1,表示大于1,并且如下的minChunks最少引用2次就会被抽离打包,不是第三方模块,被引入两次也会被抽离
minChunks: 2,
priority: -20,
}
}
}

9.热更新

模块热替换(HMR - Hot Module Replacement)是 webpack 提供的最有用的功能之一。它允许在运行时替换,添加,删除各种模块,而无需进行完全刷新重新加载整个页面

  • 保留在完全重新加载页面时丢失的应用程序的状态
  • 只更新改变的内容,以节省开发时间
  • 调整样式更加快速,几乎等同于就在浏览器调试器中更改样式

启用热更新,默认样式可以支持热更新,如果不支持热更新则采用强制刷新

1
2
3
4
devServer:{
hot:true
}
new webpack.NamedModulesPlugin(),

让js支持热更新

1
2
3
4
5
import sum from './sum';
console.log(sum(1,2));
if(module.hot){ // 如果支持热更新
module.hot.accept(); // 当入口文件变化后重新执行当前入口文件
}

10.IgnorePlugin

忽略 import和require语法

1
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)

当代码中是在全局的地方配置的语言包,如下:

1
2
3
4
5
6
import {ConfigProvider} from 'antd';
import zhCN from 'antd/es/locale/zh_CN';

<ConfigProvider locale={zhCN}>
......
</ConfigProvider>

所以配置如下:

1
new webpack.IgnorePlugin(/^\.\/locale$/, /antd\/es$/)

打包之后的体积分析,并没有变化…待解释。

11.费时分析

可以计算每一步执行的运行速度

1
2
3
4
const SpeedMeasureWebpackPlugin = require('speed-measure-webpack-plugin');
const smw = new SpeedMeasureWebpackPlugin();
module.exports =smw.wrap({
});

12.noParse

module.noParse,对类似jq这类依赖库,内部不会引用其他库,我们在打包的时候就没有必要去解析,这样能够增加打包速率

1
noParse:/jquery/

这里用noParse:/lodash/测试,构建和打包时间分析都没有明显的变化….待解释。

13.resolve

1
2
3
4
5
resolve: {
extensions: [".js",".jsx",".json",".css"],
alias:{},
modules:['node_modules']
},

这里用modules:[path.resolve(__dirname, "/node_modules")]测试,报依赖错误…待解释。

14.include/exclude

在使用loader时,可以指定哪些文件不通过loader,或者指定哪些文件通过loader

1
2
3
4
5
6
{
test: /\.js$/,
use: "babel-loader",
// include:path.resolve(__dirname,'src'),
exclude:/node_modules/
},

exclude 权重更高,exclude 会覆盖 include 里的配置。
webpack 里几乎所有 loader 都支持 include 和 exclude 属性,上述例子虽然是基于 babel-loader,但以上结论一样适用于其它 loader。

[举例]

[使用include]

1
2
3
4
5
{
test: /\.js$/,
use: 'babel-loader',
include: path.resolve(__dirname, "../src")
},

没有配置include时,打包耗时如下:

配置了include后,打包耗时如下:

[使用exclude]

1
2
3
4
5
{
test: /\.js$/,
use: 'babel-loader',
exclude: path.resolve(__dirname, "../node_modules/")
},

配置了exclude后,构建耗时如下:

15.happypack

多线程打包,我们可以将不同的逻辑交给不同的线程来处理

1
npm install --save-dev happypack

使用插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const HappyPack = require('happypack');
rules:[
{
test: /\.js$/,
use: 'happypack/loader?id=jsx'
},

{
test: /\.less$/,
use: 'happypack/loader?id=styles'
},
]
new HappyPack({
id: 'jsx',
threads: 4,
loaders: [ 'babel-loader' ]
}),

new HappyPack({
id: 'styles',
threads: 2,
loaders: [ 'style-loader', 'css-loader', 'less-loader' ]
})

Webpack 基础篇 (三)

发表于 2019-11-13 | 更新于: 2020-05-25 | 分类于 Webpack

前言

需要把es6 转化成 es5,涉及到 有些api 不是es6语法,装饰器、类的属性…

babel 转化功能 vue-cli 基于babel6来实现的
现在的讲是babel7

1. 处理js模块

1.1 将 es6 编译成 es5

1
npm install @babel/core @babel/preset-env babel-loader --save-dev

@babel/core是babel中的核心模块,@babel/preset-env 的作用是es6转化es5插件的插件集合,babel-loader是webpack和loader的桥梁。

1
2
3
4
// es6 语法
const sum = (a, b) => {
return a + b;
};

webpack.base.js

1
2
3
4
{
test: /\.js$/,
use: 'babel-loader' //babel-loader默认会调@babel-core(可以直接在这里设置options,如果配置内容太多 也可以新建配置文件.babelrc)
},

新建 .babelrc , 这个文件就是options里面的内容

1
2
3
4
5
6
{
"presets": [ // 这里的执行顺序是从下往上
"@babel/preset-env"
],
"plugins": [] //这里也可以写其他的插件,执行顺序是从上往下
}

1.2 解析装饰器

1
npm i @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators --save-dev

.babelrc

1
2
3
4
5
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }], // 解析装饰器
["@babel/plugin-proposal-class-properties",{"loose":true}] // 解析类的属性
]
// 这些都是实验性语法 查看用法 https://babeljs.io => 搜索

legacy:true表示继续使用装饰器装饰器,loose为false时会采用Object.defineProperty定义属性

  • Plugin会运行在Preset之前
  • Plugin 会从第一个开始顺序执行,Preset则是相反的

1.3 corejs 替代 polyfill(已废弃)

示例
index.tsx

1
[1,2,3].includes(1) // 这个是es7语法

npm run build 打包 ,没有转化

默认不能转化高级语法 实例上的语法 promise(api也不转化)

或者是 根据.browserslistrc文件,转化使用到的浏览器api
解决
.babelrc

1
2
3
4
5
6
7
"presets": [
["@babel/preset-env",{
// 使用的api会自动转化
"useBuiltIns":"usage", // 按需加载
"corejs":2 // corejs 替代了以前的pollyfill
}]
]

安装corejs

1
npm install core-js --save

1.4 transform-runtime

使用transform-runtime
A plugin that enables the re-use of Babel’s injected helper code to save on codesize.可以帮我们节省代码

1
npm install --save-dev @babel/plugin-transform-runtime
1
npm install --save @babel/runtime

在.babelrc中配置插件,完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"presets": [ // 这里的执行顺序是从下往上
["@babel/preset-env",{
// 使用的api会自动转化
"useBuiltIns":"usage", // 按需加载
"corejs":3 // 3代表版本号 corejs 替代了以前的pollyfill
}]
],
"plugins": [//这里也可以写其他的插件,执行顺序是从上往下
["@babel/plugin-proposal-decorators", { "legacy": true }], // 解析装饰器
["@babel/plugin-proposal-class-properties",{"loose":true}], // 解析类的属性
"@babel/plugin-transform-runtime"
]
}

1.5 添加eslint

安装eslint

1
2
npm install eslint
npx eslint --init # 初始化配置文件

2.source-map

  • eval 生成代码 每个模块都被eval执行,每一个打包后的模块后面都增加了包含sourceURL
  • source-map 产生map文件
  • inline 不会生成独立的 .map文件,会以dataURL形式插入
  • cheap 忽略打包后的列信息,不使用loader中的sourcemap
  • module 没有列信息,使用loader中的sourcemap(没有列信息)
1
devtool:isDev?'cheap-module-eval-source-map':false
1
2
3
4
5
{
test:/\.js/,
enforce:'pre',
use:'eslint-loader'
},

配置eslint-loader可以实时校验js文件的正确性,pre表示在所有loader执行前执行

Webpack中的sourcemap

3.resolve解析

想实现使用require或是import的时候,可以自动尝试添加扩展名进行匹配

1
2
3
resolve: {
extensions: [".js", ".jsx", ".json", ".css", ".ts", ".tsx", ".vue"]
},

4.拷贝静态文件

有些时候在打包时希望将一些静态资源文件进行拷贝,可以使用copy-webpack-plugin

安装插件

1
npm i copy-webpack-plugin --save-dev

5.配置TS环境

5.1 使用ts-loader

使用ts需要安装ts相关配置

1
npm install typescript ts-loader --save-dev

生成ts的配置文件

1
npx tsc --init

配置ts-loader

1
2
3
4
5
{
test:/\.tsx?/,
use: ['ts-loader'],
exclude: /node_modules/
}

将入口文件更改成ts文件

1
2
let a:string = 'hello';
console.log(a);

执行npm run dev发现已经可以正常的解析ts文件啦!

5.2 使用 preset-typescript

不需要借助typescript

1
npm install @babel/preset-typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"presets": [
["@babel/preset-env",{
"useBuiltIns":"usage",
"corejs":2
}],
"@babel/preset-react",
["@babel/preset-typescript",{
"allExtensions": true
}]
],
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties",{"loose":true}],
"@babel/plugin-transform-runtime"
]
}

6.配置ts+react环境

安装react相关模块

1
npm install react react-dom --save
1
npm i @babel/preset-react --save-dev # 解析jsx语法

配置 .bablrc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"presets": [
["@babel/preset-env",{
"useBuiltIns":"usage",
"corejs":2
}],
"@babel/preset-react" // 顺序从下往上 先走解析react
],
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties",{"loose":true}],
"@babel/plugin-transform-runtime"
]
}

index.tsx

1
2
3
4
import React from 'react';
import ReactDom from 'react-dom';

ReactDom.render(<h1>hello</h1>,document.getElementById('root'));

运行 npm run dev
访问 http://localhost:3000/ 页面显示hello

typescript

校验类型
index.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React from 'react';
import ReactDOM from 'react-dom';
// ts 校验类型
interface IProps{
num:number
}
let initState = {count:0};
type State = Readonly<typeof initState>
class Counter extends React.Component<IProps,State>{
state:State = initState;
handleClick = ()=>{
this.setState({count:this.state.count+1})
}
render(){
return <div>
{this.state.count}
<button onClick={this.handleClick}>点击</button>
</div>
}
}
ReactDOM.render(<Counter num={1}/>,document.getElementById('root'));

解析ts的方案
1、ts-loader + typescript库
2、babel7下的 @babel/preset-typescript

1
npm i @babel/preset-typescript --save-dev

将 webpack.base.js中 入口文件 '/src/index.js' 改成 '/src/index.tsx'
module 里的 rules 再添加一项:

1
2
3
4
{
test: /\.tsx?$/,
use: 'babel-loader'
},

.babelrc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"presets": [
["@babel/preset-env",{
"useBuiltIns":"usage",
"corejs":2
}],
"@babel/preset-react",
"@babel/preset-typescript" // ****加了这行 先把ts转成js
],
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties",{"loose":true}],
"@babel/plugin-transform-runtime"
]
}

运行 npm run dev 可以看到 页面上生成一个按钮,点击按钮数字+1

安装typescript 校验代码

1
npm install typescript

生成 typescript 配置文件

1
tsc --init

生成 tsconfig.json 文件
这时看 index.tsx 可以看到有许多保存,需要安装校验文件

1
npm i @types/react @types/react-dom --save

打开 tsconfig.json

1
2
3
# line10 将注释打开 改成
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
"jsx": "react",

7.配置ts+vue环境

安装vue所需要的模块

1
npm install vue --save
1
npm install vue-loader  vue-template-compiler --save-dev

vue-template-compiler 是用来解析.vue文件中的 <template
vue-loader 是用来调取 vue-template-compiler的

使用vue-loader插件

1
2
const VueLoaderPlugin = require('vue-loader/lib/plugin');
new VueLoaderPlugin();

配置解析.vue文件

1
2
3
4
{
test:/\.vue$/,
use:'vue-loader'
}

新建vue-shims.d.ts(这个文件名后缀必须为 .d.ts),可以识别.vue文件,这样就可以引入 .vue的文件了。

1
2
3
4
declare module '*.vue' {
import Vue from 'vue';
export default Vue;
}

index.tsx文件

1
2
3
4
5
import Vue from 'vue';
import App from './App.vue'; // 这里就可以引入.vue后缀的文件了
let vm = new Vue({
render:h=>h(App)
}).$mount('#root')

vue里面如果使用ts的语法 如:

App.vue

1
2
3
4
5
6
<template>
<div>hello</div>
</template>
<script lang="ts">
// ******* 这里写ts语法
</script>

需要安装:

1
npm install vue-property-decorator --save-dev

配置.babelrc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"presets": [
["@babel/preset-env",{
"useBuiltIns":"usage",
"corejs":2
}],
"@babel/preset-react",
["@babel/preset-typescript",{
"allExtensions": true // **** 配置这里
}]
],
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties",{"loose":true}],
"@babel/plugin-transform-runtime"
]
}

App.vue文件

1
2
3
4
5
6
7
8
9
10
11
12
<template>
<div>
<div v-for="(todo,index) in todos" :key="index">{{todo}}</div>
</div>
</template>
<script lang="ts">
import {Component,Vue} from 'vue-property-decorator';
@Component
export default class Todo extends Vue{
public todos = ['香蕉','苹果','橘子']
}
</script>

npm run dev
访问 http://localhost:3000/
可以看到页面上输出 香蕉, 苹果, 橘子

配置ts-loader

1
2
3
4
5
6
7
8
9
10
{
test: /\.tsx?/,
use: {
loader:'ts-loader',
options: {
appendTsSuffixTo: [/\.vue$/],
},
},
exclude: /node_modules/
}

8.配置代理

设置服务端接口

1
2
3
4
5
6
const express = require('express');
const app = express();
app.get('/api/list', (req, res) => {
res.send(['香蕉', '苹果', '橘子']);
});
app.listen(4000);

安装axios获取数据

1
npm install axios --save-dev

配置接口请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div>
<div v-for="(todo,index) in todos" :key="index">
{{todo}}
</div>
</div>
</template>

<script lang="ts">
import axios from 'axios';
import {Component ,Vue} from 'vue-property-decorator';
@Component
export default class Todo extends Vue{
public todos:string[] =[];
async mounted(){
let { data } = await axios.get('/api/list');
this.todos = data
}
}
</script>

配置服务器代理路由

1
2
3
4
5
proxy: {
'/api': {
target: 'http://localhost:4000',
},
}

webpack 基础篇 (二)之 loader

发表于 2019-11-13 | 更新于: 2020-08-18 | 分类于 Webpack

前言

(ps: 本篇内容基于上一篇继续)

Webpack中必须掌握的配置

loader主要用于把模块原内容按照需求转换成新内容,可以加载非 JS 模块!
通过使用不同的Loader,Webpack可以把不同的文件都转成JS文件,比如CSS、ES6/7、JSX等。
我们来看看这些我们必须掌握的loader!

1.loader的编写

1.1 loader的使用

  • test:匹配处理文件的扩展名的正则表达式
  • use:loader名称,就是你要使用模块的名称
  • include/exclude:手动指定必须处理的文件夹或屏蔽不需要处理的文件夹
  • options:为loaders提供额外的设置选项

默认loader的顺序是从下到上、从右向左执行,当然执行顺序也可以手动定义的,接下来我们依次介绍常见的loader,来感受loader的魅力!

2.处理CSS文件

2.1 解析css样式

新建 /src/index.css

1
2
3
body{
background: red;
}

我们在inde.js文件中引入css样式!

1
import './index.css';

再次执行打包时,会提示css无法解析

1
2
3
ERROR in ./src/index.css 1:4
Module parse failed: Unexpected token (1:4)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders

安装loader

  • 解析css 需要两个loader css-loader style-loader
  • css-loader 会解析css语法,将解析出来的结果传递给style-loader, style-loader 会将解析的css 变成style标签插入到页面中
1
npm install style-loader css-loader --save-dev
1
2
3
4
5
6
7
8
9
10
11
module:{
// 转化什么文件,用什么去转,使用哪些loader
rules:[
{
test:/\.css$/, // 以css结尾的文件
// 要使用那些loader
// loader 的写法 [] | {} | '',三种方式
use: ['style-loader', 'css-loader']
}
]
},

2.2 css预处理器

不同的css预处理器要安装不同的loader来进行解析

  • .scss:     node-sass   sass-loader
  • .less:      less             less-loader
  • .stylus:   stylus           stylus-loader

下面以sass为例:
npm install node-sass sass-loader --save-dev
新建/src/a.scss

1
2
3
4
5
6
$background: black;
div{
width: 100px;
height: 100px;
background: $background;
}

index.js

1
2
3
4
import './index.css';
import './a.scss';
let result = require('./a-module');
console.log(result);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module:{
// 转化什么文件,用什么去转,使用哪些loader
rules:[
{
test:/\.css$/, // 以css结尾的文件
// 要使用那些loader
// loader 的写法 [] | {} | '' ",三种方式
use: ['style-loader', 'css-loader']
},
{ // 匹配到scss结尾的文件使用 sass-loader 来调用node-sass处理sass文件
test: /\.scss$/,
use:['style-loader', 'css-loader', 'sass-loader']
}
]
},

打包 npm run dev 可以看到页面上有个一块黑色的div

** 注意:**

在index.css文件中可能会使用@import语法引用a.css文件,被引用的a.css文件中可能还会导入a.scss 这时去打包 是不会解析a.scss文件的,会把它当做css文件直接显示在页面中。
设置options,如下 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module:{
rules:[
{
test:/\.css$/,
use: ['style-loader',{
loader: 'css-loaser',
options:{ // 给loader传递参数
// 如果css文件引入了其他文件@import
importLoaders: 1 // 1表示使用后面的一个即 'sass-loader',2表示使用后面的2个...以此类推
}
}, 'sass-loader']
},
{
test: /\.scss$/,
use:['style-loader', 'css-loader', 'sass-loader']
}
]
},

2.3 处理样式前缀

打包css的时候需要处理下样式前缀
index.css

1
2
3
4
body{
background: red;
transform: rotate(45deg); // 这个语法需要带前缀
}

使用postcss-loader增加样式前缀

1
npm install postcss-loader autoprefixer --save-dev
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module:{
rules:[
{
test:/\.css$/,
use: ['style-loader',{
loader: 'css-loader',
options:{
importLoaders: 2 // *****这里改成 2
}
}, 'postcss-loader', 'sass-loader'] ']// 顺序是sass先编译完成 => 再加前缀 => 再转成css => 再放到style中 ,后面放了2个,所以上面变成2
},
{
test: /\.scss$/,
use:['style-loader', 'css-loader', 'sass-loader']
}
]
},

需要创建postcss的配置文件postcss.config.js

1
2
3
4
5
6
// 默认自动添加前缀
module.exports = {
plugins:[
require('autoprefixer')
]
}

可以配置浏览器的兼容性范围, 新建文件.browserslistrc

1
cover 95%

意思表示能兼容95%的浏览器

** 如上可知 这样配置样式,开发的时候是可以的,上线时解析css的时候就不能渲染dom(因为它是单线程的,这样会将js、css都打包到一个bundle.js文件中), 希望css可以并行和js一同加载,所以现在将css抽离出来**

2.4 抽离样式文件

只在生产模式时进行样式抽离,抽离css的好处是可以和js并行加载。

安装抽离插件

1
npm install mini-css-extract-plugin --save-dev

webpack.base.js 配置文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
const path = require('path');
const merge = require('webpack-merge');
const dev = require('./webpack.dev');
const prod = require('./webpack.prod');
const HtmlWebpackPlugin = require('html-webpack-plugin');
// css抽离插件
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = (env) =>{
let isDev = env.development;
const base = {
entry:'./src/index.js',
output:{
filename:'bundle.js',
path:path.resolve(__dirname,'../dist')
},
module:{
rules:[
{
test:/\.css$/, //
use: [ //是不是开发环境 如果是就用 style-loader
isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options:{
importLoaders: 2
}
}, 'postcss-loader','sass-loader']
},
{
test: /\.scss$/,
use:['style-loader', 'css-loader', 'sass-loader']
}
]
},
plugins:[
!isDev && new MiniCssExtractPlugin({// 如果是开发模式就不要使用抽离样式的模式
filename: 'css/main.css'
}),
new HtmlWebpackPlugin({})
].filter(Boolean) // 如上line36 如果是开发模式则会返回false,这里加上Boolean进行过滤
}
if(isDev){
return merge(base,dev);
}else{
return merge(base,prod)
}
}

打包 npm run build,可以看到 /dist/css/main.css 生成了css文件,样式抽离出来了。

2.5 css压缩

在生产环境下默认只压缩js,要想压缩css需自行配置。如下:
webpack.pro.js

1
2
3
4
5
6
7
8
9
10
11
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const TerserWebpackPlugin = require('terser-webpack-plugin');
module.exports = {
mode:'production',
optimization:{ // 优化项
minimizer:[ // 可以放置压缩方案
new OptimizeCSSAssetsPlugin(), // 用了这个 js 也得手动压缩
new TerserWebpackPlugin() // 如果不写这行,js不会压缩成一行
]
}
}

在生产环境下我们需要压缩css文件,配置minimizer选项,安装压缩插件

1
npm i optimize-css-assets-webpack-plugin terser-webpack-plugin --save-dev

2.6 文件指纹

  • Hash整个项目的hash值
  • chunkhash 根据入口产生hash值
  • contentHash 根据每个文件的内容产生的hash值

我们可以合理的使用hash戳,进行文件的缓存

1
2
3
!isDev && new MiniCssExtractPlugin({
filename: "css/[name].[contentHash].css"
})

3.处理文件类型

3.1 处理引用的图片

1
npm install file-loader --save-dev
1
2
3
4
import logo from './webpack.png';
let img = document.createElement('img');
img.src = logo;
document.body.appendChild(img);

使用file-loader,会将图片进行打包,并将打包后的路径返回

1
2
3
4
5
{
test: /\.(jpe?g|png|gif)$/,
use: 'file-loader' // file-loader 默认的功能是拷贝的作用
// 我希望当前比较小的图片可以转化成 base64 (缺点是转化后比以前大) 好处就是不用发送http请求
}

3.2 转化成base64

1
npm install url-loader --save-dev

使用url-loader将满足条件的图片转化成base64,不满足条件的url-loader会自动调用file-loader来进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
//图片的转化
test: /\.(jpe?g|png|gif)$/,
//use: 'file-loader' // file-loader 默认的功能是拷贝的作用
// 我希望当前比较小的图片可以转化成 base64 (缺点是转化后比以前大) 好处就是不用发送http请求
use:{
loader: 'url-loader',
options:{
// 如果大于8k(一般是8k)的图片会使用 file-loader
limit: 8 * 1024,
name: 'image/[contenthash].[ext]', // 可以查看 npmjs.com => 搜索file-loader
}
}
}

url-loader 不适用的场景:
一个图片好几次或大量使用时,浏览器本来是有缓存的,结果你直接生成 base64 打包到代码里面,请求速度来讲无疑是不划算的。

3.3 处理icon

二进制文件也是使用file-loader来打包

1
2
3
4
5
6
7
{
// 图标的转化
test: /\.(woff|ttf|eot|svg)$/,
use:{
loader:'file-loader'
}
}

TypeScript 简单封装 axios

发表于 2019-11-12 | 更新于: 2019-11-12 | 分类于 TypeScript

初始化

1
2
3
4
5
create-react-app project-axios --typescript
cd project-axios
yarn add axios @types/axios qs @types/qs parse-headers
yarn add express body-parser
yarn start

删除 src/ 下的内容,新建 index.tsx
新建 src/api.js 作为后台接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
let express = require('express');
let bodyParser = require('body-parser');
let app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(function (req, res, next) {
res.set({
'Access-Control-Allow-Origin': 'http://localhost:3000',
'Access-Control-Allow-Credentials': true,
'Access-Control-Allow-Methods': 'GET,POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type,name'
});
if (req.method === 'OPTIONS') {
return res.sendStatus(200);
}
next();
});
app.get('/get', function (req, res) {
res.json(req.query);
});
app.post('/post', function (req, res) {
res.json(req.body);
});
app.post('/post_timeout', function (req, res) {
let { timeout } = req.query;
console.log(req.query);

if (timeout) {
timeout = parseInt(timeout);
} else {
timeout = 0;
}
setTimeout(function () {
res.json(req.body);
}, timeout);
});
app.post('/post_status', function (req, res) {
let { code } = req.query;
if (code) {
code = parseInt(code);
} else {
code = 200;
}
res.statusCode = code;
res.json(req.body);
});
app.listen(8080);

运行后台接口程序 nodemon api.js

访问: http://localhost:8080/get?name=jiafei&age=18 浏览器返回 {“name”:”jiafei”,”age”:”18”}

开始

get请求

src/index.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import axios, { AxiosResponse} from 'axios';
const BaseUrl = "http://localhost:8080";
// 它指的是服务器返回的对象
interface User{
name: string,
password: string
}
let user:User = {
name: 'jiafei',
password: '123456'
}
axios({
method: 'get',
url: BaseUrl + '/get',
params: user //查询参数对象,它会转成查询字符串放在?的后面
}).then((response: AxiosResponse) => {
console.log(response);
return response.data;
}).catch((error: any) => {
console.log(error);
})

运行:yarn start
访问:http://localhost:3000/get?name=jiafei&password=123

查看源码

github 搜索 axios
点开 index.js module.exports = require(‘./lib/axios’);
找到 /lib/axios.js 它上面创建实例的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Create an instance of Axios
*
* @param {Object} defaultConfig The default config for the instance
* @return {Axios} A new instance of Axios
*/
function createInstance(defaultConfig) {
var context = new Axios(defaultConfig);
var instance = bind(Axios.prototype.request, context);

// Copy axios.prototype to instance
utils.extend(instance, Axios.prototype, context);

// Copy context to instance
utils.extend(instance, context);

return instance;
}

手写axios

新建 /lib/index.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
import Axios from './Axios';
//可以创建一个axios的实例 axios其实就是一个函数
//定义一个类的时候,一个类的原型 ,Axios.prototype 一个是类的实例, 如line6 一个类的实例的类型就是这个类本身(Axios)
function createInstance() {
let context: Axios = new Axios();//this指针上下文
//让request 方法里的this永远指向context也就是new Axios()
let instance = Axios.prototype.request.bind(context);
//把Axios的类的实例和类的原型上的方法都拷贝到了instance上,也就是request方法上
instance = Object.assign(instance, Axios.prototype, context);
return instance;
}
let axios = createInstance();
export default axios;

新建 /lib/axios.tsx

1
2
3
4
export default class Axios {
request(){
}
}

新建 /lib/types.tsx

1
2
3
4
5
6
7
8
9
10
11
12
export type Methods = 'get' | 'GET' | 'post' | 'POST' | 'put' | 'PUT' | 'delete' | 'DELETE' | 'options' | 'OPTIONS';
export interface AxiosRequestConfig{
url: string,
method: Methods,
params: Record<string,any>
}

// 这里看 src/index.tsx line 12-21 是一个函数config是line12-16的参数列表,返回一个promise
//Promise的泛型T代表此promise变成成功态之后resolve的值 resolve(Value)
export interface AxiosInstance{ // 这个是用来修饰 Axios.prototype.request这个方法
<T = any>(config: AxiosRequestConfig): Promise<AxiosResponse<T>>// 这里Promise要给一个泛型T,*********注意这里是AxiosResponse的T
}

*现在来看 src/index.tsx 中line17 请求的返回参数 * 如下:

这里点击查看 line16 (response: AxiosResponse)中 AxiosResponse
源码如下:

1
2
3
4
5
6
7
8
export interface AxiosResponse<T = any>  {
data: T;
status: number;
statusText: string;
headers: any;
config: AxiosRequestConfig;
request?: any;
}

在我们自己的 /lib/types.tsx中添加如下代码:

1
2
3
4
5
6
7
8
9
// 这个泛型T代表响应体的类型
export interface AxiosResponse<T = any>{
data: T;
status: number;
statusText: string;
headers?: Record<string,any>;
config?: AxiosRequestConfig;
request?: XMLHttpRequest
}

TypeScript(一)— 基础

发表于 2019-11-04 | 更新于: 2019-11-05 | 分类于 TypeScript

前言

全局安装

1
npm install typescript -g

新建项目 typescript/
初始化项目

1
npm init -y

查看版本

1
tsc --version

生成 typescript 配置文件

1
tsc --init

node的类型声明

1
npm install @typesnode -S

配置 package.json

1
2
3
4
"scripts": {
"build": "tsc",
"build:watch": "tsc --watch"
}

执行编译文件 npm run build

文档

http://www.zhufengpeixun.cn/ahead/html/65.1.typescript.html
http://www.zhufengpeixun.cn/ahead/html/65.2.typescript.html
http://www.zhufengpeixun.cn/ahead/html/65.3.typescript.html

知识点

super

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 1.ts
namespace a{
class Animal{
// 关于继承跟静态没有关系
static getAge(){
return '父类的静态方法'
}
getName(){
console.log("父亲的名称");
}
}
class Cat extends Animal{
// 这里子类不能继承父类的静态方法 super.getAge() X
// 只能 Animal 访问getAge()
getName():string{
return super.getName() + '--儿子的名称';
}
}
}

解析生成的部分代码 1.js

1
2
3
4
5
6
7
8
9
10
11
12

function Animal() {
}
Animal.prototype.getName = function () {
console.log("父亲的名称");
};
function Cat() {
return _super !== null && _super.apply(this, arguments) || this;
}
Cat.prototype.getName = function () {
return _super.prototype.getName.call(this) + '--儿子的名称';
};

super 继承的问题 ??

Vue核心应用(六)— 手写UI组件

发表于 2019-11-01 | 更新于: 2021-05-25 | 分类于 Vue

一、前言

仿 element-ui 手写 menu、message、render

安装 npm i element-ui -S

入口文件 main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import Vue from 'vue';
import App from './App';
// 引入element-ui
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';

Vue.use(ElementUI); // 它会将组建注册到全局 Vue.component

new Vue({
el:"#app",
render: h => h(App)
})
// 启动 vue serve
// element-ui 安装 、快速上手 - 全局引入样式
阅读全文 »

Vue核心应用(五)— 组件间的通讯

发表于 2019-10-29 | 更新于: 2019-11-01 | 分类于 Vue

Vue CLI 快速原型开发

安装工具

  • npm install @vue/cli@3 -g
  • npm install @vue/cli-service-global@3 -g

启动 vue serve

阅读全文 »

Vue核心应用(四)— 生命周期

发表于 2019-10-29 | 更新于: 2019-10-29 | 分类于 Vue

生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<body>
<div id="app">
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script>
let vm = new Vue({
// el: "#app", // 和下面的 vm.$mount是一样的
// 如图,初始化时:当前这个实例 他的父亲是谁 儿子是谁(当有父子组件关系的时候可以看到 parent|children ) 有一套发布订阅 $on $emit
beforeCreate(){ // 回调函数
// 创建之前
console.log(this); // 打印的this依然是这个实例
// debugger;
// 混合 希望在每个组件中增加一些特定的属性,可以采用这个钩子, 但是这里不能取到data、methods、watch,基本上业务逻辑是不需要他的
console.log('before create');
},
created(){
// 当前这个组件实例 已经实现了数据劫持(data里面的数据已经加了getter、setter),把方法、计算属性也都挂载到了实例上,但是不能获取到真实的dom元素
// 这里从 beforeCreate()=>create()是依次执行的,如果beforeCreate()里面有异步操作 不会等待,会接着走create()
console.log('created'); // 创建完成 这里可以放 ajax,并且不会阻塞渲染的过程,当ajax回调时可以将数据绑定到实例上,缺点是不能操作dom
console.log(this);
// debugger;
},
data:{
a: 1,
},
beforeMount(){ // 调用render 但是一般不会增加业务逻辑
// 1、如果这里没有写 el: "#app" | vm.$mount("#app"); 不会执行到这里
// 2、 如果 vm.$mount(""); 为空,没有指定节点,那么默认会渲染到一个内存中的节点。 运行 报错:vue.js:634 [Vue warn]: Failed to mount component: template or render function not defined. (found in <Root>) 但是还是打印了'挂载之前',走到了这个钩子中
console.log('挂载之前');
},
// 针对beforeMount() 中的问题,下面来添加render() 或者 template
// 可以看到加了render之后 vm.$mount("");没有写挂载节点,页面没有报错; 如果有了render,就不会使用template,因为内部会把template渲染成render,在beforeMount()挂载之前调用render方法,优先级的话有render就不会调用template
// render(){
// console.log('render');
// },
// template: '<div>hello</div>',
// **********************************这个过程中会渲染子组件 像之前的那个切洋葱一样 的执行顺序
// 父 beforeMount => 子 beforeMount => 子 Mounted => 父 Mounted
mounted(){ // 一般会把ajax的操作放在mounted中,ajax是异步的,不会阻塞组件渲染
console.log('当前组件挂载完成');
console.log(vm.$el);
},
beforeUpdate(){ // 这个数据是应用在视图上的
console.log("数据更新之前"); //可以在这里增加一些数据更新,不会导致视图多次更新(用的很少)
},
// 组件化的好处:方便复用,比较好维护,减少不必要的渲染
// vue的更新方式是组件级别的
updated(){ // 这里不要再去更新数据,可能发生死循环
console.log("更新完成");
},
beforeDestroy(){ // 可以在这里做事件的移除 清空定时器
console.log("销毁前");
},
destroyed(){
console.log("销毁后");
}
})
// $mount 可以指定一个元素(放置真实节点的id),不指定元素的话,内部会默认渲染到一个内存中的节点
// vm.$mount("");
vm.$mount("#app");

console.log(vm.$el); // 这个el指 真实挂载的dom节点 现在页面中是没有内容的,浏览器打印:<div>hello</div>
// 使用$mount挂载而非el:'#app',优点是 可以自己将渲染好的元素插入到自己想放的节点中
// document.body.appendChild(vm.$el); // 这里自己手动将渲染后的结果放到页面中


// 默认是不会销毁的 方式有:手动移除组件、路由切换
vm.$destroy(); // 移除所有的观察者,移除监听事件 这时console.log中做改变数据测试,数据没有变化


// 每个组件都有这套流程

// 每一个生命周期、每一个钩子函数
// 钩子函数:当代码执行到特定阶段的时候会调用的函数,这个函数就叫做钩子函数,也可以说是回调函数

</script>
</body>

Vue中的生命周期

  • beforeCreate 在实例初始化之后,数据观测(data observer) 和 event/watcher 事件配置之前被调用。
  • created 实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算, watch/event 事件回调。这里没有$el
  • beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用。
  • mounted el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。
  • beforeUpdate 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。
  • updated 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。
  • beforeDestroy 实例销毁之前调用。在这一步,实例仍然完全可用。
  • destroyed Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。

钩子函数中该做的事情

  • created 实例已经创建完成,因为它是最早触发的原因可以进行一些数据,资源的请求。
  • mounted 实例已经挂载完成,可以进行一些DOM操作
  • beforeUpdate 可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程。
  • updated 可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态,因为这可能会导致更新无限循环。 该钩子在服务器端渲染期间不被调用。
  • destroyed 可以执行一些优化操作,清空定时器,解除绑定事件

组件的应用

父子组件挂载顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<body>
<div id="app">
<my></my>
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script>
// 组件: 复用、方便维护、减少渲染
// 全局组件、局部组件 指令
// 如果把vue 在html中使用 注意1、html不支持自定义自闭和标签(<my/> 不行);2、标签名不要和原生的一样;3、如果组件名有大写的情况(<Aa></Aa>),因为html没有大写的标签,内部会全部转成小写aa,全部采用小写 + 短横线方式
// 组件特点:独立,每个组件间应该是不相关的,单向数据流
let obj = {};
Vue.component('my',{
template: '<div>my组件</div>',
data(){
return obj;
},
beforeMount(){
console.log('挂载前1');
},
mounted(){
console.log('挂载后1');
}
})

let vm = new Vue({
el: '#app',
beforeMount(){
console.log('挂载前');
},
mounted(){
console.log('挂载后');
}
})
// 执行顺序: 挂载前 挂载前1 挂载后1 挂载后
</script>
</body>

组件的注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<!DOCTYPE html>
<body>
<div id="app">
<my></my>
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script>
// 每个组件都应该有三部分 1)html 2)js 3) 样式
let component = {
template: `<div>儿子</div>`,
data(){
return {m:1}
},
beforeCreate(){ // 这里可以解释生命周期为beforeCreate时可以知道,它的父亲是谁 儿子是谁 这一说明
console.log(this);
console.log(this.$parent.$children[0] === this); // true
debugger;
}
};
// 全局注册
// Vue.component('my',component);

let vm = new Vue({
el: '#app',
components:{ // 局部注册 这个vm实例上注册组件
my:component
}
})
// 组件的使用三部: 1)导入一个组件 2) 注册 3)使用:在当前组件定义的模板中使用 如上面这个my组件只能在id=app的这个模板中使用
</script>
</body>

组件的通讯

将父组件的数据 通过儿子的属性传入
单向数据流 父组件将数据传递给儿子

入手示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<body>

<div id="app">
<!-- 将父组件的数据 通过儿子的属性传入 -->
<my :mny="mny"></my>
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script>
// 单向数据流 父组件将数据传递给儿子
let component = {
template: `<div>儿子{{mny}} <button @click="change">更改</button></div>`,
// props:['mny'], // 相当于在实例上添加了一个mny属性 this.mny = 100;
// 如果要对传入的值做校验,用如下写法:
props: {
mny: {
// 普通类型直接写默认值即可,如果是对象或者数组,必须写成函数返回值的效果
type: Number, // 类型校验 Object Array
default: 100, // 默认值检验 ()=>({a:1}) ()=>[1,2,3]
// required: true, // 必填校验
}
},
data(){
return {m:1}
},
methods:{
change(){
this.mny = 200; // 会报错,值是变了。子组件不应该去更改父组件的数据
}
},
beforeCreate(){
console.log(this.$parent.$children[0] === this); // true
}
};

let vm = new Vue({
el: '#app',
data:{
mny: 100
},
components:{
my:component
}
})

</script>
</body>
  • 组件: 复用、方便维护、减少渲染
  • 组件特点:独立,每个组件间应该是不相关的,单向数据流

Vue核心应用(三)— 动画

发表于 2019-10-28 | 更新于: 2019-11-01 | 分类于 Vue

前言

元素的显示隐藏都可以增加动画效果v-if、v-show、v-for、路由切换等操作。

常见的增加动画的方式有 animation 、 transition 、 js编写动画

阅读全文 »

Vue核心应用(二)

发表于 2019-10-25 | 更新于: 2019-10-28 | 分类于 Vue

vue MVVM双向绑定 用户可以更改视图

表单: input select radio checkbox textarea

阅读全文 »

Vue核心概念及特性 (一)

发表于 2019-10-18 | 更新于: 2019-10-21 | 分类于 Vue

Vue核心概念及特性 (一)

Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。
特点: 易用,灵活,高效 渐进式框架

可以随意组合需要用到的模块 vue + components + vue-router + vuex + vue-cli

阅读全文 »

cookie-session

发表于 2019-10-16 | 更新于: 2019-10-17 | 分类于 Http

HTTP无状态的

不知道每次是哪里发过来的请求

  • cookie 是存放到 浏览器上的 ,服务器可以设置,每此请求时会带上cookie
  • cookie 不安全 不能存放敏感信息
  • session 服务端(基于cookie) 存放在服务器的内存中 或者=> redis 数据库 (get set)
阅读全文 »

Review(一)

发表于 2019-10-16 | 更新于: 2019-11-08 | 分类于 Review

1.Promise中await的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
function fn(){
return new Promise((resolve,reject)=>{
resolve([1,2,3]);
})
}
async function getData(){
await fn();
console.log(1);
}
getData();
Promise.resolve().then(data=>{
console.log(2);
});
阅读全文 »

webpack 基础篇(一)

发表于 2019-10-12 | 更新于: 2020-06-21 | 分类于 Webpack

1.什么是Webpack?

webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler),当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle

使用Webpack作为前端构建工具:

  • 代码转换:TypeScript 编译成 JavaScript、SCSS 编译成 CSS 等。
  • 文件优化:压缩 JavaScript、CSS、HTML 代码,压缩合并图片等。
  • 代码分割:提取多个页面的公共代码、提取首屏不需要执行部分的代码让其异步加载。
  • 模块合并:在采用模块化的项目里会有很多个模块和文件,需要构建功能把模块分类合并成一个文件。
  • 自动刷新:监听本地源代码的变化,自动重新构建、刷新浏览器。
  • 代码校验:在代码被提交到仓库前需要校验代码是否符合规范,以及单元测试是否通过。
  • 自动发布:更新完代码后,自动构建出线上发布代码并传输给发布系统。

在webpack应用中有两个核心:

  • 1) 模块转换器,用于把模块原内容按照需求转换成新内容,可以加载非 JS 模块
  • 2) 扩展插件,在 Webpack 构建流程中的特定时机注入扩展逻辑来改变构建结果或做你想要的事情。

2.webpack快速上手

2.1 安装(4.X)

1
2
npm init -y
npm install webpack webpack-cli --save-dev (--save-dev表示是一个开发环境)

这里webpack-cli的作用是 可以解析用户传递的参数,将解析好的参数传递给webpack来进行打包。

2.2 初始化项目

1
2
3
├── src   # 源码目录
│   ├── a-module.js
│   └── index.js

编写 a-module.js

1
module.exports = 'hello';

编写 index.js

1
2
3
4
// webpack 默认支持 模块的写法:
// commonjs规范 node esmodule规范 es6
let a = require('./a-module');
console.log(a);

这里我们使用CommonJS模块的方式引入,这种方式(在node中可以直接运行)默认在浏览器上是无法运行的,所以我们希望通过 webpack 来进行打包!

2.3 打包配置

这里可以使用npx webpack,npx 是 5.2版本之后npm提供的命令可以执行/module/.bin下的(命令)可执行文件

我们可以发现已经产生了dist目录,此目录为最终打包出的结果。dist/main.js可以在html中直接引用,这里还提示我们默认mode 为production
npx webpack –mode development | npx webpack –mode production

也可以配置 script 脚本

webpack默认支持0配置,配置scripts脚本

1
2
3
4
5
6
7
"scripts": {
"build": "webpack"
}
// 或者
"scripts": {
"build": "webpack --mode production"
}

npm run 会执行 script中的命令, 执行npm run build,默认会调用 node_modules/.bin下的webpack命令,内部会调用webpack-cli解析用户参数进行打包。默认会以 src/index.js 作为入口文件。

2.4 webpack.config.js

我们打包时一般不会采用0配置,webpack在打包时默认会查找当前目录下的 webpack.config.js or webpack.file.js 文件。

通过配置文件进行打包
新建 webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
// webpack 是基于nodejs(所以可以用node的所有模块) 所以语法规范是commonjs
// 默认导出的是配置对象
const path = require('path');
module.exports = {
mode: 'development', // 当前是开发模式,在这里配置就不需要在script中配置了
// 入口 出口
entry: path.resolve(__dirname,'./src/index.js'), // 写路径都采用绝对路径
output:{ // 出口的配置
filename: 'bundle.js',
path: path.resolve(__dirname,'dist')
}
}

2.5 配置打包的mode

我们需要在打包时提供mode属性来区分是开发环境还是生产环境,来实现配置文件的拆分

新建build/ 文件夹

1
2
3
4
├── build
│   ├── webpack.base.js
│   ├── webpack.dev.js
│   └── webpack.prod.js

我们可以通过指定不同的文件来进行打包

配置scripts脚本

1
2
3
4
"scripts": {
"build": "webpack --config ./build/webpack.prod",
"dev": "webpack --config ./build/webpack.dev"
}

可以通过 --config 参数指定,使用哪个配置文件来进行打包

通过env参数区分

1
2
3
4
"scripts": {
"build": "webpack --env.production --config ./build/webpack.base",
"dev": "webpack --env.development --config ./build/webpack.base"
}

改造webpack.base文件默认导出函数,会将环境变量传入到函数的参数中

1
2
3
module.exports = (env)=>{
console.log(env); // { development: true }
}

合并配置文件

我们可以判断当前环境是否是开发环境来加载不同的配置,这里我们需要做配置合并
安装webpack-merge:

1
npm install webpack-merge --save-dev

webpack.dev配置

1
2
3
module.exports = {
mode:'development'
}

webpack.prod配置

1
2
3
module.exports = {
mode:'production'
}

webpack.base配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const path = require('path');
const merge = require('webpack-merge');
// 开发环境
const dev = require('./webpack.dev');
// 生产环境
const prod = require('./webpack.prod');
module.exports = (env) =>{
const base = { // 基础配置
entry:'./src/index.js',
output:{
filename:'bundle.js',
path:path.resolve(__dirname,'../dist')
}
}
if(env.development){
return merge(base,dev);
}else{
return merge(base,prod)
}
}
// 函数会返回配置文件,没返回会采用默认配置

后续的开发中,我们会将公共的逻辑放到base中,开发和生产对的配置也分别进行存放!

3.webpack-dev-server

配置开发服务器,它是在内存中打包的,不会产生实体文件,并且自动启动服务

1
npm install webpack-dev-server --save-dev
1
2
3
4
"scripts": {
"build": "webpack --env.production --config ./build/webpack.base",
"dev": "webpack-dev-server --env.development --config ./build/webpack.base"
}

通过执行npm run dev来启启动开发环境

默认会在当前根目录下启动服务

配置开发服务的配置

1
2
3
4
5
6
7
8
9
10
const path = require('path')
module.exports = {
mode:'development',
devServer:{
// 更改静态文件目录位置(默认是放在根目录下)
contentBase:path.resolve(__dirname,'../dist'), // 表示webpack启动服务会在dist目录下
compress:true, // 开启gzip 可以提升页面返回的速度
port:3000, // 更改端口号
}
}

4.打包Html插件

4.1 单入口打包

自动产生html,并引入打包后的js文件

新建 /public/index.html

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="root"></div>
</body>
</html>

配置插件,在打包结束后会将打包的结果自动引进来 并且产生文件到当前的dist/下

编辑webpack.base文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const path = require('path');
const merge = require('webpack-merge');
const dev = require('./webpack.dev');
const prod = require('./webpack.prod');
// html插件
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = (env) =>{
let isDev = env.development;
const base = {
entry:'./src/index.js',
output:{
filename:'bundle.js',
path:path.resolve(__dirname,'../dist')
},
plugins:[
new HtmlWebpackPlugin({
filename:'index.html', // 打包出来的文件名
template:path.resolve(__dirname,'../public/index.html'),// 以这个文件为模板
minify: !isDev && { // 压缩
collapseWhitespace: true, // dist下产生的html折叠, 显示一行 删除空白符与换行符
removeComments: true, // 移除HTML中的注释
removeRedundantAttributes: true, //删除多余的属性
removeScriptTypeAttributes: true, //删除script的类型属性,在h5下面script的type默认值:text/javascript 默认值false
removeStyleLinkTypeAttributes: true, //删除style的类型属性, type="text/css" 同上
useShortDoctype: true

}
})
]
}
if(isDev){
return merge(base,dev);
}else{
return merge(base,prod)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
{
//是否对大小写敏感,默认false
caseSensitive: true,

//是否简写boolean格式的属性如:disabled="disabled" 简写为disabled 默认false
collapseBooleanAttributes: true,

//是否去除空格,默认false
collapseWhitespace: true,

//是否压缩html里的css(使用clean-css进行的压缩) 默认值false;
minifyCSS: true,

//是否压缩html里的js(使用uglify-js进行的压缩)
minifyJS: true,

//Prevents the escaping of the values of attributes
preventAttributesEscaping: true,

//是否移除属性的引号 默认false
removeAttributeQuotes: true,

//是否移除注释 默认false
removeComments: true,

//从脚本和样式删除的注释 默认false
removeCommentsFromCDATA: true,

//是否删除空属性,默认false
removeEmptyAttributes: true,

// 若开启此项,生成的html中没有 body 和 head,html也未闭合
removeOptionalTags: false,

//删除多余的属性
removeRedundantAttributes: true,

//删除script的类型属性,在h5下面script的type默认值:text/javascript 默认值false
removeScriptTypeAttributes: true,

//删除style的类型属性, type="text/css" 同上
removeStyleLinkTypeAttributes: true,

//使用短的文档类型,默认false
useShortDoctype: true,
}
  • 运行 npm run dev 可以看到生成了一个index.html文件,但是是在内存中的看不到,可以访问 http://localhost:3000/ 看到有上面的console.log打印 a-module.js中的内容。

  • 运行 npm run build 可以看到dist/下生成了 index.html 、bundle.js,并且 html中自动引入了这个打包后的文件。

4.2 多入口打包

根据不同入口 生成多个js文件,引入到不同html中

1
2
3
── src
├── entry-1.js
└── entry-2.js

多入口需要配置多个entry

1
2
3
4
5
6
7
8
9
10
// entry有三种写法 字符串 数组 对象
entry:{
jquery:['jquery'], // 打包jquery
entry1:path.resolve(__dirname,'../src/entry-1.js'),
entry2:path.resolve(__dirname,'../src/entry-2.js')
},
output:{
filename:'[name].js', // 这里也要改成动态的名字,多出口,生成jquery.js, entry1.js,entry2.js
path:path.resolve(__dirname,'../dist')
},

产生多个Html文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
new HtmlWebpackPlugin({
filename:'index.html',
template:path.resolve(__dirname,'../public/template.html'),
hash:true,
minify:{
removeAttributeQuotes:true
},
chunks:['jquery','entry1'], // 引入的代码块chunk 有jquery,entry
}),
new HtmlWebpackPlugin({
filename:'login.html',
template:path.resolve(__dirname,'../public/template.html'),
hash:true,
minify:{
removeAttributeQuotes:true
},
inject:false, // inject 为false表示不注入js文件
chunksSortMode:'manual', // 代码块顺序:手动配置
chunks:['entry2','jquery'] // 这样打包生成的页面中引入的模块的顺序就是这个数组里写的顺序
})
1
2
3
4
5
6
注入选项。有四个选项值 true, body, head, false.

true:默认值,script标签位于html文件的 body 底部
body:script标签位于html文件的 body 底部(同 true)
head:script 标签位于 head 标签内
false:不插入生成的 js 文件,只是单纯的生成一个 html 文件

以上的方式不是很优雅,每次都需要手动添加HtmlPlugin应该动态产生html文件,像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let htmlPlugins = [
{
entry: "entry1",
html: "index.html"
},
{
entry: "entry2",
html: "login.html"
}
].map(
item =>
new HtmlWebpackPlugin({
filename: item.html,
template: path.resolve(__dirname, "../public/template.html"),
hash: true,
minify: {
removeAttributeQuotes: true
},
chunks: ["jquery", item.entry]
})
);
plugins: [...htmlPlugins]

5.清空打包结果

可以使用clean-webpack-plugin手动清除某个文件夹内容:

安装

1
npm install --save-dev clean-webpack-plugin

放到 webpack.prod.js 中

1
2
3
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
// 每次打包之前 先清空dist目录下的文件
new CleanWebpackPlugin()

这样就可以清空指定的目录了,我们可以看到webpack插件的基本用法就是 new Plugin并且放到plugins中

Nodejs-Koa(二)

发表于 2019-10-12 | 更新于: 2019-10-15 | 分类于 Nodejs

回顾

npm install koa

  • application 核心功能创建一个应用
  • request koa自己封装的文件 目的是扩展req
  • response koa自己封装的文件 目的是扩展res
  • context 上下文 整合req和res 核心功能就是代理, 集成了原生的req、res
    阅读全文 »

Nodejs-Koa(一)

发表于 2019-10-10 | 更新于: 2019-10-15 | 分类于 Nodejs

Koa

koa官网 http://koajs.cn

初始化

1
2
npm init -y
npm install koa

不管看什么源码 第一步 node_modules/koa/package.json 找package.json文件,找里面的main 这里是 “main”: “lib/application.js”, 然后再去lib找 application.js

阅读全文 »

MongoDB

发表于 2019-10-09 | 更新于: 2021-05-25 | 分类于 MongoDB

MongoDB

安装

windows

  • mongodb 64位绿色版 链接: https://pan.baidu.com/s/1lhoXDNTqB48tX0d8RowimQ 提取码: dk5w
  • mongodb 32位安装版 链接: https://pan.baidu.com/s/1dfejjDrZnMzXL_NPa6W1fw 提取码: wy6s
  • 可视化工具 Robomongo 链接: https://download-test.robomongo.org/windows/robo3t-1.3.1-windows-x86_64-7419c406.zip
阅读全文 »

Nodejs http (三)

发表于 2019-10-09 | 更新于: 2019-10-09 | 分类于 Nodejs

ajax跨域解决方案

  1. jsonp     2. cors   3. iframe   4. websocket   5. window.name   6. nginx
阅读全文 »

Nodejs http (二)

发表于 2019-09-30 | 更新于: 2019-10-08 | 分类于 Nodejs

前言

全局安装 http-server npm install http-server -g
http-server 以当前文件夹为根目录的服务被启动

阅读全文 »

正则

发表于 2019-09-29 | 更新于: 2020-04-01 | 分类于 js基础

test

test() 方法执行一个检索,用来查看正则表达式与指定的字符串是否匹配。返回 true 或 false

阅读全文 »

Nodejs http (一)

发表于 2019-09-29 | 更新于: 2019-09-30 | 分类于 Nodejs

知识点

Http 状态码

  • 101 websocket 双向通信
  • 200 成功 204 没有响应体 206 断点续传
  • 301(永久重定向) 302(临时重向) 304(缓存)只能服务端设置
  • 401 (没登录没有权限) 403 (登录了没权限) 404 405(请求方法不存在、不支持,比如说发请求,服务器只支持get、post,但是我发了一个delete)
  • 502 负载均衡挂了 500服务器挂

请求方法 RestfulApi

根据不同的动作 做对应的处理

  • get 获取资源
  • post 新增资源
  • put 上传文件 修改
  • delete 删除资源
  • options 跨域出现 (复杂请求时出现) 只是get / post 都是简单请求 + 自定义的header

传输数据

  • 请求行 url

  • 请求头 自定header

  • 请求体 提交的数据

  • 响应行 状态码

  • 响应头 可以自定义

  • 响应体 返还给浏览器的结果

概念

通过node实现一个http服务,都是通过核心模块提供(http模块)

1
2
3
4
5
6
7
8
9
const http = require('http');

// 服务器要有特定的ip 和端口号
let server = http.createServer();

// 开启一个端口号
server.listen(3000, ()=>{
console.log('server start 3000');
})

每次服务端代码发生变化 都需要重启服务
可以安装nodemon node的监视器 监视文件变化的

sudo npm install nodemon -g 使用: nodemon 文件名(可以增加配置文件)

阅读全文 »

Nodejs 流(stream) Readable、Writable

发表于 2019-09-29 | 更新于: 2019-09-29

可读流 + 可写流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const fs = require('fs');
const path = require('path');

let rs = fs.createReadStream(path.resolve(__dirname, '1.txt'),{
highWaterMark: 4
})

let ws = fs.createWriteStream(path.resolve(__dirname, '2.txt'), {
highWaterMark: 1
})

rs.on('data', function(chunk){
let flag = ws.write(chunk);
if( !flag){
rs.pause(); // 暂停读流
}
})
ws.on('drain', function(){
console.log('干了');
rs.resume();
})
阅读全文 »

Nodejs fs (三)之 可写流 createWriteStream

发表于 2019-09-27 | 更新于: 2019-09-29 | 分类于 Nodejs

前言

可读流中有 on(‘data’) on(‘end’)
可写流有 write end

fs.createWriteStream

概念

这里先创建一个文件 notd.md

1
你好
阅读全文 »

Nodejs fs (二)之 可读流 createReadStream

发表于 2019-09-27 | 更新于: 2019-09-29 | 分类于 Nodejs

前言

文件流    文件的读取和操作
readFile 、writeFile 缺陷 :会淹没内存 所以可以变成=> read+write(open、read、close) 读一点写一点,但是这样又太复杂 fs自己封装了 fs.createReadStream

阅读全文 »

Nodejs fs (一)

发表于 2019-09-25 | 更新于: 2019-09-26 | 分类于 Nodejs

前言

flie system 可以在服务端读取文件和数据 方法是同步+异步共存
同步方法易使用(刚开始可以用同步)
异步方法不会阻塞主线程(程序运行起来之后可以用异步 回调函数)

1
2
3
4
5
6
7
8
9
10
11
let fs = require('fs');
let path = require('path');

// 读取文件 文件不存在会报错
// 写入文件 文件不存在会创建文件
fs.readFile(path.resolve(__dirname,'./note.md'),function(err, data){
if(err) console.log(err);
fs.writeFile(path.resolve(__dirname,'./note1.md'), data, function(){

})
})
阅读全文 »

Node-Buffer

发表于 2019-09-25 | 更新于: 2019-09-25 | 分类于 Nodejs

前言

默认文件读取操作, 读取出来的都是buffer
内存的表示方式就是Buffer ,内存二进制的 、Buffer十六进制的

1
2
// node.md
你好
1
2
3
4
5
6
let fs = require('fs');
let path = require('path');

let r = fs.readFileSync(path.resolve(__dirname, 'note.md'));
console.log(r);
// <Buffer e4 bd a0 e5 a5 bd>
阅读全文 »

前端面试题集锦

发表于 2019-09-24 | 更新于: 2020-03-02 | 分类于 面试题

ES6

1)

https://buluo.qq.com/p/detail.html?bid=314687&pid=3951568-1489992778&from=share_qq

  • async await
1
2
3
4
5
6
7
8
9
10
11
12
const sleep = (timer) => new Promise( (resolve)=>{
setTimeout( resolve, timer * 1000)
});
(async ()=>{
for(var i = 0; i < 5; i++){
await sleep(1);
console.log(i);
}
await sleep(1);
console.log(i);
})()
// 输出结果: ->0->1->2->3->4->5
阅读全文 »

React中遇到的问题

发表于 2019-09-24 | 更新于: 2020-02-24 | 分类于 React

父=>子组件通讯

父组件:

1
2
3
4
5
6
7
8
9
10
11
//元素:
<child onRef={this.onRef} />
<p onClick={this.click.bind(this)}>父组件的点击事件</p>

//方法:
click = () =>{
this.child.childFunction()
}
onRef = (ref) =>{
this.child=ref
}
阅读全文 »

Nodejs 模块 (三)

发表于 2019-09-24 | 更新于: 2019-09-24 | 分类于 Nodejs

核心模块

  • node自带的模块

util

  • util util.inherits
  • util.promisify

1)promisify

  • ncp 拷贝的一个模块
  • 先初始化环境 npm init
  • 安装ncp yarn add ncp
阅读全文 »

Node-npm

发表于 2019-09-24 | 更新于: 2021-05-25 | 分类于 Nodejs

初始化包

package.json

1
npm init

下载包

全局安装 在任意命令行下使用

1
npm i http-server -g
阅读全文 »

Nodejs 模块 (二)

发表于 2019-09-23 | 更新于: 2019-09-23 | 分类于 Nodejs

手写require 继上一篇 深入

  • node中js文件就是一个模块
  • 为什么出现模块的概念 防止命名冲突、可以把同样的功能封装到一起
  • esModule commonjs规范(一个文件是一个模块,module.exports导出给别人使用,require来引用别人的模块)
阅读全文 »

Nodejs中的全局变量、属性

发表于 2019-09-23 | 更新于: 2019-09-24 | 分类于 Nodejs

浏览器中有window对象 this默认就是指的window,浏览器无法直接访问global对象 所以需要window来代理
在node中可以直接访问global

全局变量

1
2
3
4
5
6
7
// 下面的a现在就是全局变量
global.a = 1;
console.log(a); // 1

// let不会声明到全局作用域下
let b = 2;
console.log(global.b); // undefined
阅读全文 »

Homework

发表于 2019-09-23 | 更新于: 2019-10-11 | 分类于 其他

作业1

  • 反柯里化 实现
  • flat 实现
  • Array.prototype.reduce 模拟

作业2

  • 写一篇关于http的文章 1) 借鉴别人的文章 http 不安全 https
  • koa原理 ( 把koa源码看一下 用逼格最高代码来实现一版koa reduce => compose)
    1) compose方法 如何用async+await来重写
    2) compose next方法如何 避免多次调用
    3) ctx.body 没有赋值 如何处理

ES6 理论知识

发表于 2019-09-23 | 更新于: 2019-11-14 | 分类于 ES6

1 关于函数

  • 什么是高阶函数?

    把函数作为参数或者返回值是函数

  • 柯里化函数(函数更加具体,核心像bind,可以保留参数) => 思考:反柯里化(让函数的调用方式变大)
    Object.prototype.toString.call 像这里toString需要在对象的原型上调用,我们希望哪里都可以调用,这就是反柯里化,然函数的调用变大 需要百度

  • AOP(装饰模式) 将函数进行包装

    AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑无关的功能抽离出来,其实就是给原函数增加一层,不用管原函数内部实现

  • 发布订阅(发布者和订阅两者是没有关系的,可以做解耦) 观察者模式(核心是基于发布订阅的,将观察者放到被观察者上面,当被观察者发送变化了会通知观察者 events on emit)

    阅读全文 »

Nodejs 知识梳理

发表于 2019-09-23 | 更新于: 2019-09-27 | 分类于 Nodejs

node 是一个js 的运行时 js BOM DOM ECMASCRIPT => node中只有ECMASCRIPT + 模块

  • import export babel-> commonjs mjs
  • node 干什么? 写些脚本(比如说webpack,都是用node写的) 中间层 服务端渲染(vue react)可以实现前后端分离 可以解决前端跨域问题、数据处理
  • 实现高性能的web服务
阅读全文 »

Nodejs 模块 (一)

发表于 2019-09-23 | 更新于: 2019-09-23 | 分类于 Nodejs

知识点

es模块、commonjs模块

  • 它们的特点都是一样的 每个文件都是一个模块
    commonjs只是一个规范
    1)每个文件都是一个模块
    2)如果要使用 就需要require
    3)如果要给别人用 就需要module.exports

怎么实现模块化? 防止命名冲突
命名空间 无法彻底解决命名问题
自执行函数 node让js拥有了在服务端执行的能力,可以读写文件


阅读全文 »

JavaScript基础知识梳理

发表于 2019-09-17 | 更新于: 2021-05-25 | 分类于 js基础

for…of & for…in

for…of

mdn上的解释:for…of语句在可迭代对象(包括 Array,Map,Set,String,TypedArray,arguments 对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function* foo(){
yield 1;
yield 2;
}
for (let o of foo()) {
console.log(o);
}
// 1 2

let str = 'abcd';
for (let i of str) {
console.log(i);
}
// "a" "b" "c" "d"
阅读全文 »

Generator & async await

发表于 2019-09-16 | 更新于: 2019-09-20 | 分类于 ES6

生成器 生成迭代器的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 返回值叫迭代器
function * read(){ // 这个叫生成器函数
yield 1; //产出
yield 2;
yield 3;
}
// iterator 迭代器
let it = read();
console.log(it);
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
// 可以把上面都执行下 每次执行到yield都会停一下,执行next()才会继续走
阅读全文 »

EventLoop (浏览器 & node)

发表于 2019-09-16 | 更新于: 2020-04-06 | 分类于 Nodejs

宏任务微任务

  • 微任务: promise.then ,MutationObserver,process.nextTick

  • 宏任务:script ,ajax , 事件,requestAnimationFrame, setTimeout ,setInterval ,setImmediate (ie下),MessageChannel ,I/O ,UI rendering。

微任务 会比宏任务快,js中会先执行script脚

阅读全文 »

类中的装饰器

发表于 2019-09-16 | 更新于: 2019-09-24 | 分类于 ES6

装饰器 装饰模式

在执行类之前可以进行包装

1
2
3
4
5
6
7
8
9
10
11
12
@type
class Animal{

}
// type(Animal)
// 类似于这样 可以当做是它的语法糖

// 默认会调用这个type并且把Animal传过来
function type(Constructor){
console.log(Constructor); // webpack环境运行
}
// class Animal {}
1
2
3
4
5
6
7
8
9
10
11
12
@type1
@type2
class Animal{

}
function type1(Constructor){
console.log(1);
}
function type2(Constructor){
console.log(2);
}
// 打印 2 1 执行顺序 先走最近的
阅读全文 »

ES6-Class 类

发表于 2019-09-12 | 更新于: 2019-09-20 | 分类于 ES6

__proto______ 指向所属类的原型 浏览器中 (1).__proto________打印下
prototype 所有类都有一个prototype属性
constructor prototype.constructor 每个类的原型上都有这个属性

继承: 继承公共属性     继承实例上的属性
1
2
3
4
5
6
7
8
9
// 每个类上都有constructor 放置实例上的属性
class Animal{
constructor(){
this.type = '哺乳类';
}
}
let animal = new Animal();
console.log(animal); // Animal { type: '哺乳类' }
console.log(animal.hasOwnProperty('type'));// true 实例上的属性
阅读全文 »

模板引擎

发表于 2019-09-12 | 更新于: 2019-09-24 | 分类于 其他

如何实现一个模板引擎?

常见模板引擎 ejs jade handlerbar underscore nunjunks

阅读全文 »

箭头函数 & reduce & compose

发表于 2019-09-11 | 更新于: 2019-10-17 | 分类于 ES6

箭头函数

特点:没有this arguments prototype ,没有就向上找

1
2
3
4
5
6
7
// node中没有window 代码在浏览器中执行 
let fn = ()=>{
// console.log(arguments);
// 报错:VM364:2 Uncaught ReferenceError: arguments is not defined
console.log(this); // window
}
// console.log(fn(1,2,3));
阅读全文 »

常见的数据结构

发表于 2019-09-11 | 更新于: 2019-09-20 | 分类于 js基础

队列 栈 链表 集合 hash表 树 图

1) 队列 先进先出 (排队)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Queue{
constructor(){
this.queue = [];
}
enqueue(element){
this.queue.push(element);
}
dequeue(){
this.queue.shift();
}
}
let queue = new Queue();
queue.enqueue(1);
queue.enqueue(2);
queue.dequeue();
console.log(queue.queue); // [ 2 ]
阅读全文 »

用es5来模拟es6中的class

发表于 2019-09-11 | 更新于: 2020-05-14 | 分类于 ES6

1) new的原理

es5中没有类 用构造函数来模拟类

1
2
3
4
function Animal(){

}
// es5中的类可以当做函数来调用 ,es6中不行

首先来判断是否是通过new来调用的

1
2
3
4
5
6
7
function Animal(){
if( !(this instanceof Animal)){
throw new Error('NOT NEW');
}
}
// Animal(); // Error: NOT NEW
new Animal();
阅读全文 »

原型链 & 继承

发表于 2019-09-09 | 更新于: 2020-05-14 | 分类于 js基础

原型链

1)定义对象的几种方法

阅读全文 »

ES6模块

发表于 2019-09-06 | 更新于: 2019-09-20 | 分类于 js基础

es6模块化

面试会问到 怎么使用es6模块 esModule模块(es6) commonjs模块(node模块)
尽量不要混用
import 导入 export导出 每个文件都是一个模块
现在新建一个a模块 (a.js)

1
2
import {c, b} from './a';
console.log(c, b); // 1 2
阅读全文 »

Reflect & Symbol

发表于 2019-09-04 | 更新于: 2020-05-14 | 分类于 ES6

Reflect

反射 Object.definedProperty
里面有部分Object的方法 放到了Reflect 功能基本一致
Proxy中能代理的方法 Reflect都可以实现

阅读全文 »

Object.defineProperty-Proxy

发表于 2019-09-04 | 更新于: 2019-09-20 | 分类于 ES6

getter    setter

1
2
3
4
5
6
7
8
9
let obj = {
get a(){
// todo
console.log("取值了")
return 1;
}
}
console.log(obj.a); // 1
// 这样设置和直接给obj.a赋值是一样的,不过可以在取值的时候todo
阅读全文 »

... 解构赋值、Set&Map

发表于 2019-09-04 | 更新于: 2019-11-14 | 分类于 ES6

结构赋值

解构的方式都是根据key来实现的

1
2
3
4
5
6
7
8
9
// 数组
let arr = ['姓名','年龄'];
let [name, age] = ['姓名','年龄'];
console.log(name, age);
// 执行结果: 姓名 年龄

let [,age] = ['姓名','年龄'];
console.log(age);
// 执行结果: 年龄
1
2
3
4
// 对象
let {name,age} = {name: '加菲', age: 18};
console.log(name,age);
// 执行结果: 加菲 18

用:来重新命名 用=来赋值默认值

1
2
3
let {name,age:age1, addr="杭州"} = {name: '加菲', age: 18};
console.log(name,age1);
// 执行结果: 加菲 18
阅读全文 »

var-let-const

发表于 2019-09-03 | 更新于: 2019-11-14 | 分类于 ES6

一阅 1处


使用 var 的问题

1)声明的变量默认声明到全局(会污染全局作用域)

作用域分为:全局作用域、函数作用域

1
2
3
var a = 1;
// window.a; 这是全局作用域中的a
// 当我们想把a放在作用域里面 可以套一个函数

let + {}

1
2
3
4
5
6
7
8
9
10
{
let a = 1;
console.log(a);
}
console.log("外面", a);
/* 打印结果:
1
a is not defined
*/
// 如上 {}作用域 + let 可以实现一个作用域
阅读全文 »

Promise

发表于 2019-08-28 | 更新于: 2020-05-14 | 分类于 ES6

解决

1) 解决并发问题 (同步的多个异步方法的执行结果 promise.all)
2) 解决链式调用问题 (先获取name,再获取age,再获取…) 回调地狱 解决多个回调嵌套的问题(不是完全解决,.then.then也是回调)

分析

Promise 是一个类

1) 每次new一个 Promise 都需要传递一个执行器,执行器(executor)是立即执行的

1
2
3
4
5
6
new Promise( ()=>{
console.log("init");
})
console.log('1');
// 这里先执行的是 init 再执行 1
// 结果:init 1

阅读全文 »

柯里化

发表于 2019-08-26 | 更新于: 2020-05-07 | 分类于 js基础

概念

将一个函数拆分成多个函数


示例

1
2
3
4
5
6
// 判断类型 Object.prototype.toString.call
const checkType = (content, type) =>{
return Object.prototype.toString.call(content) === `[object ${type}]`;
}
let b = checkType(123, 'Number');
console.log(b); // true

示例拓展:

1
2
3
let b = checkType(123, 'Number');
// 当用户传入向 'Number'这样的会有传入拼写错误的情况
// 如果用户传入的 'Number'错误,那这个方法就会返回false
阅读全文 »

React 事务

发表于 2019-08-26 | 更新于: 2019-09-20 | 分类于 其他

概念

开始的时候 做某件事 结束的时候再做某件事


示例

1
2
3
const perform = ( ()=>{
console.log("说话");
})
阅读全文 »

高阶函数

发表于 2019-08-26 | 更新于: 2019-09-20 | 分类于 js基础

概念


1、一个函数的参数 是一个函数

1
2
3
4
function a(){
}
a( ()=>{})
// 函数a就是一个高阶函数

2、一个函数 返回一个函数

1
2
3
4
5
function a(){
return function (){
}
}
// 函数a就是一个高阶函数
阅读全文 »
加菲

加菲

啦啦啦~~~加油码字

82 日志
23 分类
38 标签
© 2021 加菲
由 Hexo 强力驱动
|
主题 — NexT.Gemini v5.1.4