导图社区 计算机学习笔记
这是一篇根据计算机学习笔记而归纳整理的思维导图。随着计算机在我们日常生活中扮演着越来越重要的角色,我们对计算机的学习也应该更深入、全面。
编辑于2021-09-10 10:10:25学习笔记·
JS相关
this
this是在运行时绑定的,并不是编写时绑定,它的上下文取决于函数调用时的各种条件。 this只取决于函数的调用函数。
this的绑定规则
1、由new调用则绑定到新创建的对象上 2、call或者apply、bind调用则绑定到指定对象 3、上下文对象调用则绑定到上下文对象上 4、默认:严格模式下绑定到undefined否则绑定到全局对象
函数独立调用(默认规则)
只有函数运行在非严格模式下,默认绑定才能绑定到全局对象。 但是在严格模式下调用函数则不影响默认绑定() this在严格模式下绑定的是undefined。 决定this对象的不是调用位置是否处于严格模式,而是函数体是否处于严格模式。如果函数体处于严格模式则this会被绑定为undefined,否则会被绑定到全局对象。 function foo () {} foo()
隐氏绑定
调用位置是否有上下文对象,或者说被某个上下文对象拥有或者包裹。 当函数引用有上下文对象时,隐氏绑定规则会把函数调用中的this绑定到这个上下文对象。 对象引用链中只有上一层或者说最后一层在调用位置中起作用。 function foo () {} var obj = { a: 2, foo: foo }
隐式丢失
最常见的this绑定问题就是被隐氏绑定的函数会丢失绑定对象,也就是说会应用默认绑定,从而把this绑定到全局对象或者undefined上,取决于是否是严格模式。
显示绑定
直接指定this的绑定对象 function foo () {} var obj = { foo } obj.foo()
硬绑定
call apply bind
new绑定
使用new来调用函数,或者说发生函数调用时,会自动执行下面的操作: 1、创建或者说构造一个全新的对象。。 2、这个新对象会被执行Prototype连接。 3、这个新对象会绑定函数调用的this,即this指向的是这个新对象。 4、如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。 function foo () {} var bar = new foo() new调用情况下this会绑定到新创建的对象上。
优先级
默认绑定的优先级最低 显示绑定优先级最高 new > 显示 > 隐式 > 默认
绑定例外
把null或者undefined作为this的绑定对象传入call、apply、bind时,这些值在调用时会被忽略,实际应用的是默认绑定。
软绑定
箭头函数
箭头函数的this是根据外层作用域来决定的。 在调用时并不能再用call等更改箭头函数的作用域,除非修改其上层的作用域。
原型
当试图引用对象的属性时会触发GET操作,对于默认的GET操作来说,第一步是检查对象本身是否有这个属性,如果没有就会通过原型链去查找原型对象上是否有这个属性,知道找完整条原型链,如果还没有找到,就会返回undefined
最终指向为Object.prototype
所有的原型链最终都会指向Object.prototype
更新对象属性
向对象a上更新属性值步骤: 1、如果有这个属性,则直接修改属性值,且不会在去找原型链上的属性,触发get即获取该属性时也只会从a上获取而不会在去原型链上查找,即属性屏蔽。 2、如果没有,会根据原型链查找 如果原型链上没有,则会添加到a对象上 如果原型链上有, 3、
原型链没有则直接设置
属性屏蔽
如果对象有属性且原型链也有就会发生属性屏蔽。 获取该属性时只会从原型链最底层获取,会自动屏蔽掉上层原型链的属性值。 关于属性屏蔽,以 myobj.foo = 'foo'为例: 1、myobj中没有但在原型链上层存在foo属性,并且没有被标为只读(writeable: false),那就会直接在myobj中添加foo属性,它是屏蔽属性。 2、如果上层原型链中的foo被标为只读属性,则无法修改也无法在myobj中创建foo属性,而且严格模式下还会抛出错误,非严格模式下赋值语句会被忽略。 这个限制只会出现在=操作中,Object.defineproperty并不会受影响。 3、如果原型链上层存在且是一个setter,则赋值时一定会调用这个setter,foo不会被添加到myobj上,也不会重新定义foo的setter。 三种情况中只有第一种是屏蔽属性。
函数式编程
模块化规范
CommonJS规范
一个文件就是一个模块
每个模块都有单独的作用域
通过module.exports导出成员
通过require函数载入模块
同步模式加载模块
node环境内置的规范
ESM
语言层面实现了模块化
基本特性
自动使用严格模式
每个esm都运行在私有作用域中
esm是通过cors方式请求js模块的
esm的script标签会延迟执行脚本, 等待网页渲染完成之后才会执行js脚本
导入导出
export
导出的是内存中的引用关系
export导出的并不是对象字面量 export default导出的才可以是对象字面量
export {age, name} // 并不是对象字面量,而是一个固定的用法 export default {age, name} 导出的是一个对象,import时也不是解构
导出的成员是一个常量,外部不可修改
export {name} from './module.js'直接导出导入的模块
import
文件名要写全,用打包工具可以自动处理
引用文件可以是cdn地址
要有相对路径关系,没有则认为是第三方模块
import './module.js'加载却不提取
import * as mod from ''
动态导入import('./module.js')返回promise
import('./module.js').then(() => {2021-07-20})
浏览器兼容-Polyfill
webpack
特性
代码拆分
模块化,对整个前端项目模块化
js=>script
css=>style标签
使用
loader
css-loader
处理css文件
style-loader
将css-loader处理的文件以一个style标签的形式添加到html中
file-loader
加载代码中用到的文件内容,img等
url-loader
将图片转成base64
配置limit属性,如果大于则还是调用file-loader
babel-loader
es6 => es5,需要配置babel-loader的options
工作原理
输入到输出的一个转换,一定要输出js代码,因为输出内容是被包在一个函数中,不是js可能会报错
工作管道,前面的返回结果作为后面loader的输入
loader只是在加载模块的环节工作
基本形式
为一个函数: source => source
工作原理
根据配置找到打包的入口文件,根据入口文件中的import/require得到所有的依赖资源的数据,最终生成一个文件依赖关系树
递归依赖树,根据配置中的rules找到对应类型文件的加载器loader
生成打包结果
插件
常用插件
clear-webpack-plugin清除上次打包结果目录
html-webpack-plugin 自动生成index.html
copy-webpack-plugin 赋值项目中不需要打包的静态文件
插件原理
插件几乎可以触及到webpack的每一个环节,挂载在不同的webpack生命周期钩子上
基本形式
一个函数或者包含apply方法的对象
class MyPlugin { apply (compiler) { compiler.hooks.emit.tap('myPlugin', complation => { }) } } module.exports = MyPlugin
webpack Dev Server
contentBase
内存访问项目中的资源
proxy代理api地址
devServer: { proxy: { '/api': { target: 'https.target.com' } } }
sourceMap
打包结果逆向转为源代码,调试解决问题
HMR
new webpack.HotModuleReplacementPlugin
处理js文件需要用到 HMR API
代码分割
多入口打包
使用多页应用
optimization: { splitChunks: { chunks: 'all' } }
动态导入
MiniCssExtractPlugin
动态导入css
webpack源码
定位入口
node webpack
实际上是执行 webpack.cmd webpack.cmd又去找了webpack/bin/webpack.js
网络
浏览器生成消息
生成HTTP请求消息
输入网址,解析url生成请求消息
根据url的协议来判断用何种方式访问数据,解析成协议+服务器域名+文件路径名
HTTP基本思路
1、客户端向服务器发送请求消息
HTTP的主要方法
GET
获取URI指定的信息。如果URI指定的是文件,则返回文件内容,如果URI返回的是CGI程序,则返回该程序的输出数据。 URL长度和浏览器有关 IE8最大是2083
POST
从客户端向服务器发送数据。一般用于发送表单中填写的数据等情况。
HEAD
和GET基本相同。不过只返回HTTP的消息头并不返回数据得内容。用于获取文件最后更新时间等属性信息。
OPTIONS
用于通知或查询通信选项
PUT
替换URI指定的服务器上的文件。如果URI指定的文件不存在,则创建该文件。
DELETE
删除URI指定的服务器上的文件
TRACE
将服务器收到的请求行和头部直接返回给客户端。用于在使用代理的环境中检查改写请求的情况。
CONNENT
使用代理传输加密消息时使用的方法
生成HTTP请求头消息
HTTP消息格式
请求头
消息头
请求空行
消息体
2、服务器收到请求后 对请求进行处理并返回响应
3、客户端收到消息读取其数据并显示
如果只有文本则直接结束
如果包含图片
如果包含图片,会在网页中相应位置嵌入表示图片文件的标签控制信息。 浏览器会在显示文字时搜索相应标签,如果遇到图片标签,则会在屏幕上留出来图片的空间,然后再次访问web服务器获取图片并显示在预留的空间中
向DNS服务器查询WEB服务器的IP地址
浏览器可以生成消息但不具有发送消息的能力,需要委托操作系统来完成。 委托系统发送消息时,需要提供通信对象的ip而不是域名,所以需要先查询ip地址才能发送消息。
全世界DNS服务器的大接力
委托协议栈发送消息
用电信号传输TCP/IP数据
从网线到网络设备
通过接入网进入互联网内部
服务器端的局域网有什么玄机
请求到达WEB服务器,响应返回浏览器
前端性能优化
web性能指标
rail 从用户体验角度给定一个标准
response 尽快的响应用户,应在100ms以内, 超过100ms用户就会感到延迟
animation 展示动画时,每一帧应该以16ms(每秒60帧) 进行渲染,可以保持动画效果的一致性,避免卡顿,除去绘制新帧的时间,留给执行js的时间大概在10ms
idle 空闲 当使用js线程时, 应该把任务划分到执行时间小于50ms的片段中, 这样可以释放进程用以交互
load 应该在1s内加载完成网站并可以交互
FCP 首次绘制(白屏时间) 2秒以内是比较好的
优化方案
移除造成阻塞的资源
压缩css
移除没有用的css
预先请求所需资源
减少http请求
避免重绘
预加载主要的请求
避免比较大的网络请求
避免过大的dom
最小化初始请求深度
前端加载时确保文本显示
保持较少的请求数量和较小的请求数据大小
LCP 最大内容绘制 尽量在2.5s内展示最大内容
img元素、video封面图、url背景图、 包含文本节点或其他内联元素的块级元素
img元素、video封面图、url背景图、 包含文本节点或其他内联元素的块级元素
FID 首次输入延迟 尽量在100ms以内
浏览器接收到用户操作时,主线程在忙于一个耗时较长的任务
TTI 页面到达完全可交互的时间 最好是0-3.8s
TBT 总阻塞时间 300ms以内
CLS 累计布局偏移 最好是0.1ms之内
Web Vitals
提供了一个简化的指标标准
LCP
FID
CLS
测量web vitals
性能测试工具 如lighthouse
使用web vitals库
使用浏览器插件web vitals
性能检测
不要通过单一指标来决定
不要通过一次性检测决定
不要仅在开发环境检测,也要在发布之后检测
检测工具
lighthouse
浏览器任务管理器中的lighthouse
WebPageTest
https://www.webpagetest.org/
DevTools
浏览器任务管理器
network、coverage、memory、 performance、performance monitor等面板
性能监控API
持续的性能监控方案
前端页面的生命周期 从输入url到呈现经过什么过程
1、浏览器接收到url,到网络请求的发起
1、解析url,根据域名找到ip
2、建立http请求
dns解析 找到域名对应的ip
DNS解析出ip
查找浏览器缓存
系统自身DNS缓存
host文件
没有缓存则找域名服务提供商
查根域名服务器
查COM顶级域名服务器
查权限域名服务器
找到ip或者报错
通信链路的建立即TCP/IP连接 即找到IP地址之后建立通往该地址的路径
网络模型
OSI模型
应用层
表示层
会话层
传输层
网络层
数据链路层
物理层
TCP/IP模型
应用层
传输层
网络层
数据链路层
建立TCP连接
2、发送请求
通常先到达后端的反向代理服务器
核心目的是加一些功能
负载均衡
安全防火墙
加密及SSL加速
数据压缩
解决跨域
缓存静态资源
3、服务器接收到请求并转到具体的处理后台
反向代理收到请求之后
首先会有统一的验证环节
验证通过之后进入后台程序执行阶段
完成计算后,后台会以一个HTTP响应包的形式发送给前端并结束请求
HTTP相关协议特性
长链接
http2.0
一个连接可请求多个资源
二进制分帧,实现高吞吐量
服务器端推送
以前是一个请求带一个响应,现在服务器可以向客户端的一个请求发出多个响应
设置请求优先级
http头部压缩,减少报文传输体积
4、前后台之间的http交互和涉及的缓存机制
三次握手四次挥手
缓存
协商缓存
强缓存
5、浏览器接收到数据包后的关键渲染路径
1、构建渲染模型
构建DOM树模型
构建CSS样式表模型
生成渲染树,即合并dom树和css渲染树,只包含可见的节点
2、通过渲染树绘制页面
从dom树根节点向下遍历,忽略所有不可见节点
在css树种为每个可见的子节点找到对应的规则并应用
布局阶段,根据得到的渲染树,计算节点在试视图中的位置和大小
绘制阶段,将每个节点的具体绘制方式转为屏幕上的像素
6、js引擎解析的过程
优化方式
最佳实践
1、减少DNS查找
每次主机名的解析都需要一次网络往返,增加了请求时间
2、重用TCP连接
尽可能的使用长连接,以消除因TCP握手和慢启动造成得到延迟
3、减少HTTP重定向
HTTP重定向需要额外的DNS查询、TCP握手等非常耗时
4、压缩资源
js、css、img
5、使用缓存
HTTP缓存
CDN缓存
ServiceWorker缓存
6、使用CDN
把数据放在离用户更近的地方,可以明显减少每次TCP连接网络延迟,增大吞吐量
7、删除没必要请求的资源
8、客户端使用缓存
9、内容在传输前先压缩
进了把传输字节减少到最小
10、消除不必要的请求开销
减少请求的HTTP首部数据
11、并行处理请求和响应
请求和响应的排队会导致延迟,可以尝试并行的处理请求和响应 即利用多个HTTP1.1连接实现并行下载,在可能的情况下使用HTTP管道技术
12、针对协议版本采取优化措施
升级到HTTP2.0
13、根据需要采用服务端渲染模式
14、采用与渲染的方式快速加载静态页面
页面渲染的极致性能,比较适合静态页面
浏览器相关优化
请求和响应优化
核心思路
更好的连接传输效率
更少的请求数量
更小的资源大小
合适的缓存策略
DNS解析优化
尽量减少DNS请求次数
尽量减少页面中的域名
至少2个但不超过4个
一个域名最多可以6个并发,多个域名提高并发数量
DNS缓存
由DnsCache注册表决定,chrome firefox都是1分钟
清除DNS缓存
chrome://net-internals/#dns
windows清除
ipconfig /displaydns 查看dns缓存
ipconfig /flushdns 清除dns缓存
macos
sudo killall -HUP mDNSResponder
dns-prefetch dns预解析
<link ref="dns-prefetch" href="https://baidu.com" />
只能跨域解析,即只能解析除当前页面的域名
慎用预解析
多页面重复DNS预解析会增加重复的DNS查询次数
隐式dns-prefetch
即使不设置dns-prefetch,浏览器也会对用到的静态资源的域名进行预解析
对于页面中没有出现的域名,要手动设置预解析
高版本浏览器才会支持
延长DNS缓存时间
尽可能使用A或AAAA代替CNAME
使用CNAME可以使a.com跳转到b.com
A记录指向ipv4地址 AAAA指向ipv6地址
使用cdn加速域名
自己搭建DNS服务
HTTP长连接
短连接
每进行一次http通信就要断开一次TCP连接,下次使用重新建立
为了解决短连接的问题,有些浏览器会使用Connect: keep-alive 这样不会关闭TCP连接
HTTP1.1版本引入长连接
管道机制
即在同一个TCP连接中,客户端可以发送多个请求 可以做到同时并行多个请求
content-length字段会声明本次回应的数据长度
分块传输编码
缺点
同一个TCP连接中,所有通信是按次序进行的,容易阻塞
可以减少请求数
同时多开持久连接
HTTP2.0
二进制协议
头和数据都是二进制,统称为帧
多工
客户端和浏览器可以同时发送多个请求和回应 不用按顺序一一对应
双向、实时的通信
数据流
请求或响应的数据包叫做数据流
每个流都有id,客户端为奇数,服务端偶数
可以指定数据流优先级,越高越早回应
头信息压缩
http1.0协议不带状态,每次请求要带很多重复东西
可以压缩后发送 gzip或compress
服务端和客户端可以共同维护一张头信息的表
服务器推送
压缩资源
文本内容比较小 可以用这个压缩
content-encoding accept-encoding
图片
HTTP2.0支持头部压缩
压缩内容gzip等
维护头信息表
请求数据压缩
开发者用代码压缩 同时后端自己解压
HTTP缓存
分为强缓存和协商缓存。 区别: 强缓存: 缓存命中时,浏览器是否需要向服务端询问以协商缓存的相关信息,进而判断是否需要就响应内容进行重新请求。
强缓存
直接使用本地缓存 memory cache disk cache
1、一种是设置Expires
严重依赖本地时间
2、一种是直接设置cache-control 设置过期时间,优先级比Experes高
no-store 不缓存
no-cache 强制进行协商缓存
max-age 以秒为单位
s-maxage
代理服务器缓存过期时间
private
不可被代理服务器缓存,只可被浏览器缓存
public
可被代理服务器缓存 静态资源等
Cache-control:public,max-age=1000
css文件放在dist中,因为css加载一次就可以了
js、img放在memory中,因为js可能随时执行,放在dist中io时间长,可能会阻塞
协商缓存
原来的协商缓存
使用本地缓存之前,先向服务器发 送一次请求,判断本地缓存是否有效, 解决强缓存长时间不更新的问题
响应设置last-modified:时间 和 Cache-control:no-cache
服务端根据If-Modified-Since 和文件修改时间判断缓存是否失效
缺点
根据时间戳判断不是根据内容变化
比对单位为秒,不能精确到毫秒级别
服务器不能根据修改的时间戳来识别真正的更新
基于ETag的协商缓存 HTTP1.1规范新增了ETag标签, 内容为服务端生成的哈希串
服务端设置etag 再次请求时匹配if-mone-match
还是需要设置Cache-control: no-cache
缺点
服务器生成etag需要额外开销, 可能影响服务器性能
etag分为弱验证和强验证
强验证根据资源内容生成etag
弱验证根据资源的部分属性值 修改时间等
生成速度快但无法保证每个字节一致 有可能不准确导致缓存有效性降低
缓存使用策略
强制缓存优先级高于协商缓存
可以拆解静态资源
index.html应该设置为协商缓存 确保能及时更新
img基本是替换新图片所以设置强缓存
css可能会不定期更新,使用版本号同时使用强制缓存 更新之后请求新的url
js同css采用版本号和强缓存
如果敏感信息不希望被代理服务器缓存可以设置 Cache-control:private
注意事项
1、拆分源码,分包加载
2、预估资源的缓存时效
为强制缓存设置合适的max-age
为协商缓存提供验证更新的ETag标签
3、控制代理服务器的缓存
4、避免网址的冗余
相同的资源避免设置不同的地址,不然会缓存多份资源
5、规划缓存的层次结构
CDN缓存
针对静态资源
cdn域名与主站域名不同
1、避免静态资源携带不必要的主站信息 cookie等
2、同域名下并发数限制
渲染相关优化
浏览器获取HTML到展示
1、获取HTML并构建DOM树
2、处理CSS并构建CSSOM树
3、将DOM树和CSSOM树合并成render树
4、根据渲染树来布局,计算每个节点的几何信息
5、将节点绘制到屏幕上
核心步骤
js处理,dom操作、动画等
计算元素的最终样式
页面布局,对元素尺寸即位置进行计算
绘制,绘制可视内容,颜色边框阴影等
合成,页面不同部分被绘制到不同图层上, 最终还要合成图层
渲染优化方式
关键渲染路径优化
让用户最快的看到首次渲染内容
影响因素
关键资源数量
首次渲染必须要用到的资源
关键路径长度
1、某些资源加载要等前面资源加载完成之后 2、资源很大,整体下载时间较长
关键字节数量
关键资源进量的小
优化方式
优化DOM
1、注释、空格、换行也会生成一个dom节点,过多会影响DOM树构建速度 2、冗余代码应该删除,保证HTML精简
缩小文件尺寸
压缩文件
使用HTTP缓存
优化CSS加载
js经常受阻于css,所以尽可能将非必要的css标记为非关键资源,并确保尽可能减少关键css数量,缩短传输时间
阻塞渲染的CSS
关键css也可以内联到html中 但不能太多
避免在css中使用@import
@import会增加额外的关键路径长度 加载时会串行加载,不用的话会并行加载
优化JS加载
1、文件尽量小,压缩,使用缓存等
2、异步加载
defer
<script src="./index.js" defer></script> 这种特性的script不会阻塞页面 总要等到dom解析完毕,但在DOMContentLoaded事件之前执行
多个defer脚本加载是并行但是执行是串行, 即使后面js先加载完毕也要等前面js加载完才执行
async
异步并行加载,这点和defer类似
加载完先执行,这点和defer不同 不能保证js加载顺序
预加载
preload 预加载底下的js
head标签中加link <link rel="preload" href="./index.js"> 同时script标签放在底部
prefetch 预加载将来可能用到的资源 注意是非当前页面的资源
比如index.html页面跳转到index2.html prefetch可以在index.html中加载
3、避免运行时间长的js
4、js内容放下面(关键js不推荐)
优化JS执行
进量避免js操作dom,动画尽量用css
requestAnimationFrame
使用web worker
js是单线程执行,如果js执行时间过长,就会阻塞其他工作任务, 所以一些纯计算工作可以放在web worker上处理。 在web worker上执行的子线程任务不会影响主线程,执行完成之后再把结果返回给主线程。
注意事项
web worker一旦创建就会一直执行 所以执行完后要及时关闭
DOM限制
web worker只能访问 navigator和location
web worker无法读取DOM对象,也就无法使用document、window、parent等
文件读取限制
无法读取文件,只能加载来自网络的脚本
通信限制
子线程和主线程不在同一个上下文内,所以无法直接通信 只能通过消息完成
脚本执行限制
可以用XMLHTTPRequest发起ajax 但不能使用alert和confirm弹窗
同源限制,子线程执行的js代码需要与主线程同源
使用方法
防抖和节流
计算样式优化
减少要计算样式的元素数量
css匹配规则是从右往左
使用类选择器替换标签选择器
避免使用通配符* 做选择器
使用通配符意味着要遍历页面中的每一个元素,元素数量多时性能开销很大
降低选择器的复杂性
尽量使用BEM规范
希望每行css代码只有一个选择器 对选择器的命名要求通过以下三个符号组合实现
中划线-,仅作为连字符使用,表示某个块或子元素的多个单词之间的连接符
单下划线_,作为描述一个块或其子元素的一种状态
双下划线__,作为连接块与块的子元素
示例:type-block__element_modifier
type-block是元素的名字 element是子元素,子元素有一个修饰符叫modifier <ul class="type-block"> <li class="type-block__element_moifier"></li> <li class="type-block__element_browse"></li> </ul>
页面布局优化 重绘和回流
浏览器对元素的几何属性进行计算并最终绘制的过程, 元素尺寸改变时都会触发页面的重新布局,作用范围涉及到整个文档, 所以会带来大量的性能开销
概念
重绘
元素外观等属性修改,而几何属性没有修改,只重新绘制当前元素
回流
DOM几何元素更改后,浏览器重新计算页面DOM元素的位置和大小
优化方式
触发页面布局和重绘的操作
对DOM几何属性的修改
width,height,padding,margin,left,top等
更改DOM结构
获取某些特定属性值
offsetWidth,offsetHeight等
尽量避免对样式的频繁改动
尽量使用类名统一修改,按条的话每一行修改都会触发重绘和回流
缓存对敏感属性值的计算
使用requestAnimationFrame
里面读取的元素属性,是上一帧运行之后的结果,是不会引起重新计算的。 但是如果要修改,则最好放在最后。
微信小程序
生命周期
小程序生命周期
onLanch
小程序启动,全局只会调用一次
onShow
初始化完成,或从后台切到前台
onHide
从前台切到后台
页面生命周期
指每个页面的,已经不是小程序的了
onLoad
页面加载
onShow
页面显示,切前台
onReady
页面就绪(只触发一次),页面初次渲染成功才会触发,后续再渲染就不会触发了
onHide
页面隐藏(切后台)
onUnload
页面卸载(只触发一次)
reacts
虚拟dom对比 执行深度优先策略
执行深度优先策略,即先对比根节点,有子节点的话对比子节点,全部子节点对比挖成之后对比同级节点。  此时对比顺序为: ul(1) => li(2) => p(3) => p(4) => li(5)
元素的更新
节点类型相同 (都是同级节点对比)
文本节点直接更新文本
元素节点,则更新属性,添加事件并删除旧的事件
循环旧的dom元素 看有没有key,如果有
将dom对象存储在一个对象中
循环要渲染的virtualDom的子元素
如果该元素的key已经被存储了,说明该元素已经有了不需要重新渲染
如果没有说明这个元素是新增的,需要渲染
节点类型不同
元素节点,类型不同则不对比, 而是直接用新的虚拟dom重新生成dom并替换掉旧的
组件的更新 要判断是不是同一个组件
是同一个组件
执行组件的更新操作
将最新的props传入组件中
执行组件的render方法生成virtualDom
利用diff方法对比新旧virtualDom并更新
不是
直接调用mountElement方法 将生成的virtualDom添加到页面中
节点对比完成之后,删除同一个父节点(包含其子节点)下的多余节点
文本节点则直接删除出
组件的话调用组件的生命周期函数卸载组件 如果子节点中也有组件,则需要调用子节点的生命周期函数卸载
如果有ref属性,则需要删除通过ref属性传递给节点的dom对象 virtualDom.props.ref(null)
如果有绑定事件则需要删除事件对应的从处理函数
有子节点需要递归删除子节点
fiber
fiber对象格式
{ type: 'div', // 节点类型 (元素、文本、属性) props: '', // 节点属性 stateNode: '',// 节点 Dom 对象 或者组件实例对象 // 节点标记 // 具体类型的分类 // host_root 根节点 // host_component 普通节点 // class_component 类组件 // function_component 函数组件 tag: '', effectTag: [],// 数组, 存储需要更改的 Fiber 对象 parent: '', // 当前 Fiber 的父级 Fiber child: '', // 当前 Fiber 的子级 Fiber sibling: '', // 当前 Fiber 的下一个兄弟 Fiber alternate: '' // Fiber 的备份 Fiber 比对时使用 }
react源码
jsx转为ReactElement
jsx是不能直接在浏览器运行的,需要babel进行转换。 babel转换后的代码如下: React.createElement("div", null);
首先jsx要经过babel转换 转换结果类似于:React.createElement("div", null);
其次在生成DOM时执行 React.createElement("div", null);
分离props属性和特殊属性
如果传入的config不为空,则先处理ref属性和key属性,在循环遍历处理剩下的普通属性。
将子元素挂载到props.children中
获取传入子元素的数量,如果大于1则转为数组并循环赋值,如果为1则直接赋值
给props属性赋默认值
如果defaultProps存在,则遍历defaultProps,判断传入的属性是否有值,如果没有则用defaultProps中的值替换
创建并返回 ReactElement
调用ReactElement,其实就是将参数放在一个对象中返回了
createElement代码
/** * @description: 创建React Element元素 * 1、分离props属性和特殊属性 * 2、将子元素挂在到props.children上 * 3、为props属性赋值默认值 defaultProps * 4、创建并返回 ReactElement * @param {string} type 元素类型 * @param {object} config 配置属性 props ref key等属性 * @param {array | object} children 子节点 * @return {object} ReactElement */ export function createElement(type, config, children) { // 属性名称 // 用于后面的for循环,否则在循环中创建变量会损耗性能 let propName; // Reserved names are extracted // 存储 React Element 中的普通元素属性,即不包含key ref source self const props = {}; // 特殊属性 let key = null; let ref = null; let self = null; let source = null; // 如果config不为空,则分离特殊属性 if (config != null) { // 是否有ref属性 if (hasValidRef(config)) { ref = config.ref; // 在开发环境 if (__DEV__) { warnIfStringRefCannotBeAutoConverted(config); } } // 如果有key if (hasValidKey(config)) { key = '' + config.key; } // 判断self和source属性 self = config.__self === undefined ? null : config.__self; source = config.__source === undefined ? null : config.__source; // Remaining properties are added to a new props object // 遍历config对象 for (propName in config) { // 如果当前遍历到的属性是对象自身属性 // 并且在RESERVED_PROPS对象中不存在这个属性 // RESERVED_PROPS 存储的是特殊属性 if ( hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName) ) { // 则将满足条件的属性添加到 props 对象中(普通属性) props[propName] = config[propName]; } } } // 处理子元素 // 将第三个及之后的参数挂在到 props.children 属性中 // 如果子元素是多个 props.children 是数组 // 如果子元素是一个 props.children 是对象 // 由于第三个参数开始以后都表示子元素 // 所以减去前两个参数的结果就是子元素的数量 const childrenLength = arguments.length - 2; // 如果子元素的数量是1 则直接赋值 if (childrenLength === 1) { props.children = children; } else if (childrenLength > 1) { // 如果大于1 则转为数组 const childArray = Array(childrenLength); for (let i = 0; i < childrenLength; i++) { childArray[i] = arguments[i + 2]; } if (__DEV__) { if (Object.freeze) { Object.freeze(childArray); } } props.children = childArray; } // Resolve default props // 处理默认值 if (type && type.defaultProps) { const defaultProps = type.defaultProps; for (propName in defaultProps) { if (props[propName] === undefined) { props[propName] = defaultProps[propName]; } } } // 开发环境 if (__DEV__) { if (key || ref) { const displayName = typeof type === 'function' ? type.displayName || type.name || 'Unknown' : type; if (key) { // 检测有没有通过props获取key属性 defineKeyPropWarningGetter(props, displayName); } if (ref) { defineRefPropWarningGetter(props, displayName); } } } return ReactElement( type, key, ref, self, source, ReactCurrentOwner.current, props, ); }
React架构
调度层
在React15版本中,采用了循环加递归的方式进行了VirtualDom的比对.由于递归会使用JavaScript自身的执行栈,一旦开始就无法中断,直到任务完成。如果VirtualDom树的层级比较深,VirtualDom的比对就会长期占用JavaScript的主线程,由于JavaScript又是单线程的无法同时执行其他任务,所以在比对的过程中无法响应用户操作,无法即时执行元素动画,造成了页面卡顿的现象。 在React16版本中,放弃了JavaScript递归的方式进行VirtualDom的比对,而是采用循环模拟递归。而且比对的过程是利用浏览器的空闲时间完成的,不会长期占用主线程,这就解决了VirtualDom比对造成页面卡顿的问题。 在window对象中提供了requestIdleCallback API,它可以利用浏览器的空闲时间执行任务,但是它自身也存在一些问题,比如说并不是所有的浏览器都支持它,而且它的触发频率也不是很稳定,所以React最终放弃了requestIdleCallback的使用。 在React中,官方实现了自己的任务调度库,这个库就叫做Scheduler。它可以实现在浏览器空闲时执行任务,而且还可以设置任务的优先级,最高级任务先执行,低优先级任务后执行。Scheduler存储在packages/scheduler文件夹中。
协调层
在React15版本中,协调器和渲染器交替执行,即找到了差异就直接更新差异。 在React16中,这种情况发生了变化,协调器和渲染器不再交替执行。协调器负责找出差异,在所有差异找出之后,统一交给渲染器进行Dom更新。也就是说协调器的主要任务就是找出差异部分,并为差异打上标记。
渲染层
渲染器根据协调器为Fiber节点打的标记,同步执行对应的Dom操作。 既然比对的过程从递归变成了可以中断的循环,那么React是如何解决中断更新时Dom渲染不完全的问题呢? 答案是根本就不存在这个问题。因为在整个过程中,调度器和协调器的工作是在内存中完成的,是可以被打断的,而渲染器的工作被设定成不可被打断,会直接执行完渲染的任务,所以不存在Dom渲染不完全的问题。
双缓存技术
内存中构建并直接替换的技术叫做双缓存技术。 在react中最多会有两棵fiber树,一棵展示在页面中,叫做current Fiber树 在发生更新时,会在内存中构建另一棵树,叫做workInProgress树,当构建完成之后会直接替换掉页面中的current Fiber 树,因为是在内存中所以构建速度是非常快的。
React元素如何渲染到页面中
render阶段
判断传入的节点类型是否正确 1、node可以是元素节点 2、node 可以是document节点 3、node 可以是文档碎片节点 4、node 可以是注释节点但注释内容必须是 react-mount-point-unstable react 内部会找到注释节点的父级 通过调用父级的 insertBefore 方 将 element 插入到注释节点的前面 如果不符合要求则直接报错
render源码 生成rootFiber及fiberRoot的过程
路径:src\react\packages\react-dom\src\client\ReactDOMLegacy.js /** * @description: 渲染入口 * @param {*} element 要渲染的ReactElement * @param {*} container 渲染容器 * @param {*} callback 渲染完成之后执行的回调函数 * @return {*} */ export function render( element: React$Element<any>, container: Container, callback: ?Function, ) { // 检测 container 是否是符合要求的渲染容器 // 即检测 container 是否是真实的DOM对象 // 如果不符合要求就报错 invariant( isValidContainer(container), 'Target container is not a DOM element.', ); if (__DEV__) { const isModernRoot = isContainerMarkedAsRoot(container) && container._reactRootContainer === undefined; if (isModernRoot) { console.error( 'You are calling ReactDOM.render() on a container that was previously ' + 'passed to ReactDOM.createRoot(). This is not supported. ' + 'Did you mean to call root.render(element)?', ); } } // 渲染子树到container中 return legacyRenderSubtreeIntoContainer( // 代表父组件,因为此处是初始化渲染,是没有父组件的,传递null占位 null, element, container, // 是否为服务器端渲染 true: 是服务端渲染,false不是 // 如果是服务端渲染则需要复用 container 内部的dom元素 false, callback, ); }
legacyRenderSubtreeIntoContainer
legacyRenderSubtreeIntoContainer方法的作用是将子树渲染到container容器中,初始化Fiber数据结构并创建fiberRoot和rootFiber对象。 源码如下: // src\react\packages\react-dom\src\client\ReactDOMLegacy.js /** * @description: 将子树渲染到容器中 (初始化Fiber数据结构: 创建 fiberRoot 及rootFiber) * @param {*} parentComponent 父组件 初始渲染传入了null * @param {*} children render方法中的第一个参数,要渲染的 ReactElement * @param {*} container 渲染容器 * @param {*} forceHydrate true 为服务端渲染 false为客户端渲染 * @param {*} callback 组件渲染完成之后的回调函数 * @return {*} */ function legacyRenderSubtreeIntoContainer( parentComponent: ?React$Component<any, any>, children: ReactNodeList, container: Container, forceHydrate: boolean, callback: ?Function, ) { if (__DEV__) { topLevelUpdateWarnings(container); warnOnInvalidCallback(callback === undefined ? null : callback, 'render'); } // 检测 container是否是已经初始化的容器 // react 在初始化渲染时会为最外层容器添加 _reactRootContainer 属性 // react 会根据次属性进行不同的渲染方式 // root 不存在表示初始渲染 // root 存在 表示更新 let root: RootType = (container._reactRootContainer: any); let fiberRoot; // 初始化渲染操作 if (!root) { // 初始化根 Fiber 数据结构 // 为 container 容器添加 _reactRootContainer 属性 // 在 _reactRootContainer 对象中有一个属性叫做 _internalRoot // _internalRoot 属性即为 FiberRoot 表示根节点 Fiber 数据结构 // legacyCreateRootFromDOMContainer // createLegacyRoot // new ReactDOMBlockingRoot -> this._internalRoot // createRootImpl // 其实就是在创建 fiberRoot以及rootFiber并为这两个对象添加一些默认属性 root = container._reactRootContainer = legacyCreateRootFromDOMContainer( container, forceHydrate, ); fiberRoot = root._internalRoot; if (typeof callback === 'function') { const originalCallback = callback; callback = function() { const instance = getPublicRootInstance(fiberRoot); originalCallback.call(instance); }; } // Initial mount should not be batched. unbatchedUpdates(() => { updateContainer(children, fiberRoot, parentComponent, callback); }); } else { // 更新操作 fiberRoot = root._internalRoot; if (typeof callback === 'function') { const originalCallback = callback; callback = function() { const instance = getPublicRootInstance(fiberRoot); originalCallback.call(instance); }; } // Update updateContainer(children, fiberRoot, parentComponent, callback); } // 返回实例对象 return getPublicRootInstance(fiberRoot); }
legacyCreateRootFromDOMContainer
legacyCreateRootFromDOMContainer中初始化了根Fiber对象的数据结构,为container容器添加了_reactRootContainer属性。 相关代码如下: // src\react\packages\react-dom\src\client\ReactDOMLegacy.js /** * @description: 判断是否为服务端渲染 如果不是则清空 container 容器中的节点 * @param {*} container dom对象 * @param {*} forceHydrate 是否是服务端渲染 * @return {*} */ function legacyCreateRootFromDOMContainer( container: Container, forceHydrate: boolean, ): RootType { // 检测是否是服务端渲染 const shouldHydrate = forceHydrate || shouldHydrateDueToLegacyHeuristic(container); // 如果不是 if (!shouldHydrate) { let warned = false; // let rootSibling; // 开启循环 删除 container 容器中的节点 while ((rootSibling = container.lastChild)) { if (__DEV__) { if ( !warned && rootSibling.nodeType === ELEMENT_NODE && (rootSibling: any).hasAttribute(ROOT_ATTRIBUTE_NAME) ) { warned = true; console.error( 'render(): Target node has markup rendered by React, but there ' + 'are unrelated nodes as well. This is most commonly caused by ' + 'white-space inserted around server-rendered markup.', ); } } // 删除子元素 container.removeChild(rootSibling); /** * 为什么要清除 container 中的元素? * 有时需要在 container 中放置一些占位图或者 loading 图以提高首屏加载用户体验 * 就无可避免的要向 container 中添加html标记 * 在将 ReactElement 渲染到 container 之前,必然要先清空 container * 因为占位图和 ReactElement 不能同时显示 * * 所以在加入占位代码时,最好只有一个父级元素,可以减少内部代码的循环次数以提高性能 */ } } if (__DEV__) { if (shouldHydrate && !forceHydrate && !warnedAboutHydrateAPI) { warnedAboutHydrateAPI = true; console.warn( 'render(): Calling ReactDOM.render() to hydrate server-rendered markup ' + 'will stop working in React v17. Replace the ReactDOM.render() call ' + 'with ReactDOM.hydrate() if you want React to attach to the server HTML.', ); } } // createLegacyRoot方法通过实例化ReactDOMBlockingRoot 类创建LegacyRoot return createLegacyRoot( container, shouldHydrate ? { hydrate: true, } : undefined, ); }
createLegacyRoot
createLegacyRoot方法通过实例化ReactDOMBlockingRoot 类创建LegacyRoot // src\react\packages\react-dom\src\client\ReactDOMRoot.js /** * @description: 通过它可以创建 LegacyRoot 的Fiber数据结构 * @param {*} container dom对象 * @param {*} options * @return {*} */ export function createLegacyRoot( container: Container, options?: RootOptions, ): RootType { return new ReactDOMBlockingRoot(container, LegacyRoot, options); }
ReactDOMBlockingRoot
ReactDOMBlockingRoot方法中只是初始化了container的_internalRoot属性值,即fiberRoot对象,并在fiberRoot对象的current属性指向rootFiber对象。 // src\react\packages\react-dom\src\client\ReactDOMRoot.js /** * @description: 通过它可以创建 LegacyRoot 的Fiber数据结构 * @param {*} * @return {*} */ function ReactDOMBlockingRoot( container: Container, tag: RootTag, options: void | RootOptions, ) { this._internalRoot = createRootImpl(container, tag, options); }
createRootImpl
在createRootImpl中调用了createContainer去生成一些对象,其中包含fiberRoot和rootFiber: // src\react\packages\react-dom\src\client\ReactDOMRoot.js function createRootImpl( container: Container, tag: RootTag, options: void | RootOptions, ) { // Tag is either LegacyRoot or Concurrent Root // 服务端渲染相关 const hydrate = options != null && options.hydrate === true; const hydrationCallbacks = (options != null && options.hydrationOptions) || null; // 此处生成 fiberRoot及rootFiber const root = createContainer(container, tag, hydrate, hydrationCallbacks); markContainerAsRoot(root.current, container); // 服务端渲染相关 if (hydrate && tag !== LegacyRoot) { const doc = container.nodeType === DOCUMENT_NODE ? container : container.ownerDocument; eagerlyTrapReplayableEvents(container, doc); } return root; }
createFiberRoot
而fiberRoot和rootFiber是在createContainer中调用的createFiberRoot方法生成的: // src\react\packages\react-reconciler\src\ReactFiberReconciler.js export function createContainer( containerInfo: Container, tag: RootTag, hydrate: boolean, hydrationCallbacks: null | SuspenseHydrationCallbacks, ): OpaqueRoot { return createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks); } // src\react\packages\react-reconciler\src\ReactFiberRoot.js export function createFiberRoot( containerInfo: any, tag: RootTag, hydrate: boolean, hydrationCallbacks: null | SuspenseHydrationCallbacks, ): FiberRoot { const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any); if (enableSuspenseCallback) { root.hydrationCallbacks = hydrationCallbacks; } // 创建根节点对应的 rootFiber const uninitializedFiber = createHostRootFiber(tag); // 为 rootFiber 添加 current 属性 值为 rootFiber root.current = uninitializedFiber; // 为 rootFiber 添加 stateNode 属性,值为 fiberRoot uninitializedFiber.stateNode = root; // 为 fiber 对象添加 updateQueue 属性,初始化 updateQueue 对象 // updateQueue 用于存放 Update 对象 // Update 对象用于记录组件状态的改变 initializeUpdateQueue(uninitializedFiber); return root; }
任务
在updateContainer方法(调用顺序为:render->legacyRenderSubtreeIntoContainer->updateContainer)中最核心的事情是创建一个任务对象,比如渲染DOM。在任务创建完成后会将任务放置在一个任务队列当中,之后就会等待浏览器的空闲时间,当有空闲时间时就会执行这个任务。相关源码如下:
创建任务
// src\react\packages\react-reconciler\src\ReactFiberReconciler.js /** * @description: 计算任务的过期时间,再根据任务过期时间创建Update任务,通过任务的过期时间还可以计算出任务的优先级 * @param {*} element 要渲染的 ReactElement 对象 * @param {*} container Fiber Root 对象 * @param {*} parentComponent 父组件 初始渲染为 null * @param {*} callback 渲染完成执行的函数 * @return {*} */ export function updateContainer( element: ReactNodeList, container: OpaqueRoot, parentComponent: ?React$Component<any, any>, callback: ?Function, ): ExpirationTime { if (__DEV__) { onScheduleRoot(container, element); } // container 获取 rootFiber // current currentTime suspenseConfig 是为了计算任务的过期时间 const current = container.current; // 获取当前距离 react 应用初始化的时间 const currentTime = requestCurrentTimeForUpdate(); if (__DEV__) { // $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests if ('undefined' !== typeof jest) { warnIfUnmockedScheduler(current); warnIfNotScopedWithMatchingAct(current); } } // 异步加载设置 const suspenseConfig = requestCurrentSuspenseConfig(); // 计算过期时间 // 为防止任务因为优先级原因一直被打断而未能被执行 // react 会设置一个过期时间,当时间到了过期时间的时候 // 如果任务还未执行的话,react将强制执行该任务 // 初始化渲染时,任务同步执行不涉及被打断问题 // 过期时间设置成了 1073741823 是固定的 这个表示当前任务为同步任务 const expirationTime = computeExpirationForFiber( currentTime, current, suspenseConfig, ); // 设置FiberRoot.context 首次执行返回一个 emptyContext是一个{} const context = getContextForSubtree(parentComponent); // 初始渲染时 Fiber Root 对象中的context属性值为null // 所以会进入到if中 if (container.context === null) { container.context = context; } else { container.pendingContext = context; } if (__DEV__) { if ( ReactCurrentFiberIsRendering && ReactCurrentFiberCurrent !== null && !didWarnAboutNestedUpdates ) { didWarnAboutNestedUpdates = true; console.error( 'Render methods should be a pure function of props and state; ' + 'triggering nested component updates from render is not allowed. ' + 'If necessary, trigger nested updates in componentDidUpdate.\n\n' + 'Check the render method of %s.', getComponentName(ReactCurrentFiberCurrent.type) || 'Unknown', ); } } // 创建一个待执行任务 const update = createUpdate(expirationTime, suspenseConfig); // 将要更新的内容挂载到更新对象中的payload中 // 将要更新的组件存储至payload中,方便以后读取 update.payload = {element}; callback = callback === undefined ? null : callback; // 判断callback是否存在 if (callback !== null) { if (__DEV__) { if (typeof callback !== 'function') { console.error( 'render(...): Expected the last optional `callback` argument to be a ' + 'function. Instead received: %s.', callback, ); } } // 将callback挂载到update对象中 // 其实就是一层层传递 方便 ReactElement 元素渲染完成调用 // 回调函数执行完成后会被清除 可以在代码的后面加上return进行验证 update.callback = callback; } // 将update对象加入到当前Fiber的更新队列中(updateQueue) // 待执行的任务都会被存储在 fiber.updateQueue.shared.pending中 enqueueUpdate(current, update); // 调度和更新 current 对象 scheduleWork(current, expirationTime); // 返回过期时间 return expirationTime; }
将任务放入队列
/** * @description: 将任务存放于任务队列中,创建单向链表结构存放 update next 用来串联update * @param {*} * @return {*} */ export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) { // 获取当前 Fiber 的更新队列 const updateQueue = fiber.updateQueue; // 如果更新队列不存在,就返回null if (updateQueue === null) { // 仅发生在 fiber已经被卸载 return; } // 获取待执行的 Update 任务 // 初始渲染时没有待执行的任务 const sharedQueue = updateQueue.shared; const pending = sharedQueue.pending; if (pending === null) { // This is the first update. Create a circular list. update.next = update; } else { update.next = pending.next; pending.next = update; } // 将 Update 任务存储在 pending 属性中 sharedQueue.pending = update; if (__DEV__) { if ( currentlyProcessingQueue === sharedQueue && !didWarnUpdateInsideUpdate ) { console.error( 'An update (setState, replaceState, or forceUpdate) was scheduled ' + 'from inside an update function. Update functions should be pure, ' + 'with zero side-effects. Consider using componentDidUpdate or a ' + 'callback.', ); didWarnUpdateInsideUpdate = true; } } }
任务执行前的准备工作 判断是否是同步任务
在scheduleWork方法中(调用顺序为render->legacyRenderSubtreeIntoContainer->updateContainer->scheduleWork)所做的最核心的事情是判断当前任务是否是同步任务。如果是同步任务,则调用同步任务入口函数。 scheduleWork方法其实就是 src\react\packages\react-reconciler\src\ReactFiberWorkLoop.js下的scheduleUpdateOnFiber方法
构建workInProgress Fiber 树的rootFiber
因为初始渲染属于同步任务,所以其入口为performSyncWorkOnRoot函数(src\react\packages\react-reconciler\src\ReactFiberWorkLoop.js)。 当调用这个方法后,就说明已经正式进入了render阶段,为每一个react元素构建workInProgress Fiber树中的Fiber对象。 源码路径: src\react\packages\react-reconciler\src\ReactFiberWorkLoop.js prepareFreshStack方法
构建子集Fiber
rootFiber指的就是id为root的div所对应的Fiber对象,当rootFiber对象构建完成之后,就需要构建其子集Fiber对象。子集的Fiber对象就是render方法的第一个参数,即element。而element是通过workLoopSync方法构建的。 // src\react\packages\react-reconciler\src\ReactFiberWorkLoop.js /** * @description: 构建除rootFiber之外的所有子集fiber对象 * @param {*} * @return {*} */ function workLoopSync() { // workInProgress是一个fiber对象 // 它的值不为 null 意味着该fiber对象上仍然有要更新的要执行 // while方法支撑render阶段,所有 fiber 节点的构建 // Already timed out, so perform work without checking if we need to yield. while (workInProgress !== null) { workInProgress = performUnitOfWork(workInProgress); } }
performUnitOfWork
通过while循环构建子集Fiber对象 // src\react\packages\react-reconciler\src\ReactFiberWorkLoop.js /** * @description: 通过while循环构建子集Fiber对象 * @param {*} unitOfWork * @return {*} */ function performUnitOfWork(unitOfWork: Fiber): Fiber | null { // The current, flushed, state of this fiber is the alternate. Ideally // nothing should rely on this, but relying on it here means that we don't // need an additional field on the work in progress. const current = unitOfWork.alternate; startWorkTimer(unitOfWork); setCurrentDebugFiberInDEV(unitOfWork); let next; // 初始渲染未执行 if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) { startProfilerTimer(unitOfWork); next = beginWork(current, unitOfWork, renderExpirationTime); stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true); } else { // beginWork 从父到子 构建Fiber next = beginWork(current, unitOfWork, renderExpirationTime); } resetCurrentDebugFiberInDEV(); // 为旧的 props 属性赋值 // 此次更新后 pendingProps 变为 memoizedProps unitOfWork.memoizedProps = unitOfWork.pendingProps; if (next === null) { // 从子到父 构建其余节点 Fiber 对象 next = completeUnitOfWork(unitOfWork); } ReactCurrentOwner.current = null; return next; }
构建单个子级Fiber对象的情况
在之前已经在updateQueue中获取到了已经要渲染的子集元素,即render方法的第一个参数,对应到代码中就是传入到reconcileChildren方法中的nextChildren对象。reconcileChildren方法的作用就是构建子元素所对应的Fiber对象。
构建多个子级Fiber对象
如果newChild对象是一个数组,说明有多个子元素,则需要调用reconcileChildrenArray方法来处理这种情况。 // src\react\packages\react-reconciler\src\ReactChildFiber.js /** * @description: 处理多个子节点的情况 * @param {*} * @return {*} */ function reconcileChildrenArray( returnFiber: Fiber, currentFirstChild: Fiber | null, newChildren: Array<*>, expirationTime: ExpirationTime, ): Fiber | null { if (__DEV__) { // First, validate keys. let knownKeys = null; for (let i = 0; i < newChildren.length; i++) { const child = newChildren[i]; knownKeys = warnOnInvalidKey(child, knownKeys); } } /** * 存储第一个子节点Fiber对象 * 方法返回也是第一个子节点Fiber对象 * 因为其他子节点Fiber对象都存储在上一个子Fiber节点对象的sibling属性中 */ let resultingFirstChild: Fiber | null = null; // 上一次创建的Fiber对象 let previousNewFiber: Fiber | null = null; // 初始渲染没有旧的子集 所以初始渲染时为null let oldFiber = currentFirstChild; let lastPlacedIndex = 0; let newIdx = 0; let nextOldFiber = null; // 初始渲染oldFiber为null循环不执行 for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) { if (oldFiber.index > newIdx) { nextOldFiber = oldFiber; oldFiber = null; } else { nextOldFiber = oldFiber.sibling; } const newFiber = updateSlot( returnFiber, oldFiber, newChildren[newIdx], expirationTime, ); if (newFiber === null) { if (oldFiber === null) { oldFiber = nextOldFiber; } break; } if (shouldTrackSideEffects) { if (oldFiber && newFiber.alternate === null) { deleteChild(returnFiber, oldFiber); } } lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); if (previousNewFiber === null) { resultingFirstChild = newFiber; } else { previousNewFiber.sibling = newFiber; } previousNewFiber = newFiber; oldFiber = nextOldFiber; } if (newIdx === newChildren.length) { deleteRemainingChildren(returnFiber, oldFiber); return resultingFirstChild; } // oldFiber为空 说明是初始渲染 if (oldFiber === null) { for (; newIdx < newChildren.length; newIdx++) { const newFiber = createChild( returnFiber, newChildren[newIdx], expirationTime, ); // 如果newFiber为null if (newFiber === null) { // 进入下次循环 continue; } // 初始渲染时只为newFiber添加了index属性 // 其他事没干 lastPlacedIndex 被原封不动返回了 lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); // 为当前节点设置下一个兄弟节点 if (previousNewFiber === null) { // 存储第一个子Fiber发生在第一次循环时 resultingFirstChild = newFiber; } else { // 为节点设置下一个兄弟Fiber previousNewFiber.sibling = newFiber; } // 在循环的过程中更新上一个创建的Fiber对象 previousNewFiber = newFiber; } // 返回创建的Fiber对象 return resultingFirstChild; } const existingChildren = mapRemainingChildren(returnFiber, oldFiber); // Keep scanning and use the map to restore deleted items as moves. for (; newIdx < newChildren.length; newIdx++) { const newFiber = updateFromMap( existingChildren, returnFiber, newIdx, newChildren[newIdx], expirationTime, ); if (newFiber !== null) { if (shouldTrackSideEffects) { if (newFiber.alternate !== null) { // The new fiber is a work in progress, but if there exists a // current, that means that we reused the fiber. We need to delete // it from the child list so that we don't add it to the deletion // list. existingChildren.delete( newFiber.key === null ? newIdx : newFiber.key, ); } } lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); if (previousNewFiber === null) { resultingFirstChild = newFiber; } else { previousNewFiber.sibling = newFiber; } previousNewFiber = newFiber; } } if (shouldTrackSideEffects) { // Any existing children that weren't consumed above were deleted. We need // to add them to the deletion list. existingChildren.forEach(child => deleteChild(returnFiber, child)); } return resultingFirstChild; }
子节点Fiber对象的构建流程总结
在reconcileChildren方法中调用了mountChildFibers方法去构建子集Fiber对象,所以mountChildFibers的返回值一定是已经构建好的子集Fiber对象,最后做了赋值操作。 workInProgress.child = mountChildFibers( workInProgress, null, nextChildren, renderExpirationTime ); 而reconcileChildren是由updateHostRoot方法调用的。在updateHostRoot中,调用完成reconcileChildren之后,直接返回了workInProgress.child。 在updateHostRoot中,返回子集构建的Fiber对象是为了构建子集Fiber对象的子集。 updateHostRoot的执行结果又作为了beginWork的返回值,即beginWork的返回值是已经构建好的子集Fiber。而beginWork的执行结果又是作为performUnitOfWork的返回值被返回到workLoopSync的循环中,最终在循环条件中不断构建子集的Fiber对象,直到所有子集(包括子集的子集...)全部构建完成,整个Fiber对象的构建就是通过这个循环来支撑的。 最终的执行顺序为: render->legacyRenderSubtreeIntoContainer ->updateContainer ->scheduleWork ->scheduleUpdateOnFiber ->performSyncWorkOnRoot ->workLoopSync ->performUnitOfWork ->beginWork ->updateHostRoot ->reconcileChildren ->mountChildFibers ->reconcileSingleElement(一个子节点) reconcileChildrenArray(多个子节点)
commit阶段
第一个子阶段
在commit阶段的第一个子阶段中,主要做的事情就是调用类组件的getSnapshotBeforeUpdate生命周期函数。但是getSnapshotBeforeUpdate生命周期函数是在组件更新之后才会执行的,在初始渲染时是不执行的。 第一个子阶段要执行的方法为commitBeforeMutationEffects。 // src\react\packages\react-reconciler\src\ReactFiberWorkLoop.js /** * @description: commit阶段第一个子阶段 * 调用类组件的getSnapshotBeforeUpdate生命周期函数 * @param {*} * @return {*} */ function commitBeforeMutationEffects() { // 循环effect链 while (nextEffect !== null) { // nextEffect是effect链上从firstEffect到lastEffect // 的每一个需要commit的fiber对象 // 初始渲染第一个nextEffect为App组件 // effectTag => 3 const effectTag = nextEffect.effectTag; if ((effectTag & Snapshot) !== NoEffect) { setCurrentDebugFiberInDEV(nextEffect); // 记录effect的数量 recordEffect(); // 获取当前fiber节点 const current = nextEffect.alternate; // 当nextEffect上有Snapshot这个effectTag树时 // 执行以下方法,主要是类组件调用 getSnapshotBeforeUpdate 生命周期函数 commitBeforeMutationEffectOnFiber(current, nextEffect); resetCurrentDebugFiberInDEV(); } if ((effectTag & Passive) !== NoEffect) { // If there are passive effects, schedule a callback to flush at // the earliest opportunity. if (!rootDoesHavePassiveEffects) { rootDoesHavePassiveEffects = true; scheduleCallback(NormalPriority, () => { flushPassiveEffects(); return null; }); } } nextEffect = nextEffect.nextEffect; } }
第二个子阶段
commit阶段的第二个子阶段所做的是在commitMutationEffects方法中通过循环,根据effectTag属性来执行对应的dom操作。 在commitMutationEffects中开启了一个while循环来遍历nextEffect,在循环体重,首先获取了effectTag的值,因为要根据这个值来觉得做什么类型的DOM操作。由于初始渲染时primaryEffectTag为2,相对于匹配到的是placement,所以执行的是commitPlacement方法。 在commitPlacement方法中,获取了父级和要渲染的子级并将子级添加到了父级中,在这个过程中,如果该节点有下一个兄弟女节点则执行beforeInsert的操作,如果没有则执行appenChild操作。
第三个子阶段
当进入到第三个子阶段,就说明DOM操作已经完成了。在第三个子阶段中所做的就是调用类组件的生命周期函数和函数组件的钩子函数。 第三个阶段调用的函数叫做commitLayoutEffects。在这个方法中开启了一个while循环去遍历nextEffect。 在while循环中,首先获取了effectTag的值,此时effectTag已经被赋值为1,表示DOM操作已经完成。如果类组件中调用了生命周期函数或者函数组件中调用了钩子函数,则需要处理这些生命周期和钩子函数。根据当前Fiber的tag值来区分对应的操作。 如果是类组件的话,会执行组件中的componentDidMount生命周期函数,执行完成之后会获取任务队列,如果任务队列存在的话最终会调用ReactElement渲染完成之后的回调函数,即render方法的第三个参数。
vue源码
小知识点
render和template同时存在则执行render
编译器版本
mount开始
处理render
有render则直接使用
compileToFunctions将template转成render(借助AST)
执行绑定 调用mountComponent
触发生命周期函数 beforeMount
通过新建watcher 调用_update更新$el
_update使用 __patch__对比并更新vnode
mount过程结束 执行mounted生命周期函数
js数据结构和算法相关
栈