guyuezhai / interviewsummary Goto Github PK
View Code? Open in Web Editor NEWsome summary of interview
some summary of interview
防抖函数的作用
控制函数在一定时间内执行的次数,防抖意味着N秒内函数只会执行一次,
如果N秒内再次触发,则重新计算延迟时间
const isUrl = urlStr => {
try {
const { href, origin, host, hostname, pathname } = new URL(urlStr)
return href && origin && host && hostname && pathname && true
} catch (e) {
return false
}
}
这个问题同样也需要先了解一下git仓库的三个组成部分:
三个区的转换关系以及转换所使用的命令:
git reset
、git revert
和git checkout
的共同点:用来撤销代码仓库中的某些更改。
不同点:
从commit层面说起:
git reset
可以将一个分支的末端指向之前的一个commit。然后下次git执行垃圾回收的时候,会把这个commit之后的commit都扔掉。git reset
还支持三种标记,用来标记reset指令影响的范围:
注意: 因为git reset是直接删除commit记录,从而会影响到其它开发人员的分支,所以不要在公共分支比如(develop)做这个操作
git checkout
可以将HEAD移到一个新的分支,并更新工作目录。因为可能会覆盖本地的修改,所以执行这个指令前,你需要stash
或者commit
暂存区和工作区的更改。git revert
和 git reset
的目的是一样,但做法不同,他会以创建新的commit的方式来撤销commit,这样能保留之前的commit历史,比较安全。另外,同样因为可能会覆盖本地的修改,所以执行这个指令前,你需要stash或者commit暂存区和工作区的更改。然后,从文件层面来说:
git reset
只是把文件从历史记录区拿到暂存区,不影响工作区的内容,而且不支持 --mixed、--soft、--hard
git checkout
则是把文件从暂存区拿到工作区,不影响暂存区的内容。
git rm --cached
命令时,会直接从暂存区删除文件,工作区则不会做出改变git checkout .
或者 git checkout --
命令时,会用暂存区全部或指定的文件替换工作区的文件。这个操作很危险,会清除工作区中未添加到暂存区的改动git checkout HEAD .
或者git checkout HEAD
命令时,会用HEAD指向的master分支中的全部或者部分文件替换暂存区以及工作区的文件。这个命令也是具有危险性的,因为不但会清除工作区中未提交的改动,也会清除暂存区中未提交的改动git revert
不支持文件层面的操作
给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。
说明: 初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。 你可以假设 nums1 有足够的空间(空间大小大于或等于 m + n)来保存 nums2 中的元素。
示例: 输入:
nums1 = [1,3,5,7], m = 4
nums2 =[2,4,6,7,12,16], n = 6
输出: [ 1, 2, 3, 4, 5, 6, 7, 7, 12, 16 ]
初始化nums1的空间为m+n-1
nums1的初始索引为m-1
nums2的初始索引为n-1
分别比较指针所指向的数值大小,并将大的数值放到nums1的尾部,指针做对应的-1
/**
*
* @param {number[]} arr1
* @param {number} m
* @param {number[]} arr2
* @param {number} n
*/
function merge(arr1,m,arr2,n) {
let i=m-1,j=n-1,k=m+n-1;
while (i>=0 && j>=0) {
if(arr1[i]>arr2[j]){
arr1[k]=arr1[i]
i--
k--
}else{
arr1[k]=arr2[j]
j--
k--
}
}
while(j>=0){
arr1[k]=arr2[j]
j--
k--
}
}
let nums1=[1,3,5,7]
let nums2=[2,4,6,7,12,16]
merge(nums1,4,nums2,6)
LazyMan('Tom');
// Hi I am Tom
LazyMan('Tom').sleep(10).eat('lunch');
// Hi I am Tom
// 等待了10秒...
// I am eating lunch
LazyMan('Tom').eat('lunch').drink('500ml牛奶').sleep(10).eat('dinner')
// Hi I am Tom
// I am eating lunch
//喝了500ml牛奶...
// 等待了10秒...
// I am eating diner
LazyMan('Tom').eat('lunch').eat('dinner').sleepFirst(5).sleep(10).eat('junk food').drink('10000ml奶茶');
// Hi I am Tom
// 等待了5秒...
// I am eating lunch
// I am eating dinner
// 等待了10秒...
// I am eating junk food
//喝了10000ml奶茶...
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
注意空字符串可被认为是有效字符串。
示例 1:
输入: "()"
输出: true
示例 2:
输入: "()[]{}"
输出: true
示例 3:
输入: "(]"
输出: false
示例 4:
输入: "([)]"
输出: false
示例 5:
输入: "{[]}"
输出: true
function Foo() {
Foo.a = function() {
console.log(1)
}
this.a = function() {
console.log(2)
}
}
Foo.prototype.a = function() {
console.log(3)
}
Foo.a = function() {
console.log(4)
}
Foo.a();
let obj = new Foo();
obj.a();
Foo.a();
function Foo() {
Foo.a = function() {
console.log(1)
}
this.a = function() {
console.log(2)
}
}
// 以上只是 Foo 的构建方法,没有产生实例,此刻也没有执行
Foo.prototype.a = function() {
console.log(3)
}
// 现在在 Foo 上挂载了原型方法 a ,方法输出值为 3
Foo.a = function() {
console.log(4)
}
// 现在在 Foo 上挂载了直接方法 a ,输出值为 4
Foo.a();
// 立刻执行了 Foo 上的 a 方法,也就是刚刚定义的,所以
// # 输出 4
let obj = new Foo();
/* 这里调用了 Foo 的构建方法。Foo 的构建方法主要做了两件事:
1. 将全局的 Foo 上的直接方法 a 替换为一个输出 1 的方法。
2. 在新对象上挂载直接方法 a ,输出值为 2。
*/
obj.a();
// 因为有直接方法 a ,不需要去访问原型链,所以使用的是构建方法里所定义的 this.a,
// # 输出 2
Foo.a();
// 构建方法里已经替换了全局 Foo 上的 a 方法,所以
// # 输出 1
var a = {n: 1};
var b = a;
a.x = a = {n: 2};
console.log(a.x)
console.log(b.x)
这道题的关键在于不能使用运算符号,那么一个直接的思路就是能不能不用加减乘除实现整数的加减法呢?其实不难,复习一下大学课本里面计算机组成原理,应该能想起来如何实现基本的加减乘除法。这里,我们其实只需要实现一个基本的加法:
a | b | a+b | 进位 |
---|---|---|---|
0 | 0 | 0 | 无 |
1 | 0 | 1 | 无 |
0 | 1 | 1 | 无 |
1 | 1 | 0 | 有 |
从上面的表可以看出一种实现简单的多位二进制整数加法的算法如下
m 和 n 是两个二进制整数,求 m+n:
/* -- 位运算 -- */
// 先定义位运算加法
function bitAdd(m, n){
while(m){
[m, n] = [(m & n) << 1, m ^ n];
}
return n;
}
// 位运算实现方式 1 - 循环累加7次
let multiply7_bo_1 = (num)=>
{
let sum = 0,counter = new Array(7); // 得到 [empty × 7]
while(counter.length){
sum = bitAdd(sum, num);
counter.shift();
}
return sum;
}
// 位运算实现方式 2 - 二进制进3位(乘以8)后,加自己的补码(乘以-1)
let multiply7_bo_2 = (num) => bitAdd(num << 3, -num) ;
/* -- JS hack -- */
// hack 方式 1 - 利用 Function 的构造器 & 乘号的字节码
let multiply7_hack_1 = (num) =>
new Function(["return ",num,String.fromCharCode(42),"7"].join(""))();
// hack 方式 2 - 利用 eval 执行器 & 乘号的字节码
let multiply7_hack_2 = (num) =>
eval([num,String.fromCharCode(42),"7"].join(""));
// hack 方式 3 - 利用 SetTimeout 的参数 & 乘号的字节码
setTimeout(["window.multiply7_hack_3=(num)=>(7",String.fromCharCode(42),"num)"].join(""))
/* -- 进制转换 -- */
// 进制转换方式 - 利用 toString 转为七进制整数;然后末尾补0(左移一位)后通过 parseInt 转回十进制
let multiply7_base7 =
(num)=>parseInt([num.toString(7),'0'].join(''),7);
要求:
1、只能修改 setTimeout 到 Math.floor(Math.random() * 1000 的代码
2、不能修改 Math.floor(Math.random() * 1000
3、不能使用全局变量
function print(n){
setTimeout(() => {
console.log(n);
}, Math.floor(Math.random() * 1000));
}
for(var i = 0; i < 100; i++){
print(i);
}
function print(n){
setTimeout((() => {
console.log(n)
return ()=>{}
}).call(n,[]), Math.floor(Math.random() * 1000));
}
for(var i = 0; i < 100; i++){
print(i);
}
示例 1:
nums1 = [1, 3]
nums2 = [2]
中位数是 2.0
示例 2:
nums1 = [1, 2]
nums2 = [3, 4]
中位数是(2 + 3) / 2 = 2.5
function Stack(){
let items=[]
this.push=function(item){
items.push(item)
}
this.pop=function () {
return items.pop()
}
this.peek=function () {
return items[items.length-1]
}
this.clear=function(){
items=[]
}
this.print=function () {
console.log(items.toString())
}
this.size=function(){
return items.length()
}
this.isEmpty=function(){
return items.length==0
}
}
class Stack{
constructor(){
this.items=[]
}
push(item){
this.items.push(item)
}
pop(){
return this.items.pop()
}
peek(){
let length=this.items.length
return this.items[length-1]
}
isEmpty(){
return this.items.length==0
}
clear(){
this.items=[]
}
size(){
return this.items.length
}
print(){
console.log(this.items.toString())
}
}
function Queue() {
let items=[]
this.enqueue=function (item) {
items.push(item)
}
this.dequeue=function () {
return items.shift()
}
this.front=function(){
return items[0]
}
this.isEmpty=function(){
return items.length==0
}
this.size=function(){
return items.length
}
this.print=function(){
console.log(items.toString())
}
}
let Queue=(function () {
let items=new WeakMap()
class Queue{
constructor(){
items.set(this,[])
}
enqueue(item){
let arr=items.get(this)
arr.push(item)
}
dequeue(){
let arr=items.get(this)
return arr.shift()
}
front(){
let arr=items.get(this)
return arr[0]
}
isEmpty(){
let arr=items.get(this)
return arr.length==0
}
size(){
let arr=items.get(this)
return arr.length
}
print(){
let arr=items.get(this)
console.log(arr.toString())
}
}
return Queue
})()
GitFlow是由Vincent Driessen提出的一个Git操作流程规范,包含以下几个关键分支
名称 | 说明 |
---|---|
master | 主分支 |
develop | 主开发分支,包含确定即将发布的代码 |
feature | 新功能分支,一般一个新功能对应一个分支,对于功能的拆分需要比较合理,以避免一些后面不必要的冲突 |
release | 发布分支,发布时候用的分支,一般测试时候发现的bug在这个分支就行修复 |
hotfix | hotfix分支,紧急修复bug的时候用 |
GitFlow的优势有如下几点:
然后就是GitFlow最经典的几张流程图
feature
分支都是从develop
分支创建,完成后再合并到develop
分支,等待发布。
当需要发布时,我们从develop
分支创建一个release
分支
然后这个release
分支会发布到测试环境进行测试,如果发现问题就在这个分支上直接进行修复。在所有问题修复之前,我们会不停的重复发布->测试->修复->重新发布->重新测试这个流程。
发布结束后,这个release
分支会合并到develop
分支和master
分支,从而保证不会有代码丢失。
master
分支值只能跟踪已经发布的代码,合并到master
分支上的commit
只能来自release
分支和hotfix
分支
hotfix
分支的作用是紧急修改一些bug。
他们都是从master
分支上的某个tag建立,修复结束后再合并到develop
和master
分支上
原问题标题“React 和 Vue 的 diff 时间复杂度从 O(n^3) 优化到 O(n) ,那么 O(n^3) 和 O(n) 是如何计算出来的? ”
这里的n指的是页面的VDOM节点数,这个不太严谨。如果更严谨一点,我们应该应该假设 变化之前的节点数为m,变化之后的节点数为n。
React 和 Vue 做优化的前提是“放弃了最优解“,本质上是一种权衡,有利有弊。
倘若这个算法用到别的行业,比如医药行业,肯定是不行的,为什么?
React 和 Vue 做的假设是:
检测VDOM的变化只发生在同一层
检测VDOM的变化依赖于用户指定的key
如果变化发生在不同层或者同样的元素用户指定了不同的key或者不同元素用户指定同样的key, React 和 Vue都不会检测到,就会发生莫名其妙的问题。
但是React 认为, 前端碰到上面的第一种情况概率很小,第二种情况又可以通过提示用户,让用户去解决,因此 这个取舍是值得的。 没有牺牲空间复杂度,却换来了在大多数情况下时间上的巨大提升。 明智的选择!
首先大家要有个基本概念。
其实这是一个典型的最小编辑距离的问题,相关算法有很多,比如Git中 ,提交之前会进行一次对象的diff操作,就是用的这个最小距离编辑算法。
leetcode 有原题目, 如果想明白这个O(n^3), 可以先看下这个。
对于树,我们也是一样的,我们定义三种操作,用来将一棵树转化为另外一棵树:
删除 删除一个节点,将它的children交给它的父节点
插入 在children中 插入一个节点
修改 修改节点的值
事实上,从一棵树转化为另外一棵树,我们有很多方式,我们要找到最少的。
直观的方式是用动态规划,通过这种记忆化搜索减少时间复杂度。
由于树是一种递归的数据结构,因此最简单的树的比较算法是递归处理。
详细描述这个算法可以写一篇很长的论文,这里不赘述。 大家想看代码的,这里有一份 我希望没有吓到你。
确切地说,树的最小距离编辑算法的时间复杂度是O(n^2m(1+logmn)), 我们假设m 与 n 同阶, 就会变成 O(n^3)。
其中opacity:0 和display:none,具有株连特性,如果祖先元素设有此属性,无论子元素怎么样都不会出现在DOM中。
如果父节点元素为visibility:hidden,当子元素属性为visibility:visible 那么子元素就会显现出来。
1、首先什么是HTTP协议?
http协议是超文本传输协议,位于tcp/ip四层模型中的应用层;通过请求/响应的方式在客户端和服务器之间进行通信;但是缺少安全性,http协议信息传输是通过明文的方式传输,不做任何加密,相当于在网络上裸奔;容易被中间人恶意篡改,这种行为叫做中间人攻击;
2、加密通信:
为了安全性,双方可以使用对称加密的方式key进行信息交流,但是这种方式对称加密秘钥也会被拦截,也不够安全,进而还是存在被中间人攻击风险;
于是人们又想出来另外一种方式,使用非对称加密的方式;使用公钥/私钥加解密;通信方A发起通信并携带自己的公钥,接收方B通过公钥来加密对称秘钥;然后发送给发起方A;A通过私钥解密;双发接下来通过对称秘钥来进行加密通信;但是这种方式还是会存在一种安全性;中间人虽然不知道发起方A的私钥,但是可以做到偷天换日,将拦截发起方的公钥key;并将自己生成的一对公/私钥的公钥发送给B;接收方B并不知道公钥已经被偷偷换过;按照之前的流程,B通过公钥加密自己生成的对称加密秘钥key2;发送给A;
这次通信再次被中间人拦截,尽管后面的通信,两者还是用key2通信,但是中间人已经掌握了Key2;可以进行轻松的加解密;还是存在被中间人攻击风险;
3、解决困境:权威的证书颁发机构CA来解决;
3.1制作证书:作为服务端的A,首先把自己的公钥key1发给证书颁发机构,向证书颁发机构进行申请证书;证书颁发机构有一套自己的公私钥,CA通过自己的私钥来加密key1,并且通过服务端网址等信息生成一个证书签名,证书签名同样使用机构的私钥进行加密;制作完成后,机构将证书发给A;
3.2校验证书真伪:当B向服务端A发起请求通信的时候,A不再直接返回自己的公钥,而是返回一个证书;
说明:各大浏览器和操作系统已经维护了所有的权威证书机构的名称和公钥。B只需要知道是哪个权威机构发的证书,使用对应的机构公钥,就可以解密出证书签名;接下来,B使用同样的规则,生成自己的证书签名,如果两个签名是一致的,说明证书是有效的;
签名验证成功后,B就可以再次利用机构的公钥,解密出A的公钥key1;接下来的操作,就是和之前一样的流程了;
3.3:中间人是否会拦截发送假证书到B呢?
因为证书的签名是由服务器端网址等信息生成的,并且通过第三方机构的私钥加密中间人无法篡改; 所以最关键的问题是证书签名的真伪;
4、https主要的**是在http基础上增加了ssl安全层,即以上认证过程;:
Originally posted by @GeekQiaQia in Advanced-Frontend/Daily-Interview-Question#74 (comment)
采用尤大大的回答:
1. 原生 DOM 操作 vs. 通过框架封装操作。
这是一个性能 vs. 可维护性的取舍。框架的意义在于为你掩盖底层的 DOM 操作,让你用更声明式的方式来描述你的目的,从而让你的代码更容易维护。没有任何框架可以比纯手动的优化 DOM 操作更快,因为框架的 DOM 操作层需要应对任何上层 API 可能产生的操作,它的实现必须是普适的。针对任何一个 benchmark,我都可以写出比任何框架更快的手动优化,但是那有什么意义呢?在构建一个实际应用的时候,你难道为每一个地方都去做手动优化吗?出于可维护性的考虑,这显然不可能。框架给你的保证是,你在不需要手动优化的情况下,我依然可以给你提供过得去的性能。
2. 对 React 的 Virtual DOM 的误解。
React 从来没有说过 “React 比原生操作 DOM 快”。React 的基本思维模式是每次有变动就整个重新渲染整个应用。如果没有 Virtual DOM,简单来想就是直接重置 innerHTML。很多人都没有意识到,在一个大型列表所有数据都变了的情况下,重置 innerHTML 其实是一个还算合理的操作... 真正的问题是在 “全部重新渲染” 的思维模式下,即使只有一行数据变了,它也需要重置整个 innerHTML,这时候显然就有大量的浪费。
我们可以比较一下 innerHTML vs. Virtual DOM 的重绘性能消耗:
- innerHTML: render html string O(template size) + 重新创建所有 DOM 元素 O(DOM size)
- Virtual DOM: render Virtual DOM + diff O(template size) + 必要的 DOM 更新 O(DOM change)
Virtual DOM render + diff 显然比渲染 html 字符串要慢,但是!它依然是纯 js 层面的计算,比起后面的 DOM 操作来说,依然便宜了太多。可以看到,innerHTML 的总计算量不管是 js 计算还是 DOM 操作都是和整个界面的大小相关,但 Virtual DOM 的计算量里面,只有 js 计算和界面大小相关,DOM 操作是和数据的变动量相关的。前面说了,和 DOM 操作比起来,js 计算是极其便宜的。这才是为什么要有 Virtual DOM:它保证了 1)不管你的数据变化多少,每次重绘的性能都可以接受;2) 你依然可以用类似 innerHTML 的思路去写你的应用。
3. MVVM vs. Virtual DOM
相比起 React,其他 MVVM 系框架比如 Angular, Knockout 以及 Vue、Avalon 采用的都是数据绑定:通过 Directive/Binding 对象,观察数据变化并保留对实际 DOM 元素的引用,当有数据变化时进行对应的操作。MVVM 的变化检查是数据层面的,而 React 的检查是 DOM 结构层面的。MVVM 的性能也根据变动检测的实现原理有所不同:Angular 的脏检查使得任何变动都有固定的
O(watcher count) 的代价;Knockout/Vue/Avalon 都采用了依赖收集,在 js 和 DOM 层面都是 O(change):
- 脏检查:scope digest O(watcher count) + 必要 DOM 更新 O(DOM change)
- 依赖收集:重新收集依赖 O(data change) + 必要 DOM 更新 O(DOM change)可以看到,Angular 最不效率的地方在于任何小变动都有的和 watcher 数量相关的性能代价。但是!当所有数据都变了的时候,Angular 其实并不吃亏。依赖收集在初始化和数据变化的时候都需要重新收集依赖,这个代价在小量更新的时候几乎可以忽略,但在数据量庞大的时候也会产生一定的消耗。
MVVM 渲染列表的时候,由于每一行都有自己的数据作用域,所以通常都是每一行有一个对应的 ViewModel 实例,或者是一个稍微轻量一些的利用原型继承的 "scope" 对象,但也有一定的代价。所以,MVVM 列表渲染的初始化几乎一定比 React 慢,因为创建 ViewModel / scope 实例比起 Virtual DOM 来说要昂贵很多。这里所有 MVVM 实现的一个共同问题就是在列表渲染的数据源变动时,尤其是当数据是全新的对象时,如何有效地复用已经创建的 ViewModel 实例和 DOM 元素。假如没有任何复用方面的优化,由于数据是 “全新” 的,MVVM 实际上需要销毁之前的所有实例,重新创建所有实例,最后再进行一次渲染!这就是为什么题目里链接的 angular/knockout 实现都相对比较慢。相比之下,React 的变动检查由于是 DOM 结构层面的,即使是全新的数据,只要最后渲染结果没变,那么就不需要做无用功。
Angular 和 Vue 都提供了列表重绘的优化机制,也就是 “提示” 框架如何有效地复用实例和 DOM 元素。比如数据库里的同一个对象,在两次前端 API 调用里面会成为不同的对象,但是它们依然有一样的 uid。这时候你就可以提示 track by uid 来让 Angular 知道,这两个对象其实是同一份数据。那么原来这份数据对应的实例和 DOM 元素都可以复用,只需要更新变动了的部分。或者,你也可以直接 track by $index 来进行 “原地复用”:直接根据在数组里的位置进行复用。在题目给出的例子里,如果 angular 实现加上 track by $index 的话,后续重绘是不会比 React 慢多少的。甚至在 dbmonster 测试中,Angular 和 Vue 用了 track by $index 以后都比 React 快: dbmon (注意 Angular 默认版本无优化,优化过的在下面)
顺道说一句,React 渲染列表的时候也需要提供 key 这个特殊 prop,本质上和 track-by 是一回事。
4. 性能比较也要看场合
在比较性能的时候,要分清楚初始渲染、小量数据更新、大量数据更新这些不同的场合。Virtual DOM、脏检查 MVVM、数据收集 MVVM 在不同场合各有不同的表现和不同的优化需求。Virtual DOM 为了提升小量数据更新时的性能,也需要针对性的优化,比如 shouldComponentUpdate 或是 immutable data。
- 初始渲染:Virtual DOM > 脏检查 >= 依赖收集
- 小量数据更新:依赖收集 >> Virtual DOM + 优化 > 脏检查(无法优化) > Virtual DOM 无优化
- 大量数据更新:脏检查 + 优化 >= 依赖收集 + 优化 > Virtual DOM(无法/无需优化)>> MVVM 无优化
不要天真地以为 Virtual DOM 就是快,diff 不是免费的,batching 么 MVVM 也能做,而且最终 patch 的时候还不是要用原生 API。在我看来 Virtual DOM 真正的价值从来都不是性能,而是它 1) 为函数式的 UI 编程方式打开了大门;2) 可以渲染到 DOM 以外的 backend,比如 ReactNative。
5. 总结
以上这些比较,更多的是对于框架开发研究者提供一些参考。主流的框架 + 合理的优化,足以应对绝大部分应用的性能需求。如果是对性能有极致需求的特殊情况,其实应该牺牲一些可维护性采取手动优化:比如 Atom 编辑器在文件渲染的实现上放弃了 React 而采用了自己实现的 tile-based rendering;又比如在移动端需要 DOM-pooling 的虚拟滚动,不需要考虑顺序变化,可以绕过框架的内置实现自己搞一个。
附上尤大的回答链接
链接:https://www.zhihu.com/question/31809713/answer/53544875
function changeObjProperty(o) {
o.siteUrl = "http://www.baidu.com"
o = new Object()
o.siteUrl = "http://www.google.com"
}
let webSite = new Object();
changeObjProperty(webSite);
console.log(webSite.siteUrl);
function getmaxlen(str){
if(str){
let matchStr=str.match(/(\w)\1*/g);
let lenArr=matchStr.map(item=>item.length);
let maxLen=Math.max(...lenArr);
return matchStr.reduce((res,cur)=>{
if(cur.length===maxLen){
res[cur]=maxLen
}
return res
},{})
}
return {}
}
getmaxlen('abbkejsbcccwqaa')
// => {ccc: 3}
说明:解集不能包含重复的子集。
示例
输入: nums = [1,2,3]
输出:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]
Vue 的组件对象支持了计算属性 computed 和侦听属性 watch 2 个选项,很多同学不了解什么时候该用 computed 什么时候该用 watch。先不回答这个问题,我们接下来从源码实现的角度来分析它们两者有什么区别。
计算属性的初始化是发生在 Vue 实例初始化阶段的 initState 函数中,执行了 if (opts.computed) initComputed(vm, opts.computed),initComputed 的定义在 src/core/instance/state.js 中:
const computedWatcherOptions = { computed: true }
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${key}".`,
vm
)
}
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
}
}
}
}
函数首先创建 vm._computedWatchers 为一个空对象,接着对 computed 对象做遍历,拿到计算属性的每一个 userDef,然后尝试获取这个 userDef 对应的 getter 函数,拿不到则在开发环境下报警告。接下来为每一个 getter 创建一个 watcher,这个 watcher 和渲染 watcher 有一点很大的不同,它是一个 computed watcher,因为 const computedWatcherOptions = { computed: true }。computed watcher 和普通 watcher 的差别我稍后会介绍。最后对判断如果 key 不是 vm 的属性,则调用 defineComputed(vm, key, userDef),否则判断计算属性对于的 key 是否已经被 data 或者 prop 所占用,如果是的话则在开发环境报相应的警告。
那么接下来需要重点关注 defineComputed 的实现:
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering()
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: userDef
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: userDef.get
: noop
sharedPropertyDefinition.set = userDef.set
? userDef.set
: noop
}
if (process.env.NODE_ENV !== 'production' &&
sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this
)
}
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
这段逻辑很简单,其实就是利用 Object.defineProperty 给计算属性对应的 key 值添加 getter 和 setter,setter 通常是计算属性是一个对象,并且拥有 set 方法的时候才有,否则是一个空函数。在平时的开发场景中,计算属性有 setter 的情况比较少,我们重点关注一下 getter 部分,缓存的配置也先忽略,最终 getter 对应的是 createComputedGetter(key) 的返回值,来看一下它的定义:
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
watcher.depend()
return watcher.evaluate()
}
}
}
createComputedGetter 返回一个函数 computedGetter,它就是计算属性对应的 getter。
整个计算属性的初始化过程到此结束,我们知道计算属性是一个 computed watcher,它和普通的 watcher 有什么区别呢,为了更加直观,接下来来我们来通过一个例子来分析 computed watcher 的实现。
var vm = new Vue({
data: {
firstName: 'Foo',
lastName: 'Bar'
},
computed: {
fullName: function () {
return this.firstName + ' ' + this.lastName
}
}
})
当初始化这个 computed watcher 实例的时候,构造函数部分逻辑稍有不同:
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
// ...
if (this.computed) {
this.value = undefined
this.dep = new Dep()
} else {
this.value = this.get()
}
}
可以发现 computed watcher 会并不会立刻求值,同时持有一个 dep 实例。
然后当我们的 render 函数执行访问到 this.fullName 的时候,就触发了计算属性的 getter,它会拿到计算属性对应的 watcher,然后执行 watcher.depend(),来看一下它的定义:
/**
* Depend on this watcher. Only for computed property watchers.
*/
depend () {
if (this.dep && Dep.target) {
this.dep.depend()
}
}
注意,这时候的 Dep.target 是渲染 watcher,所以 this.dep.depend() 相当于渲染 watcher 订阅了这个 computed watcher 的变化。
然后再执行 watcher.evaluate() 去求值,来看一下它的定义:
/**
* Evaluate and return the value of the watcher.
* This only gets called for computed property watchers.
*/
evaluate () {
if (this.dirty) {
this.value = this.get()
this.dirty = false
}
return this.value
}
evaluate 的逻辑非常简单,判断 this.dirty,如果为 true 则通过 this.get() 求值,然后把 this.dirty 设置为 false。在求值过程中,会执行 value = this.getter.call(vm, vm),这实际上就是执行了计算属性定义的 getter 函数,在我们这个例子就是执行了 return this.firstName + ' ' + this.lastName。
这里需要特别注意的是,由于 this.firstName 和 this.lastName 都是响应式对象,这里会触发它们的 getter,根据我们之前的分析,它们会把自身持有的 dep 添加到当前正在计算的 watcher 中,这个时候 Dep.target 就是这个 computed watcher。
最后通过 return this.value 拿到计算属性对应的值。我们知道了计算属性的求值过程,那么接下来看一下它依赖的数据变化后的逻辑。
一旦我们对计算属性依赖的数据做修改,则会触发 setter 过程,通知所有订阅它变化的 watcher 更新,执行 watcher.update() 方法:
/* istanbul ignore else */
if (this.computed) {
// A computed property watcher has two modes: lazy and activated.
// It initializes as lazy by default, and only becomes activated when
// it is depended on by at least one subscriber, which is typically
// another computed property or a component's render function.
if (this.dep.subs.length === 0) {
// In lazy mode, we don't want to perform computations until necessary,
// so we simply mark the watcher as dirty. The actual computation is
// performed just-in-time in this.evaluate() when the computed property
// is accessed.
this.dirty = true
} else {
// In activated mode, we want to proactively perform the computation
// but only notify our subscribers when the value has indeed changed.
this.getAndInvoke(() => {
this.dep.notify()
})
}
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
那么对于计算属性这样的 computed watcher,它实际上是有 2 种模式,lazy 和 active。如果 this.dep.subs.length === 0 成立,则说明没有人去订阅这个 computed watcher 的变化,仅仅把 this.dirty = true,只有当下次再访问这个计算属性的时候才会重新求值。在我们的场景下,渲染 watcher 订阅了这个 computed watcher 的变化,那么它会执行:
this.getAndInvoke(() => {
this.dep.notify()
})
getAndInvoke (cb: Function) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
this.dirty = false
if (this.user) {
try {
cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
cb.call(this.vm, value, oldValue)
}
}
}
getAndInvoke 函数会重新计算,然后对比新旧值,如果变化了则执行回调函数,那么这里这个回调函数是 this.dep.notify(),在我们这个场景下就是触发了渲染 watcher 重新渲染。
通过以上的分析,我们知道计算属性本质上就是一个 computed watcher,也了解了它的创建过程和被访问触发 getter 以及依赖更新的过程,其实这是最新的计算属性的实现,之所以这么设计是因为 Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化才会触发渲染 watcher 重新渲染,本质上是一种优化。
接下来我们来分析一下侦听属性 watch 是怎么实现的。
侦听属性的初始化也是发生在 Vue 的实例初始化阶段的 initState 函数中,在 computed 初始化之后,执行了:
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
来看一下 initWatch 的实现,它的定义在 src/core/instance/state.js 中:
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
这里就是对 watch 对象做遍历,拿到每一个 handler,因为 Vue 是支持 watch 的同一个 key 对应多个 handler,所以如果 handler 是一个数组,则遍历这个数组,调用 createWatcher 方法,否则直接调用 createWatcher:
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
这里的逻辑也很简单,首先对 hanlder 的类型做判断,拿到它最终的回调函数,最后调用 vm.$watch(keyOrFn, handler, options) 函数,$watch 是 Vue 原型上的方法,它是在执行 stateMixin 的时候定义的:
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
cb.call(vm, watcher.value)
}
return function unwatchFn () {
watcher.teardown()
}
也就是说,侦听属性 watch 最终会调用 $watch 方法,这个方法首先判断 cb 如果是一个对象,则调用 createWatcher 方法,这是因为 $watch 方法是用户可以直接调用的,它可以传递一个对象,也可以传递函数。接着执行 const watcher = new Watcher(vm, expOrFn, cb, options) 实例化了一个 watcher,这里需要注意一点这是一个 user watcher,因为 options.user = true。通过实例化 watcher 的方式,一旦我们 watch 的数据发送变化,它最终会执行 watcher 的 run 方法,执行回调函数 cb,并且如果我们设置了 immediate 为 true,则直接会执行回调函数 cb。最后返回了一个 unwatchFn 方法,它会调用 teardown 方法去移除这个 watcher。
所以本质上侦听属性也是基于 Watcher 实现的,它是一个 user watcher。其实 Watcher 支持了不同的类型,下面我们梳理一下它有哪些类型以及它们的作用。
Watcher 的构造函数对 options 做的了处理,代码如下:
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.computed = !!options.computed
this.sync = !!options.sync
// ...
} else {
this.deep = this.user = this.computed = this.sync = false
}
所以 watcher 总共有 4 种类型,我们来一一分析它们,看看不同的类型执行的逻辑有哪些差别。
通常,如果我们想对一下对象做深度观测的时候,需要设置这个属性为 true,考虑到这种情况:
var vm = new Vue({
data() {
a: {
b: 1
}
},
watch: {
a: {
handler(newVal) {
console.log(newVal)
}
}
}
})
vm.a.b = 2
这个时候是不会 log 任何数据的,因为我们是 watch 了 a 对象,只触发了 a 的 getter,并没有触发 a.b 的 getter,所以并没有订阅它的变化,导致我们对 vm.a.b = 2 赋值的时候,虽然触发了 setter,但没有可通知的对象,所以也并不会触发 watch 的回调函数了。
而我们只需要对代码做稍稍修改,就可以观测到这个变化了
watch: {
a: {
deep: true,
handler(newVal) {
console.log(newVal)
}
}
}
这样就创建了一个 deep watcher 了,在 watcher 执行 get 求值的过程中有一段逻辑:
get() {
let value = this.getter.call(vm, vm)
// ...
if (this.deep) {
traverse(value)
}
}
在对 watch 的表达式或者函数求值后,会调用 traverse 函数,它的定义在 src/core/observer/traverse.js 中:
import { _Set as Set, isObject } from '../util/index'
import type { SimpleSet } from '../util/index'
import VNode from '../vdom/vnode'
const seenObjects = new Set()
/**
* Recursively traverse an object to evoke all converted
* getters, so that every nested property inside the object
* is collected as a "deep" dependency.
*/
export function traverse (val: any) {
_traverse(val, seenObjects)
seenObjects.clear()
}
function _traverse (val: any, seen: SimpleSet) {
let i, keys
const isA = Array.isArray(val)
if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
return
}
if (val.__ob__) {
const depId = val.__ob__.dep.id
if (seen.has(depId)) {
return
}
seen.add(depId)
}
if (isA) {
i = val.length
while (i--) _traverse(val[i], seen)
} else {
keys = Object.keys(val)
i = keys.length
while (i--) _traverse(val[keys[i]], seen)
}
}
traverse 的逻辑也很简单,它实际上就是对一个对象做深层递归遍历,因为遍历过程中就是对一个子对象的访问,会触发它们的 getter 过程,这样就可以收集到依赖,也就是订阅它们变化的 watcher,这个函数实现还有一个小的优化,遍历过程中会把子响应式对象通过它们的 dep id 记录到 seenObjects,避免以后重复访问。
那么在执行了 traverse 后,我们再对 watch 的对象内部任何一个值做修改,也会调用 watcher 的回调函数了。
对 deep watcher 的理解非常重要,今后工作中如果大家观测了一个复杂对象,并且会改变对象内部深层某个值的时候也希望触发回调,一定要设置 deep 为 true,但是因为设置了 deep 后会执行 traverse 函数,会有一定的性能开销,所以一定要根据应用场景权衡是否要开启这个配置。
前面我们分析过,通过 vm.$watch 创建的 watcher 是一个 user watcher,其实它的功能很简单,在对 watcher 求值以及在执行回调函数的时候,会处理一下错误,如下:
get() {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
},
getAndInvoke() {
// ...
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
handleError 在 Vue 中是一个错误捕获并且暴露给用户的一个利器
computed watcher 几乎就是为计算属性量身定制的,我们刚才已经对它做了详细的分析,这里不再赘述了。
在我们之前对 setter 的分析过程知道,当响应式数据发送变化后,触发了 watcher.update(),只是把这个 watcher 推送到一个队列中,在 nextTick 后才会真正执行 watcher 的回调函数。而一旦我们设置了 sync,就可以在当前 Tick 中同步执行 watcher 的回调函数。
update () {
if (this.computed) {
// ...
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
只有当我们需要 watch 的值的变化到执行 watcher 的回调函数是一个同步过程的时候才会去设置该属性为 true。
通过这一小节的分析我们对计算属性和侦听属性的实现有了深入的了解,计算属性本质上是 computed watcher,而侦听属性本质上是 user watcher。就应用场景而言,计算属性适合用在模板渲染中,某个值是依赖了其它的响应式对象甚至是计算属性计算而来;而侦听属性适用于观测某个值的变化去完成一段复杂的业务逻辑。
同时我们又了解了 watcher 的 4 个 options,通常我们会在创建 user watcher 的时候配置 deep 和 sync,可以根据不同的场景做相应的配置。
- 客户端使用https的url访问web服务器,要求与服务器建立ssl连接
- web服务器收到客户端请求后, 会将网站的证书(包含公钥)传送一份给客户端
- 客户端收到网站证书后会检查证书的颁发机构以及过期时间, 如果没有问题就随机产生一个秘钥
- 客户端利用公钥将会话秘钥加密, 并传送给服务端, 服务端利用自己的私钥解密出会话秘钥
- 之后服务器与客户端使用秘钥加密传输
let regex = /\d{2,5}?/g;
let string = "123 1234 12345 123456";
console.log( string.match(regex) );
// => ["12", "12", "34", "12", "34", "12", "34", "56"]
let regex = /\d{2,5}/g;
let string = "123 1234 12345 123456";
console.log( string.match(regex) );
// => ["123", "1234", "12345", "12345"]
// 例如要匹配"good"和"nice"可以使用/good|nice/。
// 测试如下:
let regex = /good|nice/g;
let string = "good idea, nice try.";
console.log( string.match(regex) );
// => ["good", "nice"]
//但有个事实我们应该注意,比如我用/good|goodbye/,去匹配"goodbye"字符串时,
//结果是"good":
let regex = /good|goodbye/g;
let string = "goodbye";
console.log( string.match(regex) );
// => ["good"]
//把正则改成/goodbye|good/,结果是:
let regex = /goodbye|good/g;
let string = "goodbye";
console.log( string.match(regex) );
// => ["goodbye"]
//也就是说,分支结构也是惰性的,即当前面的匹配上了,后面的就不再尝试了。
//要求从 <div id="container" class="main"></div> 提取出id="container"
// 最开始想到的正则可能是
let regex=/id=".*"/
let str="<div id="container" class="main"></div>"
console.log(str.match(regex))
//=> id="container" class="main"
// 因为.是通配符,本身就匹配双引号的,而量词*又是贪婪的,当遇到container后面的双引号时,又不会停下来,就会继续匹配,直到遇到最后一个双引号为止
//
let regex=/id=".*?"/
let str="<div id="container" class="main"></div>"
console.log(str.match(regex))
//=> id="container"
给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。
示例:
给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]
Set 和 Map 主要应用场景在于 数据重组和数据存储
Set 是一种集合的数据结构,Map 是一种字典的数据结构
ES6 新增的一种数据结构,类似于数组,数据成员唯一且无序、不重复
Set 本身是一种构造函数,用来生成Set数据结构
const s = new Set([1,2,3,4,1,2,3,4,2,2,2,3,3,4])
for(let i of s){
console.log(i) // 1、2、3、4
}
// 去除重复的数组对象
[...s] // [1, 2, 3, 4] Set 转 Array
Set 对象允许存储任何类型的唯一值,无论是原始值或者是对象引用
向Set添加值时不会发生类型转换,所以number 3 和 string '3' 是两个不同的值,Set 内部会判断是否两个值相同,使用"Same-value-zero equality"的算法,类似于精确相等运算符(===), 主要的区别是在Set中会把两个为NaN的值当做相等来处理,而在js中会认为NaN与任何值都是不相等的
let s = new Set();
let a = NaN;
let b= NaN;
s.add(a);
s.add(b);
console.log(s,a==b) // Set(1) {NaN} false
let s1= new Set();
s1.add(3);
s1.add('3');
console.log([...s1]) // [3, "3"]
let s = new Set([1,2,3,2,1,3,4,5,3,2,1,2,3,5]);
console.log(s.size) // 5
let s = new Set()
s.add(1).add(2).add(3).add(1)
s.has(1) // true
s.has(0) // false
s.delete(1)
s.has(1) //false
console.log(s) //Set(2) {2, 3}
s.clear()
console.log(s) //Set(0) {}
将 Set 转换成 Array数组
// 利用ES6扩展**...*** 运算符
let set = new Set([1,2,3,4,5])
console.log([...set]) //[1, 2, 3, 4, 5]
// 利用 Array.from()
console.log(Array.from(set)) // [1, 2, 3, 4, 5]
遍历的方法(遍历的顺序为插入的顺序)
let set = new Set([1,2,3,4,5])
let keys=set.keys();
let entries=set.entries();
console.log(keys) //SetIterator {1, 2, 3, 4, 5}
console.log(set.values()) //SetIterator {1, 2, 3, 4, 5}
console.log(entries) //SetIterator {1 => 1, 2 => 2, 3 => 3, 4 => 4, 5 => 5}
for(let item of keys){
console.log(item) // 1 2 3 4 5
}
for(let item of entries){
console.log(item) // [1,1] [2,2] [3,3] [4,4] [5,5]
}
set.forEach((value,key)=>{
console.log(key+":"+value) // 1:1 2:2 3:3 4:4 5:5
})
Set 可默认遍历,默认迭代器生成函数是 values()
Set.prototype[Symbol.iterator]===Set.prototype.values // true
所以 Set 可以使用map,filter方法
let set = new Set([1,2,3,4,5])
set=new Set([...set].map(item=>item*item))
console.log([...set]) //[1, 4, 9, 16, 25]
set=new Set([...set].filter(item=>item>10))
console.log([...set]) //[16, 25]
因此使用 Set 很容易实现交集(Intersect)、并集(Union)、差集(Difference)
let set1 = new Set([1,2,3]);
let set2 = new Set([0,2,4]);
let intersect= new Set([...set1].filter(item=>set2.has(item)))
let union= new Set([...set1,...set2])
let difference= new Set([...set1].filter(item=>!set2.has(item)))
console.log(intersect) //Set(1) {2}
console.log(union) //Set(5) {1,2,3,0,4}
console.log(difference) //Set(2) {1,3}
1 + "1"
2 * "2"
[1, 2] + [2, 1]
"a" + + "b"
加性操作符:如果只有一个操作数是字符串,则将另一个操作数转换为字符串,然后再将两个字符串拼接起来
所以值为:“11”
乘性操作符:如果有一个操作数不是数值,则在后台调用 Number()将其转换为数值
Javascript中所有对象基本都是先调用valueOf方法,如果不是数值,再调用toString方法。
所以两个数组对象的toString方法相加,值为:"1,22,1"
后边的“+”将作为一元操作符,如果操作数是字符串,将调用Number方法将该操作数转为数值,如果操作数无法转为数值,则为NaN。
所以值为:"aNaN"
var testobj={
num: 0,
str: '',
boolean: true,
unf: undefined,
nul: null,
obj: {
name: '我是一个对象',
id: 1,
qwe: {
a: 1
}
},
arr: [0, 1, 2, {b: 2}],
date: new Date(0),
reg: /我是一个正则/ig,
[Symbol('1')]: 1,
func() {
console.log(123)
}
}
function deepclone(obj,map=new WeakMap()){
if(obj instanceof Date) return new Date(obj);
if(obj instanceof RegExp) return new RegExp(obj)
if(obj==null || typeof obj !='object'){
return obj
}
if(map.has(obj)){
return map.get(obj)
}
let t = new obj.constructor()
map.set(obj,t)
let keys=[...Object.getOwnPropertyNames(obj),...Object.getOwnPropertySymbols(obj)]
for(let key of keys){
t[key]=deepclone(obj[key],map)
}
return t
}
let result=deepclone(testobj)
result.data=new Date()
console.log(result,testobj)
{
num: 0,
str: '',
boolean: true,
unf: undefined,
nul: null,
obj: { name: '我是一个对象', id: 1, qwe: { a: 1 } },
arr: [ 0, 1, 2, { b: 2 } ],
date: 1970-01-01T00:00:00.000Z,
reg: /我是一个正则/gi,
func: [Function: func],
data: 2020-08-21T11:30:05.269Z,
[Symbol(1)]: 1
} {
num: 0,
str: '',
boolean: true,
unf: undefined,
nul: null,
obj: { name: '我是一个对象', id: 1, qwe: { a: 1 } },
arr: [ 0, 1, 2, { b: 2 } ],
date: 1970-01-01T00:00:00.000Z,
reg: /我是一个正则/gi,
func: [Function: func],
[Symbol(1)]: 1
}
^ $ \b \B (?=p) (?!p)
^ 与 $ 匹配 开头 与 结尾
例如把字符串的开头和结尾用"#"替换
let res="hello".replace(/^|$/,'#');
console.log(res)
// => "#hello#"
多行匹配
let res="I\nlove\njavascript".replace(/^|$/gm,"#")
console.log(result)
/*
#I#
#love#
#javascript#
*/
\b 与 \B
\b是单词的边界,具体就是\w和\W之间的位置,也包括\w和^之间的位置,也包括\w和$之间的位置
比如一个文件名是"[js] lesson_01.mp4"中的\b 如下
let res="[js] lesson_01.mp4".replace(/\b/g,"#")
console.log(res)
//=> "[#js#] #lesson_01#.#mp4#"
为什么会这样呢,仔细看
首先,\w是字符数组[0-9a-zA-Z_]的简写,即\w是字母数字或者下划线中的任一个字符。
而\W是排除[^0-9a-zA-Z_]的简写,即\W是\w之外的任一个字符
那上面代码的执行结果 "[#js#] #lesson_01#.#mp4#" 是怎么来的呢
依据上面的例子如果把所有的 \B替换成"#"
let res="[js] lesson_01.mp4".replace(/\B/g,"#")
console.log(res)
//=> "#[j#s]# l#e#s#s#o#n#_#0#1.m#p#4"
(?=p) 与 (?!p)
(?=p), p 是一个子模式,即p前面的位置
比如(?=l) 表示'l'字符前面的位置,例如
let res="hello".replace(/(?=l)/g,"#")
// => "he#l#lo"
而 (?!p) 就是 (?=p) 的反义词 例如
let res="hello".replace(/(?!l)/g,"#")
// => "#h#ell#o#"
二者的学名分别是positive lookahead和negative lookahead。
中文翻译分别是正向先行断言和负向先行断言。
ES6中,还支持positive lookbehind和negative lookbehind。
具体是(?<=p)和(?<!p)。也有书上把这四个东西,翻译成环视,
即看看右边或看看左边。但一般书上,没有很好强调这四者是个位置。
比如(?=p),一般都理解成:要求接下来的字符与p匹配,但不能包括p的那些字符。而在本人看来(?=p)就与^一样好理解,就是p前面的那个位置。
对于位置的理解,我们可以理解成空字符""。
比如"hello"字符串等价于如下的形式:
"hello" == "" + "h" + "" + "e" + "" + "l" + "" + "l" + "o" + "";
因此,把/^hello$/写成/^^hello?$/,是没有任何问题的:
var result = /^^hello?$/.test("hello");
console.log(result);
// => true
甚至可以写成更复杂的:
var result = /(?=he)^^he(?=\w)llo$\b\b$/.test("hello");
console.log(result);
// => true
也就是说字符之间的位置,可以写成多个。
把位置理解空字符,是对位置非常有效的理解方式。
/.^/ 此正则要求只有一个字符 该字符后面是开头
比如 12345678 -> 12,345,678
需要把相应的位置替换成 ","
弄出最后一个逗号 (?=\d{3}$) 就可以做到
let res="12345678".replace(/(?=\d{3}$)/g,',')
// => "12345,678"
弄出所有的逗号 要求后面3个数字一组,也就是\d{3} 至少出现一次 因此可以使用量词 '+'
let res='12345678'.replace(/(?=(\d{3})+$)/g,',')
// => "12,345,678"
其余匹配案例
//以上匹配存在问题
let res='123456789'.replace(/(?=(\d{3})+$)/g,',')
// => ",123,456,789"
// 因为以上正则仅仅把从结尾向前数,一旦是3的倍数,就把其前面的位置换成 ',' 因此才出现这个问题
// 解决 要求匹配这个位置不能是开头
// 要求这个位置不能是开头怎么办 (?!^)
let res='123456789'.replace(/(?!^)(?=(\d{3})+$)/g,',')
// => "123,456,789"
支持其他转换实现 比如把 "12345678 123456789"替换成"12,345,678 123,456,789"
此时需要修改正则 把里面的开头 ^ 和 $ 结尾 替换成 \b
let res="12345678 123456789".replace(/(?!\b)(?=(\d{3})+\b)/g,',')
// => "12,345,678 123,456,789"
其中(?!\b) 要求当前一个位置,但不是\b前面的位置,其实(?!\b) 即 \B
因此最终正则变成了 /\B(?=(\d{3})+\b)/g
let res="12345678 123456789".replace(/\B(?=(\d{3})+\b)/g,',')
// => "12,345,678 123,456,789"
密码长度6-12位,由数字、小写字符和大写字母组成,但必须至少包括2种字符。
此题,如果写成多个正则来判断,比较容易。但要写成一个正则就比较困难。
那么,我们就来挑战一下。看看我们对位置的理解是否深刻。
不考虑"但必须至少包含2种字符"这一条件
let reg=/[0-9a-zA-Z]{6,12}$/
判断是否包含有某一种字符
假设,要求的必须是数字,此时可以使用 (?=.*[0-9])
let reg=/(?=.*[0-9])^[0-9a-zA-Z]{6-12}$/
同时包含具体的两种字符
比如同时包含数字和小写字母 可以使用 (?=.[0-9])(?=.[a-z])
let reg=/(?=.*[0-9])(?=.*[a-z])^[0-9a-zA-Z]{6,12}$/
我们可以把原题变换成下列几种情况之一
以上的4种情况是或的关系(实际上,可以不用第四条)
先看一下例子
'study'.replace(/(?=s)/, '#') // #study
'study'.replace(/(?=d)/, '#') // stu#dy
'study'.replace(/(?=^)/, '#') // #study
我们从而得知,是找到匹配的pattern后之前的位置,重点强调匹配到的是位置。
明白了这个,我们来理解下/(?=pattern)^/,及找到符合pattern的开始位置,并且该位置要在^之前,能在^之前的是什么?思考下。
/^^study/.test('study') // true
看到这个应该就明白 ^之前的就只能是^,^是个锚点。
/(?=\d)^/.test('9') // true
/(?=\d)^/.test('9l') // true
/(?=\d)^/.test('l9') // false
我们现在明白 /(?=\d)^/的意思是以数字开头的字符串。
到这里我们应该明白 /(?=.*[0-9])^/ 的意思是以任意字符(可以无)加数字开头。说白了就是必须包含数字
/(?=.*[0-9])^/.test('9') // true
/(?=.*[0-9])^/.test('9l') // true
/(?=.*[0-9])^/.test('l9') // true
扩展
1、
`study`.replace(/$/, '#') // study#
`study`.replace(/$(?<=y)/, '#') //study#
`study`.replace(/(?<=d)/, '#') // stud#y
2、
`study1`.replace(/(?![a-z])/g, '#') //study#1#
`study`.replace(/(?![a-z])/g, '#') //study#
最终答案
let reg=/((?=.*[0-9])(?=.*[a-z])|(?=.*[0-9])(?=.*[A-Z]|(?=.*[a-z])(?=.*[A-Z])))^[0-9A-Za-z]{6,12}$/;
console.log( reg.test("1234567") ); // false 全是数字
console.log( reg.test("abcdef") ); // false 全是小写字母
console.log( reg.test("ABCDEFGH") ); // false 全是大写字母
console.log( reg.test("ab23C") ); // false 不足6位
console.log( reg.test("ABCDEF234") ); // true 大写字母和数字
console.log( reg.test("abcdEF234") ); // true 三者都有
“至少包含两种字符”的意思就是说,不能全部都是数字,也不能全部都是小写字母,也不能全部都是大写字母。
那么要求“不能全部都是数字”,怎么做呢?(?!p)出马!
(?!p)
let reg = /(?!^[0-9]{6,12}$)^[0-9A-Za-z]{6,12}$/;
三种都不能呢
var reg = /(?!^[0-9]{6,12}$)(?!^[a-z]{6,12}$)(?!^[A-Z]{6,12}$)^[0-9A-Za-z]{6,12}$/;
console.log( reg.test("1234567") ); // false 全是数字
console.log( reg.test("abcdef") ); // false 全是小写字母
console.log( reg.test("ABCDEFGH") ); // false 全是大写字母
console.log( reg.test("ab23C") ); // false 不足6位
console.log( reg.test("ABCDEF234") ); // true 大写字母和数字
console.log( reg.test("abcdEF234") ); // true 三者都有
从最终渲染的DOM来看,这两者都是<a>标签,
在react-router中<Link>标签需要配合<Route>标签做路由跳转,react-router接管了其默认的跳转行为,
有别于传统的页面跳转,且<Link>的跳转只触发相匹配的<Route>对应页面内容更新,不会刷新整个页面。
而<a>标签是普通的超链接,用于从当前页面跳转到href指向的另一个页面(非锚点情况)
文本参考 https://juejin.im/post/6844903487155732494
内容包括:
/a+/ 匹配连续出现的a ,要匹配连续出现的"ab"时,需要使用/(ab)+/
var regex = /(ab)+/g;
var string = "ababa abbb ababab";
console.log( string.match(regex) );
// => ["abab", "ab", "ababab"]
在多选分支结构(p1|p2)
var regex = /^I love (JavaScript|Regular Expression)$/;
console.log( regex.test("I love JavaScript") );
console.log( regex.test("I love Regular Expression") );
// => true
// => true
如果去掉正则中的括号,即/^I love JavaScript|Regular Expression$/,匹配字符串是"I love JavaScript"和"Regular Expression",当然这不是我们想要的。
以日期为例。假设格式是yyyy-mm-dd的,我们可以先写一个简单的正则:
var regex = /\d{4}-\d{2}-\d{2}/;
带括号版 可以提取年、月、日
let regex=/(\d{4})-(\d{2})-(\d{2})/;
let str="2020-08-11"
console.log(str.match(regex))
// => ["2020-08-11", "2020", "08", "11", index: 0, input: "2020-08-11", groups: undefined]
regex=/(\d{4})-(\d{2})-(\d{2})/g;
console.log(str.match(regex))
// => ["2020-08-11"]
match 返回一个数组,第一个元素是整体匹配结果,然后是各个分组(括号里)匹配的内容,然后是下标,最后是输入的文本
如果正则是否有修饰符g,match 返回的数组格式是不一样的
使用正则对象的exec方法
let regex=/(\d{4})-(\d{2})-(\d{2})/;
let str="2020-08-11"
console.log(regex.exec(str))
// => ["2020-08-11", "2020", "08", "11", index: 0, input: "2020-08-11", groups: undefined]
也可以使用构造函数的全局属性$1-$9来获取
var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
regex.test(string); // 正则操作即可,例如
//regex.exec(string);
//string.match(regex);
console.log(RegExp.$1); // "2017"
console.log(RegExp.$2); // "06"
console.log(RegExp.$3); // "12"
比如,想把yyyy-mm-dd格式,替换成mm/dd/yyyy怎么做
var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
var result = string.replace(regex, "$2/$3/$1");
console.log(result);
// => "06/12/2017"
其中replace中的,第二个参数里用$1、$2、$3指代相应的分组。等价于如下的形式:
var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
var result = string.replace(regex, function(){
return RegExp.$2+"/"+RegExp.$3+"/"+RegExp.$1;
});
console.log(result);
// => "06/12/2017"
也可以写成
var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
var result = string.replace(regex, function(match,year,month,day){
return month+"/"+day+"/"+year;
});
console.log(result);
// => "06/12/2017"
除了使用相应API来引用分组,也可以在正则本身里引用分组。但只能引用之前出现的分组,即反向引用。
还是以日期为例。
比如要写一个正则支持匹配如下三种格式:
- 2016-06-12
- 2016/06/12
- 2016.06.12
可能会想到这个正则
var regex = /\d{4}(-|\/|\.)\d{2}(-|\/|\.)\d{2}/;
var string1 = "2017-06-12";
var string2 = "2017/06/12";
var string3 = "2017.06.12";
var string4 = "2016-06/12";
console.log( regex.test(string1) ); // true
console.log( regex.test(string2) ); // true
console.log( regex.test(string3) ); // true
console.log( regex.test(string4) ); // true
其中/和.需要转义。虽然匹配了要求的情况,但也匹配"2016-06/12"这样的数据。
假设我们想要求分割符前后一致怎么办?此时需要使用反向引用:
var regex = /\d{4}(-|\/|\.)\d{2}\1\d{2}/;
var string1 = "2017-06-12";
var string2 = "2017/06/12";
var string3 = "2017.06.12";
var string4 = "2016-06/12";
console.log( regex.test(string1) ); // true
console.log( regex.test(string2) ); // true
console.log( regex.test(string3) ); // true
console.log( regex.test(string4) ); // false
注意里面的\1,表示的引用之前的那个分组(-|/|.)。不管它匹配到什么(比如-),\1都匹配那个同样的具体某个字符。
我们知道了\1的含义后,那么\2和\3的概念也就理解了,即分别指代第二个和第三个分组。
看到这里,此时,恐怕你会有三个问题。
以左括号(开括号)为准。比如:
var regex = /^((\d)(\d(\d)))\1\2\3\4$/;
var string = "1231231233";
console.log( regex.test(string) ); // true
console.log( RegExp.$1 ); // 123
console.log( RegExp.$2 ); // 1
console.log( RegExp.$3 ); // 23
console.log( RegExp.$4 ); // 3
我们可以看看这个正则匹配模式:
\10 表示什么呢 表示第10个分组 还是\1 和 0 呢 当然是第十个分组了
var regex = /(1)(2)(3)(4)(5)(6)(7)(8)(9)(#) \10+/;
var string = "123456789# ######"
console.log( regex.test(string) );
// => true
引用不存在的分组会怎样呢
因为反向引用,是引用前面的分组,但我们在正则里引用了不存在的分组时,此时正则不会报错只是匹配反向引用字符本身,例如\2,就匹配'\2'。
注意'\2' 此时表示对"2" 进行了转意
ar regex = /\1\2\3\4\5\6\7\8\9/;
console.log( regex.test("\1\2\3\4\5\6\7\8\9") );
console.log( "\1\2\3\4\5\6\7\8\9".split("") );
// => ["�", "�", "�", "�", "�", "�", "�", "8", "9"]
之前文章中出现的分组,都会捕获他们匹配到的数据,以便后续引用,因此也称他们是捕获型分组
如果只想要括号最原始的功能,但不会引用它,即不在API里引用,也不在正则里反向引用,此时可以使用非捕获分组(?:p)
var regex =/(?:ab)+/g;
var str='ababa abbb ababab';
console.log(str.match(regex));
// => ["abab", "ab", "ababab"]
去掉字符串开头和结尾的空白符
function trim(str){
return str.replace(/^\s+|\s+$/g,'')
}
console.log(trim(' hello world '))
function trim(str){
return str.replace(/^\s*(.*?)\s*$/g,'$1')
}
console.log(trim(' hello world '))
这里使用了惰性匹配 *? 不然也会匹配最后一个空格之前所有的空格
前者效率更高一些
function titleize(str){
return str.toLowerCase().replace(/(?:^|\s)\w/g,function(c){
console.log(c)
return c.toUpperCase()
})
}
console.log(titleize('hello everyone'))
// => Hello Everyone
思路是找到每个单词的首个字母,当然这里不使用非捕获匹配也是可以的。
function camelize(str){
return str.replace(/[-_\s]+(.)?/g,function(match,c){
console.log(c)
return c?c.toUpperCase():'';
})
}
console.log(camelize('-moz-transform'))
// => MozTransform
其中分组(.)表示首字母,单词的界定是,前面的字符可以是多个连字符、下划线以及空白符,正则后面的?的目的,是为了应对str尾部的字符可能不是单词字符,比如str是'-moz-transform '。
function dasherize(str){
return str.replace(/([A-Z])/g,'-$1').replace(/[-_\s]+/g,'-').toLowerCase()
}
console.log( dasherize('MozTransform') );
驼峰化的逆过程
// 将HTML特殊字符转换成等值的实体
function escapeHTML(str) {
var escapeChars = {
'¢' : 'cent',
'£' : 'pound',
'¥' : 'yen',
'€': 'euro',
'©' :'copy',
'®' : 'reg',
'<' : 'lt',
'>' : 'gt',
'"' : 'quot',
'&' : 'amp',
'\'' : '#39'
};
return str.replace(new RegExp('[' + Object.keys(escapeChars).join('') +']', 'g'), function(match) {
console.log(match)
return '&' + escapeChars[match] + ';';
});
}
console.log( escapeHTML('<div>Blah blah blah</div>') );
// => "<div>Blah blah blah</div>";
其中使用了构造函数生成的正则,然后替换相应的格式就行了
它的逆过程,使用了括号,以便提供引用,也很简单
/ 实体字符转换为等值的HTML。
function unescapeHTML(str) {
var htmlEntities = {
nbsp: ' ',
cent: '¢',
pound: '£',
yen: '¥',
euro: '€',
copy: '©',
reg: '®',
lt: '<',
gt: '>',
quot: '"',
amp: '&',
apos: '\''
};
return str.replace(/\&([^;]+);/g, function(match, key) {
if (key in htmlEntities) {
return htmlEntities[key];
}
return match;
});
}
console.log( unescapeHTML('<div>Blah blah blah</div>') );
// => "<div>Blah blah blah</div>"
通过key获取相应的分组引用,然后作为对象的键
要求匹配
- <title>regular expression</title>
- <p>laoyao bye bye</p>
不匹配
<title>wrong!</p>
匹配一个开标签,可以使用正则<[^>]+>
匹配一个闭标签,可以使用</[^>]+>
但是要求匹配成对标签,那就是要使用反向引用
var regex=/<([^>]+)>[\d\D]*<\/\1>/
var string1 = "<title>regular expression</title>";
var string2 = "<p>laoyao bye bye</p>";
var string3 = "<title>wrong!</p>";
console.log( regex.test(string1) ); // true
console.log( regex.test(string2) ); // true
console.log( regex.test(string3) ); // false
其中开标签 <[^>]+> 改成 <([^>]+)> 使用括号的目的是为了后面的反向引用,而提供分组,闭标签使用了反向引用</\1>
另外 [\d\D] 的意思是,这个字符是数字或者不是数字, 因此,也就是匹配任意字符的意思
git rebase 和 git merge一样都是用于从一个分支获取并合并到当前分支
假设一个场景,就是我们开发的feature/todo
分支要合并到master
主分支,那么用rebase
或者merge
有什么不同呢
因此,当需要保留详细的合并信息的时候建议使用git merge
,特别是需要将分支合并进入master分支是;当发现自己修改某个功能时,频繁进行了git commit提交时,发现其实过多的提交信息没有必要时,可以选择git rebase
以下数据结构中,id 代表部门编号,name 是部门名称,parentId 是父部门编号,为 0 代表一级部门,现在要求实现一个 convert 方法,把原始 list 转换成树形结构,parentId 为多少就挂载在该 id 的属性 children 数组下,结构如下:
// 原始 list 如下
let list =[
{id:1,name:'部门A',parentId:0},
{id:2,name:'部门B',parentId:0},
{id:3,name:'部门C',parentId:1},
{id:4,name:'部门D',parentId:1},
{id:5,name:'部门E',parentId:2},
{id:6,name:'部门F',parentId:3},
{id:7,name:'部门G',parentId:2},
{id:8,name:'部门H',parentId:4}
];
const result = convert(list, ...);
// 转换后的结果如下
let result = [
{
id: 1,
name: '部门A',
parentId: 0,
children: [
{
id: 3,
name: '部门C',
parentId: 1,
children: [
{
id: 6,
name: '部门F',
parentId: 3
}, {
id: 16,
name: '部门L',
parentId: 3
}
]
},
{
id: 4,
name: '部门D',
parentId: 1,
children: [
{
id: 8,
name: '部门H',
parentId: 4
}
]
}
]
},
···
];
描述: 例如 11、22、121、1221、等等 数字翻转后与原来相等
真题描述:给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例: 给定数组 nums = [-1, 0, 1, 2, -1, -4], 满足要求的三元组集合为: [ [-1, 0, 1], [-1, -1, 2] ]
三数之和延续两数之和的思路,我们可以把求和问题变成求差问题——固定其中一个数,在剩下的数中寻找是否有两个数和这个固定数相加是等于0的。
双指针法用在涉及求和、比大小类的数组题目里时,大前提往往是:该数组必须有序。否则双指针根本无法帮助我们缩小定位的范围,压根没有意义。因此这道题的第一步是将数组排序
然后,对数组进行遍历,每次遍历到哪个数字,就固定哪个数字。然后把左指针指向该数字后面一个坑里的数字,把右指针指向数组末尾,让左右指针从起点开始,向中间前进:
每次指针移动一次位置,就计算一下两个指针指向数字之和加上固定的那个数之后,是否等于0。如果是,那么我们就得到了一个目标组合;否则,分两种情况来看:
相加之和大于0,说明右侧的数偏大了,右指针左移
相加之和小于0,说明左侧的数偏小了,左指针右移
tips:这个数组在题目中要求了“不重复的三元组”,因此我们还需要做一个重复元素的跳过处理。这一点在编码实现环节大家会注意到。
let numSum=[-1, 0, 1, 2, -1, -4]
let sum=0
function numSumArr(arr,sum){
let result=[],len=arr.length;
arr=arr.sort((a,b)=>a-b)
for(let i=0;i<len-2;i++){
let j=i+1,k=len-1;
if(i>0 && arr[i]==arr[i-1]){
continue
}
while (j<k) {
if(arr[i]+arr[j]+arr[k]<sum){
j++
while (j<k && arr[j]==arr[j-1]) {
j++
}
}else if (arr[i]+arr[j]+arr[k]>sum) {
k--
while(j<k && arr[k]==arr[k+1]){
k--
}
}else{
result.push([arr[i],arr[j],arr[k]])
j++
k--
while (j<k && arr[j]==arr[j-1]) {
j++
}
while (j<k && arr[k]==arr[k+1]) {
k--
}
}
}
}
console.log('result',result)
return result
}
numSumArr(numSum,0)
Grunt、Gulp是基于任务运行的工具:
它们会自动执行指定的任务,就像流水线,把资源放上去然后通过不同插件进行加工,它们包含活跃的社区,丰富的插件,能方便的打造各种工作流。
Webpack是基于模块化打包的工具:
自动化处理模块,webpack把一切当成模块,当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。
因此这是完全不同的两类工具,而现在主流的方式是用npm script代替Grunt、Gulp,npm script同样可以打造任务流。
webpack适用于大型复杂的前端站点构建: webpack有强大的loader和插件生态,打包后的文件实际上就是一个立即执行函数,这个立即执行函数接收一个参数,这个参数是模块对象,键为各个模块的路径,值为模块内容。立即执行函数内部则处理模块之间的引用,执行模块等,这种情况更适合文件依赖复杂的应用开发.
rollup适用于基础库的打包,如vue、d3等: Rollup 就是将各个模块打包进一个文件中,并且通过 Tree-shaking 来删除无用的代码,可以最大程度上降低代码体积,但是rollup没有webpack如此多的的如代码分割、按需加载等高级功能,其更聚焦于库的打包,因此更适合库的开发.
parcel适用于简单的实验性项目: 他可以满足低门槛的快速看到效果,但是生态差、报错信息不够全面都是他的硬伤,除了一些玩具项目或者实验项目不建议使用
file-loader
:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件url-loader
:和 file-loader 类似,但是能在文件很小的情况下以 base64 的方式把文件内容注入到代码中去source-map-loader
:加载额外的 Source Map 文件,以方便断点调试image-loader
:加载并且压缩图片文件babel-loader
:把 ES6 转换成 ES5css-loader
:加载 CSS,支持模块化、压缩、文件导入等特性style-loader
:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS。eslint-loader
:通过 ESLint 检查 JavaScript 代码define-plugin
:定义环境变量html-webpack-plugin
:简化html文件创建uglifyjs-webpack-plugin
:通过UglifyJS压缩ES6代码webpack-parallel-uglify-plugin
: 多核压缩,提高压缩速度, node 是单线程的,但node能够fork子进程,基于此,webpack-parallel-uglify-plugin
能够把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程,从而实现并发编译,进而大幅提升js压缩速度webpack-bundle-analyzer
: 可视化webpack输出文件的体积mini-css-extract-plugin
: CSS提取到单独的文件中,支持按需加载const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
// ...
optimization: {
minimizer: [
new ParallelUglifyPlugin({ // 多进程压缩
cacheDir: '.cache/',
uglifyJS: {
output: {
comments: false,
beautify: false
},
compress: {
warnings: false,
drop_console: true,
collapse_vars: true,
reduce_vars: true
}
}
}),
]
}
不同的作用:
不同的用法:
Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:
在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果
Loader像一个"翻译官"把读到的源文件内容转义成新的文件内容,并且每个Loader通过链式操作,将源文件一步步翻译成想要的样子。
编写Loader时要遵循单一原则,每个Loader只做一种"转义"工作。 每个Loader的拿到的是源文件内容(source),可以通过返回值的方式将处理后的内容输出,也可以调用this.callback()方法,将内容返回给webpack。 还可以通过 this.async()生成一个callback函数,再用这个callback将处理后的内容输出出去。 此外webpack还为开发者准备了开发loader的工具函数集——loader-utils。
相对于Loader而言,Plugin的编写就灵活了许多。 webpack在运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
webpack的热更新又称热替换(Hot Module Replacement),缩写为HMR。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。
原理:
首先要知道server端和client端都做了处理工作
详细原理解析来源于知乎饿了么前端Webpack HMR 原理解析
用webpack优化前端性能是指优化webpack的输出结果,让打包的最终结果在浏览器运行快速高效
UglifyJsPlugin
和ParallelUglifyPlugin
来压缩JS文件,利用cssnano
(css-loader?minimize)来压缩cssoutput
参数和各loader的publicPath
参数来修改资源路径--optimize-minimize
来实现SpliitChunksPlugin
插件来进行公共模块抽离,利用浏览器缓存可以长期缓存这些无需频繁变动的公共代码webpack.cache
、bable-loader.cacheDirectory
、HappyPack.cache
都可以利用缓存提高rebuild效率include:path.resolve(__dirname,'src')
,当然绝大多数情况下这种操作的提升有限,除非不小心build了node_modules
文件实战文章推荐使用webpack4提升180%编译速度Tool
CommonsChunkPlugin
(webpack4中已废弃)来提取公共代码,在webpack4中由optimization.splitChunks
和optimization.runtimeChunk
替代,前者拆分代码,后者提取runtime代码。原来的CommonsChunkPlugin
产出模块时,会包含重复的代码,并且无法优化异步模块,minchunks的配置也较复杂,splitChunks
解决了这个问题;另外将optimization.runtimeChunk
设置为true或{name:"manifest"},便能将入口模块中的runtime部分提取出来externals
配置来提取常用库DllPlugin
和DllReferencePlugin
预编译资源模块,通过DllPlugin
来对那些我们引用引用绝对不会修改的npm包来进行预编译,再使用DllReferencePlugin
将预编译的模块加载进来HappyPack
或者thread-loader来实现多线程加速编译webpack-uglify-parallel
来提升uglifyPlugin
的压缩速度。原理上webpack-uglify-parallel
采用多核并行压缩来提升压缩的速度Tree-shaking
和Scope Hoisting
来剔除多余的代码webpack4 的默认配置
optimization: {
minimize: env === 'production' ? true : false, // 开发环境不压缩
splitChunks: {
chunks: "async", // 共有三个值可选:initial(初始模块)、async(按需加载模块)和all(全部模块)
minSize: 30000, // 模块超过30k自动被抽离成公共模块
minChunks: 1, // 模块被引用>=1次,便分割
maxAsyncRequests: 5, // 异步加载chunk的并发请求数量<=5
maxInitialRequests: 3, // 一个入口并发加载的chunk数量<=3
name: true, // 默认由模块名+hash命名,名称相同时多个模块将合并为1个,可以设置为function
automaticNameDelimiter: '~', // 命名分隔符
cacheGroups: { // 缓存组,会继承和覆盖splitChunks的配置
default: { // 模块缓存规则,设置为false,默认缓存组将禁用
minChunks: 2, // 模块被引用>=2次,拆分至vendors公共模块
priority: -20, // 优先级
reuseExistingChunk: true, // 默认使用已有的模块
},
vendors: {
test: /[\\/]node_modules[\\/]/, // 表示默认拆分node_modules中的模块
priority: -10
}
}
}
}
splitChunks是拆包优化的重点,如果你的项目中包含element-ui等第三方组件(组件比较大),建议单独拆包,如下所示
splitChunks: {
// ...
cacheGroups: {
elementUI: {
name: "chunk-elementUI", // 单独将 elementUI 拆包
priority: 15, // 权重需大于其它缓存组
test: /[\/]node_modules[\/]element-ui[\/]/
}
}
}
loader 解析速度如何提升。同 webpack-parallel-uglify-plugin 插件一样,HappyPack 也能实现并发编译,从而可以大幅提升 loader 的解析速度, 如下是部分配置。
const HappyPack = require('happypack');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
const createHappyPlugin = (id, loaders) => new HappyPack({
id: id,
loaders: loaders,
threadPool: happyThreadPool,
verbose: process.env.HAPPY_VERBOSE === '1' // make happy more verbose with HAPPY_VERBOSE=1
});
对于前面 loader: "happypack/loader?id=happy-babel" 这句,便需要在 plugins 中创建一个 happy-babel 的插件实例。
plugins: [
createHappyPlugin('happy-babel', [{
loader: 'babel-loader',
options: {
babelrc: true,
cacheDirectory: true // 启用缓存
}
}])
]
另外,像 vue-loader、css-loader 都支持 happyPack 加速,如下所示。
plugins: [
createHappyPlugin('happy-css', ['css-loader', 'vue-style-loader']),
new HappyPack({
loaders: [{
path: 'vue-loader',
query: {
loaders: {
scss: 'vue-style-loader!css-loader!postcss-loader!sass-loader?indentedSyntax'
}
}
}]
})
]
//webpack.dll.config.js
const webpack = require("webpack");
const path = require('path');
const CleanWebpackPlugin = require("clean-webpack-plugin");
const dllPath = path.resolve(__dirname, "../src/assets/dll"); // dll文件存放的目录
module.exports = {
entry: {
// 把 vue 相关模块的放到一个单独的动态链接库
vue: ["babel-polyfill", "fastclick", "vue", "vue-router", "vuex", "axios", "element-ui"]
},
output: {
filename: "[name]-[hash].dll.js", // 生成vue.dll.js
path: dllPath,
library: "_dll_[name]"
},
plugins: [
new CleanWebpackPlugin(["*.js"], { // 清除之前的dll文件
root: dllPath,
}),
new webpack.DllPlugin({
name: "_dll_[name]",
// manifest.json 描述动态链接库包含了哪些内容
path: path.join(__dirname, "./", "[name].dll.manifest.json")
}),
],
};
接着, 需要在 package.json 中新增 dll 命令。
"scripts": {
"dll": "webpack --mode production --config build/webpack.dll.config.js"
}
运行 npm run dll 后,会生成 ./src/assets/dll/vue.dll-[hash].js 公共js 和 ./build/vue.dll.manifest.json 资源说明文件,至此 dll 准备工作完成,接下来在 webpack 中引用即可。
externals: {
'vue': 'Vue',
'vue-router': 'VueRouter',
'vuex': 'vuex',
'elemenct-ui': 'ELEMENT',
'axios': 'axios',
'fastclick': 'FastClick'
},
plugins: [
...(config.common.needDll ? [
new webpack.DllReferencePlugin({
manifest: require("./vue.dll.manifest.json")
})
] : [])
]
dll 公共js轻易不会变化,假如在将来真的发生了更新,那么新的dll文件名便需要加上新的hash,从而避免浏览器缓存老的文件,造成执行出错。由于 hash 的不确定性,我们在 html 入口文件中没办法指定一个固定链接的 script 脚本,刚好,add-asset-html-webpack-plugin 插件可以帮我们自动引入 dll 文件。
const autoAddDllRes = () => {
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin');
return new AddAssetHtmlPlugin([{ // 往html中注入dll js
publicPath: config.common.publicPath + "dll/", // 注入到html中的路径
outputPath: "dll", // 最终输出的目录
filepath: resolve("src/assets/dll/*.js"),
includeSourcemap: false,
typeOfAsset: "js" // options js、css; default js
}]);
};
// ...
plugins: [
...(config.common.needDll ? [autoAddDllRes()] : [])
]
单页面应用可以理解为webpack的标准模式,直接在entry
中指定单页面应用的入口即可
多页面应用的话,可以使用webpack的AutoWebPlugin
来完成简单自动化的构建,但是前提是项目的目录结构也必须遵守他预设的规范。
多页面应用注意事项:
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.