react 生命周期
此阶段只有一个生命周期方法:constructor。
constructor()
用来做一些组件的初始化工作,如定义this.state的初始内容。如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数。
为什么必须先调用super(props)?
因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。
class Checkbox extends React.Component {
constructor(props) {
// ? 这时候还不能使用this
super(props);
// ? 现在开始可以使用this
console.log(props); // ? {}
console.log(this.props); // ? {}
this.state = {};
}
}
为什么super要传 props?
把 props 传进 super 是必要的,这使得基类 React.Component 可以初始化 this.props。
然而,即便在调用 super() 时没有传入 props 参数,你依然能够在 render 和其它方法中访问 this.props。
其实是 React 在调用你的构造函数之后,马上又给实例设置了一遍 props。
// React 内部
class Component {
constructor(props) {
this.props = props; // 初始化 this.props
// ...
}
}
// React 内部
const instance = new Button(props);
instance.props = props; // 给实例设置 props
// Button类组件
class Button extends React.Component {
constructor(props) {
super(); // ? 我们忘了传入 props
console.log(props); // ? {}
console.log(this.props); // ? undefined
}
}
此阶段生命周期方法:componentWillMount => render => componentDidMount
1. componentWillMount():
在组件挂载到DOM前调用,且只会被调用一次。 每一个子组件render之前立即调用; 在此方法调用this.setState不会引起组件重新渲染,也可以把写在这边的内容提前到constructor()中。
2. render(): class 组件唯一必须实现的方法
当 render 被调用时,它会检查 this.props 和 this.state 的变化并返回以下类型之一:
render() 函数应该为纯函数,这意味着在不修改组件 state 的情况下,每次调用时都返回相同的结果,并且它不会直接与浏览器交互。不能在里面执行this.setState,会有改变组件状态的副作用。
3. componentDidMount
会在组件挂载后(插入 DOM 树中)立即调用, 且只会被调用一次。依赖于 DOM 节点的初始化应该放在这里。 render之后并不会立即调用,而是所有的子组件都render完之后才会调用。
此阶段生命周期方法:componentWillReceiveProps => shouldComponentUpdate => componentWillUpdate => render => componentDidUpdate。
setState引起的state更新或父组件重新render引起的props更新,更新后的state和props相对之前无论是否有变化,都将引起子组件的重新render。
1. 父组件重新render
2. 自身setState
组件本身调用setState,无论state有没有变化。可通过shouldComponentUpdate方法优化。
1. componentWillReceiveProps(nextProps)
此方法只调用于props引起的组件更新过程中,响应 Props 变化之后进行更新的唯一方式。 参数nextProps是父组件传给当前组件的新props。根据nextProps和this.props来判断重传的props是否改变,以及做相应的处理。
2. shouldComponentUpdate(nextProps, nextState)
根据 shouldComponentUpdate() 的返回值,判断 React 组件的输出是否受当前 state 或 props 更改的影响。默认行为是 state 每次发生变化组件都会重新渲染。
当 props 或 state 发生变化时,shouldComponentUpdate() 会在渲染执行之前被调用。返回值默认为 true。
首次渲染或使用 forceUpdate() 时不会调用该方法。
此方法可以将 this.props 与 nextProps 以及 this.state 与nextState 进行比较,返回true时当前组件将继续执行更新过程,返回false则跳过更新,以此可用来减少组件的不必要渲染,优化组件性能。
请注意,返回 false 并不会阻止子组件在 state 更改时重新渲染。
PureComponent
组件,而不是手动编写 shouldComponentUpdate()
。PureComponent
会对 props 和 state 进行浅层比较,并减少了跳过必要更新的可能性。3. componentWillUpdate(nextProps, nextState)
此方法在调用render方法前执行,在这边可执行一些组件更新发生前的工作,一般较少用。
4. render
render同上
5. componentDidUpdate(prevProps, prevState)
此方法在组件更新后立即调用,可以操作组件更新的DOM。 prevProps和prevState这两个参数指的是组件更新前的props和state。
此阶段只有一个生命周期方法:componentWillUnmount
componentWillUnmount
此方法在组件被卸载前调用,可以在这里执行一些清理工作,比如清楚组件中使用的定时器,清楚componentDidMount中手动创建的DOM元素等,以避免引起内存泄漏。 componentWillUnmount() 中不应调用 setState(),因为该组件将永远不会重新渲染。组件实例卸载后,将永远不会再挂载它。
React v16.0刚推出的时候,增加了一个componentDidCatch生命周期函数,这只是一个增量式修改,完全不影响原有生命周期函数;
React v16.3,引入了两个新的生命周期:getDerivedStateFromProps,getSnapshotBeforeUpdate, 废弃掉componentWillMount、componentWillReceiveProps 以及 componentWillUpdate 三个周期(直到React 17前还可以使用,只是会有一个警告)。
生命周期函数的更改是因为 16.3 采用了 Fiber 架构,在新的 Fiber 架构中,组件的更新分为了两个阶段:
commit phase 的执行很快,但是真实 DOM 的更新很慢,所以 React 在更新的时候会暂停再恢复组件的更新以免长时间的阻塞浏览器,这就意味着 render phase 可能会被执行多次(因为有可能被打断再重新执行)。
这些生命周期都属于 render phase,render phase 可能被多次执行,所以要避免在 render phase 中的生命周期函数中引入副作用。在 16.3 之前的生命周期很容易引入副作用,所以 16.3 之后引入新的生命周期来限制开发者引入副作用。
getDerivedStateFromProps(nextProps, prevState)
React v16.3中,static getDerivedStateFromProps只在组件创建和由父组件引发的更新中调用。如果不是由父组件引发,那么getDerivedStateFromProps也不会被调用,如自身setState引发或者forceUpdate引发。
在React v16.4中改正了这一点,static getDerivedStateFromProps会在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。
特点:
getSnapshotBeforeUpdate()
getSnapshotBeforeUpdate() 只会调用一次,在最近一次渲染输出(提交到 DOM 节点)之前调用,,所以在这个生命周期能够获取这一次更新前的 DOM 的信息。此生命周期的任何返回值将作为 componentDidUpdate() 的第三个参数 “snapshot” 参数传递, 否则componentDidUpdate的第三个参数将为 undefined。
应返回 snapshot 的值(或 null)。
当渲染过程,生命周期,或子组件的构造函数中抛出错误时,会调用如下方法:
16.0 前生命周期
16.0 后生命周期:
参考: 浅析 React v16.3 新生命周期函数
在React 16前,函数式组件不能拥有状态管理?因为16以前只有类组件有对应的实例,而16以后Fiber 架构的出现,让每一个节点都拥有对应的实例,也就拥有了保存状态的能力。
Hooks的本质就是闭包和两级链表。
闭包是指有权访问另一个
函数作用域中变量或方法
的函数,创建闭包的方式就是在一个函数内创建闭包函数,通过闭包函数访问这个函数的局部变量, 利用闭包可以突破作用链域的特性,将函数内部的变量和方法
传递到外部。
一个组件包含的hooks 以链表的形式存储在fiber节点的memoizedState属性上,currentHook链表就是当前正在遍历的fiber节点的。nextCurrentHook 就是即将被添加到正在遍历fiber节点的hooks的新链表。
let currentHook: Hook | null = null;
let nextCurrentHook: Hook | null = null;
type Hooks = {
memoizedState: any, // 指向当前渲染节点 Fiber
baseState: any, // 初始化 initialState, 最新的state
baseUpdate: Update<any> | null,
// 当前需要更新的 Update ,每次更新完之后,会赋值上一个 update,方便 react 在渲染错误的边缘,数据回溯
queue: UpdateQueue<any> | null,// 可以让state变化的,即update或dispach产生的update
next: Hook | null, // link 到下一个 hooks
}
复制
其实state不是hooks独有的,类操作的setState也存在。
memoizedState,cursor 是存在哪里的?如何和每个函数组件一一对应的? react 会生成一棵组件树(或Fiber 单链表),树中每个节点对应了一个组件,hooks 的数据就作为组件的一个信息,存储在这些节点上,伴随组件一起出生,一起死亡。
为什么只能在函数最外层调用 Hook? memoizedState 是按 hook定义的顺序来放置数据的,如果 hook 顺序变化,memoizedState 并不会感知到。
自定义的 Hook 是如何影响使用它的函数组件的? 共享同一个 memoizedState,共享同一个顺序。
“Capture Value” 特性是如何产生的? 每一次 ReRender 的时候,都是重新去执行函数组件了,对于之前已经执行过的函数组件,并不会做任何操作。
setState 实现原理
setState 通过一个队列机制来实现 state 更新,当执行 setState() 时,会将需要更新的 state 浅合并后放入 状态队列,而不会立即更新 state,队列机制可以高效的批量更新 state。如果不通过setState,直接修改this.state 的值,则不会放入状态队列,当下一次调用 setState 对状态队列进行合并时,之前对 this.state 的修改将会被忽略,造成无法预知的错误。
setState()有的同步有的异步?
在React中, 如果是由React引发的事件处理(比如通过onClick引发的事件处理),调用setState不会同步更新this.state,除此之外的setState调用会同步执行this.state 。
所谓“除此之外”,指的是绕过React通过addEventListener直接添加的事件处理函数,还有通过setTimeout/setInterval产生的异步调用。
原因: 在React的setState函数实现中,会根据一个变量isBatchingUpdates判断是直接更新this.state还是放到队列中回头再说,而isBatchingUpdates默认是false,也就表示setState会同步更新this.state,但是,有一个函数batchedUpdates,这个函数会把isBatchingUpdates修改为true,而当React在调用事件处理函数之前就会调用这个batchedUpdates,造成的后果,就是由React控制的事件处理过程setState不会同步更新this.state。
setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。
调用风险
当调用 setState 时,实际上是会执行 enqueueSetState 方法,并会对 partialState 及 _pendingStateQueue 队列进行合并操作,最终通过 enqueueUpdate 执行 state 更新。
而 performUpdateIfNecessary 获取 _pendingElement、 _pendingStateQueue、_pendingForceUpdate,并调用 reaciveComponent 和 updateComponent 来进行组件更新。
但,如果在 shouldComponentUpdate 或 componentWillUpdate 方法里调用 this.setState 方法,就会造成崩溃。
这是因为在 shouldComponentUpdate 或 componentWillUpdate 方法里调用 this.setState 时,this._pendingStateQueue!=null,则 performUpdateIfNecessary 方法就会调用 updateComponent 方法进行组件更新,而 updateComponent 方法又会调用 shouldComponentUpdate和componentWillUpdate 方法,因此造成循环调用,使得浏览器内存占满后崩溃。
掉帧:在页面元素很多,且需要频繁刷新的场景下,React 15 会出现掉帧的现象,其根本原因,是大量的同步计算任务阻塞了浏览器的 UI 渲染。
默认情况下,JS 运算、页面布局和页面绘制都是运行在浏览器的主线程当中,他们之间是互斥的关系。
如果 JS 运算持续占用主线程,页面就没法得到及时的更新。
当我们调用setState更新页面的时候,React 会遍历应用的所有节点,计算出差异,然后再更新 UI,整个过程不能被打断。
如果页面元素很多,整个过程占用的时机就可能超过 16 毫秒,就容易出现掉帧的现象。
如何解决主线程长时间被 JS 运算?将JS运算切割为多个步骤,分批完成。在完成一部分任务之后,将控制权交回给浏览器,让浏览器有时间进行页面的渲染。等浏览器忙完之后,再继续之前未完成的任务。
React 15 及以下版本通过递归的方式进行渲染,使用的是 JS 引擎自身的函数调用栈,它会一直执行到栈空为止。
而Fiber实现了自己的组件调用栈,它以链表的形式遍历组件树,可以灵活的暂停、继续和丢弃执行的任务。实现方式是使用了浏览器的requestIdleCallback
window.requestIdleCallback()会在浏览器空闲时期依次调用函数,这就可以让开发者在主事件循环中执行后台或低优先级的任务,而且不会对像动画和用户交互这些延迟触发但关键的事件产生影响。函数一般会按先进先调用的顺序执行,除非函数在浏览器调用它之前就到了它的超时时间。
React 框架内部的运作可以分为 3 层:
Fiber 表征reconciliation阶段所能拆分的最小工作单元,其实指的是一种链表树,它可以用一个纯 JS 对象来表示:
const fiber = {
stateNode: {}, // 节点实例
child: {}, // 子节点
sibling: {}, // 兄弟节点
return: {}, // 表示处理完成后返回结果所要合并的目标,通常指向父节点
};
Reconciler区别
从Stack Reconciler到Fiber Reconciler,源码层面其实就是干了一件递归改循环的事情。
scheduling(调度)
scheduling(调度)是fiber reconciliation的一个过程,主要是进行任务分配,达到分段执行。任务的优先级有六种:
优先级高的任务(如键盘输入)可以打断优先级低的任务(如Diff)的执行,从而更快的生效。
Fiber Reconciler 在执行过程中,会分为 2 个阶段:
阶段一可被打断的特性,让优先级更高的任务先执行,从框架层面大大降低了页面掉帧的概率。
参考: React Fiber 原理介绍 React Fiber
Render Props: 把将要包裹的组件作为props属性传入,然后容器组件调用这个属性,并向其传参。
实现方式:
1.通过props.children(props),props.children返回的是UI元素。<RenderProps> JSX 标签中的所有内容都会作为一个 children prop 传递给 RenderProps组件。因为 RenderProps 将 {props.children} 渲染在一个 <div> 中,被传递的这些子组件最终都会出现在输出结果中。
// 定义
const RenderProps = props => <div>
{props.children(props)}
</div>
// 调用
<RenderProps>
{() => <>Hello RenderProps</>}
</RenderProps>
复制
2.通过props中的任何函数, 自行定义传入内容
// 定义
const LoginForm = props => {
const flag = false;
const allProps = { flag, ...props };
if (flag) {
return <>{props.login(allProps)}</>
} else {
return <>{props.notLogin(allProps)}</>
}
}
// 调用
<LoginForm
login={() => <h1>LOGIN</h1>}
noLogin={() => <h1>NOT LOGIN</h1>}
/>
优点: 1、支持ES6 2、不用担心props命名问题,在render函数中只取需要的state 3、不会产生无用的组件加深层级 4、render props模式的构建都是动态的,所有的改变都在render中触发,可以更好的利用组件内的生命周期。
HOC: 接受一个组件作为参数,返回一个新的组件的函数。
class Home extends React.Component {
// UI
}
export default Connect()(Home);
高阶组件由于每次都会返回一个新的组件,对于react来说,这是不利于diff和状态复用的,所以高阶组件的包装不能在render 方法中进行,而只能像上面那样在组件声明时包裹,这样也就不利于动态传参。
优点: 1、支持ES6 2、复用性强,HOC为纯函数且返回值为组件,可以多层嵌套 3、支持传入多个参数,增强了适用范围 缺点: 1、当多个HOC一起使用时,无法直接判断子组件的props是哪个HOC负责传递的 2、多个组件嵌套,容易产生同样名称的props 3、HOC可能会产生许多无用的组件,加深了组件的层级
总的来说,render props其实和高阶组件类似,就是在puru component上增加state,响应react的生命周期。
react的数据流是单向的,最常见的就是通过props由父组件向子组件传值。
1、找一个相同的父组件,既可以用props传递数据,也可以用context的方式来传递数据。 2、用一些全局机制去实现通信,比如redux等 3、发布订阅模式
React 合成事件(SyntheticEvent)是 React 模拟原生 DOM 事件所有能力的一个事件对象,即浏览器原生事件的跨浏览器包装器。
为什么要使用合成事件?
实现原理 在 React 中,“合成事件”会以事件委托方式绑定在 document 对象上,并在组件卸载(unmount)阶段自动销毁绑定的事件。
合成事件和原生事件 当真实 DOM 元素触发事件,会冒泡到 document 对象后,再处理 React 事件;所以会先执行原生事件,然后处理 React 事件;最后真正执行 document 上挂载的事件。 合成事件和原生事件最好不要混用。原生事件中如果执行了stopPropagation方法,则会导致其他React事件失效。因为所有元素的事件将无法冒泡到document上,所有的 React 事件都将无法被注册。
合成事件的事件池 合成事件对象池,是 React 事件系统提供的一种性能优化方式。合成事件对象在事件池统一管理,不同类型的合成事件具有不同的事件池。
在 React 中,render 执行的结果得到的并不是真正的 DOM 节点,而是轻量级的 JavaScript 对象,我们称之为 virtual DOM。它通过JS的Object对象模拟DOM中的节点,然后再通过特定的render方法将其渲染成真实的DOM节点。
虚拟 DOM 是 React 的一大亮点,具有batching(批处理) 和高效的 Diff 算法。batching 把所有的 DOM 操作搜集起来,一次性提交给真实的 DOM。diff 算法时间复杂度也从标准的的 Diff 算法的 O(n^3) 降到了 O(n)。
batching(批处理) 主要思想是,无论setState您在React事件处理程序或同步生命周期方法中进行多少次调用,它都将被批处理成一个更新, 最终只有一次重新渲染。
如果没有 Virtual DOM,就需要直接操作原生 DOM。在一个大型列表所有数据都变了的情况下,直接重置 innerHTML还算合理,但是,只有一行数据发生变化时,它也需要重置整个 innerHTML,这就造成了大量浪费。
innerHTML 和 Virtual DOM 的重绘性能消耗: innerHTML: render html string + 重新创建所有 DOM 元素 Virtual DOM: render Virtual DOM + diff + 必要的 DOM 更新
Virtual DOM render + diff 显然比渲染 html 字符串要慢,但是它依然是纯 js 层面的计算,比起后面的 DOM 操作来说,依然便宜了太多。innerHTML 的总计算量不管是 js 计算还是 DOM 操作都是和整个界面的大小相关,但 Virtual DOM 的计算量只有 js 计算和界面大小相关,DOM 操作是和数据的变动量相关。
Real DOM | Virtual DOM |
---|---|
1. 更新缓慢。 | 1. 更新更快。 |
2. 可以直接更新 HTML。 | 2. 无法直接更新 HTML。 |
3. 如果元素更新,则创建新DOM。 | 3. 如果元素更新,则更新 JSX 。 |
4. DOM操作代价很高。 | 4. DOM 操作非常简单。 |
5. 消耗的内存较多。 | 5. 很少的内存消耗。 |
相比起 React,其他 MVVM 系框架比如 Angular, Knockout , Vue ,Avalon 采用的都是数据绑定。通过 Directive/Binding 对象,观察数据变化并保留对实际 DOM 元素的引用,当有数据变化时进行对应的操作。MVVM 的变化检查是数据层面的,而 React 的检查是 DOM 结构层面的。
MVVM 的性能也根据变动检测的实现原理有所不同:Angular 依赖于脏检查;Knockout/Vue/Avalon 采用了依赖收集。
Angular 最不效率的地方在于任何小变动都有的和 watcher 数量相关的性能代价,当所有数据都变了的时候,Angular更有效。依赖收集在初始化和数据变化的时候都需要重新收集依赖,这个代价在小量更新的时候几乎可以忽略,但在数据量庞大的时候也会产生一定的消耗。
性能比较
在比较性能的时候,要分清楚初始渲染、小量数据更新、大量数据更新这些不同的场合。Virtual DOM、脏检查 MVVM、数据收集 MVVM 在不同场合各有不同的表现和不同的优化需求。
Virtual DOM 为了提升小量数据更新时的性能,也需要针对性的优化,比如 shouldComponentUpdate 或是 immutable data。
传统 diff 算法通过循环递归对节点进行依次对比,算法复杂度达到 O(n^3),其中 n 是树中节点的总数。O(n^3) 意味着如果要展示1000个节点,就要依次执行上十亿次的比较, 这是无法满足现代前端性能要求的。
diff 算法主要包括几个步骤:
React 通过制定大胆的diff策略,将diff算法复杂度从 O(n^3) 转换成 O(n) 。
React 对树进行分层比较,两棵树只会对同一层次的节点进行比较。 当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会进行进一步的比较。
这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。 当出现节点跨层级移动时,并不会出现移动操作,而是以该节点为根节点的树被重新创建,这是一种影响 React 性能的操作,因此 React 官方建议不要进行 DOM 节点跨层级的操作。
注意:在开发组件时,保持稳定的 DOM 结构会有助于性能的提升。例如,可以通过 CSS 隐藏或显示节点,而不是真的移除或添加 DOM 节点。
对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切的知道这点那可以节省大量的 diff 运算时间。因此 React 允许用户通过 shouldComponentUpdate() 来判断该组件是否需要进行 diff。
对于两个不同类型但结构相似的组件,不会比较二者的结构,而且替换整个组件的所有内容。不同类型的 component 是很少存在相似 DOM tree 的机会,因此这种极端因素很难在实现开发过程中造成重大影响的。
当节点处于同一层级时,React diff 提供了三种节点操作,分别为:INSERT_MARKUP(插入)、MOVE_EXISTING(移动)和 REMOVE_NODE(删除)。
<ul>
<li>Duke</li>
<li>Villanova</li>
</ul>
<ul>
<li>Connecticut</li>
<li>Duke</li>
<li>Villanova</li>
</ul>
React 并不会意识到应该保留<li>Duke</li>和<li>Villanova</li>,而是会重建每一个子元素,不会进行移动 DOM 操作。
key 优化
为了解决上述问题,React 引入了 key 属性, 对同一层级的同组子节点,添加唯一 key 进行区分。
当子元素拥有 key 时,React 使用 key 来匹配原有树上的子元素以及最新树上的子元素。如果有相同的节点,无需进行节点删除和创建,只需要将老集合中节点的位置进行移动,更新为新集合中节点的位置。
Math.random()
生成的)会导致许多组件实例和 DOM 节点被不必要地重新创建,这可能导致性能下降和子组件中的状态丢失。1. 监听数据变化的实现原理不同
Vue通过 getter/setter以及一些函数的劫持,能精确知道数据变化。 React默认是通过比较引用的方式(diff)进行的,如果不优化可能导致大量不必要的VDOM的重新渲染。
2. 数据流不同
Vue1.0中可以实现两种双向绑定:父子组件之间props可以双向绑定;组件与DOM之间可以通过v-model双向绑定。 Vue2.x中父子组件之间不能双向绑定了(但是提供了一个语法糖自动帮你通过事件的方式修改)。 React一直不支持双向绑定,提倡的是单向数据流,称之为onChange/setState()模式。
3. HoC和mixins
Vue组合不同功能的方式是通过mixin,Vue中组件是一个被包装的函数,并不简单的就是我们定义组件的时候传入的对象或者函数。 React组合不同功能的方式是通过HoC(高阶组件)。
4. 模板渲染方式的不同
模板的语法不同,React是通过JSX渲染模板, Vue是通过一种拓展的HTML语法进行渲染。 模板的原理不同,React通过原生JS实现模板中的常见语法,比如插值,条件,循环等。而Vue是在和组件JS代码分离的单独的模板中,通过指令来实现的,比如 v-if 。
举个例子,说明React的好处:react中render函数是支持闭包特性的,所以我们import的组件在render中可以直接调用。但是在Vue中,由于模板中使用的数据都必须挂在 this 上进行一次中转,所以我们import 一个组件完了之后,还需要在 components 中再声明下。
5. 渲染过程不同
Vue可以更快地计算出Virtual DOM的差异,这是由于它会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树。 React当状态被改变时,全部子组件都会重新渲染。通过shouldComponentUpdate这个生命周期方法可以进行控制,但Vue将此视为默认的优化。
6. 框架本质不同
Vue本质是MVVM框架,由MVC发展而来; React是前端组件化框架,由后端组件化发展而来。
CDN是一组分布在多个不同地理位置的 Web 服务器。当服务器离用户越远时,延迟越高。
头部内联的样式和脚本会阻塞页面的渲染,样式放在头部并使用link方式引入,脚本放在尾部并使用异步方式加载
压缩文件可以减少文件下载时间。
window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行
index.html 设置成 no-cache,这样每次请求的时候都会比对一下 index.html 文件有没变化,如果没变化就使用缓存,有变化就使用新的 index.html 文件。 其他所有文件一律使用长缓存,例如设置成缓存一年 maxAge: 1000 * 60 * 60 * 24 * 365。 前端代码使用 webpack 打包,根据文件内容生成对应的文件名,每次重新打包时只有内容发生了变化,文件名才会发生变化。
1. DNS域名解析:拿到服务器ip
客户端收到你输入的域名地址后,它首先去找本地的hosts文件,检查在该文件中是否有相应的域名、IP对应关系,如果有,则向其IP地址发送请求,如果没有,再去找DNS服务器。
2. 建立TCP链接:客户端链接服务器
TCP提供了一种可靠、面向连接、字节流、传输层的服务。对于客户端与服务器的TCP链接,必然要说的就是『三次握手』。“3次握手”的作用就是双方都能明确自己和对方的收、发能力是正常的。
客户端发送一个带有SYN标志的数据包给服务端,服务端收到后,回传一个带有SYN/ACK标志的数据包以示传达确认信息,最后客户端再回传一个带ACK标志的数据包,代表握手结束,连接成功。
SYN —— 用于初如化一个连接的序列号。 ACK —— 确认,使得确认号有效。 RST —— 重置连接。 FIN —— 该报文段的发送方已经结束向对方发送数据。
客户端:“你好,在家不。” -- SYN 服务端:“在的,你来吧。” -- SYN + ACK 客户端:“好嘞。” -- ACK
3. 发送HTTP请求
4. 服务器处理请求
5. 返回响应结果
6. 关闭TCP连接(需要4次握手)
为了避免服务器与客户端双方的资源占用和损耗,当双方没有请求或响应传递时,任意一方都可以发起关闭请求。
关闭连接时,服务器收到对方的FIN报文时,仅仅表示客户端不再发送数据了但是还能接收数据,而服务器也未必全部数据都发送给客户端,所以服务器可以立即关闭,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送,从而导致多了一次。
客户端:“兄弟,我这边没数据要传了,咱关闭连接吧。” -- FIN + seq 服务端:“收到,我看看我这边有木有数据了。” -- ACK + seq + ack 服务端:“兄弟,我这边也没数据要传你了,咱可以关闭连接了。” - FIN + ACK + seq + ack 客户端:“好嘞。” -- ACK + seq + ack
7. 浏览器解析HTML
浏览器需要加载解析的不仅仅是HTML,还包括CSS、JS,以及还要加载图片、视频等其他媒体资源。
浏览器通过解析HTML,生成DOM树,解析CSS,生成CSSOM树,然后通过DOM树和CSSPOM树生成渲染树。渲染树与DOM树不同,渲染树中并没有head、display为none等不必显示的节点。
浏览器的解析过程并非是串连进行的,比如在解析CSS的同时,可以继续加载解析HTML,但在解析执行JS脚本时,会停止解析后续HTML,会出现阻塞问题。
8. 浏览器渲染页面
根据渲染树布局,计算CSS样式,即每个节点在页面中的大小和位置等几何信息。HTML默认是流式布局的,CSS和js会打破这种布局,改变DOM的外观样式以及大小和位置。最后浏览器绘制各个节点,将页面展示给用户。
replaint:屏幕的一部分重画,不影响整体布局,比如某个CSS的背景色变了,但元素的几何尺寸和位置不变。reflow:意味着元素的几何尺寸变了,需要重新计算渲染树。
参考: 细说浏览器输入URL后发生了什么 浏览器输入 URL 后发生了什么?
路由是用来跟后端服务器进行交互的一种方式,通过不同的路径请求不同的资源。 路由这概念最开始是在后端出现, 在前后端不分离的时期, 由后端来控制路由, 服务器接收客户端的请求,解析对应的url路径, 并返回对应的页面/资源。
Ajax,全称 Asynchronous JavaScript And XML,是浏览器用来实现异步加载的一种技术方案。
在Ajax没有出现时期,大多数的网页都是通过直接返回 HTML,用户的每次更新操作都需要重新刷新页面,及其影响交互体验。为了解决这个问题,提出了Ajax(异步加载方案), 有了 Ajax 后,用户交互就不用每次都刷新页面。后来出现SPA单页应用。
SPA 中用户的交互是通过 JS 改变 HTML 内容来实现的,页面本身的 url 并没有变化,这导致了两个问题:
前端路由就是为了解决上述问题而出现的。
前端路由的实现实际上是检测 url 的变化,截获 url 地址,解析来匹配路由规则。有下面两种实现方式:
hash 就是指 url 后的 # 号以及后面的字符。 #后面 hash 值的变化,并不会导致浏览器向服务器发出请求,浏览器不发请求,也就不会刷新页面。
hash 的改变会触发 hashchange 事件,可以用onhashchange事件来监听hash值的改变。
// 监听hash变化,点击浏览器的前进后退会触发
window.onhashchange = function() { ... }
window.addEventListener('hashchange', function(event) { ...}, false);
在 HTML5 之前,浏览器就已经有了 history 对象。但在早期的 history 中只能用于多页面的跳转:
history.go(-1); // 后退一页
history.go(2); // 前进两页
history.forward(); // 前进一页
history.back(); // 后退一页
在 HTML5 的规范中,history 新增了几个 API:
history.pushState(); // 向当前浏览器会话的历史堆栈中添加一个状态
history.replaceState();// 修改了当前的历史记录项(不是新建一个)
history.state // 返回一个表示历史堆栈顶部的状态的值
复制
由于 history.pushState() 和 history.replaceState() 可以改变 url 同时,不会刷新页面,所以在 HTML5 中的 histroy 具备了实现前端路由的能力。 window对象提供了onpopstate事件来监听历史栈的改变,一旦历史栈信息发生改变, 便会触发该事件。
调用history.pushState()或history.replaceState()不会触发popstate事件。只有在做出浏览器动作时,才会触发该事件,例如执行
history.back()
或history.forward()
后触发window.onpopstate
事件。
// 历史栈改变
window.onpopstate = function() { ... }
注意:pushState() 不会造成 hashchange 事件调用, 即使新的URL和之前的URL只是锚的数据不同。
对比 | Hash | History |
---|---|---|
路径 | 带#, 路径丑 | 正常路径 |
兼容性 | >=ie8 | >=ie10 |
实用性 | 直接使用,无需服务端配合处理。 | 需服务端配合处理 |
命名空间 | 同一document | 同源 |
锚点 | 导致锚点功能失效 | 锚点功能正常 |
vue-router/react-router 都是基于前端路由的原理实现的~ react-router常用的 history 有三种形式:
Babel是代码转换器,比如将ES6转成ES5,或者将JSX转成JS等。借助Babel,开发者可以提前用上新的JS特性。
原始代码 --> [Babel Plugin] --> 转换后的代码
Plugin
实现Babel代码转换功能的核心,就是Babel插件(plugin)。Babel插件一般尽可能拆成小的力度,开发者可以按需引进, 既提高了性能,也提高了扩展性。比如对ES6转ES5的功能,Babel官方拆成了20+个插件。开发者想要体验ES6的箭头函数特性,那只需要引入transform-es2015-arrow-functions插件就可以,而不是加载ES6全家桶。
Preset
可以简单的把Babel Preset视为Babel Plugin的集合。想要将所有ES6的代码转成ES5,逐个插件引入的效率比较低下, 就可以采用Babel Preset。比如babel-preset-es2015就包含了所有跟ES6转换有关的插件。
Plugin与Preset执行顺序
可以同时使用多个Plugin和Preset,此时,它们的执行顺序非常重要。
比如.babelrc配置如下,那么执行的顺序为:
{
"presets": [
"es2015",
"es2016"
],
"plugins": [
"transform-react-jsx",
"transform-async-to-generator"
]
}
为了进一步提升网站性能,会把静态资源和动态网页分集群部署,静态资源会被部署到CDN节点上,网页中引用的资源也会变成对应的部署路径。当需要更新静态资源的时候,同时也会更新html中的引用。
如果同时改了页面结构和样式,也更新了静态资源对应的url地址,现在要发布代码上线,是先上线页面,还是先上线静态资源?
这个奇葩问题,起源于资源的 覆盖式发布,用 待发布资源 覆盖 已发布资源,就有这种问题。
解决它也好办,就是实现 非覆盖式发布。用文件的摘要信息来对资源文件进行重命名,把摘要信息放到资源文件发布路径中,这样,内容有修改的资源就变成了一个新的文件发布到线上,不会覆盖已有的资源文件。
上线过程中,先全量部署静态资源,再灰度部署页面,整个问题就比较完美的解决了。
大公司的静态资源优化方案,基本上要实现这么几个东西:
function add(a, b){
const maxLength = Math.max(a.length, b.length);
a = a.padStart(maxLength, 0);
b = b.padStart(maxLength, 0);
let t = 0;
let f = 0;
let sum = "";
for (let i = maxLength - 1; i >= 0; i--) {
t = parseInt(a[i]) + parseInt(b[i]) + f;
f = Math.floor(t / 10);
sum = `${t % 10}${sum}`;
}
if (f === 1){
sum = "1" + sum;
}
return sum;
}
function fib(n) {
if (n <= 0) {
return 0;
}
let n1 = 1;
let n2 = 1;
let sum = 1;
for(let i = 3; i <= n; i++) {
[n1, n2] = [n2, sum];
sum = n1 + n2;
}
return sum;
};