前端面试

195 阅读26分钟

HTML

DOCTYPE 有什么用?

高速浏览器用什么规范来渲染文档,当不存在的时候会混杂模式来渲染,标准模式会以浏览器的最高标准来运行.

h5 不急于 HGML 所以不需要对 dtd 的引用, 需要指定 DOCTYPE
H4 基于 HGML 所以需要 dtd 告知浏览器文档的文档类型

样式的 link @import 区别?

1.link 是 html 标签,import 是 css 提供的只加载 css
2.页面加载时,link 会被同事加载,

src 和 href 的区别?

src 替换当前元素,href 建立外界资源的联系


当 src 指向外部资源时,将会下载内容并嵌入到当前标签的位置,在请求的时候会下载并应用到文档内
当浏览器解析到该元素时会暂停其他元素的解析,直到加载,编译,执行 完成才回去处理其他元素,
所以需要放置到最后


href 指向网络资源时,会建立外部资源的链接,并且下载该资源时浏览器会并行下载,
而不会停止当前文档的处理

CSS

CSS 盒模型

标准: width = content+padding+border+margin
ie width = (content+padding+border) + margin


ie 是把 padding 和 border 算在 content 之内的,
所以 css 有属性来对应
box-sizing:content-box 标准
box-sizing:border-box 怪异

BFC 的原理?

根元素都会产生 BFC

BFC 元素的垂直边距上会发生重叠

BFC 区域不会与浮动元素的 box 重叠

是一个独立的元素,外面不会影响到内部,内部也不会影响到外部

计算 BFC 高度时浮动元素也会参与计算

BFC 可以阻止元素被浮动元素覆盖。

如何创建 BFC?

浮动会产生
overflow 不是 visible
position 不是 static 或者 relative
display 是 inline-block 或者 table-cell 或者 flex

BFC 的常见使用场景?

解决边距重叠
BFC 不和 float 重叠
清除浮动

CSS 三栏布局的实现?

使用 float 浮动元素要位于非浮动元素的上面
使用绝对定位的方式来实现
使用 flex 弹性盒模型来实现
使用 table 布局来实现   父元素设置为 table 布局子元素布局设置为 table-cell 即可
使用 inline-block 配合 calc 来实现三栏布局

你对 line-height 是如何理解的?

两行文字之间基线的距离。
CSS 中起作用的 height 和 line-height,没有定义 height 属性,
最终其表现作用一定是 line-height

单行文本垂直居中:只需要设置 height 和 line-height 相等即可
多行文本垂直居中:不知道宽高的可以使用 padding 上下一致即可

怎么让 chrome 支持小于 12px 的文字?

{font-size:10px; -webkit-transform: scale(0.8);}

如何使得一个元素的高度随着它的宽度变化,且高度一直是宽度的一半?

设置 width 属性,js 动态监听
不设置高度,设置宽度然后 padding:25% 0;
paddingmargin 的值相对于父元素设置

display:inline-block 什么时候会显示间隙?

元素存在空格
margin 存在
font-size 补位 0

flex 布局

flex 的核心的概念就是  容器  和  轴

容器包括外层的  父容器  和内层的  子容器,
轴包括  主轴  和  交叉轴,

justify-content 属性用于定义如何沿着主轴方向排列子容器。
align-items  属性用于定义如何沿着交叉轴方向分配子容器的间距。
flex-direction  属性决定主轴的方向
flex-wrap 设置换行方式
flex-flow 轴向与换行组合设置:
align-content 多行沿交叉轴对齐

子容器

在主轴上如何伸缩:flex

flex-basis 设置基准大小
flex-grow 设置扩展比例
flex-shrink 设置收缩比例
order 设置排列顺序

JS

let、const 和 var 的区别?

var 声明变量可以重复声明,而 let 不可以重复声明
var 是不受限于块级的,声明会提升,而 let 是受限于块级,仅在该块级内起作用
var 会与 window 相映射(会挂一个属性),而 let 不与 window 相映射
var 可以在声明的上面访问变量,而 let 有暂存死区,在声明的上面访问变量会报错
const 声明之后必须赋值,否则会报错
const 定义不可变的量,改变了就会报错
constlet 一样不会与 window 相映射、支持块级作用域、在声明的上面访问变量会报错


varlet允许先声明后赋值;const不允许,它声明变量时必须同时初始化变量。

varlet允许修改变量的值与类型;而const不允许。

var定义的变量的声明会提升;letconst不会。

var允许重复声明;letconst不允许。

var声明的是函数作用域;letconst声明的是块级作用域。
	函数作用域:在函数内定义变量,该变量会成为函数的局部变量,变量会在函数退出时被销毁。

	块级作用域({})是函数作用域的子集,所以letconst同样适用于函数作用域。
    
var在全局作用域中声明的变量会成为window对象的属性;letconst不会。


由于let的变量不会在作用域中提升,所以let不能引用未声明的变量,
在let声明之前的执行瞬间被称为“暂时性死区”,在此阶段引用任何后面才声明的变量都会抛出ReferenceError

循环问题

	终止循环: 
    	for循环 return false
		filter 具体使用
        every  都是具体使用    
        
        forEach循环里不能有 break || continue

	map不能终止循环,
    
    总结:
    	(1) for:当没有label标记时候,break跳出本次循环并执行循环体后的代码,
        	continue结束本次循环执行下一次循环。没有return。
        (2) Array.forEach:遍历整个数组,return false或者true
        	都是结束本次循环执行下一次循环。没有break || continue。
        (3) Array.map:map和forEach类似,有返回值,返回结果是return 值组成的数组。
        (4) for...in:会忽略break || continue。没有return。
        (5) for...of:break跳出本次循环并执行循环体后的代码,continue结束本次循环执行下一次循环,和for一样。
        	注意:for(var v in arr)v是数组值!。
        (6) Jquery.each: 
                return false跳出本次循环并执行循环体后的代码;
                return true结束本次循环执行下一次循环。没有break || continue

深浅拷贝

深: 递归, JSON.stringify 
浅: 解构,object.assign(target, ...sources)

递归时为什么会出现内存溢出的情况

递归非常消耗内存,因为需要同时保存很多的调用帧,
因为栈可存放的函数是有限制的,一旦存放了过多的函数且没有得到释放的话,
就会出现爆栈的问题。

event loop

jS 中存在宏任务和微任务,这两个分别维护一个队列,

都是采用先进先出的策略执行的,

并且微任务的优先级高于宏任务。

常见的宏任务有:
    script,setTimeout、setInterval、I/O、UI 交互事件、postMessage、MessageChannel、setImmediate(Node.js 环境)。

常见的微任务有:
    Promise.then、 MutationObserver、 process.nextTick(Node.js 环境)。

任务执行顺序如下:
1.先执行同步代码

2、遇到异步宏任务则将异步宏任务放入宏任务队列中,

3. 遇到异步微任务则将异步微任务放入微任务队列中

4. 所有同步代码执行完毕后,再将异步微任务从队列中调入主线程执行

5. 微任务执行完毕后再将异步宏任务从队列中调入主线程执行,一直循环直至所有任务执行完毕。


事件循环执行之前开始进行UI render
// 例子;
console.log("script start");

setTimeout(function () {
  console.log("setTimeout");
}, 0);

Promise.resolve()
  .then(function () {
    console.log("promise1");
  })
  .then(function () {
    console.log("promise2");
  });
console.log("script end");

//输出  script start  script end  promise1    promise2  setTimeout

NodeJS 的 Event Loop

Event Loop就是在libuv中实现的。

    timers: 执行setTimeout和setInterval中到期的callback。

    pending callback: 上一轮循环中少数的callback会放在这一阶段执行。

    idle, prepare: 仅在内部使用。

    poll: 最重要的阶段,执行pending callback,在适当的情况下回阻塞在这个阶段。

    check: 执行setImmediate(setImmediate()是将事件插入到事件队列尾部,主线程和事件队列的函数执行完成之后立即执行setImmediate指定的回调函数)的callback。

    close callbacks: 执行close事件的callback,例如socket.on('close'[,fn])或者http.server.on('close, fn)。

跨域

JSONP postMessage CORS 服务器代理

HTTP 协议的主要特点?

url 固定处理比较简单
灵活,完成不同数据类型的传输
无连接 不会保持链接,用完挤掉
无状态 任务完成就断开,下次传输需要重新建立

常见的 HTTP 状态码?

1XX:指示信息--表示信息已被接受,继续处理
2XX:成功 --表示信息已经成功返回

```
200 OK客户端请求成功
206 Partial Content:客户端发送了一个带RangeGET请求,服务器响应完成
```

3XX: 重定向 --表示资源地址已经变更

```
301 永久重定向 所有请求的页面已经转移之新的URL
302 所请求的页面已经临时转移至新的URL
304请求已经发出,但是为满足要求而不需要进行请求即可完成
```

4XX:请求错误--表示客户端请求出错

```
401 用户未登录或者为授权
403 Forbidden 对请求的页面的访问被禁止
404 Not Found 请求的资源不存在
```

5XX:服务器端出错 --表示服务器正在维修或者已坏

```
500:服务器发生不可预期的错误原来缓冲的文档还可以继续使用
503:请求未完成,服务器临时过载或当机,一段时间内可恢复正常
```

http 的缓存机制是怎么样的

两个缓存机制策略。
它又分为强缓存和协商缓存。优先级较高的是强缓存,
在命中强缓存失败的情况下,才会走协商缓存。

特点:
    不会向服务器发送网络请求,直接从缓存中读取资源
    请求返回 200 的状态码
    在 devtools 的 network 选项卡可以看到 size 显示 from disk cache 或 from memory cache
设置:
    都是通过设置 HTTP Header 来实现的

强缓存:

    - expires 网页缓存相关字段
    - cache-control   是缓存的指令
    - cache-control 高于 expires

expires : wed mar 04 XXXX
        定义了到期时间
        可以修改本地时间失效

cache-control: max-age=30
        在 30s 后过期,可以通过多种指令来组合

协商缓存:

    - Last-Modified  表示这个资源在服务器上的最后修改时间
    - ETag 当前资源文件一个唯一标识(由服务器生成),
    - 只要资源有变化,Etag就会重新生成

last 缺点
    本地打开缓存文件还是会造成被修改,
    服务器还是会发送文件过来
    以秒计时不准确
etag
    优先级搞,
    通过算法出来的,损耗性能

缓存流程:
    先强制缓存,后协商缓存
    强制生效直接用缓存,不行就协商
    协商由服务器来决定,
    协商失败,发送新资源
    成功使用缓存
    啥都没有用就设置
    启发式算法 - 响应头的 date-(last-modified)的 10% 作为缓存时间

从缓存位置上来说分为四种,并且各自有优先级,当依次查找缓存且都没有命中的时候,才会去请求网络

Service Worker
Memory Cache
Disk Cache
Push Cache

Service Worker

Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能

使用 Service Worker 的话,传输协议必须为 HTTPS,因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全

需要代码注册

坚挺 install 事件之后就可以缓存了

下次用户访问的时候就会查询是否存在缓存了

可以自由控制缓存什么文件

当没有命中缓存的时候就会调用 fetch 去获取数据

浏览器都会先从 service worker 中去获取缓存

Memory Cache

也就是内存中的缓存,读取内存中的数据比磁盘快
随着 tab 进程而释放

浏览器会把解析完成的 js 和 css 放入内存中
快速读取

Disk Cache

也就是存储在硬盘中的缓存,读取速度慢点,
但是什么都能存储到磁盘中,

比之 Memory Cache 胜在容量和存储时效性上

浏览器会把大文件放进硬盘
如果内存使用过高就会存放硬盘里面

Push Cache

http2 新增内容,当前面三个都没有命中就会被使用,只在 session 里存在

     所有的资源都能被推送,但有一定的兼容性问题

     可以推送 no-cache 和 no-store 的资源

     一旦连接被关闭,Push Cache 就被释放

     多个页面可以使用相同的 HTTP/2 连接,即可以使用同一份缓存

     Push Cache 中的缓存只能被使用一次

     浏览器可以拒绝接受已经存在的资源推送

谈谈你对原型链的理解?

每个对象都有 proto 属性,
这个属性又有 constructor 函数
constructor 函数内部又有 prototype
所以当前元素中没有某一个函数或者方法时会沿着原型链一直往上查找,
直到最顶部为止。

instanceOf 的原理?

// instanceof 原理实现
function myInstanceof(left, right) {
  先去看右边对象的原型;
  let prototype = right.prototype;
  let left = left.__proto__;
  while (true) {
    if (left === null || right === null) {
      return false;
    }
    if (prototype === left) {
      return true;
    }
    left = left.__proto__;
  }
}

new 运算符工作原理?

生成一个对象
链接原型
绑定 this
返回对象
function create() {
  let obj = {};
  let Con = [].shift.call(arguments);
  obj.__proto__ = Con.prototype;
  let result = Con.apply(obj, arguments);
  return result instanceof Object ? result : obj;
}

圣杯继承

function inherit(target, origin) {
  function F() {}
  //函数原型指向源对象
  F.prototype = origin.prototype;
  //目标对象原型对象指向新的对象
  target.prototype = new F();
  //目标对象的构造函数指向目标对象本身
  target.prototype.constructor = target;
  target.prototype.uber = origin.prototype;
}

事件的冒泡与捕获机制?

三个阶段 捕获-目标-冒泡
ie 没有冒泡
addEventListener() 捕获 true 冒泡 false

Ajax

var xhr = new XMLHttpRequest()
  // 必须在调用 open()之前指定 onreadystatechange 事件处理程序才能确保跨浏览器兼容性
  xhr.onreadystatechange = function () {
  if (xhr.readyState === 4) {
    if (xhr.status >= 200 && xhr.status < 300 || xhr.status ==== 304) {
    	console.log(xhr.responseText)
    } else {
    	console.log('Error:' + xhr.status)
    }
  }
}
// 第三个参数表示异步发送请求
xhr.open('get', '/api/getSth',  true)
// 参数为作为请求主体发送的数据
xhr.send(null)

Vue

生命周期

创建 beforeCreate
挂在
更新
销毁

创建,在 init 之后
        初始化事件和生命周期钩子
        初始化 inject provide state 属性
    created 之后
        data 已初始化,计算属性 event watch 事件回调都有了dom 没有挂在
创建-挂载
    先去看有没有 el 对象,

    无的话直接挂载

    有去看 template 模板

    再去看看有没有 template 模板

    有 转化为 render 函数,通过 render 函数去渲染 dom

    无 编译 el 对象外层 html 作为模板

挂载
    挂在前调用 render 函数生成虚拟 dom 准备替换
    挂载完成 dom 一出现,进行 dom 操作

更新
    数据更新有被调用
    虚拟 dom 渲染补丁来重新渲染 dom

销毁
    清除 watcher 子组件 事件监听这些等等

生命周期的顺序

props => methods =>data => computed => watch;

父组件创建 - 父组件挂载前 - 所有子组件创建 - 所有子组件挂载前 - 子组件挂载完 - 父组件挂载完

加入 keep-alive 之后,
    首次激活时,activated都在mounted后。
    再次激活时,本组件只走activated(另外一组件先失活deactivated)
    组件失活时,均不走beforeDestroy和destroyed

computed 和 watch 有什么区别?

computed
    计算值的场景,
    不可异步,
    缓存需要计算的值
watch
    监听属性的变化
    可以异步

key 的作用?

唯一的 key 在 diff 的时候查找元素
最小变动

keep-alive 组件有什么作用?

防止组件多次渲染,
包裹的组件不会销毁,缓存在内存中
activated 组件被命中的时候
deactivated 组件被隐藏或者弃掉的时候

mixin 和 mixins 区别?

mixin 用于全局注册混入,影响到每个组件,插件都这么做的
mixins 组件内部的 api 用于分发属性和方法进行合并的,高于原来的函数

nextTick 函数有什么作用?

在 Vue 生命周期的 created()钩子函数进行的 DOM 操作一定要放在 Vue.nextTick()的回调函数中

在数据变化后要执行的某个操作,而这个操作需要使用随数据改变而改变的 DOM 结构的时候,这个操作都应该放进 Vue.nextTick()的回调函数中。

原理:
是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新。
先收集事件,监听 dom 元素变化 MutationObserver
再去判断宏 微任务
let isUsingMicroTask = false
最后执行

    - 把传入的回调函数 cb 压入 callbacks 数组
    - 执行 timerFunc 函数,延迟调用  flushCallbacks  函数
    - 遍历执行  callbacks  数组中的所有函数

这里的  callbacks  没有直接在  nextTick  中执行回调函数的原因是保证在同一个  tick  内多次执行 nextTick ,不会开启多个异步任务,而是把这些异步任务都压成一个同步任务,在下一个  tick  执行完毕。

什么是 VNode?

虚拟 dom ,js 对象,用来比对和真实节点的差异,快速替换

diff 算法

当数据更新时。我们可以对比新数据构建的  vnode  和老数据构建的  oldVnode  的差异,
来实现外科手术式的精准更新。

同层次的节点来比对,更新最新数据.

会有新老节点来比对,
会同时给两组节点给个开头和结尾的下标,
olds olde,
news newe,
这个时候首先两两比较,就会有四种结果,
如果同一个位置对比匹配了那就保持不变,不动任何操作
如果不是同一个位置,那就移动到 new 的位置最新的节点

这个时候就再次把改变的下标往中间移动 重复这个过程
直到循环结束,
如果四种匹配都没有 那就会利用 key 来比较,
用 news 的 key 与 hash 表做匹配,匹配成功就判断 news 和匹配节点是否为 sameNode 好复用

- 如果没有 key,则直接将 news 生成新的节点插入真实 DOM
- (ps:这下可以解释为什么 v-for 的时候需要设置 key 了,如果没有 key 那么就只会做四种匹配,就算指针中间有可复用的节点都不能被复用了)

// 例子:
真实dom b d c a
old: b d c a
new: a e b f

第一步: oldS = b, oldE = a; S = a, E = f; 结束: a b d c

第二步: oldS = b, oldE = c; S = e, E = f; 结束: a e b d c

第三步: oldS = b, oldE = c; S = b, E = f; 结束: a e b d c

第四步: oldS = d, oldE = c; S = f, E = f; 结束: a e b d c f

第五步: new先结束了 删除老的节点 结束: a e b f

oldS > oldE 表示 oldCh 老节点 先遍历完,那么就将多余的vCh根据index添加到dom中去(如上图)
S > E 表示 vCh 新节点 先遍历完,那么就在真实dom中将区间为[oldS, oldE]的多余节点删掉

插件

Vue 提供的插件注册机制很简单,每个插件都需要实现一个静态的`install`方法,当我们执行`Vue.use`注册插件的时候,就会执行这个`install`方法,并且在这个`install`方法的第一个参数我们可以拿到`Vue`对象,这样的好处就是作为插件的编写方不需要再额外去`import Vue`
import Vue from "vue";

// 定义插件
const RulesPlugin = {
  // 插件应该有一个公开方法install
  // 第一个参数是Vue 构造器
  // 第二个参数是一个可选的选项对象
  install(Vue) {
    // 注入组件
    Vue.mixin({
      // 钩子函数
      created: function () {
        // 验证逻辑
        const rules = this.$options.rules;
        if (rules) {
          Object.keys(rules).forEach((key) => {
            // 取得所有规则
            const { validate, message } = rules[key];

            // 监听,键是变量,值是函数
            this.$watch(key, (newValue) => {
              // 验证规则
              const valid = validate(newValue);
              if (!valid) {
                console.log(message);
              }
            });
          });
        }
      },
    });
  },
};

// 调用插件,实际上就是调用插件的install方法
// 即RulesPlugin.install(Vue)
Vue.use(RulesPlugin);

//原理
Vue.use = function (plugin) {
  // 忽略已注册插件
  if (plugin.installed) {
    return;
  }

  // 集合转数组,并去除第一个参数
  var args = toArray(arguments, 1);

  // 把this(即Vue)添加到数组的第一个参数中
  args.unshift(this);

  // 调用install方法
  if (typeof plugin.install === "function") {
    plugin.install.apply(plugin, args);
  } else if (typeof plugin === "function") {
    plugin.apply(null, args);
  }

  // 注册成功
  plugin.installed = true;
  return this;
};

Webpack

执行流程

Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;
确定入口:根据配置中的 entry 找出所有的入口文件;
编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
完成模块编译:在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果

loader 和 plugin

loader 加载器,写在 module.rules 里面(转化飞 js 的文件)

        - babel-loader:把 ES6 转换成 ES5
        - url-loader  把文件转化为base64的
        - ....

plugins 插件,写在 plugins 数组里面
(满足自定义的需求,Webpack 的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。)

        - commons-chunk-plugin 提取公共代码
        - uglifyjs-webpack-plugin:通过`UglifyES`压缩`ES6`代码
        - babel-plugin-import   按需加载
        - NameModulesPlugin或是HashedModuleIdsPlugin  缓存名字,保持长缓存
        - ....

Loader 像一个"翻译官"把读到的源文件内容转义成新的文件内容,并且每个 Loader 通过链式操作,将源文件一步步翻译成想要的样子。编写 Loader 时要遵循单一原则,每个 Loader 只做一种"转义"工作,

Plugin 的编写就灵活了许多。 webpack 在运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果

Plugin 一个插件由以下构成:

        - 一个具名 JavaScript 函数。
        - 在它的原型上定义 apply 方法。
        - 指定一个触及到 webpack 本身的 事件钩子。
        - 操作 webpack 内部的实例特定数据。
        - 在实现功能后调用 webpack 提供的 callback。

最重要的两个资源就是 compiler 和 compilation 对象

compiler 可以理解为一个 webpack 的实例,该实例存储了 webpack 配置、打包过程等一系列的内容。
compilation 模块会被 compiler 用来创建新的编译(或新的构建)。该实例存放的是本次打包编译的内容。

        - compile 编译器开始编译
        - compilation 编译器开始一个新的编译过程
        - emit 在生成资源并输出到目录之前
        - done 完成编译

钩子函数
emit
afterEmit

webpack-dev-server 和 http 服务器如 nginx 有什么区别

webpack-dev-server 使用内存来存储 webpack 开发环境下的打包文件,并且可以使用模块热更新,他比传统的 http 服务对开发更加简单高效

React

如何用useEffect模拟componentDidMount生命周期?

useEffect(fn, []),
// 但它们并不完全相等。和componentDidMount不一样,
// useEffect会捕获 props和state。
// 所以即便在回调函数里,你拿到的还是初始的props和state。
// 如果你想得到“最新”的值,你可以使用ref。 或者一些其他的方法

// []同样也是一类常见问题的来源, 也即你以为没使用数据流里的值但其实使用了。

Capture Value 的特性

包含了state,props,useState等等
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <h1>{count}</h1>;
}
//由于 useEffect 符合 Capture Value 的特性,
//拿到的 count 值永远是初始化的 0。相当于 setInterval 永远在 count 为 0 的 Scope 中执行,
//你后续的 setCount 操作并不会产生任何作用

//需更改
setCount(count => count + 1);

// 但如果说依赖了多个属性,则需要 useReducer 函数来帮忙,
// 将更新与动作解耦,保证数据的唯一

const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;

useEffect(() => {
  const id = setInterval(() => {
    dispatch({ type: "tick" }); // Instead of setCount(c => c + step);
  }, 1000);
  return () => clearInterval(id);
}, [dispatch]);
// 在reducer内做处理

使用 useCallback 来优化 useEffect 的依赖问题

function Parent() {
  const [query, setQuery] = useState("react");

  // Preserves identity until query changes
  const fetchData = useCallback(() => {
    const url = "https://hn.algolia.com/api/v1/search?query=" + query;
    // ... Fetch data and return it ...
  }, [query]); //  Callback deps are OK

  return <Child fetchData={fetchData} />;
}

function Child({ fetchData }) {
  let [data, setData] = useState(null);

  useEffect(() => {
    fetchData().then(setData);
  }, [fetchData]); //  Effect deps are OK

  // ...
}

这样做经过 useCallback 包装过的函数可以当作普通变量作为 useEffect 的依赖。
useCallback 做的事情,就是在其依赖变化时,
返回一个新的函数引用,触发 useEffect 的依赖变化,并激活其重新执行。

什么情况下需要使用 shouldComponentUpdate

在React中,默认情况下,如果父组件数据发生了更新,那么所有子组件都会无条件更新 !!!!!!

通过shouldComponentUpdate()retrun fasle 来判断阻止 Header 组件做无意义的更新

shouldComponentUpdate()并不是每次都需要使用,而是需要的时候才会优化
class App extends React.Component {
    constructor () {
        this.state = { list: [] }
    }
    render () {
        return (
            <div>
                {/* 当list数据发生变化时,Header组件也会更新,调用 render() */}
                <Header />
                <List data={this.state.list}
            </div>
        )
    }
}

PureComponent 和 memo

class类组件中用PureComponent,无状态组件(无状态)中用memo

PureComponent, SCU中实现了浅比较

浅比较已使用大部分情况(尽量不要做深度比较)

PureComponent 与普通 Component 不同的地方在于,
PureComponent自带了一个shouldComponentUpdate(),并且进行了浅比较
// memo用法
function MyComponent (props) {
    /* 使用 props 渲染 */
}
// areEqual 也可不传
function areEqual(prevProps, nextProps) {
    if (prevProps.seconds===nextProps.seconds) {
        return true
    } else {
        return false
    }
}
export default React.memo(MyComponent, areEqual)

setState

可能是异步,也可能是同步

通常情况下,setState()为异步更新数据

在setTimeout()中setState()和自己定义的 DOM 事件是同步的

传入对象,赋值动作会被合并

传入函数,函数无法合并

this.setState()为什么异步

批处理机制

看 isBatchingUpdates 的状态,为 true 就是异步,为 false 就是同步

命中 batchUpdate 机制 - true
    生命周期(和它调用的函数)
    React 中注册的事件(和它调用的函数)
    React 可以“管理”的入口
    
哪些不能命中 batchUpdate 机制 - false
	setTimeout setInterval等(和它调用的函数)
    自定义的DOM时间(和它调用的函数)
    React“管不到”的入口

this.setState之后react做了哪些操作?

shouldComponentUpdate
componentWillUpdate
render
componentDidUpdate

异步组件

// 引入需要异步加载的组件
const LazyComponent = React.lazy(() => import('./lazyDemo') )

// 使用异步组件,异步组件加载中时,显示fallback中的内容
<React.Suspense fallback={<div>异步组件加载中</div>}>
    <LazyComponent />
</React.Suspense>

Redux 单项数据流

dispatch(action)
reducer 产生 newState
subscribe 触发通知

JSX 本质

讲JSX语法,通过 React.createElement()编译成Dom,BABEL 可以编译JSX
流程:JSX => React.createElement() => 虚拟DOM (JS对象) => 真实DOM

React 底层会通过 React.createElement() 这个方法,
将 JSX 语法转成JS对象,
React.createElement() 可以接收三个参数,第一个为标签名称,第二参数为属性,第三个参数为内容

React Diff算法

同层级的对比
没有key,优先循环新集合 发现新集合中B不等于老集合中的A,于是删除了A,创建了B,依此类推直到删除了老集合中的D,创建了C于新集合。

有key,循环新集合,去老集合里找相同的节点,有的话进行移动,没有就删除

如果类型和结构都不一样,可以通过shouldComponentUpdate来决定

fiber架构解决了什么问题

React 16 之前的版本
当我们通过render()和 setState() 进行组件渲染和更新的时候,React 主要有两个阶段:

协调阶段(Reconciler):官方解释。React 会自顶向下通过递归,遍历新数据生成新的 Virtual DOM,然后通过 Diff 算法,找到需要变更的元素(Patch),放到更新队列里面去。

渲染阶段(Renderer):遍历更新队列,通过调用宿主环境的API,实际更新渲染对应元素。宿主环境,比如 DOM、Native、WebGL 等。

在协调阶段阶段,由于是采用的递归的遍历方式,这种也被成为 Stack Reconciler,主要是为了区别 Fiber Reconciler 取的一个名字。这种方式有一个特点:
一旦任务开始进行,就无法中断,那么 js 将一直占用主线程, 一直要等到整棵 Virtual DOM 树计算完成之后,才能把执行权交给渲染引擎,
那么这就会导致一些用户交互、动画等任务无法立即得到处理,就会有卡顿,非常的影响用户体验。



fiber就是来解决这个,

把渲染更新过程拆分成多个子任务,每次只做一小部分,做完看是否还有剩余时间,
如果有继续下一个任务;
如果没有,挂起当前任务,
将时间控制权交给主线程,等主线程不忙的时候在继续执行。
这种策略叫做 Cooperative Scheduling(合作式调度),操作系统常用任务调度策略之一。

合作式调度主要就是用来分配任务的,
当有更新任务来的时候,不会马上去做 Diff 操作,而是先把当前的更新送入一个 Update Queue 中,然后交给 Scheduler 去处理,
Scheduler 会根据当前主线程的使用情况去处理这次 Update。
为了实现这种特性,使用了requestIdelCallbackAPI。
对于不支持这个API 的浏览器,React 会加上 pollyfill。

在上面我们已经知道浏览器是一帧一帧执行的,
在两个执行帧之间,主线程通常会有一小段空闲时间,
requestIdleCallback可以在这个空闲期(Idle Period)调用空闲期回调(Idle Callback),执行一些任务。