浏览器工作原理与实践

162 阅读13分钟

主讲:云隐

一、 认识在浏览器运行态下的 JS

包含:BOMDOMECMAScript

(function (context, undefined) {
  const _class = ['js', 'browser', 'vue'];

  // 向全局中挂载
  window.classArr = _class.map(item => item);

  // 获取当前页面地址
  const _url = location.href;

  // 设置 tab 标题(一个浏览器的 tab 就是一个进程)
  document.title = 'zhaowa class';

  // 获取渲染节点
  document.getElementById('app');
})(this);

追问:了解浏览器 JS 的执行态(分层)?

简述:

  • ECMAScript - 负责基础逻辑、数据处理;
  • DOM - 对于浏览器视窗内,文本的相应操作;
  • BOM - 对于浏览器本身区域能力的处理;

二、BOM - 浏览器对象模型

什么是浏览器对象模型

BOMBrowser Object Model(浏览器对象模型),浏览器模型提供了独立于内容的、可以与浏览器窗口进行滑动的对象结构,就是浏览器提供的 API

其主要对象有:

  1. window 对象 —— BOM 的核心,是 js 访问浏览器的接口,也是 ES 规定的 Global 对象;
  2. location 对象:提供当前窗口中的加载的文档有关的信息和一些导航功能。既是 window 对象属性,也是 document 的对象属性
    • window.location
    • document.location
  3. navigation 对象:获取浏览器的系统信息;
  4. screen 对象:用来表示浏览器窗口外部的显示器的信息等;
  5. history 对象:保存用户上网的历史信息;

1、Window

window 对象是整个浏览器对象模型的核心,其扮演着既是接口又是全局对象的角色,所有在全局作用域下声明的变量和内容最终都在变成 window 对象下的属性。比如:

var num = 123;

console.log(window.num); // 123

访问未声明的变量时,如果直接访问则会报错,而如果使用 window 进行访问,就像通过对象访问那样,会返回 undefined

var name = oldName; // 报错

var name2 = window.oldName; // undefined
  • window.alert()
  • window.confirm()
  • window.prompt()
  • window.open()
  • window.onerror():用于监控错误
  • window.setTimeout()
  • window.setInterval()

setTimeout 和 setInterval

setTimeoutsetInterval 他们都可以接受两个参数,第⼀个参数是⼀个回调函数,第二个参数是等待执行的时间在等待时间结束之后,就会将回调函数放到 event loop 中进行执行。他们都返回⼀个 id,传入 clearTimeoutclearInterval 能够清除这次的定时操作。

var id = setTimeout(function () {
  console.log('hello world');
}, 2000);
console.log(id); // 1

clearTimeout(id);

可视化工具网站:latentflip.com/loupe/

假设我们点击事件之后会触发 setInterval(func, 500),那么每隔 500ms 就会将 func 放入⼀次消息队列,如果此时主栈中有其他代码执行的话,就会等待其他代码执行之后再读取消息队列中的函数执行。但对于 setInterval,仅当没有该定时器的任何其他代码实例时,才将定时器代码添加到队列中,所以就会造成某个瞬间有次回调函数没有加进事件队列中去,造成丢帧。

setInterval-1.png 使用 setTimeout 模拟之后的样子,每次执行完成之后再将下次的事件推入事件队列中:

setTimeout-1.png 使用 setTimeout 模拟 setInterval

// 使用 setTimeout 模拟 setInterval
function _setTimeout() {
  var timer = setTimeout(function () {
    console.log(1);
    _setTimeout();
    clearTimeout(timer);
  }, 1000);
}

_setTimeout();

alert,confirm,prompt 等交互相关 API

alert 会弹出⼀个警告框,而 confirmprompt 则可以与用户交互,confirm 会弹出⼀个确认框,最终返回 true(用户点击确定)返回 false(用户点击取消)而 prompt 用户则可以输入⼀段文字,最终返回用户输出的结果。

这里使用了这类 API 之后,会导致页面 JS 停止执行,需要我们格外慎重。

2、Location

提供当前窗口中的加载的文档有关的信息和一些导航功能。既是 window 对象属性,也是 document 的对象属性。

空页面的时候(Chrome)

image-20220407235053910.png

Location

image-20210510230535063.png

例子:https://www.zhaowa.com/search?class=browser&id=2#comments

属性:

  • href:当前 URL全部

  • origin:返回⼀个 URL域名https://www.zhaowa.com);

  • protocol:返回⼀个 URL 协议https:)【带冒号】;

  • host:返回⼀个 URL主机名和端口www.zhaowa.com);

  • hostname:返回 URL主机名

  • port: 返回⼀个 URL 服务器使用的端口号

  • pathname: 返回的 URL 路径名/search/);

  • search:返回⼀个 URL查询部分?class=browser&id=2);

  • hash:返回⼀个 URL锚点部分#comments);

方法:

  • location.assign(''):跳转到指定 path,替换的是 pathname
  • location.replace(newURL):用新的页面替换当前页面;
    • replace() 方法不会在 History 对象中生成一个新的记录。当使用该方法时,新的 URL 将覆盖 History 对象中的当前记录,浏览器不能返回到上一个页面了;
    • location.replace("https://www.baidu.com/")
  • location.reload():重新载入当前页面;
  • location.toString():产出当前地址字符串;

面试方向:

  1. location 本身 api 操作:
    • 提取相关信息
    • api 间的对比,例如 assign vs replace
  2. 路由相关:跳转、参数、操作
    • 跳转:
      • hashhistory 跳转的区别,利用了什么 api
      • hash 为什么可以使浏览器不刷新?
        • 因为 hash 只改变了 # 后面的内容,不会改变跟地址;
    • 参数:
      • 通过 ? 的方式去处理,多参数的时候通过 & 去拼接;
      • 如何去读取参数?
      • 如果浏览器中如果存数组、对象,url 如何去处理?(decodeencode
    • 操作:
      • assignreplace 怎么去做,带来的副作用是什么?
      • 场景题:返回的路径不对? —— 可返回(history 模式可返回),是否刷新(hash 模式不会刷新)
        • 是不是使用了 replace,把前一次的历史记录清空了;
          • 如何规避:把 replace 替换为 assign
        • 是不是跳转到了第三方页面,从第三方返回的时候,从外部链接返回来自己应用链接的时候,后台会动态指向一个默认的值,所以地址不对了;
          • 如何规避:可以携带参数跳回,根据参数做判断,比如在路由的导航守卫中做预处理;
  3. url 处理:提取所有的参数,变成键值对的形式
    • 方案:1、正则;2、手写 js 处理;
  4. 理论类:URI & URL 区别
    • URIUniform Resource Identifier 统一资源标识符;
    • URLUniform Resource Location 统一资源定位符;

2、history

History 对象保存着用户上网的历史记录,从窗口被打开的那一刻算起,history 对象是用窗口的浏览历史用文档和文档状态列表的形式表示。在 HTML5 中,history 还与客户端路由息息相关。

属性:

  • history.state:存储当前页面的状态;
  • history.length: 返回历史列表中的网址数;

方法:

  • history.go():加载 history 列表中的某个具体页面;

  • history.back():加载 history 列表中的前⼀个 URL,相当于 go(-1)

  • history.forward():加载 history 列表中的下⼀个 URL,相当于 go(1)

  • history.pushState():替换地址栏地址,并且加入 history 列表,但并不会刷新页面

    • 参数 1:data
    • 参数 2:title
    • 参数 3:url
    • history.pushState({ page: 1 }, "page-1", "/pushState");
  • history.replaceState():替换地址栏地址,替换当前页面在 history 列表中的记录,并不会刷新页面

    • 参数 1:data
    • 参数 2:title
    • 参数 3:url
    • history.replaceState({ page: 2 }, "page-2", "/replaceState");
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <button onclick="pushState()">pushState</button>
    <button onclick="replaceState()">replaceState</button>
    <script>
      function pushState() {
        // 参数说明:data,title,url
        history.pushState({ page: 1 }, 'page-1', '/pushState');
      }

      function replaceState() {
        history.replaceState({ page: 2 }, 'page-2', '/replaceState');
      }

      window.onpopstate = function () {
        console.log(history, history.state);
      };
      /* 
        每当处于激活状态的历史记录条目发生变化时,popstate 事件就会在对应 window 对象上触发。
        如果当前处于激活状态的历史记录条目是由 history.pushState() 方法创建,或者由 history.replaceState() 方法修改过的,
        则 popstate 事件对象的 state 属性包含了这个历史记录条目的 state 对象的一个拷贝。

        注意:调用 history.pushState() 或者 history.replaceState() 不会触发 popstate 事件。
        popstate 事件只会在浏览器某些行为下触发, 比如点击后退、前进按钮(或者在 JavaScript 中调用 history.back()、history.forward()、history.go() 方法),
        此外,a 标签的锚点也会触发该事件。
      */
    </script>
  </body>
</html>

面试方向:

  • 路由方向:historyhash 的模式利弊;

3、navigator

浏览器系统信息大集合;

navigator.userAgent; // 获取当前用户的环境信息

// 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.60 Safari/537.36'

面试方向:

  1. userAgent 读取信息
    • 浏览器兼容性
    • 上报信息
  2. 剪切板、键盘

4、screen

表征显示区域 - 荧幕;

面试方向:判断区域大小

  • window 视窗判断:

    • 全局入口处:
      • window.innerWidth
      • window.innerHeight
    • 文本处获取:
      • document.documentElement.clientWidth
      • document.documentElement.clientHeight
      • document.body.clientWidth
      • document.body.clientHeight
  • 网页视图的 size

    • offsetHeightclientHeight 区别:offsetHeight = clientHeight + 滚动条 + 边框
    • document.documentElement.offsetWidth
    • document.documentElement.offsetHeight
    • document.body.offsetWidth
    • document.body.offsetHeight
  • 动态定位:

    • scrollLeft / scrollTop:距离常规左 / 上滚动距离
    • offsetLeft / offsetTop:距离常规左 / 上距离
  • el.getBoundingClientRect():方法返回元素的大小及其相对于视口的位置;

    • el.getBoundingClientRect().top
    • el.getBoundingClientRect().left
    • el.getBoundingClientRect().bottom
    • el.getBoundingClientRect().right
  • 兼容性 - 老版本 IEIE9 之前) 是会多出来 2 像素;

event 对象下的方法

  • preventDefault:取消事件的默认行为;

三、 Event 事件模型

浏览器事件模型中的过程主要分为三个阶段:捕获阶段、目标阶段、冒泡阶段

事件的捕获和冒泡.png

event 对象下的属性

  • bubbles:表明事件是否冒泡;
  • cancelable:表示是否可以取消事件的默认行为;
  • currentTarget:事件当前正在处理的元素**【绑定的元素】**;
    • this 是一样的;
  • defaultPrevented:为 true 则代表已经调用了 preventDefault 函数;
  • detail:事件细节;
  • eventPhase:事件所处阶段
    • 1:代表捕获;
    • 2:代表在事件目标;
    • 3:代表冒泡;
  • type:事件类型(click 等);

event 对象下的方法

<div id="app">
  <p id="dom"></p>
</div>
// 事件的传递 - 冒泡和捕获
// 冒泡 - ms(微软)提出: p => div => body => HTML => document
// 捕获 - ns(网景)提出: document => HTML => body => div => p

el.addEventListener(event, function () {}, useCapture); // useCapture 是否捕获,默认值 false
// useCapture: false - 冒泡,默认值
// useCapture: true - 捕获
  • preventDefault:取消事件的默认行为;
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <a href="/test" id="a">链接</a>

    <script>
      const $a = document.getElementById('a');
      $a.addEventListener('click', function (event) {
        event.preventDefault(); // 取消事件的默认行为,a 链接不会跳转了
      });
    </script>
  </body>
</html>
  • stopPropagation:取消事件的进一步捕获或冒泡
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="parent">
      <div id="child">child</div>
    </div>

    <script>
      const $parent = document.getElementById('parent');
      const $child = document.getElementById('child');

      $parent.addEventListener('click', function (event) {
        console.log('parent click');
      });

      $child.addEventListener('click', function (event) {
        event.stopPropagation(); // 取消事件的进⼀步捕获或冒泡,此时 parent 的行为不会触发

        console.log('child1 click');
      });

      $child.addEventListener('click', function (event) {
        console.log('child1 click again');
      });

      /* 
        此时输出结果:
          child1 click
          child1 click again
      */
    </script>
  </body>
</html>
  • stopImmediatePropagation:取消事件的进一步捕获或冒泡,同时阻止事件处理程序调用;
    • 如果有多个相同类型事件的事件监听函数绑定到同一个元素,当该类型的事件触发时,它们会按照被添加的顺序执行。如果其中某个监听函数执行了 event.stopImmediatePropagation() 方法,则当前元素剩下的监听函数将不会被执行;
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="parent">
      <div id="child">child</div>
    </div>

    <script>
      const $parent = document.getElementById('parent');
      const $child = document.getElementById('child');

      $parent.addEventListener('click', function (event) {
        console.log('parent click');
      });

      $child.addEventListener('click', function (event) {
        event.stopImmediatePropagation(); // 后面的事件行为也取消了,例如:下面的 child2 click 就不会输出
        console.log('child1 click');
      });

      $child.addEventListener('click', function (event) {
        console.log('child2 click');
      });

      /* 
        此时输出结果:
          child1 click
      */
    </script>
  </body>
</html>

追问:【三个方法要清楚,不能混】

  1. 如何阻止事件的传播
    • event.stopPropagation();
  2. 阻止默认事件 —— 例如 a 标签
    • event.preventDefault();
  3. 相同节点绑定多个同类事件
    • event.stopImmediatePropagation();

引申型面试核心: 兼容性 & 性能

  • 兼容性:手写兼容性事件绑定(IEattachEventW3CaddEventListener
  • 区别:
    • 传参:attachEvent 对于事件名需要加上 on
    • 执行顺序:attachEvent - 后绑定先执行;addEventListener - 先绑定先执行;
    • 解绑:detachEvent vs removeEventListener
    • 阻断:event.cancelBubble = true vs event.stopPropgation()
    • 默认事件拦截:event.returnValue = false vs event.preventDefault()
  • 性能优化:使用事件代理

手写兼容性事件绑定

手写兼容性事件绑定 - attachEventaddEventListener

class bindEvent {
  constructor(element) {
    this.element = element;
  }

  // 绑定
  addEventListener = (type, handler) => {
    if (this.element.addEventListener) {
      this.element.addEventListener(type, handler, false); // 这里统一走冒泡形式,因为 IE 不支持捕获
    } else if (this.element.attachEvent) {
      this.element.attachEvent('on' + type, function () {
        handler.call(element);
      });
    } else {
      this.element['on' + type] = handler;
    }
  };

  // 解绑
  removeEventListener = (type, handler) => {
    if (this.element.removeEventListener) {
      this.element.removeEventListener(type, handler, false);
    } else if (this.element.detachEvent) {
      this.element.detachEvent('on' + type, function () {
        handler.call(element);
      });
    } else {
      this.element['on' + type] = null;
    }
  };

  // 阻断
  static stopPropagation(e) {
    if (e.stopPropagation) {
      e.stopPropagation();
    } else {
      e.cancelBubble = true;
    }
  }

  // 默认拦截
  static preventDefault(e) {
    if (e.preventDefault) {
      e.preventDefault();
    } else {
      e.returnValue = false;
    }
  }
}

性能优化 - 事件代理

<ul class="list">
  <li>1</li>
  <li>2</li>
  <li>3</li>
  <li>4</li>
  <li>5</li>
  <li>6</li>
</ul>
<div class="content"></div>
// 获取节点
var list = document.querySelector('list');
var li = list.getElementsByTagName('li');

// 硬碰硬写法
for (var i = 0; i < li.length; i++) {
  li[i].addEventListener('click', function () {
    // 业务逻辑...
  });
}
  • 缺点:
    • 这种写法是对每一个节点上都绑定了 addEventListener,如果节点多的时候,要在每一个节点上绑定事件,损耗性能;
    • 如果是动态节点,节点的数量有可能会增加的,那么要在增加的逻辑上也要加上绑定事件;
// 代理后 - 利用事件传递(冒泡和捕获),减少了对 DOM 的事件绑定
function onClick(e) {
  var e = e || window.event;

  if (e.target.nodeName.toLowerCase() === 'li') {
    // 业务逻辑...
  }
}

// 此处利用了冒泡
list.addEventListener('click', onClick, false);

四、网络层

ajax 的原理:

// 1、实例化
const xhr = new XMLHttpRequest();

// 2、初始化建立
/* 
  method:请求方式 get/post
  url:请求的地址
  async:是否为异步请求
*/
xhr.open(method, url, async);

// 3、方法的发送请求 - send
/* 
  data 的处理:
    get:可以不传或传入 null
    post:encodeURIComponent 编码拼接
*/
xhr.send(data);

// 4、接收
xhr.onreadystatechange = () => {
  /* 
    xhr.readyState 的状态:
      0:尚未建立 open
      1:已经调用 open 方法但未调用 send 方法
      2:已经调用 open 方法但尚未收到返回
      3:已经收到请求返回数据
      4:请求已经完成
  */
  /* 
    面试问题:状态 3 和 4 的区别?为什么要用 4 做判断?
      状态 4 是已经请求完成,后台把请求结束,把 http 的状态码返回,并且标识请求成功,关闭链接,代表整个周期的完成;
      状态 3 是后台把请求的数据返回回来,从此时开始到关闭整个链接之前,还是可以有其他操作的;
  */
  if (xhr.readyState === 4) {
    // 判断 http 状态码
    // 为什么 304 也可以?
    if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
      // callback(xhr.responseText)
    }
  }
};

// 超时时间
xhr.timeout = 30000;
// 超时回调
xhr.ontimeout = () => {
  // 超时后的操作
};

面试方向:

  • TCP => HTTP/HTTPS;
  • 状态码
    • 2xx 4xx 5xx | 3xx
  • 浏览器缓存
    • 强缓存(Expires + cache-control) / 协商缓存(last-modified + Etag

封装手写:

// 使用方式:
ajax({
  url: 'reqUrl',
  method: 'get',
  async: true,
  timeout: 30000,
  data: {
    payload: 'text'
  }
})
  .then(
    res => {},
    err => {}
  )
  .catch(err => {});
// 代码实现:
function ajax(options) {
  const { url, method, async, data, timeout } = options;

  const xhr = new XMLHttpRequest();

  // 配置超时时间
  if (timeout) {
    xhr.timeout = timeout;
  }

  return new Promise((resolve, reject) => {
    // 成功情况
    xhr.onreadystatechange = () => {
      if (xhr.readyState === 4) {
        // 判断 http 状态码
        if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
          // 返回拦截器处理...
          // ...

          resolve(xhr.responseText);
        } else {
          reject();
        }
      }
    };

    // 失败情况
    xhr.onerror = err => reject(err);
    xhr.ontimeout = () => reject('请求超时 timeout');

    // 传参处理
    let _params = [];
    let encodeData = '';
    if (data instanceof Object) {
      for (let key in data) {
        // 参数编码
        /* 
          了解一下:encodeURIComponent 和 encodeURI 区别?
        */
        _params.push(encodeURIComponent(key) + '=' + encodeURIComponent(data[key]));
      }

      encodeData = _params.join('&'); // name=tom&age=22
    }

    // method 判断连接
    if (method === 'get') {
      const index = url.indexOf('?');

      if (index === -1) {
        // 没有 ?
        url += '?';
      } else if (index !== url.length - 1) {
        // 有 ?,并且 ? 后面有数据
        // 'https://www.baidu.com/?name=tom'
        url += '&';
      }

      /* 
        到这里的 url 有两种情况:
          1:'https://www.baidu.com/?'
          2:'https://www.baidu.com/?name=tom&'
      */
      url += encodeData;
    }

    // 建立连接
    xhr.open(method, url, async);

    // 请求拦截器处理...
    // ...

    // 发送请求
    if (method === 'get') {
      xhr.send(null);
    } else {
      // post 请求
      xhr.setRequestHeader('content-type', 'application/x-www-form-urlencoded');

      xhr.send(encodeData);
    }
  });
}

面试点:

  • content-type 代表的是内容类型 => 浏览器 => firefox chrome
    • 会涉及到浏览器的下载
    • 文件下载兼容性问题(firefox vs chrome
      • 后台返回的数据没有指定类型:
        • firefox 会优先把数据变成 text/plain 类型,当作普通文本解析;
        • chrome 会优先当作一个文件来处理,无论这个文件是 html 还是其他;
      • 可能遇到兼容性的问题:当我们点击下载一个 word 文档时,如果后台没有指定这是一个文档类型时,chrome 会正常能下载成功,而 firefox 会把它当作普通文本在页面上直接渲染出来;

ajax 的 ts 版本:

interface IOptions {
  url: string;
  type?: string;
  data: any;
  timeout?: number;
}

function formatUrl(json) {
  let dataArr = [];
  json.t = Math.random();
  for (let key in json) {
    dataArr.push(`${key}=${encodeURIComponent(json[key])}`);
  }
  return dataArr.join('&');
}

export function ajax(options: IOptions) {
  return new Promise((resolve, reject) => {
    if (!options.url) return;

    options.type = options.type || 'GET';
    options.data = options.data || {};
    options.timeout = options.timeout || 10000;

    let dataToUrlStr = formatUrl(options.data);
    let timer;

    // 1.创建
    let xhr;
    if ((window as any).XMLHttpRequest) {
      xhr = new XMLHttpRequest();
    } else {
      xhr = new ActiveXObject('Microsoft.XMLHTTP');
    }

    if (options.type.toUpperCase() === 'GET') {
      // 2.连接
      xhr.open('get', `${options.url}?${dataToUrlStr}`, true);
      // 3.发送
      xhr.send();
    } else if (options.type.toUpperCase() === 'POST') {
      // 2.连接
      xhr.open('post', options.url, true);
      xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
      // 3.发送
      xhr.send(options.data);
    }

    // 4.接收
    xhr.onreadystatechange = () => {
      if (xhr.readyState === 4) {
        clearTimeout(timer);
        if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
          resolve(xhr.responseText);
        } else {
          reject(xhr.status);
        }
      }
    };

    if (options.timeout) {
      timer = setTimeout(() => {
        xhr.abort();
        reject('超时');
      }, options.timeout);
    }

    // xhr.timeout = options.timeout;
    // xhr.ontimeout = () => {
    //     reject('超时');
    // }
  });
}

ES6 之后的 fetch API

ES6 之后,浏览器端新增了⼀个 fetch api, 他有以下的几个特点:

  • fetch api 返回⼀个 promise 的结果;
  • 默认不带 cookie,需要使用配置 credentials: "include"
fetch('https://example.com', {
  credentials: 'include'
});
  • 当网络故障时或请求被阻止时,才会标记为 reject。否则即使返回码是 500,也会 resolve 这个 promise
fetch('/ajax?foo=bar').then(function () {
  console.log('请求发送成功');
});
  • 需要借用 AbortController 中止 fetch
const controller = new AbortController();
const signal = controller.signal;

fetch('https://slowmo.glitch.me/5000', { signal })
  .then(r => r.json())
  .then(response => console.log(response))
  .catch(err => {
    if (err.name === 'AbortError') {
      console.log('Fetch was aborted');
    } else {
      console.log('Error', err);
    }
  });

// 在 2s 后中断请求,将触发 AbortError
setTimeout(() => controller.abort(), 2000);

这就是⼀个简单的请求,发送之后就会进入 resolve 状态。与普通的 ajax 请求不同的是,在服务端返回内容时,我们还需要调用⼀些方法才能拿到真正返回的结果。

  • response.text():返回字符串
  • response.json():返回 JSON
  • response.blob():⼀般指返回文件对象
  • response.arrayBuffer():返回一个二进制文件
  • response.formData():返回表单格式内容
fetch('/ajax?foo=bar').then(function (response) {
  response.text(); // 返回字符串
  response.json(); // 返回 json
  response.blob(); // ⼀般指返回文件对象
  response.arrayBuffer(); // 返回一个二进制文件
  response.formData(); // 返回表单格式内容
});

常见的 json 请求,我们需要再调用一次 response.json 来让 fetch API 返回的结果序列化为 json

fetch('/ajaxPost')
  .then(function (response) {
    return response.json();
  })
  .then(function (result) {
    console.log(result); // 这里是数据
  });

五、浏览器原理

面试题: 从 url 输入到页面展示发生了什么?

这道题可以扩展的面很广,这里截取这道题的一部分,先限定一个前提:获取到资源 => 渲染出页面

首先要了解的知识:

  • DOM
  • CSSOMCSS Object Model):把 CSS 代码解析成树形数据结构;
  • Render Tree(渲染树):DOM + CSSOM 生成树;
  • Layout Module(布局模块):计算 Render Tree 每个节点具体的状态和位置;
  • Painting(绘图):呈现到屏幕上;

回到题面上:

  • 渲染流程:Url 处理 => HTML 解析 - JS + DOM + CSSOM => render tree / js + css 执行 => layout => painting
    1. Url 处理
    2. HTML 解析,包括 JS + DOM + CSSOM
    3. DOM + CSSOM 生成 render tree,此时 js + css 执行
      1. render tree 阶段和 js + css 执行是并行发生的
      2. DOM + CSSOM 可以直接合并 render tree,是静态的
      3. js + css 执行,是动态的
    4. layout
    5. painting
  • 纵向切分:
    • bytes(62 48 65 2C...) => characters(<html></html>) => Tokens(tag tree) => Nodes(html|head|body) => DOM | CSSOM

面试方向:

  • 渲染流程
  • 解析方式 => 引擎书写(DSL
  • 【重点】性能优化(下节课)