前端随笔

本文记录了平时的一些知识和输入,由于体系化沉淀需要花时间,所以提炼一些精要记录和想法在这里,等以后有时间再整理吧。

浏览器是怎么执行 JS?

JS 是 弱类型语言,动态语言,解释型语言。

  • 动态语言,是代码在运行时需要检查类型,静态语言,是代码在运行前需要声明类型。同一个变量可以保存不同类型的数据
  • 弱类型语言,在运行时,可以对类型做隐式转化,比如 var a = 1; a = 'string';typeof a 时,执行引擎自动识别类型。
  • 解释型语言,在运行时,解释器对代码动态的解释和执行,编译型语言,运行前,将代码编译成机器能识别的二进制文件。

整体是词法和语法分析 => AST => (解释器解释 AST) 字节码 => 存入内存 =>(解释器翻译 字节码)机器码 => 机器执行,其中解释器负责解释和翻译字节码,字节码比机器码小很多,解决了内存占用的问题。

对于热点代码,整体流程 AST => (解释器解释 AST) 字节码 =>(truboFan JIT 技术,识别热点代码)机器码 => 存入内存,通过 JIT 即时编译技术,直接缓存机器码,减少翻译成本。

对于 AST 的产出,词法和语法分析 => AST链路,先分词,再解析,词法分析,解析出最小无法被分割的 token 字符,语法分析,基于分出来的词,形成 AST 语法抽象树。

如何实现垃圾回收?
  • 栈空间:执行期上下文,栈的数据结构,下标记录当前的内存起点,方法执行结束,下标下移到上一个方法的内存起点,后续新的内存分配从起点开始覆盖式的存入。

  • 堆空间:代际假说,大部分对象,存活时间短;长时间使用的对象,活的非常久;基于这个假说,堆空间分成新生代和老生代,新生代存储小的、临时对象。老生代存放大对象,或存活时间久的对象。

    • 对于新生代(新生代小,1-8m),通过 scanvege 算法,双缓冲机制,一半存,一半空闲,满了就标记垃圾数据,复制存活的到空间的空间,颠倒使用。
    • 对于老生代,通过标记清除算法。遍历调用栈,深度遍历到堆中,找目前活跃的对象,进行标记,这个时候有全停顿的问题,需要通过三色标记法解决。
  • v8通过标记清除的方式来识别没有被引用的对象,对其进行删除,这个时候内存会不连续,因此在通过标记整理的方式,让剩下来的活跃对象前移,保证内存连续性,释放多余的空间。

  • 标记清除通过深度遍历栈空间对堆空间的引用,来识别未引用的对象,但前提是必须中断主线程的执行,保证内存的引用关系不变,这样会影响整个程序的执行。

  • 为了解决这个问题,V8 引入了三色标记法,把标记任务并行执行,允许主线程继续执行操作内存。

  • 三色标记法把遍历工作并行执行,这个时候内存变化,对于正在使用的对象的标识,会导致漏标或多标,多标不影响主线程正常执行,只是会晚点回收这个对象,而漏标比较致命。

  • 漏标只存在一种情况下,即对象之前被引用,执行gc过程中引用被中断,且被引用到了黑色对象上,黑色对象不会被重新扫描,这样这个对象就漏标了。

  • 为了加速gc,v8 通过增量标记的方式触发gc,即对象的赋值逻辑做了hook,如果是白色,则直接标记成灰色,开启gc遍历,同时为了加速遍历,v8开启多线程执行并行遍历的逻辑。

如何实现的块级作用域?

以前 js 只有全局作用域和函数作用域,es6 后出现了块级作用域,用 let 和const 标识,这两部分的变量,会存储到 调用栈-执行期上下文-词法环境(栈) 中,而之前的普通变量则存到 调用栈-执行期上下文-变量环境中,找变量时,优先从词法环境中找,即实现块级作用域。

作用域链?

函数 调用栈-执行期上下文 会存储变量内容,整个用栈的形式存放,但是会存在父子关系,比如全局函数中,调用子函数,子函数的执行期上下文 outer 会指向全局函数的执行期上下文。变量的查找,就通过 outer 的路径进行查询。

this?

执行期上下文-this 会保留 this 的索引,this 只有三种情况:全局上下文、函数上下文、eval 上下文。this 的指向比较 hack,通过对象调用时,指向对象本身,但是嵌套函数,会丢失 this,比如

javascript 复制代码
var c = {
  a(){ 
    function b(){console.log(this)} // this 丢失,输出 window
    b() 
  }
}
c.a();

可以用 self 局部变量保存,或者箭头函数。

闭包原理?

JS 存储分为 栈空间、堆空间和代码空间。原始类型存储在栈空间,引用类型和引用类型中的原始类型存在堆空间中,这样拆分,可以让栈空间变小,便于上下文快速切换和栈空间垃圾回收。

  • js 8 种类型:原始类型:string、bigint(更大的范围)、number(双精度8字节64位)、bool、null、undefined、symbol,引用类型:object

执行方法前,编译该方法,进行词法和语法分析,生成执行期上下文,压入栈空间,闭包中引用的外部方法变量,在编译阶段会独立扫描出来,变成引用类型对象,存入堆空间中,从而实现闭包。

http?
  • http 0.9:只支持 html 格式,比较简单

  • http 1.0:新增请求头和响应头,支持多种格式的响应内容;新增了缓存机制 Expires 头;(每次请求,都会创建一个 TCP 连接)

  • http 1.1:新增持久连接,多个 HTTP 在一个 TCP 上传输(浏览器一个域名最多 6 个 TCP 连接);新增 Host 请求头,支持虚拟主机映射(一个 IP 多个 Host);新增 Cookie;新增 max-age 缓存头;

  • http 2.0:新增多路复用,公用一个 TCP(减少带宽竞争,慢启动,解决 HTTP 队首阻塞);

  • http 3.0:基于 UDP 实现可靠连接 QUIC 协议,解决 TCP 队首阻塞(难以推广);

    • TCP 基于流,默认所有的数据必须具备完整性,缺失了将不可用,因此任何一段数据缺失,都会导致队首阻塞。
    • http 基于 TCP,将不同资源数据同时请求和返回,理论上是可以出现数据缺失,因为资源的数据之间是独立的。
浏览器输入url后,发生了什么?
  • 打开页面,创建渲染进程
  • 浏览器进程触发一些 JS 离开事件,然后发送 URL 到网络进程去拉取请求
  • 网络进程拉取请求后,处理响应头(比如重定向),如果是 text/html,则通知到浏览器进程
  • 浏览器进程通知到渲染进程,准备接收 HTML 响应内容,渲染进程反馈确认信息,准备开始接收
  • 浏览器进程接收到确认信息后,开始更新 UI(地址栏,前进后退、页面内容等)
  • 渲染进程开始接收 HTML 响应内容,开启解析和绘制,渲染完成,通知到浏览器进程。
  • 浏览器进程更新 UI(比如 loading 状态 logo )
浏览器安全?

浏览器安全分为:页面安全、系统安全和网络安全。

页面安全?

安全的防卫目标,主要是三个部分,Dom、本地数据、网络;主要通过同源策略来避免。

  • Dom:通过 window.open 打开的子页面,可以通过 opner 操控上一个页面的 DOM,如果没有同源策略,只要使上游页面打开危险页面,在危险页面中就可以任意操控上游页面 document,相当于裸奔。
  • 跨页面通信,postmessage
  • 本地数据:主要是 cookie、localstorage 等。
  • 网络:通过 xhr 发送请求,或者 img src 等。
    • cors 机制

目前主要的攻击方式,xss 跨站脚本攻击,把问题 js 脚本想办法放入页面中,实现攻击,一般是通过存储型或反射型两种方式,存储型就是通过评论、标题填写等方式,在展示用户内容的地方,想办法注入 js 脚本,而反射则是找 UI 展示 url query 参数的页面,通过参数注入 js 脚本。只要 xss 注入成功,js 就可以拿到 cookie 等内容,虽然通过 httponly 可以避免关键的 cookie 直接被拿去随意使用,但是注入js,直接发请求仍然可以伪造用户请求,进行操作。

csrf 跨站请求伪造,就是钓鱼网站,任何页面发送的请求,都会自动带上对应域的 cookie 等信息,所以只要引诱你进入钓鱼网站,就可以伪造用户请求,进行操作。通过对 cookie 进行 samesite 设置、校验 origin & referer 或者对请求参数加上 ctoken 验证(钓鱼网站是无法读取目标网站的 cookie)可以有效避免,但是 samesite 意义不大,因为随着站点做大后,不可能完全禁止掉三方站点携带自己的 cookie,因为当域名有多个时,做 sso 会有问题,所以最好的解决方法,还是 ctoken 的方式。

系统安全-浏览器为什么要拆分这么多进程?

最开始时所有功能都在单进程内,但是这样,任何页面出现任何异常,都会导致浏览器整个崩溃,所以,需要按照浏览器维度和页面维度,两个维度进行拆分,这样即使一个页面崩溃,不会影响浏览器本身进程。

为了避免系统级漏洞,默认所有的网络获取资源都是危险的,那么处理和运行网络资源响应的进程,需要独立出来,通过安全沙箱运行,与系统API进行隔离,这个就是渲染进程,其本身没有任何系统级 API 功能,只负责处理网络获取的内容,当需要系统 API 支持时,通过 IPC 通信,和其他进程沟通。

渲染进程为了避免页面崩溃和安全性,因此理论上,同一个根域名下面(非同源策略,只要同样的二级域名)的页面,这两个问题的表现都是一致的,即这个域危险,那域下所有页面都危险,所以同域的页面,都可以运行在同一个渲染进程内,这样可以减少内存占用。

出了上面的考量,其余的网络进程、浏览器进程、GPU 进程 这些,都是以功能维度抽象拆分,和开发代码的原理没太大出入,而 插件进程、service worker 进程的考量同理。

网络安全如何避免?

http 明文传输,中间人劫持存在时,想要安全,核心是避免中间人:对 客户端请求 改、看;对 服务端响应 改、看;

通过HTTPS可以进行加解密,避免中间人理解和篡改请求、响应。https 有对称加密和非对称加密。

对称加密,就是双方互相约定加密形式,和随机数,以后的通信通过这种方式加密和解密,但这种方式没有意义,因为在沟通过程中,中间人也知道了双方的约定和随机数。

非对称加密,就是公私钥的模式,一方告诉另一方自己的公钥,并约定加密方式,另一方通过公钥进行加密,这种方式,解密只能通过私钥进行,因此中间人不知道客户端在发什么,但是只解决了 1/4 的问题,客户端请求 改 和 服务端响应 改、看 问题仍然存在。

对称加密的问题是,由于沟通过程泄漏了加密方式和随机数,中间人通过同样的加解密方式,随机数是可以被破解的。而非对称加密,通过公钥加密的数据,只有私钥能解密,是无法被破解的,因此两者结合起来即可,非对称 + 对称加密 组合可以互补。

双方先约定加解密方式和随机数,然后客户端通过公钥加密新的随机数,告诉给服务端,这个随机数无法被破解,这时双方得到了一个中间人无法破解的随机数后,后续的沟通,就可以基于对称加密和新的随机数进行。

但中间人完全可以伪装成服务端,对请求进行响应,避过组合加密,因此有 CA 机制,公钥存在 CA 中,每次通信前,先在 CA 进行认证,确认服务端身份,CA 本身的合法性也会进行认证,往上溯源 CA 链,到根 CA。

如何判断是 CA 颁发的证书?

私钥加密的内容,只有公钥能解,CA 机构通过一套公共的算法,算出信息摘要,通过自己的私钥加密成数字签名,发放给服务器,服务器将数字签名给客户端,客户端通过 CA 的公钥解密数字签名,拿到信息摘要,同时自己也通过公共的算法,算出信息摘要,两者进行核对。由于数字签名是非对称加密,避免了篡改的可能性,只能是 CA 机构加密产生的,所以只要一致,就说明没问题。

Node 模块?
  • node 16 开始原生支持 esm,通过 package.json 的 exports 字段,可以声明 esm、cjs 两种执行环境的引入入口
  • cjs 执行环境下,通过 import 函数来引入 esm,
javascript 复制代码
await import('./my-app.mjs');
  • esm 执行环境下,可以直接 import from 的方式引入 cjs
  • 引入的差异,cjs 运行时加载,同步执行,输出值的拷贝,cjs 内部的变量会被缓存;esm 编译时加载,异步执行,输出值的引用;
  • package.json 设置 type = 'module',则默认所有的引入都认为是 esm,如果设置成 type = 'commonjs',则是 cjs。
react hook?

React hook 出现,解决了 class 的状态逻辑复用问题,class 时,需要用高阶组件来复用状态逻辑,hook 出现后允许对状态进行抽象,在多个地方直接复用。

headless ui 模式,将组件的交互逻辑抽象复用,支持自定义样式 UI

React fiber 做了什么优化?

React 15 遍历虚拟 Dom,采用前序遍历,递归的实现方式无法被中断,这里其实也可以用 while 循环实现前序遍历,创建一个临时栈结构变量,解决中断问题,React 16 fiber 没用这种方式,而是去除树结构,通过 child 链、sibling 链,和 return 链,来引导遍历方向,实现中断恢复后的持续遍历。

React 16 把渲染分两部分,遍历过程和渲染过程,遍历过程可被中断,采用的是后序遍历,先左再右再自己。遍历过程的目的,是去构建 nextEffect 链,把需要重新渲染的 fiber 节点串联起来,在渲染过程中,仍然会阻塞主线程,随着 nextEffect 链的路径去对需要变更的 fiber 做 dom 操作。

状态管理demo

状态管理?
  • 如果框架是追求单向数据流,那么如果想做到不同组件间状态修改,最常见的解决办法,就是抽象公共状态和修改状态的方法,传给不同的组件。

  • 比如 React 中,数据通过 props,从父组件传给子组件,但是子组件之间互相的状态修改,只能将相关状态,提升到父组件,然后在父组件中,以回调的形式传递给不同的子组件。

  • 比如 Angular1 中,数据通过 组件通过 direct 指令来抽象,平行组件的状态修改,通过 Service 声明全局对象来解决

  • Angular1 中同时提供了一种解决思路,通过订阅监听的方式,修改某个数据,广播该数据的变更,订阅方自行获取自己想要的部分数据内容。

  • 状态提升到父组件的方式,当嵌套组件过深时,传递会变得越来越复杂,这个时候需要将状态本身与组件解耦,用到时再声明,这样状态管理本身

状态管理的核心是数据的修改和传递。当数据变更后,该数据所有的消费方能同步更新,整体的设计衍进都基于更好的逻辑抽象原则。

  • 事件通信机制:子组件 A emit 事件,带上数据,兄弟组件 B 监听该事件,获取数据,进行状态变化。

    • 这种方式,通过事件进行通信和数据传递,不遵守单向数据流,数据流较为混乱,修改和传递都是黑盒。
    • angular1 中的 event 用法。
  • 同步修改机制:直接在想改的地方改对象数据,所有用到数据的地方动态更新。

    • 这种方式,改的逻辑分散各地,后期维护困难,传递相对较好,在 View 层做双向绑定,便于查找数据的消费方。
    • angular1 中的 scope 用法。
    • mobx 提供的 observe 机制。
      • 如何做到识别 autorun 里面用到了那些变量?
      • mobx 如何做到触发 react 重新 render 的?
  • 统一修改机制:在同步修改机制上,做逻辑抽象,将修改挂到对象方法上,想改的地方调用方法修改,所有用到数据的地方动态更新。

    • 这种方式,将数据的定义和修改逻辑抽象到了同一个地方,改的逻辑不再分散,同时传递在 View 层定义,较为清晰。
    • angular1 中的 contorller 用法。
    • mobx 提供的 observe 机制。
  • dispatch 机制:在统一修改机制上,将修改抽象成事件,以类似事件的机制来统一响应修改。

    • 这种方式,和统一修改机制没有本质的区别,只是将修改的方式定义的更加清晰和规范,是一种规范层次的约束。
    • redux 提供的 dispatch action 机制。
    • mobx 提供的 action 机制。
    • react 提供的 useReducer 机制。

状态管理在状态的修改上,进步明显,但是在数据的传递上,一直在反复横跳。

  • 事件通信传递模式:通过事件,触发方直接将数据传递给消费方。

    • 这种方式,传递黑盒,不知道数据在哪里使用,会产生什么变化。
  • 双向数据流形式:提倡组件UI变化由外部状态数据驱动,双向绑定机制,只有 Model 和 View 两层,哪里用,哪里改,即 Model => View & View => Model,最重要的是,Model 和 View 的绑定,不区分 View 层的父子组件,子组件的状态由 Model 直接传递。

    • 这种方式:在处理多重嵌套组件的情况有优势,灵活自由,代码较为简约;但是子组件也可以直接改 Model 数据,而这个数据可能是父组件使用的,组件UI变化由外部状态数据驱动,但是这个外部状态的改动黑盒,难以确定是哪里修改了,UI 的变化变得无法预期。
    • angular1 的双向绑定机制
  • 单向数据流模式:在双向的基础上,约束了组件的外部状态来源,只允许组件的外部状态从父组件传递过来。

    • 这种方式,把 Model 修改的逻辑抽象成了 Action,约束并整合 Model 的改动逻辑,从而使得 Model 的改动变得可以在代码层面可控可发现。同时子组件的外部状态来源只有父组件,整个改动链路清晰,不再黑盒。
    • React 组件开发模式
  • 单向数据流 + 全局状态模式:单向数据传递的方式,在复杂嵌套下,编写麻烦,因此全局的状态管理出现,在 Model 和 View 间加上了 Action 的开发概念,即 View => Action => Model & Model => View,同时,哪里要使用或者修改,直接自己去取,修改通过 Action 机制。

    • 这种方式,再次将组件的外部状态来源直接扩展到了 Model 上,为了避免改动的混乱,结合 Action,通过开发模式上的约束,让改动变得可控可预期。
    • Redux 状态管理
    • mobx 状态管理

总结:

状态本身是数据,当两个不同组件间,需要互相控制对方 UI 或逻辑时,就需要传递数据,最早的时候是通过事件直接传递状态数据,组件 A emit 事件,带上数据,组件 B 监听该事件,获取数据,进行UI变化。但是这个方式,组件不知道传递的数据对方会怎么用,导致什么变化,或者有多少组件会用。

后续发展到状态驱动 UI 后,主要就围绕着 "如何更好的抽象管理状态本身的修改和传递?" 来进行,核心一直在 修改 和 传递 的复杂度天平中寻求平衡。

最早 Angular1 的双向绑定机制,组件的外部状态来源于 Model,而 Model 的修改不受限制,任何组件都可以修改,数据传递虽然变得简单,但是修改混乱,状态的变化不可预期,代码维护困难。

因此后续发展出了单向数据流,组件的外部状态只能来自父组件,这种设计在传递上做了取舍,增大了传递的复杂度,但是约束了修改条件,即只能组件自己改自己的状态,状态整体的变化从而变得可控。

但是单向数据流在遇到组件互相控制通信的场景时,需要提升状态到父组件,代码处理较为复杂,尤其是过深的嵌套时。因此又在传递和修改上做了让步,衍生出了大量的状态管理库,允许组件的外部状态直接来源于 Model,但是修改需要通过 Action dispatch 类似的机制进行,从代码层次约束、抽象和整合 Model 的修改逻辑,从而达成开发复杂度的平衡。

状态驱动还是事件驱动?

典型的场景,点击搜索按钮,发送请求:

  • 事件驱动:消费点击事件,直接发异步请求
  • 状态驱动:消费点击事件,修改某状态,监听某状态值,发送请求
向量数据库?

向量表示一个箭头的长度和方向,在三维坐标里面表示成 (x,y,z),这里面的三维就是三种维度的拆分,比如物种拆成:性别、毛发、寿命,当这个点相同时,即为同一个物种,这样就可以拆分成无数的维度,通过数据公式来精确的搜索,归类和定位。

indexedDB?
  • bean-li.github.io/leveldb-man...
  • 底层通过 LevelDB 实现,会有个 Log 文件进行所有读写的记录,来实现断电恢复能力
  • LevelDB 的存储分成多层,一层层向下做持久化存储,每一层有多个文件,通过 minifest 来存储每一条数据的索引,与它存储在那一层的文件映射。
  • 为什么要下沉文件?因为文件分开存储,需要一个个文件去找数据,不利于数据搜索,因此需要多路归并,将多个文件的数据合并成一个文件。
  • 为什么要多路归并?数据操作有删除、插入动作,会导致数据不连续,因此需要多路归并算法,将多个文件整合成一个文件,形成连续的数据存储,再删除老的文件,同时生成新的 Manifest 索引文件,来记录索引和文件的映射。
  • Manifest 文件记录了索引变更的历史记录,所以索引更新,则文件大小会一直增长,而 Manifest 文件的删除是在 DB 断开,并重新连接的时候,比如页面刷新,是无法触发 Manifest 删除,必须关闭所有相关页面,重新再打开进入时,触发文件删除。
GIT

修改提交消息:

  • git reset HEAD~,临时回滚提交,用于测试代码比较方便
  • 通过 git commit -amend
  • 通过 git rebase -i [基准commit] waynerv.com/posts/git-r...
    • git rebase 可以做的操作更多,会提取基准 commit 后的所有提交,进行批量操作,如下
arduino 复制代码
pick c50221f commit B
pick 73deeed commit C
pick d9623b0 commit D
pick e7c7111 commit E
pick 74199ce commit F

# 变基 ef13725..74199ce 到 ef13725(5 个提交)
#
# 命令:
# p, pick <提交> = 使用提交
# r, reword <提交> = 使用提交,但修改提交说明
# e, edit <提交> = 使用提交,进入 shell 以便进行提交修补
# s, squash <提交> = 使用提交,但融合到前一个提交
# f, fixup <提交> = 类似于 "squash",但丢弃提交说明日志
# x, exec <命令> = 使用 shell 运行命令(此行剩余部分)
# b, break = 在此处停止(使用 'git rebase --continue' 继续变基)
# d, drop <提交> = 删除提交
......
codesandbox 原理?

在浏览器编译源码并运行,核心通过 iframe 实现编译和预览,通过 postmesage 进行源码、目录结构等通信。

iframe 中,接收到源码后,通过类似 webpack 的机制,启动多个 web worker 开始编译代码,最终 Mock

代码的执行环境,比如 Commonjs 的 require 等 API,原理类似同构,通过 eval 执行编译后的代码,实现预览。

Node 文件系统模拟:fork github.com/jvilk/Brows... ,基于 indexedDB 维护了一套文件系统机制。

包加载机制:在浏览器实现 packager 包加载机制,梳理源代码的所有依赖以及依赖的依赖,所有的文件内容和路径索引都维护到 manifest 中,一个 http 返回给浏览器,同时也支持 combo 的自由组合类型获取文件源码。

编译机制:内置 babel,实现浏览器测的 webpack 机制,编译任务通过 web worker 池分配,并行进行。

执行机制:对编译后的源码进行 eval,直接执行。

缓存机制:启动service worker,对源码资源进行缓存,下次再拉取的时候直接读取缓存。

开发效能?

效能本质是交付,解决三个维度:个人维度的开发效率;多人维度的协同合作交付;持续的高效交付;

网易云 tango 低代码设计?

基于 codesandbox 能力,提供可视化的代码编写和渲染能力,低代码搭建 UI 通过源码 code 与 codeSandbox iframe 通信,搭建 UI 将源码解析成 AST,进行 UI 功能展示,并基于 AST 做源码转化。

这个方案,问题在于 codesandbox 是浏览器编译,本身不适合多页、大型复杂项目的编译,这个问题需要考虑解决掉。

阿里的低代码设计?

核心基于 DSL ,通过统一约定的 DSL,出码代码,本质是搭建系统的一定衍生。但是对开发者不友好,DSL 出码后的代码如果修改,无法再和 DSL 对其。

lit 框架?

codelabs.developers.google.com/codelabs/li...

lit 框架是基于 web component 实现的响应式框架,目前还是尝试阶段。web coponent 浏览器原生支持,适合内嵌到各种开发体系中,但是状态管理是致命问题,这种原生 API,状态管理需要自行处理,通过事件携带数据进行不同组件间的通信,整体限制较大,不适合复杂组件。通过带变量的模版字符串语法,快捷开发 template。

为什么会有这么多打包工具?

兼容性:因为 es 推行的更新,浏览器支持力度不一,因此需要转码最新的语法特性到老的版本,即语法糖,来兼容代码的执行。

三方依赖:三方代码的拉取,全局赋值容易代码污染,因此打包的方式将三方用到的逻辑打包到 bundle,能够解决污染问题。

babel 是什么?

xie.infoq.cn/article/b97...

evilrecluse.top/Babel-trave...

babel 基于 JS 语法,跑在 Node 上,整体 code => (parse) ast => (transform) ast => (generator) code 。parse 阶段,实现了对代码的词法&语法分析,解析出 AST;transform 阶段,深度优先遍历 ast,通过访问者模式,支持自定义的 AST 结构修改,即 老版 AST => 新版 AST。generate 阶段,将 AST 生成出代码片段。

由于本身也是跑在 Node V8 上,所以比 esbuild 这种用 go 语言实现的编译方式,要慢一些。

tsc 和 babel 区别?

juejin.cn/post/708488...

tsc 的编译流程和babel基本一致, 源码 => (parse) ast => (bind 作用域识别)ast => (cheker 类型校验) ast => (transformer 节点处理) ast => emit => 目标代码,会在 源码 => (parse) ast 后,进行 ast => 类型校验,同时 tsc 支持的语法有限,没有插件等扩展机制,而 babel 通过 插件等机制,语法支持的生态更好一点。babel 同时有 runtime 机制,可以根据目标浏览器,动态注入模块化的注入 polyfill 代码,而 tsc 是不会做这些副作用的,需要显示引用 core-js。tsc 在类型校验时,是多文件的处理逻辑,会合并各个地方的类型声明,而 babel 是基于单文件进行编译处理。

babel 和 webpack 的区别?

babel 本质是做语法转义,不做打包。如果代码引用了三方库,是不会将三方库代码打包进源码中。而 webpack 本身可以处理三方库代码打包进源码的操作。

因此 babel 比较适合编译运行在 node 侧的代码,这类代码转义后,通过 es 的文件系统约定,比如 require 寻找对应索引文件和代码,可以直接运行。而如果要运行在浏览器侧,则需要打包一个大 bundle js,不过目前部分浏览器原生也支持 esModule ,只是性能堪忧,遇到文件则一个个http请求去拉取文件。

webpack 如何做到 tree shaking?

tree shaking 的核心是找到文件中未被使用 export 变量和相关逻辑,做删除,因此需要分析代码,对是否使用进行标记,但是只能支持 静态 import 的场景,es6 提案的动态 import,或 commonjs 语法,在运行时加载模块,是无法提前判定哪里未使用导出的变量的。

webpack make 阶段,会生成入口文件的依赖图谱,每一个依赖是一个独立的 module,这一步是通过 AST 来解析 require import 等导入语法,遍历生成,在这一步可以对所有的导出变量进行统计和收集,存储到对象上。

webpack seal 阶段,遍历 module 依赖图谱,根据entry生成对应的chunk,在这一步,可以对之前收集的导出变量进行标记,识别哪些是会被其他 module 引入的。

webpack 代码生成阶段,将被标记成会被使用的导出变量,生成特定的代码片段,最终会生成到同一个文件 js 中,这个时候可以通过其他插件,直接 shaking 掉无用代码。

tree shaking 的一些问题?

tree shaking 是静态扫描,未做语义识别,比如最多只能做到文件导出的变量中,完全不被引用过的代码,可以被删除。如果被引入和使用了,但是实际未继续使用的,则不敢删除,因为赋值逻辑可能会产生副作用(getter setter 的触发逻辑),这个编译阶段未做处理的。

javascript 复制代码
import {foo} from './a.js';
const a = foo; // 后续再也未使用 a 变量,这个时候 tree shaking 也不敢删除引入和这一句赋值逻辑,避免影响副作用
webpack dll 用法?

dll 是 dynamic link library,在webpack 里面,本质上是缓存的作用,把之前编译过的 moudle 缓存到硬盘,减少二次编译时间,webpack 4 后,直接内置了缓存能力,不需要配置 dll。

webpack runtime 运行时?

segmentfault.com/a/119000004...

webpack 打包的运行时本质是实现模块化,将多个文件的模块代码,打包到一个文件,在这个文件中,需要实现模块化的能力。运行时也分很多实现,比如有的模块没有import代码,就不需要 require 的实现,因此runtime本身的注入,类似依赖收集的机制。runtime 大小非常大,特别是开发环境加上 热加载,会有 2w+ 行。

第一次遍历,make 阶段,收集运行时,遍历所有 module,梳理 module => runtime 的依赖关系,其中也包括动态 improt api,seal 阶段同时会产出不同的 chunk。

第二次遍历,seal 阶段,遍历所有的 chunk,通过 chunk => moudle => runtime,整合 chunk 对 runtime 的依赖。

第三次遍历,seal 阶段,遍历所有有依赖 runtime 的chunk,生成相关运行时代码,进行代码注入。

webpack chunk?

chunk 本质上是打包时的抽象单位,多个 moudle 映射到一个 chunk 中,入口 entry、异步 import 都会构成独立的 chunk,如果有不同 chunk 重复引入同一个 module 的情况,有插件可以将这种 module 拆分成独立的 chunk,避免重复打包。

webpack loader?

segmentfault.com/a/119000004...

loader 是做资源处理,返回成 JS 语法内容,以便后续 webpack 进行 make、seal 和转义,loader 支持链式调用,多个loader共同处理一份资源,下一个的输入是上一个的输出,loader 的执行时机,分 pitch 和 执行 两个阶段,类似冒泡机制,pitch 阶段,从后向前执行 loader 注册的 pitch 函数,可以用来阻断后续 loader 的执行,执行阶段,从前往后执行,正常进行资源到 js 的转化。

stylecomponent 实现原理?

developer.mozilla.org/zh-CN/docs/...

css in js 的写法实现,核心通过 es6 带标签的模版字符串语法实现,作为语法层使用。

vite?

vite 分开发模式和生产模式,核心亮点在于开发模式。

vite 把代码分成了源码和依赖两种,对于源码只在单个文件级别做转义操作,转成 esm 代码,剩余的交给浏览器执行,这样就非常快,因为少了打包过程。对于依赖的三方包代码,其嵌套import未知,因此会做预构建机制,通过 esbuild 打包依赖的包代码, 转义成 esm。

vite 在开发模式下,要使用vite提供的 html server 来访问本地页面,这和传统的开发模式,即访问页面,构建 bundle js 绑定到本地不同。vite 对源码不会做兼容处理(只支持到 es2015 版本),比如 装饰器 代码,因此如果使用最新的js特性,还需要用 babel 的插件生态,做一次转义。

vite 构建后,默认会生成 esm 的构建产物,引入时,需要声明 script 标签 type="module",不然浏览器不会以 esm 去执行,会报 export 等模块语法错误。

vite 插件如何开发?

cloud.tencent.com/developer/a...

vite 提供了三个hook用于插件的扩展, resolveId hook 用于重定向代码里的 import 路径,load hook 用于自定义加载文件的方式,拦截 module id(import 的路径)的加载,可以在这里做文件转换,比如将 svg 装成 js 代码, transform hook 用于处理和修改依赖 module 的代码内容。

防抖节流如何实现?

节流的本质是防抖,防抖是让函数的触发,需要在等待完整 x 时间后,再进行触发。而节流是让函数的触发,在 x 时间间隔内只触发一次。

防抖可通过 setTimeout 实现,但是由于浏览器对时间API的降权,切换tab等操作,会影响计时器 event loop 的触发,会有误差。

如何维护开源库?

通过 docusaurus.io/zh-CN/docs/... docusaurus 可以快速启动开源库官方,通过 yarn zhuanlan.zhihu.com/p/381794854 workspace + lerna 机制,可以构建复杂开源项目。

进程和线程?

进程是资源分配(CPU、内存、硬盘io)的最小单位。多进程本质上是单核 CPU 通过时间分片完成调度,实现"并发"。

线程属于进程,是 CPU 调度和分配的最小单位。一个线程可以单独分配到一个 CPU 内核,实现"并行"。

react lazy?

import 语法目前所有主流浏览器都原生支持,静态导入方式,放在头部,会单独发送 http 请求去获取,动态导入的方式,可以直接通过 import(''').then 来获取,这个 API 类似 require API 机制,返回的 module 结构 {default:...,其他具名导出}。

react lazy 利用 import 动态导入的方式,实现按需拉取组件依赖的资源。

JS 字符串编码?

JS 的字符串 API 全部采用 utf-16 的编码形式,比如 .length 返回 utf16 的编码字节长度,utf 16 中,小于两个字节的,用两个字节表示,长度为1,大于的,用四个字节表示,长度为2,比如 String.fromCharCode(55356, 56324) 红中占 4 个字节,.length 长度返回2。

monorepo?
ts 构建 mono repo?

tsc 构建的代码,无法定制化某个文件,到某个目录,因此只能为每个目录,单独配置 tsconfig,通过 extends 继承最外层的 tsconfig,通过 references.path 声明依赖的模块目录,最终形成统一的构建结果。

tsc 打包时,src 目录会被打包到 outDir,需要指定 rootDir 为 src,但是由于 ts rootDir 需要包括所有的 source 文件,此时如果引入 src 目录之外的 module,则会报错,比如 import '../package.json'

styled-component?

避免样式冲突,类似 css-module 的形式,不过不是写 css,再 import 到 js 中,以消费动态 classname,styled-component 是用 js 的方式直接来写 css,依赖es6带标记的模版字符串来实现。

postcss?

构建插件,将 css 语法抽象成 AST 树,便于后续插件对css进行处理,是一种基础插件,基于它衍生出了很多功能插件。

react schedule?
  • 触发时机:采用 Timer 的方式触发,舍弃 RAF。RAF 在帧内触发,由于帧内包含多次 EventLoop,Timer 的方式更能高效的利用帧内的空闲时间,进行多次 workloop。
  • 执行时机:postMessage 方式创建宏任务回调,在下一个事件循环中执行,可避免死循环。舍弃通过 Timer 创建宏任务回调,因为浏览器限制,循环创建的情况下,最短间隔是 4ms,会浪费 4ms 的空闲时间。
  • 中断机制:workloop 阻塞主线程执行,控制总耗时 5ms 以内,同时扩展中断机制。
  • 任务队列权重:以任务的预期启动时刻进行排序,内部实现最小堆,插入复杂度 O(logn),查询复杂度 O(1)
构建器 eject?

脚手架内聚构建逻辑,构建应用代码,但是逻辑黑盒,因此会提供 eject 弹出命令,将脚手架源码注入到应用中,以便应用自行定制构建逻辑,像 creat react app 脚手架一样。

ts 复用类型声明?

使用 声明对象['属性名'] 或者 Pick<声明对象,属性名 | 属性名>

相关推荐
小亦苦学编程5 分钟前
HTML基础用法介绍二
前端·javascript·css·html
无名小小卒9 分钟前
三小时快速上手TypeScript,TS速通教程(上篇、中篇、下篇、附加篇)
开发语言·前端·typescript
qq_4246358812 分钟前
要实现在Vue 2中点击按钮后在新浏览器标签页中预览PDF文件 ,pdf文件默认放大125% 禁止PDF的工具栏下载功能
前端·vue.js·pdf
JAMJAM_NoName15 分钟前
【前端学习】前端存储--Cookie、localStorage和sessionStorage
前端·学习
秋雨凉人心22 分钟前
Webpack和GuIp打包原理以及不同
开发语言·前端·javascript·webpack·gulp
john_hjy23 分钟前
4. 数据结构: 对象和数组
java·开发语言·前端
bjzhang751 小时前
使用Chrome浏览器时打开网页如何禁用缓存
前端·chrome·缓存
夏天想1 小时前
uni-app+vue3+pina实现全局加载中效果,自定义全局变量和函数可供所有页面使用
前端·javascript·uni-app
深情废杨杨1 小时前
前端vue-form表单的验证
前端·javascript·vue.js
Fenderisfine1 小时前
使用 vite 快速初始化 shadcn-vue 项目
前端·css·vue.js·前端框架·postcss