前端面试易错题收集--持续更新

html集结

1.回流与重绘

  • 什么是回流
    当render tree中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建。这就称为回流(reflow)。每个页面至少需要一次回流,就是在页面第一次加载的时候,这时候是一定会发生回流的,因为要构建render tree。在回流的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程成为重绘。

  • 什么是重绘
    当render tree中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,比如background-color。则就叫称为重绘。

  • 区别:
    他们的区别很大:
    回流必将引起重绘,而重绘不一定会引起回流。比如:只有颜色改变的时候就只会发生重绘而不会引起回流,当页面布局和几何属性改变时就需要回流。比如:添加或者删除可见的DOM元素,元素位置改变,元素尺寸改变——边距、填充、边框、宽度和高度,内容改变

javascript集结

1.宏任务微任务

宏任务
  • 可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。浏览器为了能够使得JS内部宏任务与DOM任务能够有序的执行,会每次执行完宏任务后渲染。
  • 以下属于宏任务

    script(整体代码)
    setTimeout
    setInterval
    I/O
    UI交互事件
    postMessage
    MessageChannel
    setImmediate(Node.js 环境)

微任务
  • 可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前task任务后,下一个task之前,在渲染之前。
  • 一下输入微任务

Promise.then
Object.observe
MutationObserver
process.nextTick(Node.js 环境)

2.手写一个EventBus

class Events {
  constructor() {
    this.events = new Map();
  }

  addEvent(key, fn, isOnce, ...args) {
    const value = this.events.get(key) ? this.events.get(key) : this.events.set(key, new Map()).get(key)
    value.set(fn, (...args1) => {
        fn(...args, ...args1)
        isOnce && this.off(key, fn)
    })
  }

  on(key, fn, ...args) {
    if (!fn) {
      console.error(`没有传入回调函数`);
      return
    }
    this.addEvent(key, fn, false, ...args)
  }

  fire(key, ...args) {
    if (!this.events.get(key)) {
      console.warn(`没有 ${key} 事件`);
      return;
    }
    for (let [, cb] of this.events.get(key).entries()) {
      cb(...args);
    }
  }

  off(key, fn) {
    if (this.events.get(key)) {
      this.events.get(key).delete(fn);
    }
  }

  once(key, fn, ...args) {
    this.addEvent(key, fn, true, ...args)
  }
}

3.垃圾回收

先来聊聊栈是如何垃圾回收的。其实栈的回收很简单,简单来说就是一个函数 push 进栈,执行完毕以后 pop 出来就当可以回收了
然后就是堆如何回收垃圾了,这部分的话会分为两个空间及多个算法。
两个空间分别为新生代和老生代,我们分开来讲每个空间中涉及到的算法。

  • 新生代
    新生代中的对象一般存活时间较短,空间也较小,使用 Scavenge GC 算法。

    在新生代空间中,内存空间分为两部分,分别为 From 空间和 To 空间。在这两个空间中,必定有一个空间是使用的,另一个空间是空闲的。新分配的对象会被放入 From 空间中,当 From 空间被占满时,新生代 GC 就会启动了。算法会检查 From 空间中存活的对象并复制到 To 空间中,如果有失活的对象就会销毁。当复制完成后将 From 空间和 To 空间互换,这样 GC 就结束了。

  • 老生代

    老生代中的对象一般存活时间较长且数量也多,使用了两个算法,分别是标记清除和标记压缩算法。
    在讲算法前,先来说下什么情况下对象会出现在老生代空间中:

    1.新生代中的对象是否已经经历过一次以上 Scavenge 算法,如果经历过的话,会将对象从新生代空间移到老生代空间中。
    2.To 空间的对象占比大小超过 25 %。在这种情况下,为了不影响到内存分配,会将对象从新生代空间移到老生代空间中。

    老生代中的空间很复杂,有如下几个空间

    enum AllocationSpace {
      // TODO(v8:7464): Actually map this space's memory as read-only.
      RO_SPACE,    // 不变的对象空间
      NEW_SPACE,   // 新生代用于 GC 复制算法的空间
      OLD_SPACE,   // 老生代常驻对象空间
      CODE_SPACE,  // 老生代代码对象空间
      MAP_SPACE,   // 老生代 map 对象
      LO_SPACE,    // 老生代大空间对象
      NEW_LO_SPACE,  // 新生代大空间对象
    
      FIRST_SPACE = RO_SPACE,
      LAST_SPACE = NEW_LO_SPACE,
      FIRST_GROWABLE_PAGED_SPACE = OLD_SPACE,
      LAST_GROWABLE_PAGED_SPACE = MAP_SPACE
    };
    

在老生代中,以下情况会先启动标记清除算法:

1.某一个空间没有分块的时候
2.空间中被对象超过一定限制
3.空间不能保证新生代中的对象移动到老生代中

在这个阶段中,会遍历堆中所有的对象,然后标记活的对象,在标记完成后,销毁所有没有被标记的对象。在标记大型对内存时,可能需要几百毫秒才能完成一次标记。这就会导致一些性能上的问题。为了解决这个问题,2011 年,V8 从 stop-the-world 标记切换到增量标志。在增量标记期间,GC 将标记工作分解为更小的模块,可以让 JS 应用逻辑在模块间隙执行一会,从而不至于让应用出现停顿情况。但在 2018 年,GC 技术又有了一个重大突破,这项技术名为并发标记。该技术可以让 GC 扫描和标记对象时,同时允许 JS 运行。

清除对象后会造成堆内存出现碎片的情况,当碎片超过一定限制后会启动压缩算法。在压缩过程中,将活的对象像一端移动,直到所有对象都移动完成然后清理掉不需要的内存。

React集结

1.useState更新相同的State,函数组件执行2次

下面的代码会打印两次。

const Index = () => {
  const [ number , setNumber  ] = useState(0)
  console.log('组件渲染',number)
  return <div className="page" >
    <div className="content" >
       <span>{ number }</span><br/>
       <button onClick={ () => setNumber(1) } >将number设置成1</button><br/>
       <button onClick={ () => setNumber(2) } >将number设置成2</button><br/>
       <button onClick={ () => setNumber(3) } >将number设置成3</button>
    </div>
  </div>
}
export default class Home extends React.Component{
  render(){
    return <Index />
  }
}

如上demo,三个按钮,我们期望连续点击每一个按钮,组件都会仅此渲染一次,于是我们开始实验
在这里插入图片描述

if (is(eagerState, currentState)) { 
     return
}
scheduleUpdateOnFiber(fiber, expirationTime); // 调度更新

第三次点击(三者言其多也)

那么第三次点击组件没有渲染,就很好解释了,第三次点击上一次树B中的 baseState = 1 和 setNumber(1)相等,也就直接走了return逻辑。

  • 双缓冲树:React 用 workInProgress树(内存中构建的树) 和 current(渲染树) 来实现更新逻辑。我们console.log打印的fiber都是在内存中即将 workInProgress的fiber树。双缓存一个在内存中构建,在下一次渲染的时候,直接用缓存树做为下一次渲染树,上一次的渲染树又作为缓存树,这样可以防止只用一颗树更新状态的丢失的情况,又加快了dom节点的替换与更新。

  • 更新机制:在一次更新中,首先会获取current树的 alternate作为当前的 workInProgress,渲染完毕后,workInProgress 树变为 current 树。我们用如上的树A和树B和已经保存的baseState模型,来更形象的解释了更新机制 。 hooks中的useState进行state对比,用的是缓存树上的state和当前最新的state。所有就解释了为什么更新相同的state,函数组件执行2次了。
    在这里插入图片描述

2.useEffect修改DOM元素怪异闪现

useEffect

基本上90%的情况下,都应该用这个,这个是在render结束后,你的callback函数执行,但是不会block browser painting,算是某种异步的方式吧,但是class的componentDidMount 和componentDidUpdate是同步的,在render结束后就运行,useEffect在大部分场景下都比class的方式性能更好.

useLayoutEffect

这个是用在处理DOM的时候,当你的useEffect里面的操作需要处理DOM,并且会改变页面的样式,就需要用这个,否则可能会出现出现闪屏问题, useLayoutEffect里面的callback函数会在DOM更新完成后立即执行,但是会在浏览器进行任何绘制之前运行完成,阻塞了浏览器的绘制.

function Index({ offset }){
    const card  = React.useRef(null)
    React.useEffect(()=>{
       card.current.style.left = offset
    },[])
    return <div className='box' >
        <div className='card custom' ref={card}   >square </div>
    </div>
}

export default function Home({ offset = '300px' }){
   const [ isRender , setRender ] = React.useState(false)
   return <div>
       { isRender && <Index offset={offset}  /> }
       <button onClick={ ()=>setRender(true) } > 挂载</button>
   </div>
}

在这里插入图片描述

改用下面的就不会有闪烁了

  React.useLayoutEffect(()=>{
      card.current.style.left = offset
  },[])
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值