浏览器的相关知识

477 阅读9分钟

欢迎访问我的博客

浏览器对象模型(BOM)

BOM包含5个东西:

  • location 管理 URL
  • navigator 管理浏览器
  • history 管理历史记录
  • screen 管理屏幕
  • window 管理浏览器所有的东西

location相关操作

// 改变 href 属性就可以跳转页面, 相对路径就是放在原url的后面拼接,绝对路径就是替换
location.href = 'www.baidu.com'
location.href = '#shabi'
// reload 刷新页面
location.reload()
// replace 替换当前页面
location.replace(location.href, 'www.baidu.com')

navigator相关操作

// 比较需要关注的是userAgent这个属性
// 通过对navigator.userAgent的处理可以判断当前的平台环境(andoriod, ios, web)
var u = navigator.userAgent;
var isAndroid = u.indexOf('Android') > -1 || u.indexOf('Adr') > -1; //android终端
var isiOS = !!u.match(/(i[^;]+;( U;)? CPU.+Mac OS X/); //ios终端

var browser={
    versions: function(){
        var u = navigator.userAgent, app = navigator.appVersion;
        return {
            trident: u.indexOf('Trident') > -1, //IE内核
            presto: u.indexOf('Presto') > -1, //opera内核
            webKit: u.indexOf('AppleWebKit') > -1, //苹果、谷歌内核
            gecko: u.indexOf('Gecko') > -1 && u.indexOf('KHTML') == -1,//火狐内核
            mobile: !!u.match(/AppleWebKit.*Mobile.*/), //是否为移动终端
            ios: !!u.match(/(i[^;]+;( U;)? CPU.+Mac OS X/), //ios终端
            android: u.indexOf('Android') > -1 || u.indexOf('Adr') > -1, //android终端
            iPhone: u.indexOf('iPhone') > -1 , //是否为iPhone或者QQHD浏览器
            iPad: u.indexOf('iPad') > -1, //是否iPad
            webApp: u.indexOf('Safari') == -1, //是否web应该程序,没有头部与底部
            weixin: u.indexOf('MicroMessenger') > -1, //是否微信 (2015-01-22新增)
            qq: u.match(/sQQ/i) == " qq" //是否QQ
        };
    }(),
    language:(navigator.browserLanguage || navigator.language).toLowerCase()
}

//判断是否IE内核
if(browser.versions.trident){ alert("is IE"); }
//判断是否webKit内核
if(browser.versions.webKit){ alert("is webKit"); }
//判断是否移动端
if(browser.versions.mobile||browser.versions.android||browser.versions.ios){ alert("移动端"); }

history相关操作

history对象是用来处理历史记录的, 在 HTML5 它增加了一些 API 使得它也可以做单页应用

/*
pushState的 三个参数分别是:
自定义对象, 新页面的标题, 新页面的地址
跳转到新页面地址的时候,浏览器不会刷新页面
history.pushState 并不会触发popstate事件, 除了将数据push进入历史栈
它还会改变loaction当前的属性
*/
history.pushState(null, 'title', "/profile")
// 用户点击前进和后退按钮的时候, 会触发window的popstate事件
// 前进:history.forward(), history.go()
// 后退:history.back()
window.addEventListener("popstate", function(e) {
    // state 就是 pushState 的第一个参数
    var state = e.state;
    console.log('pop state', state)
})
// replaceState的作用和pushState一样, 只是不生成一条历史纪录
history.replaceState(state1, state2)
// 表示会话历史中元素的数目
history.length

spa的原理机制

这里有两种实现方式, 一种是哈希路由的方式,另一种是history的机制. 所谓单页Web应用, 就是只有一张Web页面的应用. 是加载单个HTML 页面并在用户与应用程序交互时动态更新该页面的Web应用程序. 不像以前所有的页面跳转都要新开窗口,用户体验不好.另外最重要的一点是不会刷新页面(也就是说减少了不必要的刷新操作). 另外为啥不用ajax实现spa因为ajax的 实现方式没有记住页面状态的能力

哈希路由的方式, hash值得改变不会导致浏览器向服务器发送请求,并且改变的时候会触发hashchange事件, hashchange可以捕捉url的变化从而实现spa.

<body>
  <h1>spa hash demo</h1>
  <ul>
    <li>
      <a href="#/">home</a>
      <a href="#/arena">arena</a>
      <a href="#/questions?name=woyao">questions</a>
      <a href="#/classroom?name=woyao&height=171">classroom</a>
    </li>
  </ul>
  <div class="route"></div>
</body>
const log = console.log.bind(console)

const renderHtml = (text) => {
  let element = document.querySelector('.route')
  element.innerHTML = text
}

const responseForPath = (path) => {
  let mapper = {
    '/': 'home page',
    '/arena': 'arena page',
    '/questions': 'question page',
    '/classroom': 'classroom page',
  }
  if (path in mapper) {
    return mapper[path]
  } else {
    return 'not found'
  }
}

const argsFromQuery = (query) => {
  let o = {}
  let qs = query.split('&')
  qs.forEach(e => {
    let [k, v] = e.split('=')
    o[k] = v
  })
  return o
}

const parsedUrl = (url) => {
  let path = ''
  let query = {}
  let index = url.indexOf('?')
  if (index > -1) {
    path = url.slice(0, index)
    let q = url.slice(index + 1)
    query = argsFromQuery(q)
  } else {
    path = url
  }
  return {
    path,
    query,
  }
}

const render = () => {
  log('location.hash', location.hash)
  // 再用 parsedUrl 解析这个地址, 得到 path 和 query
  let { path, query } = parsedUrl(location.hash.slice(1))
  let r = responseForPath(path)
  renderHtml(r)
}

const bindEventHashChange = () => {
  window.addEventListener('hashchange', (event) => {
    // event.oldURL 表示变化之前的 URL, event.newURL 表示变化之后(也就是现在)的 URL
    log('event url', event.oldURL, event.newURL)
    render()
  })
}

const bindEvents = () => {
    bindEventHashChange()
}

const __main = () => {
	bindEvents()
  // 初始化渲染
	render()
}

// DOMContentLoaded 事件表示 HTML 已经加载(渲染)到页面中, 这个时候操作 DOM 元素就没有问题
document.addEventListener('DOMContentLoaded', () => {
  __main()
})

history spa

<h1>spa history demo</h1>
<ul>
  <li>
    <a href="/">home</a>
    <a href="/arena">arena</a>
    <a href="/questions?name=gua">questions</a>
    <a href="/classroom?name=gua&height=169">classroom</a>
  </li>
</ul>
<div class="route"></div>
// 代码几乎和hash的那段js代码一样,只是事件机制这边处理不一样, 我就不再重复写一次了
const bindEventPopState = () => {
  // 注意: 调用 history.pushState 并不会触发 popstate 事件
  // 当使用浏览器的前进、后退功能时会触发这个事件
  window.addEventListener('popstate', (event) => {
    // event.state 的值是 history.pushState 调用时传入的第一个参数
    log('pop state', event.state)
  })
}

const bindEventLink = () => {
  let links = document.querySelectorAll('a')
  for (let l of links) {
    l.addEventListener('click', (event) => {
      event.preventDefault()
      let self = event.target
      let path = self.href
      let state = {
        'path': path
      }
      history.pushState(state, '', path)
      render()
    })
  }
}

const bindEvents = () => {
  bindEventPopState()
  bindEventLink()
}

screen相关操作

// 可用的屏幕的宽度和高度
screen.availWidth; screen.availHeight

回流与重绘

  • 回流

    renderTree: 简单的理解就是DOM Tree和我们写的CSS结合在一起之后,渲染出了render tree

    回流: 当render tree中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建。

    • 导致回流的操作

      1. 页面首次渲染
      2. 浏览器窗口大小发生改变
      3. 元素尺寸或位置发生改变
      4. 元素内容变化(文字数量或图片大小等等)
      5. 元素字体大小变化
      6. 添加或者删除可见的DOM元素
      7. 激活CSS伪类(例如::hover)
      8. 查询某些属性或调用某些方法
    • 如何避免

      1. 避免使用table布局。

      2. 尽可能在DOM树的最末端改变class。

      3. 避免设置多层内联样式。

      4. 将动画效果应用到position属性为absolute或fixed的元素上。

      5. 避免使用CSS表达式(例如:calc())

      6. 避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性。

      7. 避免频繁操作DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中。

      8. 也可以先为元素设置display: none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。

      9. 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。

  • 重绘

    当render tree中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格. 而不会影响布局的,比如background-color。浏览器会将新样式赋予给元素并重新绘制它 这个过程称为重绘

    • 导致重绘的操作
      1. 当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility等)

url输入到渲染过程

  • DNS域名解析,根据url找到对应的服务地址

  • 三次握手构建TCP连接

  • 发送HTTP请求

  • 服务端响应请求,返回相应的资源文件

  • 解析文档

  • 构建 DOM 树和 CSSOM(css object modal)

  • 生成渲染树(render tree):从DOM树的根节点开始遍历每个可见节点,对于每个可见的节点,找到CSSOM树中对应的规则,并应用它们,根据每个可见节点以及其对应的样式,组合生成渲染树

  • Layout(回流):根据生成的渲染树,进行回流(Layout),得到节点的集合信息

  • Painting(重绘):根据渲染树及其回流得到的集合信息,得到节点的绝对像素

  • 绘制,在页面上展示,这一步还涉及到绘制层级、GPU相关的知识点

  • 加载js脚本,加载完成解析js脚本

浏览器的多进程

一般情况下在浏览器中每个tab页面可以算作一个进程。可以通过打开chorme浏览器的更多工具->任务管理器 查看当前浏览器中正在运行哪些进程

chorme 浏览器的主要进程和职责

  • Browser Process 浏览器的主进程(负责协调、主控)
    • 负责包括地址栏,书签栏,前进后退按钮等部分的工作
    • 负责处理浏览器的一些不可见的底层操作,比如网络请求和文件访问
    • 负责各个页面的管理,创建和销毁其他进程
  • Renderer Process负责一个 tab 内关于网页呈现的所有事情,页面渲染,脚本执行,事件处理等
  • Plugin Process负责控制一个网页用到的所有插件,如 flash 每种类型的插件对应一个进程,仅当使用该插件时才创建
  • GPU Process 负责处理 GPU 相关的任务

Renderer Process进程里的多个线程

  • GUI渲染线程

    负责渲染浏览器界面HTML元素,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行,在Javascript引擎运行脚本期间,GUI渲染线程都是处于就绪状态的

  • javaScript引擎线程

    JS内核,负责处理Javascript脚本程序。 一直等待着任务队列中任务的到来,然后解析Javascript脚本,运行代码。GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞

  • 定时触发器线程

    定时器setInterval与setTimeout所在线程。浏览器定时计数器并不是由JavaScript引擎计数的 因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确, 因此通过单独线程来计时并触发定时是更为合理的方案。

  • 事件触发线程

    用来控制事件轮询,JS引擎自己忙不过来,需要浏览器另开线程协助 当JS引擎执行代码块如鼠标点击、AJAX异步请求等,会将对应任务添加到事件触发线程中 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理任务队列的队尾,等待JS引擎的处理 由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)

  • 异步http请求线程

    在XMLHttpRequest在连接后是通过浏览器新开一个线程请求, 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到 JavaScript引擎的处理队列中等待处理。

关于js单线程

上面所说的线程是不能并行的, 是一个单线程,原因如下:

​ 假设存在两个线程同时操作一个DOM,一个负责修改一个负责删除,那么这个时候就需要浏览器来裁决如何生效哪个线程的执行结果。如果没懂可以看看阮一峰的博客 学过操作系统的进程调度知道可以用锁的机制解决,但是会使得javascript的实现变得复杂,想想当初javascript是一天就搞出来的