已经被裁一个多月, 因为已经很久没有找工作经验, 所以要开始第一步: 复习.
这里记录复习内容, 在找到工作前持续更新.
这个链接有相关的复习代码, 也持续更新, 内容有:
- immer最小实现
- redux最小实现
- vue2和vue3响应式最小实现
- 函数编程辅助函数compose, currying
- 函数作用域, this, 原形相关的测试
- 几个排序函数
- 加减法模拟器
- vue和react最小实现
js 相关
( js ) 作用域与this
函数的执行有执行栈, 栈底是全局作用域, 执行的时候创建作用域并入栈, 执行完毕出栈.
在创建作用域的时候:
-
决定this的值: 谁调用this就是谁. (箭头函数找上一层)
-
决定环境中变量的实际地址. (非全局作用域多一个arguments, 全局的多一些原形函数)
变量提升的原因是: var被当做变量处理, let和const被当做词法处理.
-
决定外部引用环境. (作用域栈的下一个, 变量找不到会去这里找) 全局的外部环境是null
( js ) event loop
执行栈是函数执行而来的, event loop是决定函数执行顺序的, 也决定了ui渲染和各个来源函数执行的顺序.
浏览器中除了promise是微任务, 其余全是宏任务, 包用户输入, setTimout, callback.
宏任务有多个队列, 微任务只有一个队列. 表现为无论在执行宏任务还是微任务, 执行完都会检查微任务队列.
所以如果有比较难分辨的情况, 我认为难点在于要理解: 宏任务有多个队列. 要分清那几个任务是在同一个队列里的. 这里贴一个题目.
setTimeout(function(){
console.log('定时器开始啦')
});
new Promise(function(resolve){
console.log('马上执行for循环啦');
for(var i = 0; i < 10000; i++){
i == 99 && resolve();
}
}).then(function(){
console.log('执行then函数啦')
});
console.log('代码执行结束');
( js ) 原型
-
原型: proto 属性是个accessor属性, 实际是Object.getPropertyOf和Object.setPropertyOf.
-
隐式操作: 平时的字面量对象,
.操作取值, 赋值. 实际都有隐式操作. 取值会往__proto__找, 赋值如果当前变量没就创建.字面量创建的对象有默认__proto__, 并不是空对象. 用es6省略value会创建null为原型的对象.
-
构造函数: 所有函数都会被挂上prototype属性. 这个属性会在被new的时候作为实例的__proto__. 内容是: {constructor: 自己, proto : Object.prototype}, 在继承的时候__proto__是父对象的constructor, 构造函数的__proto__也是父对象的构造函数.
-
class的extends/寄生组合式继承: 子类的__proto__的父类, 子类的prototype的__proto__是父类的prototype.
-
一些基本对象(Object, Array, Function等)的关系可以自己用__proto__和===和instanceof去画关系.
css 相关
( css ) BFC
BFC是一个布局环境, 有一定的规律, BFC内部可以嵌套另外独立的BFC.
BFC里的盒子按照常规流从上到下排布, 可能出现margin重叠(这个问题可以用嵌套个新BFC解决).
跟元素就是个BFC, 创建独立BFC的条件是float, position, overflow, flex等.
( css ) 盒模型
标准盒子是content-box, IE盒子是border-box. IE盒模型的width多包含了padding和border.
其实设计图的尺寸一般是IE盒模型写起来比较方便.
( css ) z轴顺序
从下到上如下图, 注意的是层叠上下文也是上下文, 也BFC一样有嵌套.
- background/border
- z-index负值
- 块元素
- 浮动元素
- 行内元素
- z-index: 0, auto
- z-index正值
浏览器相关问题
"输入url到页面展示"
解析dns => 通过tcp建立http连接 => 发起请求 => 接受数据 => 断开http连接 => 渲染页面
http
在网络模型中, http协议属于应用层(5或7), 下层的传输层(4)是tcp协议, 网络层(3)是ip协议.
建立连接和断开连接都用tcp进行, 就是传说中的三次握手四次挥手.(断开的过程第三次挥手是服务端发起的).
http1.1默认开启了keep-alive(默认头), 作用是减少建立和断开连接发送的tcp请求. 而是不是支持得看服务器, 协议两端的交互就像打电话的2个人.
(浏览器) 渲染网页
浏览器渲染网页总体有几步, 但不同浏览器实现其实不同.
- 把html parse成dom树.
- 把css parse成样式树.
- 通过以上2树, 构造呈现树. dom树和呈现树结构并不相同, 有些标签(head, meta, display为none等)不显示, 而有些标签(select, title属性等)会产生多个呈现树.
- 通过呈现树计算, 布局.
- 绘制.
然后在元素发生变化后分为部分渲染. 其实前端理解以后能做的优化就是: 减少渲染次数, 而这个事情框架里都做了.
跨域
( cors ) 什么情况会发生跨域
协议, 端口, host(任意级)只要有一个不同就跨域.
( cors ) 什么请求会发生跨域
- js发起的请求. (通过XMLHttpRequest和fetch)
- 字体.
- WebGL texture.
- canvas通过drawImage画的image或者video. (会把canvas变成被污染的状态, 不能toDataUri了)
- css里的
url().
( cors ) 如何让跨域请求顺利
设头, 请求端和服务端都要设. 都是Access-Control-开头的.
如果要带上cookie, 加个头withCredentials: true.
( cors ) preflight请求和简单请求
在发起非简单请求的跨域请求前, 浏览器会发起preflight请求, method是options, 来确认是否支持即将进行的请求.
简单请求的条件比较复杂, 要同时满足:
- method是get, post, post之一
- 只能手动设某几个请求头: Accept, Accept-language, Content-language, Content-type.
- Content-type只允许几个值: text/plain, multipart/form-data, application/x-www-form-urlencoded.
- XMLHttpRequest发起的请求不能监听upload事件.
- 请求中没有ReadableStream.
这些要求正常人是背不出的, 所以只要基础简单请求是幂等请求, 非简单请求是会对服务器数据造成改动的.
( cors ) 额外的解决方式
几个通常的非正常解决方案:
- jsonp. 通过非
会发生跨域的请求方式(上面提到的)来发起跨域的请求, 比如append script. - 前端服务器转发. 这个通常是下属地, 带配置host功能的转发服务器.
- 网关转发.
(浏览器) 缓存
缓存就是根据一些头信息, 来决定是否可以用本地缓存数据来替代发起一次新http请求. 浏览器http缓存分2种:
- 强缓存: 通过expires, cache-control等头, 如命中, 直接用缓存, 状态码200, (from disk cache).
- 协商缓存: 通过last-modified, e-tag, if-none-match来判断, 请求会发出, 如命中, 服务器不传输内容, 返回状态码304来使用缓存.
(浏览器) 事件流和事件委托
事件流从document流向目标, 再流向document.
addEventListener最后一个参数可以调整从哪个角度获取事件.
stopProgration和preventDefault能组织捕捉/冒泡.
事件委托指, 要给一系列子元素增加事件, 就给他的父元素增加事件, 然后通过event的字段来判断是哪个子元素.
事件委托的原理是利用事件冒泡, 优点是不用重复注册, 以及子节点变化的情况无须重新注册.
(安全) xss
cross-site scripting, 通过给html输入脚本来攻击, 防止的思路是: 要放到html上的输入要小心. 处理方法就是转义. 一般容易攻击的输入有: url, cookie, 输入框. 总之不要相信用户输入.
(安全) csrf
cross-site request forgery, 在被攻击网站登录状态下, 第三方网站向被攻击网站发起请求, 这样会带着身份信息. 通过不接受跨域请求来防范.
工程化
工程化发生在日常工作的2个使用场景: 本地开发和打包.
- 保存再刷新页面的日子一去不复返. node脚本帮我们提高开发体验.
- 到线上的代码经过打包, 加大前端代码被抄难度(混淆), 减少资源储存消耗(压缩), 另外我们也习惯了写浏览器不一定认识的代码, 也是打包过程中处理的.
工程化迭代的目标有2个方向.
- 通用的: 提高性能. 这点主要是通过减少不必要操作来实现的.(通过优化算法和业务逻辑)
- 公司内特有的: 完成一些特殊的业务流程.
webpack, rollup, vite的区别, 他们与babel的关系
- webpack和rollup是相同定位的. vite是开发时的bundleless脚本, babel大多场景是作为插件集成使用的.
- webpack与rollup的区别: webpack的系统比较复杂功能多, 产出也比较复杂, 并且产出esm的功能是实验性功能. 所以结论是能用rollup就用rollup.
webpack的loader和plugin
webpack的loader函数就是个简单的字符串处理函数, 他的复杂度在于加载的顺序. 而plugin可以认为是webpack的二开, 相当复杂, 上限特别高, 是loader的超集. (plugin里可以加载loader) plugin主要用tapable来拦截webpack各个流程的行为, webpack很多功能都是plugin实现的.
webpack流程
compile主流程分为 make, seal, emit.
- make阶段: 执行 loader, 建立模块间的关系.
- seal阶段: 根据配置与模块关系, 优化数据结构.
- emit阶段: 根据上面步骤的结果输出.
对webpack想深入建议看我首页, 有很多相关文章.
webpack的code split
希望webpack输出多个文件, 有三个办法.
- 配置
optimization.splitChunks - 配置多
entry - 使用
import(), 除了手动使用, 各种前端框架的 router 里也是会配置的. 但要注意 dynamic import 正则的话, 会让打包体积增大很多, webpack 会打包所有可能的结果.
另外在有 code split 的情况, 和有媒体资源的情况下, 要注意 publich path 是不是需要特地配置.
webpack的tree shaking与scope hoisting
这些功能在高版本的生产模式默认打开, 我们能注意的只有尽量使用esm版本的三方包, 并且自己组件用 rollup 打包.
(相关的细节和原理也可以在我以往的文章里看到)
优化前提: 如何知道卡在哪里
可以使用webpack-bundle-analyzer, speed-measure-webpack-plugin, measure-webpack-plugin来查看打包体积, 打包速度的最大问题出在哪里, 并针对性优化.
vite-plugin-inspect查看 vite 的速度.
根据自己项目的简单优化
有一些配置可以根据自己的项目来优化, 比如 resolve 的文件扩展名, alias, 一些 loader 的 include 与 exclude. 以及合理的 code split.
hmr相关
其实 react 项目有很多 hmr 失败相关的问题, 大家都不太注意到.
hmr 是基于文件的, 因为 webpack 中的 module 就是文件.
而 react 可能有一个文件多个组件, 或者是 slot 组件.
这个情况会导致 hmr 非预期. 虽然 vue 的 sfc 不灵活被诟病, 但反而不会出现这个问题.
(hmr相关的细节和原理也可以在我以往的文章里看到)
前端框架
前端框架我已经2年没看过了, 之前在这里写了一些 vue, react 的最小实现之类的代码.
所以我在这里复习一下大思路, 很多细节重构了, 大思路还是不变的. (这里做简单的总结, 具体细节可以看我之前的文章)
vue
先说个概念effect, 指一段代码的执行中变量的改变, 会让这段代码重新执行.
而 vue 就是基于 watch effect 启动的.
第一次启动后, 运行 render 函数, 得到 vdom, 然后根据 vdom 操作贴到 dom 上.
后续因为变量改变触发启动, 在获得到 vdom 后, 进行计算, 尽力算出最少的 dom 操作来改变界面.
react
react 的启动方式与 vue 有所区别, 是循环判断是否有 wipFiber.
如果有就执行对应的函数(根据是 fc 还是 cc)获得 vdom, 再进行产生/修改 dom 的操作, 最后把最新的 dom 的新增/修改/删除应用到页面上.
vue 和 react 的比较
除了上面的启动方式, 在细节上还有很多不同.
- 执行方式: react 使用 fiber 来打破性能天花板, 但本身也有一定消耗.
- 响应系统: immutable 与 reactive. immutable 干净卫生性能好, 但容易各种错误操作影响性能和产生不更新的bug. reactive 对新手用户友好, 但自己本身有性能消耗. (下个部分展开)
- 更新粒度: react 更新调用
scheduleUpdateOnFiber来更新 wipFiber, vue 是每个组件都有一个watch effect, 所以他们的更新粒度都是组件. - 编译优化: vue 在编译时纯 html 提取, 记录 props 的变化, 记录 block tree, 这样在 diff 的时候就获得了额外的信息, 以此提高速度, 来应对没有 fiber 系统的问题.
vue2 和 vue3 响应式的比较
这里从几个角度简单说一下, 细节展开也请找一下我以前的文章.
- 从形式上: vue2 用了
Watcher类, vue3用effect, 效果是类似的, 还可以看出vue2的new Watcher(vm, vm._update(vm._render()), noop, null, true))这种无回调的 Watcher, 就是effect的原形. - 调用api上:
Object.defineProperty和Proxy是大家知道最多的.Proxy的性能会更好, 因为更直接, 也不需要拦截数组方法. - 在依赖关系储存上: 也是 api 导致的, vue3的依赖关系存储在全局的 weakMap 里. vue2 的依赖直接可见的是对象上挂的
.__ob__上, 这里储存的只是重写的数组方法. 通过 definproperty 储存的依赖, 是在对象的 getter, setter 上的. 每次重新更新依赖也会消耗性能.