1-5 浏览器相关

158 阅读3分钟

原文链接(格式好):《1-5 浏览器相关》

原理知识:《1-3 课程里面的浏览器原理》

浏览器下的 JS

包含:

  • ECMAScript:基础逻辑与数据处理 const a = [1,2,3].map(i => i*2)
  • BOM:对浏览器功能的处理 location.href = 'www.a.com'
  • DOM:HTML文本的操作 document.title = 'hello'

JS 与 ES 的区别:ES 是普通话,JS 是方言;

官方话术:ECMAScript 是一种语言标准,而 JavaScript 是对这个标准的一种具体实现(额外扩展了一些浏览器交互操作)。

BOM

location

与浏览器的地址相关的

基础知识

举例:

location.href = https://www.yuque.com/u53094/ux9b3x/frct1y9u1fppzpqf?q=1#aXLrY

location 组成

  • 协议 protocol => location.protocol,https:
  • 域名 host => location.host/location.hostname,www.yuque.com
  • 端口 port => location.port,''
  • 路径 path => location.pathname,/u53094/ux9b3x/frct1y9u1fppzpqf
  • 参数 query => location.search,[?,#)之间的,?q=1
  • 锚点 anchor => location.hash,[#之后的,#aXLrY

进阶知识

location.assgin(url)

跳转到指定页面,会创建新的浏览器历史,跟location.href效果一样

location.replace(url)

跳转到指定页面,不会创建新的浏览器历史,并将之前地址的历史替换为新地址

location.reload()

刷新当前页面

location.toString()

获取当前地址,等价于location.href的值

面试常问

  1. location 的 API
  2. 路由相关:跳转、参数、操作等
  3. 实际场景分析:
    1. 项目路由模式:
      1. history:https://www.example.com/user/profile地址不带#;需要服务器支持(因为/user/profile是直接访问服务器资源的,所以需要服务器处理通配让项目只访问主 html 文件)
      2. hash:https://www.example.com/#/user/profile地址带#;不需要服务器支持(因为#后面的不会发送给服务器,所以可以永远只访问主 html)
  1. url 的处理
    1. 如何将地址参数转为对象?
function getQueryObject(search = location.search){
    const searchObj = {}
  
    if(!search) return searchObj
  
    const searchSplitArray = search.slice(1).split('&')
    searchSplitArray.map(item => {
        const [key, value] = item.split('=')
        searchObj[key] = value
    })
    return searchObj
}
    1. 使用正则判断是否为 url?
      1. 核心是要知道location的组成,然后一步步判断即可
function isURL(str) {
  // 使用正则表达式进行 URL 验证
  // 这个正则表达式并不涵盖所有可能的情况,但对于一般的 URL 验证已经足够
  const urlRegex = /^(https?|ftp)://[^\s/$.?#].[^\s]*$/;
  return urlRegex.test(str);
}

// 示例用法
console.log(isURL("https://www.example.com")); // true
console.log(isURL("ftp://file.example.com")); // true
console.log(isURL("invalid-url")); // false

// ^: 开始位置
// (https?|ftp): 匹配 "http", "https", 或 "ftp"
// ://: 匹配 "://"
// [^\s/$.?#]: 匹配任何非空白字符、"/"、"$"、"."、"?""、"#"
// .*: 匹配零个或多个任意字符
// $: 结束位置

history

与浏览器的地址历史相关的,允许你在浏览器的历史记录中向前和向后导航,以及在历史记录中添加新的条目。

基础知识

属性

history.length返回历史记录的数量,number类型

history.state返回当前页面状态,object类型,值为null

方法

history.back()跳转到历史记录上一页,等价于浏览器后退一步的操作

history.forward()跳转到历史记录下一页,等价于浏览器前进一步的操作

history.go(n)跳转到历史记录的第 n 页,n 为正数表示向前,负数表示向后

history.go(-2); // 后退两页
history.go(3);  // 前进三页

history.pushState(state, title, url)跳转到指定 url,并新增历史记录。不会触发页面的重新加载,history 路由模式使用它来进行页面跳转的

history.pushState({page: 1}, "Page 1", "/page-1");

history.replaceState(state, title, url)跳转到指定 url,不会新增历史记录,并将之前地址的历史替换为新地址。不会触发页面的重新加载

history.history.replaceStat({page: 2}, "Page 2", "/page-1")

navigator

包含浏览器信息的对象

基础知识

属性

navigator.userAgent返回浏览器信息,string类型,包含浏览器类型、版本、操作系统信息等

Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) 
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 
Edg/119.0.0.0

navigator.userAgentData返回浏览器信息,object类型,包含浏览器类型、版本、操作系统信息等,更精确

{
  "brands": [{
    "brand": "Microsoft Edge", // 浏览器品牌
    "version": "119" // 浏览器版本
  }, {
    "brand": "Chromium",
    "version": "119"
  }, {
    "brand": "Not?A_Brand",
    "version": "24"
  }],
  "mobile": false,
  "platform": "macOS"
}

navigator.language返回用户首选语言,string类型,我的值为zh-CN

navigator.platform返回浏览器运行的平台,string类型,我的值为MacIntel

navigator.cookieEnable返回浏览器是否启用了 cookie,boolean类型

面试常问

  1. UA 的获取与解读
    1. 获取:navigator.userAgentnavigator.userAgentData
    2. 解读:按需解读即可,也可以借鉴 platform.js 库
  1. 如何获取剪切板的内容?
    1. 借用 navigator.clipbord实现读取剪切板:readText()

补充

navigation通常出现在 Web APIs 中,用于提供有关浏览器导航的信息。一个常见的使用场景是在 Service Worker 中使用

screen(有可视化经历必问)

浏览器设备的屏幕相关的

基础知识

属性

screen.height/screen.width:电脑屏幕的宽高,随浏览器设备的分辨率变化

screen.availHeight/screen.availWidth:电脑屏幕的可用宽高(减去了mac/window系统的上下任务栏的高度),随浏览器设备的分辨率变化

screen.orientation:屏幕方向对象,如screen.orientation.type横向(landscape)或纵向(portrait)

参考:如何在JavaScript中获取屏幕,窗口和网页大小_js 如何计算screenx

进阶知识

a. 获取屏幕的大小有哪些方式?

  • 使用screen.width/screen.height获取电脑屏幕的宽高,随浏览器设备的分辨率变化
  • 使用window.innerWidth/window.innerHeight获取浏览器窗口的宽高(包括滚动条),随浏览器窗口大小而变化
  • 使用document.documentElement.clientWidth/document.documentElement.clientHeight获取 html 元素的宽高(不包括滚动条)
  • 使用document.body.clientWidth/document.body.clientHeight获取 body 元素的宽高(不包括滚动条)

b. 这几个值之间的关系?

1>=2==3==4

c. 这几个值分别受什么影响?

1受设备分辨率影响

2受浏览器窗口大小影响

3受给 html 元素设置的 CSS 宽高影响

4受给 body 元素设置的 CSS 宽高影响

d. 如何监听浏览器的大小变化?

监听 resize 事件

window.addEventListener('resize', () => {
  // 获取窗口当前的大小
  const currentWidth = window.innerWidth;
  const currentHeight = window.innerHeight;
  // 其他逻辑判...
}

e. offsetWidth/offsetHeightclientWidth/clientHeight的关系

clientWidth/clientHeight=元素可视区域的宽/高 + padding(若有)-滚动条(若有)

offsetWidth/offsetHieght=clientWidth/clientHeight+边框(若有)+滚动条(若有)

f. 滚动条宽高计算方式有哪些?

ⅰ. 全局入手

window.innerWidth(浏览器宽度+滚动条(若有)) - document.documentElement.clientWidth(浏览器宽度)

ⅱ. 元素入手-1(自己想的)

元素(无边框+无padding+可滚动时).offsetWidth - 元素(无边框+无padding+可滚动时).clientWidth

ⅲ. 元素入手-2(iView 的做法)

元素(可滚动时).offsetWidth - 元素(未滚动前).offsetWidth

g. 常见的scroll*属性

ⅰ. scrollWidth/scrollHeight

元素的实际宽高,非可视区域的

若无滚动条,则等于clientWidth/clientHeight

若有滚动条,则等于实际内容宽高(一般跟子元素宽高有关)+padding(若有)

ⅱ. scrollLeft/scrollTop

元素最左端/最上端与窗口最左端/最上端的距离,即滚动条左/上滚的距离

ⅲ. scrollX/scrollY===pageXOffset/pageYOffset

返回元素滚动了的距离

举例:window.scrollX===pageXOffset===document.documentElement.scrollLeft

h. offsetLeft/offsetTop

返回元素的左/上端与最近定位元素(默认为body)的左/上边的距离

i. 如何元素监听滚动事件?

一般滚动的 HTML 结构为:父元素高度小于子元素高度并且父元素允许滚动,这样才能滚动

所以监听的滚动事件是设置在父元素上面的

父元素.addEventListener('scroll', function(){
  const { scrollTop } = this
})

j. 如何判断元素是否在可视区域内?

ⅰ. 计算法-父元素为任意元素

对比父元素.scrollTop子元素.offsetTop+子元素.offsetHeight(若需要)的差值,大于 0 表明还在,小于 0 表明不在

ⅱ. 计算法(getBoundingClientRect)-父元素仅为窗口

元素.getBoundingClientRect()获取元素与可视窗口(body 0,0)之间的关系,返回值为object类型

  • width:元素宽度
  • height:元素高度
  • left:元素左边与可视窗口的左边距离
  • right:元素右边与可视窗口的左边距离 = left + width
  • top:元素上边与可视窗口的上边距离
  • botton:元素下边与可视窗口的上边距离 = top + height
  • x:元素鼠标的 X 坐标,等价于 left
  • y:元素鼠标的 Y 坐标,等价于 top
  • 兼容性问题:IE 会多出 2px(边框计算有差异)

当元素同时满足以下4个条件时,则表明在可视窗口内:

  1. 元素.left >= 0
  2. 元素.top >= 0
  3. 元素.right <=窗口宽度(document.body.clientWidth)
  4. 元素.bottom <=窗口高度(document.body.clientHeight)
ⅲ. API 法-IntersectionObserver(性能最好)

采用IntersectionObserver来观察两个元素是否重叠

基本语法:new IntersectionObserver(callback, options),其中的 callback 将会在满足和不满足重叠时分别调用一次

官网文档:

Intersection Observer API:Intersection Observer API - Web API 接口参考 | MDN

IntersectionObserver:IntersectionObserver - Web API 接口参考 | MDN

entry 对象:IntersectionObserverEntry - Web API 接口参考 | MDN

const Observer = new IntersectionObserver((entries, oberver) =>{
  // entries:所有子元素的数组,下面 observe() 每次调用时,该数组就会新增长度
  // oberver:完整的 options 对象
  entries.forEach(entry => {
    const { 
      isIntersecting // 是否满足设置的 threshold 重叠,返回 boolean 值
    } = entry

    if(isIntersecting) {
      // do something....
    } else {
      // do otherthing....
    }
  })
},
{
  root: null // 监听的父元素,默认为视口,null||ElementDom
  threshold: 1 // 表示两个元素的重叠比例,number||number[]                                  
})

Observer.observe(childrenEl) // 调用 observe 触发观察,参数为 ElementDom 类型

面试常问

a. 如何 Tab 实现吸顶效果

思路:监听父元素的滚动事件,实时判断父元素的 scrollTop 是否大于 Tab 的 offsetTop 高度,若大于则说明该 Tab 将滚出可视区域了,所以马上设置固定定位,定位的 Top/Left 需要根据父元素的 offsetLeft/offsetTop

parentEl.addEventListener("scroll", function () {
  const { scrollTop } = this;
  const { offsetTop } = tabEl;

  if (scrollTop >= offsetTop) {
    tabEl.style.position = "fixed";
    tabEl.style.top = element.offsetTop + "px";
    tabEl.style.left = element.offsetLeft + "px";
  } else {
    tabEl.style.position = "relative";
    tabEl.style.top = 0;
    tabEl.style.left = 0;
  }
});

事件模型

常说的事件队列、EventLoop 是 JS 语言的机制

普通浏览器

概念

而这里的浏览器事件模型讲的是:浏览器处理事件传播的方式,分为三个阶段

  • 捕获:先从文档根节点向下传播到目标元素
  • 目标:到达目标元素,触发其绑定的事件(若有)
  • 冒泡:最后再从目标元素向上传播到文档根节点

事件注册

使用 DOM Level 0 事件处理程序:

element.onclick = (event) => { // event 事件对象
  // 这里的 this 指向全局对象(window)
  // event 参数是通过函数参数传递的事件对象(包含事件类型、触发元素、按键等)
  console.log(this);   // 输出全局对象(window)
  console.log(event);  // 输出事件对象
  
  // do something.....
}

// 优点:
// 1、简单粗暴的赋值

// 缺点:
// 1、this 指向 window,因为是作为普通函数在全局声明的,this 就被指向 window,无法通过 this
// 直接获取到触发的元素,需要加形参表示事件对象
// 2、只支持冒泡

使用 DOM Level 2 事件处理程序:

element.addEventListener('click', (event) => {
  // 这里的 this 指向触发的元素(dom)
  // event 参数是通过函数参数传递的事件对象(包含事件类型、触发元素、按键等)
  console.log(this);   // 输出触发的元素(dom)
  console.log(event);   // 输出事件对象
  console.log(event.target === this);   // 输出 true
  
  // do something.....
  
}, false) // true:捕获时触发,false:冒泡时触发(不传默认)

// 优点:
// 1、同一个元素可以绑定多个相同事件
// 2、第三个参数可以控制事件的触发阶段:冒泡或捕获

事件解绑

使用 DOM Level 0 事件处理程序:

element.onclick = null

使用 DOM Level 2 事件处理程序:

element.removeEventListener('click', callback)
// callback 必须与 addEventListener 的一致,否则将无法解绑

// 为了避免这种请情况,一般将 callback 单独抽出来写

阻止事件传播

event.stopPropagation()

阻止当前元素上的所有本事件的传播,不在触发捕获与冒泡,但已注册未执行的事件都会按序触发

白话:我自己执行完后,只是断掉传播,别人的还是能执行

假设:一个元素上绑定了 3 个 click 事件,若在之一写了event.stopPropagation(),其他两个的 click 事件会正常触发,但都没法传播了,但其他事件(scroll)不受影响

event.stopImmediatePropagation()

阻止当前元素上的所有本事件的传播,不在触发捕获与冒泡,并且已注册未执行的事件不会再触发

白话:我自己执行完后,不仅要断掉传播,并且让别人也没法执行,做事更绝。

假设:一个元素上绑定了 3 个 click 事件,若在之一写了event.stopImmediatePropagation(),其他未执行的 click 事件不会正常触发,并且从我开始阻断传播,但其他事件(scroll)不受影响

IE 浏览器(了解即可)

概念

  • IE 8 及更早版本只支持事件冒泡,不支持事件捕获。
  • IE 9 及之后的版本开始支持事件冒泡和捕获

事件注册

element.attachEvent('onclick', callback),必须带上on

attachEvent 是后绑定先执行

element.attachEvent('onclick', () => {
  const event = window.event; // 这样访问事件对象
}) // 默认为冒泡

事件解绑

element.detachEvent('onclick', callback),必须带上on,并且 callback 必须与 attachEvent 的一致,否则将无法解绑

阻止事件传播

event.cancelBubble = true

使用场景

  1. 事件代理:
    1. 基于冒泡,可将列表 item 的点击事件绑定到父级上,在父级上通过event.target获取到触发事件的元素,然后进行对应操作。节省子元素的 item 的获取与绑定效率

面试常问

1. 区分不同的阻止

event.stopPropagation()/event.stopImmediatePropagation()/event.preventDefault()

2. 手写多浏览器兼容的事件注册、解绑、阻止等

const EventModal = {
  // 事件注册
  addEventListener(element, eventType, callback, useCaption) => {
    if(element.addEventListener) {
      // 普通浏览器
      element.addEventListener(eventType, callback, useCaption)
    } else if(element.attachEvent) {
      // IE7/8
      element.attachEvent('on' + eventType, callback)
    } else {
      element['on' + eventType] = callback
    }
  },
  // 事件解绑
  removeEventListener(element, eventType, callback) => {
    if(element.removeEventListener) {
      // 普通浏览器
      element.removeEventListener(eventType, callback, useCaption)
    } else if(element.detachEvent) {
      // IE7/8
      element.detachEvent('on' + eventType, callback)
    } else {
      element['on' + eventType] = null
    }
  },
  // 事件传播阻止
  stopPropagation(event) => {
    if(event.stopPropagation) {
      // 普通浏览器
      event.stopPropagation()
    } else {
      // IE7/8
      event.cancalBubble = true
    }
  },
  // 默认事件阻止
  preventDefault(event) => {
    if(event.preventDefault) {
      // 普通浏览器
      event.preventDefault()
    } else {
      // IE7/8
      event.returnValue = false
    }
  }
}

网络

ajax - XMLHttpRequest 对象

Async JavaScript adn XML:通过异步方式与服务器进行数据交互

基本步骤

// 1、创建对象
const xhr = new XMLHttpRequest()

// 2、创建链接(不会发送):指定类型、请求地址、是否异步(默认异步)
xhr.open('POST', 'https://www.xxx.com/api/user1')

// 3、指定回调
xhr.onreadystatechange = () => {
  if(xhr.readyState == 4 && xhr.status == 200) {
    // 请求成功回调
    const data = JSON.parse(xhr.responseText)
  }
}

// 4、发送请求
xhr.send()

进阶设置

// 超时时间
xhr.timeout = 2000 // 单位毫秒
// 超时触发事件
xhr. ontimeout = () => {
  
}

详细解释

xhr.readyState的状态有:

  • 0:未初始化,尚未调用open方法
  • 1:启动,open方法已调用
  • 2:发送,send方法已调用
  • 3:接收,正在接收服务器数据
  • 4:完成,数据接收完毕,可以在浏览器用了

xhr.statusHTTP 的状态码,服务器返回的

  • 1xx:提示信息
  • 2xx:成功,200(请求成功)
  • 3xx:重定向,301(永久重定向),302(临时重定向)
  • 4xx:客户端错误,400(请求语法有错),401(请求未授权),403(服务器拒绝服务),404(请求资源不存在)
  • 5xx:服务器错误,500(服务器错误),503(服务器已不能处理请求)

自行封装

// 使用方式:
ajax({
  url: 'http:www.xxx.com/api/xx',
  method: 'get',
  data: {
    userId: 1
  },
  timeout: 3000,
}).then(res => {
  console.log('成功:', res)
},err => {
  console.log('失败:', err)
})

// 补充完下面的函数
const ajax = (options) => {
  return new Promise((resolve, reject) => {
    const { url, method, data, timeout } = options
    const _method = method.toUpperCase()
    let _data = null
    let _url = url

    if(_method === 'GET') {
      // 参数字符串处理
      const queryString = Object.keys(data||{}).map(key => {
        return encodeURLComponent(key) + '=' + encodeURLComponent(data[key])
      }).join('&')

      _url += '?' + queryString
    } else {
      _data = data || null

      if(_data) {
        _data = JSON.stringify(_data)

        xhr.setRequestHeader('Content-type', 'application/json;charset=UTF-8')
      }
    }

    // 1、创建对象
    const xhr = new XMLHttpRequest()

    // 2、创建连接
    xhr.open(_method, _url)

    // 3、成功触发事件
    xhr.onreadystatechange = () => {
      if(xhr.readyState === 4) {
        if(xhr.status === 200) {
          const data = JSON.parse(xhr.responseText)
          resolve && resolve(data)
        } else {
          reject && reject()
        }
      }
    }

    // 4、发送请求
    xhr.send(_data)

    // 5、超时时间
    xhr.timeout = timeout

    // 6、超时触发事件
    xhr.ontimeout = () => reject && reject('超时')
    
    // 7、失败触发事件
    xhr.onerror = (err) => reject && reject(err)
  })
}

浏览器缓存

访问资源时,不必重新请求。

强缓存

明确告知浏览器一定时间内使用本地缓存

Cach-Control

  • max-age: 定义资源被认为是新鲜的最长时间(秒)。
  • s-maxage: 覆盖 max-age,但仅适用于共享缓存(比如代理服务器)。
  • public: 允许所有内容缓存,包括代理服务器。
  • private: 仅允许终端用户缓存。

Expries资源过期时间,服务器返回的 HTTP 头

协商缓存

浏览器向服务器发请求验证缓存是否有效

  • Last-Modified / If-Modified-Since:
    • 服务器通过 Last-Modified 头告诉浏览器资源的最后修改时间。
    • 浏览器通过 If-Modified-Since 头将最后的修改时间发送给服务器,如果时间仍然一致,服务器返回 304,表示资源未被修改,可以使用本地缓存。
  • ETag / If-None-Match:
    • 服务器通过 ETag 头生成一个唯一的标识符(通常是文件内容的哈希值)。
    • 浏览器通过 If-None-Match 头将该标识符发送给服务器,如果标识符仍然一致,服务器返回 304。

面试常问

1. RESTFUL 请求的区别 - GET、POST、DELETE、PUT、OPTION

GETPOSTDELETEPUTOPTION
获取数据提交数据删除数据更新数据预检,询问服务器支持哪些请求类型,通常在 CORS 请求中进行预检
参数在 url 上,有长度限制参数在请求体上,浏览器/服务器有大小限制参数在 url 上,有长度限制参数在请求体上,浏览器/服务器有大小限制无参数

2. 跨域问题

同源策略:协议、域名、端口都一致

跨域的原因是:浏览器的同源策略导致的,非同源的不允许访问

解决办法:

  1. 本地环境:可配置代理
  2. 生产环境:服务端设置响应头,允许跨域(CORS-Cross Origin Resource Share)

补充知识

window.requestIdleCallback(callback,options)

允许在浏览器空闲时执行一些任务,以避免阻塞用户界面的流畅性。通常用于拆分大型任务(任务分片),确保不会在主线程执行太久而导致影响用户体验

用之前检查兼容性

window.requestIdleCallback(callback, options);
// callback 是一个在浏览器空闲时执行的函数。
// options 是一个配置对象,可选,包含以下属性:
	// timeout:一个时间,指定在执行回调前等待的最长时间。
	// 如果空闲时间小于 timeout,则会立即执行回调。
function myIdleCallback(deadline) {
  // deadline 是一个对象,包含有关浏览器何时认为它将再次空闲的信息
  console.log(`剩余空闲时间:${deadline.timeRemaining()}ms`);

  // 执行一些任务
  for (let i = 0; i < 1000; i++) {
    // 一些计算密集的操作
  }

  // 如果任务未完成,可以请求下一次空闲回调
  if (deadline.timeRemaining() > 0) {
    window.requestIdleCallback(myIdleCallback);
  }
}

// 启动第一次空闲回调
window.requestIdleCallback(myIdleCallback);

实际场景举例:

  • 性能监测和分析:空闲时可以做
  • 自动保存和同步:空闲时可以做
  • 前端性能优化:空闲时预加载资源、图片等

window.requestAnimationFrame(callback)

可在浏览器下一次重绘之前执行指定的回调函数,以确保动画的平滑运行

function animate(timestamp) {
  console.log(timestamp); // timestamp 回调被触发的时间戳(动画开始时的时间)

  // 在这里执行动画相关的操作
  // do something.......

  // 继续下一帧动画
  window.requestAnimationFrame(animate);
}

// 启动动画
window.requestAnimationFrame(animate);

阻止默认事件

event.preventDefault()阻止浏览器的默认行为,比如:a 标签点击后会默认跳转

envnt.returnValue = falseIE 阻止浏览器的默认行为