高级JS-进程线程-微任务和宏任务-存储-正则-防抖节流-深拷贝-事件总线-AJAX请求

126 阅读10分钟

操作系统: 类似于一个大工厂

进程: 我们可以认为,启动一个应用程序,就会默认启动一个进程,也可能是多个进程(车间)

线程: 每一个进程中,都会启动至少一个线程用来执行程序代码,这个线程被称为主线程(工人)

微任务和宏任务

  • 宏任务队列:ajaxsetTimeoutsetIntervalDOM监听UI Rendering
  • 微任务队列:Promise的then回调、 Mutation Observer APIqueueMicrotask()
  1. 宏任务执行之前,必须保证微任务队列是空的;
  2. 如果不为空,那么就优先执行微任务队列中的任务(回调)
 console.log('script start');
 function foo() {
     console.log('foo function');
 }
 
 function bar() {
     console.log('bar function');
     foo()
 }
 bar()
 console.log('script end');

/* 
script start
bar function
foo function
script end*/

promise会在第一次事件循环执行

then的回调会被添加到队列中

// 宏任务
setTimeout(() => {
    console.log('setTimeout1111');
},0)

setTimeout(() => {
    console.log('setTimeout2222');
},0)

// 微任务 
console.log('1111');
new Promise((resolve, reject) => {
    console.log('2222');
    resolve('-------')
    console.log('-----1');
}).then(res => {
    console.log('then传入的回调:', res); // then的回调会被添加到队列中
})
console.log('3333');

1111
2222
-----1
3333
then传入的回调: -------
setTimeout1111
setTimeout2222

面试题一:

console.log('script start');
// 宏任务1
setTimeout(() => {
    console.log("setTimeout1");
    new Promise( (resolve) => {
        resolve(); // then加入到任务队列中
    }).then(function () {
        new Promise((resolve) => {
            resolve();
        }).then(function () {
            console.log("then4"); // 宏任务中的微任务1
        });
        console.log("then2");
    });
});

// 同步执行
new Promise(function (resolve) {
    console.log("promise1");
    resolve();
}).then(function () {
    console.log("then1"); // 微任务队列1
});

// 宏任务2
setTimeout(function () {
    console.log("setTimeout2");
});

console.log(2);

// 微任务队列2
queueMicrotask(() => {
    console.log("queueMicrotask1")
});

new Promise(function (resolve) {
    resolve();
}).then(function () {
    console.log("then3"); // 微任务队列3
});
console.log('script end');
/*
script start
promise1
2
script end
then1
queueMicrotask1
then3
setTimeout1
then2
then4
setTimeout2*/

面试题二:

async function async1() {
    console.log('async1 start')
    await async2(); // 直接执行代码
    console.log('async1 end') // 所以会加入到微任务1
}

async function async2() {
    console.log('async2') // 相当于 return undefined => Promise.resole(undefined)
}

console.log('script start')

// 宏任务1
setTimeout(function () {
    console.log('setTimeout')
}, 0)

async1();

new Promise((resolve) => {
    console.log('promise1')
    resolve();
}).then( () => {
    console.log('promise2') // 微任务2
})

console.log('script end')

/*
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout*/

抛出异常

throw 后续代码不会执行

可以抛出一个具体的信息

  function foo() {
    throw new Error('抛出异常') // 后续代码不会执行了
    console.log('后续代码');
  }
  function test() {
    // 捕获处理了异常,异常就不会传递给浏览器,后续代码可以正常执行
    try {
      foo()
    } catch (err) {
      console.log(err);
    }finally {
      console.log('不管咋都会执行的代码');
    }
  }
  test()
  console.log('处理异常后,代码执行');

本地存储

localStorage:本地存储,是一种永久性的存储方法,在关闭掉网页重新打开存储的内容依然保留

sessionStorage:会话存储,提供的是本次会话的存储,在关闭掉会话时,存储的内容会被清除

let token = localStorage.getItem('token')
if(!token){
   console.log('从服务器请求token');
   token = 'token'
   localStorage.setItem('token',token)
}

// 获取数据
let userName = localStorage.getItem('name')
let password = localStorage.getItem('password')

// 如果本地没有,则...
if(!name || !password) {
  console.log('输入账号密码')
  userName = 'www'
  password = 'console'
  
  // 将数据保存到storage
  localStorage.setItem('name',userName)
  localStorage.setItem('password',password)
}

封装Storage工具

class Cache {
  constructor(isLocal = true) {
    this.storage = isLocal ? localStorage : sessionStorage
  }

  setCache(key, value) {
    if (!value) {
      throw new Error('value error: value 必须有值')
    }
    if (value) {
      // localStorage本身是不能直接存储对象类型
      localStorage.setItem(key, JSON.stringify(value))
    }
  }

  getCache(key) {
    const result = this.storage.getItem(key)
    if (result) {
      return JSON.parse(result)
    }
  }

  removeCache(key) {
    this.storage.removeItem(key)
  }

  clearCache(key) {
    this.storage.clear()
  }
}

const localCache = new Cache()
const sessionCache = new Cache(false)

// 封装过后可以直接存
const info = {
  name:'www',
  age:19
}
sessionCache.setCache('info',info)
localCache.getCache(info)

正则表达式

更多正则查询:c.runoob.com/front-end/8…

正则表达式是一种字符串匹配利器,可以帮助我们搜索、获取、替代字符串

// 创建正则
const re1 = new RegExp('匹配规则','修饰符')
const re2 = /abc/i  // '/规则/修饰符'

常见的正则使用方法:

image.png

常见的修饰符:

image.png

  • test:看是否匹配,检测字符串是否符合正则的规则(常用)
// 检测message是否符合re1规则,返回一个布尔值
if(re1.test(message)){
  console.log('符合规则')
}else {
  console.log('不符合规则')
}

// test案例
const inputEl = document.querySelector('input')
const pEl = document.querySelector('p')

inputEl.oninput = function() {
  const value = this.value
  // 匹配长度为 5 到 8 的由字符 "a" 组成的字符串
  if(/^a{5,8}$/.test(value)) {
    pEl.textContent = '输入内容符合规则'
  }else {
    pEl.textContent = '输入内容不符合规则要求'
  }
}
  • match:拿匹配的结果,使用字符串方法,传入一个正则(常用)

match 不加修饰符g会返回第一个匹配到的结果的详情,加修饰符g会把所有匹配的结果返回

matchAll 传入的正则必须加修饰符g,给到一个迭代器,调用next()

const message = 'abc aa12a Abcd b32bb'
const re1 = /abc/ig

// 获取一个字符串中所有的abc
const res2 = message.match(re1)

// 将一个字符串中的所有abc换成cba
const res3 = message.matchAll(re1,'cba')
res3.next()
for(const item of res3){
    console.log(item)
}

规则 – 字符类

字符类:

image.png

const num = 'qwdq224 3ewg342'
// + 匹配多个,+ 相当于{1,}
const  re = /\d+/ig  
console.log(num.match(re)) // ['224', '3', '342']

反向类:

\D 非数字:除 \d 以外的任何字符,例如字母。

\S 非空格符号:除 \s 以外的任何字符,例如字母。

\W 非单字字符:除 \w 以外的任何字符,例如非拉丁字母或空格

规则 – 锚点

符号 ^ 匹配文本开头

符号 $ 匹配文本末尾

// 是否以***开头,***结尾(startsWith/endsWith有大小写限制)
const meg = 'my name is www'
if(meg.startsWith('my')){
  console.log('以my开头')
}
if(meg.endsWith('www')){
  console.log('以www结尾')
}

if(/^my/i.test(meg)){
  console.log('以my开头---')
}
if(/www$/i.test(meg)){
  console.log('以www结尾---')
}

词边界测试 \b检查位置的一侧是否匹配 \w,而另一侧则不匹配 \w

const meg = 'my name is www'
// name是一个单独的词,可以有空格
if (/\bname\b/i.test(meg)) {
  console.log('有name,有边界')
}

// 词边界的应用
const time = '12:32,234:12,21:11'
const timeRe = /\b\d\d:\d\d\b/ig
console.log(time.match(timeRe)) // ['12:32', '21:11']

规则 – 转义字符串

如果要把特殊字符作为常规字符来使用,需要对其进行转义:在它前面加个反斜杠

常见的需要转义的字符:[] \ ^ $ . | ? * + ( )

斜杠符号 / 并不是一个特殊符号,但是在字面量正则表达式中也需要转义,需要//来转义

// 匹配所有以.js或者jsx结尾的文件名
const fileNames = ['ac.js','index.jsx','index.html','utils.js']
const re = /\.jsx?$/ // ? 表示0个或者1个

// for...of做法
const newFileNames1 = []
for (const fileName of fileNames){
  if(re.test(fileName)){
    newFileNames1.push(fileName)
  }
}

// fillter过滤做法
const newFileNames2 = fileNames.filter(fileName => re.test(fileName))

console.log(newFileNames1,newFileNames2) // ['ac.js', 'index.jsx', 'utils.js']

集合和范围

集合:[eao] 意味着查找在 3 个字符 ‘a’、‘e’ 或者 `‘o’ 中的任意一个

范围:方括号也可以包含字符范围

  1. [a-z] 会匹配从 a 到 z 范围内的字母,[0-5] 表示从 0 到 5 的数字
  2. [0-9A-F] 表示两个范围:它搜索一个字符,满足数字 0 到 9 或字母 A 到 F
  3. \d ——[0-9] 相同
  4. \w ——[a-zA-Z0-9_] 相同
const number = ['199','183','110','155']
const numberRe = /^1[3456789]\d/
console.log(number.filter(phone => numberRe.test(phone))) // ['199', '183', '155']

const phoneNum = '19944446666'
const phoneNumRe = /^1[3-9]\d{9}$/
console.log( phoneNumRe.test(phoneNum)) // true

量词

数量 :

  1. 确切的位数:{5}
  2. 某个范围的位数:{3,5}

缩写:

  1. +:代表“一个或多个”,相当于 {1,}
  2. ?:代表“零个或一个”,相当于 {0,1}。换句话说,它使得符号变得可选;
  3. *:代表着“零个或多个”,相当于 {0,}。也就是说,这个字符可以多次出现或不出现
// 匹配标签名字
const htmlEl = '<div><span>哈哈哈哈</span><p>我是标题</p></div>'
const htmlRe = /<\/?[a-z][a-z0-9]*>/ig
console.log(htmlEl.match(htmlRe)) // ['</span>', '</p>', '</div>']

贪婪和惰性模式

贪婪模式:默认情况匹配规则是查找到匹配的内容后,会继续向后查找,一直找到最后一个匹配的内容

惰性模式:只要获取到对应的内容后,就不再继续向后匹配;在量词后面再加一个问号 ‘?’ 来启用它;

所以匹配模式变为 *? 或 +?,甚至将 '?' 变为 ??

const message = '我最喜欢《缘之空》和《进阶的巨人》、《美女与野兽》'
// 默认: .+采用贪婪模式
const re = /《.+》/ig

// 惰性模式
const re2 = /《.+?》/ig
console.log(message.match(re)) // ['《缘之空》和《进阶的巨人》、《美女与野兽》']
console.log(message.match(re2)) // ['《缘之空》', '《进阶的巨人》', '《美女与野兽》']

捕获组

一部分可以用括号括起来 (...),这称为捕获组,视为一个整体

允许将匹配的一部分作为结果数组中的单独项

或:在正则表达式中,它用竖线 | 表示;通常会和捕获组一起来使用,在其中表示多个值

// 捕获组
const message = '我最喜欢《缘之空》和《进阶的巨人》、《美女与野兽》'
const re2 = /《(.+?)》/ig
const iterator = message.matchAll(re2)
for (const item of iterator) {
  console.log(item[1]) // 缘之空 进阶的巨人  美女与野兽
}

// 将捕获组作为一个整体(连续两个abc)
const  info = 'asdabciasjdabcabcjd'
const re3 = /(abc){2,}/ig
console.log(info.match(re3)) // ['abcabc']

案例练习

  • 歌词解析
  function parseLyric(lyricString) {
  // 1. 根据换行符切割
  const lyricLineStrings = lyricString.split('\n')
  const lyricLines = []
  
  // 匹配时间的正则
  const re = /\[(\d{2}):(\d{2})\.(\d{2,3})\]/

  // 2. 针对每一行歌词进行解析 [00:24.380]我好想住你隔壁
  for (const lyric of lyricLineStrings) {
    // ['[00:00.000]', '00', '00', '000', index: 0, input: '[00:00.000] 作词 : 许嵩', groups: undefined]
    const timeString = re.exec(lyric)
    if (!timeString) continue // 如果有值再做操作,没值过掉这次循环,进行下次循环
    // 转毫秒
    const time1 = timeString[1] * 60 * 1000
    const time2 = timeString[2] * 1000
    const time3 = timeString[3].length === 3 ? timeString[3] * 1 : timeString[3] * 10
    // 获取时间
    const time = time1 + time2 + time3  // 0 1000 2000...

    // 3. 获取歌词, trim去空格
    const content = lyric.replace(re, '').trim()

    // 讲对象放在数组中
    lyricLines.push({ time, content })
  }
  console.log(lyricLines)
  /*
    {time: 0, content: '作词 : 许嵩'}
    {time: 1000, content: '作曲 : 许嵩'}
    {time: 2000, content: '编曲 : 许嵩'}
    ...
   */
  return lyricLines
}


//  请求的歌词...
const lyricString = '[00:00.000] 作词 : 许嵩\n
                     [00:01.000] 作曲 : 许嵩\n
                     [00:02.000] 编曲 : 许嵩\n
                     [00:22.240]天空好想下雨\n
                     [00:24.380]我好想住你隔壁\n'
// 解析歌词                  
const lyricStringInfo = parseLyric(lyricString)
  • 时间格式化

相关库:dayjsmoment

function formatDate(time, formatString) {
  // 1.将时间戳转Date
  const date = new Date(time)
  // 2.获取时间值 定义正则和值之间的关系
  const obj = {
    'y+': date.getFullYear(),
    'M+': date.getMonth() + 1,
    'd+': date.getDate(),
    'h+': date.getHours(),
    'm+': date.getMinutes(),
    's+': date.getSeconds()
  }
  // 替换
  for (const key in obj) {
    const keysRe = new RegExp(key)
    if (keysRe.test(formatString)) {
      // padStart:有两位,用0补齐
      const value = (obj[key] + '').padStart(2, '0')
      formatString = formatString.replace(keysRe, value)
    }
  }
  return formatString
}
const result = formatDate(1332322323, 'yyyy-MM-dd hh:mm:ss') // 1970-01-16 18:05:22

防抖和节流

防抖

当事件触发时,相应的函数并不会立即触发,而是会等待一定的时间 (游戏回城)

image.png

应用场景很多:

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

第三方库来实现防抖操作:lodashunderscore

<input type="text">
<script src="https://cdn.jsdelivr.net/npm/underscore@1.13.1/underscore-umd-min.js"></script>
<script>
  const inputEl = document.querySelector('input')

  let counter = 1
  inputEl.oninput = _.debounce(function() {
    console.log(`发送网络请求${counter++}`,this.value)
  },1000)
</script>

手写防抖

// 简约版
<input type="text">
<script>
  const inputEl = document.querySelector('input')

  function wqDebounce(fn, delay) {
    // 1. 记录上一次事件触发的timer
    let timer = null

    // 2. 触发事件执行的函数
    const _debounce = function(...args) {
      // 多次触发事件,取消上一次事件
      if (timer) clearTimeout(timer)
      // 延迟执行传入的回调函数
      timer = setTimeout(() => {
        fn.apply(this,args)
        timer = null // 执行函数后,讲timer置为null
      }, delay)
    }
    // 返回一个新的函数
    return _debounce
  }

  let counter = 1
  inputEl.oninput = wqDebounce(function(event) {
    console.log(`发送网络请求${counter++}`,this.value)
  }, 3000)
</script>
// 完善取消和立即执行功能
<input type="text">
<button>取消</button>
<script>
  const inputEl = document.querySelector('input')
  const buttonEl = document.querySelector('button')

  function wqDebounce (fn, delay, immediate = true) {
    // 1.记录上一次事件触发的timer
    let timer = null
    let isInvode = false // 记录有没有立即执行过

    // 2.触发事件执行的函数
    const _debounce = function (...args) {
      // 多次触发事件,取消上一次事件
      if (timer) clearTimeout(timer)

      // 第一次操作是不需要延迟
      if (immediate && !isInvode) {
        fn.apply(this, args)
        isInvode = true
        return
      }

      // 延迟执行传入的回调函数
      timer = setTimeout(() => {
        fn.apply(this, args)
        timer = null // 执行函数后,讲timer置为null
        isInvode = false
      }, delay)
    }

    // 3.给_debounce绑定一个取消函数
    _debounce.cancel = function () {
      if (timer) clearTimeout(timer)
      timer = null
      isInvode = false
    }
    // 返回一个新的函数
    return _debounce
  }

  let counter = 1
  const debounceFn = wqDebounce(function (event) {
    console.log(`发送网络请求${counter++}`, this.value)
  }, 3000)
  inputEl.oninput = debounceFn

  // 4.实现取消事件
  buttonEl.onclick = function () {
    debounceFn.cancel()
  }

节流

事件被频繁触发,节流函数会按照一定的频率来执行函数;不管在这个中间有多少次触发这个事件,执行函数的频繁总是固定的 (技能平A)

image.png

节流的应用场景:

  1. 监听页面的滚动事件
  2. 鼠标移动事件
  3. 用户频繁点击按钮操作
  4. 游戏中的一些设计

手写节流

<input type="text">
<script>
  const inputEl = document.querySelector('input')

  function wqthrottle (fn, intervar,immediate = true) {
    let startTime = 0
    return function (...args) {
      const nowTime = new Date().getTime() // 1.当前时间

      // 对立即执行进行控制
      if(!immediate && startTime === 0){
        startTime = nowTime
      }

      const waitTime = intervar - (nowTime - startTime) // 2.需要等待的时间
      if (waitTime <= 0) {
        fn.apply(this,args)
        startTime = nowTime
      }
    }
  }

  let counter = 1
  inputEl.oninput = wqthrottle(function (event) {
    console.log(`发送网络请求${counter++}`,this.value,event)
  }, 2000)
</script>

深拷贝浅拷贝

浅拷贝:内部引入对象时,依旧会相互影响

   const info = {
      name: 'wqq',
      friend: {
        age: 18
      }
    }
    // 引用赋值
    const obj1 = info

    // 浅拷贝
    const obj2 = {...info}
    obj2.friend.age = 20
    console.log(info.friend.age); // 20

    const obj3 = Object.assign({},info) // 将info拷贝到{}中生成一个新的对象
    obj3.friend.age = 20
    console.log(info.friend.age); // 20

    // 深拷贝(使用JSON)
    const obj4 = JSON.parse(JSON.stringify(info))
    obj4.friend.age = 99
    console.log(info.friend.age); // 18

深拷贝:两个对象不再有任何关系,不会相互影响

 // 手动实现深拷贝

 // 判断一个标识符是否是对象类型
 function isObject(value) {
   const valueType = typeof value
   return (value !== null) && (valueType === 'object' || valueType === 'function')
 }

 function deepCopy(originValue,map = new WeakMap()) {
   // 如果值是symbol类型
   if (typeof originValue === 'symbol') {
     return Symbol(originValue.description)
   }

   // 原始类型直接返回
   if (!isObject(originValue)) {
     return originValue
   }

   // 如果是set类型
   if (originValue instanceof Set) {
     const newSet = new Set()
     for (const setItem of originValue) {
       newSet.add(deepCopy(setItem))
     }
   }

   // 如果是函数类型,不需要进行深拷贝
   if (typeof originValue === 'function') {
     return originValue
   }

   // 如果是对象类型,才需要创建对象
   if (map.get(originValue)) {
     return map.get(originValue)
   }
   const newObj = Array.isArray(originValue) ? [] : {}
   map.set(originValue, newObj)
   // 便利普通的key
   for (const key in originValue) {
     newObj[key] = deepCopy(originValue[key],map) // 递归调用
   }

   // 单独便利symbol,直接for...of不会便利symbol
   const symbolKeys = Object.getOwnPropertySymbols(originValue)
   for (const symbolKey of symbolKeys) {
     newObj[Symbol(symbolKey.description)] = deepCopy(originValue[symbolKey],map)
   }
   return newObj
 }

 const set = new Set(['abc', 'cba'])
 const s1 = Symbol('s1')

 const info = {
   name: 'wqq',
   friend: {
     message: '大美女',
     age: 18
   },
   // 特殊类型
   set: set,
   running: () => console.log('running'),
   symbolKey: Symbol(),
   [s1]: 'aaa',
 }
 info.self = info

 const newObj = deepCopy(info)
 console.log(newObj);

AJAX发送请求

// 1.创建XMLHttpRequest对象
const xml = new XMLHttpRequest()

// 2.监听状态变化(宏任务)
xml.onreadystatechange = function () {
  // 如果状态不是DOME,直接返回
  if(xml.readyState !== XMLHttpRequest.DONE) return
  // 将字符串转成json
  const resJSON = JSON.parse(xml.response)
}

// 3.配置请求option
// 参数一:请求方式  参数二:请求地址 参数三:发送同步请求 实际开发采用异步,不会阻塞js代码继续执行
xml.open('get','http://localhost:8080'false)

// 4.发送请求(浏览器帮助发送响应请求)
xml.send()
  • XMLHttpRequest的state

image.png

  • 其他的事件监听
  1. loadstart:请求开始
  2. progress: 一个响应数据包到达,此时整个 response body 都在 response 中
  3. abort:调用 xhr.abort() 取消了请求
  4. error:发生连接错误,例如,域错误。不会发生诸如 404 这类的 HTTP 错误
  5. load:请求成功完成
  6. timeout:由于请求超时而取消了该请求(仅发生在设置了 timeout 的情况下)
  7. loadend:在 load,error,timeout 或 abort 之后触发。

获取HTTP响应的网络状态,可以通过status和statusText来获取: image.png

  • ajax请求封装
function wqajax({
  url,
  method = "get",
  data = {},
  timeout = 5000,
  headers = {},
  success,
  failure
} = {}) {
  // 1.创建xhr对象
  const xhr = new XMLHttpRequest()

  xhr.onreadystatechange = function() {
    if (xhr.readyState !== XMLHttpRequest.DONE) return
    if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
      success && success(xhr.response)
    } else {
      failure && failure(xhr.response)
    }
  }

  xhr.ontimeout = function() {
    failure && failure("timeout")
  }

  // 2.设置响应的类型
  xhr.responseType = "json"
  xhr.timeout = timeout

  // 3.发送请求
  const params = Object.keys(data).map(key => `${key}=${encodeURIComponent(data[key])}`)
  const paramsString = params.join("&")

  // 设置header
  // for (const headerKey in headers) {
  //   xhr.setRequestHeader(headerKey, headers[headerKey])
  // }

  if (method.toLowerCase() === "get") {
    xhr.open(method, url + "?" + paramsString)
    xhr.send()
  } else {
    xhr.open(method, url)
    xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded')
    xhr.send(paramsString)
  }

  return xhr
}