我终于把你送进了大厂

22,607 阅读31分钟

本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,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写出毛玻璃的效果。 你们说,老大是不是个狠人!”

image.png

<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天,这件事情得分开说。我不会主动让小白离开,是因为小白的技术还不够,不能和大章一样能出去独当一面。如果是小白因自己的事提出离职,我或许没办法阻拦,但这种一命换一命的做法,我实在没办法苟同。

我认为的前端能力等级

  1. 初级 -- 能正常执行需求,代码书写经验足。
  2. 中级 -- 能理解常用技术栈的原理,并在代码层面写出性能较好的代码,且能hold住一个项目。
  3. 高级 -- 能独挡一面。能及时解决线上响应的问题,并能提供技术解决方案。
  4. 资深 -- 能单独承担商业解决方案。
  5. 专家 -- 技术上广度要有,还要有深度。管理方面能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 是不是前面没解释清楚的 调度员 的归宿?! 解释一下这行代码: 当 currentnull 或者 currentmemoizedState 属性为 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];
}

噢,这个代码明显要更容易分析一些。

  1. 先拿到 hook 的信息 也就是 const hook = mountWorkInProgressHook();
  2. 对入参 initialState 进行判别。接着将 initialState 赋值给 hook.memoizedStatehook.baseState
  3. 接下来就申明了一个队列 queue,信息看注释。
  4. 申明调度函数 dispatch , 用来更新 state
  5. 返回一个数组,方便我们解构。也就是 :
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 节点的构建。

image.png

至于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、图片等静态资源本质上都从服务器上获取。可服务器响应客户端请求是需要时间的,返回的资源体积越大,耗时越长。所以想要快,有三方面考虑。

    1. 减少资源体积大小。
      • 优化 webpack 打包, treeShakingcodeSplit等,尽量保证打包后的体积不要太大。
      • 较大、较多的图片等资源放CDN,这也是变相的减小了包体积。代码里直接引用CDN的链接。
    2. 加载资源的优先级
      • 例如首屏,只需要优先渲染可视区的内容,非关键的延后加载即可。
      • 关键数据可以让native进行预请求,再将关键数据直接交给h5即可
    3. 缓存
      • http缓存
      • 本地缓存。例如native对图片资源进行缓存
      • 接口缓存。
  • 加载方式

    1. 懒加载
      • 骨架屏 了解一下。
    2. 预加载
      • NSR 了解一下。
    3. 缓存加载
      • CSR 了解一下
    4. 离线包
  • webview 优化

    1. 并行初始化
      • 所谓并行初始化,是指用户在进入 App 时,系统就创建 WebView 和加载模板,这样 WebView 初始化和 App 启动就可以并行进行了,这大大减少了用户等待时间。
    2. 资源预加载。资源预加载,是指提前在初始化的 WebView 里面放置一个静态资源列表,后续加载东西时,由于这部分资源已经被强缓存了,页面显示速度会更快。那么,要预加载的静态资源一般可以放:
      • 一定时间内不变的外链;
      • 一些基础框架,多端适配的 JS(如 adapter.js),性能统计的SDK 或者第三方库(如 react/vue.js);
      • 基础布局的 CSS 如 base.css。
    3. 其它。如何native 提供了接口请求的API,那针对接口请求也可做优化。

    ps: 别小瞧webview这些,做好了能给你减少100-200ms的时间。

小结一下

  1. APP启动阶段的优化方案

    • webview优化
  2. 页面白屏阶段的优化方案

    • 离线化设计,例如离线包
    • 骨架屏
    • SSR
    • NSR
  3. 首屏渲染阶段的优化方案

    • 优化请求数量
    • 接口预加载
  4. DOM性能优化

    • 长列表优化。 memo、usememo
    • 减少Render次数
    • js计算优化
  5. 性能平台

    想了解请看另外一个故事

5. 即将结束的时光

我意识到这个时间节点到来的时候,是因为我接到了一个陌生电话,是背调公司来打听小小白情况的。

是啊,这半年也快过去了。小小白成长的速度也是跟做火箭一样。我认真看了看这群可爱的小伙伴,我发现大家都能开始独当一面了,即使我离开了大家也能混得开。

我偷偷用另外一个微信加了小小白好友,以可以帮她内推的名义和小小白打得火热。唉,果然年轻就是好骗,这种稍微留个心眼就能被戳穿的伎俩忽悠得小小白舒舒服服的。

我先要来了小小白的简历,给了一些建议,并帮忙修改。

其实最干净的简历是要保证HR、面试官能迅速对你的简历进行匹配。那样,即不会形成误会,也不会浪费双方的时间。例如有些人的简历喜欢把所有了解的技术都写上,例如了解node.js ,结果面试官就往node方面往死里问,结果可想而知。

我喜欢简历上能有一份个人履历信息。

个人履历 !== 工作经历。

个人履历更像是你对自己以往工作内容的一种 述职 。举个栗子:

1. 负责toB/toC业务相关项目设计
2. 对ToB、ToC业务的前端开发和管理,把控项目进度,推进合作方达成目标有丰富经验。
3. 喜欢研究新的技术,对能提高项目性能的极其热衷,并致力于将其更新至线上产品。

工作经历

必不可少的内容之一。除了把工作时间交待清楚,还希望把你当前的角色写清楚。当然还有你的在公司负责过的业务内容。

项目经历

请按照以下模版书写

  1. 项目名称
  2. 你在此项目中的角色
  3. 简洁明了切中要害的说一下项目背景
  4. 你付出了什么
  5. 效果/结果 是什么 : 例如你做了什么,提升了下单转化率,赚了多少钱,完成了多少KPI。希望用数字来扎眼。

自我评价

我希望你的自我评价能直接将你的厉害的方面直抒胸臆。

例如;

  1. 对XXX有实践心得
  2. 研究过Vue的源码
  3. 擅长项目基建
  4. ……

不希望看见精通、熟悉、了解等字眼。因为一千个人眼里就有一千个哈姆雷特,也许你的精通是面试官的熟悉呢? 与其如此还不如交待明白你对此的技术研究程度。

end. 写在最后

万字了哈!小小白的故事其实没讲完,剧情有些狗血。就是小小白最终去了想去的大厂,然后发现了事实真相。

不过已经不重要了,结果是好的,曲终人散后各自安好便是。

再见,小小白。

再见,渣男。

小小白最后打电话喷我的,谁让我这么“舔狗式”的做法。即使真相被戳穿了依然打死不承认😂😂😂。

(祝,君安好。希望你能从这篇长文故事里得到自我成长! 另外,本篇没交待清楚的细节可能会在下一篇文。)