Skip to content

Front end Knowledge

EricYang edited this page Aug 5, 2021 · 24 revisions

记录一些前端知识

Mixed Content

  • 混合内容是指 https 页面下有非 https 资源时,浏览器的加载策略。

  • 在 Chrome 80 中,如果你的页面开启了 https,同时你在页面中请求了 http 的音频和视频资源,这些资源将将自动升级为 https ,并且默认情况下,如果它们无法通过https 加载,Chrome 将阻止它们。这样就会造成一些未支持 https 协议的资源加载失败。

  • 最合理的方案:

  1. 全站统一https-但是有些时候内容是用户产生的,或者第三方数据;
  2. 同域-nginx 强制跳转https NGINX配置:server{listen xx.xx.xx.xx; server_name xx.com; rewrite ^(.*)$ https://$host$1 permanent;};
  • 跨域的方案:
  1. 资源有http和https协议,但是url是http的, 兼容低版本浏览器; <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
  2. JS拦截簒改base host, 然后Nginx转发/后端代理;

MIME sniffing 嗅探

  • 引入MIME sniffing功能的初衷是用来弥补Web服务器响应一个图像请求时有可能返回错误的内容类型Content-Type信息这一缺陷;

  • 嗅探攻击解决方案:

  1. 给返回内容加上对应的 contentType;
  2. 添加响应头X-Content-Type-Options: nosniff,让浏览器不要尝试去嗅探;
  • 嗅探乱码原因:只对HTML内容解析有效,对于css内容中(外部样式表下)的双字节字符(如中文)解析并没有作用,如果浏览器请求回来的css资源的HTTP响应头里的Content-Type未指明"charset=utf-8"的话,浏览器根据自身的嗅探机制来决定采用哪一种编码解析,结果就会概率出现双字节字符乱码的情况;
  • 解决方法:
  1. css资源请求的响应头的Content-Type增加"charset=utf-8"声明;
  2. 使用 @charset
  3. 使用 css-unicode-loader

Cookie/Set-Cookie

  • 登录的原理:

  • 问题:通过http方式访问一个https网站时,登录有时会失效(无法登陆、跳转),Cookies中的token没有写成功

  • 原因:http和https返回的tokenName相同,由于一个是insecure一个是secure的,两者不能互相覆盖,导致cookie中的sso-token写入失败,所以也就无法登陆了。

  • 查看接口,发现:Response Headers中的Set-Cookie是拿到了返回值的,但是后面有个黄色三角感叹号,提示:This Set-Cookie was blocked because it was not sent over a secure connection and would have overwritten a cookie with the Secure attribute.

  • 解决方案

  1. 使用https,Set-Cookie中会有Secure字段;
  2. 在chrome中打开链接: chrome://flags/#site-isolation-trial-opt-out,搜索samesite上述三个选项禁用(设为disable)后重启chrome,问题解决;
  3. 删掉https下的token
  4. 无痕模式(有时也不行,是因为无痕模式打开时也写入了cookie)
  • 几种常见的策略
  1. Same Origin Policy
  2. Content Security Policy (CSP)
  3. Referrer-Policy

针对新版本Chrome>90.x? Samesite策略不能设置,导致开发时页面跳转出现问题

open -n /Applications/Google\ Chrome.app/ --args --disable-features=SameSiteByDefaultCookies

完全关闭Chrome,打开命令行运行上述命令。

删除文件/文件夹

  • rimraf 包的作用:以包的形式包装rm -rf命令,用来删除文件和文件夹的,不管文件夹是否为空,都可删除
const rimraf = require('rimraf');
rimraf('./test.txt', function (err) { // 删除当前目录下的 test.txt
  console.log(err);
});

闭包

  • 作用域链:在 JavaScript 里面,函数、块、模块都可以形成作用域(一个存放变量的独立空间),他们之间可以相互嵌套,作用域之间会形成引用关系,这条链叫做作用域链。
  • 查看作用域链:
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;

const code = `
  function func() {
    const guang = 'guang';
    function func2() {
      const ssh = 'ssh';
      {
        function func3 () {
          const suzhe = 'suzhe';
        }
      }
    }
  }
`;

const ast = parser.parse(code);

traverse(ast, {
  FunctionDeclaration (path) {
    if (path.get('id.name').node === 'func3') {
      console.log(path.scope.dump());
    }
  }
});
  • 函数和块的作用域内的变量声明会在作用域 (scope) 内创建一个绑定(变量名绑定到具体的值,也就是 binding),然后其余地方可以引用 (refer) 这个 binding,这样就是静态作用域链的变量访问顺序。

  • 静态:因为这样的嵌套关系是分析代码就可以得出的,不需要运行,按照这种顺序访问变量的链就是静态作用域链,这种链的好处是可以直观的知道变量之间的引用关系。静态作用域链是可以做静态分析的。

  • JavaScript 除了静态作用域链外,还有一个特点就是函数可以作为返回值!这就会引出闭包。

  • 父函数作用域中有很多东西与子函数无关,所以不会因为子函数没结束就一直常驻内存。这样肯定有性能问题,所以还是要销毁。但是销毁了父作用域不能影响子函数,所以要再创建个对象,要把子函数内引用(refer)的父作用域的变量打包里来,给子函数打包带走。

  • 闭包的机制:销毁父作用域后,把用到的变量包起来,打包给子函数,放到一个属性上 Scopes

  • 即使子函数没有引用父函数的变量,他也会有一个闭包--闭包最少会包含全局作用域。

  • 需要打包的只是环境内没有的,也就是闭包只保存外部引用。然后是在创建函数的时候保存到函数属性上的,创建的函数返回的时候会打包给函数,但是 JS 引擎怎么知道它要用到哪些外部引用呢,需要做 AST 扫描,很多 JS 引擎会做 Lazy Parsing,这时候去 parse 函数,正好也能知道它用到了哪些外部引用,然后把这些外部用打包成 Closure 闭包,加到 scopes 中。

  • 所以,闭包是返回函数的时候扫描函数内的标识符引用,把用到的本作用域的变量打成 Closure 包,放到 Scopes 里。调用该函数的时候,JS 引擎 会取出 Scopes 中的打包的 Closure + Global 链,设置成新的作用域链, 这就是函数用到的所有外部环境了,有了外部环境,自然就可以运行了。

  • 闭包需要扫描函数内的标识符,做静态分析,那 eval 怎么办,他有可能内容是从网络记载的,从磁盘读取的等等,内容是动态的。

  • eval 确实没法分析外部引用,也就没法打包闭包,这种就特殊处理一下,打包整个作用域就好了。因为没法静态分析动态内容所以全部打包成闭包了,本来闭包就是为了不保存全部的作用域链的内容,结果 eval 导致全部保存了,所以尽量不要用 eval,会导致闭包保存内容过多。

  • 但是 JS 引擎只处理了直接调用,也就是说直接调用 eval 才会打包整个作用域,如果不直接调用 eval,就没法分析引用,也就没法形成闭包了。

  • 黑魔法,比如 利用「不直接调用 eval 不会生成闭包,会在全局上下文执行」的特性。

  • 定义:闭包是在函数创建的时候,让函数打包带走的根据函数内的外部引用来过滤作用域链剩下的链。它是在函数创建的时候生成的作用域链的子集,是打包的外部环境。evel 因为没法分析内容,所以直接调用会把整个作用域打包(所以尽量不要用 eval,容易在闭包保存过多的无用变量),而不直接调用则没有闭包。

  • 过滤规则:

    1. 全局作用域不会被过滤掉,一定包含。所以在何处调用函数都能访问到。
    1. 其余作用域会根据是否内部有变量被当前函数所引用而过滤掉一些。不是每个返回的子函数都会生成闭包。
    1. 被引用的作用域也会过滤掉没有被引用的 binding (变量声明)。只把用到的变量打个包。

闭包的缺陷

  • JavaScript 引擎会把内存分为函数调用栈、全局作用域和堆,其中堆用于放一些动态的对象,调用栈每一个栈帧放一个函数的执行上下文,里面有一个 local 变量环境用于放内部声明的一些变量,如果是对象,会在堆上分配空间,然后把引用保存在栈帧的 local 环境中。全局作用域也是一样,只不过一般用于放静态的一些东西,有时候也叫静态域。
  • 每个栈帧的执行上下文包含函数执行需要访问的所有环境,包括 local 环境、作用域链、this等。
  • 如果子函数返回了,也就是父函数执行完了,此时,首先父函数的栈帧会销毁,子函数这个时候其实还没有被调用,所以还是一个堆中的对象,没有对应的栈帧,这时候父函数把作用域链过滤出需要用到的,形成闭包链,设置到子函数的 Scopes 属性上。
  • 父函数销毁,栈帧对应的内存马上释放,用到的堆中的对象引用会被 gc 回收,而返回的函数会把作用域链过滤出用到的引用形成闭包链放在堆中。
  • 这就导致了一个隐患:本来作用域是随着函数调用的结束而销毁的,因为整个栈帧都会被马上销毁。而形成闭包以后,转移到了堆内存。
  • 当运行这个子函数的时候,子函数会创建栈帧,如果这个函数一直在运行,那么它在堆内存中的闭包就一直占用着内存,就会使可用内存减少,严重到一定程度就算是「内存泄漏」了。
  • 所以闭包不要乱用,少打包一点东西到堆内存。

JavaScript 中的事件循环 Event Loop

  • 原因:JavaScript是单线程的语言!同一个时间只能做一件事情。

  • 浏览器里有 JS 引擎做 JS 代码的执行,利用注入的浏览器 API 完成功能,有渲染引擎做页面渲染,两者都比较纯粹,需要一个调度的方式,就是 event loop。

  • event loop 实现了 task 和 急事处理机制 microtask,而且每次 loop 结束会 check 是否要渲染,渲染之前会有 requestAnimationFrames 生命周期。

  • 帧刷新不能被拖延否则会卡顿甚至掉帧,所以就需要 JS 代码里面不要做过多计算,于是有了 requestIdleCallback 的 api,希望在每次 check 完发现还有时间就执行,没时间就不执行(这个deadline的时间也作为参数让 js 代码自己判断),为了避免一直没时间,还提供了 timeout 参数强制执行。

  • 防止计算时间过长导致渲染掉帧是 ui 框架一直关注的问题,就是怎么不阻塞渲染,让逻辑能够拆成帧间隔时间内能够执行完的小块。浏览器提供了 idelcallback 的 api,很多 ui 框架也通过递归改循环然后记录状态等方式实现了计算量的拆分,目的只有一个:loop 内的逻辑执行不能阻塞 check,也就是不能阻塞渲染引擎做帧刷新。所以不管是 JS 代码宏微任务、 requestAnimationCallback、requestIdleCallback 都不能计算时间太长。这个问题是前端开发的持续性阵痛。

image

  • js 引擎包括 parser、解释器、gc 再加一个 JIT 编译器这几部分。
  • parser:负责把 javascript 源码转成 AST
  • interpreter:解释器, 负责转换 AST 成字节码,并解释执行
  • JIT compiler:对执行时的热点函数进行编译,把字节码转成机器码,之后可以直接执行机器码
  • gc(garbage collector):垃圾回收器,清理堆内存中不再使用的对象

image

  • 如图,一般的 JS 引擎的编译流水线是 parse 源码成 AST,之后 AST 转为字节码,解释执行字节码。运行时会收集函数执行的频率,对于到达了一定阈值的热点代码,会把对应的字节码转成机器码(JIT),然后直接执行。这就是 js 代码能够生效的流程。

Clone this wiki locally