lerna

lerna

一、使用lerna

安装lerna

npm i lerna -g

初始化项目

mkdir lerna-project
cd lerna-project
lerna init

lerna.json

{
"packages": [
"packages/*"
],
"version": "0.0.0"
}

yarn workspace

管理项目形成工作空间

1、将node_modules目录安装在根目录中,子项目都可以读到根目录的node_modules 2、整个项目只有一个yarn.lock文件 3、子项目会被link至根目录的node_modules中,这样允许我们在子项目中通过import直接引用另一个子项目(需执行yarn install)

开启workspace

package.json文件设置workspaces属性

{
"name": "root",
"private": true,
"workspaces": [
"packages/*"
],
"devDependencies": {
"lerna": "^3.22.1"
}
}

创建子项目

lerna create ihooks

安装依赖

默认情况下不能往根目录里面添加模块的,执行👇命令会忽略根空间依赖检查,将node_modules安装在根目录中

yarn add chalk --ignore-workspace-root-check

有时候我们需要将子项目独享的依赖安装在子项目中怎么办呢?执行👇面命令

yarn workspace ihooks add loadsh

子项目共享依赖

yarn install
// 或者
lerna bootstrap --npm-client yarn --use-workspaces
// --npm-client yarn使用yarn作为npm的客户端

其它命令

作用命令
查看子空间信息yarn workspaces info
删除所有node_moduleslerna clean / yarn workspaces run clean
重新获取所有的node_modulesyarn install --force
查看缓存目录yarn cache dir
清除本地缓存yarn cache clean

参考文档

yarnlerna

react setState

react setState过程

setState是挂在react的Component组件的原型上,这里记录下setState的执行过程,包括何时表现为异步,何时表现为同步

窥探Componen组件:

let classComponentUpdater = {
/**
* inst:组件实例
* payload:需要更新的新状态
*/
enqueueSetState(inst, payload) {
// 根据实例获取当前fiber,react的class类组件的fiber.stateNode指向类的实例,实例的_reactInternals属性指向当前fiber
let fiber = get(inst)
// 这里会有确认优先级的操作
// let eventTime = requestEventTime()
// let lane = requestUploadLane()
let update = createUpdate(eventTime, lane)
update.payload = payload
/**
* update是一个更新对象,例如:
* {
* eventTime: ,
* lane: ,
* payload: {},
* callback: , // setState第二个参数
* }
*/
// 把当前update添加到当前fiber的updateQueue(链表)中
enqueueUpdate(fiber, update)
// 执行fiber的调度
let root = scheduleUpdateOnFiber(fiber, lane, eventTime)
},
}
// Component组价的整体结构
class Component {
constructor() {
this.updater = classComponentUpdater // 更新器
}
setState(partialState) {
this.updater.enqueueSetState(this, partialState)
}
// ...
}

简单总结下:

this.setState({}, cb)
  1. 执行Component原型上setState方法
  2. 会创建一个update对象
  3. 将update添加到当前fiber的updateQueue链表上
  4. 执行fiber的调度更新函数scheduleUpdateOnFiber

scheduleUpdateOnFiber函数

function scheduleUpdateOnFiber (fiber) {
// 根据fiber.return获取根fiber对象
let root = markUpdateLaneFromFiberToRoot(fiber)
// 开始创建一个任务,从根节点开始执行
ensureRootIsScheduled(root)
}
// 递归获取root fiber
function markUpdateLaneFromFiberToRoot (fiber) {
let parent = fiber,return
while (parent) {
fiber = parent
parent = parent.return
}
if (fiber.tag === HostRoot) {
return fiber
}
return null
}
function ensureRootIsScheduled (root) {
// ...
// 这里会先判断任务是否相同和确认优先级
// scheduleSyncCallback方法会把performSyncWorkOnRoot添加到一个队列中,等待后续执行
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root))
if (!isBatchedUpdates) { // 如果不是批量(异步)更新
// 进行同步更新
flushSyncCallbackQueue()
}
}
// react17中不是通过isBatchedUpdates判断的,但是原理相同
function batchedUpdates (fn) {
fn()
isBatchedUpdates = false
}
function flushSyncCallbackQueue () {
syncQueue.forEach(cb => cb())
syncQueue.length = 0 // 队列置空
}
function scheduleSyncCallback (cb) {
syncQueue.push(cb)
}

scheduleUpdateOnFiber函数总结:

  1. 向上递归获取root fiber,从根fiber开始执行任务
  2. 判断任务是否重复,并且确认优先级,把任务添加到一个队列中,等待后续执行
  3. 判断是否需要批量(异步)更新,如果不是就执行flushSyncCallbackQueue进行同步更新

注:在react合成事件中会默认执行batchedUpdates()函数进行批量(异步)更新,执行完后将isBatchedUpdates置为false,这也就解释了放在setTimeout中的setState会同步更新

performSyncWorkOnRoot函数

performSyncWorkOnRoot函数是真正的渲染任务了

function performSyncWorkOnRoot (workInProgress) {
let root = workInProgress
while (workInProgress) {
if (workInProgress.tag === classComponent) {
let inst = workInProgress.stateNode
//将当前fiber上的updateQueue中的各个要更新的新state和实例中的老的state进行合并
inst.state = processUpdateQueue(inst, workInProgress)
// 得到合并后的新state后执行实例的render方法,得到新的VDom,然后进行dom diff,更新Dom
inst.render()
}
workInProgress = workInProgress.child
}
commitRoot(root) // 提交阶段,并重置任务优先级
}

performSyncWorkOnRoot函数总结:

  1. 从根fiber开始递归执行,将每个classComponent的state进行合并,并执行render
  2. 提交阶段,重置任务优先级

前端监控

前端监控

一、为什么要做监控

  1. 更快发现问题并解决问题
  2. 做产品的决策依据
  3. 提升前端工程师的技术深度和广度,打造简历亮点
  4. 为业务拓展提供了更多可能性

二、前端监控目标

1、稳定性(stability)

错误名称备注
JS错误JS执行错误或者promise异常
资源异常script或者link资源加载异常
接口错误ajax或者fetch请求接口异常
白屏页面空白
// js执行序错误和资源加载 错误可以通过监听error事件来捕获
window.addEventListener('error', (e) => {
if (e.target && (e.target.src || e.target.href)) { // 资源加载错误
...
} else { // js执行错误
...
}
})
// promise异常, 可以通过监听unhandledrejection事件来捕获
window.addEventListener('unhandledrejection', (e) => {
console.log(e)
})

2、用户体验(experience)

错误名称备注
加载时间各个阶段的加载时间
fFB(tim to first byte)(首字节时间)是指浏览器发起第一个请求到数据返回第一个字节所消耗的时间,这个时间包含了网络请求时间、后端处理时间
FP(First Paint)(首次绘制)首次绘制包括了任何用户自定义的背景绘制,它是将第一个像素点绘制到屏幕的时刻
FCP(First Content Paint)(首次内容绘制)首次内容绘制是浏览器将第一个DOM渲染到屏幕的时间,可以是任何文本、图像、SVG等的时间
FMP(First Meaningful paint)(首次有意义绘制)首次有意义绘制是页面可用性的量度标准
FID(First Input Delay)(首次输入延迟用户首次和页面交互到页面响应交互的时间
卡顿超过50ms的长任务

3、业务(business)

错误名称备注
PVpage view即页面浏览量或点击量
UV指访问某个站点的不同IP地址的人数
页面的停留时间用户在每一个页面的停留时间

三、前端监控流程

  1. 前端埋点
  2. 数据上报
  3. 分析和计算 将采集到的数据进行加工和汇总
  4. 可视化展示 将数据按照各种维度进行展示
  5. 监控报警 发现问题后按照一定的条件触发报警

maidian

四、常见埋点方案

  1. 代码埋点
  • 就是以嵌入代码的形式进行埋点比如需要监控用户的点击事件,会选择在用户点击时,插入一段代码,保存这个监听行为或者直接将监听行为以某一种数据格式直接传递给服务器端
  • 优点:可以在任意时刻,精确的发送或保存所需要的数据信息;
  • 缺点:工作量较大
  1. 可视化埋点
  • 通过可视化交互的手段,代替代码埋点
  • 将业务代码和埋点代码分离,提供一个可视化交互的页面,输入为业务代码,通过这个可视化系统,可以在业务代码中自定义的增加埋点事件等等,最后输出的代码偶合了业务代码和埋点代码
  • 可视化埋点其实是用系统来代替手工插入埋点代码
  1. 无痕埋点
  • 前端的任意一个事件都被绑定一个标识,所有的事件都别记录下来
  • 通过定期上传记录文件,配合文件解析,解析出来我们想要的数据,并生成可视化报告供专业人员分析
  • 无痕埋点的优点是采集全量数据,不会出现漏埋和误埋等现象
  • 缺点是给数据传输和服务器增加压力,也无法灵活定制数据结构

react fiber

react fiber

在v15和之前的版本中,在react任意一个地方执行setState后,react会对整个页面创建虚拟dom,并对前后dom进行diff对比,然后进行渲染,这个过程是“一气呵成”的,所以它占据了主线程的大量时间,这会使页面响应度变差,也就导致了react在渲染动画,或者手势操作时会出现卡顿现象,因此react团队在react的v16版本后采用了fiber架构。

熟悉fiber之前需要先了解几个基础知识:window.requestIdleCallback单链表 image

帧率

我们知道屏幕浏览器刷新率60帧/s,平均16.6ms/帧,在一帧中浏览器做了很多事情:

在浏览器执行一帧的过程中:

  1. Input event handlers:合成线程 compositor thread 把 input 数据传给主线程, 处理事件回调。

  2. javascript: 包扩定时器、事件(scroll,resize等)、requestAnimationFrame、重排(layout)、重绘(paint)

  3. 如果在一帧内,执行完上述所有任务后,还有剩余时间的话,那就会执行requestIdleCallback回调,

window.requestIdleCallback(callback, options)
// callback(deadline): 一个function 用户要执行的回调任务, 并传入一个参数deadline
// deadline: {
// timeRemaining: 0ms, // 当前帧还剩余多少时间
// didTimeout: false, // 是否已超时
// }
// options: // 一个对象,可以设置timeout时间:超过这个时间后,不管当前是否有剩余时间必须执行此回调

因为requestIdleCallback兼容性差,所以react内部并不是直接用这个api,而是自己实现了这个api,理论效果是一样的

单链表

在fiber中有大量的单链表,用一张图表示:

image

重回fiber

1.通过fiber合理分配cpu资源,提高用户相应速度;2.通过fiber可以使reconciliation(一种diff算法)可以中断,交出主线程,去执行更重要的事情(渲染,交互等)

那么fiber是什么:fiber是一个执行单元,也是一种数据结构

执行单元fiber

每次浏览器执行完一个执行单元,react就会检查时候还有剩余时间(每一帧都会去检查),如果没有,react就放权给浏览器

image

数据机构fiber

react使用链表,将每一个VirtualDom节点及其内部所有子(不是孙子)节点表示为一个fiber。如图: image

其中A1>B1+B2就是一个fiber,B1>C1+C2是一个fiber

每个fiber其实就是一个对象,除了一些属性还包括三个指针

let fiber = {
tag: '', //当前节点类型,文本还是dom
key: 'ROOT', // 唯一标识
type: '', // 当前元素类型,span、div
stateNode: '', // fiber对应的node节点
flag: '', // placement等,副作用类型,例如: 增删改查
firstEffect: null,
lastEffect: null
// ...
// 三个指针
child: {}, // 指向当前第一个子fiber
sibling: {}, // 指向当前紧挨着的兄弟fiber
return: {}, // 指向当前的父fiber
}

react的构建过程

// 浏览器空闲时间执行
requestIdleCallback(workLoop) //react中是通过requestAnimationFrame和MessageChannel实现的
let rootFiber = {
...
}
let workInProgress = rootFiber //当前正在执行的工作单元(fiber)
function workLoop(deadLine) { // deadLine每帧剩余时间对象
while (workInProgress && deadLine.timeRemaining() > 1) {
workInProgress = performUnitOfWork(workInProgress) // 每个任务单元执行完毕后返回下一个要执行的任务单元
}
// 提交阶段
commitRoot(rootFiber)
}
function performUnitOfWork (workInProgress) {
beginWork(workInProgress) // 创建子fiber树
if (workInProgress.child) {
return workInProgress.child // 优先构建child
}
while (workInProgress) {
completeUnitWork(workInProgress) // 当前工作单元完成构建,并生成dom
if (workInProgress.sibling) {
return workInProgress.sibling // 没有child,构建sibling
}
workInProgress = workInProgress.return
// 最后没有父元素(root)退出循环
}
}
// 开始创建子Fiber树🌲
function beginWork (workInProgress) {
let nextChildren = workInProgress.props.children
return reconcileChildren(workInProgress, nextChildren)
}
function reconcileChildren (returnFiber, nextChildren) {
// 根据VDom生成fiber的同时并构建fiber链(就是给fiber的child,sibline, return属性赋值)
for (let i = 0; i < nextChildren.length; i++) {
let newFiber = createFiber(nextChildren[i])
// ...
}
}
// 创建fiber
function createFiber(element) {
return {
tag: TAG_HOST,
type: element.type,
props: element.props,
key: element.key,
// ...
}
}
function completeUnitWork (workInProgress) {
switch(workInProgress.tag) {
case TAG_HOST:
createStateNode(workInProgress) // 根据fiber生成真实dom节点
//...
}
// 完成时判断有没有对应的dom操作,有的话添加到副作用链表中
makeEffectList(workInProgress)
}
function makeEffectList (workInProgress) {
// 根据每个fiber的firstEffect和lastEffect以及flags
// 归并fiber树中各fiber的副作用,形成副作用链
// firstEffect -> nextEffect -> ... -> lastEffect
}
function commitRoot (rootFiber) {
let currentEffect = rootFiber.firstEffect
while (currentEffect) {
switch(currentEffect.flags) { // 副作用类型
case Placement:
commitPlacemen(currentEffect) // 向父dom添加子dom
}
currentEffect = currentEffect.nextEffect
}
}
function commitPlacemen (currentEffect) {
let parent = currentEffect.return.stateNode
parent.appendChild(currentEffect.stateNode)
}

总结

react将jsx经过createElement处理形成虚拟dom节点后的进行渲染,主要分为两个阶段: diff阶段和commit阶段: 在diff阶段进行新旧虚拟dom对比,进行更新,增量或者删除,并且根据虚拟dom生成fiber树

diff阶段可以暂停,因为diff阶段比较花时间,react会对任务进行拆分

commit阶段进行DOM的更新创建,此阶段不能暂停,需要“一气呵成”

浏览器的渲染机制

浏览器的渲染机制

浏览器输入url,按回车后的过程

46dd9fff4eca81e7

浏览器渲染过程

浏览器的渲染依靠浏览器的渲染引擎浏览器依赖的模块的共同协作,才能把一个网页生成出来 浏览器的渲染引擎主要包括:HTML解释器、CSS解释器、布局和JavaScript引擎等 浏览器依赖的模块主要包括:网络,存储,2D/3D 图形,音频和视频,图片解码器等 1628f1a408ef0436

大致的渲染过程及依赖模块关系图: 1628f1a408fb77c3

sdcsdcaascasc

简单说下过程: 浏览器通过网络和存储模块拿到HTML文件 --> 然后通过HTML解析器解析html文件,如果遇到外联css和js文件时就去加载文件--> 然后HTML解析器和css解释器同时进行工作,生成相对应的DOM树和浏览器可以解析的CSS树并进行样式计算(stylesheets,如:em->px, 颜色->rgb()等) --> 最后合并生成渲染树,绘制到显示器上

构建DOM树前后: 0555517b92e9

构建CSS树前后: 091656b

最后生成的渲染树: 92d707f25tplvk3

渲染树生成时,因为页面中会有一些复杂的效果,比如脱离文档流,3D动画,包括设置z-index等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树,就像ps中图层的概念一样,页面并不是一个平面而是一个3D的立体结构,这个可以在浏览器调试中看到

性能问题

现在在前端的react和vue两大框架中,都采用了virtualDom,目的是为了尽可能的减少dom操作,dom操作是昂贵的,很浪费性能的(vue的作者曾经说过:virtualDom的出现最主要的是解决了多端公用一套代码)因为操作dom必然会引起重绘或者重排,重排必定会引起重绘。所以开发者在开发的时候要避免直接操作DOM,现在大多数浏览器都会通过对重排进行优化提高性能