本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!
前言
本文主要讲述的是某种心路历程: 一枚小白成长到大厂需要的高级前端开发工程师。
技术文,干货多,放心观看呢。
0. 最后的狂欢
“来,喝!谁也别是个怂瓜蛋哈!”
说完,感情深,一口一口闷。
喂喂,这人均低消¥1600
,你这么喝下去怕不是要把我喝垮咯。
我们leader层以上的人都被喊进了小黑屋开会,主题只有一个:要裁员。 这场看起来非常热闹的聚会,我自掏腰包撺的。你说,一家上市公司,要我给裁员名单,我该怎么做。
“老大,来,喝一个。我在老大这学到了很多,技术是真牛哈!”
碰杯,干了一个。
“我一直以为 Redis
是那些写 Java
的人要搞的事情,没想到老大带着我们把Redis
给搞出来了。这下以后都不用看那些服务端的脸色了! 老大,走一个!”
继续碰杯,走一个。 目前应该就属我喝了最多的钱下肚。
“老大,我是负责搞调优的。我以前只知道做做http缓存
,搞搞懒加载
什么的。没想到在老大这里学到了高了好几个维度的调优方法。来,老大陪我干一个。”
喂喂,不会已经上头了吧?不应该是你陪老大干一个嘛。 好吧,继续碰杯,再走一个。
“喂喂,大家听我讲一下,我给大家讲一个老大的糗事如何?”
团队里为数不多的妹纸,果然不管哪一行的女生都喜欢分享八卦这种东西。有人唱戏,自然有人捧场。一波波起哄下,一个小故事出来了。
“我刚来公司不久的时候,老大总是把我提交的代码给打回来,甚至有次,我提交了6次代码,每次都被打回来了。然后我就哭了,一半是气的,一半是怕的。我记得我当时还特意问了问你们,老大是不是特别严格,会把你们提交的代码打回来。然后你们告诉我,你们都没有被老大打回过。”
我开始捂住自己的脸,这种事情都能拿出来说,也不光彩吧?
“然后吧,这都不是重点。重点是,那天晚上老大居然请我吃饭了。白天打回我的代码有多狠,晚上请吃饭就有多卑微。老大边吃饭边给我道歉,说我的代码写得不符合标准,页面render
的次数多了。例如:”
function Com (){
const [price,setPrice] = useState(0); // 初始化0 第1次
useEffect(()=>{
// fetch data
setPrice(10.8); // 拿到真实价格 第2次
},[price])
return <div>¥{price}</div>
}
// 应该改成一下风格
Api.fetch().then(
(props)=>{
render(React.memo(Com),props)
}
)
function Com (props){
const [price,setPrice] = useState(props.price); // 初始化0 第1次
useEffect(()=>{
// code
},[price])
return <div>¥{price}</div>
}
“你们大家看看,虽然我觉得有道理,但因为这就打回了6次,老大,求你好好做个人吧😂。 最后说着道歉的话再继续让我修改代码。”
噢,耶!大章第一个没忍住,喝进去的酒都喷了出来。 我知道,大家就留我在原地尴尬😅。我依然保持着笑容,微笑着面对着惨淡的世界。
“哎,你们都等等,等等! 这算个啥? 有时候视觉都会给你们切那种毛玻璃效果的小图吧?然而,有一次老大觉得我们使用图片资源太多了,就逼着我们用css写出毛玻璃的效果。 你们说,老大是不是个狠人!”
<div class="mark"> 老大喜欢手动整的毛玻璃效果</div>
$img: 'https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/388f7a44af6b4ea3af893a1fcbfadd71~tplv-k3u1fbpfcp-watermark.image';
body {
height: 100vh;
display: flex;
background-image: url($img);
}
.mark {
position: relative;
margin: auto;
width: 500px;
height: 400px;
background-color: rgba(255, 255, 255, 0.5);
overflow: hidden;
z-index: 10;
line-height: 300px;
text-align: center;
color: #FFFFFF;
font-size: 20px;
&::before {
content: "";
position: absolute;
background-position: top;
filter: blur(8px);
background-image: url($img);
background-repeat: no-repeat;
background-attachment: fixed;
background-size: cover;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
}
}
啊,喂,喂!我记得我没逼你啊!我们不是共同探讨的吗?
“哎呀,你们这跟我的事比起来都正常,不算啥好伐?”
不擅言辞的小白,我们组少数几个女生,也是最漂亮的一个。上海本地儿人,姓白,对穿搭很在行。所以,自从她加入我们第一天,来找她“滋事”的人就不少。有的是借工作上的原油来找她,有的甚至就干脆路过都要找她闲聊几句。哪怕她不怎么擅长应付这些人,但依然每天都有人……
唉,颜值即正义啊!可没想到小白也要吐槽我吗?
“你们是不知道,公司不是发了邮件说要组织舞蹈队,找老大报名。我吧没敢直接问老大,老大也不安排我,然后我每天穿不同的好看的衣服,就希望老大看见了能把我报名报上去。可结果呢,你们知道有多气吗!”
忽然地,特别安静了一会儿。大章着急催着。
“小白,你快接着说啊!大家都好奇你有多气呢!”
“结果嘞!我记得我换第三套衣服的时候,也就是第三天,老大突然告诉我,叫我以后晚上要早点下班,不要总加班。原来老大是以为我脱单处对象了!”
“噗~!”
好几个嘴里还喝着的没忍住,都喷了。然后笑了一地……
是啊,这群人太可爱了。我能带出这样一波人,我很满足。
1.做好一件事情,成就一批人
终究还是大章选择离开。这是我跟大章商量好的,我给他推荐一个很好的机会,他还能拿到这边的 N+1
赔偿。意外的是,小白来找我了。
小白说她的同窗好友在隔壁团队,已经得到消息她在被裁名单上。小白想自己离开,然后拜托我去把她捞过来。有种一命换一命的感觉。
小白还说,因为她的名字里带个“白”字,所以叫她小小白。小白求了我2天,这件事情得分开说。我不会主动让小白离开,是因为小白的技术还不够,不能和大章一样能出去独当一面。如果是小白因自己的事提出离职,我或许没办法阻拦,但这种一命换一命的做法,我实在没办法苟同。
我认为的前端能力等级
- 初级 --
能正常执行需求,代码书写经验足。
- 中级 --
能理解常用技术栈的原理,并在代码层面写出性能较好的代码,且能hold住一个项目。
- 高级 --
能独挡一面。能及时解决线上响应的问题,并能提供技术解决方案。
- 资深 --
能单独承担商业解决方案。
- 专家 --
技术上广度要有,还要有深度。管理方面能hold住业务,能搞好团队建设,能提升团队整体实力水平。
大章已经资深边缘徘徊了,而小白还处于中级阶段。至于小小白,只在初级阶段。
最终,小白和小小白请我吃了碗酸辣粉,搞定了我。小小白,22岁,工作了2年。20岁就名校毕业,真的是个学霸了。一碗酸辣粉的时间就把优化算法给我讲了透。
基于混沌时间序列分析的神经网络了解一下?
噢,不懂是吧?换个方式说,这种优化算法主要用途之一是用来预测。例如用来预测下一个月内的用户行为,分析出用户画像。你手机里那些购物APP是不是都有按你的喜好推荐商品的功能?为啥购物APP能知道你喜欢哪一类商品?其中的手段之一就是预测。
我了个去,人才啊!我忽然觉得我庙小了。我想推荐小小白去算法团队,被拒绝了。不是被算法团队拒绝了,是被小小白拒绝了。小小白说要在我这修炼半年,修炼好了就去大厂,让曾经那些看不起她觉得她菜的人仰着头看她。
好咯。年轻真好,那就开始修炼吧。我很愿意看见半年后小小白来打我的脸,去打那些人的脸。Flag就立在这了。
做好一件事情,成就一批人。 这是我一直在做的事情。
2. 修炼第一步,搞懂React原理才能写好React代码。
人来了之后,我做了第一次技术分析。没别的,先把React Hooks原理搞懂。
- 函数组件执行函数
- 执行函数组件
renderWithHooks
- 改变
ReactCurrentDispatcher
对象
- 执行函数组件
- 初始化hooks
- 通过
mountWorkInProgressHook
生成hooks链表 - 通过
mountState
来初始化useState
- 通过
dispatchAction
来控制无状态组件的更新 - 通过
mountEffect
初始化useEffect
- 通过
mountMemo
初始化useMemo
- 通过
mountRef
初始化useRef
- 通过
- 更新hooks
- 通过
updateWorkInProgressHook
找到对应的hooks
更新hooks
链表 - 通过
updateState
得到最新的state
- 通过
updateEffect
更新updateQueue
- 通过
updateMemo
判断deps
,获取or更新缓存值 - 通过
update
获取ref
对象
- 通过
先举个栗子:
import {useState,useEffect,memo} from 'react';
const Com = React.memo(({name})=><div>{name}</div>)
function App(){
const [ num , setNumber ] = useState(0);
const [ name , setName ] = useState('小白');
const handerClick=()=>{
for(let i=0; i<5;i++ ){
setTimeout(() => {
setNumber(num+1)
console.log(num)
}, 1000)
}
}
useEffect(()=>{
setName(num % 2 ? '小白' : '小小白')
},[num])
return <div>
<Com name={name}/>
<button onClick={ handerClick } >{ num }</button>
</div>
}
Q1:当你使用了hooks( 例如 useState)时,发生了什么?**
我们去看 useState
的源码: react/src/ReactHooks.js
export function useState(initialState){
const dispatcher = resolveDispatcher(); // 1
return dispatcher.useState(initialState);
}
噢,useState(initialState)
等价于 dispatcher.useState(initialState)
。 dispatcher
从中文意思上是 调度员 的意思。 也就是说你调用 useState
的时候只是通知了调度员去调度真正的 useState
。
Q2: 那调度员 dispatcher
又是什么?
看源码。
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current
return dispatcher
}
噢,dispatcher
是从 ReactCurrentDispatcher
身上来。我们来把这个此分析一下,react 当前的(current)调度员(Dispatcher)。
也就是说,到这里 Dispatcher
就已经安排好了。
Q3: 函数组件是什么时候被调用的?
就是说,你的 App 组件 是什么时候被调用的? React 16 版本的架构可以分为三层:
Scheduler
(调度层):调度任务的优先级,高优任务优先进入协调器Reconciler
(协调层):构建 Fiber 数据结构,比对 Fiber 对象找出差异, 记录 Fiber 对象要进行的 DOM 操作Renderer
(渲染层):负责将发生变化的部分渲染到页面上
我们知道 render
一个组件 首先要构建 组件的 Fiber 链表
。所以我们来看协调层的源码:react-reconciler/src/ReactFiberBeginWork.js
renderWithHooks(
null, // current Fiber
workInProgress, // workInProgress Fiber
Component, // 函数组件本身
props, // props
context, // 上下文
renderExpirationTime,// 渲染 ExpirationTime
);
……
renderWithHooks(
current, // current Fiber
workInProgress, // workInProgress Fiber
Component, // 函数组件本身
props, // props
context, // 上下文
renderExpirationTime,// 渲染 ExpirationTime
);
我们先看 renderWithHooks
几个我们我们最熟悉的参数。Component
是函数本身,props
是我们传给函数组件的信息,context
代表当前的上下文。
那,有木有可能我们的 Component
就是在 renderWithHooks
方法里被调用的?接着看源码(精简了一下)。
function renderWithHooks(
current,
workInProgress,
Component,
props,
secondArg,
nextRenderExpirationTime,
) {
renderExpirationTime = nextRenderExpirationTime;
currentlyRenderingFiber = workInProgress;
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.expirationTime = NoWork;
// 3 很重要!
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
let children = Component(props, secondArg); // 2
// code ...
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
renderExpirationTime = NoWork;
currentlyRenderingFiber = null;
currentHook = null
workInProgressHook = null;
didScheduleRenderPhaseUpdate = false;
return children; // end
}
噢,renderWithHooks
的返回值是 children, 而 children = Component(props, secondArg);
破案了,我们的函数组件就是在 renderWithHooks
被调用且最终 return
回来。
我们再回到 3 ,ReactCurrentDispatcher.current
是不是前面没解释清楚的 调度员 的归宿?! 解释一下这行代码: 当 current
为 null
或者 current
的 memoizedState
属性为 null
就把 HooksDispatcherOnMount
赋值给我们的调度员, 否则就把HooksDispatcherOnUpdate
赋值给我们的调度员。
从这两名称上又能看出个大概来,一个是 Mount
的 调度员,一个是 Update
的调度员。那也就是说,初始化 hooks
的时候就是 Mount
调度员,要更新的时候就是 Update
调度员?!
ok,案子到这算是破了80%了。
Q4: workInProgress 是什么,workInProgress.memoizedState又是什么?
workInProgress
: 从名称分析,就是工作进度 或者 正在进行中的工作 的意思吧? 那它是个对象吧? 那对象身上肯定会有一些属性用来描述不同信息对吧?
workInProgress.memoizedState
:
- 使用
useState
,保存state
信息 - 使用
useEffect
,保存effect
对象 - 使用
useMemo
, 保存缓存的值
和deps
- 使用
useRef
, 保存ref
对象。
也就是说,workInProgress.memoizedState 存放的是 我们所使用的hooks 的信息。
这里的 workInProgress.updateQueue
后面再提。
Q5: 调用 useState
的时候发生了什么。
先看精简源码。
function mountState(
initialState
){
const hook = mountWorkInProgressHook();
//如果 initialState为函数,则执行initialState函数。
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
pending: null, // 待更新的内容
dispatch: null, // 调度函数
lastRenderedReducer: basicStateReducer,
lastRenderedState: initialState, // 最新一次渲染的 state
});
// 负责更新的函数
const dispatch = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
)))
return [hook.memoizedState, dispatch];
}
噢,这个代码明显要更容易分析一些。
- 先拿到 hook 的信息 也就是
const hook = mountWorkInProgressHook();
- 对入参
initialState
进行判别。接着将initialState
赋值给hook.memoizedState
和hook.baseState
- 接下来就申明了一个队列
queue
,信息看注释。 - 申明调度函数 dispatch , 用来更新 state
- 返回一个数组,方便我们解构。也就是 :
const [x,setX] = useState(initialState);
那 dispatchAction 又是什么?
function dispatchAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
)
对照上述代码,S 代表 什么? A 代表什么?
setX
就是调用了 dispatchAction
吧? 源码中显示 dispatchAction
已经有了 currentlyRenderingFiber
, queue
两个参数了,那 setX
传入的参数应该就是第三个参数 action
了吧?
Q6: dispatchAction
到底干了什么?
function dispatchAction(fiber, queue, action) {
// code ...
// step 1 : 初始化要更新的信息
const update= {
expirationTime,
suspenseConfig,
action,
eagerReducer: null,
eagerState: null,
next: null,
}
// 判定是不是首次更新
const pending = queue.pending;
if (pending === null) { // 证明第一次更新
update.next = update;
} else { // 不是第一次更新
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
const alternate = fiber.alternate;
// 判断当前是否在渲染阶段
if ( fiber === currentlyRenderingFiber || (alternate !== null && alternate === currentlyRenderingFiber)) {
// code ...
} else {
// code ...
// 剩下的事情交由 调度层 去完成。
scheduleUpdateOnFiber(fiber, expirationTime);
}
}
Q7: Fiber 又是什么? Fiber 链表又是什么?
唉,大致看看Fiber对象上有哪些属性吧。
type Fiber = {
/************************ DOM 实例相关 *****************************/
// 标记不同的组件类型, 值详见 WorkTag
tag: WorkTag,
// 组件类型 div、span、组件构造函数
type: any,
// 实例对象, 如类组件的实例、原生 dom 实例, 而 function 组件没有实例, 因此该属性是空
stateNode: any,
/************************ 构建 Fiber 树相关 ***************************/
// 指向自己的父级 Fiber 对象
return: Fiber | null,
// 指向自己的第一个子级 Fiber 对象
child: Fiber | null,
// 指向自己的下一个兄弟 iber 对象
sibling: Fiber | null,
// 在 Fiber 树更新的过程中,每个 Fiber 都会有一个跟其对应的 Fiber
// 我们称他为 current <==> workInProgress
// 在渲染完成之后他们会交换位置
// alternate 指向当前 Fiber 在 workInProgress 树中的对应 Fiber
alternate: Fiber | null,
/************************ 状态数据相关 ********************************/
// 即将更新的 props
pendingProps: any,
// 旧的 props
memoizedProps: any,
// 旧的 state
memoizedState: any,
/************************ 副作用相关 ******************************/
// 该 Fiber 对应的组件产生的状态更新会存放在这个队列里面
updateQueue: UpdateQueue<any> | null,
// 用来记录当前 Fiber 要执行的 DOM 操作
effectTag: SideEffectTag,
// 存储要执行的 DOM 操作
firstEffect: Fiber | null,
// 单链表用来快速查找下一个 side effect
nextEffect: Fiber | null,
// 存储 DOM 操作完后的副租用 比如调用生命周期函数或者钩子函数的调用
lastEffect: Fiber | null,
// 任务的过期时间
expirationTime: ExpirationTime,
// 当前组件及子组件处于何种渲染模式 详见 TypeOfMode
mode: TypeOfMode,
};
在 React 16 中,将整个任务拆分成了一个一个小的任务进行处理,每一个小的任务指的就是一个 Fiber 节点的构建。
至于Fiber链表。
React
通过链表结构找到下一个要执行的任务单元。
要构建链表结构,需要知道每一个节点的:
父级节点
是谁子级节点
是谁,要知道他的下一个兄弟节点
是谁。
上面已经把Fiber 对象 身上挂的属性挪列很详细了。需要你去瞅瞅。
当所有DOM的Fiber对象生成完毕,那需要执行DOM操作的Fiber就会构建出Fiber链表。至于构建Fiber 链表的原理是什么,如下代码(不是源码,只是为了看得更清晰,手动写了一波。希望你有空也手动写一遍):
import React from "react"
const jsx = (
<div id="a">
<div id="b1">
<div id="c1">
<div id="d1"></div>
<div id="d2">
<div id="e1"></div>
<div id="e2"></div>
</div>
</div>
<div id="c2"></div>
</div>
<div id="b2"></div>
</div>
)
const container = document.getElementById("root")
/**
* 1. 为每一个节点构建 Fiber 对象
* 2. 构建 Fiber 链表
* 3. 提交 Fiber 链接
*/
// 创建根元素 Fiber 对象
const workInProgressRoot = {
stateNode: container,
props: {
children: [jsx]
}
}
let nextUnitOfWork = workInProgressRoot
function workLoop(deadline) {
// 如果下一个要构建的执行单元存在并且浏览器有空余时间
while (nextUnitOfWork && deadline.timeRemaining() > 0) {
// 构建执行单元并返回新的执行单元
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
}
// 如果所有的执行单元都已经构建完成
if (!nextUnitOfWork) {
// 进入到第二个阶段 执行 DOM 操作
commitRoot()
}
}
// Fiber 工作的第一个阶段
function performUnitOfWork(workInProgress) {
// 构建阶段向下走的过程
// 1. 创建当前 Fiber 节点的 DOM 对象并存储在 stateNode 属性中
// 2. 构建子级 Fiber 对象
beginWork(workInProgress)
// 如果子级存在
if (workInProgress.child) {
// 返回子级 构建子级的子级
return workInProgress.child
}
// 开始构建阶段向上走的过程
// 如果父级存在
while (workInProgress) {
// 构建 Fiber 链表
completeUnitOfWork(workInProgress)
// 如果同级存在
if (workInProgress.sibling) {
// 返回同级 构建同级的子级
return workInProgress.sibling
}
// 同级不存在 退回父级 看父级是否有同级
workInProgress = workInProgress.return
}
}
function beginWork(workInProgress) {
// 如果 Fiber 对象没有存储其对应的 DOM 对象
if (!workInProgress.stateNode) {
// 创建 DOM 对象并存储在 Fiber 对象中
workInProgress.stateNode = document.createElement(workInProgress.type)
// 为 DOM 对象添加属性
for (let attr in workInProgress.props) {
if (attr !== "children") {
workInProgress.stateNode[attr] = workInProgress.props[attr]
}
}
}
// 创建子级 Fiber 对象
if (Array.isArray(workInProgress.props.children)) {
// 记录上一次创建的子级 Fiber 对象
let previousFiber = null
// 遍历子级
workInProgress.props.children.forEach((child, index) => {
// 创建子级 Fiber 对象
let childFiber = {
type: child.type,
props: child.props,
return: workInProgress,
effectTag: "PLACEMENT"
}
// 第一个子级挂载到父级的 child 属性中
if (index === 0) {
workInProgress.child = childFiber
} else {
// 其他子级挂载到自己的上一个兄弟的 sibling 属性中
previousFiber.sibling = childFiber
}
// 更新上一个子级
previousFiber = childFiber
})
}
}
function completeUnitOfWork(workInProgress) {
let returnFiber = workInProgress.return
if (returnFiber) {
// 链头上移
if (!returnFiber.firstEffect) {
returnFiber.firstEffect = workInProgress.firstEffect
}
// lastEffect 上移
if (!returnFiber.lastEffect) {
returnFiber.lastEffect = workInProgress.lastEffect
}
// 构建链表
if (workInProgress.effectTag) {
if (returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = workInProgress
} else {
returnFiber.firstEffect = workInProgress
}
returnFiber.lastEffect = workInProgress
}
}
}
// Fiber 工作的第二阶段
function commitRoot() {
// 获取链表中第一个要执行的 DOM 操作
let currentFiber = workInProgressRoot.firstEffect
// 判断要执行 DOM 操作的 Fiber 对象是否存在
while (currentFiber) {
// 执行 DOM 操作
currentFiber.return.stateNode.appendChild(currentFiber.stateNode)
// 从链表中取出下一个要执行 DOM 操作的 Fiber 对象
currentFiber = currentFiber.nextEffect
}
}
// 在浏览器空闲的时候开始构建
requestIdleCallback(workLoop)
2. 上线1小时后,小小白哭了
自从上次分享了 React Hooks 相关的东西,安生了2个星期。小小白刻苦学习,公司加班写需求,回家继续加班写代码。
我是不是逼得人太紧了?
狗叫了(我的手机响铃声)。
“你怎么回事,运营说线上出故障了!刚答应你留个人,你就这样报答我的……”
啪!我把电话挂了。啥也没说,直接挂了。我迅速回到位置上,小小白泪流满面就差哭晕在工位上了。我这时候是不是应该喷一句: MMP,出了事就知道哭,出了问题你倒是第一时间告诉我啊!
事情是这样的,小小白做了一个活动页,内容是让用户点击屏幕抢红包。问题是,用户可以点到了就抢到了红包,理论上只要手速够快,屏幕能识别用户的手势就能抢到。也就是说你点1千次,就能抢到1千个红包。 当然了,这样的说法太夸张。用户最多抢到200块的红包,后面抢再多也不会超过200块钱。
用户手速不可能快到那个程度,可小小白的活动页,抢到3、4个红包的时候动画就显得很卡顿。用户明明点选到了却没抢到红包,问题就被活动运营上报了。
了解事情大概后,我的电话已经拨给某个运营老大。
“老铁,这个问题情况如何了?”
“目前已经有几十笔投诉了,不过预计接下来流量很大。我现在已经让手下暂停推广这个活动页了。你那边怎么搞?”
“老铁,这样。你先别让暂停推广手段,你那边的流程照常进行,半小时内不要拿真正的活动地址,你可以先整个预热活动。”
电话那边沉默了十几秒。我知道她会帮我的。
“你能确定半小时后一切正常吗?”
“能。”
“那事情要是搞定了,跟我约会。”
啪!手机丢进抽屉,神特么约会,上帝来了也别打扰我!
我走到小小白,她还趴着那哭。小白在安慰,看到我过来。
“老大,怎么做?”
“让开。”
我坐在小白的位置上。
“锁屏密码。”
“110gaosuwoyaodidiao” (110告诉我要低调)
我是记得小白今天是有一个自动埋点的SDK要上线的,不过预计是晚上,白天还在做最后的回归。
“小白,我可以相信你吗?”
“可以。”
我打开vscode,在sdk上面埋伏了一段骇客代码。主要做了几件事情:
- 所有 margin、padding 要经过主线程反复计算的css属性换算成 translate
- 红包价格相关的计算全部延后计算。先记录用户的点击数据,统一在当次活动时间结束后统一做计算。
- 注入 requestIdleCallback
这样做很危险。没有测试,没有验证。甚至不合法。 但没有比现在更差劲的结果了。
commit 代码。直接越过测试上预发环境(上线前的模拟线上环境)。
“小白,拿你手机出来。”
我看了下时间,已经过去15分钟了。这时候小小白停止哭了,应该是被我吓的。
“小小白,5分钟内把测试那边的机器拿来跑一下。能做到吗?”
小小白机械式的点了点头,随后跑去拿手机了。
我盯着手机时间,21分钟的时候,上线。小小白却突然拉住我。
“老大,这里有个小米6,还是有点卡顿。”
我看了一眼,点击红包的时候会有轻微的抖动。随即没理小小白,上线。用我的权限强行开了一个自测迭代上线。
操作完以后,就是排队等待上线了。我突然问了句小小白。
“小小白,这次我能相信你吗?”
我看到了小小白眼里的慌张,但她还是咬了咬下嘴唇,对我点了点头。我回位置上拿出手机,老板的未接数量呛死了我的屏幕。
我走到老板办公室,敲开了门。老板把水杯摔到门口。
“你现在牛逼了啊!不把我放眼里了啊!运营老大把事情捅到业务方那边,问题都上升到欧总(CEO)那了。你他吗躲什么躲,第一时间解决问题知道吗?”
唉。运营部也真是一地鸡毛,那我运营老铁是个北方妹纸,估计就是个运营老四老五,至于那个老大,我和他闹过矛盾。这次估计就是他把事情捅上去了。
我假装淡定的关上门,隔绝一下外面的声音。我的直觉告诉我,我的那些娃肯定都在直勾勾的看着我。
“老板,到底发生什么事了。”
“你手底下的人搞出故障了,你还问我什么事?!”
我看了下时间,差不多了。我当着老板面,拿出手机,拨电话给老铁,打开外音。
“喂,啥事?我现在忙着搞活动。”
“不是出故障了吗?”
“出故障了?你吗? 我这边活动正常在进行,没听到谁说出了问题呀。”
“好了。那你先忙。”
我挂掉电话,眼睛疑惑的看着老板:“老板,我今天好像就是一个新活动页上线了,骗用户抢红包的。”
说完。我拿出手机,装模作样找到活动页,自己玩了一会儿。玩过了之后玩把游戏结果给老板看。
“老板,这不是挺好的嘛?”
接着我又装模作样打给小小白。
“喂,今天的活动页是你做的吧? 你拿几个手机过来,我在总监办公室这。”
意料之外,小小白进来了,我看她脸色居然看不出一丝哭过痕迹,要多淡定就有多淡定。我让小小白每个机器都演示了一遍。
“老板,你从哪听风就是雨?活动页开局有几十单投诉不是很正常吗?我刚在跟我丈母娘聊彩礼的事儿,没注意我那个手机响,至于吗?”
从老板办公室出来,我清晰的听见了身后小小白大口吸气的声音。
“怕了?走,今天有上线需求的人一起吃个夜宵。”
半夜,线上全部验证完毕。 撸串的时候,我告诉大家。
“出了问题要淡定,了不起就是被开除了。那个总监看我不顺眼不是一天两天了,我就敢怼他! 大家不仅要有写代码的能力,更重要的事要有解决问题的能力……”
其实有些事情大家都心照不宣,小小白喝了点啤酒,酒量差,又哭了一回。
3. 小小白对性能优化感兴趣
“故障”事件过去以后,小小白不是缠着小白就是缠着我,目的是想知道如何解决那个抢红包卡顿的问题。
我们来分析一下。卡顿现象,一般可以通过用户反馈或性能监控来发现。比如我们接到用户投诉说活动页卡导致抢不了红包了,然后在性能平台上查看卡顿指标后,发现页面出现连续 10 帧超过 40ms ,这就属于严重卡顿。
如何处理呢?
先确定一下是不是服务端出了数据问题。这些可以通过抓包或者查看服务端异常日志可以做到。
如果问题出在前端,一般是两种情形:浏览器的主线程与合成线程调度不合理,以及计算耗时操作。
拿这个活动页(红包雨)来说,用户投诉说卡顿,那是直观感受,大概率和动画效果有关。动画很有很多种,但主要还是是css计算。
浏览器的主线程
主要负责运行 JavaScript
,计算 CSS 样式
,元素布局
,合成线程主要负责绘制UI
。当 width、height、margin、padding
等作为 transition
值动画时,主线程压力很大,因为计算 CSS 样式
也是耗时的。此时如何用 transform
来代替直接设置 margin、padding
。
举个栗子。加入代表红包的那个DOM元素 margin-left: 0px
,最终红包在 margin-left: 100px
的时候被点击了,那这个过程假如step = 1,那从0 ~ 100 就需要浏览器主线程计算100次,每次计算完都要合成线程绘制到 GPU 再渲染到屏幕上。 这个过程,浏览器压力可想而知有多大。
但如果用 tranform:translate(-100px,0)
,浏览器主线程计算1次,合成线程就能 0 直接绘制100 。 其中差距可想而知。
另一方面,就是计算耗时操作。
-
Virtual DOM
的出现就是为了解决频繁操作DOM导致的性能卡顿问题。如果要操作100个DOM
,那先把操作信息存在Virtual DOM
上,待所有操作完毕再讲Virtual DOM
一次性更新到真实DOM上。React 和 Vue走的不正式 Virtual DOM 路线吗?
-
js计算。例如抢红包有大有小金额,js是单线程,已经在响应用户操作了,你这时候在要js去计算,那不就只能让js单线程先计算金额,用户的行为暂时放一边,你说用户感觉不到卡顿就有鬼了。这类问题解决方法有两种。
- 延迟js计算,等待优先级高的任务全部执行完了再做js计算。
- 如果一定要及时计算结果,那就尽量在浏览器的空闲时间去计算。
这里有个知识点提一下。
页面是一帧一帧绘制出来的,当每秒绘制的帧数达到 60 时,页面是流畅的,小于这个值时,用户会感觉到卡顿。
1s 60帧,每一帧分到的时间是 1000/60 ≈ 16 ms,如果每一帧执行的时间小于16ms,就说明浏览器有空余时间。
如果任务在剩余的时间内没有完成则会停止任务执行,继续优先执行主任务,也就是说 requestIdleCallback
总是利用浏览器的空余时间执行任务。
某个周末,上午11点。我还在睡梦中(太累了),小小白打来电话。
“喂,老大你还在睡觉呐!小白说要打羽毛球,把大家都叫上了,就差你了。”
啪!我把电话扔了。 还没过一会儿,手机开始夺命连环call模式。好,我输了,我错了,我捡回来……
“喂,最好给我一个不喷你的理由。否则拉黑!”
“&%@!¥%*(·”
“说人话!”
“我说你个死渣男,昨晚又在哪鬼混了?之前答应我的约会你怕不是早就忘记了吧?!”
好吧。运营老铁你好,我错了。你都知道我是个死渣男,那忘记了是我的错吗?
“老铁,我叫你老铁,你会找老铁约会吗?”
“就算不约会,吃个饭就这么难吗?”
电话那头突然很正经的来了这么一句。我感觉似乎有点不太对头。
“我马上要滚蛋了,你就不能请我吃顿饭吗?你抽点时间吃个饭还是可以的吧?”
我赶忙电话挂掉,微信群已经爆炸了。组织架构调整,运营老大升VP了,运营老铁要离职了。我的睡意顿时全无,又一个非常令我疯狂的想法出现在脑海里。
这傻姑娘不会是害怕运营老大找她的麻烦,清算上次的账了。上次的事情已经定性了,就是个误会。但VP是有很大权限的,要是翻案……
所以,成年人做任何事情都是需要付出代价的,职场也不例外。我很庆幸我有一些关系很铁的同事,他们更像是我的朋友。
新的周一,老铁已经走了。呵,这要是没领导同意,哪能这么快就走了。我看着我手底下这些娃,都在负责各自手上的事。我再看看领导办公室,我忽然间也有了决断。我也是时候该离开了。
就跟小小白约定的那样,等半年吧。于是,我开始了疯狂的技术分享之路。
4.前端性能优化总结
其实,前端性能最重要的指标是“快
”,实在快不了的再选择“作弊或者欺诈
”的手段。
快
方面:
-
请求资源
Html、js、css、图片等静态资源本质上都从服务器上获取。可服务器响应客户端请求是需要时间的,返回的资源体积越大,耗时越长。所以想要快,有三方面考虑。
- 减少资源体积大小。
- 优化
webpack
打包,treeShaking
、codeSplit等
,尽量保证打包后的体积不要太大。 - 较大、较多的图片等资源放
CDN
,这也是变相的减小了包体积。代码里直接引用CDN
的链接。
- 优化
- 加载资源的优先级
- 例如首屏,只需要优先渲染可视区的内容,非关键的延后加载即可。
- 关键数据可以让
native
进行预请求,再将关键数据直接交给h5即可
- 缓存
http缓存
本地缓存。
例如native对图片资源进行缓存接口缓存。
- 减少资源体积大小。
-
加载方式
- 懒加载
骨架屏
了解一下。
- 预加载
NSR
了解一下。
- 缓存加载
CSR
了解一下
- 离线包
- 懒加载
-
webview 优化
并行初始化
- 所谓并行初始化,是指用户在进入
App
时,系统就创建WebView
和加载模板,这样WebView 初始化
和 App 启动就可以并行进行了,这大大减少了用户等待时间。
- 所谓并行初始化,是指用户在进入
资源预加载
。资源预加载,是指提前在初始化的 WebView 里面放置一个静态资源列表,后续加载东西时,由于这部分资源已经被强缓存了,页面显示速度会更快。那么,要预加载的静态资源一般可以放:- 一定时间内不变的外链;
- 一些基础框架,多端适配的 JS(如 adapter.js),性能统计的SDK 或者第三方库(如 react/vue.js);
- 基础布局的 CSS 如 base.css。
- 其它。如何native 提供了接口请求的API,那针对接口请求也可做优化。
ps: 别小瞧webview这些,做好了能给你减少100-200ms的时间。
小结一下
-
APP启动阶段的优化方案
- webview优化
-
页面白屏阶段的优化方案
- 离线化设计,例如离线包
- 骨架屏
- SSR
- NSR
-
首屏渲染阶段的优化方案
- 优化请求数量
- 接口预加载
-
DOM性能优化
- 长列表优化。
memo、usememo
- 减少
Render
次数 - js计算优化
- 长列表优化。
-
性能平台
5. 即将结束的时光
我意识到这个时间节点到来的时候,是因为我接到了一个陌生电话,是背调公司来打听小小白情况的。
是啊,这半年也快过去了。小小白成长的速度也是跟做火箭一样。我认真看了看这群可爱的小伙伴,我发现大家都能开始独当一面了,即使我离开了大家也能混得开。
我偷偷用另外一个微信加了小小白好友,以可以帮她内推的名义和小小白打得火热。唉,果然年轻就是好骗,这种稍微留个心眼就能被戳穿的伎俩忽悠得小小白舒舒服服的。
我先要来了小小白的简历,给了一些建议,并帮忙修改。
其实最干净的简历是要保证HR、面试官能迅速对你的简历进行匹配。那样,即不会形成误会,也不会浪费双方的时间。例如有些人的简历喜欢把所有了解的技术都写上,例如了解node.js ,结果面试官就往node方面往死里问,结果可想而知。
我喜欢简历上能有一份个人履历信息。
个人履历 !== 工作经历。
个人履历更像是你对自己以往工作内容的一种 述职
。举个栗子:
1. 负责toB/toC业务相关项目设计
2. 对ToB、ToC业务的前端开发和管理,把控项目进度,推进合作方达成目标有丰富经验。
3. 喜欢研究新的技术,对能提高项目性能的极其热衷,并致力于将其更新至线上产品。
工作经历
必不可少的内容之一。除了把工作时间交待清楚,还希望把你当前的角色写清楚。当然还有你的在公司负责过的业务内容。
项目经历
请按照以下模版书写
- 项目名称
- 你在此项目中的角色
- 简洁明了切中要害的说一下项目背景
- 你付出了什么
效果/结果 是什么
: 例如你做了什么,提升了下单转化率,赚了多少钱,完成了多少KPI。希望用数字来扎眼。
自我评价
我希望你的自我评价能直接将你的厉害的方面直抒胸臆。
例如;
- 对XXX有实践心得
- 研究过Vue的源码
- 擅长项目基建
- ……
不希望看见精通、熟悉、了解
等字眼。因为一千个人眼里就有一千个哈姆雷特,也许你的精通是面试官的熟悉呢? 与其如此还不如交待明白你对此的技术研究程度。
end. 写在最后
万字了哈!小小白的故事其实没讲完,剧情有些狗血。就是小小白最终去了想去的大厂,然后发现了事实真相。
不过已经不重要了,结果是好的,曲终人散后各自安好便是。
再见,小小白。
再见,渣男。
小小白最后打电话喷我的,谁让我这么“舔狗式”的做法。即使真相被戳穿了依然打死不承认😂😂😂。
(祝,君安好。希望你能从这篇长文故事里得到自我成长! 另外,本篇没交待清楚的细节可能会在下一篇文。)