最近美团前端面试题目整理
代码输出结果
输出结果如下:
promise.then 是微任务,它会在所有的宏任务执行完之后才会执行,同时需要 promise 内部的状态发生变化,因为这里内部没有发生变化,一直处于 pending 状态,所以不输出 3。
Nginx 的概念及其工作原理
Nginx 是一款轻量级的 Web 服务器,也可以用于反向代理、负载平衡和 HTTP 缓存等。Nginx 使用异步事件驱动的方法来处理请求,是一款面向性能设计的 HTTP 服务器。
传统的 Web 服务器如 Apache 是 process-based 模型的,而 Nginx 是基于 event-driven 模型的。正是这个主要的区别带给了 Nginx 在性能上的优势。
Nginx 架构的最顶层是一个 master process,这个 master process 用于产生其他的 worker process,这一点和 Apache 非常像,但是 Nginx 的 worker process 可以同时处理大量的 HTTP 请求,而每个 Apache process 只能处理一个。
类组件与函数组件有什么区别呢?
作为组件而言,类组件与函数组件在使用与呈现上没有任何不同,性能上在现代浏览器中也不会有明显差异
它们在开发时的心智模型上却存在巨大的差异。类组件是基于面向对象编程的,它主打的是继承、生命周期等核心概念;而函数组件内核是函数式编程,主打的是 immutable、没有副作用、引用透明等特点。
之前,在使用场景上,如果存在需要使用生命周期的组件,那么主推类组件;设计模式上,如果需要使用继承,那么主推类组件。
但现在由于 React Hooks 的推出,生命周期概念的淡出,函数组件可以完全取代类组件。
其次继承并不是组件最佳的设计模式,官方更推崇“组合优于继承”的设计概念,所以类组件在这方面的优势也在淡出。
性能优化上,类组件主要依靠
shouldComponentUpdate
阻断渲染来提升性能,而函数组件依靠React.memo
缓存渲染结果来提升性能。从上手程度而言,类组件更容易上手,从未来趋势上看,由于 React Hooks 的推出,函数组件成了社区未来主推的方案。
类组件在未来时间切片与并发模式中,由于生命周期带来的复杂度,并不易于优化。而函数组件本身轻量简单,且在 Hooks 的基础上提供了比原先更细粒度的逻辑组织与复用,更能适应 React 的未来发展。
生命周期
init
initLifecycle/Event
,往 vm 上挂载各种属性callHook: beforeCreated
: 实例刚创建initInjection/initState
: 初始化注入和data
响应性created: 创建完成,属性已经绑定, 但还未生成真实
dom`进行元素的挂载:
$el / vm.$mount()
是否有
template
: 解析成render function
*.vue
文件:vue-loader
会将<template>
编译成render function
beforeMount
: 模板编译/挂载之前执行
render function
,生成真实的dom
,并替换到dom tree
中mounted
: 组件已挂载
update
执行
diff
算法,比对改变是否需要触发UI
更新flushScheduleQueue
watcher.before
: 触发beforeUpdate
钩子 -watcher.run()
: 执行watcher
中的notify
,通知所有依赖项更新 UI触发
updated
钩子: 组件已更新actived / deactivated(keep-alive)
: 不销毁,缓存,组件激活与失活destroy
beforeDestroy
: 销毁开始销毁自身且递归销毁子组件以及事件监听
remove()
: 删除节点watcher.teardown()
: 清空依赖vm.$off()
: 解绑监听destroyed
: 完成后触发钩子
上面是 vue 的声明周期的简单梳理,接下来我们直接以代码的形式来完成 vue 的初始化
左右居中方案
行内元素:
text-align: center
定宽块状元素: 左右
margin
值为auto
不定宽块状元素:
table
布局,position + transform
TCP 的可靠传输机制
TCP 的可靠传输机制是基于连续 ARQ 协议和滑动窗口协议的。
TCP 协议在发送方维持了一个发送窗口,发送窗口以前的报文段是已经发送并确认了的报文段,发送窗口中包含了已经发送但 未确认的报文段和允许发送但还未发送的报文段,发送窗口以后的报文段是缓存中还不允许发送的报文段。当发送方向接收方发 送报文时,会依次发送窗口内的所有报文段,并且设置一个定时器,这个定时器可以理解为是最早发送但未收到确认的报文段。 如果在定时器的时间内收到某一个报文段的确认回答,则滑动窗口,将窗口的首部向后滑动到确认报文段的后一个位置,此时如 果还有已发送但没有确认的报文段,则重新设置定时器,如果没有了则关闭定时器。如果定时器超时,则重新发送所有已经发送 但还未收到确认的报文段,并将超时的间隔设置为以前的两倍。当发送方收到接收方的三个冗余的确认应答后,这是一种指示, 说明该报文段以后的报文段很有可能发生丢失了,那么发送方会启用快速重传的机制,就是当前定时器结束前,发送所有的已发 送但确认的报文段。
接收方使用的是累计确认的机制,对于所有按序到达的报文段,接收方返回一个报文段的肯定回答。如果收到了一个乱序的报文 段,那么接方会直接丢弃,并返回一个最近的按序到达的报文段的肯定回答。使用累计确认保证了返回的确认号之前的报文段都 已经按序到达了,所以发送窗口可以移动到已确认报文段的后面。
发送窗口的大小是变化的,它是由接收窗口剩余大小和网络中拥塞程度来决定的,TCP 就是通过控制发送窗口的长度来控制报文 段的发送速率。
但是 TCP 协议并不完全和滑动窗口协议相同,因为许多的 TCP 实现会将失序的报文段给缓存起来,并且发生重传时,只会重 传一个报文段,因此 TCP 协议的可靠传输机制更像是窗口滑动协议和选择重传协议的一个混合体。
深入数组
一、梳理数组 API
1. Array.of
Array.of
用于将参数依次转化为数组中的一项,然后返回这个新数组,而不管这个参数是数字还是其他。它基本上与 Array 构造器功能一致,唯一的区别就在单个数字参数的处理上
2. Array.from
从语法上看,Array.from 拥有 3 个参数:
类似数组的对象,必选;
加工函数,新生成的数组会经过该函数的加工再返回;
this
作用域,表示加工函数执行时this
的值。
这三个参数里面第一个参数是必选的,后两个参数都是可选的。我们通过一段代码来看看它的用法。
除了上述
obj
对象以外,拥有迭代器的对象还包括String、Set、Map
等,Array.from
统统可以处理,请看下面的代码。
3. Array 的判断
在 ES5 提供该方法之前,我们至少有如下 5 种方式去判断一个变量是否为数组。
ES6 之后新增了一个
Array.isArray
方法,能直接判断数据类型是否为数组,但是如果 isArray 不存在,那么Array.isArray
的 polyfill 通常可以这样写:
4. 改变自身的方法
基于 ES6,会改变自身值的方法一共有
9
个,分别为pop、push、reverse、shift、sort、splice、unshift,以及两个 ES6 新增的方法 copyWithin 和 fill
5. 不改变自身的方法
基于 ES7,不会改变自身的方法也有 9
个,分别为 concat、join、slice、toString、toLocaleString、indexOf、lastIndexOf、未形成标准的 toSource,以及 ES7 新增的方法 includes
。
其中 includes 方法需要注意的是,如果元素中有 0,那么在判断过程中不论是 +0 还是 -0 都会判断为 True,这里的
includes 忽略了 +0 和 -0
6. 数组遍历的方法
基于 ES6,不会改变自身的遍历方法一共有 12 个,分别为 forEach、every、some、filter、map、reduce、reduceRight,以及 ES6 新增的方法 entries、find、findIndex、keys、values
7. 总结
这些方法之间存在很多共性,如下:
所有插入元素的方法,比如
push、unshift
一律返回数组新的长度;所有删除元素的方法,比如
pop、shift、splice
一律返回删除的元素,或者返回删除的多个元素组成的数组;部分遍历方法,比如
forEach、every、some、filter、map、find、findIndex
,它们都包含function(value,index,array){}
和thisArg
这样两个形参。
数组和字符串方法
二、理解 JS 的类数组
在 JavaScript 中有哪些情况下的对象是类数组呢?主要有以下几种
函数里面的参数对象
arguments
;用
getElementsByTagName/ClassName/Name
获得的HTMLCollection
用
querySelector
获得的NodeList
1. arguments 对象
arguments 对象是函数中传递的参数值的集合。它是一个类似数组的对象,因为它有一个
length
属性,我们可以使用数组索引表示法arguments[1]
来访问单个值,但它没有数组中的内置方法,如:forEach、reduce、filter 和 map。
这段代码比较容易,就是直接将这个函数的 arguments 在函数内部打印出来,那么我们看下这个 arguments 打印出来的结果,请看控制台的这张截图。
从结果中可以看到,
typeof
这个arguments
返回的是object
,通过Object.prototype.toString.call
返回的结果是'[object arguments]'
,可以看出来返回的不是'[object array]'
,说明arguments
和数组还是有区别的。
我们可以使用Array.prototype.slice
将arguments
对象转换成一个数组。
注意:箭头函数中没有 arguments 对象。
当我们调用函数 four 时,它会抛出一个 ReferenceError: arguments is not defined error。使用 rest 语法,可以解决这个问题。
这会自动将所有参数值放入数组中。
arguments 不仅仅有一个 length 属性,还有一个 callee 属性,我们接下来看看这个 callee 是干什么的,代码如下所示
从控制台可以看到,输出的就是函数自身,如果在函数内部直接执行调用
callee
的话,那它就会不停地执行当前函数,直到执行到内存溢出
2. HTMLCollection
HTMLCollection 简单来说是 HTML DOM 对象的一个接口,这个接口包含了获取到的 DOM 元素集合,返回的类型是类数组对象,如果用 typeof 来判断的话,它返回的是 'object'。它是及时更新的,当文档中的 DOM 变化时,它也会随之变化。
描述起来比较抽象,还是通过一段代码来看下 HTMLCollection
最后返回的是什么,我们先随便找一个页面中有 form 表单的页面,在控制台中执行下述代码
在这个有 form 表单的页面执行上面的代码,得到的结果如下。
可以看到,这里打印出来了页面第一个 form 表单元素,同时也打印出来了判断类型的结果,说明打印的判断的类型和 arguments 返回的也比较类似,typeof 返回的都是 'object',和上面的类似。
另外需要注意的一点就是 HTML DOM 中的 HTMLCollection 是即时更新的,当其所包含的文档结构发生改变时,它会自动更新。下面我们再看最后一个 NodeList 类数组。
3. NodeList
NodeList 对象是节点的集合,通常是由 querySlector 返回的。NodeList 不是一个数组,也是一种类数组。虽然 NodeList 不是一个数组,但是可以使用 for...of 来迭代。在一些情况下,NodeList 是一个实时集合,也就是说,如果文档中的节点树发生变化,NodeList 也会随之变化。我们还是利用代码来理解一下 Nodelist 这种类数组。
从上面的代码执行的结果中可以发现,我们是通过有 CheckBox 的页面执行的代码,在结果可中输出了一个 NodeList 类数组,里面有一个 CheckBox 元素,并且我们判断了它的类型,和上面的 arguments 与 HTMLCollection 其实是类似的,执行结果如下图所示。
4. 类数组应用场景
遍历参数操作
我们在函数内部可以直接获取 arguments
这个类数组的值,那么也可以对于参数进行一些操作,比如下面这段代码,我们可以将函数的参数默认进行求和操作。
定义链接字符串函数
我们可以通过 arguments 这个例子定义一个函数来连接字符串。这个函数唯一正式声明了的参数是一个字符串,该参数指定一个字符作为衔接点来连接字符串。该函数定义如下。
传递参数使用
5. 如何将类数组转换成数组
类数组借用数组方法转数组
ES6 的方法转数组
Array.from
和ES6 的展开运算符
,都可以把arguments
这个类数组转换成数组args
类数组和数组的异同点
在前端工作中,开发者往往会忽视对类数组的学习,其实在高级 JavaScript 编程中经常需要将类数组向数组转化,尤其是一些比较复杂的开源项目,经常会看到函数中处理参数的写法,例如:
[].slice.call(arguments)
这行代码。
三、实现数组扁平化的 6 种方式
1. 方法一:普通的递归实
普通的递归思路很容易理解,就是通过循环递归的方式,一项一项地去遍历,如果每一项还是一个数组,那么就继续往下遍历,利用递归程序的方法,来实现数组的每一项的连接。我们来看下这个方法是如何实现的,如下所示
从上面这段代码可以看出,最后返回的结果是扁平化的结果,这段代码核心就是循环遍历过程中的递归操作,就是在遍历过程中发现数组元素还是数组的时候进行递归操作,把数组的结果通过数组的 concat 方法拼接到最后要返回的 result 数组上,那么最后输出的结果就是扁平化后的数组
2. 方法二:利用 reduce 函数迭代
从上面普通的递归函数中可以看出,其实就是对数组的每一项进行处理,那么我们其实也可以用 reduce
来实现数组的拼接,从而简化第一种方法的代码,改造后的代码如下所示。
3. 方法三:扩展运算符实现
这个方法的实现,采用了扩展运算符和 some 的方法,两者共同使用,达到数组扁平化的目的,还是来看一下代码
从执行的结果中可以发现,我们先用数组的 some 方法把数组中仍然是组数的项过滤出来,然后执行 concat 操作,利用 ES6 的展开运算符,将其拼接到原数组中,最后返回原数组,达到了预期的效果。
前三种实现数组扁平化的方式其实是最基本的思路,都是通过最普通递归思路衍生的方法,尤其是前两种实现方法比较类似。值得注意的是 reduce 方法,它可以在很多应用场景中实现,由于 reduce 这个方法提供的几个参数比较灵活,能解决很多问题,所以是值得熟练使用并且精通的
4. 方法四:split 和 toString 共同处理
我们也可以通过 split 和 toString 两个方法,来共同实现数组扁平化,由于数组会默认带一个 toString 的方法,所以可以把数组直接转换成逗号分隔的字符串,然后再用 split 方法把字符串重新转换为数组,如下面的代码所示。
通过这两个方法可以将多维数组直接转换成逗号连接的字符串,然后再重新分隔成数组,你可以在控制台执行一下查看结果。
5. 方法五:调用 ES6 中的 flat
我们还可以直接调用 ES6 中的 flat 方法,可以直接实现数组扁平化。先来看下 flat 方法的语法:
其中 depth 是 flat 的参数,depth 是可以传递数组的展开深度(默认不填、数值是 1),即展开一层数组。那么如果多层的该怎么处理呢?参数也可以传进 Infinity,代表不论多少层都要展开。那么我们来看下,用 flat 方法怎么实现,请看下面的代码。
可以看出,一个嵌套了两层的数组,通过将
flat
方法的参数设置为Infinity
,达到了我们预期的效果。其实同样也可以设置成 2,也能实现这样的效果。因此,你在编程过程中,发现对数组的嵌套层数不确定的时候,最好直接使用
Infinity
,可以达到扁平化。下面我们再来看最后一种场景
6. 方法六:正则和 JSON 方法共同处理
我们在第四种方法中已经尝试了用 toString 方法,其中仍然采用了将 JSON.stringify 的方法先转换为字符串,然后通过正则表达式过滤掉字符串中的数组的方括号,最后再利用 JSON.parse 把它转换成数组。请看下面的代码
可以看到,其中先把传入的数组转换成字符串,然后通过正则表达式的方式把括号过滤掉,这部分正则的表达式你不太理解的话,可以看看下面的图片
通过这个在线网站 https://regexper.com/ 可以把正则分析成容易理解的可视化的逻辑脑图。其中我们可以看到,匹配规则是:全局匹配(g)左括号或者右括号,将它们替换成空格,最后返回处理后的结果。之后拿着正则处理好的结果重新在外层包裹括号,最后通过 JSON.parse 转换成数组返回。
四、如何用 JS 实现各种数组排序
数据结构算法中排序有很多种,常见的、不常见的,至少包含十种以上。根据它们的特性,可以大致分为两种类型:比较类排序和非比较类排序。
比较类排序 :通过比较来决定元素间的相对次序,其时间复杂度不能突破
O(nlogn)
,因此也称为非线性时间比较类排序。非比较类排序 :不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。
我们通过一张图片来看看这两种分类方式分别包括哪些排序方法。
非比较类的排序在实际情况中用的比较少
1. 冒泡排序
冒泡排序是最基础的排序,一般在最开始学习数据结构的时候就会接触它。冒泡排序是一次比较两个元素,如果顺序是错误的就把它们交换过来。走访数列的工作会重复地进行,直到不需要再交换,也就是说该数列已经排序完成。请看下面的代码。
从上面这段代码可以看出,最后返回的是排好序的结果。因为冒泡排序实在太基础和简单,这里就不过多赘述了。下面我们来看看快速排序法
2. 快速排序
快速排序的基本思想是通过一趟排序,将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可以分别对这两部分记录继续进行排序,以达到整个序列有序。
上面的代码在控制台执行之后,也可以得到预期的结果。最主要的思路是从数列中挑出一个元素,称为 “基准”(pivot);然后重新排序数列,所有元素比基准值小的摆放在基准前面、比基准值大的摆在基准的后面;在这个区分搞定之后,该基准就处于数列的中间位置;然后把小于基准值元素的子数列(left)和大于基准值元素的子数列(right)递归地调用 quick 方法排序完成,这就是快排的思路。
3. 插入排序
插入排序算法描述的是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入,从而达到排序的效果
。来看一下代码
从执行的结果中可以发现,通过插入排序这种方式实现了排序效果。插入排序的思路是基于数组本身进行调整的,首先循环遍历从 i 等于 1 开始,拿到当前的 current 的值,去和前面的值比较,如果前面的大于当前的值,就把前面的值和当前的那个值进行交换,通过这样不断循环达到了排序的目的
4. 选择排序
选择排序是一种简单直观的排序算法。它的工作原理是,首先将最小的元素存放在序列的起始位置,再从剩余未排序元素中继续寻找最小元素,然后放到已排序的序列后面……以此类推,直到所有元素均排序完毕
。请看下面的代码。
这样,通过选择排序的方法同样也可以实现数组的排序,从上面的代码中可以看出该排序是表现最稳定的排序算法之一,因为无论什么数据进去都是 O(n 平方) 的时间复杂度,所以用到它的时候,数据规模越小越好
5. 堆排序
堆排序是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质,即子结点的键值或索引总是小于(或者大于)它的父节点。堆的底层实际上就是一棵完全二叉树,可以用数组实现。
根节点最大的堆叫作大根堆,根节点最小的堆叫作小根堆,你可以根据从大到小排序或者从小到大来排序,分别建立对应的堆就可以。请看下面的代码
从代码来看,堆排序相比上面几种排序整体上会复杂一些,不太容易理解。不过你应该知道两点:
一是堆排序最核心的点就在于排序前先建堆;
二是由于堆其实就是完全二叉树,如果父节点的序号为 n,那么叶子节点的序号就分别是
2n
和2n+1
。
你理解了这两点,再看代码就比较好理解了。堆排序最后有两个循环:第一个是处理父节点的顺序;第二个循环则是根据父节点和叶子节点的大小对比,进行堆的调整。通过这两轮循环的调整,最后堆排序完成。
6. 归并排序
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。我们先看一下代码。
从上面这段代码中可以看到,通过归并排序可以得到想要的结果。上面提到了分治的思路,你可以从 mergeSort 方法中看到,通过 mid 可以把该数组分成左右两个数组,分别对这两个进行递归调用排序方法,最后将两个数组按照顺序归并起来。
归并排序是一种稳定的排序方法,和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好得多,因为始终都是 O(nlogn) 的时间复杂度。而代价是需要额外的内存空间。
其中你可以看到排序相关的时间复杂度和空间复杂度以及稳定性的情况,如果遇到需要自己实现排序的时候,可以根据它们的空间和时间复杂度综合考量,选择最适合的排序方法
左边定宽,右边自适应方案
float + margin,float + calc
数字证书是什么?
现在的方法也不一定是安全的,因为没有办法确定得到的公钥就一定是安全的公钥。可能存在一个中间人,截取了对方发给我们的公钥,然后将他自己的公钥发送给我们,当我们使用他的公钥加密后发送的信息,就可以被他用自己的私钥解密。然后他伪装成我们以同样的方法向对方发送信息,这样我们的信息就被窃取了,然而自己还不知道。为了解决这样的问题,可以使用数字证书。
首先使用一种 Hash 算法来对公钥和其他信息进行加密,生成一个信息摘要,然后让有公信力的认证中心(简称 CA )用它的私钥对消息摘要加密,形成签名。最后将原始的信息和签名合在一起,称为数字证书。当接收方收到数字证书的时候,先根据原始信息使用同样的 Hash 算法生成一个摘要,然后使用公证处的公钥来对数字证书中的摘要进行解密,最后将解密的摘要和生成的摘要进行对比,就能发现得到的信息是否被更改了。
这个方法最要的是认证中心的可靠性,一般浏览器里会内置一些顶层的认证中心的证书,相当于我们自动信任了他们,只有这样才能保证数据的安全。
参考:前端进阶面试题详细解答
选择器权重计算方式
!important > 内联样式 = 外联样式 > ID 选择器 > 类选择器 = 伪类选择器 = 属性选择器 > 元素选择器 = 伪元素选择器 > 通配选择器 = 后代选择器 = 兄弟选择器
属性后面加
!import
会覆盖页面内任何位置定义的元素样式作为
style
属性写在元素内的样式id
选择器类选择器
标签选择器
通配符选择器(
*
)浏览器自定义或继承
同一级别:后写的会覆盖先写的
css 选择器的解析原则:选择器定位 DOM 元素是从右往左的方向,这样可以尽早的过滤掉一些不必要的样式规则和元素
渲染引擎什么情况下才会为特定的节点创建新的图层
层叠上下文
是 HTML 元素的三维概念,这些 HTML 元素在一条假想的相对于面向(电脑屏幕的)视窗或者网页的用户的 z 轴上延伸,HTML 元素依据其自身属性按照优先级顺序占用层叠上下文的空间。
拥有层叠上下文属性的元素会被提升为单独的一层。
拥有层叠上下文属性:
根元素 (HTML),
z-index 值不为 "auto"的 绝对/相对定位元素,
position,固定(fixed) / 沾滞(sticky)定位(沾滞定位适配所有移动设备上的浏览器,但老的桌面浏览器不支持)
z-index 值不为 "auto"的 flex 子项 (flex item),即:父元素 display: flex|inline-flex,
z-index 值不为"auto"的 grid 子项,即:父元素 display:grid
opacity 属性值小于 1 的元素(参考 the specification for opacity),
transform 属性值不为 "none"的元素,
mix-blend-mode 属性值不为 "normal"的元素,
filter 值不为"none"的元素,
perspective 值不为"none"的元素,
clip-path 值不为"none"的元素
mask / mask-image / mask-border 不为"none"的元素
isolation 属性被设置为 "isolate"的元素
在 will-change 中指定了任意 CSS 属性(参考 这篇文章)
-webkit-overflow-scrolling 属性被设置 "touch"的元素
contain 属性值为"layout","paint",或者综合值比如"strict","content"
需要剪裁(clip)的地方也会被创建为图层。
这里的剪裁指的是,假如我们把 div 的大小限定为 200 * 200 像素,而 div 里面的文字内容比较多,文字所显示的区域肯定会超出 200 * 200 的面积,这时候就产生了剪裁,渲染引擎会把裁剪文字内容的一部分用于显示在 div 区域。出现这种裁剪情况的时候,渲染引擎会为文字部分单独创建一个层,如果出现滚动条,滚动条也会被提升为单独的层。
webpack 层面如何做性能优化
优化前的准备工作
准备基于时间的分析工具:我们需要一类插件,来帮助我们统计项目构建过程中在编译阶段的耗时情况。
speed-measure-webpack-plugin
分析插件加载的时间使用
webpack-bundle-analyzer
分析产物内容
代码优化:
无用代码消除,是许多编程语言都具有的优化手段,这个过程称为 DCE (dead code elimination),即 删除不可能执行的代码;
例如我们的 UglifyJs
,它就会帮我们在生产环境中删除不可能被执行的代码,例如:
摇树优化 (Tree-shaking),这是一种形象比喻。我们把打包后的代码比喻成一棵树,这里其实表示的就是,通过工具 "摇" 我们打包后的 js 代码,将没有使用到的无用代码 "摇" 下来 (删除)。即 消除那些被 引用了但未被使用 的模块代码。
原理: 由于是在编译时优化,因此最基本的前提就是语法的静态分析,ES6 的模块机制 提供了这种可能性。不需要运行时,便可进行代码字面上的静态分析,确定相应的依赖关系。
问题: 具有 副作用 的函数无法被
tree-shaking
在引用一些第三方库,需要去观察其引入的代码量是不是符合预期;
尽量写纯函数,减少函数的副作用;
可使用
webpack-deep-scope-plugin
,可以进行作用域分析,减少此类情况的发生,但仍需要注意;
code-spliting: 代码分割技术 ,将代码分割成多份进行 懒加载 或 异步加载,避免打包成一份后导致体积过大,影响页面的首屏加载;
Webpack
中使用SplitChunksPlugin
进行拆分;按 页面 拆分: 不同页面打包成不同的文件;
按 功能 拆分:
将类似于播放器,计算库等大模块进行拆分后再懒加载引入;
提取复用的业务代码,减少冗余代码;
按 文件修改频率 拆分: 将第三方库等不常修改的代码单独打包,而且不改变其文件 hash 值,能最大化运用浏览器的缓存;
scope hoisting : 作用域提升,将分散的模块划分到同一个作用域中,避免了代码的重复引入,有效减少打包后的代码体积和运行时的内存损耗;
编译性能优化:
升级至 最新 版本的
webpack
,能有效提升编译性能;使用
dev-server
/ 模块热替换 (HMR
) 提升开发体验;监听文件变动 忽略 node_modules 目录能有效提高监听时的编译效率;
缩小编译范围
modules
: 指定模块路径,减少递归搜索;mainFields
: 指定入口文件描述字段,减少搜索;noParse
: 避免对非模块化文件的加载;includes/exclude
: 指定搜索范围/排除不必要的搜索范围;alias
: 缓存目录,避免重复寻址;babel-loader
忽略
node_moudles
,避免编译第三方库中已经被编译过的代码使用
cacheDirectory
,可以缓存编译结果,避免多次重复编译多进程并发
webpack-parallel-uglify-plugin
: 可多进程并发压缩 js 文件,提高压缩速度;HappyPack
: 多进程并发文件的Loader
解析;第三方库模块缓存:
DLLPlugin
和DLLReferencePlugin
可以提前进行打包并缓存,避免每次都重新编译;使用分析
Webpack Analyse / webpack-bundle-analyzer
对打包后的文件进行分析,寻找可优化的地方配置 profile:true,对各个编译阶段耗时进行监控,寻找耗时最多的地方
source-map
:开发:
cheap-module-eval-source-map
生产:
hidden-source-map
;
优化 webpack 打包速度
减少文件搜索范围
比如通过别名
loader
的test
,include & exclude
Webpack4
默认压缩并行Happypack
并发调用babel
也可以缓存编译Resolve
在构建时指定查找模块文件的规则使用
DllPlugin
,不用每次都重新构建externals
和DllPlugin
解决的是同一类问题:将依赖的框架等模块从构建过程中移除。它们的区别在于在 Webpack 的配置方面,
externals
更简单,而DllPlugin
需要独立的配置文件。DllPlugin
包含了依赖包的独立构建流程,而externals
配置中不包含依赖框架的生成方式,通常使用已传入 CDN 的依赖包externals
配置的依赖包需要单独指定依赖模块的加载方式:全局对象、CommonJS、AMD 等在引用依赖包的子模块时,
DllPlugin
无须更改,而externals
则会将子模块打入项目包中
优化打包体积
提取第三方库或通过引用外部文件的方式引入第三方库
代码压缩插件
UglifyJsPlugin
服务器启用
gzip
压缩按需加载资源文件
require.ensure
优化
devtool
中的source-map
剥离
css
文件,单独打包去除不必要插件,通常就是开发环境与生产环境用同一套配置文件导致
Tree Shaking
在构建打包过程中,移除那些引入但未被使用的无效代码开启
scope hosting
体积更小
创建函数作用域更小
代码可读性更好
createElement 过程
React.createElement(): 根据指定的第一个参数创建一个 React 元素
第一个参数是必填,传入的是似 HTML 标签名称,eg: ul, li
第二个参数是选填,表示的是属性,eg: className
第三个参数是选填, 子节点,eg: 要显示的文本内容
闭包
闭包其实就是一个可以访问其他函数内部变量的函数。创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以 访问到当前函数的局部变量。
因为通常情况下,函数内部变量是无法在外部访问的(即全局变量和局部变量的区别),因此使用闭包的作用,就具备实现了能在外部访问某个函数内部变量的功能,让这些内部变量的值始终可以保存在内存中。下面我们通过代码先来看一个简单的例子
闭包有两个常用的用途
闭包的第一个用途是使我们在函数外部能够访问到函数内部的变量。通过使用闭包,我们可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
函数的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。
其实闭包的本质就是作用域链的一个特殊的应用,只要了解了作用域链的创建过程,就能够理解闭包的实现原理。
大家都知道闭包其中一个作用是访问私有变量,就比如上述代码中的 fn2 访问到了 fn1 函数中的变量 a。但是此时 fn1 早已销毁,我们是如何访问到变量 a 的呢?不是都说原始类型是存放在栈上的么,为什么此时却没有被销毁掉?
接下来笔者会根据浏览器的表现来重新理解关于原始类型存放位置的说法。
先来说下数据存放的正确规则是:局部、占用空间确定的数据,一般会存放在栈中,否则就在堆中(也有例外)。 那么接下来我们可以通过 Chrome 来帮助我们验证这个说法说法。
上图中画红框的位置我们能看到一个内部的对象
[[Scopes]]
,其中存放着变量 a,该对象是被存放在堆上的,其中包含了闭包、全局对象等等内容,因此我们能通过闭包访问到本该销毁的变量。
另外最开始我们对于闭包的定位是:假如一个函数能访问外部的变量,那么这个函数它就是一个闭包,因此接下来我们看看在全局下的表现是怎么样的。
从上图我们能发现全局下声明的变量,如果是 var 的话就直接被挂到 globe 上,如果是其他关键字声明的话就被挂到 Script 上。虽然这些内容同样还是存在 [[Scopes]]
,但是全局变量应该是存放在静态区域的,因为全局变量无需进行垃圾回收,等需要回收的时候整个应用都没了。
只有在下图的场景中,原始类型才可能是被存储在栈上。
这里为什么要说可能,是因为 JS 是门动态类型语言,一个变量声明时可以是原始类型,马上又可以赋值为对象类型,然后又回到原始类型。这样频繁的在堆栈上切换存储位置,内部引擎是不是也会有什么优化手段,或者干脆全部都丢堆上?只有 const 声明的原始类型才一定存在栈上?当然这只是笔者的一个推测,暂时没有深究,读者可以忽略这段瞎想
因此笔者对于原始类型存储位置的理解为:局部变量才是被存储在栈上,全局变量存在静态区域上,其它都存储在堆上。
当然这个理解是建立的 Chrome 的表现之上的,在不同的浏览器上因为引擎的不同,可能存储的方式还是有所变化的。
闭包产生的原因
我们在前面介绍了作用域的概念,那么你还需要明白作用域链的基本概念。其实很简单,当访问一个变量时,代码解释器会首先在当前的作用域查找,如果没找到,就去父级作用域去查找,直到找到该变量或者不存在父级作用域中,这样的链路就是作用域链
需要注意的是,每一个子函数都会拷贝上级的作用域,形成一个作用域的链条。那么我们还是通过下面的代码来详细说明一下作用域链
从中可以看出,fun1 函数的作用域指向全局作用域(window)和它自己本身;fun2 函数的作用域指向全局作用域 (window)、fun1 和它本身;而作用域是从最底层向上找,直到找到全局作用域 window 为止,如果全局还没有的话就会报错。
那么这就很形象地说明了什么是作用域链,即当前函数一般都会存在上层函数的作用域的引用,那么他们就形成了一条作用域链。
由此可见,
闭包产生的本质就是:当前环境中存在指向父级作用域的引用
。那么还是拿上的代码举例。
从上面这段代码可以看出,这里 result 会拿到父级作用域中的变量,输出 2。因为在当前环境中,含有对 fun2 函数的引用,fun2 函数恰恰引用了 window、fun1 和 fun2 的作用域。因此 fun2 函数是可以访问到 fun1 函数的作用域的变量。
那是不是只有返回函数才算是产生了闭包呢?其实也不是,回到闭包的本质,我们只需要让父级作用域的引用存在即可,因此还可以这么改代码,如下所示
可以看出,其中实现的结果和前一段代码的效果其实是一样的,就是在给 fun3 函数赋值后,fun3 函数就拥有了 window、fun1 和 fun3 本身这几个作用域的访问权限;然后还是从下往上查找,直到找到 fun1 的作用域中存在 a 这个变量;因此输出的结果还是 2,最后产生了闭包,形式变了,本质没有改变。
因此最后返回的不管是不是函数,也都不能说明没有产生闭包
闭包的表现形式
返回一个函数
在定时器、事件监听、Ajax 请求、Web Workers 或者任何异步中,只要使用了回调函数,实际上就是在使用闭包
。请看下面这段代码,这些都是平常开发中用到的形式
作为函数参数传递的形式,比如下面的例子。
IIFE(立即执行函数),创建了闭包,保存了全局作用域(window)和当前函数的作用域
,因此可以输出全局的变量,如下所示
IIFE 这个函数会稍微有些特殊,算是一种自执行匿名函数,这个匿名函数拥有独立的作用域。这不仅可以避免了外界访问此 IIFE 中的变量,而且又不会污染全局作用域,我们经常能在高级的 JavaScript 编程中看见此类函数。
如何解决循环输出问题?
在互联网大厂的面试中,解决循环输出问题是比较高频的面试题,一般都会给一段这样的代码让你来解释
上面这段代码执行之后,从控制台执行的结果可以看出来,结果输出的是 5 个 6,那么一般面试官都会先问为什么都是 6?我想让你实现输出 1、2、3、4、5 的话怎么办呢?
因此结合本讲所学的知识我们来思考一下,应该怎么给面试官一个满意的解释。你可以围绕这两点来回答。
setTimeout
为宏任务,由于 JS 中单线程eventLoop 机制
,在主线程同步任务执行完后才去执行宏任务,因此循环结束后 setTimeout 中的回调才依次执行
因为
setTimeout
函数也是一种闭包,往上找它的父级作用域链就是 window
,变量 i 为 window 上的全局变量
,开始执行 setTimeout 之前变量 i 已经就是 6 了,因此最后输出的连续就都是 6。
那么我们再来看看如何按顺序依次输出 1、2、3、4、5 呢?
利用 IIFE
可以利用 IIFE(立即执行函数),当每次 for 循环时,把此时的变量 i 传递到定时器中,然后执行,改造之后的代码如下。
使用 ES6 中的 let
ES6 中新增的 let 定义变量的方式,使得 ES6 之后 JS 发生革命性的变化,让 JS 有了块级作用域,代码的作用域以块级为单位进行执行。通过改造后的代码,可以实现上面想要的结果。
定时器传入第三个参数
setTimeout 作为经常使用的定时器,它是存在第三个参数的,日常工作中我们经常使用的一般是前两个,一个是回调函数,另外一个是时间,而第三个参数用得比较少。那么结合第三个参数,调整完之后的代码如下。
从中可以看到,第三个参数的传递,可以改变 setTimeout 的执行逻辑,从而实现我们想要的结果,这也是一种解决循环输出问题的途径
常见考点
闭包能考的很多,概念和笔试题都会考。
概念题就是考考闭包是什么了。
笔试题的话基本都会结合上异步,比如最常见的:
这道题会问输出什么,有哪几种方式可以得到想要的答案?
为什么 udp 不会粘包?
TCP 协议是⾯向流的协议,UDP 是⾯向消息的协议。UDP 段都是⼀条消息,应⽤程序必须以消息为单位提取数据,不能⼀次提取任意字节的数据
UDP 具有保护消息边界,在每个 UDP 包中就有了消息头(消息来源地址,端⼝等信息),这样对于接收端来说就容易进⾏区分处理了。传输协议把数据当作⼀条独⽴的消息在⽹上传输,接收端只能接收独⽴的消息。接收端⼀次只能接收发送端发出的⼀个数据包,如果⼀次接受数据的⼤⼩⼩于发送端⼀次发送的数据⼤⼩,就会丢失⼀部分数据,即使丢失,接受端也不会分两次去接收。
垃圾回收
对于在 JavaScript 中的字符串,对象,数组是没有固定大小的,只有当对他们进行动态分配存储时,解释器就会分配内存来存储这些数据,当 JavaScript 的解释器消耗完系统中所有可用的内存时,就会造成系统崩溃。
内存泄漏,在某些情况下,不再使用到的变量所占用内存没有及时释放,导致程序运行中,内存越占越大,极端情况下可以导致系统崩溃,服务器宕机。
JavaScript 有自己的一套垃圾回收机制,JavaScript 的解释器可以检测到什么时候程序不再使用这个对象了(数据),就会把它所占用的内存释放掉。
针对 JavaScript 的来及回收机制有以下两种方法(常用):标记清除,引用计数
标记清除
v8 的垃圾回收机制基于分代回收机制,这个机制又基于世代假说,这个假说有两个特点,一是新生的对象容易早死,另一个是不死的对象会活得更久。基于这个假说,v8 引擎将内存分为了新生代和老生代。
新创建的对象或者只经历过一次的垃圾回收的对象被称为新生代。经历过多次垃圾回收的对象被称为老生代。
新生代被分为 From 和 To 两个空间,To 一般是闲置的。当 From 空间满了的时候会执行 Scavenge 算法进行垃圾回收。当我们执行垃圾回收算法的时候应用逻辑将会停止,等垃圾回收结束后再继续执行。
这个算法分为三步:
首先检查 From 空间的存活对象,如果对象存活则判断对象是否满足晋升到老生代的条件,如果满足条件则晋升到老生代。如果不满足条件则移动 To 空间。
如果对象不存活,则释放对象的空间。
最后将 From 空间和 To 空间角色进行交换。
新生代对象晋升到老生代有两个条件:
第一个是判断是对象否已经经过一次 Scavenge 回收。若经历过,则将对象从 From 空间复制到老生代中;若没有经历,则复制到 To 空间。
第二个是 To 空间的内存使用占比是否超过限制。当对象从 From 空间复制到 To 空间时,若 To 空间使用超过 25%,则对象直接晋升到老生代中。设置 25% 的原因主要是因为算法结束后,两个空间结束后会交换位置,如果 To 空间的内存太小,会影响后续的内存分配。
老生代采用了标记清除法和标记压缩法。标记清除法首先会对内存中存活的对象进行标记,标记结束后清除掉那些没有标记的对象。由于标记清除后会造成很多的内存碎片,不便于后面的内存分配。所以了解决内存碎片的问题引入了标记压缩法。
由于在进行垃圾回收的时候会暂停应用的逻辑,对于新生代方法由于内存小,每次停顿的时间不会太长,但对于老生代来说每次垃圾回收的时间长,停顿会造成很大的影响。 为了解决这个问题 V8 引入了增量标记的方法,将一次停顿进行的过程分为了多步,每次执行完一小步就让运行逻辑执行一会,就这样交替运行
React Hooks
代码逻辑聚合,逻辑复用
HOC 嵌套地狱
代替 class
React 中通常使用 类定义 或者 函数定义 创建组件:
在类定义中,我们可以使用到许多 React 特性,例如 state、 各种组件生命周期钩子等,但是在函数定义中,我们却无能为力,因此 React 16.8 版本推出了一个新功能 (React Hooks),通过它,可以更好的在函数定义组件中使用 React 特性。
函数组件与类组件的对比:无关“优劣”,只谈“不同”
类组件需要继承 class,函数组件不需要;
类组件可以访问生命周期方法,函数组件不能;
类组件中可以获取到实例化后的 this,并基于这个 this 做各种各样的事情,而函数组件不可以;
类组件中可以定义并维护 state(状态),而函数组件不可以;
但是类组件它太重了,对于解决许多问题来说,编写一个类组件实在是一个过于复杂的姿势。复杂的姿势必然带来高昂的理解成本,这也是我们所不想看到的
react hooks 的好处:
跨组件复用: 其实 render props / HOC 也是为了复用,相比于它们,Hooks 作为官方的底层 API,最为轻量,而且改造成本小,不会影响原来的组件层次结构和传说中的嵌套地狱;
类定义更为复杂
不同的生命周期会使逻辑变得分散且混乱,不易维护和管理;
时刻需要关注 this 的指向问题;
代码复用代价高,高阶组件的使用经常会使整个组件树变得臃肿;
状态与 UI 隔离: 正是由于 Hooks 的特性,状态逻辑会变成更小的粒度,并且极容易被抽象成一个自定义 Hooks,组件中的状态和 UI 变得更为清晰和隔离。
注意:
避免在 循环/条件判断/嵌套函数 中调用 hooks,保证调用顺序的稳定;
只有 函数定义组件 和 hooks 可以调用 hooks,避免在 类组件 或者 普通函数 中调用;
不能在 useEffect 中使用 useState,React 会报错提示;
类组件不会被替换或废弃,不需要强制改造类组件,两种方式能并存;
重要钩子
状态钩子 (useState): 用于定义组件的 State,其到类定义中 this.state 的功能;
生命周期钩子 (useEffect):
类定义中有许多生命周期函数,而在 React Hooks 中也提供了一个相应的函数 (useEffect),这里可以看做 componentDidMount、componentDidUpdate 和 componentWillUnmount 的结合。
useEffect(callback, [source])接受两个参数
callback: 钩子回调函数;
source: 设置触发条件,仅当 source 发生改变时才会触发;
useEffect 钩子在没有传入[source]参数时,默认在每次 render 时都会优先调用上次保存的回调中返回的函数,后再重新调用回调;
通过第二个参数,我们便可模拟出几个常用的生命周期:
componentDidMount: 传入[]时,就只会在初始化时调用一次
componentWillUnmount: 传入[],回调中的返回的函数也只会被最终执行一次
mounted: 可以使用 useState 封装成一个高度可复用的 mounted 状态;
componentDidUpdate: useEffect 每次均会执行,其实就是排除了 DidMount 后即可;
其它内置钩子:
useContext
: 获取 context 对象useReducer
: 类似于 Redux 思想的实现,但其并不足以替代 Redux,可以理解成一个组件内部的 redux:并不是持久化存储,会随着组件被销毁而销毁;
属于组件内部,各个组件是相互隔离的,单纯用它并无法共享数据;
配合 useContext`的全局性,可以完成一个轻量级的 Redux;(easy-peasy)
useCallback
: 缓存回调函数,避免传入的回调每次都是新的函数实例而导致依赖组件重新渲染,具有性能优化的效果;useMemo
: 用于缓存传入的 props,避免依赖的组件每次都重新渲染;useRef
: 获取组件的真实节点;useLayoutEffect
DOM 更新同步钩子。用法与 useEffect 类似,只是区别于执行时间点的不同
useEffect 属于异步执行,并不会等待 DOM 真正渲染后执行,而 useLayoutEffect 则会真正渲染后才触发;
可以获取更新后的 state;
自定义钩子(useXxxxx): 基于 Hooks 可以引用其它 Hooks 这个特性,我们可以编写自定义钩子,如上面的 useMounted。又例如,我们需要每个页面自定义标题:
React Hooks 的限制
不要在
循环、条件
或嵌套函数中调用 Hook
;在 React 的函数组件中调用
Hook
那为什么会有这样的限制呢?就得从 Hooks 的设计说起。Hooks 的设计初衷是为了改进 React 组件的开发模式。在旧有的开发模式下遇到了三个问题。
组件之间难以复用状态逻辑。过去常见的解决方案是高阶组件、
render props
及状态管理框架。复杂的组件变得难以理解。生命周期函数与业务逻辑耦合太深,导致关联部分难以拆分。
常见的有 this 的问题,但在 React 团队中还有类难以优化的问题,他们希望在编译优化层面做出一些改进。
这三个问题在一定程度上阻碍了 React 的后续发展,所以为了解决这三个问题,Hooks 基于函数组件开始设计。然而第三个问题决定了 Hooks 只支持函数组件。
那为什么不要在循环、条件或嵌套函数中调用 Hook 呢?因为 Hooks 的设计是基于数组实现
。在调用时按顺序加入数组中
,如果使用循环、条件或嵌套函数很有可能导致数组取值错位,执行错误的 Hook。当然,实质上 React 的源码里不是数组,是链表
。
这些限制会在编码上造成一定程度的心智负担,新手可能会写错,为了避免这样的情况,可以引入 ESLint 的 Hooks 检查插件进行预防。
useEffect 与 useLayoutEffect 区别在哪里
它们的共同点很简单,底层的函数签名是完全一致的,都是调用的
mountEffectImpl
,在使用上也没什么差异,基本可以直接替换,也都是用于处理副作用。那不同点就很大了,
useEffect
在 React 的渲染过程中是被异步调用的,用于绝大多数场景,而LayoutEffect
会在所有的 DOM 变更之后同步调用,主要用于处理 DOM 操作、调整样式、避免页面闪烁等问题。也正因为是同步处理,所以需要避免在LayoutEffect
做计算量较大的耗时任务从而造成阻塞。在未来的趋势上,两个 API 是会长期共存的,暂时没有删减合并的计划,需要开发者根据场景去自行选择。React 团队的建议非常实用,如果实在分不清,先用
useEffect
,一般问题不大;如果页面有异常,再直接替换为useLayoutEffect
即可。
浏览器存储
我们经常需要对业务中的一些数据进行存储,通常可以分为 短暂性存储 和 持久性储存。
短暂性的时候,我们只需要将数据存在内存中,只在运行时可用
持久性存储,可以分为 浏览器端 与 服务器端
浏览器:
cookie
: 通常用于存储用户身份,登录状态等http
中自动携带, 体积上限为4K
, 可自行设置过期时间localStorage / sessionStorage
: 长久储存/窗口关闭删除, 体积限制为4~5M
indexDB
服务器:
分布式缓存
redis
数据库
cookie 和 localSrorage、session、indexDB 的区别
从上表可以看到,
cookie
已经不建议用于存储。如果没有大量数据存储需求的话,可以使用localStorage
和sessionStorage
。对于不怎么改变的数据尽量使用localStorage
存储,否则可以用sessionStorage
存储。
对于 cookie
,我们还需要注意安全性
Name
,即该Cookie
的名称。Cookie
一旦创建,名称便不可更改。Value
,即该Cookie
的值。如果值为Unicode
字符,需要为字符编码。如果值为二进制数据,则需要使用BASE64
编码。Max Age
,即该Cookie
失效的时间,单位秒,也常和Expires
一起使用,通过它可以计算出其有效时间。Max Age
如果为正数,则该Cookie
在Max Age
秒之后失效。如果为负数,则关闭浏览器时Cookie
即失效,浏览器也不会以任何形式保存该Cookie
。Path
,即该Cookie
的使用路径。如果设置为/path/
,则只有路径为/path/
的页面可以访问该Cookie
。如果设置为/
,则本域名下的所有页面都可以访问该Cookie
。Domain
,即可以访问该Cookie
的域名。例如如果设置为.zhihu.com
,则所有以zhihu.com
,结尾的域名都可以访问该Cookie
。Size
字段,即此Cookie
的大小。Http
字段,即Cookie
的httponly
属性。若此属性为true
,则只有在HTTP Headers
中会带有此 Cookie 的信息,而不能通过document.cookie
来访问此 Cookie。Secure
,即该Cookie
是否仅被使用安全协议传输。安全协议。安全协议有HTTPS、SSL
等,在网络上传输数据之前先将数据加密。默认为false
。
Vue 响应式原理
Vue 的响应式原理是核心是通过 ES5 的保护对象的
Object.defindeProperty
中的访问器属性中的 get 和 set 方法,data 中声明的属性都被添加了访问器属性,当读取 data 中的数据时自动调用 get 方法,当修改 data 中的数据时,自动调用 set 方法,检测到数据的变化,会通知观察者 Wacher,观察者 Wacher 自动触发重新 render 当前组件(子组件不会重新渲染),生成新的虚拟 DOM 树,Vue 框架会遍历并对比新虚拟 DOM 树和旧虚拟 DOM 树中每个节点的差别,并记录下来,最后,加载操作,将所有记录的不同点,局部修改到真实 DOM 树上。
虚拟 DOM (Virtaul DOM): 用 js 对象模拟的,保存当前视图内所有 DOM 节点对象基本描述属性和节点间关系的树结构。用 js 对象,描述每个节点,及其父子关系,形成虚拟 DOM 对象树结构。
因为只要在
data
中声明的基本数据类型的数据,基本不存在数据不响应问题,所以重点介绍数组和对象在vue
中的数据响应问题,vue 可以检测对象属性的修改,但无法监听数组的所有变动及对象的新增和删除,只能使用数组变异方法及$set
方法。
可以看到,
arrayMethods
首先继承了Array
,然后对数组中所有能改变数组自身的方法,如push
、pop
等这些方法进行重写。重写后的方法会先执行它们本身原有的逻辑,并对能增加数组长度的 3 个方法push
、unshift
、splice
方法做了判断,获取到插入的值,然后把新添加的值变成一个响应式对象,并且再调用ob.dep.notify()
手动触发依赖通知,这就很好地解释了用vm.items.splice
(newLength
) 方法可以检测到变化
总结:Vue 采用数据劫持结合发布—订阅模式的方法,通过
Object.defineProperty()
来劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
Observer
遍历数据对象,给所有属性加上setter
和getter
,监听数据的变化compile
解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
Watcher
订阅者是Observer
和Compile
之间通信的桥梁,主要做的事情
在自身实例化时往属性订阅器 (
dep
) 里面添加自己待属性变动
dep.notice()
通知时,调用自身的update()
方法,并触发Compile
中绑定的回调
Object.defineProperty() ,那么它的用法是什么,以及优缺点是什么呢?
可以检测对象中数据发生的修改
对于复杂的对象,层级很深的话,是不友好的,需要经行深度监听,这样子就需要递归到底,这也是它的缺点。
对于一个对象中,如果你新增加属性,删除属性,**Object.defineProperty()**是不能观测到的,那么应该如何解决呢?可以通过
Vue.set()
和Vue.delete()
来实现。
Vue3.x 响应式数据原理
Vue3.x
改用Proxy
替代Object.defineProperty
。因为Proxy
可以直接监听对象和数组
的变化,并且有多达 13 种拦截方法。并且作为新标准将受到浏览器厂商重点持续的性能优化。
Proxy
只会代理对象的第一层,那么Vue3
又是怎样处理这个问题的呢?
判断当前
Reflect.get的
返回值是否为Object
,如果是则再通过reactive
方法做代理, 这样就实现了深度观测。
监测数组的时候可能触发多次 get/set,那么如何防止触发多次呢?
我们可以判断
key
是否为当前被代理对象target
自身属性,也可以判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行trigger
Proxy 相比于 defineProperty 的优势
数组变化也能监听到
不需要深度遍历监听
Proxy
是ES6
中新增的功能,可以用来自定义对象中的操作
总结
Vue
记录传入的选项,设置
$data/$el
把
data
的成员注入到Vue
实例负责调用
Observer
实现数据响应式处理(数据劫持)负责调用
Compiler
编译指令/插值表达式等Observer
数据劫持
负责把
data
中的成员转换成getter/setter
负责把多层属性转换成
getter/setter
如果给属性赋值为新对象,把新对象的成员设置为
getter/setter
添加
Dep
和Watcher
的依赖关系数据变化发送通知
Compiler
负责编译模板,解析指令/插值表达式
负责页面的首次渲染过程
当数据变化后重新渲染
Dep
收集依赖,添加订阅者(
watcher
)通知所有订阅者
Watcher
自身实例化的时候往
dep
对象中添加自己当数据变化
dep
通知所有的Watcher
实例更新视图
迭代查询与递归查询
实际上,DNS 解析是一个包含迭代查询和递归查询的过程。
递归查询指的是查询请求发出后,域名服务器代为向下一级域名服务器发出请求,最后向用户返回查询的最终结果。使用递归 查询,用户只需要发出一次查询请求。
迭代查询指的是查询请求后,域名服务器返回单次查询的结果。下一级的查询由用户自己请求。使用迭代查询,用户需要发出 多次的查询请求。
一般我们向本地 DNS 服务器发送请求的方式就是递归查询,因为我们只需要发出一次请求,然后本地 DNS 服务器返回给我 们最终的请求结果。而本地 DNS 服务器向其他域名服务器请求的过程是迭代查询的过程,因为每一次域名服务器只返回单次 查询的结果,下一级的查询由本地 DNS 服务器自己进行。
评论