前端面试题总结(react版)

面试题总结

1、说说你对react的理解?有哪些特性?

React,用于构建用户界面的 JavaScript 库,只提供了 UI 层面的解决方案 遵循组件设计模式、声明式编程范式和函数式编程概念,以使前端应用程序更高效 使用虚拟 DOM 来有效地操作 DOM,遵循从高阶组件到低阶组件的单向数据流 帮助我们将界面成了各个独立的小块,每一个块就是组件,这些组件之间可以组合、嵌套,构成整体页面

特性:JSX 语法 单向数据绑定 虚拟 DOM 声明式编程 Component

优势:高效灵活 声明式的设计,简单使用 组件式开发,提高代码复用率 单向响应的数据流会比双向绑定的更安全,速度更快

2、React组件之间如何通信?

react组件通信有几种方式:

  1. props传参

  2. pub-sub传参

  3. context发布者订阅者

  4. redux

props传参:

//父组件在子组件传递数据
import React, { Component } from 'react';
class Parents extends Component {
  render() {
    return (
      <div>
          <Children state={'name':'list'} />
      </div>
    );
  }
}

//子组件在props中接受数据
import React, { Component } from 'react';
class Children extends Component {
  render() {
  let {name} = this.props
    return (
      <div>
         	{name}
      </div>
    );
  }
}

pub-sub传参:

需要预先下载pub-sub包

在需要传值的组件中,进行信息的发布(publish): publish方法的第一个参数(yyds)为订阅名,第二个参(message1)为想要传递的变量。 PubSub.publish (‘yyds’, ‘message1’) //通过事件触发方法发布信息‘message1’

context传参:

上下文(Context) 提供了一种通过组件树传递数据的方法,无需在每个级别手动传递 props 属性。 通过Provider(提供者) 后代和Consumer(使用者) 组件 进行通信

redux

redux是管理公共数据的一个存储,像vue脚手架中的vuex类似,所有的视图都可以操作在其中的数据。

起到一个传递数据以及减少缓存的效果

3、说说React生命周期有哪些不同的阶段?每个阶段对应的方法是?

这里主要讲述react16.4之后的生命周期,可以分成三个阶段:

  • 创建阶段
  • 更新阶段
  • 卸载阶段

创建阶段

创建阶段主要分成了以下几个生命周期方法:

  • constructor
  • getDerivedStateFromProps
  • render
  • componentDidMount

constructor

实例过程中自动调用的方法,在方法内部通过super关键字获取来自父组件的props

在该方法中,通常的操作为初始化state状态或者在this上挂载方法

getDerivedStateFromProps

该方法是新增的生命周期方法,是一个静态的方法,因此不能访问到组件的实例

执行时机:组件创建和更新阶段,不论是props变化还是state变化,也会调用

在每次render方法前调用,第一个参数为即将更新的props,第二个参数为上一个状态的state,可以比较propsstate来加一些限制条件,防止无用的state更新

该方法需要返回一个新的对象作为新的state或者返回null表示state状态不需要更新

render

类组件必须实现的方法,用于渲染DOM结构,可以访问组件stateprop属性

注意: 不要在 render 里面 setState, 否则会触发死循环导致内存崩溃

componentDidMount

组件挂载到真实DOM节点后执行,其在render方法之后执行

此方法多用于执行一些数据获取,事件监听等操作

更新阶段

该阶段的函数主要为如下方法:

  • getDerivedStateFromProps
  • shouldComponentUpdate
  • render
  • getSnapshotBeforeUpdate
  • componentDidUpdate

getDerivedStateFromProps

该方法介绍同上

shouldComponentUpdate

用于告知组件本身基于当前的propsstate是否需要重新渲染组件,默认情况返回true

执行时机:到新的props或者state时都会调用,通过返回true或者false告知组件更新与否

一般情况,不建议在该周期方法中进行深层比较,会影响效率

同时也不能调用setState,否则会导致无限循环调用更新

render

介绍如上

getSnapshotBeforeUpdate

该周期函数在render后执行,执行之时DOM元素还没有被更新

该方法返回的一个Snapshot值,作为componentDidUpdate第三个参数传入

getSnapshotBeforeUpdate(prevProps, prevState) {
    console.log('#enter getSnapshotBeforeUpdate');
    return 'foo';
}

componentDidUpdate(prevProps, prevState, snapshot) {
    console.log('#enter componentDidUpdate snapshot = ', snapshot);
}

此方法的目的在于获取组件更新前的一些信息,比如组件的滚动位置之类的,在组件更新后可以根据这些信息恢复一些UI视觉上的状态

componentDidUpdate

执行时机:组件更新结束后触发

在该方法中,可以根据前后的propsstate的变化做相应的操作,如获取数据,修改DOM样式等

卸载阶段

componentWillUnmount

此方法用于组件卸载前,清理一些注册是监听事件,或者取消订阅的网络请求等

一旦一个组件实例被卸载,其不会被再次挂载,而只可能是被重新创建

参考文献

链接:

4、说说AMD、CMD、commonJS模块化规范的区别?

CommonJS

  • 常用于:服务器端nodewebpack
  • 特点:同步/运行时加载磁盘读取速度快

AMD

  • 常用于:不常用,CommonJs的浏览器端实现
  • 特点:
    • 异步加载:因为面向浏览器端,为了不影响渲染肯定是异步加载
    • 依赖前置:所有的依赖必须写在最初的依赖数组中,速度快,但是会浪费资源,预先加载了所有依赖不管你是否用到

CMD

  • 常用于:不常用,根据CommonJs和AMD实现,优化了加载方式
  • 特点:
    • 异步加载
    • 按需加载/依赖就近:用到了再引用依赖,方便了开发,缺点是速度和性能较差

5、说说你对webSocket的理解?

WebSocket,是一种网络传输协议,位于OSI模型的应用层。可在单个TCP连接上进行全双工通信,能更好的节省服务器资源和带宽并达到实时通迅

客户端和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输

优点

  • 较少的控制开销:数据包头部协议较小,不同于http每次请求需要携带完整的头部
  • 更强的实时性:相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少
  • 保持创连接状态:创建通信后,可省略状态信息,不同于HTTP每次请求需要携带身份验证
  • 更好的二进制支持:定义了二进制帧,更好处理二进制内容
  • 支持扩展:用户可以扩展websocket协议、实现部分自定义的子协议
  • 更好的压缩效果:Websocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率

应用场景

基于websocket的事实通信的特点,其存在的应用场景大概有:

  • 弹幕
  • 媒体聊天
  • 协同编辑
  • 基于位置的应用
  • 体育实况更新
  • 股票基金报价实时更新

参考文献

6、React性能优化的手段有哪些?

1、使用纯组件;
2、使用 React.memo 进行组件记忆(React.memo 是一个高阶组件),对 于相同的输入,不重复执行;
3、如果是类组件,使用 shouldComponentUpdate(这是在重新渲染组件之前触发的其中一个生命周期事件)生命周期事件,可以利用此事件来决定何时需要重新渲染组件;
4、路由懒加载;
5、使用 React Fragments 避免额外标记;

6、组件优化,在子组件中判断子组件所依赖的数据是否发生变化,在进行渲染操作

7、常用的15个数组方法

1 push()  unshift() 尾部头部添加

2 pop()  shift()  尾部头部删除

3 reverse() 颠倒数组中的元素

4 splice()  添加或删除数组中的元素

5 foreach()   遍历数组

6 sort()  对于数组的元素进行排序

7 map() 将数组映射成为另一个数组

8 filter() 从数组中找出所有符合指定条件的元素

9 reduce() 将数组合成一个值

10 concat() 连接两个或多个字符串

11 slice() 数组复制

12. find()  返回通过测试(函数内判断)的数组的第一个元素的值

13. join() && toString() 把数组中的所有元素转换成一个符串

14. toLocaleString 把数组转换为本地字符串

15  indexOf() 查找指定的字符串值在字符串中首次出现(索引)位置。

8、受控组件和非受控组件的区别以及应用场景

受控组件简单来说就是受我们控制的组件

非受控组件就是不受我们控制的组件

两者区别 1、受控组件 受控组件依赖于状态 受控组件的修改会实时映射到状态值上,此时可以对输入的内容进行校验 受控组件只有继承React.Component才会有状态 受控组件必须要在表单上使用onChange事件来绑定对应的事件 2、非受控组件 非受控组件不受状态的控制 非受控组件获取数据就是相当于操作DOM

受控组件的应用场景主要就是react表单的应用,可以更好的进行数据的保存以及修改

9、组件优化

Component的2个问题

  1. 只要执行setState(),即使不改变状态数据, 组件也会重新render() ==> 效率低

  2. 只当前组件重新render(), 就会自动重新render子组件,纵使子组件没有用到父组件的任何数据 ==> 效率低

效率高的做法

只有当组件的state或props数据发生改变时才重新render()

原因

Component中的shouldComponentUpdate()总是返回true

解决

办法1: 
	重写shouldComponentUpdate()方法
	比较新旧state或props数据, 如果有变化才返回true, 如果没有返回false
办法2:  
	使用PureComponent
	PureComponent重写了shouldComponentUpdate(), 只有state或props数据有变化才返回true
	注意: 
		只是进行state和props数据的浅比较, 如果只是数据对象内部数据变了, 返回false  
		不要直接修改state数据, 而是要产生新数据
项目中一般使用PureComponent来优化

10、为什么react元素有一个$$type属性?

目的是为了防止 XSS 攻击。因为 Synbol 无法被序列化,所以 React 可以通过有没有 $$typeof 属性来断出当前的 element 对象是从数据库来的还是自己生成的。

如果没有 $$typeof 这个属性,react 会拒绝处理该元素。

11、说说Real DOM和Virtual DOM的区别?优缺点?

img

一、是什么

Real DOM,真实 DOM,意思为文档对象模型,是一个结构化文本的抽象,在页面渲染出的每一个结点都是一个真实 DOM 结构,如下:

img

Virtual Dom,本质上是以 JavaScript 对象形式存在的对 DOM 的描述

创建虚拟 DOM 目的就是为了更好将虚拟的节点渲染到页面视图中,虚拟 DOM 对象的节点与真实 DOM 的属性一一照应

React 中,JSX 是其一大特性,可以让你在 JS 中通过使用 XML 的方式去直接声明界面的 DOM 结构

// 创建 h1 标签,右边千万不能加引号
const vDom = <h1>Hello World</h1>; 
// 找到 <div id="root"></div> 节点
const root = document.getElementById("root"); 
// 把创建的 h1 标签渲染到 root 节点上
ReactDOM.render(vDom, root); 

上述中,ReactDOM.render() 用于将你创建好的虚拟 DOM 节点插入到某个真实节点上,并渲染到页面上

JSX 实际是一种语法糖,在使用过程中会被 babel 进行编译转化成 JS 代码,上述 VDOM 转化为如下:

const vDom = React.createElement(
  'h1',
  { className: 'hClass', id: 'hId' },
  'hello world'
)

可以看到,JSX 就是为了简化直接调用 React.createElement() 方法:

  • 第一个参数是标签名,例如 h1、span、table…
  • 第二个参数是个对象,里面存着标签的一些属性,例如 id、class 等
  • 第三个参数是节点中的文本

通过 console.log(VDOM),则能够得到虚拟 VDOM 消息

img

所以可以得到,JSX 通过 babel 的方式转化成 React.createElement 执行,返回值是一个对象,也就是虚拟 DOM

二、区别

两者的区别如下:

  • 虚拟 DOM 不会进行排版与重绘操作,而真实 DOM 会频繁重排与重绘
  • 虚拟 DOM 的总损耗是“虚拟 DOM 增删改+真实 DOM 差异增删改+排版与重绘”,真实 DOM 的总损耗是“真实 DOM 完全增删改+排版与重绘”

以前文章 (opens new window)举过的例子:

传统的原生 apijQuery 去操作 DOM 时,浏览器会从构建 DOM 树开始从头到尾执行一遍流程

当你在一次操作时,需要更新 10 个 DOM 节点,浏览器没这么智能,收到第一个更新 DOM 请求后,并不知道后续还有 9 次更新操作,因此会马上执行流程,最终执行 10 次流程

而通过 VNode,同样更新 10 个 DOM 节点,虚拟 DOM 不会立即操作 DOM,而是将这 10 次更新的 diff 内容保存到本地的一个 js 对象中,最终将这个 js 对象一次性 attachDOM 树上,避免大量的无谓计算

三、优缺点

真实 DOM 的优势:

  • 易用

缺点:

  • 效率低,解析速度慢,内存占用量过高
  • 性能差:频繁操作真实 DOM,易于导致重绘与回流

使用虚拟 DOM 的优势如下:

  • 简单方便:如果使用手动操作真实 DOM 来完成页面,繁琐又容易出错,在大规模应用下维护起来也很困难
  • 性能方面:使用 Virtual DOM,能够有效避免真实 DOM 数频繁更新,减少多次引起重绘与回流,提高性能
  • 跨平台:React 借助虚拟 DOM,带来了跨平台的能力,一套代码多端运行

缺点:

  • 在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化
  • 首次渲染大量 DOM 时,由于多了一层虚拟 DOM 的计算,速度比正常稍慢

参考文献

12、关于setState同步和异步的问题

参考链接: https://blog.csdn.net/qq_46658751/article/details/123872815

https://www.jianshu.com/p/4a43f4557876

什么时候同步什么时候异步

在 React 中,如果是由 React 引发的事件处理(比如通过 onClick 引发的事件处理),调用 setState 不会同步更新 this.state,除此之外的 setState 调用会同步更新 this.state

所谓的除此之外,指的是绕过 React,通过 addEventListener 直接添加的事件处理函数,还有通过 setTimeout/setInterval 产生的异步任务

原因

在 React 的 setState 函数实现中,会根据一个变量 isBatchingUpdate 判断是直接更新 this.state 还是放到队列中回头再说,而且 isBatchingUpdate 默认是 false,也就是表示 setState 会同步更新 this.state,但是,有一个函数 batchedUpdate,这个函数会把 isBatchingUpdate 修改为 true,而当 React 在调用事件处理函数之前就会调用这个 batchedUpdates,造成的后果,就是由 React 控制的事件处理过程 setState 不会同步更新 this.state

注意

setState异步 并不是说内部由异步代码实现,其实本身执行的过程和代码是同步的,只是合成事件和钩子函数的调用在更新之前,导致在合成事件和钩子函数中没法里吗拿到更新后的值,形成了所谓的"异步"

但是可以通过 setState(partialState, callback) 中的 callback 拿到更新后的结果

(1). setState(stateChange, [callback])------对象式的setState
        1.stateChange为状态改变对象(该对象可以体现出状态的更改)
        2.callback是可选的回调函数, 它在状态更新完毕、界面也更新后(render调用后)才被调用
				
(2). setState(updater, [callback])------函数式的setState
        1.updater为返回stateChange对象的函数。
        2.updater可以接收到state和props。
        4.callback是可选的回调函数, 它在状态更新、界面也更新后(render调用后)才被调用。

总结:

​ 1.对象式的setState是函数式的setState的简写方式(语法糖)
​ 2.使用原则:
​ (1).如果新状态不依赖于原状态 ===> 使用对象方式
​ (2).如果新状态依赖于原状态 ===> 使用函数方式
​ (3).如果需要在setState()执行后获取最新的状态数据,
​ 要在第二个callback函数中读取

13、 说说react的事件机制?

使用React中并没有注意到的React事件与浏览器原生事件之间的区别,现在发现这篇文章浅显易懂,所有copy过来,首先得感谢作者。

react的事件是合成事件((Synethic event),不是原生事件

<button onClick={this.handleClick}></button> 
<input value={this.state.name} onChange={this.handleChange} />

合成事件与原生事件的区别

\1. 写法不同,合适事件是驼峰写法,而原生事件是全部小写
\2. 执行时机不同,合适事件全部委托到document上,而原生事件绑定到DOM元素本身
\3. 合成事件中可以是任何类型,比如this.handleClick这个函数,而原生事件中只能是字符串

react事件执行大致流程如下图

img

我们来看个例子深入了解下,如果我们需要实现一个组件,这个组件点击按钮会显示一个二维码,点击二维码之外的区域可以隐藏二维码,但是点击二维码本身却不会关闭,代码如下:

//代码来源于《深入React技术栈》2.1.4节
class QrCode extends Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
    this.handleClickQr = this.handleClickQr.bind(this);
    this.state = {
      active: false,
    };
  }
  
  componentDidMount() {
    document.body.addEventListener('click', e => {
      this.setState({
        active: false,
      });
    });
  }

  componentWillUnmount() {
    document.body.removeEventListener('click');
  }
  
  handleClick() {
    this.setState({
      active: !this.state.active,
    });
  }
  
  handleClickQr(e) {
    e.stopPropagation();
  }

  render() {
    return (
      <div className="qr-wrapper">
        <button className="qr" onClick={this.handleClick}>二维码</button>
        <div
          className="code"
          style={{ display: this.state.active ? 'block' : 'none' }}
          onClick={this.handleClickQr}
        >
          <img src="qr.jpg" alt="qr" />
        </div>
      </div>
    );
  }
}

上面代码从感官上感觉确实可以实现要求的组件,但事实上我们运行上述代码可以发现,点击二维码本身也会导致二维码的隐藏,现在就有意思了,我们来仔细分析一下。
  其实React事件并没有原生的绑定在真实的DOM上,而是使用了行为委托方式实现事件机制。

img

如上图所示,在JavaScript中,事件的触发实质上是要经过三个阶段:事件捕获、目标对象本身的事件处理和事件冒泡,假设在div中触发了click事件,实际上首先经历捕获阶段会由父级元素将事件一直传递到事件发生的元素,执行完目标事件本身的处理事件后,然后经历冒泡阶段,将事件从子元素向父元素冒泡。正因为事件在DOM的传递经历这样一个过程,从而为行为委托提供了可能。通俗地讲,行为委托的实质就是将子元素事件的处理委托给父级元素处理。React会将所有的事件都绑定在最外层(document),使用统一的事件监听,并在冒泡阶段处理事件,当挂载或者卸载组件时,只需要在通过的在统一的事件监听位置增加或者删除对象,因此可以提高效率。
  并且React并没有使用原生的浏览器事件,而是在基于Virtual DOM的基础上实现了合成事件(SyntheticEvent),事件处理程序接收到的是SyntheticEvent的实例。SyntheticEvent完全符合W3C的标准,因此在事件层次上具有浏览器兼容性,与原生的浏览器事件一样拥有同样的接口,可以通过stopPropagation()preventDefault()相应的中断。如果需要访问当原生的事件对象,可以通过引用nativeEvent获得。

img

上图为大致的React事件机制的流程图,React中的事件机制分为两个阶段:事件注册和事件触发:

  1. 事件注册  
    React在组件加载(mount)和更新(update)时,其中的ReactDOMComponent会对传入的事件属性进行处理,对相关事件进行注册和存储。document中注册的事件不处理具体的事件,仅对事件进行分发。ReactBrowserEventEmitter作为事件注册入口,担负着事件注册和事件触发。注册事件的回调函数由EventPluginHub来统一管理,根据事件的类型(type)和组件标识(_rootNodeID)为key唯一标识事件并进行存储。
  2. 事件执行
    事件执行时,document上绑定事件ReactEventListener.dispatchEvent会对事件进行分发,根据之前存储的类型(type)和组件标识(_rootNodeID)找到触发事件的组件。ReactEventEmitter利用EventPluginHub中注入(inject)的plugins(例如:SimpleEventPluginEnterLeaveEventPlugin)会将原生的DOM事件转化成合成的事件,然后批量执行存储的回调函,回调函数的执行分为两步,第一步是将所有的合成事件放到事件队列里面,第二步是逐个执行。需要注意的是,浏览器原生会为每个事件的每个listener创建一个事件对象,可以从这个事件对象获取到事件的引用。这会造成高额的内存分配,React在启动时就会为每种对象分配内存池,用到某一个事件对象时就可以从这个内存池进行复用,节省内存。

再回到我们刚开始的问题,现在看起来就很没有很费解了,之所以会出现上面的问题是因为我们混用了React的事件机制和DOM原生的事件机制,认为通过:

handleClickQr(e) {
    e.stopPropagation();
}

就能阻止原生的事件传播,其实在事件委托的情形下是不能实现这一点的。当然解决的办法也不复杂,不要将React事件和DOM原生事件混用。

componentDidMount() {
  document.body.addEventListener('click', e => {
    this.setState({
      active: false,
    });
  });
 
  document.querySelector('.code').addEventListener('click', e => {
    e.stopPropagation();
  })
}

componentWillUnmount() {
  document.body.removeEventListener('click');
  document.querySelector('.qr').removeEventListener('click');
}

或者通过事件原件对象中的target进行判断:

componentDidMount() {
  document.body.addEventListener('click', e => {
    if (e.target && e.target.matches('div.code')) {
      return;
    }
 
    this.setState({
      active: false,
    });
  });
}

都可以解决异常关闭的问题。

阻止事件冒泡

事实上阻止合成事件的方法有很多,但是有个原则:阻止合成事件的冒泡不会阻止原生事件的冒泡,但是阻止原生事件的冒泡会阻止合成事件的冒泡。

-----------------------以上是不是没明白,我们继续看下面解释---------------------------

React不会将事件处理函数直接绑定到真实的节点上,而是把所有的事件绑定到结构的最外层,使用一个统一的事件监听器。
这个监听器维持了一个映射,保存所有组件内部的事件监听和处理函数。当事件发生时,首先被这个统一的事件监听器处理,
然后在映射里找到真正的事件处理函数并调用。

合成事件分为以下三个主要过程:

一 事件注册

所有事件都会注册到document上,拥有统一的回调函数dispatchEvent来执行事件分发

二 事件合成

从原生的nativeEvent对象生成合成事件对象,同一种事件类型只能生成一个合成事件Event,如onclick这个类型的事件,dom上所有带有通过jsx绑定的onClick的回调函数都会按顺序(冒泡或者捕获)会放到Event._dispatchListeners 这个数组里,后面依次执行它

三 事件派发

每次触发事件都会执行根节点上 addEventListener 注册的回调,也就是 ReactEventListener.dispatchEvent 方法,事件分发入口函数。该函数的主要业务逻辑如下:
\1. 找到事件触发的 DOM 和 React Component
\2. 从该 React Component,调用 findParent 方法,遍历得到所有父组件,存在数组中。
\3. 从该组件直到最后一个父组件,根据之前事件存储,用 React 事件名 + 组件 key,找到对应绑定回调方法,执行,详细过程为:
\4. 根据 DOM 事件构造 React 合成事件。
\5. 将合成事件放入队列。
\6. 批处理队列中的事件(包含之前未处理完的,先入先处理)

React合成事件的冒泡并不是真的冒泡,而是节点的遍历。

----------------------------来对比下与原生事件区别----------------------------------

React事件处理的特性

React的事件系统和浏览器事件系统相比,主要增加了两个特性:事件代理和事件自动绑定

1、事件代理

\1. 区别于浏览器事件处理方式,React并未将事件处理函数与对应的DOM节点直接关联,而是在顶层使用了一个全局事件监听器监听所有的事件;
\2. React会在内部维护一个映射表记录事件与组件事件处理函数的对应关系;
\3. 当某个事件触发时,React根据这个内部映射表将事件分派给指定的事件处理函数;
\4. 当映射表中没有事件处理函数时,React不做任何操作;
\5. 当一个组件安装或者卸载时,相应的事件处理函数会自动被添加到事件监听器的内部映射表中或从表中删除。

2、事件自动绑定

\1. 在JavaScript中创建回调函数时,一般要将方法绑定到特定的实例,以保证this的正确性;
\2. 在React中,每个事件处理回调函数都会自动绑定到组件实例(使用ES6语法创建的例外);

注意:事件的回调函数被绑定在React组件上,而不是原始的元素上,即事件回调函数中的
this所指的是组件实例而不是DOM元素;

3、合成事件

\1. 与浏览器事件处理稍微有不同的是,React中的事件处理程序所接收的事件参数是被称为“合成事件(SyntheticEvent)”的实例。
合成事件是对浏览器原生事件跨浏览器的封装,并与浏览器原生事件有着同样的接口,如stopPropagation(),preventDefault()等,并且
这些接口是跨浏览器兼容的。
\2. 如果需要使用浏览器原生事件,可以通过合成事件的nativeEvent属性获取

-------------------说了这么多,来总结下react合成事件优点------------------------

React在事件处理的优点

\1. 几乎所有的事件代理(delegate)到 document ,达到性能优化的目的
\2. 对于每种类型的事件,拥有统一的分发函数 dispatchEvent
\3. 事件对象(event)是合成对象(SyntheticEvent),不是原生的,其具有跨浏览器兼容的特性
\4. react内部事件系统实现可以分为两个阶段: 事件注册、事件分发,几乎所有的事件均委托到document上,而document上事件的回调函数只有一个:ReactEventListener.dispatchEvent,然后进行相关的分发
\5. 对于冒泡事件,是在 document 对象的冒泡阶段触发。对于非冒泡事件,例如focus,blur,是在 document 对象的捕获阶段触发,最后在 dispatchEvent 中决定真正回调函数的执行

原文:人类身份验证 - SegmentFault

原文:[react事件机制 - 看风景就 - 博客园](

14、说说你对fiber架构的理解?解决了什么问题?

长文预警:原《React Fiber性能优化(内部试讲)》的完整版

性能优化是一个系统性的工程,如果只看到局部,引入算法,当然是越快越好; 但从整体来看,在关键点引入缓存,可以秒杀N多算法,或另辟蹊径,探索事件的本质,可能用户要的并不是快……

React16启用了全新的架构,叫做Fiber,其最大的使命是解决大型React项目的性能问题,再顺手解决之前的一些痛点。

痛点
主要有如下几个:

组件不能返回数组,最见的场合是UL元素下只能使用LI,TR元素下只能使用TD或TH,这时这里有一个组件循环生成LI或TD列表时,我们并不想再放一个DIV,这会破坏HTML的语义。
弹窗问题,之前一直使用不稳定的unstable_renderSubtreeIntoContainer。弹窗是依赖原来DOM树的上下文,因此这个API第一个参数是组件实例,通过它得到对应虚拟DOM,然后一级级往上找,得到上下文。它的其他参数也很好用,但这个方法一直没有转正。。。
异常处理,我们想知道哪个组件出错,虽然有了React DevTool,但是太深的组件树查找起来还是很吃力。希望有个方法告诉我出错位置,并且出错时能让我有机会进行一些修复工作
HOC的流行带来两个问题,毕竟是社区兴起的方案,没有考虑到ref与context的向下传递。
组件的性能优化全凭人肉,并且主要集中在SCU,希望框架能干些事情,即使不用SCU,性能也能上去。

解决进度
16.0 让组件支持返回任何数组类型,从而解决数组问题; 推出createPortal API ,解决弹窗问题; 推出componentDidCatch新钩子, 划分出错误组件与边界组件, 每个边界组件能修复下方组件错误一次, 再次出错,转交更上层的边界组件来处理,解决异常处理问题。
16.2 推出Fragment组件,可以看作是数组的一种语法糖。
16.3 推出createRef与forwardRef解决Ref在HOC中的传递问题,推出new Context API,解决HOC的context传递问题(主要是SCU作崇)
而性能问题,从16.0开始一直由一些内部机制来保证,涉及到批量更新及基于时间分片的限量更新。

一个小实验

我们可以通过以下实验来窥探React16的优化思想。

function randomHexColor(){
return “#” + (“0000”+ (Math.random() * 0x1000000 << 0).toString(16)).substr(-6);
}
setTimeout(function() {
var k = 0;
var root = document.getElementById(“root”);
for(var i = 0; i < 10000; i++){
k += new Date - 0 ;
var el = document.createElement(“div”);
el.innerHTML = k;
root.appendChild(el);
el.style.cssText = background:${randomHexColor()};height:40px;
}
}, 1000);

这是一个拥有10000个节点的插入操作,包含了innerHTML与样式设置,花掉1000ms。

我们再改进一下,分派次插入节点,每次只操作100个节点,共100次,发现性能异常的好!

function randomHexColor() {
return “#” + (“0000” + (Math.random() * 0x1000000 << 0).toString(16)).substr(-6);
}
var root = document.getElementById(“root”);
setTimeout(function () {
function loop(n) {
var k = 0;
console.log(n);
for (var i = 0; i < 100; i++) {
k += new Date - 0;
var el = document.createElement(“div”);
el.innerHTML = k;
root.appendChild(el);
el.style.cssText = background:${randomHexColor()};height:40px;
}
if (n) {
setTimeout(function () {
loop(n - 1);
}, 40);
}
}
loop(100);
}, 1000);

究其原因是因为浏览器是单线程,它将GUI描绘,时间器处理,事件处理,JS执行,远程资源加载统统放在一起。当做某件事,只有将它做完才能做下一件事。如果有足够的时间,浏览器是会对我们的代码进行编译优化(JIT)及进行热代码优化,一些DOM操作,内部也会对reflow进行处理。reflow是一个性能黑洞,很可能让页面的大多数元素进行重新布局。

浏览器的运作流程

渲染 -> tasks -> 渲染 -> tasks -> 渲染 -> tasks -> …

这些tasks中有些我们可控,有些不可控,比如setTimeout什么时候执行不好说,它总是不准时; 资源加载时间不可控。但一些JS我们可以控制,让它们分派执行,tasks的时长不宜过长,这样浏览器就有时间优化JS代码与修正reflow!下图是我们理想中的渲染过程

总结一句,就是让浏览器休息好,浏览器就能跑得更快。

如何让代码断开重连

JSX是一个快乐出奇蛋,一下子满足你两个愿望:组件化与标签化。并且JSX成为组件化的标准化语言。

但标签化是天然套嵌的结构,意味着它会最终编译成递归执行的代码。因此React团队称React16之前的调度器为栈调度器,栈没有什么不好,栈显浅易懂,代码量少,但它的坏处不能随意break掉,continue掉。根据我们上面的实验,break后我们还要重新执行,我们需要一种链表的结构。

链表是对异步友好的。链表在循环时不用每次都进入递归函数,重新生成什么执行上下文,变量对象,激活对象,性能当然比递归好。

因此Reat16设法将组件的递归更新,改成链表的依次执行。如果页面有多个虚拟DOM树,那么就将它们的根保存到一个数组中。

ReactDOM.render(, node1)
ReactDOM.render(, node2)
//node1与node2不存在包含关系,那么这页面就有两棵虚拟DOM树

如果仔细阅读源码,React这个纯视图库其实也是三层架构。在React15有虚拟DOM层,它只负责描述结构与逻辑;内部组件层,它们负责组件的更新, ReactDOM.render、 setState、 forceUpdate都是与它们打交道,能让你多次setState,只执行一次真实的渲染, 在适合的时机执行你的组件实例的生命周期钩子; 底层渲染层, 不同的显示介质有不同的渲染方法,比如说浏览器端,它使用元素节点,文本节点,在Native端,会调用oc, java的GUI, 在canvas中,有专门的API方法。。。

虚拟DOM是由JSX转译过来的,JSX的入口函数是React.createElement, 可操作空间不大, 第三大的底层API也非常稳定,因此我们只能改变第二层。

React16将内部组件层改成Fiber这种数据结构,因此它的架构名也改叫Fiber架构。Fiber节点拥有return, child, sibling三个属性,分别对应父节点, 第一个孩子, 它右边的兄弟, 有了它们就足够将一棵树变成一个链表, 实现深度优化遍历。

如何决定每次更新的数量

在React15中,每次更新时,都是从根组件或setState后的组件开始,更新整个子树,我们唯一能做的是,在某个节点中使用SCU断开某一部分的更新,或者是优化SCU的比较效率。

React16则是需要将虚拟DOM转换为Fiber节点,首先它规定一个时间段内,然后在这个时间段能转换多少个FiberNode,就更新多少个。

因此我们需要将我们的更新逻辑分成两个阶段,第一个阶段是将虚拟DOM转换成Fiber, Fiber转换成组件实例或真实DOM(不插入DOM树,插入DOM树会reflow)。Fiber转换成后两者明显会耗时,需要计算还剩下多少时间。并且转换实例需要调用一些钩子,如componentWillMount, 如果是重复利用已有的实例,这时就是调用componentWillReceiveProps, shouldComponentUpdate, componentWillUpdate,这时也会耗时。

为了让读者能直观了解React Fiber的运作过程,我们简单实现一下ReactDOM.render, 但不保证会跑起来。

首先是一些简单的方法:

var queue = []
ReacDOM.render = function (root, container) {
queue.push(root)
updateFiberAndView()
}
function getVdomFormQueue() {
return queue.shift()
}
function Fiber(vnode){
for(var i in vnode){
this[i] = vnode[i]
}
this.uuid = Math.random()
}
//我们简单的Fiber目前来看,只比vdom多了一个uuid属性
function toFiber(vnode){
if(!vnode.uuid){
return new Fiber(vnode)
}
return vnode
}

updateFiberAndView要实现React的时间分片,我们先用setTimeout模拟。我们暂时不用理会updateView怎么实现,可能它就是updateComponentOrElement中将它们放到又一个列队,需再出来执行insertBefore, componentDidMount操作呢!

function updateFiberAndView() {
var now = new Date - 0;
var deadline = new Date + 100;
updateView() //更新视图,这会耗时,因此需要check时间
if (new Date < deadline) {
var vdom = getVdomFormQueue()
var fiber = vdom, firstFiber
var hasVisited = {}
do {//深度优先遍历
var fiber = toFiber(fiber);//A处
if(!firstFiber){
fibstFiber = fiber
}
if (!hasVisited[fiber.uuid]) {
hasVisited[fiber.uuid] = 1
//根据fiber.type实例化组件或者创建真实DOM
//这会耗时,因此需要check时间
updateComponentOrElement(fiber);
if (fiber.child) {
//向下转换
if (newDate - 0 > deadline) {
queue.push(fiber.child)//时间不够,放入栈
break
}
fiber = fiber.child;
continue //让逻辑跑回A处,不断转换child, child.child, child.child.child
}
}
//如果组件没有children,那么就向右找
if (fiber.sibling) {
fiber = fiber.sibling;
continue //让逻辑跑回A处
}
// 向上找
fiber = fiber.return
if(fiber === fibstFiber || !fiber){
break
}
} while (1)
}
if (queue.length) {
setTimeout(updateFiberAndView, 40)
}
}

里面有一个do while循环,每一次都是小心翼翼进行计时,时间不够就将来不及处理的节点放进列队。

updateComponentOrElement无非是这样:

function updateComponentOrElement(fiber){
var {type, stateNode, props} = fiber
if(!stateNode){
if(typeof type === “string”){
fiber.stateNode = document.createElement(type)
}else{
var context = {}//暂时免去这个获取细节
fiber.stateNode = new type(props, context)
}
}
if(stateNode.render){
//执行componentWillMount等钩子
children = stateNode.render()
}else{
children = fiber.childen
}
var prev = null;
 //这里只是mount的实现,update时还需要一个oldChildren, 进行key匹配,重复利用已有节点
for(var i = 0, n = children.length; i < n; i++){
var child = children[i];
child.return = fiber;
if(!prev){
fiber.child = child
}else{
prev.sibling = child
}
prev = child;
}
}

因此这样Fiber的return, child, sibling就有了,可以happy地进行深度优先遍历了。

如何调度时间才能保证流畅

刚才的updateFiberAndView其实有一个问题,我们安排了100ms来更新视图与虚拟DOM,然后再安排40ms来给浏览器来做其他事。如果我们的虚拟DOM树很小,其实不需要100ms; 如果我们的代码之后, 浏览器有更多其他事要干, 40ms可能不够。IE10出现了setImmediate,requestAnimationFrame这些新定时器,让我们这些前端,其实浏览器有能力让页面更流畅地运行起来。

浏览器本身也不断进化中,随着页面由简单的展示转向WebAPP,它需要一些新能力来承载更多节点的展示与更新。

下面是一些自救措施:

requestAnimationFrame
requestIdleCallback
web worker
IntersectionObserver

我们依次称为浏览器层面的帧数控制调用,闲时调用,多线程调用, 进入可视区调用。

requestAnimationFrame在做动画时经常用到,jQuery新版本都使用它。web worker在angular2开始就释出一些包,实验性地用它进行diff数据。IntersectionObserver可以用到ListView中。而requestIdleCallback是一个生脸孔,而React官方恰恰看上它。

刚才说updateFiberAndView有出两个时间段,一个给自己的,一个给浏览器的。requestAnimationFrame能帮我们解决第二个时间段,从而确保整体都是60帧或75帧(这个帧数可以在操作系统的显示器刷新频率中设置)流畅运行。

我们看requestIdleCallback是怎么解决这问题的

它的第一个参数是一个回调,回调有一个参数对象,对象有一个timeRemaining方法,就相当于new Date - deadline,并且它是一个高精度数据, 比毫秒更准确, 至少浏览器到底安排了多少时间给更新DOM与虚拟DOM,我们不用管。第二个时间段也不用管,不过浏览器可能1,2秒才执行这个回调,因此为了保险起见,我们可以设置第二个参数,让它在回调结束后300ms才执行。要相信浏览器,因为都是大牛们写的,时间的调度比你安排更有效率。

于是我们的updateFiberAndView可以改成这样:

function updateFiberAndView(dl) {
updateView() //更新视图,这会耗时,因此需要check时间
if (dl.timeRemaining() > 1) {
var vdom = getVdomFormQueue()
var fiber = vdom, firstFiber
var hasVisited = {}
do {//深度优先遍历
var fiber = toFiber(fiber);//A处
if(!firstFiber){
fibstFiber = fiber
}
if (!hasVisited[fiber.uuid]) {
hasVisited[fiber.uuid] = 1
//根据fiber.type实例化组件或者创建真实DOM
//这会耗时,因此需要check时间
updateComponentOrElement(fiber);
if (fiber.child) {
//向下转换
if (dl.timeRemaining() > 1) {
queue.push(fiber.child)//时间不够,放入栈
break
}
fiber = fiber.child;
continue //让逻辑跑回A处,不断转换child, child.child, child.child.child
}
}
//…略
} while (1)
}
if (queue.length) {
requetIdleCallback(updateFiberAndView, {
timeout:new Date + 100
}
)
}
}

到这里,ReactFiber基于时间分片的限量更新讲完了。实际上React为了照顾绝大多数的浏览器,自己实现了requestIdleCallback。

批量更新

但React团队觉得还不够,需要更强大的东西。因为有的业务对视图的实时同步需求并不强烈,希望将所有逻辑都跑完才更新视图,于是有了batchedUpdates,目前它还不是一个稳定的API,因此大家使用它时要这样用ReactDOM.unstable_batchedUpdates。

这个东西怎么实现呢?就是搞一个全局的开关,如果打开了,就让updateView不起作用。

var isBatching = false
function batchedUpdates(callback, event) {
let keepbook = isBatching;
isBatching = true;
try {
return callback(event);
} finally {
isBatching = keepbook;
if (!isBatching) {
requetIdleCallback(updateFiberAndView, {
timeout:new Date + 1
}
}
}
};

function updateView(){
if(isBatching){
return
}
//更新视图
}

事实上,当然没有这么简单,考虑到大家看不懂React的源码,大家可以看一下anujs是怎么实现的:

https://github.com/RubyLouvre/anu/blob/master/packages/fiber/scheduleWork.js#L94-L113

React内部也大量使用batchedUpdates来优化用户代码,比如说在事件回调中setState,在commit阶段的钩子(componentDidXXX)中setState 。

可以说,setState是对单个组件的合并渲染,batchedUpdates是对多个组件的合并渲染。合并渲染是React最主要的优化手段。

为什么使用深度优化遍历

React通过Fiber将树的遍历变成了链表的遍历,但遍历手段有这么多种,为什么偏偏使用DFS?!

这涉及一个很经典的消息通信问题。如果是父子通信,我们可以通过props进行通信,子组件可以保存父的引用,可以随时call父组件。如果是多级组件间的通信,或不存在包含关系的组件通信就麻烦了,于是React发明了上下文对象(context)。

context一开始是一个空对象,为了方便起见,我们称之为unmaskedContext。

当它遇到一个有getChildContext方法的组件时,那个方法会产生一个新context,与上面的合并,然后将新context作为unmaskedContext往下传。

当它遇到一个有contextTypes的组件,context就抽取一部分内容给这个组件进行实例化。这个只有部分内容的context,我们称之为maskedContext。

组件总是从unmaskedContext中割一块肉下来作为自己的context。可怜!

如果子组件没有contextTypes,那么它就没有任何属性。

在React15中,为了传递unmaskedContext,于是大部分方法与钩子都留了一个参数给它。但这么大架子的context竟然在文档中没有什么地位。那时React团队还没有想好如何处理组件通信,因此社区一直用舶来品Redux来救命。这情况一直到Redux的作者入主React团队。

还有一个隐患,它可能被SCU比较时是用maskedContext,而不是unmaskedContext。

基于这些问题,终于new Context API出来了。首先, unmaskedContext 不再像以前那样各个方法中来往穿梭了,有一个独立的contextStack。开始时就push进一个空对象,到达某个组件需要实例化时,就取它第一个。当再次访问这个组件时, 就像它从栈中弹出。因此我们需要深度优先遍历,保证每点节点都访问两次。

相同的情况还有container,container是我们某个元素虚拟DOM需要用到的真实父节点。在React15中,它会装在一个containerInfo对象也层层传送。

我们知道,虚拟DOM分成两大类,一种是组件虚拟DOM,type为函数或类,它本身不产生节点,而是生成组件实例,而通过render方法,产生下一级的虚拟DOM。一种是元素虚拟DOM,type为标签名,会产生DOM节点。上面的元素虚拟DOM的stateNode(DOM节点),就是下方的元素虚拟DOM的contaner。

这种独立的栈机制有效地解决了内部方法的参数冗余问题。

但有一个问题,当第一次渲染完毕后,contextStack置为空了。然后我们位于虚拟DOM树的某个组件setState,这时它的context应该如何获取呢?React的解决方式是,每次都是从根开始渲染,通过updateQueue加速跳过没有更新的 节点——每个组件在setState或forceUpdate时,都会创建一个updateQueue属性在它的上面。anujs则是保存它之前的unmaskedContext到实例上,unmaskedContext可以看作是上面所有context的并集,并且一个可以当多个使用。

当我们批量更新时,可能有多少不连续的子组件被更新了,其中两个组件之间的某个组件使用了SCU return false,这个SCU应该要被忽视。 因此我们引用一些变量让它透明化。就像forceUpdate能让组件无视SCU一样。

为什么要对生命周期钩子大换血

React将虚拟DOM的更新过程划分两个阶段,reconciler阶段与commit阶段。reconciler阶段对应早期版本的diff过程,commit阶段对应早期版本的patch过程。

一些迷你React,如preact会将它们混合在一起,一边diff一边patch(幸好它使用了Promise.then来优化,确保每次只更新一个组件) 。

有些迷你React则是通过减少移动进行优化,于是绞尽脑汁,用上各种算法,最短编辑距离,最长公共子序列,最长上升子序列。。。

其实基于算法的优化是一种绝望的优化,就类似玛雅文明因为找不到铜矿一直停留于石器时代,诞生了伟大的工匠精神把石器打磨得美伦美奂。

之所以这么说,因为diff算法都用于组件的新旧children比较,children一般不会出现过长的情况,有点大炮打蚊子。况且当我们的应用变得非常庞大,页面有上万个组件,要diff这么多组件,再卓绝的算法也不能保证浏览器不会累趴。因为他们没想到浏览器也会累趴,也没有想到这是一个长跑的问题。如果是100米短跑,或者1000米竞赛,当然越快越好。如果是马拉松,就需要考虑到保存体力了,需要注意休息了。性能是一个系统性的工程。

在我们的代码里面,休息就是检测时间然后断开Fiber链。

updateFiberAndView里面先进行updateView,由于节点的更新是不可控,因此全部更新完,才检测时间。并且我们完全不用担心updateView会出问题,因为updateView实质上是在batchedUpdates中,里面有try catch。而接下来我们基于DFS更新节点,每个节点都要check时间,这个过程其实很害怕出错的, 因为组件在挂载过程中会调三次钩子/方法(constructor, componentWillMount, render), 组件在更新过程中会调4次钩子 (componentWillReceiveProps, shouldUpdate, componentWillUpdate), 总不能每个方法都用try catch包起来,这样会性能很差。而constructor, render是不可避免的,于是对三个willXXX动刀了。

在早期版本中,componentWillMount与componentWillReceiveProps会做内部优化,执行多次setState都会延后到render时进行合并处理。因此用户就肆意setState了。这些willXXX还可以让用户任意操作DOM。 操作DOM会可能reflow,这是官方不愿意看到的。于是官方推出了getDerivedStateFromProps,让你在render设置新state,你主要返回一个新对象,它就主动帮你setState。由于这是一个静态方法,你不能操作instance,这就阻止了你多次操作setState。由于没有instance,也就没有http://instance.refs.xxx,你也没有机会操作DOM了。这样一来,getDerivedStateFromProps的逻辑应该会很简单,这样就不会出错,不会出错,就不会打断DFS过程。

getDerivedStateFromProps取代了原来的componentWillMount与componentWillReceiveProps方法,而componentWillUpdate本来就是可有可无,以前完全是为了对称好看。

在即使到来的异步更新中,reconciler阶段可能执行多次,才执行一次commit,这样也会导致willXXX钩子执行多次,违反它们的语义,它们的废弃是不可逆转的。

在进入commi阶段时,组件多了一个新钩子叫getSnapshotBeforeUpdate,它与commit阶段的钩子一样只执行一次。

如果出错呢,在componentDidMount/Update后,我们可以使用componentDidCatch方法。于是整个流程变成这样:

reconciler阶段的钩子都不应该操作DOM,最好也不要setState,我们称之为轻量钩子*。commit阶段的钩子则对应称之为重量钩子**。

任务系统

updateFiberAndView是位于一个requestIdleCallback中,因此它的时间很有限,分给DFS部分的时间也更少,因此它们不能做太多事情。这怎么办呢,标记一下,留给commit阶段做。于是产生了一个任务系统。

每个Fiber分配到新的任务时,就通过位操作,累加一个sideEffect。sideEffect字面上是副作用的意思,非常重FP流的味道,但我们理解为任务更方便我们的理解。

每个Fiber可能有多个任务,比如它要插入DOM或移动,就需要加上Replacement,需要设置样式,需要加上Update。

怎么添加任务呢?

fiber.effectTag |= Update

怎么保证不会重复添加相同的任务?

fiber.effectTag &= ~DidCapture;

在commit阶段,怎么知道它包含了某项任务?

if(fiber.effectTag & Update){ /操作属性/}

React内置这么多任务,从DOM操作到Ref处理到回调唤起。。。

顺便说一下anu的任务名,是基于素数进行乘除。

https://github.com/RubyLouvre/anu/blob/master/packages/fiber/commitWork.js

无论是位操作还是素数,我们只要保证某个Fiber的相同性质任务只执行一次就行了。

此外,任务系统还有另一个存在意义,保证一些任务优先执行,某些任务是在另一些任务之前。我们称之为任务分拣。这就像快递的仓库管理一样,有了归类才好进行优化。比如说,元素虚拟DOM的插入移动操作必须在所有任务之前执行,移除操作必须在componentWillUnmount后执行。这些任务之所以是这个顺序,因为这样做才合理,都经过高手们的严密推敲,经过React15时代的大众验证。

Fiber的连体婴结构

连体婴是一个可怕的名词,想想就不舒服,因为事实上Fiber就是一个不寻常的结构,直到现在我的anujs还没有很好实现这结构。Fiber有一个叫alternate的属性,你们称之为备胎,替死鬼,替身演员。你也可以视它为git的开发分支,稳定没错的那个则是master。每次 setState时,组件实例stateNode上有一个_reactInternalFiber的对象,就是master分支,然后立即复制一个一模一样的专门用来踩雷的alternate对象。

alternate对象会接受上方传递下来的新props,然后从getDerivedStateFromProps得到新state,于是render不一样的子组件,子组件再render,渐渐的,master与alternate的差异越来越大,当某一个子组件出错,于是我们又回滚到该边界组件的master分支。

可以说,React16通过Fiber这种数据结构模拟了git的三种重要操作, git add, git commit, git revert。

有关连体婴结构的思考,可以参看我另一篇文章《从错误边界到回滚到MWI》,这里就不再展开。

中间件系统

说起中间件系统,大家可能对koa与redux里面的洋葱模型比较熟悉。

早在React15时代,已经有一个叫Transaction的东西,与洋葱模型一模一样。在 Transaction 的源码中有一幅特别的 ASCII 图,形象的解释了 Transaction 的作用。

简单地说,一个Transaction 就是将需要执行的 method 使用 wrapper 封装起来,再通过 Transaction 提供的 perform 方法执行。而在 perform 之前,先执行所有 wrapper 中的 initialize 方法;perform 完成之后(即 method 执行后)再执行所有的 close 方法。一组 initialize 及 close 方法称为一个 wrapper,从上面的示例图中可以看出 Transaction 支持多个 wrapper 叠加。

这个东西有什么用呢? 最少有两个用处,在更新DOM时,收集当前获取焦点的元素与选区,更新结束后,还原焦点与选区(因为插入新节点会引起焦点丢失,document.activeElement变成body,或者是autoFocus,让焦点变成其他input,导致我们正在输入的input的光标不见了,无法正常输入)。在更新时,我们需要保存一些非受控组件,在更新后,对非受控组件进行还原(非受控组件是一个隐涩的知识点,目的是让那些没有设置onChange的表单元素无法手动改变它的值)。当然了,contextStack, containerStack的初次入栈与清空也可以做成中间件。中间件就是分布在batchedUpdates的两侧,一种非常易于扩展的设计,为什么不多用用呢!

总结

React Fiber是对React来说是一次革命,解决了React项目严重依赖于手工优化的痛点,通过系统级别的时间调度,实现划时代的性能优化。鬼才般的Fiber结构,为异常边界提供了退路,也为限量更新提供了下一个起点。React团队的人才济济,创造力非凡,别出心裁,从更高的层次处理问题,这是其他开源团队不可多见。这也是我一直选择与学习React的原因所在。

15、什么是强缓存和协商缓存?

强缓存:直接从本地副本比对读取, 不去请求服务器 ,返回的状态码是 200 。 协商缓存: 会去服务器比对 ,若没改变才直接读取本地缓存,返回的状态码是 304 。 强缓存主要包括 expires 和 cache-control 。 expires 是 HTTP1.0 中定义的缓存字段。 当我们请求一个资源,服务器返回时,可以在 Response Headers 中增加 expires 字段表示资源的过期时间

16、 说说你对@reduxjs/toolkit的理解?和react-redux有什么区别?

Redux
redux只是一种架构模式,它可以应用到任意需要使用它的框架,react,vue等等。它是为了解决相对复杂的应用中不同组件之间共享状态而产生的,比如react中两个组件要访问同一个状态,可以把它提到最近的父组件,然后向下传递,但应用一旦复杂了,这样就会变得繁琐。redux这种模式就解决了类似这样的问题。

redux就是提供了一个叫store的容器里面的state存放了全局的数据状态,对外提供了三个方法getState(), dispatch(), subscribe()

getState(): 用来获取state的值,dispatch(action)用来发起一个action告诉一个叫reducer的函数怎么去更新state,同时把上一次的state作为参数也传给reducer, reducer拿到参数后,返回更新后的state, 得到新的state后就需要渲染组件,可以手动去调用render方法,但这样恒麻烦,通过subscribe接受一个调用render的函数放在一个数组中,每次dispatch的时候除了会通过reducer改变state,还会遍历还数组中的函数去调用,这样每次数据发生变化就可以重新去渲染组件。

react-redux
react-redux跟redux不同的是,它是专门为react服务的,它将redux中store的概念和React中context的概念结合起来,解决了相对复杂的react应用中不同组件共享状态传值的问题,它提供一个Provider的容器组件,该组件接收外界通过props将store传给它,并将store放在context中,子组件可以在connect的时候取到store,Connect接收mapStateToProps(该诉高阶组件需要什么样的数据,是一个接收state值作为参数返回所需state对象的函数), mapDispatchToProps(该诉高阶组件怎么样去触发dispatch,是一个函数,接收dispatch作为参数返回包含触发dispatch的函数的对象)作为参数返回一个以当前业务组件作为参数的高阶组件,明白了redux的实现原理,熟悉React,react-redux中间件的原理也就明白了。

17、 如何通过原生js实现一个节流函数和防抖函数?

js防抖

特点:

  1. 当事件触发时,相应的函数并不会立即触发,而是会等待一定的时间(非常短的时间);
  2. 当事件密集触发时,函数的触发会被频繁的推迟;
  3. 只有等待了一段时间也没有事件触发,才会真正的执行响应函数;
image-20221106185306252

应用场景:

  • Ø输入框中频繁的输入内容,搜索或者提交信息;
  • Ø频繁的点击按钮,触发某个事件;
  • Ø监听浏览器滚动事件,完成某些特定操作;
  • Ø用户缩放浏览器的resize事件;

实现方式

第三方库实现

<input type="text"> <!--html标签-->

<script src="https://cdn.jsdelivr.net/npm/underscore@1.13.1/underscore-umd-min.js"></script>
<script>
const inputEl = document.querySelector("input")
let counter = 0
const inputChange = function(event) {
      console.log(`发送了第${++counter}次网络请求`, this, event)
}

// 防抖处理
inputEl.oninput = _.debounce(inputChange, 2000)//当用户停止输入的时候延时2秒后执行
</script>
手动实现(原理)

基本实现:

function debounce(fn, delay) {
  // 1.定义一个定时器, 保存上一次的定时器
  let timer = null
  // 2.真正执行的函数
  const _debounce = function() {
    // 取消上一次的定时器
    if (timer) clearTimeout(timer)
    // 延迟执行
    timer = setTimeout(() => {
      // 外部传入的真正要执行的函数
      fn()
    }, delay)
  }

  return _debounce
}
this绑定/事件对象
function debounce(fn, delay) {
  // 1.定义一个定时器, 保存上一次的定时器
  let timer = null
  // 2.真正执行的函数
  const _debounce = function(...args) {
    // 取消上一次的定时器
    if (timer) clearTimeout(timer)
    // 延迟执行
    timer = setTimeout(() => {
      // 外部传入的真正要执行的函数
      fn.apply(this, args)
    }, delay)
  }
  return _debounce
}

第一次立即执行
function debounce(fn, delay, immediate = false) {
  // 1.定义一个定时器, 保存上一次的定时器
  let timer = null
  let isInvoke = false

  // 2.真正执行的函数
  const _debounce = function(...args) {
    // 取消上一次的定时器
    if (timer) clearTimeout(timer)

    // 判断是否需要立即执行
    if (immediate && !isInvoke) {
      fn.apply(this, args)
      isInvoke = true
    } else {
      // 延迟执行
      timer = setTimeout(() => {
        // 外部传入的真正要执行的函数
        fn.apply(this, args)
        isInvoke = false
      }, delay)
    }
  }

  return _debounce
}

取消功能
function debounce(fn, delay, immediate = false) {
  // 1.定义一个定时器, 保存上一次的定时器
  let timer = null
  let isInvoke = false

  // 2.真正执行的函数
  const _debounce = function(...args) {
    // 取消上一次的定时器
    if (timer) clearTimeout(timer)

    // 判断是否需要立即执行
    if (immediate && !isInvoke) {
      fn.apply(this, args)
      isInvoke = true
    } else {
      // 延迟执行
      timer = setTimeout(() => {
        // 外部传入的真正要执行的函数
        fn.apply(this, args)
        isInvoke = false
        timer = null
      }, delay)
    }
  }

  // 封装取消功能
  _debounce.cancel = function() {
    if (timer) clearTimeout(timer)
    timer = null
    isInvoke = false
  }

  return _debounce
}
函数返回值
<body>
  
  <input type="text">
  <script src="./05_debounce-v5-函数返回值.js"></script>
  <script>
    const inputEl = document.querySelector("input")
    let counter = 0

    const inputChange = function(event) {
      console.log(`发送了第${++counter}次网络请求`, this, event)

      // 返回值
      return "aaaaaaaaaaaa"
    }

    // 防抖处理--拿到包装好的事件处理回调函数--回调函数的返回值是Promise
    const debounceChange = debounce(inputChange, 3000, false, (res) => {
      console.log("拿到真正执行函数的返回值:", res)
    })
    const tempCallback = () => {
      debounceChange().then(res => {
        console.log("Promise的返回值结果:", res)
      })
    }
    inputEl.oninput = tempCallback
  </script>
</body>
</html>

js节流

特点:

  1. 当事件触发时,会执行这个事件的响应函数;
  2. 如果这个事件会被频繁触发,那么节流函数会按照一定的频率来执行函数;
  3. 不管在这个中间有多少次触发这个事件,执行函数的频繁总是固定的;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-95oO1uXD-1672398315332)(D:/专高/专高4/ls/js节流与防抖/images/image-20221106190337808.png)]

应用场景:

Ø 游戏中的一些设计–王者荣耀 英雄的普攻;

Ø监听页面的滚动事件;

Ø 鼠标移动事件;

Ø 用户频繁点击按钮操作;

实现方式:

第三方库实现

<input type="text"> <!--html标签-->

<script src="https://cdn.jsdelivr.net/npm/underscore@1.13.1/underscore-umd-min.js"></script>
<script>
const inputEl = document.querySelector("input")
let counter = 0
const inputChange = function(event) {
      console.log(`发送了第${++counter}次网络请求`, this, event)
}

// 节流处理
inputEl.oninput =_.throttle(inputChange, 2000)//当用户不断触发事件按照2秒的频率执行
</script>

手动实现(原理)

基本实现:
function throttle(fn, interval, options) {
  // 1.记录上一次的开始时间
  let lastTime = 0

  // 2.事件触发时, 真正执行的函数
  const _throttle = function() {

    // 2.1.获取当前事件触发时的时间
    const nowTime = new Date().getTime()

    // 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间, 计算出还剩余多长事件需要去触发函数
    const remainTime = interval - (nowTime - lastTime)
    if (remainTime <= 0) {
      // 2.3.真正触发函数
      fn()
      // 2.4.保留上次触发的时间
      lastTime = nowTime
    }
  }

  return _throttle
}
控制开始执行一次(默认开启,后续可以通过参数来关闭)
function throttle(fn, interval, options = { leading: true, trailing: false }) {
  // 1.记录上一次的开始时间
  const { leading, trailing } = options
  let lastTime = 0

  // 2.事件触发时, 真正执行的函数
  const _throttle = function() {

    // 2.1.获取当前事件触发时的时间
    const nowTime = new Date().getTime()
    if (!lastTime && !leading) lastTime = nowTime

    // 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间, 计算出还剩余多长事件需要去触发函数
    const remainTime = interval - (nowTime - lastTime)
    if (remainTime <= 0) {
      // 2.3.真正触发函数
      fn()
      // 2.4.保留上次触发的时间
      lastTime = nowTime
    }
  }

  return _throttle
}
最后执行一次
function throttle(fn, interval, options = { leading: true, trailing: false }) {
  // 1.记录上一次的开始时间
  const { leading, trailing } = options
  let lastTime = 0
  let timer = null

  // 2.事件触发时, 真正执行的函数
  const _throttle = function() {

    // 2.1.获取当前事件触发时的时间
    const nowTime = new Date().getTime()
    if (!lastTime && !leading) lastTime = nowTime

    // 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间, 计算出还剩余多长事件需要去触发函数
    const remainTime = interval - (nowTime - lastTime)
    if (remainTime <= 0) {
      if (timer) {
        clearTimeout(timer)
        timer = null
      }

      // 2.3.真正触发函数
      fn()
      // 2.4.保留上次触发的时间
      lastTime = nowTime
      return
    }

    if (trailing && !timer) {
      timer = setTimeout(() => {
        timer = null
        lastTime = !leading ? 0: new Date().getTime()
        fn()
      }, remainTime)
    }
  }

  return _throttle
}

18、说说webpack中常见的loader?解决了什么问题?

一、是什么

loader 用于对模块的"源代码"进行转换,在 import 或"加载"模块时预处理文件

webpack做的事情,仅仅是分析出各种模块的依赖关系,然后形成资源列表,最终打包生成到指定的文件中。如下图所示:

img

webpack内部中,任何文件都是模块,不仅仅只是js文件

默认情况下,在遇到import或者require加载模块的时候,webpack只支持对jsjson 文件打包

csssasspng等这些类型的文件的时候,webpack则无能为力,这时候就需要配置对应的loader进行文件内容的解析

在加载模块的时候,执行顺序如下:

img

webpack 碰到不识别的模块的时候,webpack 会在配置的中查找该文件解析规则

关于配置loader的方式有三种:

  • 配置方式(推荐):在 webpack.config.js文件中指定 loader
  • 内联方式:在每个 import 语句中显式指定 loader
  • CLI 方式:在 shell 命令中指定它们

#配置方式

关于loader的配置,我们是写在module.rules属性中,属性介绍如下:

  • rules是一个数组的形式,因此我们可以配置很多个loader
  • 每一个loader对应一个对象的形式,对象属性test 为匹配的规则,一般情况为正则表达式
  • 属性use针对匹配到文件类型,调用对应的 loader 进行处理

代码编写,如下形式:

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          { loader: 'style-loader' },
          {
            loader: 'css-loader',
            options: {
              modules: true
            }
          },
          { loader: 'sass-loader' }
        ]
      }
    ]
  }
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

二、特性

这里继续拿上述代码,来讲讲loader的特性

从上述代码可以看到,在处理css模块的时候,use属性中配置了三个loader分别处理css文件

因为loader支持链式调用,链中的每个loader会处理之前已处理过的资源,最终变为js代码。顺序为相反的顺序执行,即上述执行方式为sass-loadercss-loaderstyle-loader

除此之外,loader的特性还有如下:

  • loader 可以是同步的,也可以是异步的
  • loader 运行在 Node.js 中,并且能够执行任何操作
  • 除了常见的通过 package.jsonmain 来将一个 npm 模块导出为 loader,还可以在 module.rules 中使用 loader 字段直接引用一个模块
  • 插件(plugin)可以为 loader 带来更多特性
  • loader 能够产生额外的任意文件

可以通过 loader 的预处理函数,为 JavaScript 生态系统提供更多能力。用户现在可以更加灵活地引入细粒度逻辑,例如:压缩、打包、语言翻译和更多其他特性

三、常见的loader

在页面开发过程中,我们经常性加载除了js文件以外的内容,这时候我们就需要配置响应的loader进行加载

常见的loader如下:

  • style-loader: 将css添加到DOM的内联样式标签style里
  • css-loader :允许将css文件通过require的方式引入,并返回css代码
  • less-loader: 处理less
  • sass-loader: 处理sass
  • postcss-loader: 用postcss来处理CSS
  • autoprefixer-loader: 处理CSS3属性前缀,已被弃用,建议直接使用postcss
  • file-loader: 分发文件到output目录并返回相对路径
  • url-loader: 和file-loader类似,但是当文件小于设定的limit时可以返回一个Data Url
  • html-minify-loader: 压缩HTML
  • babel-loader :用babel来转换ES6文件到ES

下面给出一些常见的loader的使用:

css-loader

分析 css 模块之间的关系,并合成⼀个 css

npm install --save-dev css-loader

1

rules: [
  ...,
 {
  test: /\.css$/,
    use: {
      loader: "css-loader",
      options: {
     // 启用/禁用 url() 处理
     url: true,
     // 启用/禁用 @import 处理
     import: true,
        // 启用/禁用 Sourcemap
        sourceMap: false
      }
    }
 }
]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

如果只通过css-loader加载文件,这时候页面代码设置的样式并没有生效

原因在于,css-loader只是负责将.css文件进行一个解析,而并不会将解析后的css插入到页面中

如果我们希望再完成插入style的操作,那么我们还需要另外一个loader,就是style-loader

#style-loader

css-loader 生成的内容,用 style 标签挂载到页面的 head

npm install --save-dev style-loader

1

rules: [
  ...,
 {
  test: /\.css$/,
    use: ["style-loader", "css-loader"]
 }
]

1
2
3
4
5
6
7

同一个任务的 loader 可以同时挂载多个,处理顺序为:从右到左,从下往上

#less-loader

开发中,我们也常常会使用lesssassstylus预处理器编写css样式,使开发效率提高,这里需要使用less-loader

npm install less-loader -D

1

rules: [
  ...,
 {
  test: /\.css$/,
    use: ["style-loader", "css-loader","less-loader"]
 }
]

1
2
3
4
5
6
7

#raw-loader

webpack中通过 import方式导入文件内容,该loader并不是内置的,所以首先要安装

npm install --save-dev raw-loader

1

然后在 webpack.config.js 中进行配置

module.exports = {
  ...,
  module: {
      rules: [
      {
        test: /\.(txt|md)$/,
        use: 'raw-loader'
     }
    ]
 }
}

1
2
3
4
5
6
7
8
9
10
11

#file-loader

把识别出的资源模块,移动到指定的输出⽬目录,并且返回这个资源在输出目录的地址(字符串)

npm install --save-dev file-loader

1

rules: [
  ...,
 {
  test: /\.(png|jpe?g|gif)$/,
    use: {
      loader: "file-loader",
      options: {
        // placeholder 占位符 [name] 源资源模块的名称
        // [ext] 源资源模块的后缀
        name: "[name]_[hash].[ext]",
        //打包后的存放位置
        outputPath: "./images",
        // 打包后文件的 url
        publicPath: './images',
      }
    }
 }
]

url-loader

可以处理理 file-loader 所有的事情,但是遇到图片格式的模块,可以选择性的把图片转成 base64 格式的字符串,并打包到 js 中,对小体积的图片比较合适,大图片不合适。

npm install --save-dev url-loader

1

rules: [
  ...,
 {
  test: /\.(png|jpe?g|gif)$/,
    use: {
      loader: "url-loader",
      options: {
        // placeholder 占位符 [name] 源资源模块的名称
        // [ext] 源资源模块的后缀
        name: "[name]_[hash].[ext]",
        //打包后的存放位置
        outputPath: "./images"
        // 打包后文件的 url
        publicPath: './images',
        // 小于 100 字节转成 base64 格式
        limit: 100
      }
    }
 }
]

参考文献

  • https://webpack.docschina.org/concepts/loaders/
  • https://segmentfault.com/a/1190000018680530
  • https://vue3js.cn/interview/

19、 说说javascript内存泄漏的几种情况?

一、是什么

内存泄漏(Memory leak)是在计算机科学中,由于疏忽或错误造成程序未能释放已经不再使用的内存

并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费

程序的运行需要内存。只要程序提出要求,操作系统或者运行时就必须供给内存

对于持续运行的服务进程,必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃

img

C语言中,因为是手动管理内存,内存泄露是经常出现的事情。

char * buffer; buffer = (char*) malloc(42); // Do something with buffer free(buffer);

上面是 C 语言代码,malloc方法用来申请内存,使用完毕之后,必须自己用free方法释放内存。

这很麻烦,所以大多数语言提供自动内存管理,减轻程序员的负担,这被称为"垃圾回收机制"

二、垃圾回收机制

Javascript 具有自动垃圾回收机制(GC:Garbage Collecation),也就是说,执行环境会负责管理代码执行过程中使用的内存

原理:垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存

通常情况下有两种实现方式:

  • 标记清除
  • 引用计数

标记清除

JavaScript最常用的垃圾收回机制

当变量进入执行环境是,就标记这个变量为“进入环境“。进入环境的变量所占用的内存就不能释放,当变量离开环境时,则将其标记为“离开环境“

垃圾回收程序运行的时候,会标记内存中存储的所有变量。然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉

在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了

随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存

举个例子:

var m = 0,n = 19 // 把 m,n,add() 标记为进入环境。
add(m, n) // 把 a, b, c标记为进入环境。
console.log(n) // a,b,c标记为离开环境,等待垃圾回收。
function add(a, b) {
  a++
  var c = a + b
  return c
}

引用计数

语言引擎有一张"引用表",保存了内存里面所有的资源(通常是各种值)的引用次数。如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放

如果一个值不再需要了,引用数却不为0,垃圾回收机制无法释放这块内存,从而导致内存泄漏

const arr = [1, 2, 3, 4];
console.log('hello world');

面代码中,数组[1, 2, 3, 4]是一个值,会占用内存。变量arr是仅有的对这个值的引用,因此引用次数为1。尽管后面的代码没有用到arr,它还是会持续占用内存

如果需要这块内存被垃圾回收机制释放,只需要设置如下:

arr = null

通过设置arrnull,就解除了对数组[1,2,3,4]的引用,引用次数变为 0,就被垃圾回收了

小结

有了垃圾回收机制,不代表不用关注内存泄露。那些很占空间的值,一旦不再用到,需要检查是否还存在对它们的引用。如果是的话,就必须手动解除引用

三、常见内存泄露情况

意外的全局变量

function foo(arg) {
    bar = "this is a hidden global variable";
}

另一种意外的全局变量可能由 this 创建:

function foo() {
    this.variable = "potential accidental global";
}
// foo 调用自己,this 指向了全局对象(window)
foo();

上述使用严格模式,可以避免意外的全局变量

定时器也常会造成内存泄露

var someResource = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        // 处理 node 和 someResource
        node.innerHTML = JSON.stringify(someResource));
    }
}, 1000);

如果id为Node的元素从DOM中移除,该定时器仍会存在,同时,因为回调函数中包含对someResource的引用,定时器外面的someResource也不会被释放

包括我们之前所说的闭包,维持函数内局部变量,使其得不到释放

function bindEvent() {
  var obj = document.createElement('XXX');
  var unused = function () {
    console.log(obj, '闭包内引用obj obj不会被释放');
  };
  obj = null; // 解决方法
}

没有清理对DOM元素的引用同样造成内存泄露

const refA = document.getElementById('refA');
document.body.removeChild(refA); // dom删除了
console.log(refA, 'refA'); // 但是还存在引用能console出整个div 没有被回收
refA = null;
console.log(refA, 'refA'); // 解除引用

包括使用事件监听addEventListener监听的时候,在不监听的情况下使用removeEventListener取消对事件监听

20、 说说React生命周期中有哪些坑?如何避免?

避免生命周期中的坑需要做好两件事:1、不在恰当的时候调用了不该调用的代码;2、在需要调用时,不要忘了调用。

那么下面7种情况最容易造成生命周期的坑:

  • getDerivedStateFromProps 容易编写反模式代码,使受控组件和非受控组件区分模糊
  • componentWillMount 在 React 中已被标记弃用,不推荐使用,主要的原因是因为新的异步架构会导致它被多次调用,所以网络请求以及事件绑定应该放到 componentDidMount 中
  • componentWillReceiveProps 同样也被标记弃用,被 getDerivedStateFromProps 所取代,主要原因是性能问题。
  • shouldComponentUpdate 通过返回 true 或者 false 来确定是否需要触发新的渲染。主要用于性能优化。
  • componentWillUpdate 同样是由于新的异步渲染机制,而被标记废弃,不推荐使用,原先的逻辑可结合 getSnapshotBeforeUpdate 与 componentDidUpdate 改造使用。
  • 如果在 componentWillUnmount 函数中忘记解除事件绑定,取消定时器等清理操作,容易引发 bug。
  • 如果没有添加错误边界处理,当渲染发生异常时,用户将会看到一个无法操作的白屏,所以一定要添加。

追问:React 的请求应该放到哪里?为什么?

  • 对于异步请求,应该放到componentDidMount 中去操作,从生命周期函数执行顺序来看,除了componentDidMount 之外还有下面两种选择:

    • constructor 中可以放,但是不推荐,因为 constructor 主要用于初始化 state 和函数绑定,并不承载业务逻辑,而且随着类属性的流行,constructor 很少用啦
    • componentWillMount 已被标记废弃,因为在新的异步渲染下该方法会触发多次渲染,容易引发bug,不利于 React 的后期维护和更新
  • 所以React 的请求放在 componentDidMount 里是最好的选择。

21、 说说redux的实现原理是什么,写出核心代码?

前言

相信很多人都在使用redux作为前端状态管理库进去项目开发,但仍然停留在“知道怎么用,但仍然不知道其核心原理”的阶段,接下来带大家分析一下redux和react-redux两个库的核心思想和API

redux

1.为什么要使用redux?

随着互联网的高速发展,我们的应用变得越来越复杂,进行导致我们的组件之间的关系也变得日趋复杂,往往需要将状态父组件 -> 子组件 -> 子子组件 -> 又或者只是简单的 父组件与子组件之间的props传递也会导致我们的数据流变得难以维护,因为二次开发者不在熟悉项目的情况下无法第一时间确定数据来源是由谁发起的。使用redux之后,所有的状态都来自于store中的state,并且store通过react-redux中的Provider组件可以传递到Provider组件下的所有组件,也就是说store中的state对于Provider下的组件来说就是全局的。

2.redux的核心原理是什么?

1.将应用的状态统一放到state中,由store来管理state。
2.reducer的作用是返回一个新的state去更新store中对用的state。
3.按redux的原则,UI层每一次状态的改变都应通过action去触发,action传入对应的reducer 中,reducer返回一个新的state更新store中存放的state,这样就完成了一次状态的更新
4.subscribe是为store订阅监听函数,这些订阅后的监听函数是在每一次dipatch发起后依次执行
5.可以添加中间件对提交的dispatch进行重写

3.redux的api有哪些?

**1.**createStore 创建仓库,接受reducer作为参数
2.bindActionCreator 绑定store.dispatch和action 的关系
3.combineReducers 合并多个reducers
4.applyMiddleware 洋葱模型的中间件,介于dispatch和action之间,重写dispatch
5.compose 整合多个中间件

22、谈谈你对BFC的理解?

img

是什么?

我们在页面布局的时候,经常会出现下面几种情况

这个元素的高度怎么没了?

这两栏布局怎么没法自适应?

这两种元素的间距怎么有点奇怪的样子?

。。。

归根揭底是元素之间相互影响,导致了意料之外的情况,这里就涉及到了BFC概念。

BFC(Block Formatting Context),即块级格式化上下文,他是页面中的一块渲染区域,并且有属于自己一套的渲染规则。

1.内部的盒子会在垂直方向上一个接一个的放置。

2.对于同一个BFC的两个相邻盒子的margin会发生重叠,与方向无关。

3.每一个元素的左外边距与包含块的左边界相接触(从左到右),即使是浮动元素也是如此。

4.BFC区域不会与float的元素区域重叠。

5.计算BFC的高度时,浮动元素也参与计算。

6.BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响外面的元素,反之亦然。

触发条件

触发BFC条件包含但是不限于

根元素,即HTML元素。

浮动元素。float值为left、right

overflow的值不为visible,为auto、scroll、hidden

display的值为inline-block、inltable-cell、table-caption、table、inline-table、flex、inline-flex、grid、inline-grid

position的值为absolute或fixed

运用场景

利用BFC的特性,我们将BFC运用在以下的场景

1.防止margin重叠(塌陷)

<style>
p {
color: #f55;
background: #fcc;
width: 200px;
line-height: 100px;
text-align:center;
margin: 100px;
    }
</style>
<body>
<p>Haha</p>
<p>Hehe</p>
</body>

img

两个p元素之间的距离为100px,发生了margin重叠(塌陷),以最大的为准,如果第一个margin为80的话,两个p元素之间的距离也是100,以最大的为准。

前面讲到,同一个BFC的两个相邻盒子的margin会发生重叠。

可以在p外面包裹一层容器,并触发这个容器生成一个BFC,那么两个p不属于同一个BFC,则不会出现margin重叠。

<style>
    .wrap {
        overflow: hidden;// 新的BFC
    }
    p {
        color: #f55;
        background: #fcc;
        width: 200px;
        line-height: 100px;
        text-align:center;
        margin: 100px;
    }
</style>
<body>
<p>Haha</p>
<divclass="wrap">
<p>Hehe</p>
</div>
</body>

这时候,则不会发生重叠

img

清楚内部浮动

<style>
    .par {
        border: 5px solid #fcc;
        width: 300px;
    }
 
    .child {
        border: 5px solid #f66;
        width:100px;
        height: 100px;
        float: left;
    }
</style>
<body>
    <div class="par">
        <div class="child"></div>
        <div class="child"></div>
    </div>
</body>

页面显示如下

img

而BFC在计算高度的时候,浮动元素也会参与,所以我们可以触发.par元素才能触发BFC,则内部浮动元素计算高度也会计算。

.par {
overflow: hidden;
}

实现出来的效果如下

img

适应多栏布局

这里举例一个两栏布局

<style>
body {
width: 300px;
position: relative;
    }

.aside {
width: 100px;
height: 150px;
float: left;
background: #f66;
    }

.main {
height: 200px;
background: #fcc;
    }
</style>
<body>
<divclass="aside"></div>
<divclass="main"></div>
</body>

效果图如下

img

前面讲到,每一个元素的左外边距与包含块的左边界相接触。

因此,虽然.aslide为浮动元素,但是main的左边依然会与包含块的左边相接触而BFC区域不会与浮动盒子重叠

所以我们可以通过触发main生成BFC,以此适应两栏布局

.main {
overflow: hidden;
}

这时候,新的BFC不会与浮动的.aside元素重叠。因此会根据包含块的宽度,和.aside的宽度,自动变窄

效果如下

img

小结

可以看到上面几个案例,都体现了BFC实际上就是一个页面的独立容器,里面的子元素不影响外面的元素

23、说说TCP为什么需要三次握手和四次握手

#一、三次握手

三次握手(Three-way Handshake)其实就是指建立一个TCP连接时,需要客户端和服务器总共发送3个包

主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备

过程如下:

  • 第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN©,此时客户端处于 SYN_SENT 状态
  • 第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,为了确认客户端的 SYN,将客户端的 ISN+1作为ACK的值,此时服务器处于 SYN_RCVD 的状态
  • 第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,值为服务器的ISN+1。此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接

img

上述每一次握手的作用如下:

  • 第一次握手:客户端发送网络包,服务端收到了 这样服务端就能得出结论:客户端的发送能力、服务端的接收能力是正常的。
  • 第二次握手:服务端发包,客户端收到了 这样客户端就能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。不过此时服务器并不能确认客户端的接收能力是否正常
  • 第三次握手:客户端发包,服务端收到了。 这样服务端就能得出结论:客户端的接收、发送能力正常,服务器自己的发送、接收能力也正常

通过三次握手,就能确定双方的接收和发送能力是正常的。之后就可以正常通信了

#为什么不是两次握手?

如果是两次握手,发送端可以确定自己发送的信息能对方能收到,也能确定对方发的包自己能收到,但接收端只能确定对方发的包自己能收到 无法确定自己发的包对方能收到

并且两次握手的话, 客户端有可能因为网络阻塞等原因会发送多个请求报文,延时到达的请求又会与服务器建立连接,浪费掉许多服务器的资源

#二、四次挥手

tcp终止一个连接,需要经过四次挥手

过程如下:

  • 第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于 FIN_WAIT1 状态,停止发送数据,等待服务端的确认
  • 第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 +1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT状态
  • 第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态
  • 第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 +1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态,服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态

img

#四次挥手原因

服务端在收到客户端断开连接Fin报文后,并不会立即关闭连接,而是先发送一个ACK包先告诉客户端收到关闭连接的请求,只有当服务器的所有报文发送完毕之后,才发送FIN报文断开连接,因此需要四次挥手

#三、总结

一个完整的三次握手四次挥手如下图所示:

img

#参考文献

  • https://zhuanlan.zhihu.com/p/53374516
  • https://segmentfault.com/a/1190000020610336

24、清除浮动的方法(最常用的4种)

为什么要清除浮动?

清除浮动主要是为了解决,父元素因为子级元素浮动引起的内部高度为0的问题

1.如下,我给父盒子设置一个boder,内部放两个盒子一个big 一个small,未给big和small设置浮动,则他们会默认撑开父盒子

img

2.当我给内部两个盒子加上float属性的时候

img

顶部深蓝色盒子就会顶上来,然后父盒子因为没设置高度,变成一条线,big和small已经浮动了

总结一下:

当父元素不给高度的时候,

内部元素不浮动时会撑开

而浮动的时候,父元素变成一条线

这时候很多人会想到新建标签clear:both和float 方法,但是这两种方法并不推荐使用!

什么是clear:both

clear:both:本质就是闭合浮动, 就是让父盒子闭合出口和入口,不让子盒子出来

清除浮动的方法(最常用的4种)
1.额外标签法(在最后一个浮动标签后,新加一个标签,给其设置clear:both;)(不推荐)

Document
big
small
额外标签法

此时

img

如果我们清除了浮动,父元素自动检测子盒子最高的高度,然后与其同高。

优点:通俗易懂,方便

缺点:添加无意义标签,语义化差

不建议使用。

2.父级添加overflow属性(父元素添加overflow:hidden)(不推荐)

通过触发BFC方式,实现清除浮动

.fahter{
width: 400px;
border: 1px solid deeppink;
overflow: hidden;
}
优点:代码简洁

缺点:内容增多的时候容易造成不会自动换行导致内容被隐藏掉,无法显示要溢出的元素

不推荐使用

{

什么是BFC

BFC:块级格式化上下文,它是指一个独立的块级渲染区域,只有Block-level BOX参与,该区域拥有一套渲染规则来约束块级盒子的布局,且与区域外部无关。

在一个Web页面的CSS渲染中,块级格式化上下文 (Block Fromatting Context)是按照块级盒子布局的。W3C对BFC的定义如下:

浮动元素和绝对定位元素,非块级盒子的块级容器(例如 inline-blocks, table-cells, 和 table-captions),以及overflow值不为“visiable”的块级盒子,都会为他们的内容创建新的BFC(块级格式上下文)。

为了便于理解,我们换一种方式来重新定义BFC。一个HTML元素要创建BFC,则满足下列的任意一个或多个条件即可:

1、float的值不是none。
2、position的值不是static或者relative。
3、display的值是inline-block、table-cell、flex、table-caption或者inline-flex
4、overflow的值不是visible

BFC是一个独立的布局环境,其中的元素布局是不受外界的影响,并且在一个BFC中,块盒与行盒(行盒由一行中所有的内联元素所组成)都会垂直的沿着其父元素的边框排列。

怎么创建BFC

要显示的创建一个BFC是非常简单的,只要满足上述4个CSS条件之一就行。例如:

<div class="container">
  你的内容
</div>

在类container中添加类似 overflow: scroll,overflow: hidden,display: flex,float: left,或 display: table 的规则来显示创建BFC。虽然添加上述的任意一条都能创建BFC,但会有一些副作用:

1、display: table 可能引发响应性问题
2、overflow: scroll 可能产生多余的滚动条
3、float: left 将把元素移至左侧,并被其他元素环绕
4、overflow: hidden 将裁切溢出元素

}

3.使用after伪元素清除浮动(推荐使用)

.clearfix:after{/伪元素是行内元素 正常浏览器清除浮动方法/
content: “”;
display: block;
height: 0;
clear:both;
visibility: hidden;
}
.clearfix{
*zoom: 1;/*ie6清除浮动的方式 号只有IE6-IE7执行,其他浏览器不执行/
}

big
small
优点:符合闭合浮动思想,结构语义化正确

缺点:ie6-7不支持伪元素:after,使用zoom:1触发hasLayout.

推荐使用

4.使用before和after双伪元素清除浮动

.clearfix:after,.clearfix:before{
content: “”;
display: table;
}
.clearfix:after{
clear: both;
}
.clearfix{
*zoom: 1;
}

big
small

优点:代码更简洁

缺点:用zoom:1触发hasLayout.

推荐使用
————————————————
版权声明:本文为CSDN博主「h_qingyi」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/h_qingyi/article/details/81269667

25、 说说你对栈、队列的理解?应用场景?

1.栈

**栈(stack)**又名堆栈,它是一种运算受限的线性表,限定仅在表尾进行插入和删除操作的线性表

表尾这一端被称为栈顶,相反地另一端被称为栈底,向栈顶插入元素被称为进栈、入栈、压栈,从栈顶删除元素又称作出栈

所以其按照先进后出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据,具有记忆作用

实现一个栈:

class Stack {
  constructor() {
    this.items = [];
  }

  /**
   * 添加一个(或几个)新元素到栈顶
   * @param {*} element 新元素
   */
  push(element) {
    this.items.push(element)
  }

  /**
   * 移除栈顶的元素,同时返回被移除的元素
   */
  pop() {
    return this.items.pop()
  }

  /**
   * 返回栈顶的元素,不对栈做任何修改(这个方法不会移除栈顶的元素,仅仅返回它)
   */
  peek() {
    return this.items[this.items.length - 1]
  }

  /**
   * 如果栈里没有任何元素就返回true,否则返回false
   */
  isEmpty() {
    return this.items.length === 0
  }

  /**
   * 移除栈里的所有元素
   */
  clear() {
    this.items = []
  }

  /**
   * 返回栈里的元素个数。这个方法和数组的length属性很类似
   */
  size() {
    return this.items.length
  }
}

关于栈的操作主要的方法如下:

  • push:入栈操作
  • pop:出栈操作

二.队列

跟栈十分相似,队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作

进行插入操作的端称为队尾,进行删除操作的端称为队头,当队列中没有元素时,称为空队列

在队列中插入一个队列元素称为入队,从队列中删除一个队列元素称为出队。因为队列只允许在一端插入,在另一端删除,所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为先进先出

简单实现一个队列,如下:

class Queue {
    constructor() {
        this.list = []
        this.frontIndex = 0
        this.tailIndex = 0
    }
    enqueue(item) {
        this.list[this.tailIndex++] = item
    }
    unqueue() {
        const item  = this.list[this.frontIndex]
        this.frontIndex++        
        return item
    }
}

三.应用场景

借助栈的先进后出的特性,可以简单实现一个逆序输出的功能,首先把所有元素依次入栈,然后把所有元素出栈并输出

包括编译器的在对输入的语法进行分析的时候,例如"()“、”{}“、”[]"这些成对出现的符号,借助栈的特性,凡是遇到括号的前半部分,即把这个元素入栈,凡是遇到括号的后半部分就比对栈顶元素是否该元素相匹配,如果匹配,则前半部分出栈,否则就是匹配出错

包括函数调用和递归的时候,每调用一个函数,底层都会进行入栈操作,出栈则返回函数的返回值

生活中的例子,可以把乒乓球盒比喻成一个堆栈,球一个一个放进去(入栈),最先放进去的要等其后面的全部拿出来后才能出来(出栈),这种就是典型的先进后出模型

队列

当我们需要按照一定的顺序来处理数据,而该数据的数据量在不断地变化的时候,则需要队列来帮助解题

队列的使用广泛应用在广度优先搜索中,例如层次遍历一个二叉树的节点值

生活中的例子,排队买票,排在队头的永远先处理,后面的必须等到前面的全部处理完毕再进行处理,这也是典型的先进先出模型

参考文献
https://baike.baidu.com/item/%E6%A0%88/12808149
[https://baike.baidu.com/item/%E9%98%9F%E5%88%97/14580481](

26、 说说你对git rebase 和git merge的理解?区别?

git merge和git rebase的区别, 切记:永远用rebase

来谈一下git merge和git rebase的区别。

img

Git无疑现在已经成为最流行的代码管理工具之一。其中有两个命令,对很多程序员造成了很多的困惑,一个是merge,一个是rebase。

这些困惑主要纠结于到底应该用merge还是用rebase。

在继续深入探讨之前,我先抛出我的观点。如果你想拥有一套稳定的,健壮的代码, 永远要使用rebase。

不为别的,就为了rebase可以给你提供一套清晰的代码历史。

相反的, merge会给你一套乱七八糟的代码历史。当你看到这样的代码历史的时候,我相信你绝对没有心情去研究每一个历史对应的代码。

好,接下来我们就详细分析一下这两个命令的作用。假设我们的repo有这么个主branch: master。

每个程序员在创建自己的代码之前,要首先创建自己的个人分支,然后代码修改开始。

假如你有6个程序员一起工作, 你就会有6个程序员的分支, 如果你使用merge, 你的代码历史树就会有六个branch跟这个主的branch交织在一起。

那个画风我相信对你一定很熟悉。想着那个画风感觉到一切都好无助, 有个词儿比较合适,叫做欲仙欲死。

这就是merge命令下生成的代码分支历史。

那么rebase又能做到什么程度呢?Rebase永远不会导致多个历史分支进行交织。它永远都是一条线。纯洁而又干脆。轻轻爽爽的, 从不拖泥带水。

那为什么会这样呢?

先说一下merge。Merge命令会保留所有commit的历史时间。每个人对代码的提交是各式各样的。尽管这些时间对于程序本身并没有任何意义。但是merge的命令初衷就是为了保留这些时间不被修改。这样也就形成了以merge时间为基准的网状历史结构。每个分支上都会继续保留各自的代码记录, 主分支上只保留merge的历史记录。子分支随时都有可能被删除。子分子删除以后,你能够看到的记录也就是,merge某branch到某branch上了。这个历史记录描述基本上是没有意义的。

还有一个比较有意思的是你不能,也不应该去修改这个历史记录描述。那是因为这个merge记录里面,不仅仅包含你自己的代码,也包含别人的代码。到这里你能想象有多乱吧?

再来说一下rebase, 这个命令会始终把你最新的修改放到最前头。比如你对主branch进行rebase以后, 你的所有修改就会在主branch当前所有的修改之前。你会更有信心保证你的代码运行畅通无阻。通过你自己的测试以后, 你就可以放心的把代码合并到主的branch里面了。

这里值得一提的是,rebase通常是发生在自己的个人branch上的。它的基础就是现有的主branch。这样做的好处就是保证每个人的代码都可以运行在当前最新的主branch的代码上。

这里再提一个比较有意思的现象。在我工作的这么多公司之中,只有不多的几家在使用rebase, 有相当数量的公司还在使用merge。经过观察我发现, 凡是代码管理混乱的项目, 或者整个项目组的做决定者不太清楚代码整洁的重要性, 或者脑子有问题的, 他们都在使用merge。哈哈,我开玩笑呢。

不过, 我还是建议大家去亲身体验一下rebase的好处吧。

27、 说说git常用的命令有哪些?

git链接: 查看git常用命令

28、CDN的特点及意义?

CDN简介:

CDN的全称是Content Delivery Network,即内容分发网络。其目的是通过在现有的Internet中增加一层新的网络架构,将网站的内容发布到最接近用户的网络"边缘",使用户可以就近取得所需的内容,解决 Internet网络拥挤的状况,提高用户访问网站的响应速度。从技术上全面解决由于网络带宽小、用户访问量大、网点分布不均等原因所造成的用户访问网站响应速度慢的问题。 (也就是一个服务器的内容,平均分部到多个服务器上,服务器智能识别,让用户获取离用户最近的服务器,提高速度。

实现思路:

其基本思路就是尽可能避开互联网上有可能影响数据传输速度和稳定性的瓶颈和环节,使内容传输的更快、更稳。通过在网络各处放置节点服务器所构成的在现有的互联网基础之上的一层智能虚拟网络,CDN系统能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上。

CDN的作用:

实际上,内容分发布网络(CDN)是一种新型的网络构建方式,它是为能在传统的IP网发布宽带丰富媒体而特别优化的网络覆盖层;而从广义的角度,CDN代表了一种基于质量与秩序的网络服务模式。

CDN的特点:

与目前现有的内容发布模式相比较,CDN强调了网络在内容发布中的重要性。通过引入主动的内容管理层的和全局负载均衡,CDN从根本上区别于传统的内容发布模式。
  1、CDN网站加速 提高了企业站点(尤其含有大量图片和静态页面站点)的访问速度,并大大提高以上性质站点的稳定性
2、镜像服务 消除了不同运营商之间互联的瓶颈造成的影响,实现了跨运营商的网络加速,保证不同网络中的用户都能得到良好的访问质量。
3、远程加速 远程访问用户根据DNS负载均衡技术 智能自动选择Cache服务器,选择的Cache服务器,加快远程访问的速度
4、带宽优化 自动生成服务器的远程Mirror(镜像)cache服务器,远程用户访问时从cache服务器上读取数据,减少远程访问的带宽、分担网络流量、减轻原站点WEB服务器负载等功能。
5、集群抗攻击 广泛分布的CDN节点加上节点之间的智能冗于机制,可以有效地预防黑客入侵以及降低各种D.D.o.S攻击对网站的影响,同时保证较好的服务质量。

CDN原理

表现为一个透明的数据传输通道,这种透明性表现在网络的质量保证仅仅停留在数据包的层面,而不能根据内容对象的不同区分服务质量。此外,由于IP网的"尽力而为"的特性使得其质量保证是依靠在用户和应用服务器之间端到端地提供充分的、远大于实际所需的带宽通量来实现的。在这样的内容发布模式下,不仅大量宝贵的骨干带宽被占用,同时ICP的应用服务器的负载也变得非常重,而且不可预计。当发生一些热点事件和出现浪涌流量时,会产生局部热点效应,从而使应用服务器过载退出服务。这种基于中心的应用服务器的内容发布模式的另外一个缺陷在于个性化服务的缺失和对宽带服务价值链的扭曲,内容提供商承担了他们不该干也干不好的内容发布服务。
  纵观整个宽带服务的价值链,内容提供商和用户位于整个价值链的两端,中间依靠网络服务提供商将其串接起来。随着互联网工业的成熟和商业模式的变革,在这条价值链上的角色越来越多也越来越细分。比如内容/应用的运营商、托管服务提供商、骨干网络服务提供商、接入服务提供商等等。在这一条价值链上的每一个角色都要分工合作、各司其职才能为客户提供良好的服务,从而带来多赢的局面。从内容与网络的结合模式上看,内容的发布已经走过了ICP的内容(应用)服务器和IDC这两个阶段。IDC的热潮也催生了托管服务提供商这一角色。但是,IDC并不能解决内容的有效发布问题。内容位于网络的中心并不能解决骨干带宽的占用和建立IP网络上的流量秩序。因此将内容推到网络的边缘,为用户提供就近性的边缘服务,从而保证服务的质量和整个网络上的访问秩序就成了一种显而易见的选择。而这就是内容发布网(CDN)服务模式。CDN的建立解决了困扰内容运营商的内容"集中与分散"的两难选择,无疑对于构建良好的互联网价值链是有价值的,也是不可或缺的最优网站加速服务。
————————————————
版权声明:本文为CSDN博主「__爱吃香菜」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_59763367/article/details/128318670

29、从浏览器地址栏输入url到显示页面的步骤

基础版本

浏览器根据请求的 URL 交给 DNS 域名解析,找到真实 IP,向服务器发起请求;
服务器交给后台处理完成后返回数据,浏览器接收文件(HTML、JS、CSS、图像等);
浏览器对加载到的资源(HTML、JS、CSS 等)进行语法解析,建立相对应的内部数据结构(如 HTML 的 DOM);
载入解析到的资源文件,渲染页面,完成。
详细版

1.在浏览器地址栏输入URL
2.浏览器查看缓存,如果请求资源在缓存中并且新鲜,跳转到转码步骤

如果资源未缓存,发起新请求
如果已缓存,检验是否足够新鲜,足够新鲜直接提供给客户端,否则与服务器进行验证。
检验新鲜通常有两个HTTP头进行控制 Expires和Cache-Control:
HTTP1.0提供Expires,值为一个绝对时间表示缓存新鲜日期
HTTP1.1增加了Cache-Control:max-age=,值为以秒为单位的最大新鲜时间
浏览器解析URL获取协议,主机,端口,path
4.浏览器组装一个HTTP(GET)请求报文
5.浏览器获取主机ip地址,过程如下:
浏览器缓存
本机缓存
hosts文件
路由器缓存
ISP DNS缓存
DNS递归查询(可能存在负载均衡导致每次IP不一样)
6.打开一个socket与目标IP地址,端口建立TCP链接,三次握手如下:

客服端发送一个TCP的SYN=1,Seq=X的包到服务器端口
服务器发回SYN=1,ACK=X+1,Seq=Y的响应包
客户端发送ACK=Y+1,Seq=Z
7.TCP链接建立后发送HTTP请求
8.服务器接受请求并解析,将请求转发到服务器程序,如虚拟主机使用HTTP Host头部判断请求的服务程序
9.服务器检查HTTP请求头是否包含缓存验证信息如果验证缓存新鲜,返回304等对应状态码
10.处理程序读取完整请求并准备HTTP响应,可能需要查询数据库等操作
11.服务器将响应报文通过TCP连接发送回浏览器
12.浏览器接受HTTP响应,然后根据情况选择关闭TCP连接或者保留重用,关闭TCP连接的四次握手如下

主动方发送Fin=1,Ack=Z,Seq=X报文
被动方发送ACK=X+1,Seq=Z报文
被动方发送Fin=1,ACK=X,Seq=Y报文
主动方发送ACK=Y,Seq=X报文
13.浏览器检查响应状态码:是否为1XX,3XX,4XX,5XX,这些情况处理与2XX不同
14.如何资源可缓存,进行缓存
15.对响应进行解码(例如gzip压缩 )
16.根据资源类型决定如何处理(假设资源为HTML文档)
17.解析HTML文档,构件DOM树,下载资源,构造CSSOM树,执行js脚本,这些操作没有严格的先后顺序,以下分别解释
18.构建DOM树:

Tokenizing:根据HTML规范将字符流解析为标记
Lexing:词法分析将标记转换为对象并定义属性和规则
DOM construction:根据HTML标记关系将对象组成DOM树
19.解析过程中遇到图片、样式表、js文件,启动下载
20.构建CSSOM树:

Tokenizing:字符流转换为标记流
Node:根据标记创建节点
CSSOM:节点创建CSSOM树
21.根据DOM树和CSSOM树构建渲染树:

从DOM树的根节点遍历所有可见节点,不可见节点包括:(script、meta 这样本身不可见的标签,被css隐藏的节点,如 display:none)
对每一个可见节点,找到恰当的CSSOM规则并应用
发不可视节点,找到恰当的CSSOM规则并应用
22.js解析如下:

浏览器创建Document对象并解析HTML,将解析到的元素和文本节点添加到文档中,此时document.readystate为loading
HTML解析器遇到没有async和defer的script时,将他们添加到文档中,然后执行行内或外部脚本。这些脚本会同步执行,并且在脚本下载和执行时解析器会暂停。这样就可以用document.write()把文本插入到输入流中。同步脚本经常简单定义函数和注册事件处理程序,他们可以遍历和操作script和他们之前的文档内容
当解析器遇到设置了 async 属性的 script 时,开始下载脚本并继续解析文档。脚本会在它下载完成后尽快执行,但是解析器不会停下了等它下载。异步脚本禁止使用document.write(),它们可以访问自己script和之前的文档元素
当文档完成解析,document.readState变成interactive
所有defer脚本会按照在文档出现的顺序执行,延迟脚本能访问完整文档树,禁止使用document.write()
浏览器**在Document对象上触发DOMContentLoaded事件
此时文档完全解析完成,浏览器可能还在等待如图片等内容加载,等这些内容完成载入并且所有异步脚本完成载入和执行,document.readState变为complete,window触发load事件
23.显示页面(HTML解析过程中会逐步显示页面)

详细简版

1.从浏览器接收 url 到开启网络请求线程(这一部分可以展开浏览器的机制以及进程与线程之间的关系)
2.开启网络线程到发出一个完整的HTTP请求(这一部分涉及到dns查询,TCP/IP 请求,五层原因特网协议栈等知识)
3.从服务器接收到请求到对应后台接收到请求(这一部分可能涉及到负载均衡,安全拦截以及后台内部的处理等等)
4.后台和前台的 HTTP 交互(这一部分包括 HTTP 头部、响应码、报文结构、cookie 等知识,可以提下静态资源的 cookie 优化,以及编码解码,如gzip压缩等)
5.单独拎出来的缓存问题,HTTP 的缓存(这部分包括http缓存头部,ETag,catch-control 等)
6.浏览器接受到 HTTP 数据包后的解析流程(解析html -词法分析然后解析成 dom 树、解析 css 生存 css 规则树、合并成 render 树,然后 layout、painting渲染、复合图层的合成、GPU 绘制、外链资源的处理、loaded 和 DOMContentLoaded 等)
7.CSS的可视化格式模型(元素的渲染规则,如包含块,控制框,BFC,IFC等概念)
8.JS引擎解析过程(JS的解释阶段,预处理阶段,执行阶段生存执行上下文,V0,作用域链、回收机制等等)
9.其它(可以拓展不同的知识模块,如跨域,web安全,hybrid模式等等内容)
————————————————
版权声明:本文为CSDN博主「Python学长」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_43356200/article/details/107363084

30、 如果需要手动写动画,你认为最小时间间隔是多久,为什么?

写动画从来没怎么注意过,今天看了个问题,说css3写动画最小的时间间隔是多少,其原因,之前一直想着只要在人眼识别的范围内都可以把。

现在记录一下,涨涨姿势。

多数显示器的默认频率是60HZ,即每秒刷新60次。所以理论上的最小间隔是 1/60*1000ms = 16.7ms

31、 SPA(单页应用)首屏加载速度慢怎么解决?

(1)减小入口文件积
(2)静态资源本地缓存
(3)UI框架按需加载
(4)图片资源的压缩
(5)组件重复打包
(6)开启GZip压缩
(7)使用SSR

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值