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
},[])