diff --git a/404.html b/404.html new file mode 100644 index 0000000..36a0086 --- /dev/null +++ b/404.html @@ -0,0 +1,37 @@ + + +
+ + + + + +什么是前端性能优化?
前端性能
是指⻚⾯信息加⼯(⽐如数据展现、动画、操作效率等)的效率。
优化
是指借助相关技术⼿段提⾼这样的效率。
文章参考
前端性能优化之利用 Chrome Dev Tools 进行页面性能分析 - 知乎
FP、FCP、FMP、LCP 都是什么 P? - 知乎
我们知道,现在就是⼀个“流量为王”的时代,⼀个⽹站最重要的的就是⽤⼾,有了⽤⼾你才能有 业务,打⽐⽅,你是⼀个电商⽹站,那么你肯定希望你的⽤⼾越多越好,这样才会有更多的⼈去浏 览你的商品,从⽽在你的⽹站上花钱,买东西,这样你才能产⽣收益,但假如你的⽹站打开要⼗⼏ 秒,请求接⼝要⼗⼏秒,那⽤⼾还愿意等么?
看⼀下以下的⽤⼾体验图:
国外⼀些著名公司的调研:
所以说,做好性能优化,提⾼⽤⼾体验很重要!
⻚⾯运⾏的时间线(统计了从浏览器从⽹址开始导航到 window.onload
事件触发的⼀系列关键的时间点):
Performance API
是⼀组⽤于衡量 web 应⽤性能的标准接⼝,学习链接:Performance API
常⽤ Performance API:
performance.timing可以获取⽹⻚运⾏过程中每个时间点对应的时间戳(绝对时间,ms),但却即将废弃
performance.getEntries(),以对象数组的⽅式返回所有资源的数据,包括 css,img,script, xmlhttprequest,link 等等
performance.getEntriesByType(:string),和上⾯的 getEntries ⽅法类似,不过是多了⼀层类型 的筛选,常⻅性能类型可以有navigation(⻚⾯导航)、resource(资源加载)、paint(绘制指标) 等
// ⻚⾯导航时间
+performance.getEntriesByType("navigation");
+// 静态资源
+performance.getEntriesByType("resource");
+// 绘制指标
+performance.getEntriesByType("paint");
+
+/*需要定时轮询, 才能持续获取性能指标*/
+
performance.getEntriesByName(
+ "https://i0.hdslb.com/bfs/svgnext/BDC/danmu_square_line/v1.json"
+);
+
+performance.getEntriesByName(
+ "https://cloud.tencent.com/developer/api/user/session"
+);
+
+/*需要定时轮询, 才能持续获取性能指标*/
+
performance.timing.navigationStart
的时间差console.log(performance.now());
+// 5483324.099999994
+
(观察者模式)推荐,主要⽤于监测性能度量事件
/* 写法⼀ */
+//直接往 PerformanceObserver() ⼊参匿名回调函数,成功 new 了⼀个PerformanceObserver 类的,名为 observer 的对象
+var observer = new PerformanceObserver(function (list, obj) {
+ var entries = list.getEntries();
+ for (var i = 0; i < entries.length; i++) {
+ //处理“navigation”和“resource”事件
+ }
+});
+//调⽤ observer 对象的 observe() ⽅法
+observer.observe({ entryTypes: ["navigation", "resource"] });
+
+/* 写法⼆ */
+//预先声明回调函数 perf_observer
+function perf_observer(list, observer) {
+ //处理“navigation”事件
+}
+//再将其传⼊ PerformanceObserver(),成功 new 了⼀个 PerformanceObserver 类的,名为observer2 的对象
+var observer2 = new PerformanceObserver(perf_observer);
+//调⽤ observer2 对象的 observe() ⽅法
+observer2.observe({ entryTypes: ["navigation"] });
+
实例化 PerformanceObserver 对象,observe ⽅法的 entryTypes 主要性能类型有哪些?
console.log(PerformanceObserver.supportedEntryTypes);
+/*
+['element', 'event', 'first-input', 'largest-contentful-paint', 'layoutshift',
+'longtask', 'mark', 'measure', 'navigation', 'paint', 'resource',
+'visibility-state']
+*/
+
具体每个性能类型的含义:
类型 | 描述 |
---|---|
element | 元素加载时间,实例项是 PerformanceElementTiming 对象。 |
event | 事件延迟,实例项是 PerformanceEventTiming 对象。 |
first-input | ⽤⼾第⼀次与⽹站交互(即点击链接、点击按钮或使⽤⾃定义的 JavaScript 控件时)到浏览器实际能够响应该交互的时间,称之为 Firstinputdelay‒FID。 |
largest-contentful-paint | 屏幕上触发的最⼤绘制元素,实例项是 LargestContentfulPaint 对象。 |
layout-shift | 元素移动时候的布局稳定性,实例项是 LayoutShift 对象。 |
long-animation-frame | ⻓动画关键帧。 |
longtask | ⻓任务实例,归属于 PerformanceLongTaskTiming 对象。 |
mark | ⽤⼾⾃定义的性能标记。实例项是 PerformanceMark 对象。 |
measure | ⽤⼾⾃定义的性能测量。实例项是 PerformanceMeasure 对象。 |
navigation | ⻚⾯导航出去的时间,实例项是 PerformancePaintTiming 对象。 |
pain | ⻚⾯加载时内容渲染的关键时刻(第⼀次绘制,第⼀次有内容的绘制,实例项是 PerformancePaintTiming 对象。 |
resource | ⻚⾯中资源的加载时间信息,实例项是 PerformanceResourceTiming 对象。 |
visibility-state | ⻚⾯可⻅性状态更改的时间,即选项卡何时从前台更改为后台,反之亦然。实例项是 VisibilityStateEntry 对象。 |
soft-navigation | - |
⾸次绘制(First Paint)和⾸次内容绘制(First Contentful Paint)
⾸次绘制(FP)和⾸次内容绘制(FCP)。在浏览器导航并渲染出像素点后,这些性能指标点⽴即被标记。 这些点对于⽤⼾⽽⾔⼗分重要,直乎感官体验!
⾸次绘制(FP),⾸次渲染的时间点。FP 和 FCP 有点像,但 FP ⼀定先于 FCP 发⽣,例如⼀个⻚⾯加载时,第⼀个 DOM 还没绘制完成,但是可能这时⻚⾯的背景颜⾊已经出来了,这时 FP 指标就被记录下来了。⽽ FCP 会在⻚⾯绘制完第⼀个 DOM 内容后记录。
⾸次内容绘制(FCP),⾸次内容绘制的时间,指⻚⾯从开始加载到⻚⾯内容的任何部分在屏幕上完成渲染的时间。
/* PerformanceObserver监控 */
+const observer = new PerformanceObserver((list) => {
+ const entries = list.getEntries();
+ entries.forEach((entry) => {
+ if (entry.name === "first-paint") {
+ console.log("FP(⾸次绘制):", entry.startTime);
+ } else if (entry.name === "first-contentful-paint") {
+ console.log("FCP(⾸次内容绘制):", entry.startTime);
+ }
+ });
+});
+observer.observe({ entryTypes: ["paint"] });
+
+/* performance.getEntriesByName*/
+console.log(
+ "FP(⾸次绘制):" + performance.getEntriesByName("first-paint")[0].startTime
+);
+console.log(
+ "FCP(⾸次内容绘制):" +
+ performance.getEntriesByName("first-contentful-paint")[0].startTime
+);
+
⾸次有效绘制(First Meaningful Paint)
有效内容,这种⼀般很难清晰地界定哪些元素的加载是「有⽤」的(因此⽬前尚⽆规范),但对于开发者他们⾃⼰⽽⾔,他们更知道⻚⾯的哪些部分对于⽤⼾⽽⾔是最为有⽤的,所以这样的衡量标准更多的时候是掌握在开发者⼿上!
const observer = new PerformanceObserver((list) => {
+ const entries = list.getEntries();
+ entries.forEach((entry) => {
+ if (entry.name === "https://xxxxxx.xxx.jpg") {
+ console.log(entry.startTime);
+ }
+ });
+});
+observer.observe({ entryTypes: ["resource"] }); // 可以是图⽚、某个Dom元素
+
可交互时间(TTI
)
指标测量⻚⾯从开始加载(FCP)到主要⼦资源完成渲染,并能够快速、可靠地响应⽤⼾输⼊所需的时间。阻塞会影响正常可交互的时间,浏览器
主线程⼀次只能处理⼀个任务
,如果主线程⻓时间被占⽤,那么可交互时间也会变⻓,所以更多的 TTI 都是发⽣在主线程处于空闲的时间点
良好的TTI
应该控制在 5 秒以内。
测量TTI
的最佳⽅法是在⽹站上运⾏ Lighthouse 性能审核
console.log(performance.timing.domInteractive); // 可交互时间点
+
⻓任务(Long Task)
浏览器主线程⼀次只能处理⼀个任务。 某些情况下,⼀些任务将可能会花费很⻓的时间来执⾏,持续占⽤主进程资源,如果这种情况发⽣了,主线程阻塞,剩下的任务只能在队列中等待。
⽤⼾所感知到的可能是输⼊的延迟,或者是哐当⼀下全部出现。这些是当今⽹⻚糟糕体验的主要来源之⼀。
Long Tasks API 认为任何超过 50 毫秒的任务(Task)都可能存在潜在的问题,并将这些任务相关信息回调给给前端。
把 long task 时间定义为 50ms 的主要理论依据是 Chrome 提出的 RAIL 模型,RAIL 认为事件响应应该在 100ms 以内,滚动和动画处理应该在 16ms 以内,才能保证好的⽤⼾体验,⽽如果⼀个 task 执⾏超过 50ms,则很有可能让体验达不到 RAIL 的标准,故我们需要重点关注执⾏时间超过 50ms 的任务。
const observer = new PerformanceObserver((list) => {
+ const entries = list.getEntries();
+ entries.forEach((entry) => {
+ console.log("Long Task(⻓任务):", entry);
+ });
+});
+observer.observe({ entryTypes: ["longtask"] });
+
Largest Contentful Paint (LCP)
:最⼤内容绘制, ⽤于衡量加载性能。 为了提供良好的⽤⼾体 验,LCP 应在⽹⻚⾸次开始加载后的 2.5 秒内发⽣。First Input Delay (FID)
:⾸次输⼊延迟,⽤于衡量可交互性。为了提供良好的⽤⼾体验,⻚⾯的 FID 应不超过 100 毫秒。Cumulative Layout Shift (CLS)
:累积布局偏移,⽤于衡量视觉稳定性。为了提供良好的⽤⼾体 验,⻚⾯应保持 0.1 或更低的 CLSLighthouse-知名测评⼯具
说起异步加载
,我们需要先了解⼀下什么是同步加载?
// 默认就是同步加载
+<script src="http://abc.com/script.js"></script>
+
阻塞模式
,会阻⽌浏览器的后续处理,停⽌了后续的⽂件的解析,执⾏, 如图像的渲染。流览器之所以会采⽤同步模式,是因为加载的 js ⽂件中有对 dom 的操作,重定向, 输出 document 等默认⾏为,所以同步才是最安全的。所以⼀般我们都会把 script 标签放置在 body 结束标签之前,减少阻塞。⼏种常⻅的异步加载脚本⽅式:
async 和 defer
在 JavaScript 脚本增加 async 或者 defer 属性
// ⾯试经常问: script标签的defer和async的区别? //
+defer要等到html解析完成之后执⾏脚本
+<script src="main.js" defer></script>
+// async异步加载脚本后便会执⾏脚本
+<script src="main.js" async></script>
+
动态添加 script 标签
// js代码中动态添加script标签,并将其插⼊⻚⾯
+const script = document.createElement("script");
+script.src = "a.js";
+document.head.appendChild(script);
+
通过 XHR 异步加载 js
// ⾯试经常问: 谈谈JS中的 XMLHttpRequest 对象的理解?
+var xhr = new XMLHttpRequest();
+/*
+第⼀个参数是请求类型
+第⼆个参数是请求的URL
+第三个参数是是否为异步请求
+*/
+xhr.open("get", "/getUser", true); // true代表我们需要异步加载该脚本
+xhr.setRequestHeader("testHeader", "1111"); // ⾃定义Header
+xhr.send(null); // 参数为请求主体发送的数据,为必填项,当不需要发送数据时,使⽤null
+xhr.onreadyStateChange = function () {
+ if (xhr.readystate === 4) {
+ // ⾯试经常问: 说出你知道的哪些HTTP状态码?
+ if (xhr.status === 304 || (xhr.status >= 200 && xhr.status < 300)) {
+ console.log("成功, result: ", xhr.responseText);
+ } else {
+ console.log("错误, errCode:", xhr.status);
+ }
+ }
+};
+
随着 Webpack 等构建⼯具的能⼒越来越强,开发者在构建阶段可以随⼼所欲地打造项⽬流程,与此同 时按需加载和按需打包的技术曝光度也越来越⾼,甚⾄决定着⼯程化构建的结果,直接影响应⽤的性 能优化。
两者的概念:
按需打包
按需打包⼀般通过两种⽅法来实现:
import { Button } from "antd";
+// 假设我们的业务使⽤了Button组件,同时该组件库没有提供ES Module版本,
+// 那么这样的引⽤会导致最终打包的代码是所有antd导出的内容,这样会⼤⼤增加代码的体积
+
+// 但是如果我们组件库提供了ES Module版本(静态分析能⼒),并且开启了Tree Shaking功能,
+// 那么我们就可以通过“摇树”特性
+// 将不会被使⽤的代码在构建阶段移除。
+
正确使⽤ Tree Shaking 的姿势:
antd 组件库
// package.json
+{
+ // ...
+ "main": "lib/index.js", // 暴露CommonJS规范代码lib/index.js
+ "module": "es/index.js", // ⾮package.json标准字段,打包⼯具专⽤字段,指定符合ESM规范的⼊⼝⽂件
+ // 副作⽤配置字段,告诉打包⼯具遇到sideEffects匹配到的资源,均为⽆副作⽤的模块呢?
+ "sideEffects": [
+ "*.css",
+ " expample.js"
+ ],
+}
+
// 啥叫作副作⽤模块
+// expample.js
+const b = 2;
+export const a = 1;
+console.log(b);
+
项⽬:
Tree Shaking ⼀般与 Babel 搭配使⽤,需要在项⽬⾥⾯配置 Babel,因为 Babel 默认会把 ESM 规范打包 成 CommonJs 代码,所以需要通过配置 babel-preset-env#moudles 编译降级
production: {
+ presets: [
+ '@babel/preset-env',
+ {
+ modules: false
+ }
+ ]
+}
+
webpack4.0 以上在 mode 为 production 的时候会⾃动开启 Tree Shaking,实际就是依赖了、UglifyJS 等压缩插件,默认配置
const config = {
+ mode: 'production',
+ optimization: {
+ // 三类标记:
+ // used export: 被使⽤过的export会这样标记
+ // unused ha by rmony export: 没有被使⽤过的export被这样标记
+ // harmony import: 所有import会被这样标记
+ usedExports: true, // 使⽤usedExports进⾏标记
+ minimizer: {
+ new TerserPlugin({...}) // ⽀持删除未引⽤代码的压缩器
+ }
+ }
+}
+
[
+ {
+ libraryName: "antd",
+ libraryDirectory: "lib", // default: lib
+ style: true,
+ },
+ {
+ libraryName: "antd",
+ },
+];
+
import { TimePicker } from "antd"
+↓ ↓ ↓ ↓ ↓ ↓
+var _button = require('antd/lib/time-picker');
+
按需加载
如何才能动态地按需导⼊模块呢?
动态导⼊ import(module)
⽅法加载模块并返回⼀个 promise,该 promise resolve 为⼀个包含其所有导出的模块对象。我们可以在代码中的任意位置调⽤这个表达式。不兼容浏览器,可以⽤ Babel 进⾏转换(@babel/plugin-syntax-dynamic-import
)
// say.js
+export function hi() {
+ alert(`你好`);
+}
+export function bye() {
+ alert(`拜拜`);
+}
+export default function() {
+ alert("默认到处");
+}
+{
+hi: () => {},
+bye: () => {},
+default:"sdsd"
+}
+
<!DOCTYPE html>
+<script>
+ async function load() {
+ let say = await import("./say.js");
+ say.hi(); // 你好
+ say.bye(); // 拜拜
+ say.default(); // 默认导出
+ }
+</script>
+<button onclick="load()">Click me</button>
+
如果让你⼿写⼀个不考虑兼容性的 import(module)⽅法,你会怎么写?可以看下以下 Function-like
// 利⽤ES6模块化来实现
+const dynamicImport = (url) => {
+ return new Promise((resolve, reject) => {
+ // 创建script标签
+ const script = document.createElement("script");
+ const tempGlobal = "__tempModuleVariable" + Math.random().toString(32).substring(2);
+ // 通过设置 type="module",告诉浏览器该脚本是⼀个 ES6 模块,需要按照
+ 模块规范进⾏导⼊和导出
+ script.type = "module";
+ script.crossorigin="anonymous"; // 跨域
+ script.textContent = `import * as m from "${url}";window.${tempGlobal} = m;`;
+ // load 回调
+ script.onload = () => {
+ resolve(window[tempGlobal]);
+ delete window[tempGlobal];
+ script.remove();
+ };
+ // error回调
+ script.onerror = () => {
+ reject(new Error(`Fail to load module script with URL:${url}`));
+ delete window[tempGlobal];
+ script.remove();
+ };
+ document.documentElement.appendChild(script);
+ });
+}
+
可以从代码分割、服务端渲染、组件缓存、⻓列表优化等⻆度去分析Vue性能优化常⻅的策略。
const router = createRouter({
+ routes: [
+ // 借助import()实现异步组件
+ { path: '/foo', component: () => import('./Foo.vue') }
+ ]
+})
+
keep-alive
缓存⻚⾯:避免重复创建组件实例,且能保留缓存组件状态<keep-alive>
+ <component :is="Component"></component>
+</keep-alive>
+
v-show
复⽤DOM:避免重复创建组件<template>
+ <div class="cell">
+ <!-- 这种情况⽤v-show复⽤DOM,⽐v-if效果好 -->
+ <div v-show="value" class="on">
+ <Count :num="10000"/> display:none
+ </div>
+ <section v-show="!value" class="off">
+ <Count :num="10000"/>
+ </section>
+ </div>
+</template>
+
v-once
<!-- single element -->
+<span v-once>This will never change: {{msg}}</span>
+<!-- the element have children -->
+<div v-once>
+ <h1>comment</h1>
+ <p>{{msg}}</p>
+</div>
+<!-- component -->
+<my-component v-once :comment="msg"></my-component>
+<!-- `v-for` directive -->
+<ul>
+ <li v-for="i in list" v-once>{{i}}</li>
+</ul>
+
⻓列表性能优化:如果是⼤数据⻓列表,可采⽤虚拟滚动,只渲染少部分区域的内容,第三库vuevirtual-scroller
、vue-virtual-scroll-grid
图片懒加载
<!-- vue-lazyload -->
+<img v-lazy="/static/img/1.png">
+
import { createApp } from 'vue';
+import { Button, Select } from 'element-plus';
+
+const app = createApp()
+app.use(Button)
+app.use(Select)
+
####【render过程】避免不必要的Render
类组件跳过没有必要的组件更新
, 对应的技巧⼿段:PureComponent、React.memo、 shouldComponentUpdate。PureComponent 是对类组件的 Props 和 State 进⾏浅⽐较
React.memo是对函数组件的 Props 进⾏浅⽐较
shouldComponentUpdate是React类组件的钩⼦,在该钩⼦函数我们可以对前后props进⾏深⽐对,返回false可以禁⽌更新组件,我们可以⼿动控制组件的更新
传给⼦组件的派⽣状态或函数,每次都是新的引⽤,这样会导致⼦组件重新刷新
import { useCallback, useState, useMemo } from 'react';
+const [count, setCount] = useState(0);
+// 保证函数引⽤是⼀样的,在将该函数作为props往下传递给其他组件的时候,不会导致
+// 其他组件像PureComponent、shouldComponentUpdate、React.memo等相关优化失效
+// const oldFunc = () => setCount(count => count + 1)
+const newFunc = useCallback(() => setCount(count => count + 1), [])
+// useMemo与useCallback ⼏乎是99%相似,只是useMemo⼀般⽤于密集型计算⼤的⼀些缓存,
+// 它得到的是函数执⾏的结果
+const calcValue = useMemo(() => {
+ return Array(100000).fill('').map(v => /*耗时计算*/ v);
+}, [count]);
+
如果⼀个P组件,它有4个⼦组件ABCD,本⾝有个状态state p, 该状态只影响到AB ,那么我们可以把AB组件进⾏封装, state p 维护⾥⾯,那么state p变化了,也不会影响到CD组件的渲染
import ReactDOM from "react-dom";
+import { createContext, useState, useContext, useMemo } from "react";
+const Context = createContext({ val: 0 });
+const MyProvider = ({ children }) => {
+ const [val, setVal] = useState(0);
+ const handleClick = useCallback(() => {
+ setVal(val + 1);
+ },[val]);
+ const value = useMemo(() => {
+ return {
+ val: val
+ };
+ }, [val]);
+ return (
+ <Context.Provider value={value}>
+ {children}
+ <button onClick={handleClick}>context change</button>
+ </Context.Provider>
+ );
+};
+
+const useVal = () => useContext(Context);
+const Child1 = () => {
+ const { val } = useVal();
+ console.log("Child1重新渲染", val);
+ return <div>Child1</div>;
+};
+const Child2 = () => {
+ console.log("Child2只渲染⼀次");
+ return <div>Child2</div>;
+};
+function App() {
+return (
+ <MyProvider>
+ <Child1 />
+ <Child2 />
+ </MyProvider>
+ );
+}
+const rootElement = document.getElementById("root");
+ReactDOM.render(<App/>, rootElement);
+
那我如果使⽤索引值index作为key,为啥不推荐?⾯试题
// ⽆⽤更新
+<!-- 更新前 -->
+<li key="0">Tom</li>
+<li key="1">Sam</li>
+<li key="2">Ben</li>
+<li key="3">Pam</li>
+<!-- 删除后更新 -->
+<li key="0">Sam</li>
+<li key="1">Ben</li>
+<li key="2">Pam</li>
+
+// 输⼊错乱
+<!-- 更新前 -->
+<input key="0" value="1" id="id1"/>
+<input key="1" value="2" id="id2"/>
+<input key="3" value="3" id="id3"/>
+<input key="4" value="4" id="id4"/>
+<!-- 删除后更新 -->
+<input key="1" value="1" id="id2"/>
+<input key="3" value="2" id="id3"/>
+<input key="4" value="3" id="id4"/>
+
React.lazy
⽅法import { lazy, Suspense, Component } from "react"
+const Com = lazy(() => {
+ return new Promise((resolve, reject) => {
+ setTimeout(() => {
+ if (Math.random() > 0.5) {
+ reject(new Error("error"))
+ } else {
+ resolve(import("./Component"))
+ }
+ }, 1000)
+ })
+})
+// ...
+<Suspense fallback="加载...">
+ <Com />
+</Suspense>
+
参考文章
webpack 打包原理及流程解析,超详细!
webpack打包原理 ? 看完这篇你就懂了 !
在目前的项目中,我们会有很多依赖包,webpack负责将浏览器不能识别的文件类型、语法等转化为可识别的前端三剑客(html,css,js),并在这个过程中充当组织者与优化者的角色。
- Bundle(捆绑包)是指将所有相关的模块和资源打包在一起形成的单个文件。它是应用程序的最终输出,可以在浏览器中加载和执行。
- 捆绑包通常由Webpack根据入口点(entry)和它们的依赖关系自动创建。当你运行Webpack构建时,它会根据配置将所有模块和资源打包成一个或多个捆绑包。
- Chunk(代码块)是Webpack在打包过程中生成的中间文件,它代表着一个模块的集合。
- Webpack 根据代码的拓扑结构和配置将模块组织成不同的代码块。每个代码块可以是一个独立的文件,也可以与其他代码块组合成一个捆绑包。
- Webpack使用代码分割(code splitting)技术将应用程序代码拆分成更小的代码块,以便在需要时进行按需加载。这有助于减小初始加载的文件大小,提高应用程序的性能。
- 在Webpack中,捆绑包和代码块之间存在一对多的关系。一个捆绑包可以包含多个代码块,而一个代码块也可以属于多个不同的捆绑包。这取决于Webpack配置中的拆分点(split points)和代码块的依赖关系。
- 总结起来,bundle 是Webpack打包过程的最终输出文件,而chunk是Webpack在打包过程中生成的中间文件,用于组织和按需加载模块。
- 入口起点(entry point)指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。
- 进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。
- 每个依赖项随即被处理,最后输出到称之为 bundles 的文件中。
- output 属性告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件,默认值为 ./dist。
- 基本上,整个应用程序结构,都会被编译到你指定的输出路径的文件夹中。
模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。
代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。
- loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。
- loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。
- 本质上,webpack loader 将所有类型的文件,转换为应用程序的依赖图(和最终的 bundle)可以直接引用的模块。
- loader 被用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。
- 插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件接口功能极其强大,可以用来处理各种各样的任务。
Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程 :
在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。
关注前端可以很好地提高性能。如果我们可以将后端响应时间缩短一半,整体响应时间只能减少 5%~10%。而如果关注前端性能,同样是将其响应时间减少一半,则整体响应时间可以减少 40%~45%。
改进前端通常只需要较少的时间和资源,减少后端延迟会带来很大的改动。
只有 10%~20%的最终用户响应时间花在了下载 HTML 文档上,其余的 80%~90%时间花在了下载页面中的所有组件上。
在前端日常开发中,技术上的选择是非常重要的。为什么要讲这个呢?因为现象频发。
前端工程化严重的当下,轻量化的框架慢慢被遗忘掉了。并不是所有的业务场景都适合使用工程化框架,react/vue 并不轻量。
复杂的框架是为了解决复杂的业务
如果研发 h5、PC 展示等场景简单的业务时候,javascript 原生 配合一些轻量化插件更适合。
多页面应用也并不都是缺点。根据业务不同而选择不一样的技术是非常重要的,是每个前端都应该反思的事情。
这方面是导致卡顿的关键问题。
我们的老朋友 NetWork 想必前端同学都很熟悉。我们先来看一下 network 面板 从面板上我们可以看出一些信息:
瀑布图是什么呢?
瀑布图就是上方图片后面的 waterfall 纵列
瀑布图是一个级联图, 展示了浏览器如何加载资源并渲染成网页. 图中的每一行都是一次单独的浏览器请求. 这个图越长, 说明加载网页过程中所发的请求越多. 每一行的宽度, 代表浏览器发出请求并下载该资源的过程中所耗费的时间。它的侧重点在于分析网路链路
瀑布图颜色说明:
DNS Lookup [深绿色] - 在浏览器和服务器进行通信之前, 必须经过 DNS 查询, 将域名转换成 IP 地址. 在这个阶段, 你可以处理的东西很少. 但幸运的是, 并非所有的请求都需要经过这一阶段.
Initial Connection [橙色] - 在浏览器发送请求之前, 必须建立 TCP 连接. 这个过程仅仅发生在瀑布图中的开头几行, 否则这就是个性能问题(后边细说).
SSL/TLS Negotiation [紫色] - 如果你的页面是通过 SSL/TLS 这类安全协议加载资源, 这段时间就是浏览器建立安全连接的过程. 目前 Google 将 HTTPS 作为其 搜索排名因素 之一, SSL/TLS 协商的使用变得越来越普遍了.
Time To First Byte (TTFB) [绿色] - TTFB 是浏览器请求发送到服务器的时间+服务器处理请求时间+响应报文的第一字节到达浏览器的时间. 我们用这个指标来判断你的 web 服务器是否性能不够, 或者说你是否需要使用 CDN.
Downloading (蓝色) - 这是浏览器用来下载资源所用的时间. 这段时间越长, 说明资源越大. 理想情况下, 你可以通过控制资源的大小来控制这段时间的长度.
那么除了瀑布图的长度外,我们如何才能判断一个瀑布图的状态是健康的呢?
首先, 减少所有资源的加载时间. 亦即减小瀑布图的宽度. 瀑布图越窄, 网站的访问速度越快.
其次, 减少请求数量 也就是降低瀑布图的高度. 瀑布图越矮越好.
最后, 通过优化资源请求顺序来加快渲染时间. 从图上看, 就是将绿色的"开始渲染"线向左移. 这条线向左移动的越远越好.
这样,我们就可以从 network 的角度去排查“慢”的问题。
项目构建后生成的 bundle 包是压缩后的。webpack-bundle-analyzer 是一款包分析工具。
我们先来看一下它能带来的效果。如下图:
从上图来看,我们的 bundle 包被解析的一览无余。其中模块面积占的越大说明在 bundle 包中 size 越大。就值得注意了,重点优化一下。
它能够排查出来的信息有
显示包中所有打入的模块 显示模块 size 及 gzip 后的 size 排查包中的模块情形是非常有必要的,通过 webpack-bundle-analyzer 来排查出一些无用的模块,过大的模块。然后进行优化。以减少我们的 bundle 包 size,减少加载时长。
安装
# NPM
+npm install --save-dev webpack-bundle-analyzer
+# Yarn
+yarn add -D webpack-bundle-analyzer
+
使用(as a Webpack-Plugin)
const BundleAnalyzerPlugin =
+ require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
+
+module.exports = {
+ plugins: [new BundleAnalyzerPlugin()],
+};
+
然后构建包完毕后会自动弹出一个窗口展示上图信息。
chrome 自带的 performance 模块。先附上一个官网文档传送门:Performance
可以检测很多方面的数据,多数情况的性能排查上用的比较多。如果想要深入了解的同学建议去看一下官方文档。
接下来我们来说一下在 performance 面板中如何排差“慢”的问题,它给我们提供了哪些信息呢。先附上一张 performance 的面板图片。 从上图中可以分析出一些指标
这些指标就是我们需要重点关注的,当然 performance 的功能并不止于此。
先记住如何获取到这些指标,后面来一一进行解析优化。
获取各个阶段的响应时间,我们所要用到的接口是 PerformanceNavigationTiming 接口。
PerformanceNavigationTiming 提供了用于存储和检索有关浏览器文档事件的指标的方法和属性。 例如,此接口可用于确定加载或卸载文档需要多少时间。
function showNavigationDetails() {
+ const [entry] = performance.getEntriesByType("navigation");
+ console.table(entry.toJSON());
+}
+
使用这个函数,我们就可以获取各个阶段的响应时间,如图: 参数说明
- navigationStart 加载起始时间
- redirectStart 重定向开始时间(如果发生了 HTTP 重定向,每次重定向都和当前文档同域的话,就返回开始重定向的 fetchStart 的值。其他情况,则返回 0)
- redirectEnd 重定向结束时间(如果发生了 HTTP 重定向,每次重定向都和当前文档同域的话,就返回最后一次重定向接受完数据的时间。其他情况则返回 0)
- fetchStart 浏览器发起资源请求时,如果有缓存,则返回读取缓存的开始时间
- domainLookupStart 查询 DNS 的开始时间。如果请求没有发起 DNS 请求,如 keep-alive,缓存等,则返回 fetchStart
- domainLookupEnd 查询 DNS 的结束时间。如果没有发起 DNS 请求,同上
- connectStart 开始建立 TCP 请求的时间。如果请求是 keep-alive,缓存等,则返回 domainLookupEnd
- (secureConnectionStart) 如果在进行 TLS 或 SSL,则返回握手时间
- connectEnd 完成 TCP 链接的时间。如果是 keep-alive,缓存等,同 connectStart
- requestStart 发起请求的时间
- responseStart 服务器开始响应的时间
- domLoading 从图中看是开始渲染 dom 的时间,具体未知
- domInteractive 未知
- domContentLoadedEventStart 开始触发 DomContentLoadedEvent 事件的时间
- domContentLoadedEventEnd DomContentLoadedEvent 事件结束的时间
- domComplete 从图中看是 dom 渲染完成时间,具体未知
- loadEventStart 触发 load 的时间,如没有则返回 0
- loadEventEnd load 事件执行完的时间,如没有则返回 0
- unloadEventStart unload 事件触发的时间
- unloadEventEnd unload 事件执行完的时间
关于我们的 Web 性能,我们会用到的时间参数:
根据这些时间参数,我们就可以判断哪一阶段对性能有影响。
有一些业务状况是没有上述的一些调试工具该怎么办呢?我们可以利用抓包工具进行对页面信息对抓取,上述我们通过 chrome 工具排查出来的指标,也可以通过抓包工具进行抓取。
这里我推荐一款抓包工具 charles。
前端的优化种类繁多,主要包含三个方面的优化:网络优化(对加载时所消耗的网络资源优化),代码优化(资源加载完后,脚本解释执行的速度),框架优化(选择性能较好的框架,比如 benchmark)。
中文(摇树),webpack 构建优化中重要一环。摇树用于清除我们项目中的一些无用代码,它依赖于 ES 中的模块语法。
比如日常使用 lodash 的时候
import _ from "lodash";
+
如果如上引用 lodash 库,在构建包的时候是会把整个 lodash 包打入到我们的 bundle 包中的。
import _isEmpty from "lodash/isEmpty";
+
如果如上引用 lodash 库,在构建包的时候只会把 isEmpty 这个方法抽离出来再打入到我们的 bundle 包中。
这样的化就会大大减少我们包的 size。所以在日常引用第三方库的时候,需要注意导入的方式。
如何开启摇树
在 webpack4.x 中默认对 tree-shaking 进行了支持。 在 webpack2.x 中使用 tree-shaking:传送门
中文(分包)
在没配置任何东西的情况下,webpack 4 就智能的帮你做了代码分包。入口文件依赖的文件都被打包进了 main.js,那些大于 30kb 的第三方包,如:echarts、xlsx、dropzone 等都被单独打包成了一个个独立 bundle。
其它被我们设置了异步加载的页面或者组件变成了一个个 chunk,也就是被打包成独立的 bundle。
它内置的代码分割策略是这样的:
- 新的 chunk 是否被共享或者是来自 node_modules 的模块
- 新的 chunk 体积在压缩之前是否大于 30kb
- 按需加载 chunk 的并发请求数量小于等于 5 个
- 页面初始加载时的并发请求数量小于等于 3 个
大家可以根据自己的项目环境来更改配置。配置代码如下:
splitChunks({
+ cacheGroups: {
+ vendors: {
+ name: `chunk-vendors`,
+ test: /[\\/]node_modules[\\/]/,
+ priority: -10,
+ chunks: "initial",
+ },
+ dll: {
+ name: `chunk-dll`,
+ test: /[\\/]bizcharts|[\\/]\@antv[\\/]data-set/,
+ priority: 15,
+ chunks: "all",
+ reuseExistingChunk: true,
+ },
+ common: {
+ name: `chunk-common`,
+ minChunks: 2,
+ priority: -20,
+ chunks: "all",
+ reuseExistingChunk: true,
+ },
+ },
+});
+
没有使用 webpack4.x 版本的项目,依然可以通过按需加载的形式进行分包,使得我们的包分散开,提升加载性能。
按需加载也是以前分包的重要手段之一
这里推荐一篇非常好的文章:webpack 如何使用按需加载
与 3.2 的分包不同。大家可能没发现,上面 2.3 的 bundle 包解析中有个有趣的现象,上面项目的技术栈是 react,但是 bundle 包中并没有 react、react-dom、react-router 等。
因为把这些插件“拆”开了。并没有一起打在 bundle 中。而是放在了 CDN 上。下面我举一个例子来解释一下。
假设:原本 bundle 包为 2M,一次请求拉取。拆分为 bundle(1M) + react 桶(CDN)(1M) 两次请求并发拉取。
从这个角度来看,1+1 的模式拉取资源更快。
换一个角度来说,全量部署项目的情况,每次部署 bundle 包都将重新拉取。比较浪费资源。react 桶的方式可以命中强缓存,这样的化,就算全量部署也只需要重新拉取左侧 1M 的 bundle 包即可,节省了服务器资源。优化了加载速度。
注意:在本地开发过程中,react 等资源建议不要引入 CDN,开发过程中刷新频繁,会增加 CDN 服务其压力,走本地就好。
服务端配置 gzip 压缩后可大大缩减资源大小。
Nginx 配置方式
http {
+ gzip on;
+ gzip_buffers 32 4K;
+ gzip_comp_level 6;
+ gzip_min_length 100;
+ gzip_types application/javascript text/css text/xml;
+ gzip_disable "MSIE [1-6]\.";
+ gzip_vary on;
+}
+
配置完成后在 response header 中可以查看。
开发中比较重要的一个环节,我司自己的图床工具是自带压缩功能的,压缩后直接上传到 CDN 上。
如果公司没有图床工具,我们该如何压缩图片呢?我推荐几种我常用的方式
图片压缩是常用的手法,因为设备像素点的关系,UI 给予的图片一般都是 x2,x4 的,所以压缩就非常有必要。
如果页面中有一张效果图,比如真机渲染图,UI 手拿着刀不让你压缩。这时候不妨考虑一下分割图片。
建议单张土图片的大小不要超过 100k,我们在分割完图片后,通过布局再拼接在一起。可以图片加载效率。
这里注意一点,分割后的每张图片一定要给 height,否则网速慢的情况下样式会塌陷。
南方叫精灵图,北方叫雪碧图。这个现象就很有趣。
在网站中有很多小图片的时候,一定要把这些小图片合并为一张大的图片,然后通过 background 分割到需要展示的图片。
这样的好处是什么呢?先来普及一个规则
浏览器请求资源的时候,同源域名请求资源的时候有最大并发限制,chrome 为 6 个,就比如你的页面上有 10 个相同 CDN 域名小图片,那么需要发起 10 次请求去拉取,分两次并发。第一次并发请求回来后,发起第二次并发。
如果你把 10 个小图片合并为一张大图片的画,那么只用一次请求即可拉取下来 10 个小图片的资源。减少服务器压力,减少并发,减少请求次数。
附上一个 sprite 的例子。
中文(内容分发网络),服务器是中心化的,CDN 是“去中心化的”。
在项目中有很多东西都是放在 CDN 上的,比如:静态文件,音频,视频,js 资源,图片。那么为什么用 CDN 会让资源加载变快呢?
举个简单的例子:
以前买火车票大家都只能去火车站买,后来我们买火车票就可以在楼下的火车票代售点买了。
你细品。
所以静态资源度建议放在 CDN 上,可以加快资源加载的速度。
懒加载也叫延迟加载,指的是在长网页中延迟加载图像,是一种非常好的优化网页性能的方式。
当可视区域没有滚到资源需要加载的地方时候,可视区域外的资源就不会加载。
可以减少服务器负载,常适用于图片很多,页面较长的业务场景中。
如何使用懒加载呢?
图片懒加载 layzr.js
中文(字体图表),现在比较流行的一种用法。使用字体图表有几种好处
- 矢量
- 轻量
- 易修改
- 不占用图片资源请求。
就像上面说的雪碧图,如果都用字体图标来替换的画,一次请求都免了,可以直接打到 bundle 包中。
使用前提是 UI 给点力,设计趋向于字体图标,提前给好资源,建立好字体图标库。
逻辑后移是一种比较常见的优化手段。用一个打开文章网站的操作来举个例子。
没有逻辑后移处理的请求顺序是这个样子的 页面的展示主体是文章展示,如果文章展示的请求靠后了,那么渲染文章出来的时间必然靠后,因为有可能因为请求阻塞等情况,影响请求响应情况,如果超过一次并发的情况的话,会更加的慢。如图的这种情况也是在我们项目中发生过的。
很明显我们应该把主体“请求文章”接口前移,把一些非主体的请求逻辑后移。这样的话可以尽快的把主体渲染出来,就会快很多。
优化后的顺序是这个样子的。 在平常的开发中建议时常注意逻辑后移的情况,突出主体逻辑。可以极大的提升用户体验。
在数据量大的应用场景中,需要着重注意算法复杂度问题。
在这个方面可以参考 Javascript 算法之复杂度分析这篇文章。
如上面 Performance 解析出的 Javascript 执行指标上,可以推测出来你的 code 执行效率如何,如果执行时间过长就要考虑一下是否要优化一下复杂度了。
在时间换空间,空间换时间的选择上,要根据业务场景来进行取舍。
拿 react 举例,组件分割方面不要太深。需要控制组件的渲染,尤其是深层组件的 render。
老生常谈的话题,我们可以一些方式来优化组件渲染
中文(node 中间件)
中间件主要是指封装所有 Http 请求细节处理的方法。一次 Http 请求通常包含很多工作,如记录日志、ip 过滤、查询字符串、请求体解析、Cookie 处理、权限验证、参数验证、异常处理等,但对于 Web 应用而言,并不希望接触到这么多细节性的处理,因此引入中间件来简化和隔离这些基础设施与业务逻辑之间的细节,让我们能够关注在业务的开发上,以达到提升开发效率的目的。
使用 node middleware 合并请求。减少请求次数。这种方式也是非常实用的。
Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。
合理实用 web worker 可以优化复杂计算任务。这里直接抛阮一峰的入门文章:传送门
缓存的原理就是更快读写的存储介质+减少 IO+减少 CPU 计算=性能优化。而性能优化的第一定律就是:优先考虑使用缓存。
缓存的主要手段有:浏览器缓存、CDN、反向代理、本地缓存、分布式缓存、数据库缓存。
每个网页或多或少都涉及到一些 CSS 动画,通常简单的动画对于性能的影响微乎其微,然而如果涉及到稍显复杂的动画,不当的处理方式会使性能问题变得十分突出。
像 Chrome, FireFox, Safari, IE9+和最新版本的 Opera 都支持 GPU 加速,当它们检测到页面中某个 DOM 元素应用了某些 CSS 规则时就会开启。
虽然我们可能不想对元素应用 3D 变换,可我们一样可以开启 3D 引擎。例如我们可以用 transform: translateZ(0) 来开启 GPU 加速 。
只对我们需要实现动画效果的元素应用以上方法,如果仅仅为了开启硬件加速而随便乱用,那是不合理的。
Ajax 在发送的数据成功后,为了提高页面的响应速度和用户体验,会把请求的 URL 和返回的响应结果保存在缓存内,当下一次调用 Ajax 发送相同的请求(URL 和参数完全相同)时,它就会直接从缓存中拿数据。
在进行 Ajax 请求的时候,可以选择尽量使用 get 方法,这样可以使用客户端的缓存,提高请求速度。
Resource Hints(资源预加载)是非常好的一种性能优化方法,可以大大降低页面加载时间,给用户更加流畅的用户体验。
现代浏览器使用大量预测优化技术来预测用户行为和意图,这些技术有预连接、资源与获取、资源预渲染等。
Resource Hints 的思路有如下两个:
实现 Resource Hints 的方法有很多种,可分为基于 link 标签的 DNS-prefetch、subresource、preload、 prefetch、preconnect、prerender,和本地存储 localStorage。
渲染过程在服务器端完成,最终的渲染结果 HTML 页面通过 HTTP 协议发送给客户端,又被认为是‘同构'或‘通用',如果你的项目有大量的 detail 页面,相互特别频繁,建议选择服务端渲染。
服务端渲染(SSR)除了 SEO 还有很多时候用作首屏优化,加快首屏速度,提高用户体验。但是对服务器有要求,网络传输数据量大,占用部分服务器运算资源。
Vue 的 Nuxt.js 和 React 的 next.js 都是服务端渲染的方法。
UNPKG 是一个提供 npm 包进行 CDN 加速的站点,因此,可以将一些比较固定了依赖写入 html 模版中,从而提高网页的性能。首先,需要将这些依赖声明为 external,以便 webpack 打包时不从 node_modules 中加载这些资源,配置如下:
externals: { 'react': 'React' }
+
其次,你需要将所依赖的资源写在 html 模版中,这一步需要用到 html-webpack-plugin。下面是一段示例:
<% if (htmlWebpackPlugin.options.node_env === 'development') { %>
+ <script src="https://unpkg.com/react@16.7.0/umd/react.development.js"></script>
+<% } else { %>
+ <script src="https://unpkg.com/react@16.7.0/umd/react.production.min.js"></script>
+<% } %>
+
这段代码需要注入 node_env,以便在开发的时候能够获得更友好的错误提示。也可以选择一些比较自动的库,来帮助我们完成这个过程,比如 webpack-cdn-plugin,或者 dynamic-cdn-webpack-plugin。
还有一些比较常用的优化方法我没有列举出来,例如将样式表放在顶部,将脚本放在底部,减少重绘,按需加载,模块化等。方法很多,对症下药才是关键。
借鉴了很多大佬最后总结出来的文章,希望自己和同为菜鸟的小伙伴可以永远怀着一颗学徒的心。
<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>hash路由</title>
+ </head>
+ <body>
+ <!-- hash路由 -->
+
+ <a href="#/a">跳转 A 页面</a>
+ <a href="#/b">跳转 B 页面</a>
+ <div id="box" style="border: 10px solid #000; height: 200px"></div>
+
+ <button onclick="to('/a')">跳转到 A路由</button>
+ <button onclick="to('/b')">跳转到 B路由</button>
+ <button onclick="to('/c')">跳转到 C路由</button>
+ <script>
+ let box = document.getElementById("box");
+ window.addEventListener("hashchange", function (e) {
+ //hashchange
+ box.innerHTML = location.hash;
+ console.log(e);
+ });
+
+ function to(path) {
+ box.innerHTML = path;
+ history.pushState({}, null, path);
+ }
+ </script>
+ </body>
+</html>
+
在 HTML 文档中,history.pushState() 方法向浏览器的会话历史栈增加了一个条目。
该方法是异步的。为 popstate 事件增加监听器,以确定导航何时完成。state 参数将在其中可用。
语法
pushState(state, unused)
+pushState(state, unused, url)
+
参数
state
state 对象是一个 JavaScript 对象,其与通过 pushState() 创建的新历史条目相关联。每当用户导航到新的 state,都会触发 popstate 事件,并且该事件的 state 属性包含历史条目 state 对象的副本。
state 对象可以是任何可以序列化的对象。因为 Firefox 将 state 对象保存到用户的磁盘上,以便用户重启浏览器可以恢复,我们对 state 对象序列化的表示施加了 16 MiB 的限制。如果你传递的 state 对象的序列化表示超出了 pushState() 可接受的大小,该方法将抛出异常。如果你需要更多的空间,建议使用 sessionStorage 和/或 localStorage。
unused
由于历史原因,该参数存在且不能忽略;传递一个空字符串是安全的,以防将来对该方法进行更改。
url 可选
新历史条目的 URL。请注意,浏览器不会在调用 pushState() 之后尝试加载该 URL,但是它可能会在以后尝试加载该 URL,例如,在用户重启浏览器之后。新 URL 可以不是绝对路径;如果它是相对的,它将相对于当前的 URL 进行解析。新的 URL 必须与当前 URL 同源;否则,pushState() 将抛出异常。如果该参数没有指定,则将其设置为当前文档的 URL。
在正式开始看路由的实现之前,先来看看自定义元素和自定义事件
<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>自定义事件</title>
+ </head>
+ <body>
+ <h1>用一个构造函数 CustomEvent</h1>
+ <script>
+ // 创建自定义事件
+ const catFound = new CustomEvent("animalfound", {
+ detail: {
+ name: "猫",
+ },
+ });
+ const dogFound = new CustomEvent("animalfound", {
+ detail: {
+ name: "狗",
+ },
+ });
+
+ // 添加合适的事件监听器
+ window.addEventListener("animalfound", (e) => console.log(e.detail.name));
+
+ // 触发事件
+ window.dispatchEvent(catFound);
+ window.dispatchEvent(dogFound);
+
+ // 控制台中输出“猫”和“狗”
+ </script>
+ </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Document</title>
+ </head>
+ <body>
+ <!-- 注意这个 is !!!! -->
+ <p is="word-count">xiaoyu</p>
+ <script>
+ // Create a class for the element
+ class WordCount extends HTMLParagraphElement {
+ constructor() {
+ // Always call super first in constructor
+ super();
+
+ // count words in element's parent element
+ var wcParent = this.parentNode;
+ console.log(wcParent);
+
+ function countWords(node) {
+ var text = node.innerText || node.textContent;
+ return text.length;
+ }
+
+ var count = "Words: " + countWords(wcParent);
+
+ // Create a shadow root
+ var shadow = this.attachShadow({ mode: "open" });
+
+ // Create text node and add word count to it
+ var text = document.createElement("span");
+ text.textContent = count;
+
+ // Append it to the shadow root
+ shadow.appendChild(text);
+
+ // Update count when element content changes
+ setInterval(function () {
+ var count = "Words: " + countWords(wcParent);
+ text.textContent = count;
+ }, 200);
+ }
+ }
+
+ // Define the new element
+ customElements.define("word-count", WordCount, { extends: "p" });
+
+ let ctor = customElements.get("word-count");
+ console.log(ctor);//获取构造函数
+ console.log(customElements.getName(WordCount) === "word-count");//比较
+ </script>
+ </body>
+</html>
+
<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>自定义元素</title>
+ <style>
+ body{
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 80vh;
+ }
+ </style>
+ </head>
+ <body>
+ <popup-info
+ img="../../../exer/photo/图片1.png"
+ text="Your card validation code (CVC) is an extra
+ security feature — it is the last 3 or 4
+ numbers on the back of your card."></popup-info>
+
+ <script>
+ // Create a class for the element
+ // 自主定制元素的构造函数必须扩展HTMLElement。@@@@!!!!
+ class PopUpInfo extends HTMLElement {
+ constructor() {
+ // Always call super first in constructor
+ super();
+
+ // Create a shadow root
+ // Element.attachShadow() 方法给指定的元素挂载一个 Shadow DOM,并且返回对 ShadowRoot 的引用。@@@@!!!
+ var shadow = this.attachShadow({ mode: "open" });
+ // console.log(this.shadowRoot);
+ // console.log(shadow);
+
+ // Create spans
+ var wrapper = document.createElement("span");
+ wrapper.setAttribute("class", "wrapper");
+ var icon = document.createElement("span");
+ icon.setAttribute("class", "icon");
+ icon.setAttribute("tabindex", 0);
+ var info = document.createElement("span");
+ info.setAttribute("class", "info");
+
+ // Take attribute content and put it inside the info span
+ var text = this.getAttribute("text");
+ info.textContent = text;
+
+ // Insert icon
+ var imgUrl;
+ if (this.hasAttribute("img")) {
+ imgUrl = this.getAttribute("img");
+ } else {
+ imgUrl = "img/default.png";
+ }
+ var img = document.createElement("img");
+ img.src = imgUrl;
+ icon.appendChild(img);
+
+ // Create some CSS to apply to the shadow dom
+ var style = document.createElement("style");
+
+ style.textContent =
+ ".wrapper {" +
+ "position: relative;" +
+ "}" +
+ ".info {" +
+ "font-size: 0.8rem;" +
+ "width: 200px;" +
+ "display: inline-block;" +
+ "border: 1px solid black;" +
+ "padding: 10px;" +
+ "background: white;" +
+ "border-radius: 10px;" +
+ "opacity: 0;" +
+ "transition: 0.6s all;" +
+ "position: absolute;" +
+ "bottom: 20px;" +
+ "left: 10px;" +
+ "z-index: 3;" +
+ "}" +
+ "img {" +
+ "width: 1.2rem" +
+ "}" +
+ ".icon:hover + .info, .icon:focus + .info {" +
+ "opacity: 1;" +
+ "}";
+
+ // attach the created elements to the shadow dom
+
+ shadow.appendChild(style);
+ shadow.appendChild(wrapper);
+ wrapper.appendChild(icon);
+ wrapper.appendChild(info);
+ }
+ }
+
+ // Define the new element
+ customElements.define("popup-info", PopUpInfo);
+ </script>
+ </body>
+</html>
+
路由界面文件
<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Document</title>
+ <style>
+ .c-link{
+ background-color: yellow;
+ width: 100px;
+ line-height: 40px;
+ margin: 20px;
+ cursor: pointer;
+ display: inline-block;
+ text-align: center;
+ }
+ .c-link:active{
+ transform: scale(0.8);
+ }
+ </style>
+ </head>
+ <body>
+ <div class="product-item">测试的产品</div>
+ <h3>原始路径在页面加载时写到了你的剪切板上!!!意味着你可以直接在URL地址栏进行粘贴</h1>
+ <div class="flex">
+ <ul class="menu-x">
+ <c-link to="/" class="c-link">首页</c-link>
+ <c-link to="/about" class="c-link">关于</c-link>
+ </ul>
+ </div>
+ <div>
+ <c-router>
+ <c-route path="/" component="home" default></c-route>
+ <c-route path="/detail/:id" component="detail"></c-route>
+ <c-route path="/about" component="about"></c-route>
+ </c-router>
+ </div>
+ <!-- 记录开始渲染的地址 -->
+ <script>
+ navigator.clipboard.writeText(location.href)
+ </script>
+
+ <script src="./router.js"></script>
+ </body>
+</html>
+
router.js
const oriPushState = history.pushState;
+/*
+不借助第三方工具库实现路由,我们需要思考以下几个问题:
+如何实现自定义标签,如vue的<router-view>,React的<Router>
+如何实现业务组件
+如何动态切换路由
+*/
+
+/* 如果想监听 pushState 和 replaceState 行为,可以通过在方法里面主动去触发 popstate 事件,
+另一种是重写history.pushState,通过创建自己的eventedPushState自定义事件,并手动派发,实际使用过程中就可以监听了。 */
+// 重写pushState
+history.pushState = function (state, title, url) {
+ // 触发原事件
+ oriPushState.apply(history, [state, title, url]);
+ // 自定义事件
+ var event = new CustomEvent("c-popstate", {
+ detail: {
+ state,
+ title,
+ url
+ }
+ });
+ //触发这个事件
+ window.dispatchEvent(event);
+}
+
+// <c-link to="/" class="c-link">首页</c-link>
+class CustomLink extends HTMLElement {
+ connectedCallback() {
+ this.addEventListener("click", ev => {
+ ev.preventDefault();
+ const to = this.getAttribute("to");
+ // 更新浏览历史记录
+ history.pushState("", "", to);
+ })
+ }
+}
+window.customElements.define("c-link", CustomLink);
+
+// 优先于c-router注册
+// <c-toute path="/" component="home" default></c-toute>
+class CustomRoute extends HTMLElement {
+ #data = null;
+ getData() {
+ return {
+ default: this.hasAttribute("default"),
+ path: this.getAttribute("path"),
+ component: this.getAttribute("component")
+ }
+ }
+}
+window.customElements.define("c-route", CustomRoute);
+
+// 容器组件
+class CustomComponent extends HTMLElement {
+ async connectedCallback() {
+ // 获取组件的path,即html的路径
+ const strPath = this.getAttribute("path");
+ // 加载html
+ const cInfos = await loadComponent(strPath);
+ const shadow = this.attachShadow({ mode: "closed" });
+ // 添加html对应的内容
+ this.#addElement(shadow, cInfos);
+ }
+ #addElement(shadow, info) {
+ // 添加模板内容
+ if (info.template) {
+ shadow.appendChild(info.template.content.cloneNode(true));
+ }
+ // 添加脚本
+ if (info.script) {
+ // 防止全局污染,并获得根节点
+ var fun = new Function(`${info.script.textContent}`);
+ // 绑定脚本的this为当前的影子根节点
+ fun.bind(shadow)();
+ }
+ // 添加样式
+ if (info.style) {
+ shadow.appendChild(info.style);
+ }
+ }
+}
+window.customElements.define("c-component", CustomComponent);
+
+// <c-router></c-router>
+class CustomRouter extends HTMLElement {
+ #routes
+ connectedCallback() {
+ const routeNodes = this.querySelectorAll("c-route");
+
+ // 获取子节点的路由信息
+ this.#routes = Array.from(routeNodes).map(node => node.getData());
+ // 查找默认的路由
+ const defaultRoute = this.#routes.find(r => r.default) || this.#routes[0];
+ // 渲染对应的路由
+ this.#onRenderRoute(defaultRoute);
+ // 监听路由变化
+ this.#listenerHistory();
+ }
+
+ // 渲染路由对应的内容
+ #onRenderRoute(route) {
+ var el = document.createElement("c-component");
+ el.setAttribute("path", `/${route.component}`);
+ el.id = "_route_";
+ this.append(el);
+ }
+
+ // 卸载路由清理工作
+ #onUploadRoute(route) {
+ this.removeChild(this.querySelector("#_route_"));
+ }
+
+ // 监听路由变化
+ #listenerHistory() {
+ // 导航的路由切换
+ window.addEventListener("popstate", ev => {
+ console.log("onpopstate:", ev);
+ const url = location.pathname.endsWith(".html") ? "/" : location.pathname;
+ const route = this.#getRoute(this.#routes, url);
+ console.log(route);
+ this.#onUploadRoute();
+ this.#onRenderRoute(route);
+ });
+ // pushStat或replaceSate
+ window.addEventListener("c-popstate", ev => {
+ console.log("c-popstate:", ev);
+ const detail = ev.detail;
+ const route = this.#getRoute(this.#routes, detail.url);
+ this.#onUploadRoute();
+ this.#onRenderRoute(route);
+ })
+ }
+
+ // 路由查找
+ #getRoute(routes, url) {
+ console.log(routes,url);
+ return routes.find(function (r) {
+ const path = r.path;
+ const strPaths = path.split('/');
+ const strUrlPaths = url.split("/");
+ //注意这里有点关键!!!
+ let match = true;
+ for (let i = 0; i < strPaths.length; i++) {
+ if (strPaths[i].startsWith(":")) {
+ continue;
+ }
+ match = strPaths[i] === strUrlPaths[i];
+ if (!match) {
+ break;
+ }
+ }
+ return match;
+ })
+ }
+}
+window.customElements.define("c-router", CustomRouter);
+
+// 动态加载组件并解析
+async function loadComponent(path) {
+ const defaultPath="http://localhost:5000"
+ this.caches = this.caches || {};
+ // 缓存存在,直接返回
+ if (this.caches[path]) {
+ return this.caches[path];
+ }
+ console.log(path);
+ const res = await fetch(defaultPath+path).then(res => res.text());
+ console.log(res);
+ // 利用DOMParser校验
+ // DOMParser 可以将存储在字符串中的 XML 或 HTML 源代码解析为一个 DOM Document。
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(res, "text/html");
+ // 解析模板,脚本,样式
+ const template = doc.querySelector("template");
+ const script = doc.querySelector("script");
+ const style = doc.querySelector("style");
+ // 缓存内容
+ this.caches[path] = {
+ template,
+ script,
+ style
+ }
+ return this.caches[path];
+}
+
pages文件夹中的页面文件,模仿远程的文件
about.html
<template>
+ About Me!
+</template>
+
detail.html
<template>
+ <div>商品详情</div>
+ <div id="detail">
+ 商品ID:<span id="product-id" class="product-id"></span>
+ </div>
+</template>
+
+<script>
+ this.querySelector("#product-id").textContent=history.state.id;
+</script>
+
+<style>
+ .product-id{
+ color:red;
+ }
+</style>
+
home.html
<template>
+ <div>商品清单</div>
+ <div id="product-list">
+ <div>
+ <a data-id="10" class="product-item c-link">香蕉</a>
+ </div>
+ <div>
+ <a data-id="11" class="product-item c-link">苹果</a>
+ </div>
+ <div>
+ <a data-id="12" class="product-item c-link">葡萄</a>
+ </div>
+ </div>
+</template>
+
+<script>
+ let container = this.querySelector("#product-list");
+ // 触发历史更新
+ // 事件代理
+ container.addEventListener("click", function (ev) {
+ console.log("item clicked");
+ if (ev.target.classList.contains("product-item")) {
+ const id = +ev.target.dataset.id;
+ history.pushState({
+ id
+ }, "", `/detail/${id}`)
+ }
+ })
+</script>
+
+<style>
+ .product-item {
+ cursor: pointer;
+ color: blue;
+ }
+</style>
+
<!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>
+ <h1>Proxy实现的双向数据绑定</h1>
+ <input type="text" id="input">
+ <p id="show"></p>
+ </div>
+ <script>
+ let obj={}
+ const input=document.getElementById('input')
+ const show=document.getElementById('show')
+ // 设置代理
+ let newObj=new Proxy(obj,{
+ get(target,key){
+ return Reflect.get(target,key)
+ },
+ set(target,key,value){
+ if(key==='text'){
+ input.value=value
+ show.innerHTML=value//这不实现了双向绑定
+ }
+ return Reflect.set(target,key,value)
+ }
+ })
+
+ input.addEventListener('keyup',function(e){//'input'
+ newObj.text=e.target.value
+ })
+ </script>
+</body>
+</html>
+
<!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>Object.defineProperty方法</title>
+</head>
+<body>
+ <script>
+ let number=18
+ let person={
+ name:'张三',
+ sex:'男',
+ age:number
+ }
+/*
+value和 get 是同一个作用,只能同时用一个。writable和set是同一个作用,用一个。
+所以,set和get 一个阵营 ,而value和writable一个阵营,不能两个阵营同时存在
+*/
+ Object.defineProperty(person,'age',{
+ //基本配置项
+ // value:18,
+ enumerable:true,//控制属性是否可以枚举,默认值为false
+ // writable:true,//控制属性是否可以被修改,默认值为false
+ configurable:true,//控制属性是否可以被删除,默认为false
+
+ //当有人读取person的age属性时,get函数(getter)就会被调用,且返回值就是age的值
+ get:function(){
+ console.log('有人读取了age属性');
+ return number
+ },
+ //当有人修改person的age属性时,set函数(setter)就会被调用,且返回的值是更改后的值
+ set(value){
+ console.log('有人修改了age属性,且值是'+value);
+ number=value
+ }
+ })
+ console.log(person);
+ console.log(Object.keys(person));
+ console.log(String.fromCharCode(97));//OHohohohoho
+ </script>
+</body>
+</html>
+
<!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>
+ <script>
+ let obj={
+ a:1
+ }
+ let newTarget=new Proxy(obj,{
+ set(target,key,value,receiver){
+ console.log('set',target,key,value,receiver);
+ },
+ get(target,key,receiver){
+ console.log('get',target,key,receiver);
+ }
+ })
+
+ newTarget.a
+ newTarget.a=10
+ </script>
+</body>
+</html>
+
<!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>
+ Proxy和Reflect
+ <script>
+ let arr=[1,2,3,4]
+ console.log(arr[-1]);//undefined?为什么?
+
+ // 回到原来的问题
+ function createArray(arr){
+ let handle={
+ get(target,index,receiver){
+ index=Number(index)
+ if(index<0){
+ index+=target.length
+ }
+ return Reflect.get(target,index,receiver)
+ }
+ }
+ return new Proxy(arr,handle)
+ }
+
+ arr=createArray(arr)
+ console.log(arr[-1]);
+console.log("-------------------------");
+ var star={
+ name:'zhoujielun',
+ age:18,
+ phone:'13287950909'
+ }
+ //代理陷阱
+ var proxy=new Proxy(star,{
+ get:function(target,key,receiver){
+ console.log(target,key,receiver);//代理对象、代理key值、Proxy代理对象
+ if(key==='phone'){
+ return "经纪人电话:133333333333"
+ }else{
+ // return target[key]
+ return Reflect.get(target,key,receiver)//一样
+ }
+ }
+ })
+
+
+ // proxy.name
+ console.log(proxy.name);
+ console.log(proxy.age);
+ console.log(proxy.phone);//原来这里也很重要
+ </script>
+</body>
+</html>
+
——小菜鸡一个说法
其实我感觉我在这上面没有太多的话语权,我自己开始课程学算法的时候就觉的难,根本听不进去,然后又有其他事情,就学不进一点算法。但是我觉的我做的好的一个点是我上课的时候还是去了课堂上听课,知道老师讲哪里来了,知道双指针、滑动窗口、并查集、动态规划、二分是啥。后面准备面试了,才发现算法在开发岗面试经常能碰到。后面开始准备还是对咱们这个课上听到的有点印象,虽然说重新学的过程很难,主要你能坚持下来,多刷题,相信会找到学算法的门路的!
面试的算法我挂了一题(美团一面,因为算法挂了),还有一题没完全写出来但讲好了思路(腾讯二面——字符串相乘),主要是刚开始,有些地方确实薄弱
面试中的算法baseline
笔试中的算法一般是出两道到三道:第一道非常的简单,但是还是有点难,能解决的样子。让我印象深的就是恒生电子考的全是金融相关的算法题,贪心、动规,你还得了解一下金融相关的知识,比如买卖股票;哦,还有一个印象深,阿里,三道算法一个不会,第一题我本来用JS内置方法是能解决的,但超时,这个题首先就是你要回溯出各个项,然后还有遍历判断,这遍历可有门道了,不会!
先来聊聊面试,这是大家从学校走向社会的重要一步。校招和社招的面试,一般来说有2-3轮技术面试和1轮HR面试。技术面试可能现场也可能电话,HR面试有些公司还不一定有,这种情况就是三轮技术面,当然可能有的公司面试跟上面说的不太一样,但正常来说是这样的。
对于技术面试来说,基本可以这样讲:技术面试=基础知识和业务逻辑面试+算法面试。所谓基础知识和业务逻辑面试,就是对你应聘岗位进行相关知识的考察,通俗地讲就是看你有没有干这份工作的专业能力。比如你要应聘前端岗位,那js、css、html和 jQuery的一些问题肯定会问。第一步如果你过了的话,那就来到了算法面试,通常会以代码的形式考察,很少会单讲算法。
从上面的:技术面试=基础知识和业务逻辑面试+算法面试 来看,对于业务逻辑知识层面的,那没的说,你想从事这个岗位的工作,那这一部分知识是必备的。但我们可以看出算法的普遍性,这也正是算法重要的原因之一:它是一种通用的考察点,不管你应聘哪个岗位都可以进行考察;
另外考察算法的另一个非常重要的原因是:它包含了太多的逻辑思维,可以考察你思考问题的逻辑和解决问题的能力;这一点也是面试官比较看重的,因为它可以反映出你的潜力,我曾经听阿里一位资深面试官这样讲过:当一个人逻辑思维和能力不错的情况下,你还会担心专业的业务知识方面他不行或者学不会吗?”管中窥豹,算法的重要性我想大家都应该明白了。
其实想说的算法重要的原因是:它是你扎实基本功的反映之一,这些东西很大程度上会决定你未来在IT这条路上到底能走多远。 现实点说,由于现在互联网行业薪酬较高的实际情况,很多人会报班或者半路出家去学IT,其实这变相拉低了广义上程序员的门槛,似乎大家都可以通过这条路来寻求高薪。那作为想或者已经从事这个行业的我们,如果你是科班的,那再好不过了,请珍惜这个机会;如果你不是,但也想干这行,在竞争越来越激烈的今天,必须要有点硬功夫,而上面说的算法就是其中之一,当然还包括类似于数据结构、汇编、组原、计网、数学等等,如果这些学好的话,它们是和别人竞争的一项无形的资本,也就是我们说的会让你有区分度。
计算机相关专业出来的,大学四年数据结构与算法都学不好,有什么能拿出来的呢,我是这样的想法。
我的学习路径:
成果:连续的两个月刷题,自己确实在这上面学到了很多,我自己更有想法好好去学去刷算法题。面试算法基本能过了现在!
/**
+ * @param {number[]} nums
+ * @param {number} target
+ * @return {number}
+ */
+var searchInsert = function(nums, target) {
+ let left=0,right=nums.length-1
+ while(left<=right){
+ const mid=Math.floor((left+right)/2)
+ if(nums[mid]==target){
+ return mid
+ }else if(nums[mid]>target){
+ right=mid-1
+ }else{
+ left=mid+1
+ }
+ }
+ return left//left就是安插的那个点!!!
+};
+
/**
+ * @param {number[]} nums
+ * @param {number} target
+ * @return {number[]}
+ */
+var searchRange = function(nums, target) {
+ let index=search(nums,target)
+ if(index==-1)return [-1,-1]
+
+ let left=index,rigth=index
+ while(nums[left]==target || nums[rigth]==target){
+ if(nums[left]==target)left--
+ if(nums[rigth]==target)rigth++
+ }
+ return [left+1,rigth-1]
+};
+
+//又是开始进行二分查找!!!
+const search=(nums,target)=>{
+ let low=0,high=nums.length
+ while(low<=high){
+ const mid=Math.floor((low+high)/2)
+ if(nums[mid]==target){
+ return mid
+ }else if(nums[mid]>target){
+ high=mid-1
+ }else{
+ low=mid+1
+ }
+ }
+ return -1
+}
+
/**
+ * @param {number} x
+ * @return {number}
+ */
+var mySqrt = function (x) {
+ //用二分法进行求解:左闭右开区间
+ let low = 0, high = Math.ceil(x / 2)//优化方案点
+ while (low < high) {
+ const mid = Math.ceil((high + low) / 2)
+ const res = mid * mid
+ if (res == x) {
+ return mid
+ } else if (res > x) {
+ high = mid - 1
+ } else {
+ low = mid
+ }
+ }
+ return low
+};
+
/**
+ * @param {number} num
+ * @return {boolean}
+ */
+var isPerfectSquare = function(num) {
+ //4=1+3 9=1+3+5 16=1+3+5+7以此类推,模仿它可以使用一个while循环,
+ // 不断减去一个从1开始不断增大的奇数,若最终减成了0,说明是完全平方数,否则,不是。
+ let num1=1;
+ while(num>0){
+ num-=num1
+ num1+=2
+ }
+ return num==0
+};
+
/**
+ * @param {number} num
+ * @return {boolean}
+ */
+var isPerfectSquare = function (num) {
+ //用二分法来进行求解:和上面的那个题目差不多!!!
+ let low = 0, high = Math.ceil(num / 2)
+ while (low < high) {
+ const mid = Math.ceil((low + high) / 2)
+ const res = mid * mid
+ if(res==num){
+ return true
+ }else if(res<num){
+ low=mid
+ }else{
+ high=mid-1
+ }
+ }
+ return false
+};
+
/**
+ * Definition for a binary tree node.
+ * function TreeNode(val, left, right) {
+ * this.val = (val===undefined ? 0 : val)
+ * this.left = (left===undefined ? null : left)
+ * this.right = (right===undefined ? null : right)
+ * }
+ */
+/**
+ * @param {TreeNode} root
+ * @return {number[]}
+ */
+var inorderTraversal = function (root) {
+ const result = []
+ const traverse = (root) => {
+ if (root == null) return;
+ traverse(root.left)
+ result.push(root.val)
+ traverse(root.right)
+ }
+ traverse(root)
+ return result
+};
+
/**
+ * Definition for a binary tree node.
+ * function TreeNode(val, left, right) {
+ * this.val = (val===undefined ? 0 : val)
+ * this.left = (left===undefined ? null : left)
+ * this.right = (right===undefined ? null : right)
+ * }
+ */
+/**
+ * @param {TreeNode} root
+ * @return {number}
+ */
+var maxDepth = function(root) {
+ if(root==null)return 0
+ return Math.max(maxDepth(root.left),maxDepth(root.right))+1
+};
+
/**
+ * Definition for a binary tree node.
+ * function TreeNode(val, left, right) {
+ * this.val = (val===undefined ? 0 : val)
+ * this.left = (left===undefined ? null : left)
+ * this.right = (right===undefined ? null : right)
+ * }
+ */
+/**
+ * @param {TreeNode} root
+ * @return {TreeNode}
+ */
+var invertTree = function(root) {
+ //判断不存在直接返回
+ if(root==null)return root
+ //存在进行下面的处理
+ const tmp=root.left
+ root.left=root.right
+ root.right=tmp
+ //遍历
+ invertTree(root.left)
+ invertTree(root.right)
+
+ return root
+};
+
/**
+ * Definition for a binary tree node.
+ * function TreeNode(val, left, right) {
+ * this.val = (val===undefined ? 0 : val)
+ * this.left = (left===undefined ? null : left)
+ * this.right = (right===undefined ? null : right)
+ * }
+ */
+/**
+ * @param {TreeNode} root
+ * @return {boolean}
+ */
+var isSymmetric = function(root) {
+ return dfs(root.left,root.right)
+};
+
+function dfs(left,right){
+ if(left==null && right==null)return true //都没有
+ if(left==null || right==null)return false //只有一个
+ if(left.val!=right.val)return false //两者都有
+ return dfs(left.left,right.right) && dfs(left.right,right.left)
+}
+
/**
+ * Definition for a binary tree node.
+ * function TreeNode(val, left, right) {
+ * this.val = (val===undefined ? 0 : val)
+ * this.left = (left===undefined ? null : left)
+ * this.right = (right===undefined ? null : right)
+ * }
+ */
+/**
+ * @param {TreeNode} root
+ * @return {number}
+ */
+var diameterOfBinaryTree = function(root) {
+ let maxLen=0
+ // 二叉树最大深度的变种!
+ const maxline=(root)=>{
+ if(root==null)return 0
+ const left=maxline(root.left)
+ const right=maxline(root.right)
+ maxLen=Math.max(maxLen,right+left)
+ return Math.max(left,right)+1
+ }
+ maxline(root)
+ return maxLen
+};
+
/**
+ * Definition for a binary tree node.
+ * function TreeNode(val, left, right) {
+ * this.val = (val===undefined ? 0 : val)
+ * this.left = (left===undefined ? null : left)
+ * this.right = (right===undefined ? null : right)
+ * }
+ */
+/**
+ * @param {TreeNode} root
+ * @return {number[][]}
+ */
+var levelOrder = function (root) {
+ const queue = [root], res = [] //是类似队列的操作
+ if(root==null)return res// 注意这里还有一个条件判断
+
+ while (queue.length) {
+ const len = queue.length
+ const arr = []
+ for (let i = 0; i < len; i++) {
+ const node = queue.shift()
+ arr.push(node.val)
+ if (node.left) {
+ queue.push(node.left)
+ }
+ if (node.right) {
+ queue.push(node.right)
+ }
+ }
+ res.push(arr)
+ }
+ return res
+};
+
/**
+ * Definition for a binary tree node.
+ * function TreeNode(val, left, right) {
+ * this.val = (val===undefined ? 0 : val)
+ * this.left = (left===undefined ? null : left)
+ * this.right = (right===undefined ? null : right)
+ * }
+ */
+/**
+ * @param {number[]} nums
+ * @return {TreeNode}
+ */
+var sortedArrayToBST = function (nums) {
+ // 注意这个函数带上的两个参数
+ function buildTree(low, high) {
+ if (low > high) return null//注意这里的终止条件
+ // 下面进行树的生成
+ const mid = Math.floor((low + high) / 2)
+ const root = new TreeNode(nums[mid])
+ root.left = buildTree(low, mid - 1)
+ root.right = buildTree(mid + 1, high)
+ return root
+ }
+ return buildTree(0, nums.length - 1)
+};
+
/**
+ * Definition for a binary tree node.
+ * function TreeNode(val, left, right) {
+ * this.val = (val===undefined ? 0 : val)
+ * this.left = (left===undefined ? null : left)
+ * this.right = (right===undefined ? null : right)
+ * }
+ */
+/**
+ * @param {TreeNode} root
+ * @return {boolean}
+ */
+var isValidBST = function(root) {
+ //中序遍历求解!!!
+ // 二叉搜索树「中序遍历」得到的值构成的序列一定是升序的,
+ // 这启示我们在中序遍历的时候实时检查当前节点的值是否大于前一个中序遍历到的节点的值即可。
+ let stack = [];
+ let inorder = -Infinity;
+
+ while (stack.length || root !== null) {
+ // 入栈节点
+ while (root !== null) {
+ stack.push(root);
+ root = root.left;
+ }
+ root = stack.pop();
+ // 如果中序遍历得到的节点的值小于等于前一个 inorder,说明不是二叉搜索树
+ if (root.val <= inorder) {
+ return false;
+ }
+ inorder = root.val;
+ root = root.right;
+ }
+ return true;
+};
+
Error! 没有考虑子树的所有节点都必须大于或小于根节点
例如:[5,4,6,null,null,3,7],这样的做法只是考虑在两层间的对比!
var isValidBST = function (root) {
+ if (root == null) return true
+ if ((root.left != null && root.val <= root.left.val)
+ || (root.right != null && root.right.val <= root.val)) {
+ return false
+ }
+ return isValidBST(root.left) && isValidBST(root.right)
+};
+
另一种解题思路:先遍历收集在进行对比
/**
+ * Definition for a binary tree node.
+ * function TreeNode(val, left, right) {
+ * this.val = (val===undefined ? 0 : val)
+ * this.left = (left===undefined ? null : left)
+ * this.right = (right===undefined ? null : right)
+ * }
+ */
+/**
+ * @param {TreeNode} root
+ * @return {boolean}
+ */
+var isValidBST = function (root) {
+ //直接可以遍历出来再进行对比
+ const res = []
+ const travel = (root) => {
+ if (root == null) return
+ travel(root.left)
+ res.push(root.val)
+ travel(root.right)
+ }
+ travel(root)
+ let flag = true;
+ for (let i = 1; i < res.length; i++) {
+ if (res[i - 1] >= res[i]) flag = false
+ }
+ return flag
+};
+
/**
+ * Definition for a binary tree node.
+ * function TreeNode(val, left, right) {
+ * this.val = (val===undefined ? 0 : val)
+ * this.left = (left===undefined ? null : left)
+ * this.right = (right===undefined ? null : right)
+ * }
+ */
+/**
+ * @param {TreeNode} root
+ * @param {number} k
+ * @return {number}
+ */
+var kthSmallest = function (root, k) {
+ //中序遍历找第k个元素
+ let i = 0, value
+ const travel = (root) => {
+ if (root == null) return
+ travel(root.left)
+ if (++i == k) {
+ value = root.val;
+ return
+ }
+ travel(root.right)
+ }
+ travel(root)
+ return value
+};
+
/**
+ * Definition for a binary tree node.
+ * function TreeNode(val, left, right) {
+ * this.val = (val===undefined ? 0 : val)
+ * this.left = (left===undefined ? null : left)
+ * this.right = (right===undefined ? null : right)
+ * }
+ */
+/**
+ * @param {TreeNode} root
+ * @return {number[]}
+ */
+var rightSideView = function (root) {
+ return levelOrder(root)
+};
+
+
+function levelOrder(root) {
+ let res = [], stack = [root]
+ if (root == null) return []
+
+ while (stack.length) {
+ let arr = []
+ let len = stack.length
+ for (let i = 0; i < len; i++) {
+ const node = stack.shift()
+ arr.push(node.val)
+ if (node.left) {
+ stack.push(node.left)
+ }
+ if (node.right) {
+ stack.push(node.right)
+ }
+ }
+ //这里直接push最后一个就行了!!!
+ res.push(arr[arr.length-1])
+ }
+ return res
+}
+
/**
+ * Definition for a binary tree node.
+ * function TreeNode(val, left, right) {
+ * this.val = (val===undefined ? 0 : val)
+ * this.left = (left===undefined ? null : left)
+ * this.right = (right===undefined ? null : right)
+ * }
+ */
+/**
+ * @param {TreeNode} root
+ * @return {number[][]}
+ */
+var zigzagLevelOrder = function (root) {
+ if(root==null)return []//注意这里的一个条件判断!!!
+
+ const res = [], queue = [root]
+ let order = true
+ while (queue.length) {
+ let arr = []
+ let len = queue.length
+ for (let i = 0; i < len; i++) {
+ const node = queue.shift()
+ if (order) {
+ arr.push(node.val)
+ } else {
+ arr.unshift(node.val)
+ }
+ if (node.left) {
+ queue.push(node.left)
+ }
+ if (node.right) {
+ queue.push(node.right)
+ }
+ }
+ res.push(arr)
+ order = !order
+ }
+ return res
+};
+
/**
+ * Definition for a binary tree node.
+ * function TreeNode(val, left, right) {
+ * this.val = (val===undefined ? 0 : val)
+ * this.left = (left===undefined ? null : left)
+ * this.right = (right===undefined ? null : right)
+ * }
+ */
+/**
+ * @param {TreeNode} root
+ * @return {void} Do not return anything, modify root in-place instead.
+ */
+var flatten = function(root) {
+ let list=[]
+ travel(root,list)
+ for(let i=1;i<list.length;i++){
+ const prev=list[i-1],cur=list[i]
+ prev.left=null
+ prev.right=cur
+ }
+};
+
+function travel(root,list){
+ //先序遍历进行收集!!!
+ if(root==null)return
+ list.push(root)
+ travel(root.left,list)
+ travel(root.right,list)
+}
+
/**
+ * Definition for a binary tree node.
+ * function TreeNode(val, left, right) {
+ * this.val = (val===undefined ? 0 : val)
+ * this.left = (left===undefined ? null : left)
+ * this.right = (right===undefined ? null : right)
+ * }
+ */
+/**
+ * @param {number[]} preorder
+ * @param {number[]} inorder
+ * @return {TreeNode}
+ */
+// 优化方案
+var buildTree = function (preorder, inorder) {
+ const helper = (p_start, p_end, i_start, i_end) => {
+ if (p_start > p_end) return null
+ const rootVal = preorder[p_start]//根节点的值
+ const root = new TreeNode(rootVal)//根节点
+ const mid = inorder.indexOf(rootVal)//根节点在
+ let leftNum = mid - i_start //左子树的节点数
+ root.left = helper(p_start + 1, p_start + leftNum, i_start, mid - 1)
+ root.right = helper(p_start + leftNum + 1, p_end, mid + 1, i_end)
+ return root
+ }
+ return helper(0, preorder.length - 1, 0, inorder.length - 1)
+};
+
+
+//第一种写法
+function buildTree(preorder, inorder) {
+ if (preorder.length == 0 || inorder.length == 0) return null
+ const root = new TreeNode(preorder[0])
+ const mid = inorder.indexOf(preorder[0])
+ root.left = buildTree(preorder.slice(1, mid + 1), inorder.slice(0, mid))
+ root.right = buildTree(preorder.slice(mid + 1), inorder.slice(mid + 1))
+ return root
+}
+
/**
+ * Definition for a binary tree node.
+ * function TreeNode(val, left, right) {
+ * this.val = (val===undefined ? 0 : val)
+ * this.left = (left===undefined ? null : left)
+ * this.right = (right===undefined ? null : right)
+ * }
+ */
+/**
+ * @param {TreeNode} root
+ * @param {number} targetSum
+ * @return {number}
+ */
+var pathSum = function(root, targetSum) {
+ let ans=0
+ const map=new Map()
+ dfs(root,0)
+ return ans
+// 前缀和定义
+// 用它干什么
+// HashMap存的是什么
+// 恢复状态代码的意义:题目中可以拿 node 值为5的节点来说
+
+ function dfs(root,preSum){
+ if(root==null)return
+ let target=preSum+root.val
+ map.set(preSum,(map.get(preSum)||0)+1)
+ ans+=(map.get(target-targetSum)||0)
+
+ dfs(root.left,target)
+ dfs(root.right,target)
+
+ map.set(preSum,map.get(preSum)-1)
+ }
+};
+
/**
+ * Definition for a binary tree node.
+ * function TreeNode(val) {
+ * this.val = val;
+ * this.left = this.right = null;
+ * }
+ */
+/**
+ * @param {TreeNode} root
+ * @param {TreeNode} p
+ * @param {TreeNode} q
+ * @return {TreeNode}
+ */
+var lowestCommonAncestor = function(root, p, q) {
+
+ const travel=(root,p,q)=>{
+ if(root==null ||root==p ||root==q)return root
+ let left=travel(root.left,p,q)
+ let right=travel(root.right,p,q)
+
+ // 后续遍历中进行处理!需要进行往上返回!
+ if(left!=null &&right!=null)return root
+ if(left==null)return right
+ if(right==null)return left
+
+ }
+
+ return travel(root,p,q)
+};
+
/**
+ * Definition for a binary tree node.
+ * function TreeNode(val, left, right) {
+ * this.val = (val===undefined ? 0 : val)
+ * this.left = (left===undefined ? null : left)
+ * this.right = (right===undefined ? null : right)
+ * }
+ */
+/**
+ * @param {TreeNode} root
+ * @return {number}
+ */
+const maxPathSum = (root) => {
+ let maxSum = Number.MIN_SAFE_INTEGER; // 最大路径和
+
+ const dfs = (root) => {
+ if (root == null) { // 遍历到null节点,收益0
+ return 0;
+ }
+ const left = dfs(root.left); // 左子树提供的最大路径和
+ const right = dfs(root.right); // 右子树提供的最大路径和
+
+ const innerMaxSum = left + root.val + right; // 当前子树内部的最大路径和
+ maxSum = Math.max(maxSum, innerMaxSum); // 挑战最大纪录
+
+ const outputMaxSum = root.val + Math.max( left, right); // 当前子树对外提供的最大和
+
+ // 如果对外提供的路径和为负,直接返回0。否则正常返回
+ return outputMaxSum < 0 ? 0 : outputMaxSum;
+ };
+
+ dfs(root); // 递归的入口
+
+ return maxSum;
+};
+
/**
+ * Definition for a binary tree node.
+ * function TreeNode(val, left, right) {
+ * this.val = (val===undefined ? 0 : val)
+ * this.left = (left===undefined ? null : left)
+ * this.right = (right===undefined ? null : right)
+ * }
+ */
+/**
+ * @param {TreeNode} p
+ * @param {TreeNode} q
+ * @return {boolean}
+ */
+//如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。
+var isSameTree = function (p, q) {
+ if (p == null && q == null) return true
+ if (p == null || q == null) return false
+ if (p.val != q.val) return false
+ return isSameTree(p.left, q.left) && isSameTree(p.right, q.right)
+};
+
/**
+ * @param {number} n
+ * @return {number}
+ */
+var climbStairs = function (n) {
+ const dp=[1,2]
+ for(let i=2;i<n;i++){
+ dp[i]=dp[i-1]+dp[i-2]
+ }
+ return dp[n-1]
+};
+
/**
+ * @param {number} numRows
+ * @return {number[][]}
+ */
+var generate = function (numRows) {
+ // 打印输出一个杨辉三角
+ const dp = Array.from({ length: numRows }, () => new Array(numRows).fill(1))
+ for (let i = 0; i < numRows; i++) {
+ for (let j = 0; j < numRows; j++) {
+ if (j > 0 && j < i) {
+ dp[i][j] = dp[i - 1][j] + dp[i - 1][j - 1]
+ }
+ }
+ }
+ //进行push进去
+ let res = []
+ for (let i = 0; i < numRows; i++) {
+ res.push(dp[i].slice(0, i + 1))
+ }
+ return res
+};
+
/**
+ * @param {number[]} nums
+ * @return {number}
+ */
+var rob = function (nums) {
+ //如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
+ let len = nums.length
+ if (len <= 2) return Math.max.apply(null, nums)
+ let max = nums[0];
+ let M = nums[0]
+ let dp = [nums[0], nums[1]]
+ if (max < dp[1]) max = dp[1]
+ for (let i = 2; i < len; i++) {
+ //这个M表示间隔两位以上的最大数
+ if (dp[i - 2] > M) M = dp[i - 2]
+ dp[i] = M + nums[i]
+ if (max < dp[i]) max = dp[i]
+ }
+ return max
+};
+
/**
+ * @param {number} n
+ * @return {number}
+ */
+var numSquares = function (n) {
+ //用动态规划求解
+ let dp = new Array(n + 1).fill(0)//其实主要是初始化dp[0]
+ //每一个对应的位置最大可以是本身1+1+1……
+ for (let i = 1; i <= n; i++) {
+ dp[i] = i;//每次都将当前数字先更新为最大的结果,最坏的结果
+ //这里的j是平方数的底子
+ for (let j = 1; i - j * j >= 0; j++) {
+ dp[i] = Math.min(dp[i], dp[i - j * j] + 1)
+ }
+ }
+ return dp[n]
+};
+
/**
+ * @param {number[]} coins
+ * @param {number} amount
+ * @return {number}
+ */
+var coinChange = function(coins, amount) {
+ // 定义dp数组
+ let dp=new Array(amount+1).fill(Infinity)
+ dp[0]=0
+ //注意两层for循环的遍历,分别遍历的是啥?
+ for(let i=0;i<coins.length;i++){
+ for(let j=coins[i];j<=amount;j++){
+ dp[j]=Math.min(dp[j],dp[j-coins[i]]+1)
+ }
+ }
+ if(dp[amount]==Infinity)return -1
+ return dp[amount]
+};
+
/**
+ * @param {number[]} nums
+ * @return {number}
+ */
+var lengthOfLIS = function(nums) {
+ let n=nums.length;
+ let dp=new Array(n).fill(1)
+ for(let i=1;i<n;i++){
+ for(let j=0;j<i;j++){
+ if(nums[i]>nums[j]){
+ dp[i]=Math.max(dp[i],dp[j]+1)
+ }
+ }
+ }
+ return Math.max.apply(null,dp)
+};
+
/**
+ * @param {number[]} nums
+ * @return {boolean}
+ */
+var canPartition = function(nums) {
+ // 显然是0-1背包问题
+ let n=nums.length
+ let target=nums.reduce((p,v)=>p+v,0)/2
+ if(!Number.isInteger(target))return false
+ let dp=new Array(target+1).fill(0)
+ for(let i=0;i<n;i++){
+ for(let j=target;j>=nums[i];j--){
+ dp[j]=Math.max(dp[j],dp[j-nums[i]]+nums[i])
+ }
+ }
+ return dp[target]==target
+};
+
/**
+ * @param {number} m
+ * @param {number} n
+ * @return {number}
+ */
+var uniquePaths = function(m, n) {
+ let dp=Array.from({length:m},()=>new Array(n).fill(1))
+ for(let i=1;i<m;i++){
+ for(let j=1;j<n;j++){
+ dp[i][j]=dp[i][j-1]+dp[i-1][j]
+ }
+ }
+ return dp[m-1][n-1]
+};
+
/**
+ * @param {number[][]} grid
+ * @return {number}
+ */
+var minPathSum = function(grid) {
+ let m=grid.length
+ let n=grid[0].length
+ let dp=Array.from({length:m},()=>new Array(n))
+ dp[0][0]=grid[0][0]
+ for(let i=0;i<m;i++){
+ for(let j=0;j<n;j++){
+ if(i==0 && j>0){
+ dp[i][j]=dp[i][j-1]+grid[i][j]
+ }
+ if(j==0 && i>0){
+ dp[i][j]=dp[i-1][j]+grid[i][j]
+ }
+ if(j>0 && i>0){
+ dp[i][j]=Math.min(dp[i-1][j],dp[i][j-1])+grid[i][j]
+ }
+ }
+ }
+ return dp[m-1][n-1]
+};
+
/**
+ * @param {number[]} nums
+ * @param {number} val
+ * @return {number}
+ */
+var removeElement = function(nums, val) {
+ let slow=0
+ for(let fast=0;fast<nums.length;fast++){
+ if(nums[fast]!=val){
+ nums[slow]=nums[fast]
+ slow++
+ }
+ }
+ return slow
+};
+
/**
+ * @param {number[]} nums
+ * @return {number}
+ */
+var removeDuplicates = function(nums) {
+ let slow=0
+ for(let fast=0;fast<nums.length;fast++){
+ if(nums[fast]!=nums[slow]){
+ slow++
+ nums[slow]=nums[fast]
+ }
+ }
+ return slow+1
+};
+
/**
+ * @param {number[]} nums
+ * @return {void} Do not return anything, modify nums in-place instead.
+ */
+var moveZeroes = function(nums) {
+ let slow=0;
+ for(let fast=0;fast<nums.length;fast++){
+ if(nums[fast]!=0){
+ nums[slow]=nums[fast]
+ slow++
+ }
+ }
+ for(let i=slow;i<nums.length;i++){
+ nums[i]=0
+ }
+};
+
/**
+ * @param {number} target
+ * @param {number[]} nums
+ * @return {number}
+ */
+var minSubArrayLen = function (target, nums) {
+ //用滑动窗口进行求解
+ let start = 0, end = 0;
+ const n = nums.length;
+ let sum = 0, ans = n + 1;//这个尽量设一个较大的值
+ while (end < n) {
+ sum += nums[end]
+ end++
+ while (sum >= target) {
+ ans = Math.min(ans, end - start)
+ sum -= nums[start]
+ start++
+ }
+ }
+ return ans == n + 1 ? 0 : ans
+};
+
/**
+ * @param {number[]} fruits
+ * @return {number}
+ */
+var totalFruit = function (fruits) {
+ if (fruits.length <= 2) return fruits.length
+ //用滑动窗口+哈希来求解
+ const map = new Map()
+ let right = 0, left = 0//left来进行标记
+ let max = -Infinity
+ while (right < fruits.length) {
+ const type = fruits[right]
+ right++
+ //注意这个位置求值后下面结尾也要注意一下!!!
+ const it = Array.from(map.values())
+ max = Math.max(max, it.reduce((p, v) => p + v, 0))
+
+ map.set(type, (map.get(type) || 0) + 1)
+
+
+ while (map.size > 2) {
+ const ty = fruits[left]
+ left++
+ map.set(ty, map.get(ty) - 1)
+
+ if (map.get(ty) == 0) {
+ map.delete(ty)
+ }
+ }
+
+ }
+ const it = Array.from(map.values())
+ max = Math.max(max, it.reduce((p, v) => p + v, 0))
+
+ return max == -Infinity ? fruits.length : max
+};
+
/**
+ * @param {number[]} height
+ * @return {number}
+ */
+var maxArea = function(height) {
+ let maxA=0
+ let left=0,right=height.length-1
+ while(left<right){
+ let area=Math.min(height[left],height[right])*(right-left)
+ maxA=Math.max(maxA,area)
+ if(height[left]<height[right]){
+ left++
+ }else{
+ right--
+ }
+ }
+ return maxA
+};
+
/**
+ * @param {number[]} nums
+ * @return {number[][]}
+ */
+var threeSum = function(nums) {
+ //这里有三个去重的点子
+ const len=nums.length;
+ const res=[];
+ nums.sort((a,b)=>a-b)
+ for(let i=0;i<len-2;i++){
+ //第一点优化:但凡开始记录,后面的三数和绝对大于0
+ if(nums[i]>0)break;
+ //第二点优化:跳过重复的点
+ if(i>0 && nums[i]==nums[i-1])continue;
+ let L=i+1,R=len-1
+ while(L<R){
+ const target=nums[i]+nums[L]+nums[R];
+ if(target==0){
+ res.push([nums[i],nums[L],nums[R]])
+ //第三层优化:
+ while(L<R && nums[L]==nums[L+1])L++
+ while(L<R && nums[R]==nums[R+1])R--
+ L++
+ R--
+ }else if(target<0){
+ L++
+ }else{
+ R--
+ }
+ }
+ }
+ return res
+};
+
这个写得差点时间超限:
/**
+ * @param {number[]} nums
+ * @return {number[][]}
+ */
+var threeSum = function(nums) {
+ nums.sort((a,b)=>a-b)
+ const res=[]
+ for(let i=1;i<nums.length;i++){
+ const first=nums[i-1];
+ let left=i,right=nums.length-1
+ while(left<right){
+ const target=nums[left]+nums[right]+first;
+ if(target==0){
+ res.push([first,nums[left],nums[right]]+"")
+ left++
+ right--
+ }
+ else if(target<0){
+ left++
+ }else{
+ right--
+ }
+ }
+ }
+ let result=[...new Set(res)].map(str=>str.split(",").map(s=>Number(s)))
+ return result
+};
+
/**
+ * @param {number[]} height
+ * @return {number}
+ */
+var trap = function (height) {
+ //利用双指针方法进行求解!!!这个方法比较简单!!!
+ let ans = 0;
+ let left = 0, right = height.length - 1;
+ let leftMax = 0, rightMax = 0
+ while (left < right) {
+ leftMax = Math.max(leftMax, height[left])
+ rightMax = Math.max(rightMax, height[right])
+ if (height[left] < height[right]) {
+ ans += leftMax - height[left]
+ left++
+ } else {
+ ans += rightMax - height[right]
+ right--
+ }
+ }
+ return ans
+};
+
/**
+ * @param {string} s
+ * @return {number}
+ */
+var lengthOfLongestSubstring = function (s) {
+ let res = 0;
+ let left = 0, right = 0;
+ let window = {}
+ while (right < s.length) {
+ const c = s[right]
+ right++;
+ window[c] = (window[c] || 0) + 1
+
+ while (window[c] > 1) {
+ const d = s[left];
+ left++;
+ window[d]--
+ }
+ res = Math.max(res, right - left)
+ }
+ return res
+};
+
前面用了一个JS方法来求解的
/**
+ * @param {string} s
+ * @return {number}
+ */
+var lengthOfLongestSubstring = function (s) {
+ //滑动窗口问题
+ let max = 0;
+ let slow = 0, fast = 0;
+ while (fast < s.length) {
+ if (!s.slice(slow, fast).includes(s[fast])) {
+ fast++
+ } else {
+ slow++
+ }
+ max = Math.max(fast - slow, max)
+ }
+ return max
+};
+
/**
+ * @param {string} s
+ * @param {string} t
+ * @return {string}
+ */
+var minWindow = function (s, t) {
+ // 哈希表 need 记录需要匹配的字符及对应的出现次数
+ // 哈希表 window 记录窗口中满足 need 条件的字符及其出现次数
+ let need = new Map();
+ let window = new Map();
+ //先将need填充好
+ for (let i = 0; i < t.length; i++) {
+ if (need.has(t[i])) {
+ need.set(t[i], need.get(t[i]) + 1)
+ } else {
+ need.set(t[i], 1)
+ }
+ }
+
+ let left = 0, right = 0;
+ let valid = 0;
+ // 记录最小覆盖子串的起始索引及长度
+ let start = 0, len = Infinity;
+ while (right < s.length) {
+ // c 是将移入窗口的字符
+ const c = s[right]
+ // 扩大窗口
+ right++
+ // 进行窗口内数据的一系列更新
+ if (need.has(c)) {
+ if (window.has(c)) {
+ window.set(c, window.get(c) + 1)
+ } else {
+ window.set(c, 1)
+ }
+ if (window.get(c) === need.get(c)) {
+ valid++
+ }
+ }
+ // 判断左侧窗口是否要收缩
+ while (valid === need.size) {
+ // 在这里更新最小覆盖子串
+ if (right - left < len) {
+ start = left
+ len = right - left
+ }
+ // d 是将移出窗口的字符
+ const d = s[left]
+ // 缩小窗口
+ left++
+ // 进行窗口内数据的一系列更新
+ if(need.has(d)){
+ if(window.get(d)==need.get(d)){
+ valid--
+ }
+ window.set(d,window.get(d)-1)
+ }
+ }
+ }
+ // 返回最小覆盖子串
+ return len==Infinity ?'':s.substr(start,len)
+};
+
/**
+ * @param {string} s
+ * @param {string} p
+ * @return {number[]}
+ */
+var findAnagrams = function (s, p) {
+ // 定义需求和窗口
+ const need = new Map()
+ const window = new Map()
+ // 收集需求
+ for (const c of p) {
+ need.set(c, (need.get(c) || 0) + 1);
+ }
+ let left = 0, right = 0, ans = [];//存储结果
+ let valid = 0;//统计是否达到需求数
+ while (right < s.length) {
+ const c = s[right]
+ right++;
+ // 对窗口内数据进行更新
+ if (need.has(c)) {
+ window.set(c, (window.get(c) || 0) + 1)
+ if (need.get(c) === window.get(c)) {
+ valid++
+ }
+ }
+ // 判断左侧窗口是否需要收缩
+ while (right - left == p.length) {
+ // 符合窗口条件,把索引值加入结果数组
+ if (valid === need.size) {
+ ans.push(left)
+ }
+ const d = s[left]
+ left++;
+ // 条件成立进行窗口收缩!
+ if (need.has(d)) {
+ if (window.get(d) === need.get(d)) {
+ valid--
+ }
+ window.set(d, window.get(d) - 1)
+ }
+ }
+
+ }
+ return ans
+};
+
/**
+ * @param {string} s1
+ * @param {string} s2
+ * @return {boolean}
+ */
+var checkInclusion = function(s1, s2) {
+ let need=new Map()
+ let window=new Map()
+ //先进行need的收集!!!
+ for(const c of s1){
+ need.set(c,(need.get(c)||0)+1)
+ }
+ let left=0,right=0
+ let valid=0
+ while(right<s2.length){
+ const c=s2[right]//目前探寻的是最右边的填充
+ right++
+ //进行窗口的一系列更新
+ if(need.has(c)){
+ window.set(c,(window.get(c)||0)+1)
+ if(window.get(c)==need.get(c)){
+ valid++
+ }
+ }
+ //判断窗口是否需要进行缩放
+ while(right-left>=s1.length){
+ //找到了合法的子串
+ if(valid==need.size){
+ return true
+ }
+ const d=s2[left]
+ left++
+ //进行窗口的一系列更新
+ if(need.has(d)){
+ if(window.get(d)==need.get(d)){
+ valid--
+ }
+ window.set(d,window.get(d)-1)
+ }
+ }
+ }
+ return false
+
+};
+
/**
+ * @param {number[]} nums
+ * @param {number} k
+ * @return {number[]}
+ */
+var maxSlidingWindow = function(nums, k) {
+ const n = nums.length;
+ const q = [];
+ for (let i = 0; i < k; i++) {
+ while (q.length && nums[i] >= nums[q[q.length - 1]]) {
+ q.pop();
+ }
+ q.push(i);
+ }
+
+ const ans = [nums[q[0]]];
+ for (let i = k; i < n; i++) {
+ while (q.length && nums[i] >= nums[q[q.length - 1]]) {
+ q.pop();
+ }
+ q.push(i);
+ while (q[0] <= i - k) {
+ q.shift();
+ }
+ ans.push(nums[q[0]]);
+ }
+ return ans;
+};
+
class Stack {
+ constructor() {
+ this.stack = [];
+ }
+ pop() {
+ return this.stack.pop();
+ }
+ push(item) {
+ this.stack.push(item);
+ }
+ peek() {
+ return this.stack[this.getCount() - 1];
+ }
+ getCount() {
+ return this.stack.length;
+ }
+ isEmpty() {
+ return this.getCount() === 0;
+ }
+}
+//栈的几种操作:出栈、入栈、栈顶、栈是否为空、栈的大小
+
export default class Queue{
+ constructor(){
+ this.queue=[]
+ }
+ enQueue(item){
+ this.queue.push(item)
+ }
+ deQueue(){
+ this.queue.shift()
+ }
+ getHeader(){
+ return this.queue[0]
+ }
+ getLength(){
+ return this.queue.length
+ }
+ isEmpty(){
+ return this.getLength()===0
+ }
+}
+
class SqQueue {
+ constructor(length) {
+ this.queue = new Array(length + 1); //预留空位
+ //队头
+ this.first = 0;
+ //队尾
+ this.last = 0;
+ //当前队列的大小
+ this.size = 0;
+ }
+ enQueue(item) {
+ //判断队尾 + 1 是否为队头
+ //如果是就代表需要扩容数组(下面的一个判断条件是队列已满)
+ // % this.queue.length 是为了防止数组越界
+ if (this.isFull()) {
+ this.resize(this.getLength() * 2 + 1);
+ }
+ this.queue[this.last] = item;
+ this.size++;
+ this.last = (this.last + 1) % this.queue.length;
+ }
+ deQueue() {
+ let r = this.getHeader();
+ this.queue[this.first] = null;
+ this.first = (this.first + 1) % this.queue.length;
+ this.size--;
+ //判断当前队列大小是否过小
+ //为了保证不浪费空间,在队列空间等于总长度的四分之一时 且不为2时缩小总长度为当前的一半
+ if (this.size <= this.getLength() / 4 && this.getLength() % 2 === 0) {
+ this.resize(this.getLength() / 2 + 1);
+ }
+ return r;
+ }
+ getHeader() {
+ if (this.isEmpty()) {
+ throw Error("Queue is empty");
+ }
+ return this.queue[this.first];
+ }
+ getLength() {
+ return this.queue.length - 1;
+ }
+ isEmpty() {
+ return this.first === this.last;
+ }
+ isFull() {
+ return this.first === (this.last + 1) % this.queue.length;
+ }
+ resize(length) {
+ let q = new Array(length);
+ for (let i = 0; i < length; i++) {
+ q[i] = this.queue[(i + this.first) % this.queue.length];
+ }
+ this.queue = q;
+ this.first = 0;
+ this.last = this.size;
+ }
+}
+
class Node{
+ constructor(value,next=null) {
+ this.value=value
+ this.next=next
+ }
+}
+
+class LinkedList{
+ constructor(value) {
+ this.head=new Node(value)
+ }
+ //查找节点
+ findNode(value){
+ let currentNode=this.head
+ while(currentNode.value !==value && currentNode!=null){
+ currentNode=currentNode.next
+ }
+ return currentNode;
+ }
+ //指定位置插入节点
+ insertAfter(value,newValue){
+ const newNode=new Node(newValue)
+ const currentNode=this.findNode(value)
+
+ newNode.next=currentNode.next
+ currentNode.next=newNode
+ }
+ //在尾部插入节点
+ append(value){
+ const newNode=new Node(value)
+ let currentNode=this.head;
+ while(currentNode.next){
+ currentNode=currentNode.next
+ }
+ currentNode.next=newNode
+ }
+ //在头部插入节点
+ prepend(value){
+ const newNode=new Node(value)
+ newNode.next=this.head
+ this.head=newNode
+ }
+ //删除指定节点
+ remove(value){
+ let currentNode=this.head;
+ let previousNode=null;
+
+ while(currentNode.value!=value){
+ previousNode=currentNode;
+ currentNode=currentNode.next
+ }
+ if(currentNode===this.head){
+ this.head=currentNode.next
+ }else{
+ previousNode.next=currentNode.next
+ }
+ }
+ //删除头部节点
+ removeHead(){
+ this.head=this.head.next
+ }
+ //删除尾部节点
+ removeTail(){
+ let currentNode=this.head;
+ let previousNode=null
+ while(currentNode.next){
+ previousNode=currentNode
+ currentNode=currentNode.next
+ }
+ previousNode.next=null
+ }
+ //遍历链表节点
+ traverse(){
+ let currentNode=this.head
+ while(currentNode){
+ console.log(currentNode.value);
+ currentNode=currentNode.next
+ }
+ }
+
+}
+
+
+//操作实例
+let list=new LinkedList(1)
+list.append(2)
+list.append(3)
+list.append(4)
+
+list.insertAfter(2,5)
+list.prepend(6)
+list.remove(3)
+list.removeHead()
+list.removeTail()
+
class MaxHeap {
+ constructor() {
+ this.heap = [];
+ }
+ size() {
+ return this.heap.length;
+ }
+ empty() {
+ return this.size() === 0;
+ }
+ add(item) {
+ this.heap.push(item);
+ this._shiftUp(this.size() - 1);
+ }
+ removeMax() {
+ this._shiftDown(0);
+ }
+ getParentIndex(k) {
+ return parseInt((k - 1) / 2);
+ }
+ getLeftIndex(k) {
+ return k * 2 + 1;
+ }
+ getRightIndex(k) {
+ return k * 2 + 2;
+ }
+ _shiftUp(k) {
+ //如果当前节点比父节点大,就交换
+ while (this.heap[k] > this.heap[this.getParentIndex(k)]) {
+ this._swap(k, this.getParentIndex(k));
+ //将索引变成父节点
+ k = this.getParentIndex(k);
+ }
+ }
+ _shiftDown(k) {
+ //交换首位并删除末尾
+ this._swap(k, this.size() - 1);
+ this.heap.splice(this.size() - 1, 1);
+ //判断节点是否有左孩子,因为二叉堆的特性,有右必有左
+ while (this.getLeftIndex(k) < this.size()) {
+ let j = this.getLeftIndex(k);
+ //判断是否有右孩子,并且右孩子是否大于左孩子
+ if (j + 1 < this.size() && this.heap[j + 1] > this.heap[j]) j++;
+ //判断父节点是否已经比子节点都大
+ if (this.heap[k] >= this.heap[j]) break;
+ this._swap(k, j);
+ k = j;
+ }
+ }
+ _swap(left, right) {
+ let rightValue = this.heap[right];
+ this.heap[right] = this.heap[left];
+ this.heap[left] = rightValue;
+ }
+}
+
+/*
+堆的插入操作是单一节点的上浮,时间复杂度 O(logn)
+堆的删除操作是单一节点的下沉,时间复杂度 O(logn)
+注意建堆操作的时间复杂度是 O(n) // 不要误认为是O(nlogn),有两种建立堆的方式。。
+*/
+
import Queue from "./3.实现一个单链队列";
+class Node{
+ constructor(value){
+ this.value=value;
+ this.left=null
+ this.right=null
+ }
+}
+
+class BST{
+ constructor(){
+ this.root=null
+ this.size=0;
+ }
+ getSize(){
+ return this.size
+ }
+ isEmpty(){
+ return this.size===0
+ }
+ addNode(v){
+ this.root=this._addChild(this.root,v)
+ }
+ //添加节点时,需要比较添加的节点值和当前节点值的大小
+ _addChild(node,v){
+ if(!node){
+ this.size++
+ return new Node(v)
+ }
+ if(node.value>v){
+ node.left=this._addChild(node.left,v)
+ }else if(node.value<v){
+ node.right=this._addChild(node.right,v)
+ }
+ return node
+ }
+ //先序遍历:可以用于打印树的结构
+ preTraversal(){
+ this._pre(this.root)
+ }
+ _pre(node){
+ if(node){
+ console.log(node.value);
+ this._pre(node.left)
+ this._pre(node.right)
+ }
+ }
+ //中序遍历:可以用于排序,对于BST来说,中序遍历可以实现一次遍历就得到有序值
+ midTraversal(){
+ this._mid(this.root)
+ }
+ _mid(node){
+ if(node){
+ this._mid(node.left)
+ console.log(node.value);
+ this._mid(node.right)
+ }
+ }
+ //后续遍历:可以用于先操作子节点再操作父节点的场景
+ backTraversal(){
+ this._back(this.root)
+ }
+ _back(node){
+ if(node){
+ this._back(node.left);
+ this._back(node.right);
+ console.log(node.value);
+ }
+ }
+ //广度遍历
+ breadthTraversal(){
+ if(!this.root)return null
+ let q=new Queue()
+ //将根节点入队
+ q.enQueue(this.root)
+ //循环判断队列是否为空,为空代表树遍历完毕
+ while(!q.isEmpty()){
+ //将队首出队,判断是否有左右子树,有的话,就先左后右入队
+ let n=q.deQueue()
+ console.log(n.value);
+ if(n.left)q.enQueue(n.left)
+ if(n.right)q.enQueue(n.right)
+ }
+ }
+ getMin(){
+ return this._getMin(this.root).value
+ }
+ _getMin(node){
+ if(!node.left)return node
+ return this._getMin(node.left)
+ }
+ getMax(){
+ return this._getMax(this.root).value
+ }
+ _getMax(node){
+ if(!node.right)return node
+ return this._getMax(node.right)
+ }
+ //向下取整
+ floor(v){
+ let node =this._floor(this.root,v)
+ return node ?node.value:null
+ }
+ _floor(node,v){
+ if(!node)return null
+ if(node.value==v)return v
+ //如果当前节点值还比需要的值大,就继续递归
+ if(node.value>v){
+ return this._floor(node.left,v)
+ }
+ //如果节点还拥有右子树
+ let right=this._floor(node.right,v)
+ if(right)return right
+ return node
+ }
+ //向上取整的基本操作一样的
+ cell(v){
+ let node =this._floor(this.root,v)
+ return node?node.value:null
+ }
+ _cell(node,v){
+ if(!node)return null
+ if(node.value==v)return v
+ if(node.value<v){
+ return this._cell(node.right,v)
+ }
+ let left=this._floor(node.left,v)
+ if(left)return left
+ return node
+ }
+}
+
/**
+ * @param {number[]} nums
+ * @return {number}
+ */
+var singleNumber = function(nums) {
+ //这里的空间复杂度挺高的!!!
+ // let obj={}
+ // nums.forEach((num,index)=>{
+ // if(obj[num]){
+ // obj[num]=obj[num]+1
+ // }else{
+ // obj[num]=1
+ // }
+ // })
+ // for(let key in obj){
+ // if(obj[key]==1){
+ // return key
+ // }
+ // }
+
+ //进行异或操作
+ // 2 ^ 2 ^ 1 = 0 ^ 1 = 1
+ let res=0
+ for(const num of nums){
+ res^=num
+ }
+ return res
+};
+
/**
+ * @param {number[]} nums
+ * @return {void} Do not return anything, modify nums in-place instead.
+ */
+var nextPermutation = function(nums) {
+ let len=nums.length
+ let i=len-2//注意这里为什么是这样!
+ //从后开始寻找非降序的元素
+ while(i>=0 && nums[i]>=nums[i+1]){
+ i--
+ }
+ if(i>=0){
+ let j=len-1
+ while(j>=0 && nums[i]>=nums[j]){//从后往前走找到大于之前的那个数,进行交换
+ j--
+ }
+ swap(nums,i,j)
+ }
+ reverse(nums,i+1)//翻转最开始找到数后面的的一些数字
+};
+
+function swap(nums,i,j){
+ let tmp=nums[i]
+ nums[i]=nums[j]
+ nums[j]=tmp
+}
+
+function reverse(nums,start){
+ let end=nums.length-1
+ while(start<end){
+ swap(nums,start,end)
+ start++
+ end--
+ }
+}
+
/**
+ * @param {number[]} nums
+ * @param {number} k
+ * @return {number}
+ */
+const subarraySum = (nums, k) => {
+ /*
+ 遍历 nums 之前,我们让 -1 对应的前缀和为 0,这样通式在边界情况也成立。
+ 即在遍历之前,map 初始放入 0:1 键值对(前缀和为0出现1次了)。
+ */
+ const map = { 0: 1 };//可以想象一下前几个的前缀和正好等于k
+ let prefixSum = 0;
+ let count = 0;
+
+ for (let i = 0; i < nums.length; i++) {
+ prefixSum += nums[i];
+ /* 前缀和之差等于k,只关心等于 k 的前缀和之差出现的次数c,就知道了有c个子数组求和等于k。 */
+ if (map[prefixSum - k]) {
+ count += map[prefixSum - k];
+ }
+
+ if (map[prefixSum]) {
+ map[prefixSum]++;
+ } else {
+ map[prefixSum] = 1;
+ }
+ }
+ return count;
+};
+
/**
+ * @param {number[]} nums
+ * @param {number} target
+ * @return {number[]}
+ */
+var twoSum = function (nums, target) {
+ const map = new Map()
+ for (let i = 0; i < nums.length; i++) {
+ if (map.has(target - nums[i])) {
+ return [map.get(target - nums[i]), i]
+ }
+ map.set(nums[i], i)
+ }
+};
+
/**
+ * @param {string[]} strs
+ * @return {string[][]}
+ */
+var groupAnagrams = function(strs) {
+ //要点,就是将字符串排序后就是相同的东西了
+ //还有,就是map来进行存储,key 为排序后的字符串,value为一个数组
+ let map=new Map();
+ for(const str of strs){
+ //sort排序一下key
+ let key = str.split("").sort().join("");
+ if(map.has(key))map.get(key).push(str)
+ else map.set(key,[str])
+ }
+ let arr=Array.from(map.values())
+ return arr
+};
+
/**
+ * @param {number[]} nums
+ * @return {number}
+ */
+var longestConsecutive = function(nums) {
+ //用之前两数之和的那个思想
+ nums.sort((a,b)=>a-b)
+ let map=new Map()
+ let max=0//最长连续的个数!!!
+ for(let i=0;i<nums.length;i++){
+ if(map.has(nums[i]-1))map.set(nums[i],map.get(nums[i]-1)+1)
+ else map.set(nums[i],1)
+ max=Math.max(max,map.get(nums[i]))
+ }
+ return max
+};
+
/**
+ * @param {string} s
+ * @return {boolean}
+ */
+var isValid = function (s) {
+ const map = {
+ '(': -1,
+ ')': 1,
+ '{': -2,
+ '}': 2,
+ '[': -3,
+ ']': 3
+ }
+ const stack=[]
+ for(const c of s){
+ if(map[c]<0){
+ stack.push(map[c])
+ }else if(map[c]>0){
+ let top=stack.pop()
+ if(top+map[c]!=0)return false
+ }
+ }
+ if(stack.length!=0)return false
+ return true
+};
+
+var MinStack = function() {
+ //搞了一个辅助栈!!!
+ this.stack=[]
+ this.min_stack=[Infinity]
+};
+
+/**
+ * @param {number} val
+ * @return {void}
+ */
+MinStack.prototype.push = function(val) {
+ this.stack.push(val)
+ this.min_stack.push(Math.min(this.min_stack[this.min_stack.length-1],val))
+};
+
+/**
+ * @return {void}
+ */
+MinStack.prototype.pop = function() {
+ this.stack.pop()
+ this.min_stack.pop()
+};
+
+/**
+ * @return {number}
+ */
+MinStack.prototype.top = function() {
+ return this.stack[this.stack.length-1]
+};
+
+/**
+ * @return {number}
+ */
+MinStack.prototype.getMin = function() {
+ return this.min_stack[this.min_stack.length-1]
+};
+
+/**
+ * Your MinStack object will be instantiated and called as such:
+ * var obj = new MinStack()
+ * obj.push(val)
+ * obj.pop()
+ * var param_3 = obj.top()
+ * var param_4 = obj.getMin()
+ */
+
const decodeString = (s) => {
+ let numStack = []; // 存倍数的栈
+ let strStack = []; // 存 待拼接的str 的栈
+ let num = 0; // 倍数的“搬运工”
+ let result = ''; // 字符串的“搬运工”
+ for (const char of s) { // 逐字符扫描
+ if (!isNaN(char)) { // 遇到数字
+ num = num * 10 + Number(char); // 算出倍数
+ } else if (char == '[') { // 遇到 [
+ strStack.push(result); // result串入栈
+ result = ''; // 入栈后清零
+ numStack.push(num); // 倍数num进入栈等待
+ num = 0; // 入栈后清零
+ } else if (char == ']') { // 遇到 ],两个栈的栈顶出栈
+ let repeatTimes = numStack.pop(); // 获取拷贝次数
+ result = strStack.pop() + result.repeat(repeatTimes); // 构建子串
+ } else {
+ result += char; // 遇到字母,追加给result串
+ }
+ }
+ return result;
+};
+
/**
+ * @param {number[]} temperatures
+ * @return {number[]}
+ */
+var dailyTemperatures = function (temperatures) {
+ // 单调递减栈
+ let stack = [];
+ let n = temperatures.length;
+ let res = new Array(n).fill(0);
+
+ // 遍历每日温度,维护一个单调栈,存储下标
+ for (let i = 0; i < n; i++) {
+ // 当日温度大于栈顶温度,说明栈顶温度的升温日找到了,栈顶出栈并计算天数;继续判断栈顶元素
+ while (stack.length && temperatures[i] > temperatures[stack[stack.length - 1]]) {
+ const top = stack.pop();
+ res[top] = i - top;
+ }
+ // 栈为空 或 每日温度小于等于栈顶温度 => 直接入栈
+ stack.push(i)
+ }
+
+ return res;
+};
+
/**
+ * @param {number[]} heights
+ * @return {number}
+ */
+var largestRectangleArea = function (heights) {
+ let maxArea = 0, stack = [];
+ let len = heights.length
+ //单调递减栈
+ for (let i = 0; i <= len; i++) {
+ while (stack.length > 0 && (heights[i] < heights[stack[stack.length - 1]] || i === len)) {
+ let height = heights[stack.pop()],
+ width = stack.length > 0 ? i - stack[stack.length - 1] - 1 : i;
+
+ maxArea = Math.max(maxArea, width * height);
+ }
+
+ stack.push(i);
+ }
+
+ return maxArea;
+};
+
+
/**
+ * @param {string} s
+ * @param {string} t
+ * @return {boolean}
+ */
+var backspaceCompare = function(s, t) {
+ let sStack=[],tStack=[]
+ for(const c of s){
+ if(c=='##'){
+ sStack.pop()
+ }else{
+ sStack.push(c)
+ }
+ }
+ for(const c of t){
+ if(c=='##'){
+ tStack.pop()
+ }else{
+ tStack.push(c)
+ }
+ }
+ return sStack.join("")==tStack.join("")
+};
+
/**
+ * @param {number[]} nums
+ * @param {number} k
+ * @return {number}
+ */
+var findKthLargest = function(nums, k) {
+ // 初始化小顶堆
+ // 请注意:我们将堆中所有元素取反,从而用大顶堆来模拟小顶堆
+ const maxHeap = new MaxHeap([]);
+ // 将数组的前 k 个元素入堆
+ for (let i = 0; i < k; i++) {
+ pushMinHeap(maxHeap, nums[i]);
+ }
+ // 从第 k+1 个元素开始,保持堆的长度为 k
+ for (let i = k; i < nums.length; i++) {
+ // 若当前元素大于堆顶元素,则将堆顶元素出堆、当前元素入堆
+ if (nums[i] > peekMinHeap(maxHeap)) {
+ popMinHeap(maxHeap);
+ pushMinHeap(maxHeap, nums[i]);
+ }
+ }
+ // 返回堆中元素
+ return getMinHeap(maxHeap)[0];
+};
+
+/* 元素入堆 */
+function pushMinHeap(maxHeap, val) {
+ // 元素取反
+ maxHeap.push(-val);
+}
+
+/* 元素出堆 */
+function popMinHeap(maxHeap) {
+ // 元素取反
+ return -maxHeap.pop();
+}
+
+/* 访问堆顶元素 */
+function peekMinHeap(maxHeap) {
+ // 元素取反
+ return -maxHeap.peek();
+}
+
+/* 取出堆中元素 */
+function getMinHeap(maxHeap) {
+ // 元素取反
+ return maxHeap.getMaxHeap().map((num) => -num);
+}
+
+class MaxHeap {
+ constructor(arr) {
+ this.heap = arr;
+ }
+ size() {
+ return this.heap.length;
+ }
+ isEmpty() {
+ return this.size() === 0;
+ }
+ peek(){
+ return this.heap[0]
+ }
+ getLeftChild(i) {
+ return i * 2 + 1;
+ }
+ getRightChild(i) {
+ return i * 2 + 2;
+ }
+ getParent(i) {
+ return parseInt((i - 1) / 2);
+ }
+ push(item) {
+ this.heap.push(item);
+ this.shiftUp(this.size() - 1);
+ }
+ pop() {
+ this.shiftDown(0);
+ }
+ shiftUp(i) {
+ while (this.heap[i] > this.heap[this.getParent(i)]) {
+ this.swap(i, this.getParent(i));
+ i = this.getParent(i);
+ }
+ }
+ shiftDown(i) {
+ //交换值并且删除最后一个值
+ this.swap(i, this.size() - 1);
+ this.heap.pop();
+
+ while (this.getLeftChild(i) < this.size()) {
+ let j = this.getLeftChild(i);
+ if (j + 1 < this.size() && this.heap[j] < this.heap[j + 1]) j++;
+ if (this.heap[i] >= this.heap[j]) break;
+ this.swap(i, j);
+ i = j;
+ }
+ }
+ swap(i, j) {
+ const tmp = this.heap[i];
+ this.heap[i] = this.heap[j];
+ this.heap[j] = tmp;
+ }
+ getMaxHeap(){
+ return this.heap
+ }
+}
+
+
/**
+ * @param {number[]} nums
+ * @param {number} k
+ * @return {number[]}
+ */
+var topKFrequent = function (nums, k) {
+ const map = {}
+ for (const it of nums) {
+ map[it] = (map[it] || 0) + 1
+ }
+ let numArr = Object.values(map)
+ const maxHeap = new MaxHeap([]);
+ // 将数组的前 k 个元素入堆
+ for (let i = 0; i < k; i++) {
+ pushMinHeap(maxHeap, numArr[i]);
+ }
+ // 从第 k+1 个元素开始,保持堆的长度为 k
+ for (let i = k; i < numArr.length; i++) {
+ // 若当前元素大于堆顶元素,则将堆顶元素出堆、当前元素入堆
+ if (numArr[i] > peekMinHeap(maxHeap)) {
+ popMinHeap(maxHeap);
+ pushMinHeap(maxHeap, numArr[i]);
+ }
+ }
+ // 返回堆中元素
+ let res = []
+ let narr = getMinHeap(maxHeap);
+ for (const key in map) {
+ if (narr.includes(map[key])) {
+ res.push(key)
+ }
+ }
+ return res
+};
+
+
+function pushMinHeap(maxHeap, val) {
+ // 元素取反
+ maxHeap.push(-val);
+}
+
+/* 元素出堆 */
+function popMinHeap(maxHeap) {
+ // 元素取反
+ return -maxHeap.pop();
+}
+
+/* 访问堆顶元素 */
+function peekMinHeap(maxHeap) {
+ // 元素取反
+ return -maxHeap.peek();
+}
+
+/* 取出堆中元素 */
+function getMinHeap(maxHeap) {
+ // 元素取反
+ return maxHeap.getMaxHeap().map((num) => -num);
+}
+
+class MaxHeap {
+ constructor(arr) {
+ this.heap = arr;
+ }
+ size() {
+ return this.heap.length;
+ }
+ isEmpty() {
+ return this.size() === 0;
+ }
+ peek() {
+ return this.heap[0]
+ }
+ getLeftChild(i) {
+ return i * 2 + 1;
+ }
+ getRightChild(i) {
+ return i * 2 + 2;
+ }
+ getParent(i) {
+ return parseInt((i - 1) / 2);
+ }
+ push(item) {
+ this.heap.push(item);
+ this.shiftUp(this.size() - 1);
+ }
+ pop() {
+ this.shiftDown(0);
+ }
+ shiftUp(i) {
+ while (this.heap[i] > this.heap[this.getParent(i)]) {
+ this.swap(i, this.getParent(i));
+ i = this.getParent(i);
+ }
+ }
+ shiftDown(i) {
+ //交换值并且删除最后一个值
+ this.swap(i, this.size() - 1);
+ this.heap.pop();
+
+ while (this.getLeftChild(i) < this.size()) {
+ let j = this.getLeftChild(i);
+ if (j + 1 < this.size() && this.heap[j] < this.heap[j + 1]) j++;
+ if (this.heap[i] >= this.heap[j]) break;
+ this.swap(i, j);
+ i = j;
+ }
+ }
+ swap(i, j) {
+ const tmp = this.heap[i];
+ this.heap[i] = this.heap[j];
+ this.heap[j] = tmp;
+ }
+ getMaxHeap() {
+ return this.heap
+ }
+}
+
+
/**
+ * @param {number[][]} mat
+ * @return {number[][]}
+ */
+var diagonalSort = function (mat) {
+ const n = mat.length;
+ const m = mat[0].length;
+ const diag = new Array(m + n).fill().map(() => []);
+ //先进行收集管道数据
+ for (let i = 0; i < n; i++) {
+ for (let j = 0; j < m; j++) {
+ diag[i - j + m].push(mat[i][j]);
+ }
+ }
+ //进行排序
+ diag.forEach(d => d.sort((a, b) => b - a));
+ //进行填充
+ for (let i = 0; i < n; i++) {
+ for (let j = 0; j < m; j++) {
+ mat[i][j] = diag[i - j + m].pop();
+ }
+ }
+ return mat;
+};
+
/**
+ * @param {number[][]} matrix
+ * @return {number[]}
+ */
+var spiralOrder = function (matrix) {
+ if (matrix.length == 0 || matrix[0].length == 0) { return [] }
+ //数据的准备
+ let row = matrix.length, col = matrix[0].length
+ const direction = [[0, 1], [1, 0], [0, -1], [-1, 0]]
+ const visited = Array.from({ length: row }, () => new Array(col).fill(false))
+ let total=row*col
+ let curRow=0,curCol=0,directionIndex=0
+ let order=new Array(total)
+
+ for(let i=0;i<total;i++){
+ //具体操作
+ visited[curRow][curCol]=true
+ order[i]=matrix[curRow][curCol]
+ //方便下一次操作
+ let newRow=curRow+direction[directionIndex][0]
+ let newCol=curCol+direction[directionIndex][1]
+
+ //进行范围的判断
+ if(!(newRow>=0 &&newRow<row && newCol>=0 && newCol<col && !(visited[newRow][newCol]))){
+ directionIndex=(directionIndex+1)%4
+ }
+
+ curRow+=direction[directionIndex][0]
+ curCol+=direction[directionIndex][1]
+
+ }
+ return order
+};
+
/**
+ * @param {number[][]} board
+ * @return {void} Do not return anything, modify board in-place instead.
+ */
+var gameOfLife = function (board) {
+ // 在增加一个数组
+ const m = board.length, n = board[0].length
+ const other = Array.from({ length: m }, () => new Array(n).fill(0))
+ for(let i=0;i<m;i++){
+ for(let j=0;j<n;j++){
+ if(getOnes(board,i,j)<2){
+ other[i][j]=0
+ }else if(getOnes(board,i,j)==2){
+ other[i][j]=board[i][j]
+ }else if(getOnes(board,i,j)==3){
+ other[i][j]=1
+ }else if(getOnes(board,i,j)>3){
+ other[i][j]=0
+ }
+ }
+ }
+ //身上复制
+ for(let i=0;i<m;i++){
+ for(let j=0;j<n;j++){
+ board[i][j]=other[i][j]
+ }
+ }
+};
+//获取周围的一些 "1"
+function getOnes(board, i, j) {
+ const arr = [
+ board[i]?.[j + 1],
+ board[i]?.[j - 1],
+ board[i + 1]?.[j],
+ board[i + 1]?.[j - 1],
+ board[i + 1]?.[j + 1],
+ board[i - 1]?.[j],
+ board[i - 1]?.[j + 1],
+ board[i - 1]?.[j - 1]
+ ]
+ return arr.filter(item => item == 1).length
+}
+
/**
+ * @param {number[][]} matrix
+ * @return {void} Do not return anything, modify matrix in-place instead.
+ */
+var rotate = function (matrix) {
+ //两遍循环的问题
+ let n=matrix.length
+ for(let i=0;i<Math.floor(n/2);i++){
+ for(let j=0;j<Math.floor((n+1)/2);j++){
+ let tmp=matrix[i][j]
+ matrix[i][j]=matrix[n-1-j][i]
+ matrix[n-1-j][i]=matrix[n-1-i][n-1-j]
+ matrix[n-1-i][n-1-j]=matrix[j][n-1-i]
+ matrix[j][n-1-i]=tmp
+ }
+ }
+};
+
/**
+ * @param {number[][]} matrix
+ * @return {void} Do not return anything, modify matrix in-place instead.
+ */
+var setZeroes = function(matrix) {
+ //把零的位置全部存下啦
+ let m=matrix.length;
+ let n=matrix[0].length
+ obj={
+ i:new Set(),
+ j:new Set()
+ }
+
+ //把零的位置遍历一遍
+ for(let i=0;i<m;i++){
+ for(let j=0;j<n;j++){
+ if(matrix[i][j]==0){
+ obj.i.add(i)
+ obj.j.add(j)
+ }
+ }
+ }
+
+ for(let i=0;i<m;i++){
+ for(let j=0;j<n;j++){
+ if(obj.i.has(i) ||obj.j.has(j)){
+ matrix[i][j]=0
+ }
+ }
+ }
+};
+
/**
+ * @param {number[]} nums
+ * @return {boolean}
+ */
+// 由题目描述,我们需要达到最后一个下标,那么最后一个下标的数字其实是可以不用考虑的。
+// 那么我们可以假设只有两个数字(比如 [2,4][2, 4][2,4]),这个时候第一个数字如果是大于等于 111 的数就成立;
+// 如果是三个数字的话(比如 [3,0,4][3, 0, 4][3,0,4]),第一个数字大于等于 222 时成立。
+// 依此类推,一个数字可以到达的位置必须是这个数字标记的长度值,
+// 有:nums[i]>=jnums[i] >= jnums[i]>=j 成立时才可以到达后面第 jjj 个目标。
+
+var canJump = function(nums) {
+ // 必须到达end下标的数字
+ let end = nums.length - 1;
+
+ for (let i = nums.length - 2; i >= 0; i--) {
+ if (end - i <= nums[i]) {
+ end = i;
+ }
+ }
+
+ return end == 0;
+};
+
/**
+ * @param {number[]} prices
+ * @return {number}
+ */
+var maxProfit = function (prices) {
+ let maxprofit = 0
+ let minprice = Infinity
+ for (let i = 0; i < prices.length; i++) {
+ if (minprice > prices[i]) {
+ minprice = prices[i]
+ } else if (maxprofit < prices[i] - minprice) {
+ maxprofit = prices[i] - minprice
+ }
+ }
+ return maxprofit
+};
+
/**
+ * @param {number[]} nums
+ * @return {number}
+ */
+var jump = function(nums) {
+ let curIndex = 0
+ let nextIndex = 0
+ let steps = 0
+ // 以最小的步数增加最大的覆盖范围,直到覆盖范围覆盖了终点
+ for(let i = 0; i < nums.length - 1; i++) {
+ nextIndex = Math.max(nums[i] + i, nextIndex)
+ if(i === curIndex) {
+ curIndex = nextIndex
+ steps++
+ }
+ }
+
+ return steps
+};
+
手写一个基本且常规的原生请求模板
// 创建一个XMLHttpRequest对象
+const xhr = new XMLHttpRequest();
+// 打开一个 URL
+xhr.open("get", "http://127.0.0.1:8000/server");
+// 最后发送请求
+xhr.send();
+
+//两种进行处理的方式
+// >>>>>>>>>>>>> first-start
+xhr.onreadystatechange = function () {
+ if (xhr.readyState === 4) {
+ if (xhr.status >= 200 && xhr.status < 300) {
+ result.innerHTML = xhr.response;
+ }
+ }
+};
+// >>>>>>>>>>>>> first-end
+// >>>>>>>>>>>>> second-start
+xhr.addEventListener("load", reqListener);
+function reqListener() {
+ console.log(this.responseText);
+}
+// >>>>>>>>>>>>> second-end
+
重要属性介绍:
`,6),r={href:"https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest",target:"_blank",rel:"noopener noreferrer"},k=p(`
XMLHttpRequest.onreadystatechange
:当 readyState 属性发生变化时,调用的事件处理器。XMLHttpRequest.readyState
:返回 一个无符号短整型(unsigned short)数字,代表请求的状态码。readystate 属性有五个,保存 XMLHttpRequest 的状态。
0:请求未初始化 + 1:服务器连接已建立 + 2:请求已收到 + 3:正在处理请求 + 4:请求已完成且响应已就绪 +
XMLHttpRequest.response
:返回一个 ArrayBuffer、Blob、Document,或 DOMString,具体是哪种类型取决于 XMLHttpRequest.responseType 的值。其中包含整个响应实体(response entity body)。XMLHttpRequest.responseText
:返回一个 DOMString,该 DOMString 包含对请求的响应,如果请求未成功或尚未发送,则返回 null。XMLHttpRequest.responseType
:一个用于定义响应类型的枚举值(enumerated value)。XMLHttpRequest.responseURL
:返回经过序列化(serialized)的响应 URL,如果该 URL 为空,则返回空字符串。XMLHttpRequest.responseXML
:返回一个 Document,其中包含该请求的响应,如果请求未成功、尚未发送或是不能被解析为 XML 或 HTML,则返回 null。XMLHttpRequest.status
:返回一个无符号短整型(unsigned short)数字,代表请求的响应状态。XMLHttpRequest.statusText
:返回一个 DOMString,其中包含 HTTP 服务器返回的响应状态。与 XMLHTTPRequest.status 不同的是,它包含完整的响应状态文本(例如,"200 OK")。XMLHttpRequest.timeout
:一个无符号长整型(unsigned long)数字,表示该请求的最大请求时间(毫秒),若超出该时间,请求会自动终止。XMLHttpRequest.upload
:XMLHttpRequestUpload,代表上传进度。XMLHttpRequest.withCredentials
:一个布尔值,用来指定跨域 Access-Control 请求是否应当带有授权信息,如 cookie 或授权 header 头。
GET 请求
// 1.创建对象
+const xhr = new XMLHttpRequest();
+// 设置响应体为json
+xhr.responseType = "json"; //如果要获取JSON数据的,就这样设置
+//get方式设置请求参数
+xhr.open("get", "http://127.0.0.1:8000/server?a=100&b=200&c=300");
+// 3.发送
+xhr.send();
+// 4.事件绑定,处理服务器端的返回数据 onreadystatechange
+xhr.onreadystatechange = function () {
+ // 判断(服务端反悔了所有的结果)
+ if (xhr.readyState === 4) {
+ // 判断响应状态码 200 403 404 500 401
+ // 2XX:成功
+ if (xhr.status >= 200 && xhr.status < 300) {
+ // 处理结果 行 头 空行 体
+ // 1.响应行
+ console.log(xhr.status); //状态码
+ console.log(xhr.statusText); //状态字符串
+ console.log(xhr.getAllResponseHeaders); //所有响应头
+ console.log(xhr.response); //响应体
+ result.innerHTML = xhr.response;
+ }
+ }
+};
+
POST 请求
// 1.创建对象
+const xhr = new XMLHttpRequest();
+// 2.初始化,设置请求方法和URL
+xhr.open("post", "http://127.0.0.1:8000/server"); //post匹配服务器也应该为post
+
+// 设置请求头
+xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+xhr.setRequestHeader("name", "luoyu"); //自定义请求头
+
+// 3.发送(请求体)
+xhr.send("a=100&b=200&c=300"); //post的参数在send()中可以有,注意是post
+xhr.onreadystatechange = function () {
+ // 判断(服务端返回了所有的结果)
+ if (xhr.readyState === 4) {
+ if (xhr.status >= 200 && xhr.status < 300) {
+ result.innerHTML = xhr.response;
+ }
+ }
+};
+
超时与网络异常
const xhr = new XMLHttpRequest();
+// 超时设置 2s 设置,2s钟还没响应就取消
+xhr.timeout = 2000;
+// 设置超时回调
+xhr.ontimeout = function () {
+ alert("你的网络请求超时了!");
+};
+// 网络异常回调
+xhr.onerror = function () {
+ alert("你的网络出现了异常");
+};
+xhr.open("get", "http://127.0.0.1:8000/delay");
+xhr.send();
+xhr.onreadystatechange = function () {
+ if (xhr.readyState === 4) {
+ if (xhr.status >= 200 && xhr.status < 300) {
+ result.innerHTML = xhr.response;
+ }
+ }
+};
+
请求重复问题
let isSending = false; //标识是否在发送AJAX请求
+let x = null;
+const btns = document.querySelectorAll("button");
+btns[0].onclick = function () {
+ if (isSending) x.abort();
+ x = new XMLHttpRequest(); //现在发现个问题,这个要作为对象来new一个
+ isSending = true;
+ x.open("get", "http://127.0.0.1:8000/delay");
+ x.send();
+ // 这里的话我给请求的地方进行了三秒的延迟
+ x.onreadystatechange = function () {
+ if (x.readyState === 4) {
+ isSending = false;
+ }
+ };
+};
+
监测进度
var oReq = new XMLHttpRequest();
+
+// 在请求调用 open() 之前添加事件监听。否则 progress 事件将不会被触发。
+oReq.addEventListener("progress", updateProgress);
+oReq.addEventListener("load", transferComplete);
+oReq.addEventListener("error", transferFailed);
+oReq.addEventListener("abort", transferCanceled);
+
+oReq.open();
+
+// ...
+
+// 服务端到客户端的传输进程(下载)
+function updateProgress(oEvent) {
+ if (oEvent.lengthComputable) {
+ var percentComplete = (oEvent.loaded / oEvent.total) * 100;
+ // ...
+ } else {
+ // 总大小未知时不能计算进程信息
+ }
+}
+
+function transferComplete(evt) {
+ console.log("The transfer is complete.");
+}
+
+function transferFailed(evt) {
+ console.log("An error occurred while transferring the file.");
+}
+
+function transferCanceled(evt) {
+ console.log("The transfer has been canceled by the user.");
+}
+
绕过缓存
在面试的时候,面试官问过我,关于强缓存,在没到达过期时间之前如何更新呢?因为你可能更改了服务端资源。——更新路径
有一个跨浏览器兼容的方法,就是给 URL 添加时间戳。请确保你酌情地添加了 "?" or "&" 。例如,将:
http://example.com/bar.html -> http://example.com/bar.html?12345
+http://example.com/bar.html?foobar=baz -> http://example.com/bar.html?foobar=baz&12345
+
因为本地缓存都是以 URL 作为索引的,这样就可以使每个请求都是唯一的,也就可以这样来绕开缓存。
你也可以用下面的方法自动更改缓存:
Copy to Clipboard
+const req = new XMLHttpRequest();
+
+req.open("GET", url + (/\\?/.test(url) ? "&" : "?") + new Date().getTime());
+req.send(null);
+
同步请求和异步请求
XMLHttpRequest 支持同步和异步通信。但是,一般来说,出于性能原因,异步请求应优先于同步请求。 同步请求阻止代码的执行,这会导致屏幕上出现“冻结”和无响应的用户体验。
xhr.open("GET", "/bar/foo.txt", true); //异步,默认
+xhr.open("GET", "http://www.mozilla.org/", false); //同步
+
注意:当您使用 async=false 时,请不要编写 onreadystatechange 函数 - 把代码放到 send() 语句后面即可:
xmlhttp.open("GET","/try/ajax/ajax_info.txt",false);
+xmlhttp.send();
+document.getElementById("myDiv").innerHTML=xmlhttp.responseText;
+
备注: 从 Gecko 30.0,Blink 39.0 和 Edge 13 开始,主线程上的同步请求由于对用户体验的负面影响而被弃用。同步 XHR 不允许所有新的 XHR 功能(如 timeout 或 abort)。这样做会调用 InvalidAccessError。
Fetch有几个特点的了解一下,面试可能问,深挖你的知识点!
fetch 发送 GET 请求
fetch("http://ajax-base-api-t.itheima.net/api/getbooks")
+ .then((response) => {
+ //这个response是一个Response对象,需要通过一个异步操作取出其中的内容
+ return response.json();
+ })
+ .then((data) => {
+ //经过response.json()处理过的数据
+ console.log(data);
+ })
+ .catch((err) => {
+ console.log(err);
+ });
+
+/* 下面使用async-await改写 :把代码封装成async异步函数 */
+async function getData() {
+ try {
+ //先获取Response对象
+ let response = await fetch(
+ "http://ajax-base-api-t.itheima.net/api/getbooks"
+ );
+ console.log(response);
+
+ //需要通过response.json() 取出response对象中的结果
+ let json = await response.json();
+ console.log(json);
+ } catch (error) {
+ console.log(error);
+ }
+}
+getData();
+
fetch 发送 POST 请求
//post发送:json格式
+async function add() {
+ let obj = {
+ bookname: "魔法书之如何学好前端",
+ author: "阿利亚",
+ publisher: "格兰芬多",
+ };
+ let res = await fetch("http://ajax-base-api-t.itheima.net/api/addbook", {
+ method: "post",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(obj),
+ });
+ let json = await res.json();
+ console.log(json);
+}
+add();
+
fetch 封装
//封装http函数(fetch请求)
+async function http(obj) {
+ let { method, url, params, data } = obj;
+ let res;
+ //params需要处理-->转化成key1=value1&key2=value2的形式
+ if (params) {
+ //固定写法:将params参数拼接成参数字符串
+ let str = new URLSearchParams(params).toString();
+ //拼接到url上去
+ url += "?" + str;
+ }
+
+ //data需要处理-->如果有data,此时需要写完整的代码headers...
+ if (data) {
+ res = await fetch(url, {
+ method: method,
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(data),
+ });
+ } else {
+ res = await fetch(url);
+ }
+ //把获取的结果经过处理之后,返回出去
+ return res.json();
+}
+
+//测试代码1
+async function fn1() {
+ //通过http函数发送get请求获取数据
+ let result = await http({
+ method: "get",
+ url: "http://ajax-base-api-t.itheima.net/api/getbooks",
+ params: {
+ id: 2,
+ },
+ });
+ console.log(result);
+}
+fn1();
+
+//测试代码2
+async function fn2() {
+ //通过http函数发送post请求获取数据
+ let result = await http({
+ method: "post",
+ url: "http://ajax-base-api-t.itheima.net/api/addbook",
+ data: {
+ bookname: "如何高新就业",
+ author: "大佬",
+ publisher: "哈哈出版社",
+ },
+ });
+ console.log(result);
+}
+fn2();
+
GET类型的CSRF GET类型的CSRF利用非常简单,只需要一个HTTP请求,一般会这样利用:
![](http://bank.example/withdraw?account=xiaoming&amount=10000&for=hacker)
+
在受害者访问含有这个img的页面后,浏览器会自动向http://bank.example/withdraw?account=xiaoming&amount=10000&for=hacker发出一次HTTP请求。bank.example就会收到包含受害者登录信息的一次跨域请求。 POST类型的CSRF 这种类型的CSRF利用起来通常使用的是一个自动提交的表单,如:
<form action="http://bank.example/withdraw" method=POST>
+ <input type="hidden" name="account" value="xiaoming" />
+ <input type="hidden" name="amount" value="10000" />
+ <input type="hidden" name="for" value="hacker" />
+</form>
+<script> document.forms[0].submit(); </script>
+
访问该页面后,表单会自动提交,相当于模拟用户完成了一次POST操作。 POST类型的攻击通常比GET要求更加严格一点,但仍并不复杂。任何个人网站、博客,被黑客上传页面的网站都有可能是发起攻击的来源,后端接口不能将安全寄托在仅允许POST上面。 链接类型的CSRF 链接类型的CSRF并不常见,比起其他两种用户打开页面就中招的情况,这种需要用户点击链接才会触发。这种类型通常是在论坛中发布的图片中嵌入恶意链接,或者以广告的形式诱导用户中招,攻击者通常会以比较夸张的词语诱骗用户点击,例如:
<a href="http://test.com/csrf/withdraw.php?amount=1000&for=hacker" taget="_blank">
+ 重磅消息!!
+ <a/>
+
由于之前用户登录了信任的网站A,并且保存登录状态,只要用户主动访问上面的这个PHP页面,则表示攻击成功。
CSRF通常从第三方网站发起,被攻击的网站无法防止攻击发生,只能通过增强自己网站针对CSRF的防护能力来提升安全性。 上文中讲了CSRF的两个特点:
- CSRF(通常)发生在第三方域名。
- CSRF攻击者不能获取到Cookie等信息,只是使用。
针对这两点,我们可以专门制定防护策略,如下:
- 阻止不明外域的访问
- 同源检测
- Samesite Cookie
- 提交时要求附加本域才能获取的信息
- CSRF Token
- 双重Cookie验证
既然CSRF大多来自第三方网站,那么我们就直接禁止外域(或者不受信任的域名)对我们发起请求。 那么问题来了,我们如何判断请求是否来自外域呢? 在HTTP协议中,每一个异步请求都会携带两个Header,用于标记来源域名:
- Origin Header
- Referer Header
这两个Header在浏览器发起请求时,大多数情况会自动带上,并且不能由前端自定义内容。 服务器可以通过解析这两个Header中的域名,确定请求的来源域。
在部分与CSRF有关的请求中,请求的Header中会携带Origin字段。字段内包含请求的域名(不包含path及query)。 如果Origin存在,那么直接使用Origin中的字段确认来源域名就可以。 但是Origin在以下两种情况下并不存在:
`,19),d=n("strong",null,"IE11同源策略:",-1),m={href:"https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy#IE_Exceptions",target:"_blank",rel:"noopener noreferrer"},h=n("li",null,[n("strong",null,"302重定向:"),a(" 在302重定向之后Origin不包含在重定向的请求中,因为Origin可能会被认为是其他来源的敏感信息。对于302重定向的情况来说都是定向到新的服务器上的URL,因此浏览器不想将Origin泄漏到新的服务器上。")],-1),v=t(`根据HTTP协议,在HTTP头中有一个字段叫Referer,记录了该HTTP请求的来源地址。 对于Ajax请求,图片和script等资源请求,Referer为发起请求的页面地址。对于页面跳转,Referer为打开页面历史记录的前一个页面地址。因此我们使用Referer中链接的Origin部分可以得知请求的来源域名。 这种方法并非万无一失,Referer的值是由浏览器提供的,虽然HTTP协议上有明确的要求,但是每个浏览器对于Referer的具体实现可能有差别,并不能保证浏览器自身没有安全漏洞。使用验证 Referer 值的方法,就是把安全性都依赖于第三方(即浏览器)来保障,从理论上来讲,这样并不是很安全。在部分情况下,攻击者可以隐藏,甚至修改自己请求的Referer。 2014年,W3C的Web应用安全工作组发布了Referrer Policy草案,对浏览器该如何发送Referer做了详细的规定。截止现在新版浏览器大部分已经支持了这份草案,我们终于可以灵活地控制自己网站的Referer策略了。新版的Referrer Policy规定了五种Referer策略:No Referrer、No Referrer When Downgrade、Origin Only、Origin When Cross-origin、和 Unsafe URL。之前就存在的三种策略:never、default和always,在新标准里换了个名称。他们的对应关系如下:
策略名称 | 属性值(新) | 属性值(旧) |
---|---|---|
No Referrer | no-Referrer | never |
No Referrer When Downgrade | no-Referrer-when-downgrade | default |
Origin Only | (same or strict) origin | origin |
Origin When Cross Origin | (strict) origin-when-crossorigin | - |
Unsafe URL | unsafe-url | always |
根据上面的表格因此需要把Referrer Policy的策略设置成same-origin,对于同源的链接和引用,会发送Referer,referer值为Host不带Path;跨域访问则不携带Referer。例如:aaa.com引用bbb.com的资源,不会发送Referer。 设置Referrer Policy的方法有三种:
上面说的这些比较多,但我们可以知道一个问题:攻击者可以在自己的请求中隐藏Referer。如果攻击者将自己的请求这样填写:
![](https://awps-assets.meituan.net/mit-x/blog-images-bundle-2018b/ff0cdbee.example/withdraw?amount=10000&for=hacker)
+
那么这个请求发起的攻击将不携带Referer。 另外在以下情况下Referer没有或者不可信:
当Origin和Referer头文件不存在时该怎么办?如果Origin和Referer都不存在,建议直接进行阻止,特别是如果您没有使用随机CSRF Token(参考下方)作为第二次检查。
通过Header的验证,我们可以知道发起请求的来源域名,这些来源域名可能是网站本域,或者子域名,或者有授权的第三方域名,又或者来自不可信的未知域名。 我们已经知道了请求域名是否是来自不可信的域名,我们直接阻止掉这些的请求,就能防御CSRF攻击了吗? 且慢!当一个请求是页面请求(比如网站的主页),而来源是搜索引擎的链接(例如百度的搜索结果),也会被当成疑似CSRF攻击。所以在判断的时候需要过滤掉页面请求情况,通常Header符合以下情况:
Accept: text/html
+Method: GET
+
但相应的,页面请求就暴露在了CSRF的攻击范围之中。如果你的网站中,在页面的GET请求中对当前用户做了什么操作的话,防范就失效了。 例如,下面的页面请求:
GET https://example.com/addComment?comment=XXX&dest=orderId
+
注:这种严格来说并不一定存在CSRF攻击的风险,但仍然有很多网站经常把主文档GET请求挂上参数来实现产品功能,但是这样做对于自身来说是存在安全风险的。 另外,前面说过,CSRF大多数情况下来自第三方域名,但并不能排除本域发起。如果攻击者有权限在本域发布评论(含链接、图片等,统称UGC),那么它可以直接在本域发起攻击,这种情况下同源策略无法达到防护的作用。 综上所述:同源验证是一个相对简单的防范方法,能够防范绝大多数的CSRF攻击。但这并不是万无一失的,对于安全性要求较高,或者有较多用户输入内容的网站,我们就要对关键的接口做额外的防护措施。
前面讲到CSRF的另一个特征是,攻击者无法直接窃取到用户的信息(Cookie,Header,网站内容等),仅仅是冒用Cookie中的信息。 而CSRF攻击之所以能够成功,是因为服务器误把攻击者发送的请求当成了用户自己的请求。那么我们可以要求所有的用户请求都携带一个CSRF攻击者无法获取到的Token。服务器通过校验请求是否携带正确的Token,来把正常的请求和攻击的请求区分开,也可以防范CSRF的攻击。
<input type=”hidden” name=”csrftoken” value=”tokenvalue”/>
+
这样,就把Token以参数的形式加入请求了。 3. 服务器验证Token是否正确 当用户从客户端得到了Token,再次提交给服务器的时候,服务器需要判断Token的有效性,验证过程是先解密Token,对比加密字符串以及时间戳,如果加密字符串一致且时间未过期,那么这个Token就是有效的。 这种方法要比之前检查Referer或者Origin要安全一些,Token可以在产生并放于Session之中,然后在每次请求时把Token从Session中拿出,与请求中的Token进行比对,但这种方法的比较麻烦的在于如何把Token以参数的形式加入请求。 下面将以Java为例,介绍一些CSRF Token的服务端校验逻辑,代码如下:
HttpServletRequest req = (HttpServletRequest)request;
+HttpSession s = req.getSession();
+
+// 从 session 中得到 csrftoken 属性
+String sToken = (String)s.getAttribute("csrftoken");
+if(sToken == null){
+ // 产生新的 token 放入 session 中
+ sToken = generateToken();
+ s.setAttribute("csrftoken",sToken);
+ chain.doFilter(request, response);
+} else{
+ // 从 HTTP 头中取得 csrftoken
+ String xhrToken = req.getHeader(“csrftoken”);
+ // 从请求参数中取得 csrftoken
+ String pToken = req.getParameter(“csrftoken”);
+ if(sToken != null && xhrToken != null && sToken.equals(xhrToken)){
+ chain.doFilter(request, response);
+ }else if(sToken != null && pToken != null && sToken.equals(pToken)){
+ chain.doFilter(request, response);
+ }else{
+ request.getRequestDispatcher(“error.jsp”).forward(request,response);
+ }
+}
+
在大型网站中,使用Session存储CSRF Token会带来很大的压力。访问单台服务器session是同一个。但是现在的大型网站中,我们的服务器通常不止一台,可能是几十台甚至几百台之多,甚至多个机房都可能在不同的省份,用户发起的HTTP请求通常要经过像Ngnix之类的负载均衡器之后,再路由到具体的服务器上,由于Session默认存储在单机服务器内存中,因此在分布式环境下同一个用户发送的多次HTTP请求可能会先后落到不同的服务器上,导致后面发起的HTTP请求无法拿到之前的HTTP请求存储在服务器中的Session数据,从而使得Session机制在分布式环境下失效,因此在分布式集群中CSRF Token需要存储在Redis之类的公共存储空间。 由于使用Session存储,读取和验证CSRF Token会引起比较大的复杂度和性能问题,目前很多网站采用Encrypted Token Pattern方式。这种方法的Token是一个计算出来的结果,而非随机生成的字符串。这样在校验时无需再去读取存储的Token,只用再次计算一次即可。 这种Token的值通常是使用UserID、时间戳和随机数,通过加密的方法生成。这样既可以保证分布式服务的Token一致,又能保证Token不容易被破解。 在token解密成功之后,服务器可以访问解析值,Token中包含的UserID和时间戳将会被拿来被验证有效性,将UserID与当前登录的UserID进行比较,并将时间戳与当前时间进行比较。
Token是一个比较有效的CSRF防护方法,只要页面没有XSS漏洞泄露Token,那么接口的CSRF攻击就无法成功。 但是此方法的实现比较复杂,需要给每一个页面都写入Token(前端无法使用纯静态页面),每一个Form及Ajax请求都携带这个Token,后端对每一个接口都进行校验,并保证页面Token及请求Token一致。这就使得这个防护策略不能在通用的拦截上统一拦截处理,而需要每一个页面和接口都添加对应的输出和校验。这种方法工作量巨大,且有可能遗漏。 验证码和密码其实也可以起到CSRF Token的作用哦,而且更安全。为什么很多银行等网站会要求已经登录的用户在转账时再次输入密码,现在是不是有一定道理了?
在会话中存储CSRF Token比较繁琐,而且不能在通用的拦截上统一处理所有的接口。 那么另一种防御措施是使用双重提交Cookie。利用CSRF攻击不能获取到用户Cookie的特点,我们可以要求Ajax和表单请求携带一个Cookie中的值。 双重Cookie采用以下流程:
此方法相对于CSRF Token就简单了许多。可以直接通过前后端拦截的的方法自动化实现。后端校验也更加方便,只需进行请求中字段的对比,而不需要再进行查询和存储Token。 当然,此方法并没有大规模应用,其在大型网站上的安全性还是没有CSRF Token高,原因我们举例进行说明。 由于任何跨域都会导致前端无法获取Cookie中的字段(包括子域名之间),于是发生了如下情况:
用双重Cookie防御CSRF的优点:
缺点:
防止CSRF攻击的办法已经有上面的预防措施。为了从源头上解决这个问题,Google起草了一份草案来改进HTTP协议,那就是为Set-Cookie响应头新增Samesite属性,它用来标明这个 Cookie是个“同站 Cookie”,同站Cookie只能作为第一方Cookie,不能作为第三方Cookie,Samesite 有两个属性值,分别是 Strict 和 Lax,下面分别讲解:
这种称为严格模式,表明这个 Cookie 在任何情况下都不可能作为第三方 Cookie,绝无例外。比如说 b.com 设置了如下 Cookie:
Set-Cookie: foo=1; Samesite=Strict
+Set-Cookie: bar=2; Samesite=Lax
+Set-Cookie: baz=3
+
我们在 a.com 下发起对 b.com 的任意请求,foo 这个 Cookie 都不会被包含在 Cookie 请求头中,但 bar 会。举个实际的例子就是,假如淘宝网站用来识别用户登录与否的 Cookie 被设置成了 Samesite=Strict,那么用户从百度搜索页面甚至天猫页面的链接点击进入淘宝后,淘宝都不会是登录状态,因为淘宝的服务器不会接受到那个 Cookie,其它网站发起的对淘宝的任意请求都不会带上那个 Cookie。
这种称为宽松模式,比 Strict 放宽了点限制:假如这个请求是这种请求(改变了当前页面或者打开了新页面)且同时是个GET请求,则这个Cookie可以作为第三方Cookie。比如说 b.com设置了如下Cookie:
Set-Cookie: foo=1; Samesite=Strict
+Set-Cookie: bar=2; Samesite=Lax
+Set-Cookie: baz=3
+
当用户从 a.com 点击链接进入 b.com 时,foo 这个 Cookie 不会被包含在 Cookie 请求头中,但 bar 和 baz 会,也就是说用户在不同网站之间通过链接跳转是不受影响了。但假如这个请求是从 a.com 发起的对 b.com 的异步请求,或者页面跳转是通过表单的 post 提交触发的,则bar也不会发送。 生成Token放到Cookie中并且设置Cookie的Samesite,Java代码如下:
private void addTokenCookieAndHeader(HttpServletRequest httpRequest, HttpServletResponse httpResponse) {
+ //生成token
+ String sToken = this.generateToken();
+ //手动添加Cookie实现支持“Samesite=strict”
+ //Cookie添加双重验证
+ String CookieSpec = String.format("%s=%s; Path=%s; HttpOnly; Samesite=Strict", this.determineCookieName(httpRequest), sToken, httpRequest.getRequestURI());
+ httpResponse.addHeader("Set-Cookie", CookieSpec);
+ httpResponse.setHeader(CSRF_TOKEN_NAME, token);
+ }
+
如果SamesiteCookie被设置为Strict,浏览器在任何跨域请求中都不会携带Cookie,新标签重新打开也不携带,所以说CSRF攻击基本没有机会。 但是跳转子域名或者是新标签重新打开刚登陆的网站,之前的Cookie都不会存在。尤其是有登录的网站,那么我们新打开一个标签进入,或者跳转到子域名的网站,都需要重新登录。对于用户来讲,可能体验不会很好。 如果SamesiteCookie被设置为Lax,那么其他网站通过页面跳转过来的时候可以使用Cookie,可以保障外域连接打开页面时用户的登录状态。但相应的,其安全性也比较低。 另外一个问题是Samesite的兼容性不是很好,现阶段除了从新版Chrome和Firefox支持以外,Safari以及iOS Safari都还不支持,现阶段看来暂时还不能普及。 而且,SamesiteCookie目前有一个致命的缺陷:不支持子域。例如,种在topic.a.com下的Cookie,并不能使用a.com下种植的SamesiteCookie。这就导致了当我们网站有多个子域名时,不能使用SamesiteCookie在主域名存储用户登录信息。每个子域名都需要用户重新登录一次。 总之,SamesiteCookie是一个可能替代同源验证的方案,但目前还并不成熟,其应用场景有待观望。
前面所说的,都是被攻击的网站如何做好防护。而非防止攻击的发生,CSRF的攻击可以来自:
对于来自黑客自己的网站,我们无法防护。但对其他情况,那么如何防止自己的网站被利用成为攻击的源头呢?
grid-template-columns
grid-gap
repeat()
minmax()
auto-fill
fr
<!DOCTYPE html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width">
+ <title>CSS Grid starting point</title>
+ <style>
+ body {
+ width: 90%;
+ max-width: 900px;
+ margin: 2em auto;
+ font: .9em/1.2 Arial, Helvetica, sans-serif;
+ }
+
+ .container > div {
+ border-radius: 5px;
+ padding: 10px;
+ background-color: rgb(207,232,220);
+ border: 2px solid rgb(79,185,227);
+ }
+ /* 补充 */
+ .container {
+ display: grid;
+ /* 加三个宽度为200px的列。 */
+ grid-template-columns: 200px 200px 200px ;
+ /* 除了长度和百分比,我们也可以用fr这个单位来灵活地定义网格的行与列的大小。
+ fr这个单位表示了可用空间的一个比例 */
+ grid-template-columns: 2fr 1fr 1fr;
+ /* 使用 grid-column-gap (en-US) 属性来定义列间隙;使用 grid-row-gap (en-US) 来定义行间隙;
+ 使用 grid-gap (en-US) 可以同时设定两者。
+ !!!!注意:间隙距离可以用任何长度单位包括百分比来表示,但不能使用fr单位 */
+ grid-gap: 20px 10px;/* 行 列 */
+ gap: 20px 10px;/* 两个一样 */
+ /* 使用repeat来重复构建具有某些宽度配置的某些列
+ repeat(3, 1fr):得到了 3 个1fr的列
+ repeat(2, 2fr 1fr): 相当于填入了2fr 1fr 2fr 1fr*/
+ grid-template-columns: repeat(3, 1fr);
+ /* 隐式网格中生成的行/列大小是参数默认是auto,大小会根据放入的内容自动调整。
+ 当然,你也可以使用grid-auto-rows和grid-auto-columns属性手动设定隐式网格的大小。
+ 下面的例子将grid-auto-rows设为了100px,然后你可以看到那些隐式网格中的行
+ (因为这个例子里没有设定grid-template-rows,因此,所有行都位于隐式网格内)现在都是 100 像素高了。
+ 简单来说,隐式网格就是为了放显式网格放不下的元素,浏览器根据已经定义的显式网格自动生成的网格部分。 */
+ grid-auto-rows: 100px;
+ /* minmax 函数为一个行/列的尺寸设置了取值范围。比如设定为 minmax(100px, auto),那么尺寸就至少为 100 像素,
+ 并且如果内容尺寸大于 100 像素则会根据内容自动调整。 */
+ grid-auto-rows: minmax(100px,auto);
+ /*
+ 自动使用多列填充
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+ */
+ }
+ </style>
+ </head>
+
+<body>
+ <h1>Simple grid example</h1>
+ <div class="container">
+ <div>就能(´ڡ\`ლ)好吃的.∑(っ°Д°;)っ卧槽,不见了</div>
+ <div>Two</div>
+ <div>Three</div>
+ <div>Four Lorem ipsum dolor sit amet consectetur adipisicing elit. At doloribus error animi eius labore rerum quo saepe nihil veritatis! Necessitatibus, similique facere? Voluptatem eos consequatur tempora non alias. Repudiandae, nihil!</div>
+ <div>Five</div>
+ <div>Six</div>
+ <div>Seven Lorem ipsum dolor sit amet consectetur adipisicing elit. Consequuntur blanditiis exercitationem eos veritatis officia, enim dolores nostrum minima iure, repellat eius, similique suscipit dolorem eaque? Porro dolores quidem consequuntur facilis!</div>
+ </div>
+</body>
+</html>
+
grid-template-areas
grid-template-areas属性的使用规则如下:
- 你需要填满网格的每个格子
- 对于某个横跨多个格子的元素,重复写上那个元素grid-area属性定义的区域名字
- 所有名字只能出现在一个连续的区域,不能在不同的位置出现
- 一个连续的区域必须是一个矩形
- 使用.符号,让一个格子留空
<!DOCTYPE html>
+<html lang="en-us">
+ <head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width" />
+ <title>CSS Grid - line-based placement starting point</title>
+ <style>
+ body {
+ width: 90%;
+ max-width: 900px;
+ margin: 2em auto;
+ font: 0.9em/1.2 Arial, Helvetica, sans-serif;
+ }
+
+ header,
+ footer {
+ border-radius: 5px;
+ padding: 10px;
+ background-color: rgb(207, 232, 220);
+ border: 2px solid rgb(79, 185, 227);
+ }
+
+ aside {
+ border-right: 1px solid #999;
+ }
+ .container {
+ display: grid;
+ grid-template-areas:
+ "header header"
+ "sidebar content"
+ "footer footer";
+ grid-template-columns: 1fr 3fr;
+ grid-gap: 20px;
+ }
+ /*
+选择器(目标){
+ grid-area:区域名字;
+}
+*/
+ header {
+ grid-area: header;
+ }
+
+ article {
+ grid-area: content;
+ }
+
+ aside {
+ grid-area: sidebar;
+ }
+
+ footer {
+ grid-area: footer;
+ }
+ </style>
+ </head>
+
+ <body>
+ <div class="container">
+ <header>This is my lovely blog</header>
+ <article>
+ <h1>My article</h1>
+ <p>
+ Duis felis orci, pulvinar id metus ut, rutrum luctus orci. Cras
+ porttitor imperdiet nunc, at ultricies tellus laoreet sit amet. Sed
+ auctor cursus massa at porta. Integer ligula ipsum, tristique sit amet
+ orci vel, viverra egestas ligula. Curabitur vehicula tellus neque, ac
+ ornare ex malesuada et. In vitae convallis lacus. Aliquam erat
+ volutpat. Suspendisse ac imperdiet turpis. Aenean finibus sollicitudin
+ eros pharetra congue. Duis ornare egestas augue ut luctus. Proin
+ blandit quam nec lacus varius commodo et a urna. Ut id ornare felis,
+ eget fermentum sapien.
+ </p>
+
+ <p>
+ Nam vulputate diam nec tempor bibendum. Donec luctus augue eget
+ malesuada ultrices. Phasellus turpis est, posuere sit amet dapibus ut,
+ facilisis sed est. Nam id risus quis ante semper consectetur eget
+ aliquam lorem. Vivamus tristique elit dolor, sed pretium metus
+ suscipit vel. Mauris ultricies lectus sed lobortis finibus. Vivamus eu
+ urna eget velit cursus viverra quis vestibulum sem. Aliquam tincidunt
+ eget purus in interdum. Cum sociis natoque penatibus et magnis dis
+ parturient montes, nascetur ridiculus mus.
+ </p>
+ </article>
+ <aside>
+ <h2>Other things</h2>
+ <p>
+ Nam vulputate diam nec tempor bibendum. Donec luctus augue eget
+ malesuada ultrices. Phasellus turpis est, posuere sit amet dapibus ut,
+ facilisis sed est.
+ </p>
+ </aside>
+ <footer>Contact me@mysite.com</footer>
+ </div>
+ </body>
+</html>
+
grid-template
<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>grid-template</title>
+ <style>
+ body{
+ min-height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content:space-around;
+ }
+ .content{
+ width: 600px;
+ height: 400px;
+ border: 1px solid red;
+ display: grid;
+ gap: 10px;
+ /* 这个属性是所简写属性:grid-template-rows、grid-template-columns与grid-template-areas。 */
+ /* 下面这个是缩写!!! 最下面一行是 grid-template-columns*/
+ grid-template:
+ "a a a" 1fr
+ "b c c" 1fr
+ "b c c" 1fr /
+ 1fr 1fr 1fr;
+ }
+ .a{
+ grid-area: a;
+ background-color: lime;
+ }
+ .b{
+ grid-area: b;
+ background-color: aqua;
+ }
+ .c{
+ grid-area: c;
+ background-color: blueviolet;
+ }
+
+ .wrap{
+ width: 600px;
+ height: 400px;
+ border: 1px solid red;
+ display: grid;
+ gap: 10px;
+ grid-template:100px auto / 100px 1fr 1fr;/* 先是规定行,后是规定列!!! */
+ }
+ .plot{
+ background-color: brown;
+ }
+ </style>
+</head>
+<body>
+ <div class="content">
+ <div class="a">小雨</div>
+ <div class="b">好哇好哇</div>
+ <div class="c">华为</div>
+ </div>
+ <div class="wrap">
+ <div class="plot">1</div>
+ <div class="plot">2</div>
+ <div class="plot">3</div>
+ <div class="plot">4</div>
+ <div class="plot">5</div>
+ <div class="plot">6</div>
+ </div>
+</body>
+</html>
+
grid-area
grid-row
grid-column
<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>grid-area</title>
+ <style>
+ body{
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 100vh;
+ margin: 0;
+ padding: 0;
+ }
+ .content{
+ width:500px ;
+ height: 500px;
+ /* border: 1px salmon solid; */
+ position: relative;
+ }
+ .bg,.cover{
+ width: inherit;
+ height: inherit;
+ display: grid;
+ grid-template: repeat(5,1fr)/repeat(5,1fr);
+ gap: 5px 5px;
+ position: absolute;
+ z-index: 10;
+ }
+ .bg>.plot{
+ background-color: lawngreen;
+ }
+ .cover{
+ z-index: 20;
+ }
+ .cover>div{
+ border-radius: 50%;
+ }
+ .item1{
+ background-color: blue;
+ grid-row: 1/3;
+ grid-column: 1/3;
+ }
+ .item2{
+ background-color: palevioletred;
+ }
+ .item3{
+ background-color: yellow;
+ grid-area: 3/3/6/6;/* top / left / bottom / right */
+ }
+ </style>
+</head>
+<body>
+ <!--
+ CSS 属性 grid-area 是一种对于 grid-row-start (en-US)、grid-column-start (en-US)、grid-row-end (en-US) 和 grid-column-end (en-US) 的简写,
+ 通过基线(line),跨度(span)或没有(自动)的网格放置在 grid row 中指定一个网格项的大小和位置,继而确定 grid area 的边界。
+ -->
+ <div class="content">
+ <div class="bg">
+ <div class="plot"></div>
+ <div class="plot"></div>
+ <div class="plot"></div>
+ <div class="plot"></div>
+ <div class="plot"></div>
+ <div class="plot"></div>
+ <div class="plot"></div>
+ <div class="plot"></div>
+ <div class="plot"></div>
+ <div class="plot"></div>
+ <div class="plot"></div>
+ <div class="plot"></div>
+ <div class="plot"></div>
+ <div class="plot"></div>
+ <div class="plot"></div>
+ <div class="plot"></div>
+ <div class="plot"></div>
+ <div class="plot"></div>
+ <div class="plot"></div>
+ <div class="plot"></div>
+ <div class="plot"></div>
+ <div class="plot"></div>
+ <div class="plot"></div>
+ <div class="plot"></div>
+ <div class="plot"></div>
+ </div>
+ <div class="cover">
+ <div class="item1"></div>
+ <div class="item2"></div>
+ <div class="item3"></div>
+ </div>
+ </div>
+</body>
+</html>
+
使用 stash 的一个场景
问题场景:
+甲和乙同时修改master分支代码。
+甲修改了一部分,在本地,未提交
+乙修改了一部分代码,提交到了远程
+甲如何更新到乙修改的代码,同时本地修改保留?
+
+解决:
+1、执行git stash #暂存这些变更
+2、git pull origin #拉取远程代码
+3、git stash pop #重新应用储藏的变更
+4、再次提交自己的代码到远程
+ git commit -a -m "提交说明"
+ git push origin master
+
使用 stash 的另一个场景
问题场景:
+甲同学在自己的分支上开发进行一半了。
+但是代码还不想进行提交(切换分支要清空工作区)。
+现在要修改别的分支问题的时候。
+
+1、git stash:保存开发到一半的代码
+2、git commit -m '修改问题'
+3、git stash pop:将代码追加到最新的提交之后
+
<!-- test.html -->
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Document</title>
+ </head>
+ <body>
+ <script src="./module.js"></script>
+ <script>
+ foo();
+ bar();
+
+ msg = "小雨呀";
+ foo();
+ </script>
+ </body>
+</html>
+
// module.js
+/* 全局函数模式:将不同的功能封装成不同的全局函数…… */
+let msg="xiaoyu"
+function foo(){
+ console.log("foo()",msg);
+}
+
+function bar(){
+ console.log("bar()",msg);
+}
+
<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Document</title>
+</head>
+<body>
+ <script src="./module.js"></script>
+ <script>
+ obj.foo()
+ obj.msg="小雨"
+ obj.foo()
+ </script>
+</body>
+</html>
+
// module.js
+/* namespace 模式:简单对象封装…… */
+let obj={
+ msg:"xiaoyu",
+ foo(){
+ console.log("foo()",this.msg);
+ }
+}
+
<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Document</title>
+</head>
+<body>
+ <script src="./index.js"></script>
+ <script>
+ util.foo()
+ </script>
+</body>
+</html>
+
// index.js
+/* IIFE模式:匿名函数自调用(闭包) */
+// IIFE模式
+
+let util=(function(){
+ let msg="xiaoyu";
+ function foo(){
+ console.log("foo()",msg);
+ }
+ var module={
+ foo
+ }
+
+ return module
+})()
+
<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Document</title>
+</head>
+<body>
+ <script src="./index.js"></script>
+ <script>
+ module()
+ </script>
+</body>
+</html>
+
// index.js
+/* IIFE增强模式:引入依赖 这是现代模块化实现的基石 */
+
+(function(window,document){
+ let msg="xiaoyu";
+ function foo(){
+ console.log("foo()",msg);
+ }
+ window.module=foo
+ document.querySelector("body").style.backgroundColor="red"
+})(window,document)
+
<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Document</title>
+</head>
+<body>
+ <!-- 注意顺序不能变化,这种依赖一开始就写好的!!! -->
+ <script src="./js/dataService.js"></script>
+ <script src="./js/alerter.js"></script>
+ <script src="./app.js"></script>
+</body>
+</html>
+
// js/dataService.js
+// 定义一个没有依赖的模块
+(function(){
+ let name="dataService.js"
+ function getName(){
+ return name;
+ }
+ window.dataService={getName}
+})(window)
+
// js/alerter.js
+//定义一个有依赖的模块
+(function(window,dataService){
+ let msg="alerter.js"
+ function showMsg(){
+ console.log(msg,dataService.getName());
+ }
+ window.alerter={showMsg}
+})(window,dataService)
+
// app.js
+(function(alerter){
+ alerter.showMsg()
+})(alerter)
+
<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Document</title>
+</head>
+<body>
+ <!-- 入口,运用!!! 这个require.js自己去下载-->
+ <script data-main="./js/main.js" src="./js/lib/require.js"></script>
+</body>
+</html>
+
// js/main.js
+(function(){
+ //66666 注意这个配置!!!
+ requirejs.config({
+ baseUrl:"js/",//基本的路径,出发点在根目录下
+ paths:{//配置路径
+ alerter:"./modules/alerter",
+ dataService:"./modules/dataService"
+ }
+ })
+
+ require(["alerter"],function(alerter){
+ alerter.showMsg();
+ })
+})()
+
+// https://www.runoob.com/w3cnote/requirejs-tutorial-1.html
+
// js/modules/dataService.js
+// 定义没有依赖的模块
+
+define(function() {
+ // 'use strict';
+ let name="dataService.js"
+ function getName(){
+ return name;
+ }
+
+ //暴露模块
+ return {getName}
+});
+
// js/modules/alerter.js
+// 定义有依赖的模块
+define([
+ "dataService"
+], function(dataService) {
+ // 'use strict';
+ let msg="alerter.js";
+ function showMsg(){
+ console.log(msg,dataService.getName());
+ }
+
+ return {showMsg}
+});
+
<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Document</title>
+</head>
+<body>
+ <script src="./js/libs/sea.js"></script>
+ <script>
+ seajs.use("./js/modules/main.js")
+ </script>
+</body>
+</html>
+
// ./js/modules/main.js
+//定义没有依赖的模块
+define(function(require){
+ let module1=require("./module1")
+ module1.foo()
+ let module4=require("./module4")
+ module4.fun2()
+})
+
// ./js/modules/module1.js
+//定义没有依赖的模块
+define(function(require,exports,module){
+ let msg="module1";
+ function foo(){
+ return msg;
+ }
+ //暴露模块
+ module.exports={foo}
+})
+
// ./js/modules/module4.js
+//定义有依赖的模块
+define(function(require,exports,module){
+ let msg="module4";
+ // 同步
+ let module2=require("./module2")
+ module2()
+ //异步引用
+ require.async("./module3.js",function(module3){
+ module3.fun()
+ })
+ function fun2(){
+ console.log(msg);
+ }
+ exports.fun2=fun2
+})
+
// module2.js
+//定义没有依赖的模块
+define(function(require,exports,module){
+ let msg="module2";
+ function bar(){
+ console.log(msg);
+ }
+ //暴露模块
+ module.exports=bar
+})
+
// module3.js
+//定义没有依赖的模块
+define(function(require,exports,module){
+ let msg="module3";
+ function fun(){
+ console.log(msg);
+ }
+ //暴露模块
+ exports.fun=fun
+})
+
{
+ "name": "commonjs-node",
+ "version": "1.0.0",
+ "description": "",
+ "main": "main.js",
+ "scripts": {
+ "test": "echo \\"Error: no test specified\\" && exit 1"
+ },
+ "author": "",
+ "license": "ISC"
+}
+
// main.js
+// 将其他的模块汇集到主模块
+let module1 =require("./modules/module1")
+let module2 =require("./modules/module2")
+let module3 =require("./modules/module3")
+
+module1.foo()
+module2()
+module3.bar()
+module3.foo()
+
// modules/module1.js
+// module.exports = value 暴露一个对象
+
+module.exports={
+ msg:"module1",
+ foo(){
+ console.log("foo()",this.msg);
+ }
+}
+
// modules/module2.js
+// 暴露一个函数 module.exports = function(){}
+
+module.exports=function(){
+ console.log("module2");
+}
+
// modules/module3.js
+// exports.xxx = value
+exports.foo = function(){
+ console.log("foo() module3");
+}
+
+exports.bar = function(){
+ console.log("bar() module3");
+}
+
{
+ "name": "es6",
+ "version": "1.0.0",
+ "description": "",
+ "main": "main.js",
+ "scripts": {
+ "test": "echo \\"Error: no test specified\\" && exit 1"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "type": "module"
+}
+
<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Document</title>
+</head>
+<body>
+ <!-- ES6 注意路径哈 node 和 浏览器环境-->
+ <script type="module" src="./main.js"></script>
+</body>
+</html>
+
// main.js
+//注意js类型为module
+// 默认导出不带括号,其他带括号
+// 括号里面的名字得匹配引入的,括号外的可以变名字
+import Person,{name,age,sayName,obj} from './js/count.js'
+
+console.log(name,age,sayName(),obj);
+const p1=new Person('xiaoma',18)
+console.log(p1);
+
+import hh from "./js/test.js"
+hh()
+
// js/count.js
+// ES6模块功能主要由两个命令构成:export 和 import
+// export用于规定模块的对外接口 import用于输入其他模块提供的功能
+// 一个模块就是一个独立的文件
+export const name='zhnagsan'
+export const age=18
+export function sayName(){
+ return 'my name is 小马哥'
+}
+/* export{
+ name,
+ age,
+ sayName
+} */
+export const obj={
+ foo:'foo'
+}
+
+
+class Person{
+ constructor(name,age){
+ this.name=name;
+ this.age=age;
+ }
+ sayName(){
+ return this.name
+ }
+ sayAge(){
+ return this.age
+ }
+}
+// 默认导出
+export default Person
+
// js/test.js
+export default function(){
+ console.log("sb");
+}
+
我们知道,现在就是⼀个“流量为王”的时代,⼀个⽹站最重要的的就是⽤⼾,有了⽤⼾你才能有 业务,打⽐⽅,你是⼀个电商⽹站,那么你肯定希望你的⽤⼾越多越好,这样才会有更多的⼈去浏 览你的商品,从⽽在你的⽹站上花钱,买东西,这样你才能产⽣收益,但假如你的⽹站打开要⼗⼏ 秒,请求接⼝要⼗⼏秒,那⽤⼾还愿意等么?
看⼀下以下的⽤⼾体验图:
国外⼀些著名公司的调研:
所以说,做好性能优化,提⾼⽤⼾体验很重要!
⻚⾯运⾏的时间线(统计了从浏览器从⽹址开始导航到 window.onload
事件触发的⼀系列关键的时间点):
// ⻚⾯导航时间
+performance.getEntriesByType("navigation");
+// 静态资源
+performance.getEntriesByType("resource");
+// 绘制指标
+performance.getEntriesByType("paint");
+
+/*需要定时轮询, 才能持续获取性能指标*/
+
performance.getEntriesByName(
+ "https://i0.hdslb.com/bfs/svgnext/BDC/danmu_square_line/v1.json"
+);
+
+performance.getEntriesByName(
+ "https://cloud.tencent.com/developer/api/user/session"
+);
+
+/*需要定时轮询, 才能持续获取性能指标*/
+
console.log(performance.now());
+// 5483324.099999994
+
/* 写法⼀ */
+//直接往 PerformanceObserver() ⼊参匿名回调函数,成功 new 了⼀个PerformanceObserver 类的,名为 observer 的对象
+var observer = new PerformanceObserver(function (list, obj) {
+ var entries = list.getEntries();
+ for (var i = 0; i < entries.length; i++) {
+ //处理“navigation”和“resource”事件
+ }
+});
+//调⽤ observer 对象的 observe() ⽅法
+observer.observe({ entryTypes: ["navigation", "resource"] });
+
+/* 写法⼆ */
+//预先声明回调函数 perf_observer
+function perf_observer(list, observer) {
+ //处理“navigation”事件
+}
+//再将其传⼊ PerformanceObserver(),成功 new 了⼀个 PerformanceObserver 类的,名为observer2 的对象
+var observer2 = new PerformanceObserver(perf_observer);
+//调⽤ observer2 对象的 observe() ⽅法
+observer2.observe({ entryTypes: ["navigation"] });
+
实例化 PerformanceObserver 对象,observe ⽅法的 entryTypes 主要性能类型有哪些?
console.log(PerformanceObserver.supportedEntryTypes);
+/*
+['element', 'event', 'first-input', 'largest-contentful-paint', 'layoutshift',
+'longtask', 'mark', 'measure', 'navigation', 'paint', 'resource',
+'visibility-state']
+*/
+
具体每个性能类型的含义:
类型 | 描述 |
---|---|
element | 元素加载时间,实例项是 PerformanceElementTiming 对象。 |
event | 事件延迟,实例项是 PerformanceEventTiming 对象。 |
first-input | ⽤⼾第⼀次与⽹站交互(即点击链接、点击按钮或使⽤⾃定义的 JavaScript 控件时)到浏览器实际能够响应该交互的时间,称之为 Firstinputdelay‒FID。 |
largest-contentful-paint | 屏幕上触发的最⼤绘制元素,实例项是 LargestContentfulPaint 对象。 |
layout-shift | 元素移动时候的布局稳定性,实例项是 LayoutShift 对象。 |
long-animation-frame | ⻓动画关键帧。 |
longtask | ⻓任务实例,归属于 PerformanceLongTaskTiming 对象。 |
mark | ⽤⼾⾃定义的性能标记。实例项是 PerformanceMark 对象。 |
measure | ⽤⼾⾃定义的性能测量。实例项是 PerformanceMeasure 对象。 |
navigation | ⻚⾯导航出去的时间,实例项是 PerformancePaintTiming 对象。 |
pain | ⻚⾯加载时内容渲染的关键时刻(第⼀次绘制,第⼀次有内容的绘制,实例项是 PerformancePaintTiming 对象。 |
resource | ⻚⾯中资源的加载时间信息,实例项是 PerformanceResourceTiming 对象。 |
visibility-state | ⻚⾯可⻅性状态更改的时间,即选项卡何时从前台更改为后台,反之亦然。实例项是 VisibilityStateEntry 对象。 |
soft-navigation | - |
⾸次绘制(First Paint)和⾸次内容绘制(First Contentful Paint)
⾸次绘制(FP)和⾸次内容绘制(FCP)。在浏览器导航并渲染出像素点后,这些性能指标点⽴即被标记。 这些点对于⽤⼾⽽⾔⼗分重要,直乎感官体验!
⾸次绘制(FP),⾸次渲染的时间点。FP 和 FCP 有点像,但 FP ⼀定先于 FCP 发⽣,例如⼀个⻚⾯加载时,第⼀个 DOM 还没绘制完成,但是可能这时⻚⾯的背景颜⾊已经出来了,这时 FP 指标就被记录下来了。⽽ FCP 会在⻚⾯绘制完第⼀个 DOM 内容后记录。
⾸次内容绘制(FCP),⾸次内容绘制的时间,指⻚⾯从开始加载到⻚⾯内容的任何部分在屏幕上完成渲染的时间。
/* PerformanceObserver监控 */
+const observer = new PerformanceObserver((list) => {
+ const entries = list.getEntries();
+ entries.forEach((entry) => {
+ if (entry.name === "first-paint") {
+ console.log("FP(⾸次绘制):", entry.startTime);
+ } else if (entry.name === "first-contentful-paint") {
+ console.log("FCP(⾸次内容绘制):", entry.startTime);
+ }
+ });
+});
+observer.observe({ entryTypes: ["paint"] });
+
+/* performance.getEntriesByName*/
+console.log(
+ "FP(⾸次绘制):" + performance.getEntriesByName("first-paint")[0].startTime
+);
+console.log(
+ "FCP(⾸次内容绘制):" +
+ performance.getEntriesByName("first-contentful-paint")[0].startTime
+);
+
⾸次有效绘制(First Meaningful Paint)
有效内容,这种⼀般很难清晰地界定哪些元素的加载是「有⽤」的(因此⽬前尚⽆规范),但对于开发者他们⾃⼰⽽⾔,他们更知道⻚⾯的哪些部分对于⽤⼾⽽⾔是最为有⽤的,所以这样的衡量标准更多的时候是掌握在开发者⼿上!
const observer = new PerformanceObserver((list) => {
+ const entries = list.getEntries();
+ entries.forEach((entry) => {
+ if (entry.name === "https://xxxxxx.xxx.jpg") {
+ console.log(entry.startTime);
+ }
+ });
+});
+observer.observe({ entryTypes: ["resource"] }); // 可以是图⽚、某个Dom元素
+
可交互时间(TTI
)
指标测量⻚⾯从开始加载(FCP)到主要⼦资源完成渲染,并能够快速、可靠地响应⽤⼾输⼊所需的时间。阻塞会影响正常可交互的时间,浏览器
主线程⼀次只能处理⼀个任务
,如果主线程⻓时间被占⽤,那么可交互时间也会变⻓,所以更多的 TTI 都是发⽣在主线程处于空闲的时间点
良好的TTI
应该控制在 5 秒以内。
测量TTI
的最佳⽅法是在⽹站上运⾏ Lighthouse 性能审核
console.log(performance.timing.domInteractive); // 可交互时间点
+
⻓任务(Long Task)
浏览器主线程⼀次只能处理⼀个任务。 某些情况下,⼀些任务将可能会花费很⻓的时间来执⾏,持续占⽤主进程资源,如果这种情况发⽣了,主线程阻塞,剩下的任务只能在队列中等待。
⽤⼾所感知到的可能是输⼊的延迟,或者是哐当⼀下全部出现。这些是当今⽹⻚糟糕体验的主要来源之⼀。
Long Tasks API 认为任何超过 50 毫秒的任务(Task)都可能存在潜在的问题,并将这些任务相关信息回调给给前端。
把 long task 时间定义为 50ms 的主要理论依据是 Chrome 提出的 RAIL 模型,RAIL 认为事件响应应该在 100ms 以内,滚动和动画处理应该在 16ms 以内,才能保证好的⽤⼾体验,⽽如果⼀个 task 执⾏超过 50ms,则很有可能让体验达不到 RAIL 的标准,故我们需要重点关注执⾏时间超过 50ms 的任务。
const observer = new PerformanceObserver((list) => {
+ const entries = list.getEntries();
+ entries.forEach((entry) => {
+ console.log("Long Task(⻓任务):", entry);
+ });
+});
+observer.observe({ entryTypes: ["longtask"] });
+
Largest Contentful Paint (LCP)
:最⼤内容绘制, ⽤于衡量加载性能。 为了提供良好的⽤⼾体 验,LCP 应在⽹⻚⾸次开始加载后的 2.5 秒内发⽣。First Input Delay (FID)
:⾸次输⼊延迟,⽤于衡量可交互性。为了提供良好的⽤⼾体验,⻚⾯的 FID 应不超过 100 毫秒。Cumulative Layout Shift (CLS)
:累积布局偏移,⽤于衡量视觉稳定性。为了提供良好的⽤⼾体 验,⻚⾯应保持 0.1 或更低的 CLSLighthouse-知名测评⼯具
说起异步加载
,我们需要先了解⼀下什么是同步加载?
// 默认就是同步加载
+<script src="http://abc.com/script.js"></script>
+
阻塞模式
,会阻⽌浏览器的后续处理,停⽌了后续的⽂件的解析,执⾏, 如图像的渲染。流览器之所以会采⽤同步模式,是因为加载的 js ⽂件中有对 dom 的操作,重定向, 输出 document 等默认⾏为,所以同步才是最安全的。所以⼀般我们都会把 script 标签放置在 body 结束标签之前,减少阻塞。⼏种常⻅的异步加载脚本⽅式:
async 和 defer
在 JavaScript 脚本增加 async 或者 defer 属性
// ⾯试经常问: script标签的defer和async的区别? //
+defer要等到html解析完成之后执⾏脚本
+<script src="main.js" defer></script>
+// async异步加载脚本后便会执⾏脚本
+<script src="main.js" async></script>
+
动态添加 script 标签
// js代码中动态添加script标签,并将其插⼊⻚⾯
+const script = document.createElement("script");
+script.src = "a.js";
+document.head.appendChild(script);
+
通过 XHR 异步加载 js
// ⾯试经常问: 谈谈JS中的 XMLHttpRequest 对象的理解?
+var xhr = new XMLHttpRequest();
+/*
+第⼀个参数是请求类型
+第⼆个参数是请求的URL
+第三个参数是是否为异步请求
+*/
+xhr.open("get", "/getUser", true); // true代表我们需要异步加载该脚本
+xhr.setRequestHeader("testHeader", "1111"); // ⾃定义Header
+xhr.send(null); // 参数为请求主体发送的数据,为必填项,当不需要发送数据时,使⽤null
+xhr.onreadyStateChange = function () {
+ if (xhr.readystate === 4) {
+ // ⾯试经常问: 说出你知道的哪些HTTP状态码?
+ if (xhr.status === 304 || (xhr.status >= 200 && xhr.status < 300)) {
+ console.log("成功, result: ", xhr.responseText);
+ } else {
+ console.log("错误, errCode:", xhr.status);
+ }
+ }
+};
+
随着 Webpack 等构建⼯具的能⼒越来越强,开发者在构建阶段可以随⼼所欲地打造项⽬流程,与此同 时按需加载和按需打包的技术曝光度也越来越⾼,甚⾄决定着⼯程化构建的结果,直接影响应⽤的性 能优化。
两者的概念:
按需打包
按需打包⼀般通过两种⽅法来实现:
import { Button } from "antd";
+// 假设我们的业务使⽤了Button组件,同时该组件库没有提供ES Module版本,
+// 那么这样的引⽤会导致最终打包的代码是所有antd导出的内容,这样会⼤⼤增加代码的体积
+
+// 但是如果我们组件库提供了ES Module版本(静态分析能⼒),并且开启了Tree Shaking功能,
+// 那么我们就可以通过“摇树”特性
+// 将不会被使⽤的代码在构建阶段移除。
+
正确使⽤ Tree Shaking 的姿势:
antd 组件库
// package.json
+{
+ // ...
+ "main": "lib/index.js", // 暴露CommonJS规范代码lib/index.js
+ "module": "es/index.js", // ⾮package.json标准字段,打包⼯具专⽤字段,指定符合ESM规范的⼊⼝⽂件
+ // 副作⽤配置字段,告诉打包⼯具遇到sideEffects匹配到的资源,均为⽆副作⽤的模块呢?
+ "sideEffects": [
+ "*.css",
+ " expample.js"
+ ],
+}
+
// 啥叫作副作⽤模块
+// expample.js
+const b = 2;
+export const a = 1;
+console.log(b);
+
项⽬:
Tree Shaking ⼀般与 Babel 搭配使⽤,需要在项⽬⾥⾯配置 Babel,因为 Babel 默认会把 ESM 规范打包 成 CommonJs 代码,所以需要通过配置 babel-preset-env#moudles 编译降级
production: {
+ presets: [
+ '@babel/preset-env',
+ {
+ modules: false
+ }
+ ]
+}
+
webpack4.0 以上在 mode 为 production 的时候会⾃动开启 Tree Shaking,实际就是依赖了、UglifyJS 等压缩插件,默认配置
const config = {
+ mode: 'production',
+ optimization: {
+ // 三类标记:
+ // used export: 被使⽤过的export会这样标记
+ // unused ha by rmony export: 没有被使⽤过的export被这样标记
+ // harmony import: 所有import会被这样标记
+ usedExports: true, // 使⽤usedExports进⾏标记
+ minimizer: {
+ new TerserPlugin({...}) // ⽀持删除未引⽤代码的压缩器
+ }
+ }
+}
+
[
+ {
+ libraryName: "antd",
+ libraryDirectory: "lib", // default: lib
+ style: true,
+ },
+ {
+ libraryName: "antd",
+ },
+];
+
import { TimePicker } from "antd"
+↓ ↓ ↓ ↓ ↓ ↓
+var _button = require('antd/lib/time-picker');
+
按需加载
如何才能动态地按需导⼊模块呢?
动态导⼊ import(module)
⽅法加载模块并返回⼀个 promise,该 promise resolve 为⼀个包含其所有导出的模块对象。我们可以在代码中的任意位置调⽤这个表达式。不兼容浏览器,可以⽤ Babel 进⾏转换(@babel/plugin-syntax-dynamic-import
)
// say.js
+export function hi() {
+ alert(\`你好\`);
+}
+export function bye() {
+ alert(\`拜拜\`);
+}
+export default function() {
+ alert("默认到处");
+}
+{
+hi: () => {},
+bye: () => {},
+default:"sdsd"
+}
+
<!DOCTYPE html>
+<script>
+ async function load() {
+ let say = await import("./say.js");
+ say.hi(); // 你好
+ say.bye(); // 拜拜
+ say.default(); // 默认导出
+ }
+</script>
+<button onclick="load()">Click me</button>
+
如果让你⼿写⼀个不考虑兼容性的 import(module)⽅法,你会怎么写?可以看下以下 Function-like
// 利⽤ES6模块化来实现
+const dynamicImport = (url) => {
+ return new Promise((resolve, reject) => {
+ // 创建script标签
+ const script = document.createElement("script");
+ const tempGlobal = "__tempModuleVariable" + Math.random().toString(32).substring(2);
+ // 通过设置 type="module",告诉浏览器该脚本是⼀个 ES6 模块,需要按照
+ 模块规范进⾏导⼊和导出
+ script.type = "module";
+ script.crossorigin="anonymous"; // 跨域
+ script.textContent = \`import * as m from "\${url}";window.\${tempGlobal} = m;\`;
+ // load 回调
+ script.onload = () => {
+ resolve(window[tempGlobal]);
+ delete window[tempGlobal];
+ script.remove();
+ };
+ // error回调
+ script.onerror = () => {
+ reject(new Error(\`Fail to load module script with URL:\${url}\`));
+ delete window[tempGlobal];
+ script.remove();
+ };
+ document.documentElement.appendChild(script);
+ });
+}
+
可以从代码分割、服务端渲染、组件缓存、⻓列表优化等⻆度去分析Vue性能优化常⻅的策略。
const router = createRouter({
+ routes: [
+ // 借助import()实现异步组件
+ { path: '/foo', component: () => import('./Foo.vue') }
+ ]
+})
+
keep-alive
缓存⻚⾯:避免重复创建组件实例,且能保留缓存组件状态<keep-alive>
+ <component :is="Component"></component>
+</keep-alive>
+
v-show
复⽤DOM:避免重复创建组件<template>
+ <div class="cell">
+ <!-- 这种情况⽤v-show复⽤DOM,⽐v-if效果好 -->
+ <div v-show="value" class="on">
+ <Count :num="10000"/> display:none
+ </div>
+ <section v-show="!value" class="off">
+ <Count :num="10000"/>
+ </section>
+ </div>
+</template>
+
v-once
<!-- single element -->
+<span v-once>This will never change: {{msg}}</span>
+<!-- the element have children -->
+<div v-once>
+ <h1>comment</h1>
+ <p>{{msg}}</p>
+</div>
+<!-- component -->
+<my-component v-once :comment="msg"></my-component>
+<!-- \`v-for\` directive -->
+<ul>
+ <li v-for="i in list" v-once>{{i}}</li>
+</ul>
+
⻓列表性能优化:如果是⼤数据⻓列表,可采⽤虚拟滚动,只渲染少部分区域的内容,第三库vuevirtual-scroller
、vue-virtual-scroll-grid
图片懒加载
<!-- vue-lazyload -->
+<img v-lazy="/static/img/1.png">
+
import { createApp } from 'vue';
+import { Button, Select } from 'element-plus';
+
+const app = createApp()
+app.use(Button)
+app.use(Select)
+
####【render过程】避免不必要的Render
类组件跳过没有必要的组件更新
, 对应的技巧⼿段:PureComponent、React.memo、 shouldComponentUpdate。PureComponent 是对类组件的 Props 和 State 进⾏浅⽐较
React.memo是对函数组件的 Props 进⾏浅⽐较
shouldComponentUpdate是React类组件的钩⼦,在该钩⼦函数我们可以对前后props进⾏深⽐对,返回false可以禁⽌更新组件,我们可以⼿动控制组件的更新
传给⼦组件的派⽣状态或函数,每次都是新的引⽤,这样会导致⼦组件重新刷新
import { useCallback, useState, useMemo } from 'react';
+const [count, setCount] = useState(0);
+// 保证函数引⽤是⼀样的,在将该函数作为props往下传递给其他组件的时候,不会导致
+// 其他组件像PureComponent、shouldComponentUpdate、React.memo等相关优化失效
+// const oldFunc = () => setCount(count => count + 1)
+const newFunc = useCallback(() => setCount(count => count + 1), [])
+// useMemo与useCallback ⼏乎是99%相似,只是useMemo⼀般⽤于密集型计算⼤的⼀些缓存,
+// 它得到的是函数执⾏的结果
+const calcValue = useMemo(() => {
+ return Array(100000).fill('').map(v => /*耗时计算*/ v);
+}, [count]);
+
如果⼀个P组件,它有4个⼦组件ABCD,本⾝有个状态state p, 该状态只影响到AB ,那么我们可以把AB组件进⾏封装, state p 维护⾥⾯,那么state p变化了,也不会影响到CD组件的渲染
import ReactDOM from "react-dom";
+import { createContext, useState, useContext, useMemo } from "react";
+const Context = createContext({ val: 0 });
+const MyProvider = ({ children }) => {
+ const [val, setVal] = useState(0);
+ const handleClick = useCallback(() => {
+ setVal(val + 1);
+ },[val]);
+ const value = useMemo(() => {
+ return {
+ val: val
+ };
+ }, [val]);
+ return (
+ <Context.Provider value={value}>
+ {children}
+ <button onClick={handleClick}>context change</button>
+ </Context.Provider>
+ );
+};
+
+const useVal = () => useContext(Context);
+const Child1 = () => {
+ const { val } = useVal();
+ console.log("Child1重新渲染", val);
+ return <div>Child1</div>;
+};
+const Child2 = () => {
+ console.log("Child2只渲染⼀次");
+ return <div>Child2</div>;
+};
+function App() {
+return (
+ <MyProvider>
+ <Child1 />
+ <Child2 />
+ </MyProvider>
+ );
+}
+const rootElement = document.getElementById("root");
+ReactDOM.render(<App/>, rootElement);
+
那我如果使⽤索引值index作为key,为啥不推荐?⾯试题
// ⽆⽤更新
+<!-- 更新前 -->
+<li key="0">Tom</li>
+<li key="1">Sam</li>
+<li key="2">Ben</li>
+<li key="3">Pam</li>
+<!-- 删除后更新 -->
+<li key="0">Sam</li>
+<li key="1">Ben</li>
+<li key="2">Pam</li>
+
+// 输⼊错乱
+<!-- 更新前 -->
+<input key="0" value="1" id="id1"/>
+<input key="1" value="2" id="id2"/>
+<input key="3" value="3" id="id3"/>
+<input key="4" value="4" id="id4"/>
+<!-- 删除后更新 -->
+<input key="1" value="1" id="id2"/>
+<input key="3" value="2" id="id3"/>
+<input key="4" value="3" id="id4"/>
+
React.lazy
⽅法import { lazy, Suspense, Component } from "react"
+const Com = lazy(() => {
+ return new Promise((resolve, reject) => {
+ setTimeout(() => {
+ if (Math.random() > 0.5) {
+ reject(new Error("error"))
+ } else {
+ resolve(import("./Component"))
+ }
+ }, 1000)
+ })
+})
+// ...
+<Suspense fallback="加载...">
+ <Com />
+</Suspense>
+
在目前的项目中,我们会有很多依赖包,webpack负责将浏览器不能识别的文件类型、语法等转化为可识别的前端三剑客(html,css,js),并在这个过程中充当组织者与优化者的角色。
- Bundle(捆绑包)是指将所有相关的模块和资源打包在一起形成的单个文件。它是应用程序的最终输出,可以在浏览器中加载和执行。
- 捆绑包通常由Webpack根据入口点(entry)和它们的依赖关系自动创建。当你运行Webpack构建时,它会根据配置将所有模块和资源打包成一个或多个捆绑包。
- Chunk(代码块)是Webpack在打包过程中生成的中间文件,它代表着一个模块的集合。
- Webpack 根据代码的拓扑结构和配置将模块组织成不同的代码块。每个代码块可以是一个独立的文件,也可以与其他代码块组合成一个捆绑包。
- Webpack使用代码分割(code splitting)技术将应用程序代码拆分成更小的代码块,以便在需要时进行按需加载。这有助于减小初始加载的文件大小,提高应用程序的性能。
- 在Webpack中,捆绑包和代码块之间存在一对多的关系。一个捆绑包可以包含多个代码块,而一个代码块也可以属于多个不同的捆绑包。这取决于Webpack配置中的拆分点(split points)和代码块的依赖关系。
- 总结起来,bundle 是Webpack打包过程的最终输出文件,而chunk是Webpack在打包过程中生成的中间文件,用于组织和按需加载模块。
- 入口起点(entry point)指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。
- 进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。
- 每个依赖项随即被处理,最后输出到称之为 bundles 的文件中。
- output 属性告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件,默认值为 ./dist。
- 基本上,整个应用程序结构,都会被编译到你指定的输出路径的文件夹中。
模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。
代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。
- loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。
- loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。
- 本质上,webpack loader 将所有类型的文件,转换为应用程序的依赖图(和最终的 bundle)可以直接引用的模块。
- loader 被用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。
- 插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件接口功能极其强大,可以用来处理各种各样的任务。
Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程 :
在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。
',23);function f(W,q){const a=t("ExternalLinkIcon");return i(),o("div",null,[s,b,e("p",null,[k,h,e("a",u,[l("webpack 打包原理及流程解析,超详细!"),c(a)]),d,e("a",_,[l("webpack打包原理 ? 看完这篇你就懂了 !"),c(a)]),w]),m])}const g=n(r,[["render",f],["__file","Webpack打包原理.html.vue"]]),v=JSON.parse('{"path":"/advance/Webpack%E6%89%93%E5%8C%85%E5%8E%9F%E7%90%86.html","title":"说Webpack 打包原理","lang":"zh-CN","frontmatter":{},"headers":[{"level":2,"title":"Webpack 介绍","slug":"webpack-介绍","link":"#webpack-介绍","children":[]},{"level":2,"title":"webpack 核心概念","slug":"webpack-核心概念","link":"#webpack-核心概念","children":[]},{"level":2,"title":"webpack 构建流程","slug":"webpack-构建流程","link":"#webpack-构建流程","children":[]}],"filePathRelative":"advance/Webpack打包原理.md","git":{"createdTime":1716636128000,"updatedTime":1716636128000,"contributors":[{"name":"xiaoyu","email":"luoyu2003@outlook.com","commits":1}]},"readingTime":{"minutes":5.4,"words":1620}}');export{g as comp,v as data}; diff --git "a/assets/Web\345\272\224\347\224\250\345\256\211\345\205\250.html-C-um36wI.js" "b/assets/Web\345\272\224\347\224\250\345\256\211\345\205\250.html-C-um36wI.js" new file mode 100644 index 0000000..9b618fc --- /dev/null +++ "b/assets/Web\345\272\224\347\224\250\345\256\211\345\205\250.html-C-um36wI.js" @@ -0,0 +1 @@ +import{_ as t,o,c as a,a as e}from"./app-B-BkP2m_.js";const c={},s=e("h1",{id:"web应用安全",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#web应用安全"},[e("span",null,"Web应用安全")])],-1),n=e("p",null,"正在更新……",-1),r=[s,n];function i(_,l){return o(),a("div",null,r)}const d=t(c,[["render",i],["__file","Web应用安全.html.vue"]]),h=JSON.parse('{"path":"/computer/Web%E5%BA%94%E7%94%A8%E5%AE%89%E5%85%A8.html","title":"Web应用安全","lang":"zh-CN","frontmatter":{},"headers":[],"filePathRelative":"computer/Web应用安全.md","git":{"createdTime":1715780535000,"updatedTime":1715780535000,"contributors":[{"name":"xiaoyu","email":"luoyu2003@outlook.com","commits":1}]},"readingTime":{"minutes":0.03,"words":9}}');export{d as comp,h as data}; diff --git a/assets/app-B-BkP2m_.js b/assets/app-B-BkP2m_.js new file mode 100644 index 0000000..4e87704 --- /dev/null +++ b/assets/app-B-BkP2m_.js @@ -0,0 +1,26 @@ +/** +* @vue/shared v3.4.27 +* (c) 2018-present Yuxi (Evan) You and Vue contributors +* @license MIT +**//*! #__NO_SIDE_EFFECTS__ */function Rl(e,t){const n=new Set(e.split(","));return r=>n.has(r)}const Ee={},cn=[],et=()=>{},ca=()=>!1,qn=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),Pl=e=>e.startsWith("onUpdate:"),Te=Object.assign,Ol=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},ua=Object.prototype.hasOwnProperty,he=(e,t)=>ua.call(e,t),te=Array.isArray,un=e=>Or(e)==="[object Map]",ki=e=>Or(e)==="[object Set]",oe=e=>typeof e=="function",Re=e=>typeof e=="string",nn=e=>typeof e=="symbol",ke=e=>e!==null&&typeof e=="object",wi=e=>(ke(e)||oe(e))&&oe(e.then)&&oe(e.catch),Ai=Object.prototype.toString,Or=e=>Ai.call(e),fa=e=>Or(e).slice(8,-1),Ci=e=>Or(e)==="[object Object]",Il=e=>Re(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,fn=Rl(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),Ir=e=>{const t=Object.create(null);return n=>t[n]||(t[n]=e(n))},da=/-(\w)/g,nt=Ir(e=>e.replace(da,(t,n)=>n?n.toUpperCase():"")),ha=/\B([A-Z])/g,rn=Ir(e=>e.replace(ha,"-$1").toLowerCase()),Gn=Ir(e=>e.charAt(0).toUpperCase()+e.slice(1)),Jr=Ir(e=>e?`on${Gn(e)}`:""),Bt=(e,t)=>!Object.is(e,t),Yr=(e,t)=>{for(let n=0;nK?Ie(h,A,F,!0,!1,Q):_(p,b,S,A,F,j,$,M,Q)},De=(h,p,b,S,A,F,j,$,M)=>{let R=0;const K=p.length;let Q=h.length-1,Y=K-1;for(;R<=Q&&R<=Y;){const le=h[R],ae=p[R]=M?Pt(p[R]):lt(p[R]);if(Jt(le,ae))y(le,ae,b,null,A,F,j,$,M);else break;R++}for(;R<=Q&&R<=Y;){const le=h[Q],ae=p[Y]=M?Pt(p[Y]):lt(p[Y]);if(Jt(le,ae))y(le,ae,b,null,A,F,j,$,M);else break;Q--,Y--}if(R>Q){if(R<=Y){const le=Y+1,ae=leY)for(;R<=Q;)Ne(h[R],A,F,!0),R++;else{const le=R,ae=R,_e=new Map;for(R=ae;R<=Y;R++){const Ye=p[R]=M?Pt(p[R]):lt(p[R]);Ye.key!=null&&_e.set(Ye.key,R)}let we,Fe=0;const rt=Y-ae+1;let on=!1,oo=0;const En=new Array(rt);for(R=0;R