{point.title}
+ {point.note ? ( +{point.note}
+ ) : null} + {point.children?.length ? ( +diff --git a/package-lock.json b/package-lock.json
index 365e98c..0b05959 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,7 @@
"name": "fly-personal-site",
"version": "0.1.0",
"dependencies": {
+ "lucide-react": "^1.17.0",
"motion": "^12.40.0",
"next": "16.2.7",
"react": "19.2.4",
@@ -4634,6 +4635,14 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/lucide-react": {
+ "version": "1.17.0",
+ "resolved": "https://registry.npmmirror.com/lucide-react/-/lucide-react-1.17.0.tgz",
+ "integrity": "sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w==",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
@@ -9217,6 +9226,12 @@
"yallist": "^3.0.2"
}
},
+ "lucide-react": {
+ "version": "1.17.0",
+ "resolved": "https://registry.npmmirror.com/lucide-react/-/lucide-react-1.17.0.tgz",
+ "integrity": "sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w==",
+ "requires": {}
+ },
"magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
diff --git a/package.json b/package.json
index bad435d..8954b2c 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,7 @@
"lint": "eslint"
},
"dependencies": {
+ "lucide-react": "^1.17.0",
"motion": "^12.40.0",
"next": "16.2.7",
"react": "19.2.4",
diff --git a/public/articles/browser/browser-figure-1.awebp b/public/articles/browser/browser-figure-1.awebp
new file mode 100644
index 0000000..bf88e25
Binary files /dev/null and b/public/articles/browser/browser-figure-1.awebp differ
diff --git a/public/articles/browser/browser-figure-10.awebp b/public/articles/browser/browser-figure-10.awebp
new file mode 100644
index 0000000..81d7840
Binary files /dev/null and b/public/articles/browser/browser-figure-10.awebp differ
diff --git a/public/articles/browser/browser-figure-11.awebp b/public/articles/browser/browser-figure-11.awebp
new file mode 100644
index 0000000..82a5907
Binary files /dev/null and b/public/articles/browser/browser-figure-11.awebp differ
diff --git a/public/articles/browser/browser-figure-12.awebp b/public/articles/browser/browser-figure-12.awebp
new file mode 100644
index 0000000..02ca570
Binary files /dev/null and b/public/articles/browser/browser-figure-12.awebp differ
diff --git a/public/articles/browser/browser-figure-13.awebp b/public/articles/browser/browser-figure-13.awebp
new file mode 100644
index 0000000..38af2ee
Binary files /dev/null and b/public/articles/browser/browser-figure-13.awebp differ
diff --git a/public/articles/browser/browser-figure-2.awebp b/public/articles/browser/browser-figure-2.awebp
new file mode 100644
index 0000000..c07409d
Binary files /dev/null and b/public/articles/browser/browser-figure-2.awebp differ
diff --git a/public/articles/browser/browser-figure-3.awebp b/public/articles/browser/browser-figure-3.awebp
new file mode 100644
index 0000000..6f51504
Binary files /dev/null and b/public/articles/browser/browser-figure-3.awebp differ
diff --git a/public/articles/browser/browser-figure-4.awebp b/public/articles/browser/browser-figure-4.awebp
new file mode 100644
index 0000000..9326782
Binary files /dev/null and b/public/articles/browser/browser-figure-4.awebp differ
diff --git a/public/articles/browser/browser-figure-5.awebp b/public/articles/browser/browser-figure-5.awebp
new file mode 100644
index 0000000..66e8a4b
Binary files /dev/null and b/public/articles/browser/browser-figure-5.awebp differ
diff --git a/public/articles/browser/browser-figure-6.awebp b/public/articles/browser/browser-figure-6.awebp
new file mode 100644
index 0000000..573345f
Binary files /dev/null and b/public/articles/browser/browser-figure-6.awebp differ
diff --git a/public/articles/browser/browser-figure-7.awebp b/public/articles/browser/browser-figure-7.awebp
new file mode 100644
index 0000000..1688584
Binary files /dev/null and b/public/articles/browser/browser-figure-7.awebp differ
diff --git a/public/articles/browser/browser-figure-8.awebp b/public/articles/browser/browser-figure-8.awebp
new file mode 100644
index 0000000..640fbde
Binary files /dev/null and b/public/articles/browser/browser-figure-8.awebp differ
diff --git a/public/articles/browser/browser-figure-9.awebp b/public/articles/browser/browser-figure-9.awebp
new file mode 100644
index 0000000..8d2c9a8
Binary files /dev/null and b/public/articles/browser/browser-figure-9.awebp differ
diff --git a/public/articles/code-output/code-output-figure-1.awebp b/public/articles/code-output/code-output-figure-1.awebp
new file mode 100644
index 0000000..508d486
Binary files /dev/null and b/public/articles/code-output/code-output-figure-1.awebp differ
diff --git a/public/articles/css/css-figure-1.awebp b/public/articles/css/css-figure-1.awebp
new file mode 100644
index 0000000..f1422d8
Binary files /dev/null and b/public/articles/css/css-figure-1.awebp differ
diff --git a/public/articles/css/css-figure-10.awebp b/public/articles/css/css-figure-10.awebp
new file mode 100644
index 0000000..a2c404d
Binary files /dev/null and b/public/articles/css/css-figure-10.awebp differ
diff --git a/public/articles/css/css-figure-11.awebp b/public/articles/css/css-figure-11.awebp
new file mode 100644
index 0000000..b80ddc4
Binary files /dev/null and b/public/articles/css/css-figure-11.awebp differ
diff --git a/public/articles/css/css-figure-12.awebp b/public/articles/css/css-figure-12.awebp
new file mode 100644
index 0000000..4b1945b
Binary files /dev/null and b/public/articles/css/css-figure-12.awebp differ
diff --git a/public/articles/css/css-figure-13.awebp b/public/articles/css/css-figure-13.awebp
new file mode 100644
index 0000000..28ce886
Binary files /dev/null and b/public/articles/css/css-figure-13.awebp differ
diff --git a/public/articles/css/css-figure-14.awebp b/public/articles/css/css-figure-14.awebp
new file mode 100644
index 0000000..ff61706
Binary files /dev/null and b/public/articles/css/css-figure-14.awebp differ
diff --git a/public/articles/css/css-figure-15.awebp b/public/articles/css/css-figure-15.awebp
new file mode 100644
index 0000000..01c56a3
Binary files /dev/null and b/public/articles/css/css-figure-15.awebp differ
diff --git a/public/articles/css/css-figure-16.awebp b/public/articles/css/css-figure-16.awebp
new file mode 100644
index 0000000..60cc73d
Binary files /dev/null and b/public/articles/css/css-figure-16.awebp differ
diff --git a/public/articles/css/css-figure-17.awebp b/public/articles/css/css-figure-17.awebp
new file mode 100644
index 0000000..e33a9da
Binary files /dev/null and b/public/articles/css/css-figure-17.awebp differ
diff --git a/public/articles/css/css-figure-18.awebp b/public/articles/css/css-figure-18.awebp
new file mode 100644
index 0000000..e1a5576
Binary files /dev/null and b/public/articles/css/css-figure-18.awebp differ
diff --git a/public/articles/css/css-figure-19.awebp b/public/articles/css/css-figure-19.awebp
new file mode 100644
index 0000000..feb8687
Binary files /dev/null and b/public/articles/css/css-figure-19.awebp differ
diff --git a/public/articles/css/css-figure-2.awebp b/public/articles/css/css-figure-2.awebp
new file mode 100644
index 0000000..6a9029b
Binary files /dev/null and b/public/articles/css/css-figure-2.awebp differ
diff --git a/public/articles/css/css-figure-3.awebp b/public/articles/css/css-figure-3.awebp
new file mode 100644
index 0000000..a39433f
Binary files /dev/null and b/public/articles/css/css-figure-3.awebp differ
diff --git a/public/articles/css/css-figure-4.awebp b/public/articles/css/css-figure-4.awebp
new file mode 100644
index 0000000..9991bac
Binary files /dev/null and b/public/articles/css/css-figure-4.awebp differ
diff --git a/public/articles/css/css-figure-5.awebp b/public/articles/css/css-figure-5.awebp
new file mode 100644
index 0000000..de8d267
Binary files /dev/null and b/public/articles/css/css-figure-5.awebp differ
diff --git a/public/articles/css/css-figure-6.awebp b/public/articles/css/css-figure-6.awebp
new file mode 100644
index 0000000..b0fbb02
Binary files /dev/null and b/public/articles/css/css-figure-6.awebp differ
diff --git a/public/articles/css/css-figure-7.awebp b/public/articles/css/css-figure-7.awebp
new file mode 100644
index 0000000..b1c5518
Binary files /dev/null and b/public/articles/css/css-figure-7.awebp differ
diff --git a/public/articles/css/css-figure-8.awebp b/public/articles/css/css-figure-8.awebp
new file mode 100644
index 0000000..43f96b8
Binary files /dev/null and b/public/articles/css/css-figure-8.awebp differ
diff --git a/public/articles/css/css-figure-9.awebp b/public/articles/css/css-figure-9.awebp
new file mode 100644
index 0000000..5cb1c32
Binary files /dev/null and b/public/articles/css/css-figure-9.awebp differ
diff --git a/public/articles/html/script-defer-async-timeline.awebp b/public/articles/html/script-defer-async-timeline.awebp
new file mode 100644
index 0000000..6e06440
Binary files /dev/null and b/public/articles/html/script-defer-async-timeline.awebp differ
diff --git a/public/articles/javascript-part-1/javascript-part-1-figure-1.awebp b/public/articles/javascript-part-1/javascript-part-1-figure-1.awebp
new file mode 100644
index 0000000..7499c1d
Binary files /dev/null and b/public/articles/javascript-part-1/javascript-part-1-figure-1.awebp differ
diff --git a/public/articles/javascript-part-1/javascript-part-1-figure-2.awebp b/public/articles/javascript-part-1/javascript-part-1-figure-2.awebp
new file mode 100644
index 0000000..a638abe
Binary files /dev/null and b/public/articles/javascript-part-1/javascript-part-1-figure-2.awebp differ
diff --git a/public/articles/javascript-part-1/javascript-part-1-figure-3.awebp b/public/articles/javascript-part-1/javascript-part-1-figure-3.awebp
new file mode 100644
index 0000000..fdd973f
Binary files /dev/null and b/public/articles/javascript-part-1/javascript-part-1-figure-3.awebp differ
diff --git a/public/articles/javascript-part-1/javascript-part-1-figure-4.awebp b/public/articles/javascript-part-1/javascript-part-1-figure-4.awebp
new file mode 100644
index 0000000..98f402d
Binary files /dev/null and b/public/articles/javascript-part-1/javascript-part-1-figure-4.awebp differ
diff --git a/public/articles/javascript-part-1/javascript-part-1-figure-5.awebp b/public/articles/javascript-part-1/javascript-part-1-figure-5.awebp
new file mode 100644
index 0000000..d2fe6a7
Binary files /dev/null and b/public/articles/javascript-part-1/javascript-part-1-figure-5.awebp differ
diff --git a/public/articles/javascript-part-1/javascript-part-1-figure-6.awebp b/public/articles/javascript-part-1/javascript-part-1-figure-6.awebp
new file mode 100644
index 0000000..a445ab0
Binary files /dev/null and b/public/articles/javascript-part-1/javascript-part-1-figure-6.awebp differ
diff --git a/public/articles/javascript-part-2/javascript-part-2-figure-1.awebp b/public/articles/javascript-part-2/javascript-part-2-figure-1.awebp
new file mode 100644
index 0000000..f96de9f
Binary files /dev/null and b/public/articles/javascript-part-2/javascript-part-2-figure-1.awebp differ
diff --git a/public/articles/javascript-part-2/javascript-part-2-figure-2.awebp b/public/articles/javascript-part-2/javascript-part-2-figure-2.awebp
new file mode 100644
index 0000000..db54a87
Binary files /dev/null and b/public/articles/javascript-part-2/javascript-part-2-figure-2.awebp differ
diff --git a/public/articles/network/network-figure-1.awebp b/public/articles/network/network-figure-1.awebp
new file mode 100644
index 0000000..bdebaef
Binary files /dev/null and b/public/articles/network/network-figure-1.awebp differ
diff --git a/public/articles/network/network-figure-10.awebp b/public/articles/network/network-figure-10.awebp
new file mode 100644
index 0000000..9e16404
Binary files /dev/null and b/public/articles/network/network-figure-10.awebp differ
diff --git a/public/articles/network/network-figure-11.awebp b/public/articles/network/network-figure-11.awebp
new file mode 100644
index 0000000..ecd85bd
Binary files /dev/null and b/public/articles/network/network-figure-11.awebp differ
diff --git a/public/articles/network/network-figure-12.awebp b/public/articles/network/network-figure-12.awebp
new file mode 100644
index 0000000..6f702ea
Binary files /dev/null and b/public/articles/network/network-figure-12.awebp differ
diff --git a/public/articles/network/network-figure-13.awebp b/public/articles/network/network-figure-13.awebp
new file mode 100644
index 0000000..c53a79c
Binary files /dev/null and b/public/articles/network/network-figure-13.awebp differ
diff --git a/public/articles/network/network-figure-14.awebp b/public/articles/network/network-figure-14.awebp
new file mode 100644
index 0000000..58a0767
Binary files /dev/null and b/public/articles/network/network-figure-14.awebp differ
diff --git a/public/articles/network/network-figure-15.awebp b/public/articles/network/network-figure-15.awebp
new file mode 100644
index 0000000..7a5ff9d
Binary files /dev/null and b/public/articles/network/network-figure-15.awebp differ
diff --git a/public/articles/network/network-figure-16.awebp b/public/articles/network/network-figure-16.awebp
new file mode 100644
index 0000000..bdcc078
Binary files /dev/null and b/public/articles/network/network-figure-16.awebp differ
diff --git a/public/articles/network/network-figure-17.awebp b/public/articles/network/network-figure-17.awebp
new file mode 100644
index 0000000..7317a27
Binary files /dev/null and b/public/articles/network/network-figure-17.awebp differ
diff --git a/public/articles/network/network-figure-18.awebp b/public/articles/network/network-figure-18.awebp
new file mode 100644
index 0000000..de57147
Binary files /dev/null and b/public/articles/network/network-figure-18.awebp differ
diff --git a/public/articles/network/network-figure-19.awebp b/public/articles/network/network-figure-19.awebp
new file mode 100644
index 0000000..73172dd
Binary files /dev/null and b/public/articles/network/network-figure-19.awebp differ
diff --git a/public/articles/network/network-figure-2.awebp b/public/articles/network/network-figure-2.awebp
new file mode 100644
index 0000000..bd305a4
Binary files /dev/null and b/public/articles/network/network-figure-2.awebp differ
diff --git a/public/articles/network/network-figure-20.awebp b/public/articles/network/network-figure-20.awebp
new file mode 100644
index 0000000..49ca280
Binary files /dev/null and b/public/articles/network/network-figure-20.awebp differ
diff --git a/public/articles/network/network-figure-3.awebp b/public/articles/network/network-figure-3.awebp
new file mode 100644
index 0000000..0f5739f
Binary files /dev/null and b/public/articles/network/network-figure-3.awebp differ
diff --git a/public/articles/network/network-figure-4.awebp b/public/articles/network/network-figure-4.awebp
new file mode 100644
index 0000000..bec7bce
Binary files /dev/null and b/public/articles/network/network-figure-4.awebp differ
diff --git a/public/articles/network/network-figure-5.awebp b/public/articles/network/network-figure-5.awebp
new file mode 100644
index 0000000..3eb5b76
Binary files /dev/null and b/public/articles/network/network-figure-5.awebp differ
diff --git a/public/articles/network/network-figure-6.awebp b/public/articles/network/network-figure-6.awebp
new file mode 100644
index 0000000..4971d23
Binary files /dev/null and b/public/articles/network/network-figure-6.awebp differ
diff --git a/public/articles/network/network-figure-7.awebp b/public/articles/network/network-figure-7.awebp
new file mode 100644
index 0000000..6fa20e3
Binary files /dev/null and b/public/articles/network/network-figure-7.awebp differ
diff --git a/public/articles/network/network-figure-8.awebp b/public/articles/network/network-figure-8.awebp
new file mode 100644
index 0000000..31d58f2
Binary files /dev/null and b/public/articles/network/network-figure-8.awebp differ
diff --git a/public/articles/network/network-figure-9.awebp b/public/articles/network/network-figure-9.awebp
new file mode 100644
index 0000000..75da169
Binary files /dev/null and b/public/articles/network/network-figure-9.awebp differ
diff --git a/public/articles/performance/performance-figure-1.awebp b/public/articles/performance/performance-figure-1.awebp
new file mode 100644
index 0000000..4d34aff
Binary files /dev/null and b/public/articles/performance/performance-figure-1.awebp differ
diff --git a/public/articles/performance/performance-figure-2.awebp b/public/articles/performance/performance-figure-2.awebp
new file mode 100644
index 0000000..b0fbb02
Binary files /dev/null and b/public/articles/performance/performance-figure-2.awebp differ
diff --git a/public/articles/react-part-1/react-part-1-figure-1.awebp b/public/articles/react-part-1/react-part-1-figure-1.awebp
new file mode 100644
index 0000000..844cf97
Binary files /dev/null and b/public/articles/react-part-1/react-part-1-figure-1.awebp differ
diff --git a/public/articles/react-part-1/react-part-1-figure-2.awebp b/public/articles/react-part-1/react-part-1-figure-2.awebp
new file mode 100644
index 0000000..e4af633
Binary files /dev/null and b/public/articles/react-part-1/react-part-1-figure-2.awebp differ
diff --git a/public/articles/react-part-1/react-part-1-figure-3.awebp b/public/articles/react-part-1/react-part-1-figure-3.awebp
new file mode 100644
index 0000000..316a7b0
Binary files /dev/null and b/public/articles/react-part-1/react-part-1-figure-3.awebp differ
diff --git a/public/articles/react-part-1/react-part-1-figure-4.awebp b/public/articles/react-part-1/react-part-1-figure-4.awebp
new file mode 100644
index 0000000..605a1dc
Binary files /dev/null and b/public/articles/react-part-1/react-part-1-figure-4.awebp differ
diff --git a/public/articles/react-part-1/react-part-1-figure-5.awebp b/public/articles/react-part-1/react-part-1-figure-5.awebp
new file mode 100644
index 0000000..0d394dd
Binary files /dev/null and b/public/articles/react-part-1/react-part-1-figure-5.awebp differ
diff --git a/public/articles/react-part-1/react-part-1-figure-6.awebp b/public/articles/react-part-1/react-part-1-figure-6.awebp
new file mode 100644
index 0000000..10c5a73
Binary files /dev/null and b/public/articles/react-part-1/react-part-1-figure-6.awebp differ
diff --git a/public/articles/react-part-1/react-part-1-figure-7.awebp b/public/articles/react-part-1/react-part-1-figure-7.awebp
new file mode 100644
index 0000000..4c4d24e
Binary files /dev/null and b/public/articles/react-part-1/react-part-1-figure-7.awebp differ
diff --git a/public/articles/react-part-1/react-part-1-figure-8.awebp b/public/articles/react-part-1/react-part-1-figure-8.awebp
new file mode 100644
index 0000000..13fae66
Binary files /dev/null and b/public/articles/react-part-1/react-part-1-figure-8.awebp differ
diff --git a/public/articles/react-part-2/react-part-2-figure-1.awebp b/public/articles/react-part-2/react-part-2-figure-1.awebp
new file mode 100644
index 0000000..04ae857
Binary files /dev/null and b/public/articles/react-part-2/react-part-2-figure-1.awebp differ
diff --git a/public/articles/react-part-2/react-part-2-figure-2.awebp b/public/articles/react-part-2/react-part-2-figure-2.awebp
new file mode 100644
index 0000000..1b1b36a
Binary files /dev/null and b/public/articles/react-part-2/react-part-2-figure-2.awebp differ
diff --git a/public/articles/react-part-2/react-part-2-figure-3.awebp b/public/articles/react-part-2/react-part-2-figure-3.awebp
new file mode 100644
index 0000000..89f997d
Binary files /dev/null and b/public/articles/react-part-2/react-part-2-figure-3.awebp differ
diff --git a/public/articles/react-part-2/react-part-2-figure-4.awebp b/public/articles/react-part-2/react-part-2-figure-4.awebp
new file mode 100644
index 0000000..d90baa0
Binary files /dev/null and b/public/articles/react-part-2/react-part-2-figure-4.awebp differ
diff --git a/public/articles/react-part-2/react-part-2-figure-5.awebp b/public/articles/react-part-2/react-part-2-figure-5.awebp
new file mode 100644
index 0000000..2666ce5
Binary files /dev/null and b/public/articles/react-part-2/react-part-2-figure-5.awebp differ
diff --git a/public/articles/react-part-2/react-part-2-figure-6.awebp b/public/articles/react-part-2/react-part-2-figure-6.awebp
new file mode 100644
index 0000000..3f91176
Binary files /dev/null and b/public/articles/react-part-2/react-part-2-figure-6.awebp differ
diff --git a/public/articles/react-part-2/react-part-2-figure-7.awebp b/public/articles/react-part-2/react-part-2-figure-7.awebp
new file mode 100644
index 0000000..ba43bda
Binary files /dev/null and b/public/articles/react-part-2/react-part-2-figure-7.awebp differ
diff --git a/public/articles/vue-part-1/vue-part-1-figure-1.awebp b/public/articles/vue-part-1/vue-part-1-figure-1.awebp
new file mode 100644
index 0000000..67bce83
Binary files /dev/null and b/public/articles/vue-part-1/vue-part-1-figure-1.awebp differ
diff --git a/public/articles/vue-part-1/vue-part-1-figure-2.awebp b/public/articles/vue-part-1/vue-part-1-figure-2.awebp
new file mode 100644
index 0000000..2d03403
Binary files /dev/null and b/public/articles/vue-part-1/vue-part-1-figure-2.awebp differ
diff --git a/public/articles/vue-part-1/vue-part-1-figure-3.awebp b/public/articles/vue-part-1/vue-part-1-figure-3.awebp
new file mode 100644
index 0000000..a5e7a11
Binary files /dev/null and b/public/articles/vue-part-1/vue-part-1-figure-3.awebp differ
diff --git a/public/articles/vue-part-1/vue-part-1-figure-4.awebp b/public/articles/vue-part-1/vue-part-1-figure-4.awebp
new file mode 100644
index 0000000..46540a3
Binary files /dev/null and b/public/articles/vue-part-1/vue-part-1-figure-4.awebp differ
diff --git a/public/articles/vue-part-1/vue-part-1-figure-5.awebp b/public/articles/vue-part-1/vue-part-1-figure-5.awebp
new file mode 100644
index 0000000..7403a4e
Binary files /dev/null and b/public/articles/vue-part-1/vue-part-1-figure-5.awebp differ
diff --git a/public/articles/vue-part-1/vue-part-1-figure-6.awebp b/public/articles/vue-part-1/vue-part-1-figure-6.awebp
new file mode 100644
index 0000000..b249e99
Binary files /dev/null and b/public/articles/vue-part-1/vue-part-1-figure-6.awebp differ
diff --git a/public/articles/vue-part-1/vue-part-1-figure-7.awebp b/public/articles/vue-part-1/vue-part-1-figure-7.awebp
new file mode 100644
index 0000000..a7caa43
Binary files /dev/null and b/public/articles/vue-part-1/vue-part-1-figure-7.awebp differ
diff --git a/public/articles/vue-part-2/vue-part-2-figure-1.awebp b/public/articles/vue-part-2/vue-part-2-figure-1.awebp
new file mode 100644
index 0000000..973bb56
Binary files /dev/null and b/public/articles/vue-part-2/vue-part-2-figure-1.awebp differ
diff --git a/public/fly-signal.png b/public/fly-signal.png
deleted file mode 100644
index 6ed2363..0000000
Binary files a/public/fly-signal.png and /dev/null differ
diff --git a/src/app/globals.css b/src/app/globals.css
index 72df76b..bd2be60 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -1,8 +1,8 @@
@import "tailwindcss";
:root {
- --background: #101411;
- --foreground: #f4efe4;
+ --background: #05050b;
+ --foreground: #ffffff;
}
@theme inline {
@@ -18,38 +18,292 @@
}
html {
+ background: var(--background);
+ overflow-x: clip;
+ overscroll-behavior-y: none;
scroll-behavior: smooth;
}
body {
margin: 0;
+ min-height: 100dvh;
+ overflow-x: clip;
background: var(--background);
color: var(--foreground);
font-family: var(--font-body), sans-serif;
}
::selection {
- background: #d3ff30;
- color: #101411;
+ background: #22d3ee;
+ color: #05050b;
}
a {
text-decoration: none;
}
-.noise-layer {
+.tech-doc-content {
+ animation: doc-rise 720ms ease both;
+ contain: paint;
+ max-width: 100%;
+ color: rgba(255, 255, 255, 0.74);
+ font-size: 16px;
+ line-height: 2;
+ overflow-wrap: anywhere;
+}
+
+.tech-doc-toc a {
+ text-decoration: none;
+}
+
+.tech-doc-toc div::-webkit-scrollbar,
+.tech-doc-content pre::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+}
+
+.tech-doc-toc div::-webkit-scrollbar-thumb,
+.tech-doc-content pre::-webkit-scrollbar-thumb {
+ border-radius: 999px;
+ background: rgba(103, 232, 249, 0.38);
+}
+
+.tech-doc-content::before {
+ content: "";
+ display: block;
+ height: 3px;
+ margin-bottom: 28px;
+ border-radius: 999px;
+ background: linear-gradient(90deg, #22d3ee, #a855f7, #f0abfc, #84cc16);
+ box-shadow: 0 0 26px rgba(34, 211, 238, 0.42);
+}
+
+.tech-doc-content :where(h3, h4) {
+ margin: 42px 0 16px;
+ scroll-margin-top: 32px;
+ color: #ffffff;
+ font-weight: 900;
+ line-height: 1.35;
+}
+
+.tech-doc-content h3 {
+ font-size: clamp(1.45rem, 2vw, 2rem);
+ background: linear-gradient(90deg, #67e8f9, #f0abfc 58%, #bef264);
+ -webkit-background-clip: text;
+ background-clip: text;
+ color: transparent;
+ text-shadow: 0 0 30px rgba(34, 211, 238, 0.2);
+}
+
+.tech-doc-content h4 {
+ font-size: 1.05rem;
+ color: #a5f3fc;
+}
+
+.tech-doc-content :where(p, ul, ol, blockquote, pre) {
+ margin: 16px 0;
+}
+
+.tech-doc-content :where(ul, ol) {
+ padding-left: 1.35rem;
+}
+
+.tech-doc-content li {
+ margin: 8px 0;
+ padding-left: 0.2rem;
+}
+
+.tech-doc-content li::marker {
+ color: #67e8f9;
+ font-weight: 800;
+}
+
+.tech-doc-content a {
+ color: #67e8f9;
+ border-bottom: 1px solid rgba(103, 232, 249, 0.35);
+ transition:
+ color 160ms ease,
+ border-color 160ms ease,
+ text-shadow 160ms ease;
+}
+
+.tech-doc-content a:hover {
+ color: #f0abfc;
+ border-color: rgba(240, 171, 252, 0.75);
+ text-shadow: 0 0 18px rgba(240, 171, 252, 0.35);
+}
+
+.tech-doc-content strong {
+ color: #ffffff;
+ font-weight: 900;
+}
+
+.tech-doc-content code {
+ border: 1px solid rgba(103, 232, 249, 0.18);
+ border-radius: 8px;
+ background: rgba(34, 211, 238, 0.1);
+ color: #a5f3fc;
+ font-family: var(--font-code), monospace;
+ font-size: 0.9em;
+ padding: 0.1rem 0.38rem;
+}
+
+.tech-doc-content pre {
+ position: relative;
+ max-width: 100%;
+ min-width: 0;
+ width: 100%;
+ overflow-x: auto;
+ border: 1px solid rgba(45, 212, 191, 0.28);
+ border-radius: 20px;
+ padding-top: 2.75rem;
+ background:
+ linear-gradient(180deg, rgba(15, 23, 42, 0.96) 0 2.75rem, transparent 2.75rem),
+ radial-gradient(circle at 16% 0%, rgba(45, 212, 191, 0.2), transparent 28%),
+ radial-gradient(circle at 88% 8%, rgba(14, 165, 233, 0.22), transparent 26%),
+ #020617;
+ box-shadow:
+ inset 0 1px 0 rgba(255, 255, 255, 0.08),
+ inset 0 0 0 1px rgba(255, 255, 255, 0.03),
+ 0 16px 46px rgba(2, 6, 23, 0.42),
+ 0 0 24px rgba(45, 212, 191, 0.08);
+}
+
+.tech-doc-content pre::before {
+ content: attr(data-language);
+ position: absolute;
+ left: 1.1rem;
+ top: 0.9rem;
+ color: #5eead4;
+ font-family: var(--font-code), monospace;
+ font-size: 0.72rem;
+ font-weight: 800;
+ letter-spacing: 0.16em;
+}
+
+.tech-doc-code-copy {
+ position: absolute;
+ right: 0.85rem;
+ top: 0.56rem;
+ z-index: 1;
+ width: 4.1rem;
+ border: 1px solid rgba(103, 232, 249, 0.38);
+ border-radius: 999px;
+ background: rgba(8, 47, 73, 0.72);
+ color: #cffafe;
+ cursor: pointer;
+ font-family: var(--font-body), sans-serif;
+ font-size: 0.78rem;
+ font-weight: 800;
+ line-height: 1;
+ padding: 0.52rem 0.72rem;
+ transition:
+ background 160ms ease,
+ border-color 160ms ease,
+ box-shadow 160ms ease,
+ color 160ms ease;
+}
+
+.tech-doc-code-copy:hover,
+.tech-doc-code-copy[data-copied="true"] {
+ border-color: rgba(94, 234, 212, 0.9);
+ background: rgba(20, 184, 166, 0.22);
+ color: #ffffff;
+ box-shadow: 0 0 24px rgba(45, 212, 191, 0.26);
+}
+
+.tech-doc-content pre code {
+ display: block;
+ width: max-content;
+ min-width: max-content;
+ max-width: none;
+ border: 0;
+ border-radius: 0;
+ background: transparent;
+ color: rgba(255, 255, 255, 0.82);
+ font-size: 0.88rem;
+ line-height: 1.8;
+ padding: 0 1.25rem 1.25rem;
+ white-space: pre;
+}
+
+.tech-doc-content img {
+ width: 100%;
+ max-width: 100%;
+ height: auto;
+ object-fit: contain;
+ margin: 24px 0;
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ border-radius: 18px;
+ background: rgba(255, 255, 255, 0.04);
+ box-shadow: 0 18px 52px rgba(34, 211, 238, 0.12);
+}
+
+.tech-doc-content blockquote {
+ border-left: 3px solid #22d3ee;
+ border-radius: 14px;
+ background: rgba(34, 211, 238, 0.08);
+ color: rgba(255, 255, 255, 0.78);
+ padding: 0.8rem 1rem;
+}
+
+.tech-doc-content hr {
+ height: 1px;
+ margin: 32px 0;
+ border: 0;
+ background: linear-gradient(90deg, transparent, rgba(103, 232, 249, 0.45), transparent);
+}
+
+.neon-aurora {
+ position: absolute;
+ inset: -18% -10% auto -10%;
+ z-index: -3;
+ height: 72%;
+ pointer-events: none;
+ background:
+ radial-gradient(circle at 12% 24%, rgba(34, 211, 238, 0.44), transparent 28%),
+ radial-gradient(circle at 48% 8%, rgba(99, 102, 241, 0.36), transparent 30%),
+ radial-gradient(circle at 86% 32%, rgba(217, 70, 239, 0.42), transparent 28%),
+ radial-gradient(circle at 74% 72%, rgba(132, 204, 22, 0.18), transparent 24%);
+ filter: blur(12px) saturate(1.18);
+ transform: translateZ(0);
+}
+
+.cyber-grid {
+ position: absolute;
+ inset: 0;
+ z-index: -2;
+ pointer-events: none;
+ background-image:
+ linear-gradient(rgba(34, 211, 238, 0.08) 1px, transparent 1px),
+ linear-gradient(90deg, rgba(217, 70, 239, 0.08) 1px, transparent 1px);
+ background-size: 64px 64px;
+ mask-image: linear-gradient(to bottom, black 0%, black 56%, transparent 100%);
+}
+
+.scanline {
position: absolute;
inset: 0;
z-index: -1;
pointer-events: none;
- opacity: 0.2;
- background-image:
- radial-gradient(circle at 18% 30%, rgba(244, 239, 228, 0.18) 0 1px, transparent 1px),
- radial-gradient(circle at 72% 64%, rgba(211, 255, 48, 0.14) 0 1px, transparent 1px);
- background-size: 34px 34px, 46px 46px;
+ opacity: 0.18;
+ background-image: linear-gradient(to bottom, rgba(255, 255, 255, 0.1) 1px, transparent 1px);
+ background-size: 100% 7px;
mix-blend-mode: screen;
}
+@keyframes doc-rise {
+ from {
+ opacity: 0;
+ transform: translateY(18px);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
@media (prefers-reduced-motion: reduce) {
*,
*::before,
diff --git a/src/app/life/page.tsx b/src/app/life/page.tsx
new file mode 100644
index 0000000..dcc690e
--- /dev/null
+++ b/src/app/life/page.tsx
@@ -0,0 +1,12 @@
+import { PlaceholderPage } from "@/components/placeholder-page";
+
+export default function LifePage() {
+ return (
+
+ {article.description} +
+{article.description}
+ ++ {description} +
+ + ++ launch sequence +
++ all dossiers +
++ {listDescription} +
+{item.label}
-{item.value}
-signal
-- A new home for notes, projects, experiments, and the quiet parts of making things. -
-What this site becomes
-Stack and rhythm
-- 首页先完成品牌感和结构感。后续可以接入 MDX 博客、项目详情页、关于页和部署流程,把旧 VitePress 的内容慢慢迁过来。 -
-{module.description}
+© 2026 fly. Built with Next.js, Tailwind CSS and Motion.
+ + Gitea repository +{eyebrow}
+{description}
++ {String(index + 1).padStart(2, "0")} +
+{label}
+{point.note}
+ ) : null} + {point.children?.length ? ( +XSS 攻击指的是跨站脚本攻击,是一种代码注入攻击。攻击者通过在网站注入恶意脚本,使之在用户的浏览器上运行,从而盗取用户的信息如 cookie 等。
+XSS 的本质是因为网站没有对恶意代码进行过滤,与正常的代码混合在一起了,浏览器没有办法分辨哪些脚本是可信的,从而导致了恶意代码的执行。
+攻击者可以通过这种攻击方式可以进行以下操作:
+XSS 可以分为存储型、反射型和 DOM 型:
+1)存储型 XSS 的攻击步骤:
+这种攻击常⻅于带有⽤户保存数据的⽹站功能,如论坛发帖、商品评论、⽤户私信等。
+2)反射型 XSS 的攻击步骤:
+反射型 XSS 跟存储型 XSS 的区别是:存储型 XSS 的恶意代码存在数据库⾥,反射型 XSS 的恶意代码存在 URL ⾥。
+反射型 XSS 漏洞常⻅于通过 URL 传递参数的功能,如⽹站搜索、跳转等。 由于需要⽤户主动打开恶意的 URL 才能⽣效,攻击者往往会结合多种⼿段诱导⽤户点击。
+3)DOM 型 XSS 的攻击步骤:
+DOM 型 XSS 跟前两种 XSS 的区别:DOM 型 XSS 攻击中,取出和执⾏恶意代码由浏览器端完成,属于前端JavaScript ⾃身的安全漏洞,⽽其他两种 XSS 都属于服务端的安全漏洞。
+可以看到XSS危害如此之大, 那么在开发网站时就要做好防御措施,具体措施如下:
++++
+- CSP 指的是内容安全策略,它的本质是建立一个白名单,告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截由浏览器自己来实现。
+- 通常有两种方式来开启 CSP,一种是设置 HTTP 首部中的 Content-Security-Policy,一种是设置 meta 标签的方式
+
CSRF 攻击指的是跨站请求伪造攻击,攻击者诱导用户进入一个第三方网站,然后该网站向被攻击网站发送跨站请求。如果用户在被攻击网站中保存了登录状态,那么攻击者就可以利用这个登录状态,绕过后台的用户验证,冒充用户向服务器执行一些操作。
+CSRF 攻击的本质是利用 cookie 会在同源请求中携带发送给服务器的特点,以此来实现用户的冒充。
+常见的 CSRF 攻击有三种:
+CSRF 攻击可以使用以下方法来防护:
+中间⼈ (Man-in-the-middle attack, MITM) 是指攻击者与通讯的两端分别创建独⽴的联系, 并交换其所收到的数据, 使通讯的两端认为他们正在通过⼀个私密的连接与对⽅直接对话, 但事实上整个会话都被攻击者完全控制。在中间⼈攻击中,攻击者可以拦截通讯双⽅的通话并插⼊新的内容。
+攻击过程如下:
+⽹络劫持分为两种:
+(1)DNS劫持: (输⼊京东被强制跳转到淘宝这就属于dns劫持)
+(2)HTTP劫持: (访问⾕歌但是⼀直有贪玩蓝⽉的⼴告),由于http明⽂传输,运营商会修改你的http响应内容(即加⼴告)
+DNS劫持由于涉嫌违法,已经被监管起来,现在很少会有DNS劫持,⽽http劫持依然⾮常盛⾏,最有效的办法就是全站HTTPS,将HTTP加密,这使得运营商⽆法获取明⽂,就⽆法劫持你的响应内容。
+从本质上说,进程和线程都是 CPU 工作时间片的一个描述:
+进程是资源分配的最小单位,线程是CPU调度的最小单位。
+一个进程就是一个程序的运行实例。详细解释就是,启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程。进程是运行在虚拟内存上的,虚拟内存是用来解决用户对硬件资源的无限需求和有限的硬件资源之间的矛盾的。从操作系统角度来看,虚拟内存即交换文件;从处理器角度看,虚拟内存即虚拟地址空间。
+如果程序很多时,内存可能会不够,操作系统为每个进程提供一套独立的虚拟地址空间,从而使得同一块物理内存在不同的进程中可以对应到不同或相同的虚拟地址,变相的增加了程序可以使用的内存。
+进程和线程之间的关系有以下四个特点:
+(1)进程中的任意一线程执行出错,都会导致整个进程的崩溃。
+(2)线程之间共享进程中的数据。
+(3)当一个进程关闭之后,操作系统会回收进程所占用的内存, 当一个进程退出时,操作系统会回收该进程所申请的所有资源;即使其中任意线程因为操作不当导致内存泄漏,当进程退出时,这些内存也会被正确回收。
+(4)进程之间的内容相互隔离。 进程隔离就是为了使操作系统中的进程互不干扰,每一个进程只能访问自己占有的数据,也就避免出现进程 A 写入数据到进程 B 的情况。正是因为进程之间的数据是严格隔离的,所以一个进程如果崩溃了,或者挂起了,是不会影响到其他进程的。如果进程之间需要进行数据的通信,这时候,就需要使用用于进程间通信的机制了。
+Chrome浏览器的架构图:
+
+从图中可以看出,最新的 Chrome 浏览器包括:
这些进程的功能:
+所以,打开一个网页,最少需要四个进程:1 个网络进程、1 个浏览器进程、1 个 GPU 进程以及 1 个渲染进程。如果打开的页面有运行插件的话,还需要再加上 1 个插件进程。
+虽然多进程模型提升了浏览器的稳定性、流畅性和安全性,但同样不可避免地带来了一些问题:
+浏览器的渲染进程的线程总共有五种:
+
+(1)GUI渲染线程
+负责渲染浏览器页面,解析HTML、CSS,构建DOM树、构建CSSOM树、构建渲染树和绘制页面;当界面需要重绘或由于某种操作引发回流时,该线程就会执行。
注意:GUI渲染线程和JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
+(2)JS引擎线程 +JS引擎线程也称为JS内核,负责处理Javascript脚本程序,解析Javascript脚本,运行代码;JS引擎线程一直等待着任务队列中任务的到来,然后加以处理,一个Tab页中无论什么时候都只有一个JS引擎线程在运行JS程序;
+注意:GUI渲染线程与JS引擎线程的互斥关系,所以如果JS执行的时间过长,会造成页面的渲染不连贯,导致页面渲染加载阻塞。
+(3)时间触发线程 +时间触发线程属于浏览器而不是JS引擎,用来控制事件循环;当JS引擎执行代码块如setTimeOut时(也可是来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件触发线程中;当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理;
+注意:由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行);
+(4)定时器触发进程 +定时器触发进程即setInterval与setTimeout所在线程;浏览器定时计数器并不是由JS引擎计数的,因为JS引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确性;因此使用单独线程来计时并触发定时器,计时完毕后,添加到事件队列中,等待JS引擎空闲后执行,所以定时器中的任务在设定的时间点不一定能够准时执行,定时器只是在指定时间点将任务添加到事件队列中;
+注意:W3C在HTML标准中规定,定时器的定时时间不能小于4ms,如果是小于4ms,则默认为4ms。
+(5)异步http请求线程
+(1)管道通信
+管道是一种最基本的进程间通信机制。管道就是操作系统在内核中开辟的一段缓冲区,进程1可以将需要交互的数据拷贝到这段缓冲区,进程2就可以读取了。
+管道的特点:
+(2)消息队列通信
+消息队列就是一个消息的列表。用户可以在消息队列中添加消息、读取消息等。消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。 每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型的数据结构。可以通过发送消息来避免命名管道的同步和阻塞问题。但是消息队列与命名管道一样,每个数据块都有一个最大长度的限制。
+使用消息队列进行进程间通信,可能会收到数据块最大长度的限制约束等,这也是这种通信方式的缺点。如果频繁的发生进程间的通信行为,那么进程需要频繁地读取队列中的数据到内存,相当于间接地从一个进程拷贝到另一个进程,这需要花费时间。
+(3)信号量通信
+共享内存最大的问题就是多进程竞争内存的问题,就像类似于线程安全问题。我们可以使用信号量来解决这个问题。信号量的本质就是一个计数器,用来实现进程之间的互斥与同步。例如信号量的初始值是 1,然后 a 进程来访问内存1的时候,我们就把信号量的值设为 0,然后进程b 也要来访问内存1的时候,看到信号量的值为 0 就知道已经有进程在访问内存1了,这个时候进程 b 就会访问不了内存1。所以说,信号量也是进程之间的一种通信方式。
+(4)信号通信
+信号(Signals )是Unix系统中使用的最古老的进程间通信的方法之一。操作系统通过信号来通知进程系统中发生了某种预先规定好的事件(一组事件中的一个),它也是用户进程之间通信和同步的一种原始机制。
+(5)共享内存通信
+共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问(使多个进程可以访问同一块内存空间)。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
+(6)套接字通信
+上面说的共享内存、管道、信号量、消息队列,他们都是多个进程在一台主机之间的通信,那两个相隔几千里的进程能够进行通信吗?答是必须的,这个时候 Socket 这家伙就派上用场了,例如我们平时通过浏览器发起一个 http 请求,然后服务器给你返回对应的数据,这种就是采用 Socket 的通信方式了。
+所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。
+系统中的资源可以分为两类:
+产生死锁的原因:
+(1)竞争资源
+(2)进程间推进顺序非法
+若P1保持了资源R1,P2保持了资源R2,系统处于不安全状态,因为这两个进程再向前推进,便可能发生死锁。例如,当P1运行到P1:Request(R2)时,将因R2已被P2占用而阻塞;当P2运行到P2:Request(R1)时,也将因R1已被P1占用而阻塞,于是发生进程死锁
+产生死锁的必要条件:
+预防死锁的方法:
+实现多个标签页之间的通信,本质上都是通过中介者模式来实现的。因为标签页之间没有办法直接通信,因此我们可以找一个中介者,让标签页和中介者进行通信,然后让这个中介者来进行消息的转发。通信方法如下:
+Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。
+Service Worker 实现缓存功能一般分为三个步骤:首先需要先注册 Service Worker,然后监听到 install 事件以后就可以缓存需要的文件,那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。以下是这个步骤的实现:
// index.js
+if (navigator.serviceWorker) {
+ navigator.serviceWorker
+ .register('sw.js')
+ .then(function(registration) {
+ console.log('service worker 注册成功')
+ })
+ .catch(function(err) {
+ console.log('servcie worker 注册失败')
+ })
+}
+// sw.js
+// 监听 `install` 事件,回调中缓存所需文件
+self.addEventListener('install', e => {
+ e.waitUntil(
+ caches.open('my-cache').then(function(cache) {
+ return cache.addAll(['./index.html', './index.js'])
+ })
+ )
+})
+// 拦截所有请求事件
+// 如果缓存中已经有请求的数据就直接用缓存,否则去请求数据
+self.addEventListener('fetch', e => {
+ e.respondWith(
+ caches.match(e.request).then(function(response) {
+ if (response) {
+ return response
+ }
+ console.log('fetch source')
+ })
+ )
+})
+打开页面,可以在开发者工具中的 Application 看到 Service Worker 已经启动了:
+
+在 Cache 中也可以发现所需的文件已被缓存:
+
浏览器缓存的全过程:
+
+很多网站的资源后面都加了版本号,这样做的目的是:每次升级了 JS 或 CSS 文件后,为了防止浏览器进行缓存,强制改变版本号,客户端浏览器就会重新下载新的 JS 或 CSS 文件 ,以保证用户能够及时获得网站的最新更新。
使用强缓存策略时,如果缓存资源有效,则直接使用缓存资源,不必再向服务器发起请求。
+强缓存策略可以通过两种方式来设置,分别是 http 头信息中的 Expires 属性和 Cache-Control 属性。
+(1)服务器通过在响应头中添加 Expires 属性,来指定资源的过期时间。在过期时间以内,该资源可以被缓存使用,不必再向服务器发送请求。这个时间是一个绝对时间,它是服务器的时间,因此可能存在这样的问题,就是客户端的时间和服务器端的时间不一致,或者用户可以对客户端时间进行修改的情况,这样就可能会影响缓存命中的结果。
+(2)Expires 是 http1.0 中的方式,因为它的一些缺点,在 HTTP 1.1 中提出了一个新的头部属性就是 Cache-Control 属性,它提供了对资源的缓存的更精确的控制。它有很多不同的值,
+Cache-Control可设置的字段:
public:设置了该字段值的资源表示可以被任何对象(包括:发送请求的客户端、代理服务器等等)缓存。这个字段值不常用,一般还是使用max-age=来精确控制;private:设置了该字段值的资源只能被用户浏览器缓存,不允许任何代理服务器缓存。在实际开发当中,对于一些含有用户信息的HTML,通常都要设置这个字段值,避免代理服务器(CDN)缓存;no-cache:设置了该字段需要先和服务端确认返回的资源是否发生了变化,如果资源未发生变化,则直接使用缓存好的资源;no-store:设置了该字段表示禁止任何缓存,每次都会向服务端发起新的请求,拉取最新的资源;max-age=:设置缓存的最大有效期,单位为秒;s-maxage=:优先级高于max-age=,仅适用于共享缓存(CDN),优先级高于max-age或者Expires头;max-stale[=]:设置了该字段表明客户端愿意接收已经过期的资源,但是不能超过给定的时间限制。一般来说只需要设置其中一种方式就可以实现强缓存策略,当两种方式一起使用时,Cache-Control 的优先级要高于 Expires。
+no-cache和no-store很容易混淆:
+如果命中强制缓存,我们无需发起新的请求,直接使用缓存内容,如果没有命中强制缓存,如果设置了协商缓存,这个时候协商缓存就会发挥作用了。
+上面已经说到了,命中协商缓存的条件有两个:
+max-age=xxx 过期了no-store使用协商缓存策略时,会先向服务器发送一个请求,如果资源没有发生修改,则返回一个 304 状态,让浏览器使用本地的缓存副本。如果资源发生了修改,则返回修改后的资源。
+协商缓存也可以通过两种方式来设置,分别是 http 头信息中的Etag 和Last-Modified属性。
+(1)服务器通过在响应头中添加 Last-Modified 属性来指出资源最后一次修改的时间,当浏览器下一次发起请求时,会在请求头中添加一个 If-Modified-Since 的属性,属性值为上一次资源返回时的 Last-Modified 的值。当请求发送到服务器后服务器会通过这个属性来和资源的最后一次的修改时间来进行比较,以此来判断资源是否做了修改。如果资源没有修改,那么返回 304 状态,让客户端使用本地的缓存。如果资源已经被修改了,则返回修改后的资源。使用这种方法有一个缺点,就是 Last-Modified 标注的最后修改时间只能精确到秒级,如果某些文件在1秒钟以内,被修改多次的话,那么文件已将改变了但是 Last-Modified 却没有改变,这样会造成缓存命中的不准确。
+(2)因为 Last-Modified 的这种可能发生的不准确性,http 中提供了另外一种方式,那就是 Etag 属性。服务器在返回资源的时候,在头信息中添加了 Etag 属性,这个属性是资源生成的唯一标识符,当资源发生改变的时候,这个值也会发生改变。在下一次资源请求时,浏览器会在请求头中添加一个 If-None-Match 属性,这个属性的值就是上次返回的资源的 Etag 的值。服务接收到请求后会根据这个值来和资源当前的 Etag 的值来进行比较,以此来判断资源是否发生改变,是否需要返回资源。通过这种方式,比 Last-Modified 的方式更加精确。
+当 Last-Modified 和 Etag 属性同时出现的时候,Etag 的优先级更高。使用协商缓存的时候,服务器需要考虑负载平衡的问题,因此多个服务器上资源的 Last-Modified 应该保持一致,因为每个服务器上 Etag 的值都不一样,因此在考虑负载平衡时,最好不要设置 Etag 属性。
+总结:
+强缓存策略和协商缓存策略在缓存命中时都会直接使用本地的缓存副本,区别只在于协商缓存会向服务器发送一次请求。它们缓存不命中时,都会向服务器发送请求来获取资源。在实际的缓存机制中,强缓存策略和协商缓存策略是一起合作使用的。浏览器首先会根据请求的信息判断,强缓存是否命中,如果命中则直接使用资源。如果不命中则根据头信息向服务器发起请求,使用协商缓存,如果协商缓存命中的话,则服务器不返回资源,浏览器直接使用本地资源的副本,如果协商缓存不命中,则浏览器返回最新的资源给浏览器。
+对于浏览器的缓存,主要针对的是前端的静态资源,最好的效果就是,在发起请求之后,拉取相应的静态资源,并保存在本地。如果服务器的静态资源没有更新,那么在下次请求的时候,就直接从本地读取即可,如果服务器的静态资源已经更新,那么我们再次请求的时候,就到服务器拉取新的资源,并保存在本地。这样就大大的减少了请求的次数,提高了网站的性能。这就要用到浏览器的缓存策略了。
+所谓的浏览器缓存指的是浏览器将用户请求过的静态资源,存储到电脑本地磁盘中,当浏览器再次访问时,就可以直接从本地加载,不需要再去服务端请求了。
+使用浏览器缓存,有以下优点:
+浏览器的主要功能是将用户选择的 web 资源呈现出来,它需要从服务器请求资源,并将其显示在浏览器窗口中,资源的格式通常是 HTML,也包括 PDF、image 及其他格式。用户用 URI(Uniform Resource Identifier 统一资源标识符)来指定所请求资源的位置。
+HTML 和 CSS 规范中规定了浏览器解释 html 文档的方式,由 W3C 组织对这些规范进行维护,W3C 是负责制定 web 标准的组织。但是浏览器厂商纷纷开发自己的扩展,对规范的遵循并不完善,这为 web 开发者带来了严重的兼容性问题。
+浏览器可以分为两部分,shell 和 内核。其中 shell 的种类相对比较多,内核则比较少。也有一些浏览器并不区分外壳和内核。从 Mozilla 将 Gecko 独立出来后,才有了外壳和内核的明确划分。
+浏览器内核主要分成两部分:
+最开始渲染引擎和 JS 引擎并没有区分的很明确,后来 JS 引擎越来越独立,内核就倾向于只指渲染引擎。
+(1) IE 浏览器内核:Trident 内核,也是俗称的 IE 内核;
+(2) Chrome 浏览器内核:统称为 Chromium 内核或 Chrome 内核,以前是 Webkit 内核,现在是 Blink内核;
+(3) Firefox 浏览器内核:Gecko 内核,俗称 Firefox 内核;
+(4) Safari 浏览器内核:Webkit 内核;
+(5) Opera 浏览器内核:最初是自己的 Presto 内核,后来加入谷歌大军,从 Webkit 又到了 Blink 内核;
+(6) 360浏览器、猎豹浏览器内核:IE + Chrome 双内核;
+(7) 搜狗、遨游、QQ 浏览器内核:Trident(兼容模式)+ Webkit(高速模式);
+(8) 百度浏览器、世界之窗内核:IE 内核;
+(9) 2345浏览器内核:好像以前是 IE 内核,现在也是 IE + Chrome 双内核了;
+(10)UC 浏览器内核:这个众口不一,UC 说是他们自己研发的 U3 内核,但好像还是基于 Webkit 和 Trident ,还有说是基于火狐内核。
+值得注意的是,和⼤多数浏览器不同,Chrome 浏览器的每个标签⻚都分别对应⼀个呈现引擎实例。每个标签⻚都是⼀个独⽴的进程。
+浏览器渲染主要有以下步骤:
+大致过程如图所示:
+
注意: 这个过程是逐步完成的,为了更好的用户体验,渲染引擎将会尽可能早的将内容呈现到屏幕上,并不会等到所有的html 都解析完成之后再去构建和布局 render 树。它是解析完一部分内容就显示一部分内容,同时,可能还在通过网络下载其余内容。
+(1)针对JavaScript: JavaScript既会阻塞HTML的解析,也会阻塞CSS的解析。因此我们可以对JavaScript的加载方式进行改变,来进行优化:
+(1)尽量将JavaScript文件放在body的最后
+(2) body中间尽量不要写<script>标签
(3)<script>标签的引入资源方式有三种,有一种就是我们常用的直接引入,还有两种就是使用 async 属性和 defer 属性来异步引入,两者都是去异步加载外部的JS文件,不会阻塞DOM的解析(尽量使用异步加载)。三者的区别如下:
(2)针对CSS:使用CSS有三种方式:使用link、@import、内联样式,其中link和@import都是导入外部样式。它们之间的区别:
+外部样式如果长时间没有加载完毕,浏览器为了用户体验,会使用浏览器会默认样式,确保首次渲染的速度。所以CSS一般写在headr中,让浏览器尽快发送请求去获取css样式。
+所以,在开发过程中,导入外部样式使用link,而不用@import。如果css少,尽可能采用内嵌样式,直接写在style标签中。
+(3)针对DOM树、CSSOM树: +可以通过以下几种方式来减少渲染的时间:
+(4)减少回流与重绘:
+table布局, 一个小的改动可能会使整个table进行重新布局documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中display: none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。浏览器针对页面的回流与重绘,进行了自身的优化——渲染队列
+浏览器会将所有的回流、重绘的操作放在一个队列中,当队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会对队列进行批处理。这样就会让多次的回流、重绘变成一次回流重绘。
+将多个读操作(或者写操作)放在一起,就会等所有的读操作进入队列之后执行,这样,原本应该是触发多次回流,变成了只触发一次回流。
+JavaScript 的加载、解析与执行会阻塞文档的解析,也就是说,在构建 DOM 时,HTML 解析器若遇到了 JavaScript,那么它会暂停文档的解析,将控制权移交给 JavaScript 引擎,等 JavaScript 引擎运行完毕,浏览器再从中断的地方恢复继续解析文档。也就是说,如果想要首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的原因。当然在当下,并不是说 script 标签必须放在底部,因为你可以给 script 标签添加 defer 或者 async 属性。
+Webkit 和 Firefox 都做了这个优化,当执行 JavaScript 脚本时,另一个线程解析剩下的文档,并加载后面需要通过网络加载的资源。这种方式可以使资源并行加载从而使整体速度更快。需要注意的是,预解析并不改变 DOM 树,它将这个工作留给主解析过程,自己只解析外部资源的引用,比如外部脚本、样式表及图片。
+理论上,既然样式表不改变 DOM 树,也就没有必要停下文档的解析等待它们。然而,存在一个问题,JavaScript 脚本执行时可能在文档的解析过程中请求样式信息,如果样式还没有加载和解析,脚本将得到错误的值,显然这将会导致很多问题。所以如果浏览器尚未完成 CSSOM 的下载和构建,而我们却想在此时运行脚本,那么浏览器将延迟 JavaScript 脚本执行和文档的解析,直至其完成 CSSOM 的下载和构建。也就是说,在这种情况下,浏览器会先下载和构建 CSSOM,然后再执行 JavaScript,最后再继续文档的解析。
+为尽快完成首次渲染,我们需要最大限度减小以下三种可变因素:
+(1)关键资源的数量。
+(2)关键路径长度。
+(3)关键字节的数量。
+关键资源是可能阻止网页首次渲染的资源。这些资源越少,浏览器的工作量就越小,对 CPU 以及其他资源的占用也就越少。同样,关键路径长度受所有关键资源与其字节大小之间依赖关系图的影响:某些资源只能在上一资源处理完毕之后才能开始下载,并且资源越大,下载所需的往返次数就越多。最后,浏览器需要下载的关键字节越少,处理内容并让其出现在屏幕上的速度就越快。要减少字节数,我们可以减少资源数(将它们删除或设为非关键资源),此外还要压缩和优化各项资源,确保最大限度减小传送大小。
+优化关键渲染路径的常规步骤如下:
+(1)对关键路径进行分析和特性描述:资源数、字节数、长度。
+(2)最大限度减少关键资源的数量:删除它们,延迟它们的下载,将它们标记为异步等。
+(3)优化关键字节数以缩短下载时间(往返次数)。
+(4)优化其余关键资源的加载顺序:您需要尽早下载所有关键资产,以缩短关键路径长度
+首先渲染的前提是生成渲染树,所以 HTML 和 CSS 肯定会阻塞渲染。如果你想渲染的越快,你越应该降低一开始需要渲染的文件大小,并且扁平层级,优化选择器。然后当浏览器在解析到 script 标签时,会暂停构建 DOM,完成后才会从暂停的地方重新开始。也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的原因。
+当然在当下,并不是说 script 标签必须放在底部,因为你可以给 script 标签添加 defer 或者 async 属性。当 script 标签加上 defer 属性以后,表示该 JS 文件会并行下载,但是会放到 HTML 解析完成后顺序执行,所以对于这种情况你可以把 script 标签放在任意位置。对于没有任何依赖的 JS 文件可以加上 async 属性,表示 JS 文件下载和解析不会阻塞渲染。
+Cookie是最早被提出来的本地存储方式,在此之前,服务端是无法判断网络中的两个请求是否是同一用户发起的,为解决这个问题,Cookie就出现了。Cookie的大小只有4kb,它是一种纯文本文件,每次发起HTTP请求都会携带Cookie。
+Cookie的特性:
+如果需要域名之间跨域共享Cookie,有两种方法:
+Cookie的使用场景:
+LocalStorage是HTML5新引入的特性,由于有的时候我们存储的信息较大,Cookie就不能满足我们的需求,这时候LocalStorage就派上用场了。
+LocalStorage的优点:
+LocalStorage的缺点:
+LocalStorage的常用API:
+// 保存数据到 localStorage
+localStorage.setItem('key', 'value');
+
+// 从 localStorage 获取数据
+let data = localStorage.getItem('key');
+
+// 从 localStorage 删除保存的数据
+localStorage.removeItem('key');
+
+// 从 localStorage 删除所有保存的数据
+localStorage.clear();
+
+// 获取某个索引的Key
+localStorage.key(index)
+LocalStorage的使用场景:
+SessionStorage和LocalStorage都是在HTML5才提出来的存储方案,SessionStorage 主要用于临时保存同一窗口(或标签页)的数据,刷新页面时不会删除,关闭窗口或标签页之后将会删除这些数据。
+SessionStorage与LocalStorage对比:
+SessionStorage的常用API:
+// 保存数据到 sessionStorage
+sessionStorage.setItem('key', 'value');
+
+// 从 sessionStorage 获取数据
+let data = sessionStorage.getItem('key');
+
+// 从 sessionStorage 删除保存的数据
+sessionStorage.removeItem('key');
+
+// 从 sessionStorage 删除所有保存的数据
+sessionStorage.clear();
+
+// 获取某个索引的Key
+sessionStorage.key(index)
+SessionStorage的使用场景
+Cookie由以下字段组成:
+/test,那么只有/test路径下的页面可以读取此cookie。HTTPOnly 属性 ,该属性用来设置cookie能否通过脚本来访问,默认为空,即可以通过脚本访问。在客户端是不能通过js代码去设置一个httpOnly类型的cookie的,这种类型的cookie只能通过服务端来设置。该属性用于防止客户端脚本通过document.cookie属性访问Cookie,有助于保护Cookie不被跨站脚本攻击窃取或篡改。但是,HTTPOnly的应用仍存在局限性,一些浏览器可以阻止客户端脚本对Cookie的读操作,但允许写操作;此外大多数浏览器仍允许通过XMLHTTP对象读取HTTP响应中的Set-Cookie头。总结: +服务器端可以使用 Set-Cookie 的响应头部来配置 cookie 信息。一条cookie 包括了5个属性值 expires、domain、path、secure、HttpOnly。其中 expires 指定了 cookie 失效的时间,domain 是域名、path是路径,domain 和 path 一起限制了 cookie 能够被哪些 url 访问。secure 规定了 cookie 只能在确保安全的情况下传输,HttpOnly 规定了这个 cookie 只能被服务器访问,不能使用 js 脚本访问。
+浏览器端常用的存储技术是 cookie 、localStorage 和 sessionStorage。
+上面几种方式都是存储少量数据的时候的存储方式,当需要在本地存储大量数据的时候,我们可以使用浏览器的 indexDB 这是浏览器提供的一种本地的数据库存储机制。它不是关系型数据库,它内部采用对象仓库的形式存储数据,它更接近 NoSQL 数据库。
+IndexedDB 具有以下特点:
+跨域问题其实就是浏览器的同源策略造成的。
+++同源策略限制了从同一个源加载的文档或脚本如何与另一个源的资源进行交互。这是浏览器的一个用于隔离潜在恶意文件的重要的安全机制。同源指的是:协议、端口号、域名必须一致。
+
下表给出了与 URL store.company.com/dir/page.ht… 的源进行对比的示例:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +| URL | 是否跨域 | 原因 |
|---|---|---|
| store.company.com/dir/page.ht… | 同源 | 完全相同 |
| store.company.com/dir/inner/a… | 同源 | 只有路径不同 |
| store.company.com/secure.html | 跨域 | 协议不同 |
| store.company.com:81/dir/etc.htm… | 跨域 | 端口不同 ( http:// 默认端口是80) |
| news.company.com/dir/other.h… | 跨域 | 主机不同 |
同源策略:protocol(协议)、domain(域名)、port(端口)三者必须一致。
+同源政策主要限制了三个方面:
+同源政策的目的主要是为了保证用户的信息安全,它只是对 js 脚本的一种限制,并不是对浏览器的限制,对于一般的 img、或者script 脚本请求都不会有跨域的限制,这是因为这些操作都不会通过响应结果来进行可能出现安全问题的操作。
+下面是MDN对于CORS的定义:
+++跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain)上的Web应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域HTTP 请求。
+
CORS需要浏览器和服务器同时支持,整个CORS过程都是浏览器完成的,无需用户参与。因此实现CORS的关键就是服务器,只要服务器实现了CORS请求,就可以跨源通信了。
+浏览器将CORS分为简单请求和非简单请求:
+简单请求不会触发CORS预检请求。若该请求满足以下两个条件,就可以看作是简单请求:
+1)请求方法是以下三种方法之一:
+2)HTTP的头信息不超出以下几种字段:
+若不满足以上条件,就属于非简单请求了。
+(1)简单请求过程:
+对于简单请求,浏览器会直接发出CORS请求,它会在请求的头信息中增加一个Orign字段,该字段用来说明本次请求来自哪个源(协议+端口+域名),服务器会根据这个值来决定是否同意这次请求。如果Orign指定的域名在许可范围之内,服务器返回的响应就会多出以下信息头:
+Access-Control-Allow-Origin: http://api.bob.com // 和Orign一直
+Access-Control-Allow-Credentials: true // 表示是否允许发送Cookie
+Access-Control-Expose-Headers: FooBar // 指定返回其他字段的值
+Content-Type: text/html; charset=utf-8 // 表示文档类型
+如果Orign指定的域名不在许可范围之内,服务器会返回一个正常的HTTP回应,浏览器发现没有上面的Access-Control-Allow-Origin头部信息,就知道出错了。这个错误无法通过状态码识别,因为返回的状态码可能是200。
+在简单请求中,在服务器内,至少需要设置字段:Access-Control-Allow-Origin
(2)非简单请求过程
+非简单请求是对服务器有特殊要求的请求,比如请求方法为DELETE或者PUT等。非简单请求的CORS请求会在正式通信之前进行一次HTTP查询请求,称为预检请求。
+浏览器会询问服务器,当前所在的网页是否在服务器允许访问的范围内,以及可以使用哪些HTTP请求方式和头信息字段,只有得到肯定的回复,才会进行正式的HTTP请求,否则就会报错。
+预检请求使用的请求方法是OPTIONS,表示这个请求是来询问的。他的头信息中的关键字段是Orign,表示请求来自哪个源。除此之外,头信息中还包括两个字段:
+服务器在收到浏览器的预检请求之后,会根据头信息的三个字段来进行判断,如果返回的头信息在中有Access-Control-Allow-Origin这个字段就是允许跨域请求,如果没有,就是不同意这个预检请求,就会报错。
+服务器回应的CORS的字段如下:
+Access-Control-Allow-Origin: http://api.bob.com // 允许跨域的源地址
+Access-Control-Allow-Methods: GET, POST, PUT // 服务器支持的所有跨域请求的方法
+Access-Control-Allow-Headers: X-Custom-Header // 服务器支持的所有头信息字段
+Access-Control-Allow-Credentials: true // 表示是否允许发送Cookie
+Access-Control-Max-Age: 1728000 // 用来指定本次预检请求的有效期,单位为秒
+只要服务器通过了预检请求,在以后每次的CORS请求都会自带一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。
+在非简单请求中,至少需要设置以下字段:
+'Access-Control-Allow-Origin'
+'Access-Control-Allow-Methods'
+'Access-Control-Allow-Headers'
+OPTIONS请求次数过多就会损耗页面加载的性能,降低用户体验度。所以尽量要减少OPTIONS请求次数,可以后端在请求的返回头部添加:Access-Control-Max-Age:number。它表示预检请求的返回结果可以被缓存多久,单位是秒。该字段只对完全一样的URL的缓存设置生效,所以设置了缓存时间,在这个时间范围内,再次发送请求就不需要进行预检请求了。
+在CORS请求中,如果想要传递Cookie,就要满足以下三个条件:
+withCredentials默认情况下在跨域请求,浏览器是不带 cookie 的。但是我们可以通过设置 withCredentials 来进行传递 cookie.
+// 原生 xml 的设置方式
+var xhr = new XMLHttpRequest();
+xhr.withCredentials = true;
+// axios 设置方式
+axios.defaults.withCredentials = true;
+*jsonp的原理就是利用<script>标签没有跨域限制,通过<script>标签src属性,发送带有callback参数的GET请求,服务端将接口返回数据拼凑到callback函数中,返回给浏览器,浏览器解析执行,从而前端拿到callback函数返回的数据。
+1)原生JS实现:
<script>
+ var script = document.createElement('script');
+ script.type = 'text/javascript';
+ // 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数
+ script.src = 'http://www.domain2.com:8080/login?user=admin&callback=handleCallback';
+ document.head.appendChild(script);
+ // 回调执行函数
+ function handleCallback(res) {
+ alert(JSON.stringify(res));
+ }
+ </script>
+服务端返回如下(返回时即执行全局函数):
+handleCallback({"success": true, "user": "admin"})
+2)Vue axios实现:
+this.$http = axios;
+this.$http.jsonp('http://www.domain2.com:8080/login', {
+ params: {},
+ jsonp: 'handleCallback'
+}).then((res) => {
+ console.log(res);
+})
+后端node.js代码:
+var querystring = require('querystring');
+var http = require('http');
+var server = http.createServer();
+server.on('request', function(req, res) {
+ var params = querystring.parse(req.url.split('?')[1]);
+ var fn = params.callback;
+ // jsonp返回设置
+ res.writeHead(200, { 'Content-Type': 'text/javascript' });
+ res.write(fn + '(' + JSON.stringify(params) + ')');
+ res.end();
+});
+server.listen('8080');
+console.log('Server is running at port 8080...');
+JSONP的缺点:
+postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:
+用法:postMessage(data,origin)方法接受两个参数:
+1)a.html:(domain1.com/a.html)
+<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
+<script>
+ var iframe = document.getElementById('iframe');
+ iframe.onload = function() {
+ var data = {
+ name: 'aym'
+ };
+ // 向domain2传送跨域数据
+ iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');
+ };
+ // 接受domain2返回数据
+ window.addEventListener('message', function(e) {
+ alert('data from domain2 ---> ' + e.data);
+ }, false);
+</script>
+2)b.html:(domain2.com/b.html)
+<script>
+ // 接收domain1的数据
+ window.addEventListener('message', function(e) {
+ alert('data from domain1 ---> ' + e.data);
+ var data = JSON.parse(e.data);
+ if (data) {
+ data.number = 16;
+ // 处理后再发回domain1
+ window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');
+ }
+ }, false);
+</script>
+nginx代理跨域,实质和CORS跨域原理一样,通过配置文件设置请求响应头Access-Control-Allow-Origin…等字段。
+1)nginx配置解决iconfont跨域 +浏览器跨域访问js、css、img等常规静态资源被同源策略许可,但iconfont字体文件(eot|otf|ttf|woff|svg)例外,此时可在nginx的静态资源服务器中加入以下配置。
+location / {
+ add_header Access-Control-Allow-Origin *;
+}
+2)nginx反向代理接口跨域 +跨域问题:同源策略仅是针对浏览器的安全策略。服务器端调用HTTP接口只是使用HTTP协议,不需要同源策略,也就不存在跨域问题。 +实现思路:通过Nginx配置一个代理服务器域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域访问。
+nginx具体配置:
+#proxy服务器
+server {
+ listen 81;
+ server_name www.domain1.com;
+ location / {
+ proxy_pass http://www.domain2.com:8080; #反向代理
+ proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
+ index index.html index.htm;
+ # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
+ add_header Access-Control-Allow-Origin http://www.domain1.com; #当前端只跨域不带cookie时,可为*
+ add_header Access-Control-Allow-Credentials true;
+ }
+}
+node中间件实现跨域代理,原理大致与nginx相同,都是通过启一个代理服务器,实现数据的转发,也可以通过设置cookieDomainRewrite参数修改响应头中cookie中域名,实现当前域的cookie写入,方便接口登录认证。
+1)非vue框架的跨域 +使用node + express + http-proxy-middleware搭建一个proxy服务器。
+var xhr = new XMLHttpRequest();
+// 前端开关:浏览器是否读写cookie
+xhr.withCredentials = true;
+// 访问http-proxy-middleware代理服务器
+xhr.open('get', 'http://www.domain1.com:3000/login?user=admin', true);
+xhr.send();
+var express = require('express');
+var proxy = require('http-proxy-middleware');
+var app = express();
+app.use('/', proxy({
+ // 代理跨域目标接口
+ target: 'http://www.domain2.com:8080',
+ changeOrigin: true,
+ // 修改响应头信息,实现跨域并允许带cookie
+ onProxyRes: function(proxyRes, req, res) {
+ res.header('Access-Control-Allow-Origin', 'http://www.domain1.com');
+ res.header('Access-Control-Allow-Credentials', 'true');
+ },
+ // 修改响应信息中的cookie域名
+ cookieDomainRewrite: 'www.domain1.com' // 可以为false,表示不修改
+}));
+app.listen(3000);
+console.log('Proxy server is listen at port 3000...');
+2)vue框架的跨域
+node + vue + webpack + webpack-dev-server搭建的项目,跨域请求接口,直接修改webpack.config.js配置。开发环境下,vue渲染服务和接口代理服务都是webpack-dev-server同一个,所以页面与代理接口之间不再跨域。
+webpack.config.js部分配置:
+module.exports = {
+ entry: {},
+ module: {},
+ ...
+ devServer: {
+ historyApiFallback: true,
+ proxy: [{
+ context: '/login',
+ target: 'http://www.domain2.com:8080', // 代理跨域目标接口
+ changeOrigin: true,
+ secure: false, // 当代理某些https服务报错时用
+ cookieDomainRewrite: 'www.domain1.com' // 可以为false,表示不修改
+ }],
+ noInfo: true
+ }
+}
+此方案仅限主域相同,子域不同的跨域应用场景。实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。 +1)父窗口:(domain.com/a.html)
+<iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
+<script>
+ document.domain = 'domain.com';
+ var user = 'admin';
+</script>
+1)子窗口:(child.domain.com/a.html)
+<script>
+ document.domain = 'domain.com';
+ // 获取父窗口中变量
+ console.log('get js data from parent ---> ' + window.parent.user);
+</script>
+实现原理:a欲与b跨域相互通信,通过中间页c来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。
+具体实现:A域:a.html -> B域:b.html -> A域:c.html,a与b不同域只能通过hash值单向通信,b与c也不同域也只能单向通信,但c与a同域,所以c可通过parent.parent访问a页面所有对象。
+1)a.html:(domain1.com/a.html)
+<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
+<script>
+ var iframe = document.getElementById('iframe');
+ // 向b.html传hash值
+ setTimeout(function() {
+ iframe.src = iframe.src + '#user=admin';
+ }, 1000);
+
+ // 开放给同域c.html的回调方法
+ function onCallback(res) {
+ alert('data from c.html ---> ' + res);
+ }
+</script>
+2)b.html:(.domain2.com/b.html)
+<iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe>
+<script>
+ var iframe = document.getElementById('iframe');
+ // 监听a.html传来的hash值,再传给c.html
+ window.onhashchange = function () {
+ iframe.src = iframe.src + location.hash;
+ };
+</script>
+3)c.html:(www.domain1.com/c.html)
+<script>
+ // 监听b.html传来的hash值
+ window.onhashchange = function () {
+ // 再通过操作同域a.html的js回调,将结果传回
+ window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
+ };
+</script>
+window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。
+1)a.html:(domain1.com/a.html)
+var proxy = function(url, callback) {
+ var state = 0;
+ var iframe = document.createElement('iframe');
+ // 加载跨域页面
+ iframe.src = url;
+ // onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
+ iframe.onload = function() {
+ if (state === 1) {
+ // 第2次onload(同域proxy页)成功后,读取同域window.name中数据
+ callback(iframe.contentWindow.name);
+ destoryFrame();
+ } else if (state === 0) {
+ // 第1次onload(跨域页)成功后,切换到同域代理页面
+ iframe.contentWindow.location = 'http://www.domain1.com/proxy.html';
+ state = 1;
+ }
+ };
+ document.body.appendChild(iframe);
+ // 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)
+ function destoryFrame() {
+ iframe.contentWindow.document.write('');
+ iframe.contentWindow.close();
+ document.body.removeChild(iframe);
+ }
+};
+// 请求跨域b页面数据
+proxy('http://www.domain2.com/b.html', function(data){
+ alert(data);
+});
+2)proxy.html:(domain1.com/proxy.html)
+中间代理页,与a.html同域,内容为空即可。 +3)b.html:(domain2.com/b.html)
+<script>
+ window.name = 'This is domain2 data!';
+</script>
+通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。
+WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。
+原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。
+1)前端代码:
+<div>user input:<input type="text"></div>
+<script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script>
+<script>
+var socket = io('http://www.domain2.com:8080');
+// 连接成功处理
+socket.on('connect', function() {
+ // 监听服务端消息
+ socket.on('message', function(msg) {
+ console.log('data from server: ---> ' + msg);
+ });
+ // 监听服务端关闭
+ socket.on('disconnect', function() {
+ console.log('Server socket has closed.');
+ });
+});
+document.getElementsByTagName('input')[0].onblur = function() {
+ socket.send(this.value);
+};
+</script>
+2)Nodejs socket后台:
+var http = require('http');
+var socket = require('socket.io');
+// 启http服务
+var server = http.createServer(function(req, res) {
+ res.writeHead(200, {
+ 'Content-type': 'text/html'
+ });
+ res.end();
+});
+server.listen('8080');
+console.log('Server is running at port 8080...');
+// 监听socket连接
+socket.listen(server).on('connection', function(client) {
+ // 接收信息
+ client.on('message', function(msg) {
+ client.send('hello:' + msg);
+ console.log('data from client: ---> ' + msg);
+ });
+ // 断开处理
+ client.on('disconnect', function() {
+ console.log('Client socket has closed.');
+ });
+});
+客户端想获得一个服务器的数据,但是因为种种原因无法直接获取。于是客户端设置了一个代理服务器,并且指定目标服务器,之后代理服务器向目标服务器转交请求并将获得的内容发送给客户端。这样本质上起到了对真实服务器隐藏真实客户端的目的。实现正向代理需要修改客户端,比如修改浏览器配置。
+服务器为了能够将工作负载分不到多个服务器来提高网站性能 (负载均衡)等目的,当其受到请求后,会首先根据转发规则来确定请求应该被转发到哪个服务器上,然后将请求转发到对应的真实服务器上。这样本质上起到了对客户端隐藏真实服务器的作用。 +一般使用反向代理后,需要通过修改 DNS 让域名解析到代理服务器 IP,这时浏览器无法察觉到真正服务器的存在,当然也就不需要修改配置了。
+两者区别如图示:
+
+正向代理和反向代理的结构是一样的,都是 client-proxy-server 的结构,它们主要的区别就在于中间这个 proxy 是哪一方设置的。在正向代理中,proxy 是 client 设置的,用来隐藏 client;而在反向代理中,proxy 是 server 设置的,用来隐藏 server。
Nginx 是一款轻量级的 Web 服务器,也可以用于反向代理、负载平衡和 HTTP 缓存等。Nginx 使用异步事件驱动的方法来处理请求,是一款面向性能设计的 HTTP 服务器。
+传统的 Web 服务器如 Apache 是 process-based 模型的,而 Nginx 是基于event-driven模型的。正是这个主要的区别带给了 Nginx 在性能上的优势。
+Nginx 架构的最顶层是一个 master process,这个 master process 用于产生其他的 worker process,这一点和Apache 非常像,但是 Nginx 的 worker process 可以同时处理大量的HTTP请求,而每个 Apache process 只能处理一个。
+事件是用户操作网页时发生的交互动作,比如 click/move, 事件除了用户触发的动作外,还可以是文档加载,窗口滚动和大小调整。事件被封装成一个 event 对象,包含了该事件发生时的所有相关信息( event 的属性)以及可以对事件进行的操作( event 的方法)。
+事件是用户操作网页时发生的交互动作或者网页本身的一些操作,现代浏览器一共有三种事件模型:
+事件委托本质上是利用了浏览器事件冒泡的机制。因为事件在冒泡过程中会上传到父节点,父节点可以通过事件对象获取到目标节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件,这种方式称为事件委托(事件代理)。
+使用事件委托可以不必要为每一个子元素都绑定一个监听事件,这样减少了内存上的消耗。并且使用事件代理还可以实现事件的动态绑定,比如说新增了一个子节点,并不需要单独地为它添加一个监听事件,它绑定的事件会交给父元素中的监听函数来处理。
+如果有一个列表,列表之中有大量的列表项,需要在点击列表项的时候响应一个事件:
+<ul id="list">
+ <li>item 1</li>
+ <li>item 2</li>
+ <li>item 3</li>
+ ......
+ <li>item n</li>
+</ul>
+如果给每个列表项一一都绑定一个函数,那对于内存消耗是非常大的,效率上需要消耗很多性能。因此,比较好的方法就是把这个点击事件绑定到他的父层,也就是 ul 上,然后在执行事件时再去匹配判断目标元素,所以事件委托可以减少大量的内存消耗,节约效率。
+给上述的例子中每个列表项都绑定事件,在很多时候,需要通过 AJAX 或者用户操作动态的增加或者去除列表项元素,那么在每一次改变的时候都需要重新给新增的元素绑定事件,给即将删去的元素解绑事件;如果用了事件委托就没有这种麻烦了,因为事件是绑定在父层的,和目标元素的增减是没有关系的,执行到目标元素是在真正响应执行事件函数的过程中去匹配的,所以使用事件在动态绑定事件的情况下是可以减少很多重复工作的。
+// 来实现把 #list 下的 li 元素的事件代理委托到它的父层元素也就是 #list 上:
+// 给父层元素绑定事件
+document.getElementById('list').addEventListener('click', function (e) {
+ // 兼容性处理
+ var event = e || window.event;
+ var target = event.target || event.srcElement;
+ // 判断是否匹配目标元素
+ if (target.nodeName.toLocaleLowerCase === 'li') {
+ console.log('the content is: ', target.innerHTML);
+ }
+});
+在上述代码中, target 元素则是在 #list 元素之下具体被点击的元素,然后通过判断 target 的一些属性(比如:nodeName,id 等等)可以更精确地匹配到某一类 #list li 元素之上;
+当然,事件委托也是有局限的。比如 focus、blur 之类的事件没有事件冒泡机制,所以无法实现事件委托;mousemove、mouseout 这样的事件,虽然有事件冒泡,但是只能不断通过位置去计算定位,对性能消耗高,因此也是不适合于事件委托的。
+当然事件委托不是只有优点,它也是有缺点的,事件委托会影响页面性能,主要影响因素有:
+DOM层数;在必须使用事件委托的地方,可以进行如下的处理:
+ajax的局部刷新区域body元素上,进行绑定场景:给页面的所有的a标签添加click事件,代码如下:
+document.addEventListener("click", function(e) {
+ if (e.target.nodeName == "A")
+ console.log("a");
+}, false);
+但是这些a标签可能包含一些像span、img等元素,如果点击到了这些a标签中的元素,就不会触发click事件,因为事件绑定上在a标签元素上,而触发这些内部的元素时,e.target指向的是触发click事件的元素(span、img等其他元素)。
+这种情况下就可以使用事件委托来处理,将事件绑定在a标签的内部元素上,当点击它的时候,就会逐级向上查找,知道找到a标签为止,代码如下:
+document.addEventListener("click", function(e) {
+ var node = e.target;
+ while (node.parentNode.nodeName != "BODY") {
+ if (node.nodeName == "A") {
+ console.log("a");
+ break;
+ }
+ node = node.parentNode;
+ }
+}, false);
+因为 js 是单线程运行的,在代码执行时,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行。在执行同步代码时,如果遇到异步事件,js 引擎并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当异步事件执行完毕后,再将异步事件对应的回调加入到一个任务队列中等待执行。任务队列可以分为宏任务队列和微任务队列,当当前执行栈中的事件执行完毕后,js 引擎首先会判断微任务队列中是否有任务可以执行,如果有就将微任务队首的事件压入栈中执行。当微任务队列中的任务都执行完成后再去执行宏任务队列中的任务。
+Event Loop 执行顺序如下所示:
+可以把执行栈认为是一个存储函数调用的栈结构,遵循先进后出的原则。
+
+当开始执行 JS 代码时,根据先进后出的原则,后执行的函数会先弹出栈,可以看到,
foo 函数后执行,当执行完毕后就从栈中弹出了。
平时在开发中,可以在报错中找到执行栈的痕迹:
+function foo() {
+ throw new Error('error')
+}
+function bar() {
+ foo()
+}
+bar()
+
+可以看到报错在
foo 函数,foo 函数又是在 bar 函数中调用的。当使用递归时,因为栈可存放的函数是有限制的,一旦存放了过多的函数且没有得到释放的话,就会出现爆栈的问题
function bar() { bar()}bar()
+Node 中的 Event Loop 和浏览器中的是完全不相同的东西。
+Node 的 Event Loop 分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。
+
(1)Timers(计时器阶段):初次进入事件循环,会从计时器阶段开始。此阶段会判断是否存在过期的计时器回调(包含 setTimeout 和 setInterval),如果存在则会执行所有过期的计时器回调,执行完毕后,如果回调中触发了相应的微任务,会接着执行所有微任务,执行完微任务后再进入 Pending callbacks 阶段。
+(2)Pending callbacks:执行推迟到下一个循环迭代的I / O回调(系统调用相关的回调)。
+(3)Idle/Prepare:仅供内部使用。
+(4)Poll(轮询阶段):
+(5)Check(查询阶段):会检查是否存在 setImmediate 相关的回调,如果存在则执行所有回调,执行完毕后,如果回调中触发了相应的微任务,会接着执行所有微任务,执行完微任务后再进入 Close callbacks 阶段。
+(6)Close callbacks:执行一些关闭回调,比如socket.on('close', ...)等。
+下面来看一个例子,首先在有些情况下,定时器的执行顺序其实是随机的
+setTimeout(() => { console.log('setTimeout')}, 0)setImmediate(() => { console.log('setImmediate')})
+对于以上代码来说,setTimeout 可能执行在前,也可能执行在后
setTimeout(fn, 0) === setTimeout(fn, 1),这是由源码决定的setTimeout 回调setImmediate 回调先执行了当然在某些情况下,他们的执行顺序一定是固定的,比如以下代码:
+const fs = require('fs')
+fs.readFile(__filename, () => {
+ setTimeout(() => {
+ console.log('timeout');
+ }, 0)
+ setImmediate(() => {
+ console.log('immediate')
+ })
+})
+在上述代码中,setImmediate 永远先执行。因为两个代码写在 IO 回调中,IO 回调是在 poll 阶段执行,当回调执行完毕后队列为空,发现存在 setImmediate 回调,所以就直接跳转到 check 阶段去执行回调了。
上面都是 macrotask 的执行情况,对于 microtask 来说,它会在以上每个阶段完成前清空 microtask 队列,下图中的 Tick 就代表了 microtask
+
setTimeout(() => {
+ console.log('timer21')
+}, 0)
+Promise.resolve().then(function() {
+ console.log('promise1')
+})
+对于以上代码来说,其实和浏览器中的输出是一样的,microtask 永远执行在 macrotask 前面。
+最后来看 Node 中的 process.nextTick,这个函数其实是独立于 Event Loop 之外的,它有一个自己的队列,当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行。
setTimeout(() => {
+ console.log('timer1')
+ Promise.resolve().then(function() {
+ console.log('promise1')
+ })
+}, 0)
+process.nextTick(() => {
+ console.log('nextTick')
+ process.nextTick(() => {
+ console.log('nextTick')
+ process.nextTick(() => {
+ console.log('nextTick')
+ process.nextTick(() => {
+ console.log('nextTick')
+ })
+ })
+ })
+})
+对于以上代码,永远都是先把 nextTick 全部打印出来。
+事件触发有三个阶段:
+window 往事件触发处传播,遇到注册的捕获事件会触发window 传播,遇到注册的冒泡事件会触发事件触发一般来说会按照上面的顺序进行,但是也有特例,如果给一个 body 中的子节点同时注册冒泡和捕获事件,事件触发会按照注册的顺序执行。
// 以下会先打印冒泡然后是捕获
+node.addEventListener(
+ 'click',
+ event => {
+ console.log('冒泡')
+ },
+ false
+)
+node.addEventListener(
+ 'click',
+ event => {
+ console.log('捕获 ')
+ },
+ true
+)
+通常使用 addEventListener 注册事件,该函数的第三个参数可以是布尔值,也可以是对象。对于布尔值 useCapture 参数来说,该参数默认值为 false ,useCapture 决定了注册的事件是捕获事件还是冒泡事件。对于对象参数来说,可以使用以下几个属性:
capture:布尔值,和 useCapture 作用一样once:布尔值,值为 true 表示该回调只会调用一次,调用后会移除监听passive:布尔值,表示永远不会调用 preventDefault一般来说,如果只希望事件只触发在目标上,这时候可以使用 stopPropagation 来阻止事件的进一步传播。通常认为 stopPropagation 是用来阻止事件冒泡的,其实该函数也可以阻止捕获事件。
stopImmediatePropagation 同样也能实现阻止事件,但是还能阻止该事件目标执行别的注册事件。
node.addEventListener(
+ 'click',
+ event => {
+ event.stopImmediatePropagation()
+ console.log('冒泡')
+ },
+ false
+)
+// 点击 node 只会执行上面的函数,该函数不会执行
+node.addEventListener(
+ 'click',
+ event => {
+ console.log('捕获 ')
+ },
+ true
+)
+V8 实现了准确式 GC,GC 算法采用了分代式垃圾回收机制。因此,V8 将内存(堆)分为新生代和老生代两部分。
+(1)新生代算法
+新生代中的对象一般存活时间较短,使用 Scavenge GC 算法。
+在新生代空间中,内存空间分为两部分,分别为 From 空间和 To 空间。在这两个空间中,必定有一个空间是使用的,另一个空间是空闲的。新分配的对象会被放入 From 空间中,当 From 空间被占满时,新生代 GC 就会启动了。算法会检查 From 空间中存活的对象并复制到 To 空间中,如果有失活的对象就会销毁。当复制完成后将 From 空间和 To 空间互换,这样 GC 就结束了。
+(2)老生代算法
+老生代中的对象一般存活时间较长且数量也多,使用了两个算法,分别是标记清除算法和标记压缩算法。
+先来说下什么情况下对象会出现在老生代空间中:
+老生代中的空间很复杂,有如下几个空间
+enum AllocationSpace {
+ // TODO(v8:7464): Actually map this space's memory as read-only.
+ RO_SPACE, // 不变的对象空间
+ NEW_SPACE, // 新生代用于 GC 复制算法的空间
+ OLD_SPACE, // 老生代常驻对象空间
+ CODE_SPACE, // 老生代代码对象空间
+ MAP_SPACE, // 老生代 map 对象
+ LO_SPACE, // 老生代大空间对象
+ NEW_LO_SPACE, // 新生代大空间对象
+ FIRST_SPACE = RO_SPACE,
+ LAST_SPACE = NEW_LO_SPACE,
+ FIRST_GROWABLE_PAGED_SPACE = OLD_SPACE,
+ LAST_GROWABLE_PAGED_SPACE = MAP_SPACE
+};
+在老生代中,以下情况会先启动标记清除算法:
+在这个阶段中,会遍历堆中所有的对象,然后标记活的对象,在标记完成后,销毁所有没有被标记的对象。在标记大型对内存时,可能需要几百毫秒才能完成一次标记。这就会导致一些性能上的问题。为了解决这个问题,2011 年,V8 从 stop-the-world 标记切换到增量标志。在增量标记期间,GC 将标记工作分解为更小的模块,可以让 JS 应用逻辑在模块间隙执行一会,从而不至于让应用出现停顿情况。但在 2018 年,GC 技术又有了一个重大突破,这项技术名为并发标记。该技术可以让 GC 扫描和标记对象时,同时允许 JS 运行。
+清除对象后会造成堆内存出现碎片的情况,当碎片超过一定限制后会启动压缩算法。在压缩过程中,将活的对象向一端移动,直到所有对象都移动完成然后清理掉不需要的内存。
+const promise = new Promise((resolve, reject) => {
+ console.log(1);
+ console.log(2);
+});
+promise.then(() => {
+ console.log(3);
+});
+console.log(4);
+输出结果如下:
+1
+2
+4
+promise.then 是微任务,它会在所有的宏任务执行完之后才会执行,同时需要promise内部的状态发生变化,因为这里内部没有发生变化,一直处于pending状态,所以不输出3。
+const promise1 = new Promise((resolve, reject) => {
+ console.log('promise1')
+ resolve('resolve1')
+})
+const promise2 = promise1.then(res => {
+ console.log(res)
+})
+console.log('1', promise1);
+console.log('2', promise2);
+输出结果如下:
+promise1
+1 Promise{<resolved>: resolve1}
+2 Promise{<pending>}
+resolve1
+需要注意的是,直接打印promise1,会打印出它的状态值和参数。
+代码执行过程如下:
+promise1;resolve函数, 将promise1的状态改变为resolved, 并将结果保存下来;promise1.then这个微任务,将它放入微任务队列;promise2是一个新的状态为pending的Promise;promise1的状态是resolved;promise2的状态是pending;promise1.then这个微任务且状态为resolved,执行它。const promise = new Promise((resolve, reject) => {
+ console.log(1);
+ setTimeout(() => {
+ console.log("timerStart");
+ resolve("success");
+ console.log("timerEnd");
+ }, 0);
+ console.log(2);
+});
+promise.then((res) => {
+ console.log(res);
+});
+console.log(4);
+输出结果如下:
+1
+2
+4
+timerStart
+timerEnd
+success
+代码执行过程如下:
+1;steTimeout,它是一个宏任务,放入宏任务队列;Promise的状态此时还是pending,所以promise.then先不执行;steTimeout;timerStart,然后遇到了resolve,将promise的状态改为resolved且保存结果并将之前的promise.then推入微任务队列,再执行timerEnd;promise.then,打印出resolve的结果。Promise.resolve().then(() => {
+ console.log('promise1');
+ const timer2 = setTimeout(() => {
+ console.log('timer2')
+ }, 0)
+});
+const timer1 = setTimeout(() => {
+ console.log('timer1')
+ Promise.resolve().then(() => {
+ console.log('promise2')
+ })
+}, 0)
+console.log('start');
+输出结果如下:
+start
+promise1
+timer1
+promise2
+timer2
+代码执行过程如下:
+Promise.resolve().then是一个微任务,加入微任务队列startPromise.resolve().then,打印出promise1timer2,它是一个宏任务,将其加入宏任务队列,此时宏任务队列有两个任务,分别是timer1、timer2;timer1,打印timer1;Promise.resolve().then,它是一个微任务,加入微任务队列promise2;timer2定时器,打印出timer2;const promise = new Promise((resolve, reject) => {
+ resolve('success1');
+ reject('error');
+ resolve('success2');
+});
+promise.then((res) => {
+ console.log('then:', res);
+}).catch((err) => {
+ console.log('catch:', err);
+})
+输出结果如下:
+then:success1
+这个题目考察的就是Promise的状态在发生变化之后,就不会再发生变化。开始状态由pending变为resolve,说明已经变为已完成状态,下面的两个状态的就不会再执行,同时下面的catch也不会捕获到错误。
Promise.resolve(1)
+ .then(2)
+ .then(Promise.resolve(3))
+ .then(console.log)
+输出结果如下:
+1
+Promise {<fulfilled>: undefined}
+Promise.resolve方法的参数如果是一个原始值,或者是一个不具有then方法的对象,则Promise.resolve方法返回一个新的Promise对象,状态为resolved,Promise.resolve方法的参数,会同时传给回调函数。
+then方法接受的参数是函数,而如果传递的并非是一个函数,它实际上会将其解释为then(null),这就会导致前一个Promise的结果会传递下面。
+const promise1 = new Promise((resolve, reject) => {
+ setTimeout(() => {
+ resolve('success')
+ }, 1000)
+})
+const promise2 = promise1.then(() => {
+ throw new Error('error!!!')
+})
+console.log('promise1', promise1)
+console.log('promise2', promise2)
+setTimeout(() => {
+ console.log('promise1', promise1)
+ console.log('promise2', promise2)
+}, 2000)
+输出结果如下:
+promise1 Promise {<pending>}
+promise2 Promise {<pending>}
+
+Uncaught (in promise) Error: error!!!
+promise1 Promise {<fulfilled>: "success"}
+promise2 Promise {<rejected>: Error: error!!}
+Promise.resolve(1)
+ .then(res => {
+ console.log(res);
+ return 2;
+ })
+ .catch(err => {
+ return 3;
+ })
+ .then(res => {
+ console.log(res);
+ });
+输出结果如下:
+1
+2
+Promise是可以链式调用的,由于每次调用 .then 或者 .catch 都会返回一个新的 promise,从而实现了链式调用, 它并不像一般任务的链式调用一样return this。
上面的输出结果之所以依次打印出1和2,是因为resolve(1)之后走的是第一个then方法,并没有进catch里,所以第二个then中的res得到的实际上是第一个then的返回值。并且return 2会被包装成resolve(2),被最后的then打印输出2。
Promise.resolve().then(() => {
+ return new Error('error!!!')
+}).then(res => {
+ console.log("then: ", res)
+}).catch(err => {
+ console.log("catch: ", err)
+})
+输出结果如下:
+"then: " "Error: error!!!"
+返回任意一个非 promise 的值都会被包裹成 promise 对象,因此这里的return new Error('error!!!')也被包裹成了return Promise.resolve(new Error('error!!!')),因此它会被then捕获而不是catch。
const promise = Promise.resolve().then(() => {
+ return promise;
+})
+promise.catch(console.err)
+输出结果如下:
+Uncaught (in promise) TypeError: Chaining cycle detected for promise #<Promise>
+这里其实是一个坑,.then 或 .catch 返回的值不能是 promise 本身,否则会造成死循环。
Promise.resolve(1)
+ .then(2)
+ .then(Promise.resolve(3))
+ .then(console.log)
+输出结果如下:
+1
+看到这个题目,好多的then,实际上只需要记住一个原则:.then 或.catch 的参数期望是函数,传入非函数则会发生值透传。
第一个then和第二个then中传入的都不是函数,一个是数字,一个是对象,因此发生了透传,将resolve(1) 的值直接传到最后一个then里,直接打印出1。
Promise.reject('err!!!')
+ .then((res) => {
+ console.log('success', res)
+ }, (err) => {
+ console.log('error', err)
+ }).catch(err => {
+ console.log('catch', err)
+ })
+输出结果如下:
+error err!!!
+我们知道,.then函数中的两个参数:
也就是说Promise.resolve('1')的值会进入成功的函数,Promise.reject('2')的值会进入失败的函数。
在这道题中,错误直接被then的第二个参数捕获了,所以就不会被catch捕获了,输出结果为:error err!!!'
但是,如果是像下面这样:
+Promise.resolve()
+ .then(function success (res) {
+ throw new Error('error!!!')
+ }, function fail1 (err) {
+ console.log('fail1', err)
+ }).catch(function fail2 (err) {
+ console.log('fail2', err)
+ })
+在then的第一参数中抛出了错误,那么他就不会被第二个参数不活了,而是被后面的catch捕获到。
Promise.resolve('1')
+ .then(res => {
+ console.log(res)
+ })
+ .finally(() => {
+ console.log('finally')
+ })
+Promise.resolve('2')
+ .finally(() => {
+ console.log('finally2')
+ return '我是finally2返回的值'
+ })
+ .then(res => {
+ console.log('finally2后面的then函数', res)
+ })
+输出结果如下:
+1
+finally2
+finally
+finally2后面的then函数 2
+.finally()一般用的很少,只要记住以下几点就可以了:
.finally()方法不管Promise对象最后的状态如何都会执行.finally()方法的回调函数不接受任何的参数,也就是说你在.finally()函数中是无法知道Promise最终的状态是resolved还是rejected的.finally()的错误捕获:
Promise.resolve('1')
+ .finally(() => {
+ console.log('finally1')
+ throw new Error('我是finally中抛出的异常')
+ })
+ .then(res => {
+ console.log('finally后面的then函数', res)
+ })
+ .catch(err => {
+ console.log('捕获错误', err)
+ })
+输出结果为:
+'finally1'
+'捕获错误' Error: 我是finally中抛出的异常
+function runAsync (x) {
+ const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
+ return p
+}
+
+Promise.all([runAsync(1), runAsync(2), runAsync(3)]).then(res => console.log(res))
+输出结果如下:
+1
+2
+3
+[1, 2, 3]
+首先,定义了一个Promise,来异步执行函数runAsync,该函数传入一个值x,然后间隔一秒后打印出这个x。
+之后再使用Promise.all来执行这个函数,执行的时候,看到一秒之后输出了1,2,3,同时输出了数组[1, 2, 3],三个函数是同步执行的,并且在一个回调函数中返回了所有的结果。并且结果和函数的执行顺序是一致的。
function runAsync (x) {
+ const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
+ return p
+}
+function runReject (x) {
+ const p = new Promise((res, rej) => setTimeout(() => rej(`Error: ${x}`, console.log(x)), 1000 * x))
+ return p
+}
+Promise.all([runAsync(1), runReject(4), runAsync(3), runReject(2)])
+ .then(res => console.log(res))
+ .catch(err => console.log(err))
+输出结果如下:
+// 1s后输出
+1
+3
+// 2s后输出
+2
+Error: 2
+// 4s后输出
+4
+可以看到。catch捕获到了第一个错误,在这道题目中最先的错误就是runReject(2)的结果。如果一组异步操作中有一个异常都不会进入.then()的第一个回调函数参数中。会被.then()的第二个回调函数捕获。
function runAsync (x) {
+ const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
+ return p
+}
+Promise.race([runAsync(1), runAsync(2), runAsync(3)])
+ .then(res => console.log('result: ', res))
+ .catch(err => console.log(err))
+输出结果如下:
+1
+'result: ' 1
+2
+3
+then只会捕获第一个成功的方法,其他的函数虽然还会继续执行,但是不是被then捕获了。
+function runAsync(x) {
+ const p = new Promise(r =>
+ setTimeout(() => r(x, console.log(x)), 1000)
+ );
+ return p;
+}
+function runReject(x) {
+ const p = new Promise((res, rej) =>
+ setTimeout(() => rej(`Error: ${x}`, console.log(x)), 1000 * x)
+ );
+ return p;
+}
+Promise.race([runReject(0), runAsync(1), runAsync(2), runAsync(3)])
+ .then(res => console.log("result: ", res))
+ .catch(err => console.log(err));
+输出结果如下:
+0
+Error: 0
+1
+2
+3
+可以看到在catch捕获到第一个错误之后,后面的代码还不执行,不过不会再被捕获了。
+注意:all和race传入的数组中如果有会抛出异常的异步任务,那么只有最先抛出的错误会被捕获,并且是被then的第二个参数或者后面的catch捕获;但并不会影响数组中其它的异步任务的执行。
async function async1() {
+ console.log("async1 start");
+ await async2();
+ console.log("async1 end");
+}
+async function async2() {
+ console.log("async2");
+}
+async1();
+console.log('start')
+输出结果如下:
+async1 start
+async2
+start
+async1 end
+代码的执行过程如下:
+async1 start,之后遇到了await,它会阻塞async1后面代码的执行,因此会先去执行async2中的同步代码async2,然后跳出async1;async1函数后,执行同步代码start;await后面的内容async1 end。这里可以理解为await后面的语句相当于放到了new Promise中,下一行及之后的语句相当于放在Promise.then中。
+async function async1() {
+ console.log("async1 start");
+ await async2();
+ console.log("async1 end");
+ setTimeout(() => {
+ console.log('timer1')
+ }, 0)
+}
+async function async2() {
+ setTimeout(() => {
+ console.log('timer2')
+ }, 0)
+ console.log("async2");
+}
+async1();
+setTimeout(() => {
+ console.log('timer3')
+}, 0)
+console.log("start")
+输出结果如下:
+async1 start
+async2
+start
+async1 end
+timer2
+timer3
+timer1
+代码的执行过程如下:
+async1,打印出async1 start;async2,进入async2,遇到定时器timer2,加入宏任务队列,之后打印async2;async2阻塞了后面代码的执行,所以执行后面的定时器timer3,将其加入宏任务队列,之后打印start;async1 end,遇到定时器timer1,将其加入宏任务队列;timer2,timer3,timer1,没有微任务,所以直接所有的宏任务按照先进先出的原则执行。async function async1 () {
+ console.log('async1 start');
+ await new Promise(resolve => {
+ console.log('promise1')
+ })
+ console.log('async1 success');
+ return 'async1 end'
+}
+console.log('srcipt start')
+async1().then(res => console.log(res))
+console.log('srcipt end')
+输出结果如下:
+script start
+async1 start
+promise1
+script end
+这里需要注意的是在async1中await后面的Promise是没有返回值的,也就是它的状态始终是pending状态,所以在await之后的内容是不会执行的,包括async1后面的 .then。
async function async1 () {
+ console.log('async1 start');
+ await new Promise(resolve => {
+ console.log('promise1')
+ resolve('promise1 resolve')
+ }).then(res => console.log(res))
+ console.log('async1 success');
+ return 'async1 end'
+}
+console.log('srcipt start')
+async1().then(res => console.log(res))
+console.log('srcipt end')
+这里是对上面一题进行了改造,加上了resolve。
+输出结果如下:
+script start
+async1 start
+promise1
+script end
+promise1 resolve
+async1 success
+async1 end
+async function async1() {
+ console.log("async1 start");
+ await async2();
+ console.log("async1 end");
+}
+
+async function async2() {
+ console.log("async2");
+}
+
+console.log("script start");
+
+setTimeout(function() {
+ console.log("setTimeout");
+}, 0);
+
+async1();
+
+new Promise(resolve => {
+ console.log("promise1");
+ resolve();
+}).then(function() {
+ console.log("promise2");
+});
+console.log('script end')
+输出结果如下:
+script start
+async1 start
+async2
+promise1
+script end
+async1 end
+promise2
+setTimeout
+代码执行过程如下:
+async function async1 () {
+ await async2();
+ console.log('async1');
+ return 'async1 success'
+}
+async function async2 () {
+ return new Promise((resolve, reject) => {
+ console.log('async2')
+ reject('error')
+ })
+}
+async1().then(res => console.log(res))
+输出结果如下:
+async2
+Uncaught (in promise) error
+可以看到,如果async函数中抛出了错误,就会终止错误结果,不会继续向下执行。
+如果想要让错误不足之处后面的代码执行,可以使用catch来捕获:
+async function async1 () {
+ await Promise.reject('error!!!').catch(e => console.log(e))
+ console.log('async1');
+ return Promise.resolve('async1 success')
+}
+async1().then(res => console.log(res))
+console.log('script start')
+这样的输出结果就是:
+script start
+error!!!
+async1
+async1 success
+const first = () => (new Promise((resolve, reject) => {
+ console.log(3);
+ let p = new Promise((resolve, reject) => {
+ console.log(7);
+ setTimeout(() => {
+ console.log(5);
+ resolve(6);
+ console.log(p)
+ }, 0)
+ resolve(1);
+ });
+ resolve(2);
+ p.then((arg) => {
+ console.log(arg);
+ });
+}));
+first().then((arg) => {
+ console.log(arg);
+});
+console.log(4);
+输出结果如下:
+3
+7
+4
+1
+2
+5
+Promise{<resolved>: 1}
+代码的执行过程如下:
+resolve(6)不会再执行;console.log(p)打印出Promise{<resolved>: 1};const async1 = async () => {
+ console.log('async1');
+ setTimeout(() => {
+ console.log('timer1')
+ }, 2000)
+ await new Promise(resolve => {
+ console.log('promise1')
+ })
+ console.log('async1 end')
+ return 'async1 success'
+}
+console.log('script start');
+async1().then(res => console.log(res));
+console.log('script end');
+Promise.resolve(1)
+ .then(2)
+ .then(Promise.resolve(3))
+ .catch(4)
+ .then(res => console.log(res))
+setTimeout(() => {
+ console.log('timer2')
+}, 1000)
+输出结果如下:
+script start
+async1
+promise1
+script end
+1
+timer2
+timer1
+代码的执行过程如下:
+const p1 = new Promise((resolve) => {
+ setTimeout(() => {
+ resolve('resolve3');
+ console.log('timer1')
+ }, 0)
+ resolve('resovle1');
+ resolve('resolve2');
+}).then(res => {
+ console.log(res) // resolve1
+ setTimeout(() => {
+ console.log(p1)
+ }, 1000)
+}).finally(res => {
+ console.log('finally', res)
+})
+执行结果为如下:
+resolve1
+finally undefined
+timer1
+Promise{<resolved>: undefined}
+需要注意的是最后一个定时器打印出的p1其实是.finally的返回值,我们知道.finally的返回值如果在没有抛出错误的情况下默认会是上一个Promise的返回值,而这道题中.finally上一个Promise是.then(),但是这个.then()并没有返回值,所以p1打印出来的Promise的值会是undefined,如果在定时器的下面加上一个return 1,则值就会变成1。
console.log('1');
+
+setTimeout(function() {
+ console.log('2');
+ process.nextTick(function() {
+ console.log('3');
+ })
+ new Promise(function(resolve) {
+ console.log('4');
+ resolve();
+ }).then(function() {
+ console.log('5')
+ })
+})
+process.nextTick(function() {
+ console.log('6');
+})
+new Promise(function(resolve) {
+ console.log('7');
+ resolve();
+}).then(function() {
+ console.log('8')
+})
+
+setTimeout(function() {
+ console.log('9');
+ process.nextTick(function() {
+ console.log('10');
+ })
+ new Promise(function(resolve) {
+ console.log('11');
+ resolve();
+ }).then(function() {
+ console.log('12')
+ })
+})
+输出结果如下:
+1
+7
+6
+8
+2
+4
+3
+5
+9
+11
+10
+12
+(1)第一轮事件循环流程分析如下:
+console.log,输出1。setTimeout,其回调函数被分发到宏任务Event Queue中。暂且记为setTimeout1。process.nextTick(),其回调函数被分发到微任务Event Queue中。记为process1。Promise,new Promise直接执行,输出7。then被分发到微任务Event Queue中。记为then1。setTimeout,其回调函数被分发到宏任务Event Queue中,记为setTimeout2。| 宏任务Event Queue | 微任务Event Queue |
|---|---|
| setTimeout1 | process1 |
| setTimeout2 | then1 |
上表是第一轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了1和7。发现了process1和then1两个微任务:
process1,输出6。then1,输出8。第一轮事件循环正式结束,这一轮的结果是输出1,7,6,8。
+(2)第二轮时间循环从**setTimeout1**宏任务开始:
process.nextTick(),同样将其分发到微任务Event Queue中,记为process2。new Promise立即执行输出4,then也分发到微任务Event Queue中,记为then2。| 宏任务Event Queue | 微任务Event Queue |
|---|---|
| setTimeout2 | process2 |
| then2 |
第二轮事件循环宏任务结束,发现有process2和then2两个微任务可以执行:
第二轮事件循环结束,第二轮输出2,4,3,5。
+(3)第三轮事件循环开始,此时只剩setTimeout2了,执行。
+process.nextTick()分发到微任务Event Queue中。记为process3。new Promise,输出11。then分发到微任务Event Queue中,记为then3。| 宏任务Event Queue | 微任务Event Queue |
|---|---|
| process3 | |
| then3 |
第三轮事件循环宏任务执行结束,执行两个微任务process3和then3:
第三轮事件循环结束,第三轮输出9,11,10,12。
+整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。
+console.log(1)
+
+setTimeout(() => {
+ console.log(2)
+})
+
+new Promise(resolve => {
+ console.log(3)
+ resolve(4)
+}).then(d => console.log(d))
+
+setTimeout(() => {
+ console.log(5)
+ new Promise(resolve => {
+ resolve(6)
+ }).then(d => console.log(d))
+})
+
+setTimeout(() => {
+ console.log(7)
+})
+
+console.log(8)
+输出结果如下:
+1
+3
+8
+4
+2
+5
+6
+7
+代码执行过程如下:
+console.log(1);
+
+setTimeout(() => {
+ console.log(2);
+ Promise.resolve().then(() => {
+ console.log(3)
+ });
+});
+
+new Promise((resolve, reject) => {
+ console.log(4)
+ resolve(5)
+}).then((data) => {
+ console.log(data);
+})
+
+setTimeout(() => {
+ console.log(6);
+})
+
+console.log(7);
+代码输出结果如下:
+1
+4
+7
+5
+2
+3
+6
+代码执行过程如下:
+Promise.resolve().then(() => {
+ console.log('1');
+ throw 'Error';
+}).then(() => {
+ console.log('2');
+}).catch(() => {
+ console.log('3');
+ throw 'Error';
+}).then(() => {
+ console.log('4');
+}).catch(() => {
+ console.log('5');
+}).then(() => {
+ console.log('6');
+});
+执行结果如下:
+1
+3
+5
+6
+在这道题目中,我们需要知道,无论是thne还是catch中,只要throw 抛出了错误,就会被catch捕获,如果没有throw出错误,就被继续执行后面的then。
+setTimeout(function () {
+ console.log(1);
+}, 100);
+
+new Promise(function (resolve) {
+ console.log(2);
+ resolve();
+ console.log(3);
+}).then(function () {
+ console.log(4);
+ new Promise((resove, reject) => {
+ console.log(5);
+ setTimeout(() => {
+ console.log(6);
+ }, 10);
+ })
+});
+console.log(7);
+console.log(8);
+输出结果为:
+2
+3
+7
+8
+4
+5
+6
+1
+代码执行过程如下:
+做完这道题目,我们就需要格外注意,每个定时器的时间,并不是所有定时器的时间都为0哦。
+function foo() {
+ console.log( this.a );
+}
+
+function doFoo() {
+ foo();
+}
+
+var obj = {
+ a: 1,
+ doFoo: doFoo
+};
+
+var a = 2;
+obj.doFoo()
+输出结果:2
+在Javascript中,this指向函数执行时的当前对象。在执行foo的时候,执行环境就是doFoo函数,执行环境为全局。所以,foo中的this是指向window的,所以会打印出2。
+var a = 10
+var obj = {
+ a: 20,
+ say: () => {
+ console.log(this.a)
+ }
+}
+obj.say()
+
+var anotherObj = { a: 30 }
+obj.say.apply(anotherObj)
+输出结果:10 10
+我么知道,箭头函数时不绑定this的,它的this来自原其父级所处的上下文,所以首先会打印全局中的 a 的值10。后面虽然让say方法指向了另外一个对象,但是仍不能改变箭头函数的特性,它的this仍然是指向全局的,所以依旧会输出10。
+但是,如果是普通函数,那么就会有完全不一样的结果:
+var a = 10
+var obj = {
+ a: 20,
+ say(){
+ console.log(this.a)
+ }
+}
+obj.say()
+var anotherObj={a:30}
+obj.say.apply(anotherObj)
+输出结果:20 30
+这时,say方法中的this就会指向他所在的对象,输出其中的a的值。
+function a() {
+ console.log(this);
+}
+a.call(null);
+打印结果:window对象
+根据ECMAScript262规范规定:如果第一个参数传入的对象调用者是null或者undefined,call方法将把全局对象(浏览器上是window对象)作为this的值。所以,不管传入null 还是 undefined,其this都是全局对象window。所以,在浏览器上答案是输出 window 对象。
+要注意的是,在严格模式中,null 就是 null,undefined 就是 undefined:
+'use strict';
+
+function a() {
+ console.log(this);
+}
+a.call(null); // null
+a.call(undefined); // undefined
+var obj = {
+ name : 'cuggz',
+ fun : function(){
+ console.log(this.name);
+ }
+}
+obj.fun() // cuggz
+new obj.fun() // undefined
+使用new构造函数时,其this指向的是全局环境window。
+var obj = {
+ say: function() {
+ var f1 = () => {
+ console.log("1111", this);
+ }
+ f1();
+ },
+ pro: {
+ getPro:() => {
+ console.log(this);
+ }
+ }
+}
+var o = obj.say;
+o();
+obj.say();
+obj.pro.getPro();
+输出结果:
+1111 window对象
+1111 obj对象
+window对象
+解析:
+var myObject = {
+ foo: "bar",
+ func: function() {
+ var self = this;
+ console.log(this.foo);
+ console.log(self.foo);
+ (function() {
+ console.log(this.foo);
+ console.log(self.foo);
+ }());
+ }
+};
+myObject.func();
+输出结果:bar bar undefined bar
+解析:
+window.number = 2;
+var obj = {
+ number: 3,
+ db1: (function(){
+ console.log(this);
+ this.number *= 4;
+ return function(){
+ console.log(this);
+ this.number *= 5;
+ }
+ })()
+}
+var db1 = obj.db1;
+db1();
+obj.db1();
+console.log(obj.number); // 15
+console.log(window.number); // 40
+这道题目看清起来有点乱,但是实际上是考察this指向的:
+var length = 10;
+function fn() {
+ console.log(this.length);
+}
+
+var obj = {
+ length: 5,
+ method: function(fn) {
+ fn();
+ arguments[0]();
+ }
+};
+
+obj.method(fn, 1);
+输出结果: 10 2
+解析:
+var a = 1;
+function printA(){
+ console.log(this.a);
+}
+var obj={
+ a:2,
+ foo:printA,
+ bar:function(){
+ printA();
+ }
+}
+
+obj.foo(); // 2
+obj.bar(); // 1
+var foo = obj.foo;
+foo(); // 1
+输出结果: 2 1 1
+解析:
+var x = 3;
+var y = 4;
+var obj = {
+ x: 1,
+ y: 6,
+ getX: function() {
+ var x = 5;
+ return function() {
+ return this.x;
+ }();
+ },
+ getY: function() {
+ var y = 7;
+ return this.y;
+ }
+}
+console.log(obj.getX()) // 3
+console.log(obj.getY()) // 6
+输出结果:3 6
+解析:
+ var a = 10;
+ var obt = {
+ a: 20,
+ fn: function(){
+ var a = 30;
+ console.log(this.a)
+ }
+ }
+ obt.fn(); // 20
+ obt.fn.call(); // 10
+ (obt.fn)(); // 20
+输出结果: 20 10 20
+解析:
+function a(xx){
+ this.x = xx;
+ return this
+};
+var x = a(5);
+var y = a(6);
+
+console.log(x.x) // undefined
+console.log(y.x) // 6
+输出结果: undefined 6
+解析:
+function foo(something){
+ this.a = something
+}
+
+var obj1 = {
+ foo: foo
+}
+
+var obj2 = {}
+
+obj1.foo(2);
+console.log(obj1.a); // 2
+
+obj1.foo.call(obj2, 3);
+console.log(obj2.a); // 3
+
+var bar = new obj1.foo(4)
+console.log(obj1.a); // 2
+console.log(bar.a); // 4
+输出结果: 2 3 2 4
+解析:
+function foo(something){
+ this.a = something
+}
+
+var obj1 = {}
+
+var bar = foo.bind(obj1);
+bar(2);
+console.log(obj1.a); // 2
+
+var baz = new bar(3);
+console.log(obj1.a); // 2
+console.log(baz.a); // 3
+输出结果: 2 2 3
+这道题目和上面题目差不多,主要都是考察this绑定的优先级。记住以下结论即可:this绑定的优先级:new绑定 > 显式绑定 > 隐式绑定 > 默认绑定。
+(function(){
+ var x = y = 1;
+})();
+var z;
+
+console.log(y); // 1
+console.log(z); // undefined
+console.log(x); // Uncaught ReferenceError: x is not defined
+这段代码的关键在于:var x = y = 1; 实际上这里是从右往左执行的,首先执行y = 1, 因为y没有使用var声明,所以它是一个全局变量,然后第二步是将y赋值给x,讲一个全局变量赋值给了一个局部变量,最终,x是一个局部变量,y是一个全局变量,所以打印x是报错。
+var a, b
+(function () {
+ console.log(a);
+ console.log(b);
+ var a = (b = 3);
+ console.log(a);
+ console.log(b);
+})()
+console.log(a);
+console.log(b);
+输出结果:
+undefined
+undefined
+3
+3
+undefined
+3
+这个题目和上面题目考察的知识点类似,b赋值为3,b此时是一个全局变量,而将3赋值给a,a是一个局部变量,所以最后打印的时候,a仍旧是undefined。
+var friendName = 'World';
+(function() {
+ if (typeof friendName === 'undefined') {
+ var friendName = 'Jack';
+ console.log('Goodbye ' + friendName);
+ } else {
+ console.log('Hello ' + friendName);
+ }
+})();
+输出结果:Goodbye Jack
+我们知道,在 JavaScript中, Function 和 var 都会被提升(变量提升),所以上面的代码就相当于:
+var name = 'World!';
+(function () {
+ var name;
+ if (typeof name === 'undefined') {
+ name = 'Jack';
+ console.log('Goodbye ' + name);
+ } else {
+ console.log('Hello ' + name);
+ }
+})();
+这样,答案就一目了然了。
+function fn1(){
+ console.log('fn1')
+}
+var fn2
+
+fn1()
+fn2()
+
+fn2 = function() {
+ console.log('fn2')
+}
+
+fn2()
+输出结果:
+fn1
+Uncaught TypeError: fn2 is not a function
+fn2
+这里也是在考察变量提升,关键在于第一个fn2(),这时fn2仍是一个undefined的变量,所以会报错fn2不是一个函数。
+function a() {
+ var temp = 10;
+ function b() {
+ console.log(temp); // 10
+ }
+ b();
+}
+a();
+
+function a() {
+ var temp = 10;
+ b();
+}
+function b() {
+ console.log(temp); // 报错 Uncaught ReferenceError: temp is not defined
+}
+a();
+在上面的两段代码中,第一段是可以正常输出,这个应该没啥问题,关键在于第二段代码,它会报错Uncaught ReferenceError: temp is not defined。这时因为在b方法执行时,temp 的值为undefined。
+ var a=3;
+ function c(){
+ alert(a);
+ }
+ (function(){
+ var a=4;
+ c();
+ })();
+js中变量的作用域链与定义时的环境有关,与执行时无关。执行环境只会改变this、传递的参数、全局变量等
+function fun(n, o) {
+ console.log(o)
+ return {
+ fun: function(m){
+ return fun(m, n);
+ }
+ };
+}
+var a = fun(0); a.fun(1); a.fun(2); a.fun(3);
+var b = fun(0).fun(1).fun(2).fun(3);
+var c = fun(0).fun(1); c.fun(2); c.fun(3);
+输出结果:
+undefined 0 0 0
+undefined 0 1 2
+undefined 0 1 1
+这是一道关于闭包的题目,对于fun方法,调用之后返回的是一个对象。我们知道,当调用函数的时候传入的实参比函数声明时指定的形参个数要少,剩下的形参都将设置为undefined值。所以 console.log(o); 会输出undefined。而a就是是fun(0)返回的那个对象。也就是说,函数fun中参数 n 的值是0,而返回的那个对象中,需要一个参数n,而这个对象的作用域中没有n,它就继续沿着作用域向上一级的作用域中寻找n,最后在函数fun中找到了n,n的值是0。了解了这一点,其他运算就很简单了,以此类推。
f = function() {return true;};
+g = function() {return false;};
+(function() {
+ if (g() && [] == ![]) {
+ f = function f() {return false;};
+ function g() {return true;}
+ }
+})();
+console.log(f());
+输出结果: false
+这里首先定义了两个变量f和g,我们知道变量是可以重新赋值的。后面是一个匿名自执行函数,在 if 条件中调用了函数 g(),由于在匿名函数中,又重新定义了函数g,就覆盖了外部定义的变量g,所以,这里调用的是内部函数 g 方法,返回为 true。第一个条件通过,进入第二个条件。
+第二个条件是[] == ![],先看 ![] ,在 JavaScript 中,当用于布尔运算时,比如在这里,对象的非空引用被视为 true,空引用 null 则被视为 false。由于这里不是一个 null, 而是一个没有元素的数组,所以 [] 被视为 true, 而 ![] 的结果就是 false 了。当一个布尔值参与到条件运算的时候,true 会被看作 1, 而 false 会被看作 0。现在条件变成了 [] == 0 的问题了,当一个对象参与条件比较的时候,它会被求值,求值的结果是数组成为一个字符串,[] 的结果就是 '' ,而 '' 会被当作 0 ,所以,条件成立。
+两个条件都成立,所以会执行条件中的代码, f 在定义是没有使用var,所以他是一个全局变量。因此,这里会通过闭包访问到外部的变量 f, 重新赋值,现在执行 f 函数返回值已经成为 false 了。而 g 则不会有这个问题,这里是一个函数内定义的 g,不会影响到外部的 g 函数。所以最后的结果就是 false。
+function Person(name) {
+ this.name = name
+}
+var p2 = new Person('king');
+console.log(p2.__proto__) //Person.prototype
+console.log(p2.__proto__.__proto__) //Object.prototype
+console.log(p2.__proto__.__proto__.__proto__) // null
+console.log(p2.__proto__.__proto__.__proto__.__proto__)//null后面没有了,报错
+console.log(p2.__proto__.__proto__.__proto__.__proto__.__proto__)//null后面没有了,报错
+console.log(p2.constructor)//Person
+console.log(p2.prototype)//undefined p2是实例,没有prototype属性
+console.log(Person.constructor)//Function 一个空函数
+console.log(Person.prototype)//打印出Person.prototype这个对象里所有的方法和属性
+console.log(Person.prototype.constructor)//Person
+console.log(Person.prototype.__proto__)// Object.prototype
+console.log(Person.__proto__) //Function.prototype
+console.log(Function.prototype.__proto__)//Object.prototype
+console.log(Function.__proto__)//Function.prototype
+console.log(Object.__proto__)//Function.prototype
+console.log(Object.prototype.__proto__)//null
+这道义题目考察原型、原型链的基础,记住就可以了。
+// a
+function Foo () {
+ getName = function () {
+ console.log(1);
+ }
+ return this;
+}
+// b
+Foo.getName = function () {
+ console.log(2);
+}
+// c
+Foo.prototype.getName = function () {
+ console.log(3);
+}
+// d
+var getName = function () {
+ console.log(4);
+}
+// e
+function getName () {
+ console.log(5);
+}
+
+Foo.getName(); // 2
+getName(); // 4
+Foo().getName(); // 1
+getName(); // 1
+new Foo.getName(); // 2
+new Foo().getName(); // 3
+new new Foo().getName(); // 3
+输出结果:2 4 1 1 2 3 3
+解析:
+var F = function() {};
+Object.prototype.a = function() {
+ console.log('a');
+};
+Function.prototype.b = function() {
+ console.log('b');
+}
+var f = new F();
+f.a();
+f.b();
+F.a();
+F.b()
+输出结果:
+a
+Uncaught TypeError: f.b is not a function
+a
+b
+解析:
+function Foo(){
+ Foo.a = function(){
+ console.log(1);
+ }
+ this.a = function(){
+ console.log(2)
+ }
+}
+
+Foo.prototype.a = function(){
+ console.log(3);
+}
+
+Foo.a = function(){
+ console.log(4);
+}
+
+Foo.a();
+let obj = new Foo();
+obj.a();
+Foo.a();
+输出结果:4 2 1
+解析:
+function Dog() {
+ this.name = 'puppy'
+}
+Dog.prototype.bark = () => {
+ console.log('woof!woof!')
+}
+const dog = new Dog()
+console.log(Dog.prototype.constructor === Dog && dog.constructor === Dog && dog instanceof Dog)
+输出结果:true
+解析: +因为constructor是prototype上的属性,所以dog.constructor实际上就是指向Dog.prototype.constructor;constructor属性指向构造函数。instanceof而实际检测的是类型是否在实例的原型链上。
+constructor是prototype上的属性,这一点很容易被忽略掉。constructor和instanceof 的作用是不同的,感性地来说,constructor的限制比较严格,它只能严格对比对象的构造函数是不是指定的值;而instanceof比较松散,只要检测的类型在原型链上,就会返回true。
+var A = {n: 4399};
+var B = function(){this.n = 9999};
+var C = function(){var n = 8888};
+B.prototype = A;
+C.prototype = A;
+var b = new B();
+var c = new C();
+A.n++
+console.log(b.n);
+console.log(c.n);
+输出结果:9999 4400
+解析:
+function A(){
+}
+function B(a){
+ this.a = a;
+}
+function C(a){
+ if(a){
+this.a = a;
+ }
+}
+A.prototype.a = 1;
+B.prototype.a = 1;
+C.prototype.a = 1;
+
+console.log(new A().a);
+console.log(new B().a);
+console.log(new C(2).a);
+输出结果:1 undefined 2
+解析:
+function Parent() {
+ this.a = 1;
+ this.b = [1, 2, this.a];
+ this.c = { demo: 5 };
+ this.show = function () {
+ console.log(this.a , this.b , this.c.demo );
+ }
+}
+
+function Child() {
+ this.a = 2;
+ this.change = function () {
+ this.b.push(this.a);
+ this.a = this.b.length;
+ this.c.demo = this.a++;
+ }
+}
+
+Child.prototype = new Parent();
+var parent = new Parent();
+var child1 = new Child();
+var child2 = new Child();
+child1.a = 11;
+child2.a = 12;
+parent.show();
+child1.show();
+child2.show();
+child1.change();
+child2.change();
+parent.show();
+child1.show();
+child2.show();
+输出结果:
+parent.show(); // 1 [1,2,1] 5
+
+child1.show(); // 11 [1,2,1] 5
+child2.show(); // 12 [1,2,1] 5
+
+parent.show(); // 1 [1,2,1] 5
+
+child1.show(); // 5 [1,2,1,11,12] 5
+
+child2.show(); // 6 [1,2,1,11,12] 5
+这道题目值得神帝,他涉及到的知识点很多,例如this的指向、原型、原型链、类的继承、数据类型等。
+解析:
+Child的构造函数原本是指向Child的,题目显式将Child类的原型对象指向了Parent类的一个实例,需要注意Child.prototype指向的是Parent的实例parent,而不是指向Parent这个类。parent是一个Parent类的实例,Child.prorotype指向的是Parent类的另一个实例,两者在堆内存中互不影响,所以上述操作不影响parent实例,所以输出结果不变;child1执行了change()方法后,发生了怎样的变化呢?Child.prototype上的b数组,this.a会指向child1的a属性,所以Child.prototype.b变成了**[1,2,1,11]**;this.a和this.b的指向与上一句一致,故结果为child1.a变为4;child1自身属性并没有c这个属性,所以此处的this.c会指向Child.prototype.c,this.a值为4,为原始类型,故赋值操作时会直接赋值,Child.prototype.c.demo的结果为4,而this.a随后自增为5(4 + 1 = 5)。child2执行了change()方法, 而child2和child1均是Child类的实例,所以他们的原型链指向同一个原型对象Child.prototype,也就是同一个parent实例,所以child2.change()中所有影响到原型对象的语句都会影响child1的最终输出结果。Child.prototype上的b数组,this.a会指向child2的a属性,所以Child.prototype.b变成了**[1,2,1,11,12]**;this.a和this.b的指向与上一句一致,故结果为child2.a变为5;child2自身属性并没有c这个属性,所以此处的this.c会指向Child.prototype.c,故执行结果为Child.prototype.c.demo的值变为child2.a的值5,而child2.a最终自增为6(5 + 1 = 6)。function SuperType(){
+ this.property = true;
+}
+
+SuperType.prototype.getSuperValue = function(){
+ return this.property;
+};
+
+function SubType(){
+ this.subproperty = false;
+}
+
+SubType.prototype = new SuperType();
+SubType.prototype.getSubValue = function (){
+ return this.subproperty;
+};
+
+var instance = new SubType();
+console.log(instance.getSuperValue());
+输出结果:true
+实际上,这段代码就是在实现原型链继承,SubType继承了SuperType,本质是重写了SubType的原型对象,代之以一个新类型的实例。SubType的原型被重写了,所以instance.constructor指向的是SuperType。具体如下:
+
写在最后:
+感谢您的耐心,读完了这么长的文章。读到这里,是否有一点收获呢?不得不说,这些面试题真是考验人的JavaScript基础能力,尤其是后面的原型和继承相关的题目,太绕了,值得仔细研究!
+近期,笔者在忙于毕业论文,可能更新会比较慢,还望大家见谅。
+如果觉得本文有帮助,记得点个赞哦。
\ No newline at end of file diff --git a/src/content/notes/frontend/css.html b/src/content/notes/frontend/css.html new file mode 100644 index 0000000..6025a5a --- /dev/null +++ b/src/content/notes/frontend/css.html @@ -0,0 +1,1246 @@ +| 选择器 | 格式 | 优先级权重 |
|---|---|---|
| id选择器 | #id | 100 |
| 类选择器 | #classname | 10 |
| 属性选择器 | a[ref=“eee”] | 10 |
| 伪类选择器 | li:last-child | 10 |
| 标签选择器 | div | 1 |
| 伪元素选择器 | li:after | 1 |
| 相邻兄弟选择器 | h1+p | 0 |
| 子选择器 | ul>li | 0 |
| 后代选择器 | li a | 0 |
| 通配符选择器 | * | 0 |
对于选择器的优先级:
+注意事项:
+一、无继承性的属性
+二、有继承性的属性
+| 属性值 | 作用 |
|---|---|
| none | 元素不显示,并且会从文档流中移除。 |
| block | 块类型。默认宽度为父元素宽度,可设置宽高,换行显示。 |
| inline | 行内元素类型。默认宽度为内容宽度,不可设置宽高,同行显示。 |
| inline-block | 默认宽度为内容宽度,可以设置宽高,同行显示。 |
| list-item | 像块类型元素一样显示,并添加样式列表标记。 |
| table | 此元素会作为块级表格来显示。 |
| inherit | 规定应该从父元素继承display属性的值。 |
(1)block: 会独占一行,多个元素会另起一行,可以设置width、height、margin和padding属性;
+(2)inline: 元素不会独占一行,设置width、height属性无效。但可以设置水平方向的margin和padding属性,不能设置垂直方向的padding和margin;
+(3)inline-block: 将对象设置为inline对象,但对象的内容作为block对象呈现,之后的内联对象会被排列在同一行内。
+对于行内元素和块级元素,其特点如下:
+(1)行内元素
+(2)块级元素
+两者都是外部引用CSS的方式,它们的区别如下:
+这两个属性都是让元素隐藏,不可见。两者区别如下:
+(1)在渲染树中
+display:none会让元素完全从渲染树中消失,渲染时不会占据任何空间;visibility:hidden不会让元素从渲染树中消失,渲染的元素还会占据相应的空间,只是内容不可见。(2)是否是继承属性
+display:none是非继承属性,子孙节点会随着父节点从渲染树消失,通过修改子孙节点的属性也无法显示;visibility:hidden是继承属性,子孙节点消失是由于继承了hidden,通过设置visibility:visible可以让子孙节点显示;
+(3)修改常规文档流中元素的 display 通常会造成文档的重排,但是修改visibility属性只会造成本元素的重绘;(4)如果使用读屏器,设置为display:none的内容不会被读取,设置为visibility:hidden的内容会被读取。
p::before {content:"第一章:";}
+p::after {content:"Hot!";}
+p::first-line {background:red;}
+p::first-letter {font-size:30px;}
+a:hover {color: #FF00FF}
+p:first-child {color: red}
+总结: 伪类是通过在元素选择器上加⼊伪类改变元素状态,⽽伪元素通过对元素的操作进⾏对元素的改变。
+实现动画效果的方法比较多,Javascript 中可以通过定时器 setTimeout 来实现,CSS3 中可以使用 transition 和 animation 来实现,HTML5 中的 canvas 也可以实现。除此之外,HTML5 提供一个专门用于请求动画的API,那就是 requestAnimationFrame,顾名思义就是请求动画帧。
+MDN对该方法的描述:
+++window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。
+
语法: window.requestAnimationFrame(callback); 其中,callback是下一次重绘之前更新动画帧所调用的函数(即上面所说的回调函数)。该回调函数会被传入DOMHighResTimeStamp参数,它表示requestAnimationFrame() 开始去执行回调函数的时刻。该方法属于宏任务,所以会在执行完微任务之后再去执行。
取消动画: 使用cancelAnimationFrame()来取消执行动画,该方法接收一个参数——requestAnimationFrame默认返回的id,只需要传入这个id就可以取消动画了。
+优势:
+setTimeout执行动画的缺点:它通过设定间隔时间来不断改变图像位置,达到动画效果。但是容易出现卡顿、抖动的现象;原因是:
+CSS3中的盒模型有以下两种:标准盒子模型、IE盒子模型
+
+
+盒模型都是由四个部分组成的,分别是margin、border、padding和content。
标准盒模型和IE盒模型的区别在于设置width和height时,所对应的范围不同:
+可以通过修改元素的box-sizing属性来改变元素的盒模型:
+box-sizeing: content-box表示标准盒模型(默认值)box-sizeing: border-box表示IE盒模型(怪异盒模型)translate 是 transform 属性的⼀个值。改变transform或opacity不会触发浏览器重新布局(reflow)或重绘(repaint),只会触发复合(compositions)。⽽改变绝对定位会触发重新布局,进⽽触发重绘和复合。transform使浏览器为元素创建⼀个 GPU 图层,但改变绝对定位会使⽤到 CPU。 因此translate()更⾼效,可以缩短平滑动画的绘制时间。 ⽽translate改变位置时,元素依然会占据其原始空间,绝对定位就不会发⽣这种情况。
+浏览器会把inline内联元素间的空白字符(空格、换行、Tab等)渲染成一个空格。为了美观,通常是一个<li>放在一行,这导致<li>换行后产生换行字符,它变成一个空格,占用了一个字符的宽度。
解决办法:
+(1)为<li>设置float:left。不足:有些容器是不能设置浮动,如左右切换的焦点图等。
(2)将所有<li>写在同一行。不足:代码不美观。
(3)将<ul>内的字符尺寸直接设为0,即font-size:0。不足:<ul>中的其他字符尺寸也被设为0,需要额外重新设定其他字符尺寸,且在Safari浏览器依然会出现空白间隔。
(4)消除<ul>的字符间隔letter-spacing:-8px,不足:这也设置了<li>内的字符间隔,因此需要将<li>内的字符间隔设为默认letter-spacing:normal。
通过修改某个属性值呈现的内容就可以被替换的元素就称为“替换元素”。
+替换元素除了内容可替换这一特性以外,还有以下特性:
+替换元素的尺寸从内而外分为三类:
+这三层结构的计算规则具体如下: +(1)如果没有CSS尺寸和HTML尺寸,则使用固有尺寸作为最终的宽高。 +(2)如果没有CSS尺寸,则使用HTML尺寸作为最终的宽高。 +(3)如果有CSS尺寸,则最终尺寸由CSS属性决定。 +(4)如果“固有尺寸”含有固有的宽高比例,同时仅设置了宽度或仅设置了高度,则元素依然按照固有的宽高比例显示。 +(5)如果上面的条件都不符合,则最终宽度表现为300像素,高度为150像素。 +(6)内联替换元素和块级替换元素使用上面同一套尺寸计算规则。
+(1)BMP,是无损的、既支持索引色也支持直接色的点阵图。这种图片格式几乎没有对数据进行压缩,所以BMP格式的图片通常是较大的文件。
+(2)GIF是无损的、采用索引色的点阵图。采用LZW压缩算法进行编码。文件小,是GIF格式的优点,同时,GIF格式还具有支持动画以及透明的优点。但是GIF格式仅支持8bit的索引色,所以GIF格式适用于对色彩要求不高同时需要文件体积较小的场景。
+(3)JPEG是有损的、采用直接色的点阵图。JPEG的图片的优点是采用了直接色,得益于更丰富的色彩,JPEG非常适合用来存储照片,与GIF相比,JPEG不适合用来存储企业Logo、线框类的图。因为有损压缩会导致图片模糊,而直接色的选用,又会导致图片文件较GIF更大。
+(4)PNG-8是无损的、使用索引色的点阵图。PNG是一种比较新的图片格式,PNG-8是非常好的GIF格式替代者,在可能的情况下,应该尽可能的使用PNG-8而不是GIF,因为在相同的图片效果下,PNG-8具有更小的文件体积。除此之外,PNG-8还支持透明度的调节,而GIF并不支持。除非需要动画的支持,否则没有理由使用GIF而不是PNG-8。
+(5)PNG-24是无损的、使用直接色的点阵图。PNG-24的优点在于它压缩了图片的数据,使得同样效果的图片,PNG-24格式的文件大小要比BMP小得多。当然,PNG24的图片还是要比JPEG、GIF、PNG-8大得多。
+(6)SVG是无损的矢量图。SVG是矢量图意味着SVG图片由直线和曲线以及绘制它们的方法组成。当放大SVG图片时,看到的还是线和曲线,而不会出现像素点。SVG图片在放大时,不会失真,所以它适合用来绘制Logo、Icon等。
+(7)WebP是谷歌开发的一种新图片格式,WebP是同时支持有损和无损压缩的、使用直接色的点阵图。从名字就可以看出来它是为Web而生的,什么叫为Web而生呢?就是说相同质量的图片,WebP具有更小的文件体积。现在网站上充满了大量的图片,如果能够降低每一个图片的文件大小,那么将大大减少浏览器和服务器之间的数据传输量,进而降低访问延迟,提升访问体验。目前只有Chrome浏览器和Opera浏览器支持WebP格式,兼容性不太好。
+CSSSprites(精灵图),将一个页面涉及到的所有图片都包含到一张大图中去,然后利用CSS的 background-image,background-repeat,background-position属性的组合进行背景定位。
+优点:
+CSS Sprites能很好地减少网页的http请求,从而大大提高了页面的性能,这是CSS Sprites最大的优点;CSS Sprites能减少图片的字节,把3张图片合并成1张图片的字节总是小于这3张图片的字节总和。缺点:
+CSSSprites在开发的时候相对来说有点麻烦,需要借助photoshop或其他工具来对每个背景单元测量其准确的位置。CSS Sprites在维护的时候比较麻烦,页面背景有少许改动时,就要改这张合并的图片,无需改的地方尽量不要动,这样避免改动更多的CSS,如果在原来的地方放不下,又只能(最好)往下加图片,这样图片的字节就增加了,还要改动CSS。以 iPhone XS 为例,当写 CSS 代码时,针对于单位 px,其宽度为 414px & 896px,也就是说当赋予一个 DIV元素宽度为 414px,这个 DIV 就会填满手机的宽度;
+而如果有一把尺子来实际测量这部手机的物理像素,实际为 1242*2688 物理像素;经过计算可知,1242/414=3,也就是说,在单边上,一个逻辑像素=3个物理像素,就说这个屏幕的像素密度为 3,也就是常说的 3 倍屏。
+对于图片来说,为了保证其不失真,1 个图片像素至少要对应一个物理像素,假如原始图片是 500300 像素,那么在 3 倍屏上就要放一个 1500900 像素的图片才能保证 1 个物理像素至少对应一个图片像素,才能不失真。
+
+当然,也可以针对所有屏幕,都只提供最高清图片。虽然低密度屏幕用不到那么多图片像素,而且会因为下载多余的像素造成带宽浪费和下载延迟,但从结果上说能保证图片在所有屏幕上都不会失真。
还可以使用 CSS 媒体查询来判断不同的像素密度,从而选择不同的图片:
+my-image { background: (low.png); }
+@media only screen and (min-device-pixel-ratio: 1.5) {
+ #my-image { background: (high.png); }
+}
+(1)line-height的概念:
+(2)line-height 的赋值方式:
+加载性能:
+(1)css压缩:将写好的css进行打包压缩,可以减小文件体积。
+(2)css单一样式:当需要下边距和左边距的时候,很多时候会选择使用 margin:top 0 bottom 0;但margin-bottom:bottom;margin-left:left;执行效率会更高。
+(3)减少使用@import,建议使用link,因为后者在页面加载时一起加载,前者是等待页面加载完成之后再进行加载。
+选择器性能:
+(1)关键选择器(key selector)。选择器的最后面的部分为关键选择器(即用来匹配目标元素的部分)。CSS选择符是从右到左进行匹配的。当使用后代选择器的时候,浏览器会遍历所有子元素来确定是否是指定的元素等等;
+(2)如果规则拥有ID选择器作为其关键选择器,则不要为规则增加标签。过滤掉无关的规则(这样样式系统就不会浪费时间去匹配它们了)。
+(3)避免使用通配规则,如*{}计算次数惊人,只对需要用到的元素进行选择。
+(4)尽量少的去对标签进行选择,而是用class。
+(5)尽量少的去使用后代选择器,降低选择器的权重值。后代选择器的开销是最高的,尽量将选择器的深度降到最低,最高不要超过三层,更多的使用类来关联每一个标签元素。
+(6)了解哪些属性是可以通过继承而来的,然后避免对这些属性重复指定规则。
+渲染性能:
+(1)慎重使用高性能属性:浮动、定位。
+(2)尽量减少页面重排、重绘。
+(3)去除空规则:{}。空规则的产生原因一般来说是为了预留样式。去除这些空规则无疑能减少css文档体积。
+(4)属性值为0时,不加单位。
+(5)属性值为浮动小数0.**,可以省略小数点之前的0。
+(6)标准化各种浏览器前缀:带浏览器前缀的在前。标准属性在后。
+(7)不使用@import前缀,它会影响css的加载速度。
+(8)选择器优化嵌套,尽量避免层级过深。
+(9)css雪碧图,同一页面相近部分的小图标,方便使用,减少页面的请求次数,但是同时图片本身会变大,使用时,优劣考虑清楚,再使用。
+(10)正确使用display的属性,由于display的作用,某些样式组合会无效,徒增样式体积的同时也影响解析性能。
+(11)不滥用web字体。对于中文网站来说WebFonts可能很陌生,国外却很流行。web fonts通常体积庞大,而且一些浏览器在下载web fonts时会阻塞页面渲染损伤性能。
+可维护性、健壮性:
+(1)将具有相同属性的样式抽离出来,整合并通过class在页面中进行使用,提高css的可维护性。
+(2)样式与内容分离:将css代码定义到外部css中。
+预处理器, 如:less,sass,stylus,用来预编译sass或者less,增加了css代码的复用性。层级,mixin, 变量,循环, 函数等对编写以及开发UI组件都极为方便。
后处理器, 如: postCss,通常是在完成的样式表中根据css规范处理css,让其更加有效。目前最常做的是给css属性添加浏览器私有前缀,实现跨浏览器兼容性的问题。
css预处理器为css增加一些编程特性,无需考虑浏览器的兼容问题,可以在CSS中使用变量,简单的逻辑程序,函数等在编程语言中的一些基本的性能,可以让css更加的简洁,增加适应性以及可读性,可维护性等。
其它css预处理器语言:Sass(Scss), Less, Stylus, Turbine, Swithch css, CSS Cacheer, DT Css。
使用原因:
+CSS代码,可以应用到老项目中(1)冒号(:)用于CSS3伪类,双冒号(::)用于CSS3伪元素。
+(2)::before就是以一个子元素的存在,定义在元素主体内容之前的一个伪元素。并不存在于dom之中,只存在在页面之中。
注意: :before 和 :after 这两个伪元素,是在CSS2.1里新出现的。起初,伪元素的前缀使用的是单冒号语法,但随着Web的进化,在CSS3的规范里,伪元素的语法被修改成使用双冒号,成为::before、::after。
margin正值时,可以让margin使用负值解决;font-size时,可通过设置font-size:0、letter-spacing、word-spacing解决;overflow: hidden; // 溢出隐藏
+text-overflow: ellipsis; // 溢出用省略号显示
+white-space: nowrap; // 规定段落中的文本不进行换行
+overflow: hidden; // 溢出隐藏
+text-overflow: ellipsis; // 溢出用省略号显示
+display:-webkit-box; // 作为弹性伸缩盒子模型显示。
+-webkit-box-orient:vertical; // 设置伸缩盒子的子元素排列方式:从上到下垂直排列
+-webkit-line-clamp:3; // 显示的行数
+注意:由于上面的三个属性都是 CSS3 的属性,没有浏览器可以兼容,所以要在前面加一个-webkit- 来兼容一部分浏览器。
他们都是 CSS 预处理器,是 CSS 上的一种抽象层。他们是一种特殊的语法/语言编译成 CSS。 例如 Less 是一种动态样式语言,将 CSS 赋予了动态语言的特性,如变量,继承,运算, 函数,LESS 既可以在客户端上运行 (支持 IE 6+, Webkit, Firefox),也可以在服务端运行 (借助 Node.js)。
+为什么要使用它们?
+媒体查询由⼀个可选的媒体类型和零个或多个使⽤媒体功能的限制了样式表范围的表达式组成,例如宽度、⾼度和颜⾊。媒体查询,添加⾃CSS3,允许内容的呈现针对⼀个特定范围的输出设备⽽进⾏裁剪,⽽不必改变内容本身,适合web⽹⻚应对不同型号的设备⽽做出对应的响应适配。
+媒体查询包含⼀个可选的媒体类型和满⾜CSS3规范的条件下,包含零个或多个表达式,这些表达式描述了媒体特征,最终会被解析为true或false。如果媒体查询中指定的媒体类型匹配展示⽂档所使⽤的设备类型,并且所有的表达式的值都是true,那么该媒体查询的结果为true。那么媒体查询内的样式将会⽣效。
+<!-- link元素中的CSS媒体查询 -->
+<link rel="stylesheet" media="(max-width: 800px)" href="example.css" />
+<!-- 样式表中的CSS媒体查询 -->
+<style>
+@media (max-width: 600px) {
+ .facet_sidebar {
+ display: none;
+ }
+}
+</style>
+简单来说,使用 @media 查询,可以针对不同的媒体类型定义不同的样式。@media 可以针对不同的屏幕尺寸设置不同的样式,特别是需要设置设计响应式的页面,@media 是非常有用的。当重置浏览器大小的过程中,页面也会根据浏览器的宽度和高度重新渲染页面。
+CSS 工程化是为了解决以下问题:
+以下三个方向都是时下比较流行的、普适性非常好的 CSS 工程化实践:
+基于这三个方向,可以衍生出一些具有典型意义的子问题,这里我们逐个来看:
+(1)预处理器:为什么要用预处理器?它的出现是为了解决什么问题?
+预处理器,其实就是 CSS 世界的“轮子”。预处理器支持我们写一种类似 CSS、但实际并不是 CSS 的语言,然后把它编译成 CSS 代码:
+
+那为什么写 CSS 代码写得好好的,偏偏要转去写“类 CSS”呢?这就和本来用 JS 也可以实现所有功能,但最后却写 React 的 jsx 或者 Vue 的模板语法一样——为了爽!要想知道有了预处理器有多爽,首先要知道的是传统 CSS 有多不爽。随着前端业务复杂度的提高,前端工程中对 CSS 提出了以下的诉求:
这三点是传统 CSS 所做不到的,也正是预处理器所解决掉的问题。预处理器普遍会具备这样的特性:
+(2)PostCss:PostCss 是如何工作的?我们在什么场景下会使用 PostCss?
+
+它和预处理器的不同就在于,预处理器处理的是 类CSS,而 PostCss 处理的就是 CSS 本身。Babel 可以将高版本的 JS 代码转换为低版本的 JS 代码。PostCss 做的是类似的事情:它可以编译尚未被浏览器广泛支持的先进的 CSS 语法,还可以自动为一些需要额外兼容的语法增加前缀。更强的是,由于 PostCss 有着强大的插件机制,支持各种各样的扩展,极大地强化了 CSS 的能力。
PostCss 在业务中的使用场景非常多:
+(3)Webpack 能处理 CSS 吗?如何实现? +Webpack 能处理 CSS 吗:
+如何用 Webpack 实现对 CSS 的处理:
+在实际使用中,css-loader 的执行顺序一定要安排在 style-loader 的前面。因为只有完成了编译过程,才可以对 css 代码进行插入;若提前插入了未编译的代码,那么 webpack 是无法理解这坨东西的,它会无情报错。
+以图片显示为例:
+window.innerHeight 是浏览器可视区的高度;document.body.scrollTop || document.documentElement.scrollTop 是浏览器滚动的过的距离;imgs.offsetTop 是元素顶部距离文档顶部的高度(包括滚动条的距离);img.offsetTop < window.innerHeight + document.body.scrollTop;通常 z-index 的使用是在有两个重叠的标签,在一定的情况下控制其中一个在另一个的上方或者下方出现。z-index值越大就越是在上层。z-index元素的position属性需要是relative,absolute或是fixed。
+z-index属性在下列情况下会失效:
+常用的布局单位包括像素(px),百分比(%),em,rem,vw/vh。
(1)像素(px)是页面布局的基础,一个像素表示终端(电脑、手机、平板等)屏幕所能显示的最小的区域,像素分为两种类型:CSS像素和物理像素:
(2)百分比(%),当浏览器的宽度或者高度发生变化时,通过百分比单位可以使得浏览器中的组件的宽和高随着浏览器的变化而变化,从而实现响应式的效果。一般认为子元素的百分比相对于直接父元素。
(3)em和rem相对于px更具灵活性,它们都是相对长度单位,它们之间的区别:em相对于父元素,rem相对于根元素。
+(4)vw/vh是与视图窗口有关的单位,vw表示相对于视图窗口的宽度,vh表示相对于视图窗口高度,除了vw和vh外,还有vmin和vmax两个相关的单位。
+vw/vh 和百分比很类似,两者的区别:
+%):大部分相对于祖先元素,也有相对于自身的情况比如(border-radius、translate等)三者的区别:
+使用场景:
+一般两栏布局指的是左边一栏宽度固定,右边一栏宽度自适应,两栏布局的具体实现:
+.outer {
+ height: 100px;
+}
+.left {
+ float: left;
+ width: 200px;
+ background: tomato;
+}
+.right {
+ margin-left: 200px;
+ width: auto;
+ background: gold;
+}
+.left{
+ width: 100px;
+ height: 200px;
+ background: red;
+ float: left;
+ }
+ .right{
+ height: 300px;
+ background: blue;
+ overflow: hidden;
+ }
+.outer {
+ display: flex;
+ height: 100px;
+}
+.left {
+ width: 200px;
+ background: tomato;
+}
+.right {
+ flex: 1;
+ background: gold;
+}
+.outer {
+ position: relative;
+ height: 100px;
+}
+.left {
+ position: absolute;
+ width: 200px;
+ height: 100px;
+ background: tomato;
+}
+.right {
+ margin-left: 200px;
+ background: gold;
+}
+.outer {
+ position: relative;
+ height: 100px;
+}
+.left {
+ width: 200px;
+ background: tomato;
+}
+.right {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 200px;
+ background: gold;
+}
+三栏布局一般指的是页面中一共有三栏,左右两栏宽度固定,中间自适应的布局,三栏布局的具体实现:
+.outer {
+ position: relative;
+ height: 100px;
+}
+
+.left {
+ position: absolute;
+ width: 100px;
+ height: 100px;
+ background: tomato;
+}
+
+.right {
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 200px;
+ height: 100px;
+ background: gold;
+}
+
+.center {
+ margin-left: 100px;
+ margin-right: 200px;
+ height: 100px;
+ background: lightgreen;
+}
+.outer {
+ display: flex;
+ height: 100px;
+}
+
+.left {
+ width: 100px;
+ background: tomato;
+}
+
+.right {
+ width: 100px;
+ background: gold;
+}
+
+.center {
+ flex: 1;
+ background: lightgreen;
+}
+.outer {
+ height: 100px;
+}
+
+.left {
+ float: left;
+ width: 100px;
+ height: 100px;
+ background: tomato;
+}
+
+.right {
+ float: right;
+ width: 200px;
+ height: 100px;
+ background: gold;
+}
+
+.center {
+ height: 100px;
+ margin-left: 100px;
+ margin-right: 200px;
+ background: lightgreen;
+}
+.outer {
+ height: 100px;
+ padding-left: 100px;
+ padding-right: 200px;
+}
+
+.left {
+ position: relative;
+ left: -100px;
+
+ float: left;
+ margin-left: -100%;
+
+ width: 100px;
+ height: 100px;
+ background: tomato;
+}
+
+.right {
+ position: relative;
+ left: 200px;
+
+ float: right;
+ margin-left: -200px;
+
+ width: 200px;
+ height: 100px;
+ background: gold;
+}
+
+.center {
+ float: left;
+
+ width: 100%;
+ height: 100px;
+ background: lightgreen;
+}
+.outer {
+ height: 100px;
+}
+
+.left {
+ float: left;
+ margin-left: -100%;
+
+ width: 100px;
+ height: 100px;
+ background: tomato;
+}
+
+.right {
+ float: left;
+ margin-left: -200px;
+
+ width: 200px;
+ height: 100px;
+ background: gold;
+}
+
+.wrapper {
+ float: left;
+
+ width: 100%;
+ height: 100px;
+ background: lightgreen;
+}
+
+.center {
+ margin-left: 100px;
+ margin-right: 200px;
+ height: 100px;
+}
+.parent { position: relative;} .child { position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%);}
+.parent {
+ position: relative;
+}
+
+.child {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ margin: auto;
+}
+.parent {
+ position: relative;
+}
+
+.child {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ margin-top: -50px; /* 自身 height 的一半 */
+ margin-left: -50px; /* 自身 width 的一半 */
+}
+.parent {
+ display: flex;
+ justify-content:center;
+ align-items:center;
+}
+移动端适配主要有两个维度:
+为了能让页面的尺寸自适应,可以使用 rem,em,vw,vh 等相对单位。
+Flex是FlexibleBox的缩写,意为"弹性布局",用来为盒状模型提供最大的灵活性。任何一个容器都可以指定为Flex布局。行内元素也可以使用Flex布局。注意,设为Flex布局以后,子元素的float、clear和vertical-align属性将失效。采用Flex布局的元素,称为Flex容器(flex container),简称"容器"。它的所有子元素自动成为容器成员,称为Flex项目(flex item),简称"项目"。容器默认存在两根轴:水平的主轴(main axis)和垂直的交叉轴(cross axis),项目默认沿水平主轴排列。
+以下6个属性设置在容器上:
+以下6个属性设置在项目上:
+简单来说: +flex布局是CSS3新增的一种布局方式,可以通过将一个元素的display属性值设置为flex从而使它成为一个flex容器,它的所有子元素都会成为它的项目。一个容器默认有两条轴:一个是水平的主轴,一个是与主轴垂直的交叉轴。可以使用flex-direction来指定主轴的方向。可以使用justify-content来指定元素在主轴上的排列方式,使用align-items来指定元素在交叉轴上的排列方式。还可以使用flex-wrap来规定当一行排列不下时的换行方式。对于容器中的项目,可以使用order属性来指定项目的排列顺序,还可以使用flex-grow来指定当排列空间有剩余的时候,项目的放大比例,还可以使用flex-shrink来指定当排列空间不足时,项目的缩小比例。
+响应式网站设计(Responsive Web design)是一个网站能够兼容多个终端,而不是为每一个终端做一个特定的版本。
关于原理: 基本原理是通过媒体查询(@media)查询检测不同的设备屏幕尺寸做处理。
+关于兼容: 页面头部必须有mate声明的viewport。
<meta name="’viewport’" content="”width=device-width," initial-scale="1." maximum-scale="1,user-scalable=no”"/>
+浮动的定义: 非IE浏览器下,容器不设高度且子元素浮动时,容器高度不能被内容撑开。 此时,内容会溢出到容器外面而影响布局。这种现象被称为浮动(溢出)。
+浮动的工作原理:
+浮动元素可以左右移动,直到遇到另一个浮动元素或者遇到它外边缘的包含框。浮动框不属于文档流中的普通流,当元素浮动之后,不会影响块级元素的布局,只会影响内联元素布局。此时文档流中的普通流就会表现得该浮动框不存在一样的布局模式。当包含框的高度小于浮动框的时候,此时就会出现“高度塌陷”。
+浮动元素引起的问题?
+清除浮动的方式如下:
+height属性clear:both样式overflow:hidden或者overflow:auto.clearfix:after{
+ content: "\200B";
+ display: table;
+ height: 0;
+ clear: both;
+ }
+ .clearfix{
+ *zoom: 1;
+ }
+使用clear属性清除浮动,其语法如下:
+clear:none|left|right|both
+如果单看字面意思,clear:left 是“清除左浮动”,clear:right 是“清除右浮动”,实际上,这种解释是有问题的,因为浮动一直还在,并没有清除。
+官方对clear属性解释:“元素盒子的边不能和前面的浮动元素相邻”,对元素设置clear属性是为了避免浮动元素对该元素的影响,而不是清除掉浮动。
+还需要注意 clear 属性指的是元素盒子的边不能和前面的浮动元素相邻,注意这里“前面的”3个字,也就是clear属性对“后面的”浮动元素是不闻不问的。考虑到float属性要么是left,要么是right,不可能同时存在,同时由于clear属性对“后面的”浮动元素不闻不问,因此,当clear:left有效的时候,clear:right必定无效,也就是此时clear:left等同于设置clear:both;同样地,clear:right如果有效也是等同于设置clear:both。由此可见,clear:left和clear:right这两个声明就没有任何使用的价值,至少在CSS世界中是如此,直接使用clear:both吧。
+一般使用伪元素的方式清除浮动:
+.clear::after{ content:''; display: block; clear:both;}
+clear属性只有块级元素才有效的,而::after等伪元素默认都是内联水平,这就是借助伪元素清除浮动影响时需要设置display属性值的原因。
+先来看两个相关的概念:
+块格式化上下文(Block Formatting Context,BFC)是Web页面的可视化CSS渲染的一部分,是布局过程中生成块级盒子的区域,也是浮动元素与其他元素的交互限定区域。
+通俗来讲:BFC是一个独立的布局环境,可以理解为一个容器,在这个容器中按照一定规则进行物品摆放,并且不会影响其它环境中的物品。如果一个元素符合触发BFC的条件,则BFC中的元素布局不受外部影响。
+创建BFC的条件:
+BFC的特点:
+BFC的作用:
+overflow:hidden。.left{
+ width: 100px;
+ height: 200px;
+ background: red;
+ float: left;
+ }
+ .right{
+ height: 300px;
+ background: blue;
+ overflow: hidden;
+ }
+
+<div class="left"></div>
+<div class="right"></div>
+左侧设置float:left,右侧设置overflow: hidden。这样右边就触发了BFC,BFC的区域不会与浮动元素发生重叠,所以两侧就不会发生重叠,实现了自适应两栏布局。
问题描述: +两个块级元素的上外边距和下外边距可能会合并(折叠)为一个外边距,其大小会取其中外边距值大的那个,这种行为就是外边距折叠。需要注意的是,浮动的元素和绝对定位这种脱离文档流的元素的外边距不会折叠。重叠只会出现在垂直方向。
+计算原则: +折叠合并后外边距的计算原则如下:
+解决办法: +对于折叠的情况,主要有两种:兄弟之间重叠和父子之间重叠 +(1)兄弟之间重叠
+display: inline-blockfloatabsolute/fixed(2)父子之间重叠
+overflow: hiddenborder:1px solid transparentdisplay: inline-block层叠顺序,英文称作 stacking order,表示元素发生层叠时有着特定的垂直显示顺序。下面是盒模型的层叠规则:
+
+对于上图,由上到下分别是:
+(1)背景和边框:建立当前层叠上下文元素的背景和边框。
+(2)负的z-index:当前层叠上下文中,z-index属性值为负的元素。
+(3)块级盒:文档流内非行内级非定位后代元素。
+(4)浮动盒:非定位浮动元素。
+(5)行内盒:文档流内行内级非定位后代元素。
+(6)z-index:0:层叠级数为0的定位元素。
+(7)正z-index:z-index属性值为正的定位元素。
注意: 当定位元素z-index:auto,生成盒在当前层叠上下文中的层级为 0,不会建立新的层叠上下文,除非是根元素。
+position有以下属性值:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +| 属性值 | 概述 |
|---|---|
| absolute | 生成绝对定位的元素,相对于static定位以外的一个父元素进行定位。元素的位置通过left、top、right、bottom属性进行规定。 |
| relative | 生成相对定位的元素,相对于其原来的位置进行定位。元素的位置通过left、top、right、bottom属性进行规定。 |
| fixed | 生成绝对定位的元素,指定元素相对于屏幕视⼝(viewport)的位置来指定元素位置。元素的位置在屏幕滚动时不会改变,⽐如回到顶部的按钮⼀般都是⽤此定位⽅式。 |
| static | 默认值,没有定位,元素出现在正常的文档流中,会忽略 top, bottom, left, right 或者 z-index 声明,块级元素从上往下纵向排布,⾏级元素从左向右排列。 |
| inherit | 规定从父元素继承position属性的值 |
前面三者的定位方式如下:
+position:relative/absolute/fixed的元素,就以该元素为基准定位,如果没找到,就以浏览器边界定位。如下两个图所示:
+
(1)首先判断display属性是否为none,如果为none,则position和float属性的值不影响元素最后的表现。
+(2)然后判断position的值是否为absolute或者fixed,如果是,则float属性失效,并且display的值应该被设置为table或者block,具体转换需要看初始转换值。
+(3)如果position的值不为absolute或者fixed,则判断float属性的值是否为none,如果不是,则display的值则按上面的规则转换。注意,如果position的值为relative并且float属性的值存在,则relative相对于浮动后的最终位置定位。
+(4)如果float的值为none,则判断元素是否为根元素,如果是根元素则display属性按照上面的规则转换,如果不是,则保持指定的display属性值不变。
+总的来说,可以把它看作是一个类似优先级的机制,"position:absolute"和"position:fixed"优先级最高,有它存在的时候,浮动不起作用,'display'的值也需要调整;其次,元素的'float'特性的值不是"none"的时候或者它是根元素的时候,调整'display'的值;最后,非根元素,并且非浮动元素,并且非绝对定位的元素,'display'特性值同设置值。
+共同点:
+不同点:
+sticky 英文字面意思是粘贴,所以可以把它称之为粘性定位。语法:position: sticky; 基于用户的滚动位置来定位。
+粘性定位的元素是依赖于用户的滚动,在 position:relative 与 position:fixed 定位之间切换。它的行为就像 position:relative; 而当页面滚动超出目标区域时,它的表现就像 position:fixed;,它会固定在目标位置。元素定位表现为在跨越特定阈值前为相对定位,之后为固定定位。这个特定阈值指的是 top, right, bottom 或 left 之一,换言之,指定 top, right, bottom 或 left 四个阈值其中之一,才可使粘性定位生效。否则其行为与相对定位相同。
+CSS绘制三角形主要用到的是border属性,也就是边框。
+平时在给盒子设置边框时,往往都设置很窄,就可能误以为边框是由矩形组成的。实际上,border属性是右三角形组成的,下面看一个例子:
+div {
+ width: 0;
+ height: 0;
+ border: 100px solid;
+ border-color: orange blue red green;
+}
+将元素的长宽都设置为0,显示出来的效果是这样的:
+
+所以可以根据border这个特性来绘制三角形:
+(1)三角1
div { width: 0; height: 0; border-top: 50px solid red; border-right: 50px solid transparent; border-left: 50px solid transparent;}
+
+(2)三角2
div {
+ width: 0;
+ height: 0;
+ border-bottom: 50px solid red;
+ border-right: 50px solid transparent;
+ border-left: 50px solid transparent;
+}
+
+(3)三角3
div {
+ width: 0;
+ height: 0;
+ border-left: 50px solid red;
+ border-top: 50px solid transparent;
+ border-bottom: 50px solid transparent;
+}
+
+(4)三角4
div {
+ width: 0;
+ height: 0;
+ border-right: 50px solid red;
+ border-top: 50px solid transparent;
+ border-bottom: 50px solid transparent;
+}
+
+(5)三角5
div {
+ width: 0;
+ height: 0;
+ border-top: 100px solid red;
+ border-right: 100px solid transparent;
+}
+
+还有很多,就不一一实现了,总体的原则就是通过上下左右边框来控制三角形的方向,用边框的宽度比来控制三角形的角度。
用CSS实现扇形的思路和三角形基本一致,就是多了一个圆角的样式,实现一个90°的扇形:
+div{
+ border: 100px solid transparent;
+ width: 0;
+ heigt: 0;
+ border-radius: 100px;
+ border-top-color: red;
+}
+.square {
+ width: 10%;
+ height: 10vw;
+ background: tomato;
+}
+.square {
+ width: 20%;
+ height: 0;
+ padding-top: 20%;
+ background: orange;
+}
+.square {
+ width: 30%;
+ overflow: hidden;
+ background: yellow;
+}
+.square::after {
+ content: '';
+ display: block;
+ margin-top: 100%;
+}
+transform: scale(0.5,0.5);
+<meta name="viewport" content="width=device-width, initial-scale=0.5, minimum-scale=0.5, maximum-scale=0.5"/>
+这样就能缩放到原来的0.5倍,如果是1px那么就会变成0.5px。viewport只针对于移动端,只在移动端上才能看到效果
+在谷歌下css设置字体大小为12px及以下时,显示都是一样大小,都是默认12px。
+解决办法:
+1px 问题指的是:在一些 Retina屏幕 的机型上,移动端页面的 1px 会变得很粗,呈现出不止 1px 的效果。原因很简单——CSS 中的 1px 并不能和移动设备上的 1px 划等号。它们之间的比例关系有一个专门的属性来描述:
window.devicePixelRatio = 设备的物理像素 / CSS像素。
+打开 Chrome 浏览器,启动移动端调试模式,在控制台去输出这个 devicePixelRatio 的值。这里选中 iPhone6/7/8 这系列的机型,输出的结果就是2:
+
+这就意味着设置的 1px CSS 像素,在这个设备上实际会用 2 个物理像素单元来进行渲染,所以实际看到的一定会比 1px 粗一些。
+解决1px 问题的三种思路:
如果之前 1px 的样式这样写:
+border:1px solid #333
+可以先在 JS 中拿到 window.devicePixelRatio 的值,然后把这个值通过 JSX 或者模板语法给到 CSS 的 data 里,达到这样的效果(这里用 JSX 语法做示范):
+<div id="container" data-device={{window.devicePixelRatio}}></div>
+然后就可以在 CSS 中用属性选择器来命中 devicePixelRatio 为某一值的情况,比如说这里尝试命中 devicePixelRatio 为2的情况:
+#container[data-device="2"] {
+ border:0.5px solid #333
+}
+直接把 1px 改成 1/devicePixelRatio 后的值,这是目前为止最简单的一种方法。这种方法的缺陷在于兼容性不行,IOS 系统需要8及以上的版本,安卓系统则直接不兼容。
+这个方法的可行性会更高,兼容性也更好。唯一的缺点是代码会变多。
+思路是先放大、后缩小:在目标元素的后面追加一个 ::after 伪元素,让这个元素布局为 absolute 之后、整个伸展开铺在目标元素上,然后把它的宽和高都设置为目标元素的两倍,border值设为 1px。接着借助 CSS 动画特效中的放缩能力,把整个伪元素缩小为原来的 50%。此时,伪元素的宽高刚好可以和原有的目标元素对齐,而 border 也缩小为了 1px 的二分之一,间接地实现了 0.5px 的效果。
+代码如下:
+#container[data-device="2"] {
+ position: relative;
+}
+#container[data-device="2"]::after{
+ position:absolute;
+ top: 0;
+ left: 0;
+ width: 200%;
+ height: 200%;
+ content:"";
+ transform: scale(0.5);
+ transform-origin: left top;
+ box-sizing: border-box;
+ border: 1px solid #333;
+ }
+}
+这个思路就是对 meta 标签里几个关键属性下手:
+<meta name="viewport" content="initial-scale=0.5, maximum-scale=0.5, minimum-scale=0.5, user-scalable=no">
+这里针对像素比为2的页面,把整个页面缩放为了原来的1/2大小。这样,本来占用2个物理像素的 1px 样式,现在占用的就是标准的一个物理像素。根据像素比的不同,这个缩放比例可以被计算为不同的值,用 js 代码实现如下:
+const scale = 1 / window.devicePixelRatio;
+// 这里 metaEl 指的是 meta 标签对应的 Dom
+metaEl.setAttribute('content', `width=device-width,user-scalable=no,initial-scale=${scale},maximum-scale=${scale},minimum-scale=${scale}`);
+这样解决了,但这样做的副作用也很大,整个页面被缩放了。这时 1px 已经被处理成物理像素大小,这样的大小在手机上显示边框很合适。但是,一些原本不需要被缩小的内容,比如文字、图片等,也被无差别缩小掉了。
\ No newline at end of file diff --git a/src/content/notes/frontend/html.html b/src/content/notes/frontend/html.html new file mode 100644 index 0000000..52ed15d --- /dev/null +++ b/src/content/notes/frontend/html.html @@ -0,0 +1,372 @@ +src和href都是用来引用外部的资源,它们的区别如下:
+语义化是指根据内容的结构化(内容语义化),选择合适的标签(代码语义化)。通俗来讲就是用正确的标签做正确的事情。
+语义化的优点如下:
+常见的语义化标签:
+<header></header> 头部
+
+<nav></nav> 导航栏
+
+<section></section> 区块(有语义化的div)
+
+<main></main> 主要区域
+
+<article></article> 主要内容
+
+<aside></aside> 侧边栏
+
+<footer></footer> 底部
+DOCTYPE是HTML5中一种标准通用标记语言的文档类型声明,它的目的是告诉浏览器(解析器)应该以什么样(html或xhtml)的文档类型定义来解析文档,不同的渲染模式会影响浏览器对 CSS 代码甚⾄ JavaScript 脚本的解析。它必须声明在HTML⽂档的第⼀⾏。
+浏览器渲染页面的两种模式(可通过document.compatMode获取,比如,语雀官网的文档类型是CSS1Compat):
+如果没有defer或async属性,浏览器会立即加载并执行相应的脚本。它不会等待后续加载的文档元素,读取到就会开始加载和执行,这样就阻塞了后续文档的加载。
+下图可以直观的看出三者之间的区别:
+
+其中蓝色代表js脚本网络加载时间,红色代表js脚本执行时间,绿色代表html解析。
defer 和 async属性都是去异步加载外部的JS脚本文件,它们都不会阻塞页面的解析,其区别如下:
+meta 标签由 name 和 content 属性定义,用来描述网页文档的属性,比如网页的作者,网页描述,关键词等,除了HTTP标准固定了一些name作为大家使用的共识,开发者还可以自定义name。
常用的meta标签:
+(1)charset,用来描述HTML文档的编码类型:
<meta charset="UTF-8" >
+(2) keywords,页面关键词:
<meta name="keywords" content="关键词" />
+(3)description,页面描述:
<meta name="description" content="页面描述内容" />
+(4)refresh,页面重定向和刷新:
<meta http-equiv="refresh" content="0;url=" />
+(5)viewport,适配移动端,可以控制视口的大小和比例:
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
+其中,content 参数有以下几种:
width viewport :宽度(数值/device-width)height viewport :高度(数值/device-height)initial-scale :初始缩放比例maximum-scale :最大缩放比例minimum-scale :最小缩放比例user-scalable :是否允许用户缩放(yes/no)(6)搜索引擎索引方式:
+<meta name="robots" content="index,follow" />
+其中,content 参数有以下几种:
all:文件将被检索,且页面上的链接可以被查询;none:文件将不被检索,且页面上的链接不可以被查询;index:文件将被检索;follow:页面上的链接可以被查询;noindex:文件将不被检索;nofollow:页面上的链接不可以被查询。(1) audio:音频
+<audio src='' controls autoplay loop='true'></audio>
+属性:
+(2)video视频
+<video src='' poster='imgs/aa.jpg' controls></video>
+属性:
+(3)source标签 +因为浏览器对视频格式支持程度不一样,为了能够兼容不同的浏览器,可以通过source来指定视频源。
+<video>
+ <source src='aa.flv' type='video/flv'></source>
+ <source src='aa.mp4' type='video/mp4'></source>
+</video>
+表单类型:
+表单属性:
+表单事件:
+设置规则:min < low < high < max
+它们选择的对象可以是标签,可以是类(需要加点),可以是ID(需要加#)
+HTML5 提供了两种在客户端存储数据的新方法:
+<img draggable="true" />
+<canvas id="myCanvas" width="200" height="100"></canvas>
+总结: +(1)新增语义化标签:nav、header、footer、aside、section、article +(2)音频、视频标签:audio、video +(3)数据存储:localStorage、sessionStorage +(4)canvas(画布)、Geolocation(地理定位)、websocket(通信协议) +(5)input标签新增属性:placeholder、autocomplete、autofocus、required +(6)history API:go、forward、back、pushstate
+移除的元素有:
+响应式页面中经常用到根据屏幕密度设置不同的图片。这时就用到了 img 标签的srcset属性。srcset属性用于设置不同屏幕密度下,img 会自动加载不同的图片。用法如下:
+<img src="image-128.png" srcset="image-256.png 2x" />
+使用上面的代码,就能实现在屏幕密度为1x的情况下加载image-128.png, 屏幕密度为2x时加载image-256.png。
+按照上面的实现,不同的屏幕密度都要设置图片地址,目前的屏幕密度有1x,2x,3x,4x四种,如果每一个图片都设置4张图片,加载就会很慢。所以就有了新的srcset标准。代码如下:
+<img src="image-128.png"
+ srcset="image-128.png 128w, image-256.png 256w, image-512.png 512w"
+ sizes="(max-width: 360px) 340px, 128px" />
+其中srcset指定图片的地址和对应的图片质量。sizes用来设置图片的尺寸零界点。对于 srcset 中的 w 单位,可以理解成图片质量。如果可视区域小于这个质量的值,就可以使用。浏览器会自动选择一个最小的可用图片。
+sizes语法如下:
+sizes="[media query] [length], [media query] [length] ... "
+sizes就是指默认显示128px, 如果视区宽度大于360px, 则显示340px。
+a b span img input select strong;div ul ol li dl dt dd h1 h2 h3 h4 h5 h6 p;空元素,即没有内容的HTML元素。空元素是在开始标签中关闭的,也就是空元素没有闭合标签:
+<br>、<hr>、<img>、<input>、<link>、<meta>;<area>、<base>、<col>、<colgroup>、<command>、<embed>、<keygen>、<param>、<source>、<track>、<wbr>。在 HTML 页面中,如果在执行脚本时,页面的状态是不可相应的,直到脚本执行完成后,页面才变成可相应。web worker 是运行在后台的 js,独立于其他脚本,不会影响页面的性能。 并且通过 postMessage 将结果回传到主线程。这样在进行复杂操作的时候,就不会阻塞主线程了。
+如何创建 web worker:
+离线存储指的是:在用户没有与因特网连接时,可以正常访问站点或应用,在用户与因特网连接时,更新用户机器上的缓存文件。
+**原理:**HTML5的离线存储是基于一个新建的 .appcache 文件的缓存机制(不是存储技术),通过这个文件上的解析清单离线存储资源,这些资源就会像cookie一样被存储了下来。之后当网络在处于离线状态下时,浏览器会通过被离线存储的数据进行页面展示
使用方法: +(1)创建一个和 html 同名的 manifest 文件,然后在页面头部加入 manifest 属性:
+<html lang="en" manifest="index.manifest">
+(2)在 cache.manifest 文件中编写需要离线存储的资源:
CACHE MANIFEST
+ #v0.11
+ CACHE:
+ js/app.js
+ css/style.css
+ NETWORK:
+ resourse/logo.png
+ FALLBACK:
+ / /offline.html
+(3)在离线状态时,操作 window.applicationCache 进行离线缓存的操作。
如何更新缓存:
+(1)更新 manifest 文件
+(2)通过 javascript 操作
+(3)清除浏览器缓存
+注意事项:
+(1)浏览器对缓存数据的容量限制可能不太一样(某些浏览器设置的限制是每个站点 5MB)。
+(2)如果 manifest 文件,或者内部列举的某一个文件不能正常下载,整个更新过程都将失败,浏览器继续全部使用老的缓存。
+(3)引用 manifest 的 html 必须与 manifest 文件同源,在同一个域下。
+(4)FALLBACK 中的资源必须和 manifest 文件同源。
+(5)当一个资源被缓存后,该浏览器直接请求这个绝对路径也会访问缓存中的资源。
+(6)站点中的其他页面即使没有设置 manifest 属性,请求的资源如果在缓存中也从缓存中访问。
+(7)当 manifest 文件发生改变时,资源请求本身也会触发更新。
+iframe 元素会创建包含另外一个文档的内联框架(即行内框架)。
+优点:
+缺点:
+label标签来定义表单控件的关系:当用户选择label标签时,浏览器会自动将焦点转到和label标签相关的表单控件上。
+<label for="mobile">Number:</label>
+<input type="text" id="mobile"/>
+<label>Date:<input type="text"/></label>
+(1)SVG: +SVG可缩放矢量图形(Scalable Vector Graphics)是基于可扩展标记语言XML描述的2D图形的语言,SVG基于XML就意味着SVG DOM中的每个元素都是可用的,可以为某个元素附加Javascript事件处理器。在 SVG 中,每个被绘制的图形均被视为对象。如果 SVG 对象的属性发生变化,那么浏览器能够自动重现图形。
+其特点如下:
+(2)Canvas: +Canvas是画布,通过Javascript来绘制2D图形,是逐像素进行渲染的。其位置发生改变,就会重新进行绘制。
+其特点如下:
+注:矢量图,也称为面向对象的图像或绘图图像,在数学上定义为一系列由线连接的点。矢量文件中的图形元素称为对象。每个对象都是一个自成一体的实体,它具有颜色、形状、轮廓、大小和屏幕位置等属性。
+文档的头部描述了文档的各种属性和信息,包括文档的标题、在 Web 中的位置以及和其他文档的关系等。绝大多数文档头部包含的数据都不会真正作为内容显示给读者。
+下面这些标签可用在 head 部分:<base>, <link>, <meta>, <script>, <style>, <title>。
其中 <title> 定义文档的标题,它是 head 部分中唯一必需的元素。
<!Doctype html>有何作用? 严格模式与混杂模式如何区分?它们有何意义?文档声明的作用: 文档声明是为了告诉浏览器,当前HTML文档使用什么版本的HTML来写的,这样浏览器才能按照声明的版本来正确的解析。
的作用:<!doctype html> 的作用就是让浏览器进入标准模式,使用最新的 HTML5 标准来解析渲染页面;如果不写,浏览器就会进入混杂模式,我们需要避免此类情况发生。
严格模式与混杂模式的区分:
+W3C标准解析代码;区分:网页中的DTD,直接影响到使用的是严格模式还是浏览模式,可以说DTD的使用与这两种方式的区别息息相关。
DOCTYPE ,那么它一般以严格模式呈现(严格 DTD ——严格模式);DTD 和 URI 的 DOCTYPE ,也以严格模式呈现,但有过渡 DTD 而没有 URI (统一资源标识符,就是声明最后的地址)会导致页面以混杂模式呈现(有 URI 的过渡 DTD ——严格模式;没有 URI 的过渡 DTD ——混杂模式);DOCTYPE 不存在或形式不正确会导致文档以混杂模式呈现(DTD不存在或者格式不正确——混杂模式);HTML5 没有 DTD ,因此也就没有严格模式与混杂模式的区别,HTML5 有相对宽松的 法,实现时,已经尽可能大的实现了向后兼容(HTML5 没有严格和混杂之分)。总之,严格模式让各个浏览器统一执行一套规范兼容模式保证了旧网站的正常运行。
+产生乱码的原因:
+gbk的编码,而内容中的中文字是utf-8编码的,这样浏览器打开即会出现html乱码,反之也会出现乱码;html网页编码是gbk,而程序从数据库中调出呈现是utf-8编码的内容也会造成编码乱码;解决办法:
+gbk,而数据库储存数据编码格式是UTF-8,此时需要程序查询数据库数据显示数据前进程序转码;(1)渐进增强(progressive enhancement):主要是针对低版本的浏览器进行页面重构,保证基本的功能情况下,再针对高级浏览器进行效果、交互等方面的改进和追加功能,以达到更好的用户体验。 +(2)优雅降级 graceful degradation: 一开始就构建完整的功能,然后再针对低版本的浏览器进行兼容。
+两者区别:
+“优雅降级”观点认为应该针对那些最高级、最完善的浏览器来设计网站。而将那些被认为“过时”或有功能缺失的浏览器下的测试工作安排在开发周期的最后阶段,并把测试对象限定为主流浏览器(如 IE、Mozilla 等)的前一个版本。 在这种设计范例下,旧版的浏览器被认为仅能提供“简陋却无妨 (poor, but passable)” 的浏览体验。可以做一些小的调整来适应某个特定的浏览器。但由于它们并非我们所关注的焦点,因此除了修复较大的错误之外,其它的差异将被直接忽略。
+“渐进增强”观点则认为应关注于内容本身。内容是建立网站的诱因,有的网站展示它,有的则收集它,有的寻求,有的操作,还有的网站甚至会包含以上的种种,但相同点是它们全都涉及到内容。这使得“渐进增强”成为一种更为合理的设计范例。这也是它立即被 Yahoo 所采纳并用以构建其“分级式浏览器支持 (Graded Browser Support)”策略的原因所在。
+JavaScript共有八种数据类型,分别是 Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt。
+其中 Symbol 和 BigInt 是ES6 中新增的数据类型:
+这些数据可以分为原始数据类型和引用数据类型:
+两种类型的区别在于存储位置的不同:
+堆和栈的概念存在于数据结构和操作系统内存中,在数据结构中:
+在操作系统中,内存被分为栈区和堆区:
+(1)typeof
+console.log(typeof 2); // number
+console.log(typeof true); // boolean
+console.log(typeof 'str'); // string
+console.log(typeof []); // object
+console.log(typeof function(){}); // function
+console.log(typeof {}); // object
+console.log(typeof undefined); // undefined
+console.log(typeof null); // object
+其中数组、对象、null都会被判断为object,其他判断都正确。
+(2)instanceof
+instanceof可以正确判断对象的类型,其内部运行机制是判断在其原型链中能否找到该类型的原型。
console.log(2 instanceof Number); // false
+console.log(true instanceof Boolean); // false
+console.log('str' instanceof String); // false
+
+console.log([] instanceof Array); // true
+console.log(function(){} instanceof Function); // true
+console.log({} instanceof Object); // true
+可以看到,instanceof只能正确判断引用数据类型,而不能判断基本数据类型。instanceof 运算符可以用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。
(3) constructor
+console.log((2).constructor === Number); // true
+console.log((true).constructor === Boolean); // true
+console.log(('str').constructor === String); // true
+console.log(([]).constructor === Array); // true
+console.log((function() {}).constructor === Function); // true
+console.log(({}).constructor === Object); // true
+constructor有两个作用,一是判断数据的类型,二是对象实例通过 constrcutor 对象访问它的构造函数。需要注意,如果创建一个对象来改变它的原型,constructor就不能用来判断数据类型了:
function Fn(){};
+
+Fn.prototype = new Array();
+
+var f = new Fn();
+
+console.log(f.constructor===Fn); // false
+console.log(f.constructor===Array); // true
+(4)Object.prototype.toString.call()
+Object.prototype.toString.call() 使用 Object 对象的原型方法 toString 来判断数据类型:
var a = Object.prototype.toString;
+
+console.log(a.call(2));
+console.log(a.call(true));
+console.log(a.call('str'));
+console.log(a.call([]));
+console.log(a.call(function(){}));
+console.log(a.call({}));
+console.log(a.call(undefined));
+console.log(a.call(null));
+同样是检测对象obj调用toString方法,obj.toString()的结果和Object.prototype.toString.call(obj)的结果不一样,这是为什么?
+这是因为toString是Object的原型方法,而Array、function等类型作为Object的实例,都重写了toString方法。不同的对象类型调用toString方法时,根据原型链的知识,调用的是对应的重写之后的toString方法(function类型返回内容为函数体的字符串,Array类型返回元素组成的字符串…),而不会去调用Object上原型toString方法(返回对象的具体类型),所以采用obj.toString()不能得到其对象类型,只能将obj转换为字符串类型;因此,在想要得到对象的具体类型时,应该调用Object原型上的toString方法。
+Object.prototype.toString.call(obj).slice(8,-1) === 'Array';
+obj.__proto__ === Array.prototype;
+Array.isArrray(obj);
+obj instanceof Array
+Array.prototype.isPrototypeOf(obj)
+首先 Undefined 和 Null 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined 和 null。
+undefined 代表的含义是未定义,null 代表的含义是空对象。一般变量声明了但还没有定义的时候会返回 undefined,null主要用于赋值给一些可能会返回对象的变量,作为初始化。
+undefined 在 JavaScript 中不是一个保留字,这意味着可以使用 undefined 来作为一个变量名,但是这样的做法是非常危险的,它会影响对 undefined 值的判断。我们可以通过一些方法获得安全的 undefined 值,比如说 void 0。
+当对这两种类型使用 typeof 进行判断时,Null 类型化会返回 “object”,这是一个历史遗留的问题。当使用双等号对两种类型的值进行比较时会返回 true,使用三个等号时会返回 false。
+typeof null 的结果是Object。
+在 JavaScript 第一个版本中,所有值都存储在 32 位的单元中,每个单元包含一个小的 类型标签(1-3 bits) 以及当前要存储值的真实数据。类型标签存储在每个单元的低位中,共有五种数据类型:
+000: object - 当前存储的数据指向一个对象。
+ 1: int - 当前存储的数据是一个 31 位的有符号整数。
+010: double - 当前存储的数据指向一个双精度的浮点数。
+100: string - 当前存储的数据指向一个字符串。
+110: boolean - 当前存储的数据是布尔值。
+如果最低位是 1,则类型标签标志位的长度只有一位;如果最低位是 0,则类型标签标志位的长度占三位,为存储其他四种数据类型提供了额外两个 bit 的长度。
+有两种特殊数据类型:
+那也就是说null的类型标签也是000,和Object的类型标签一样,所以会被判定为Object。
+instanceof 运算符用于判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。
+function myInstanceof(left, right) {
+ // 获取对象的原型
+ let proto = Object.getPrototypeOf(left)
+ // 获取构造函数的 prototype 对象
+ let prototype = right.prototype;
+
+ // 判断构造函数的 prototype 对象是否在对象的原型链上
+ while (true) {
+ if (!proto) return false;
+ if (proto === prototype) return true;
+ // 如果没有找到,就继续从其原型上找,Object.getPrototypeOf方法用来获取指定对象的原型
+ proto = Object.getPrototypeOf(proto);
+ }
+}
+在开发过程中遇到类似这样的问题:
+let n1 = 0.1, n2 = 0.2
+console.log(n1 + n2) // 0.30000000000000004
+这里得到的不是想要的结果,要想等于0.3,就要把它进行转化:
+(n1 + n2).toFixed(2) // 注意,toFixed为四舍五入
+toFixed(num) 方法可把 Number 四舍五入为指定小数位数的数字。那为什么会出现这样的结果呢?
计算机是通过二进制的方式存储数据的,所以计算机计算0.1+0.2的时候,实际上是计算的两个数的二进制的和。0.1的二进制是0.0001100110011001100...(1100循环),0.2的二进制是:0.00110011001100...(1100循环),这两个数的二进制都是无限循环的数。那JavaScript是如何处理无限循环的二进制小数呢?
一般我们认为数字包括整数和小数,但是在 JavaScript 中只有一种数字类型:Number,它的实现遵循IEEE 754标准,使用64位固定长度来表示,也就是标准的double双精度浮点数。在二进制科学表示法中,双精度浮点数的小数部分最多只能保留52位,再加上前面的1,其实就是保留53位有效数字,剩余的需要舍去,遵从“0舍1入”的原则。
+根据这个原则,0.1和0.2的二进制数相加,再转化为十进制数就是:0.30000000000000004。
下面看一下双精度数是如何保存的:
+
对于0.1,它的二进制为:
+0.00011001100110011001100110011001100110011001100110011001 10011...
+转为科学计数法(科学计数法的结果就是浮点数):
+1.1001100110011001100110011001100110011001100110011001*2^-4
+可以看出0.1的符号位为0,指数位为-4,小数位为:
+1001100110011001100110011001100110011001100110011001
+那么问题又来了,指数位是负数,该如何保存呢?
+IEEE标准规定了一个偏移量,对于指数部分,每次都加这个偏移量进行保存,这样即使指数是负数,那么加上这个偏移量也就是正数了。由于JavaScript的数字是双精度数,这里就以双精度数为例,它的指数部分为11位,能表示的范围就是0~2047,IEEE固定双精度数的偏移量为1023。
+-1022~1013。对于上面的0.1的指数位为-4,-4+1023 = 1019 转化为二进制就是:1111111011.
所以,0.1表示为:
+0 1111111011 1001100110011001100110011001100110011001100110011001
+说了这么多,是时候该最开始的问题了,如何实现0.1+0.2=0.3呢?
+对于这个问题,一个直接的解决方法就是设置一个误差范围,通常称为“机器精度”。对JavaScript来说,这个值通常为2-52,在ES6中,提供了Number.EPSILON属性,而它的值就是2-52,只要判断0.1+0.2-0.3是否小于Number.EPSILON,如果小于,就可以判断为0.1+0.2 ===0.3
function numberepsilon(arg1,arg2){
+ return Math.abs(arg1 - arg2) < Number.EPSILON;
+}
+
+console.log(numberepsilon(0.1 + 0.2, 0.3)); // true
+因为 undefined 是一个标识符,所以可以被当作变量来使用和赋值,但是这样会影响 undefined 的正常判断。表达式 void ___ 没有返回值,因此返回结果是 undefined。void 并不改变表达式的结果,只是让表达式不返回值。因此可以用 void 0 来获得 undefined。
+NaN 指“不是一个数字”(not a number),NaN 是一个“警戒值”(sentinel value,有特殊用途的常规值),用于指出数字类型中的错误情况,即“执行数学运算没有成功,这是失败后返回的结果”。
+typeof NaN; // "number"
+NaN 是一个特殊值,它和自身不相等,是唯一一个非自反(自反,reflexive,即 x === x 不成立)的值。而 NaN !== NaN 为 true。
+为了将值转换为相应的基本类型值,抽象操作 ToPrimitive 会首先(通过内部操作 DefaultValue)检查该值是否有valueOf()方法。如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用 toString() 的返回值(如果存在)来进行强制类型转换。
+如果 valueOf() 和 toString() 均不返回基本类型值,会产生 TypeError 错误。
+以下这些是假值: +• undefined +• null +• false +• +0、-0 和 NaN +• ""
+假值的布尔强制类型转换结果为 false。从逻辑上说,假值列表以外的都应该是真值。
+|| 和 && 首先会对第一个操作数执行条件判断,如果其不是布尔值就先强制转换为布尔类型,然后再执行条件判断。
+|| 和 && 返回它们其中一个操作数的值,而非条件判断的结果
+在 JavaScript 中,基本类型是没有属性和方法的,但是为了便于操作基本类型的值,在调用基本类型的属性或方法时 JavaScript 会在后台隐式地将基本类型的值转换为对象,如:
+const a = "abc";
+a.length; // 3
+a.toUpperCase(); // "ABC"
+在访问'abc'.length时,JavaScript 将'abc'在后台转换成String('abc'),然后再访问其length属性。
JavaScript也可以使用Object函数显式地将基本类型转换为包装类型:
var a = 'abc'
+Object(a) // String {"abc"}
+也可以使用valueOf方法将包装类型倒转成基本类型:
var a = 'abc'
+var b = Object(a)
+var c = b.valueOf() // 'abc'
+看看如下代码会打印出什么:
+var a = new Boolean( false );
+if (!a) {
+ console.log( "Oops" ); // never runs
+}
+答案是什么都不会打印,因为虽然包裹的基本类型是false,但是false被包裹成包装类型后就成了对象,所以其非值为false,所以循环体中的内容不会运行。
首先要介绍ToPrimitive方法,这是 JavaScript 中每个值隐含的自带的方法,用来将值 (无论是基本类型值还是对象)转换为基本类型值。如果值为基本类型,则直接返回值本身;如果值为对象,其看起来大概是这样:
/**
+* @obj 需要转换的对象
+* @type 期望的结果类型
+*/
+ToPrimitive(obj,type)
+type的值为number或者string。
(1)当type为number时规则如下:
obj的valueOf方法,如果为原始值,则返回,否则下一步;obj的toString方法,后续同上;TypeError 异常。(2)当type为string时规则如下:
obj的toString方法,如果为原始值,则返回,否则下一步;obj的valueOf方法,后续同上;TypeError 异常。可以看出两者的主要区别在于调用toString和valueOf的先后顺序。默认情况下:
type默认为string;type默认为number。总结上面的规则,对于 Date 以外的对象,转换为基本类型的大概规则可以概括为一个函数:
+var objToNumber = value => Number(value.valueOf().toString())
+objToNumber([]) === 0
+objToNumber({}) === NaN
+而 JavaScript 中的隐式类型转换主要发生在+、-、*、/以及==、>、<这些运算符之间。而这些运算符只能操作基本类型值,所以在进行这些运算前的第一步就是将两边的值用ToPrimitive转换成基本类型,再进行操作。
以下是基本类型的值在不同操作符的情况下隐式转换的规则 (对于对象,其会被ToPrimitive转换成基本类型,所以最终还是要应用基本类型转换规则):
+操作符
++操作符的两边有至少一个string类型变量时,两边的变量都会被隐式转换为字符串;其他情况下两边的变量都会被转换为数字。1 + '23' // '123'
+ 1 + false // 1
+ 1 + Symbol() // Uncaught TypeError: Cannot convert a Symbol value to a number
+ '1' + false // '1false'
+ false + true // 1
+-、*、\操作符NaN也是一个数字
1 * '23' // 23
+ 1 * false // 0
+ 1 / 'aa' // NaN
+==操作符操作符两边的值都尽量转成number:
3 == true // false, 3 转为number为3,true转为number为1
+'0' == false //true, '0'转为number为0,false转为number为0
+'0' == 0 // '0'转为number为0
+<和>比较符如果两边都是字符串,则比较字母表顺序:
+'ca' < 'bd' // false
+'a' < 'b' // true
+其他情况下,转换为数字再比较:
+'12' < 13 // true
+false > -1 // true
+以上说的是基本类型的隐式转换,而对象会被ToPrimitive转换为基本类型再进行转换:
var a = {}
+a > 2 // false
+其对比过程如下:
+a.valueOf() // {}, 上面提到过,ToPrimitive默认type为number,所以先valueOf,结果还是个对象,下一步
+a.toString() // "[object Object]",现在是一个字符串了
+Number(a.toString()) // NaN,根据上面 < 和 > 操作符的规则,要转换成数字
+NaN > 2 //false,得出比较结果
+又比如:
+var a = {name:'Jack'}
+var b = {age: 18}
+a + b // "[object Object][object Object]"
+运算过程如下:
+a.valueOf() // {},上面提到过,ToPrimitive默认type为number,所以先valueOf,结果还是个对象,下一步
+a.toString() // "[object Object]"
+b.valueOf() // 同理
+b.toString() // "[object Object]"
+a + b // "[object Object][object Object]"
++ 操作符什么时候用于字符串的拼接?根据 ES5 规范,如果某个操作数是字符串或者能够通过以下步骤转换为字符串的话,+ 将进行拼接操作。如果其中一个操作数是对象(包括数组),则首先对其调用 ToPrimitive 抽象操作,该抽象操作再调用 [[DefaultValue]],以数字作为上下文。如果不能转换为字符串,则会将其转换为数字类型来进行计算。
+简单来说就是,如果 + 的其中一个操作数是字符串(或者通过以上步骤最终得到字符串),则执行字符串拼接,否则执行数字加法。
+那么对于除了加法的运算符来说,只要其中一方是数字,那么另一方就会被转为数字。
+JavaScript中Number.MAX_SAFE_INTEGER表示最⼤安全数字,计算结果是9007199254740991,即在这个数范围内不会出现精度丢失(⼩数除外)。但是⼀旦超过这个范围,js就会出现计算不准确的情况,这在⼤数计算的时候不得不依靠⼀些第三⽅库进⾏解决,因此官⽅提出了BigInt来解决此问题。
+扩展运算符:
+let outObj = {
+ inObj: {a: 1, b: 2}
+}
+let newObj = {...outObj}
+newObj.inObj.a = 2
+console.log(outObj) // {inObj: {a: 2, b: 2}}
+Object.assign():
+let outObj = {
+ inObj: {a: 1, b: 2}
+}
+let newObj = Object.assign({}, outObj)
+newObj.inObj.a = 2
+console.log(outObj) // {inObj: {a: 2, b: 2}}
+可以看到,两者都是浅拷贝。
+(1)块级作用域: 块作用域由 { }包括,let和const具有块级作用域,var不存在块级作用域。块级作用域解决了ES5中的两个问题:
(2)变量提升: var存在变量提升,let和const不存在变量提升,即在变量只能在声明之后使用,否在会报错。
+(3)给全局添加属性: 浏览器的全局对象是window,Node的全局对象是global。var声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是let和const不会。
+(4)重复声明: var声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。const和let不允许重复声明变量。
+(5)暂时性死区: 在使用let、const命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。使用var声明的变量不存在暂时性死区。
+(6)初始值设置: 在变量声明时,var 和 let 可以不用设置初始值。而const声明变量必须设置初始值。
+(7)指针指向: let和const都是ES6新增的用于创建变量的语法。 let创建的变量是可以更改指针指向(可以重新赋值)。但const声明的变量是不允许改变指针的指向。
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +| 区别 | var | let | const |
|---|---|---|---|
| 是否有块级作用域 | × | ✔️ | ✔️ |
| 是否存在变量提升 | ✔️ | × | × |
| 是否添加全局属性 | ✔️ | × | × |
| 能否重复声明变量 | ✔️ | × | × |
| 是否存在暂时性死区 | × | ✔️ | ✔️ |
| 是否必须设置初始值 | × | × | ✔️ |
| 能否改变指针指向 | ✔️ | ✔️ | × |
const保证的并不是变量的值不能改动,而是变量指向的那个内存地址不能改动。对于基本类型的数据(数值、字符串、布尔值),其值就保存在变量指向的那个内存地址,因此等同于常量。
+但对于引用类型的数据(主要是对象和数组)来说,变量指向数据的内存地址,保存的只是一个指针,const只能保证这个指针是固定不变的,至于它指向的数据结构是不是可变的,就完全不能控制了。
+箭头函数是ES6中的提出来的,它没有prototype,也没有自己的this指向,更不可以使用arguments参数,所以不能New一个箭头函数。
+new操作符的实现步骤如下:
+所以,上面的第二、三步,箭头函数都是没有办法执行的。
+(1)箭头函数比普通函数更加简洁
+let fn = () => void doesNotReturn();
+(2)箭头函数没有自己的this
+箭头函数不会创建自己的this, 所以它没有自己的this,它只会在自己作用域的上一层继承this。所以箭头函数中this的指向在它在定义时已经确定了,之后不会改变。
+(3)箭头函数继承来的this指向永远不会改变
+var id = 'GLOBAL';
+var obj = {
+ id: 'OBJ',
+ a: function(){
+ console.log(this.id);
+ },
+ b: () => {
+ console.log(this.id);
+ }
+};
+obj.a(); // 'OBJ'
+obj.b(); // 'GLOBAL'
+new obj.a() // undefined
+new obj.b() // Uncaught TypeError: obj.b is not a constructor
+对象obj的方法b是使用箭头函数定义的,这个函数中的this就永远指向它定义时所处的全局执行环境中的this,即便这个函数是作为对象obj的方法调用,this依旧指向Window对象。需要注意,定义对象的大括号{}是无法形成一个单独的执行环境的,它依旧是处于全局执行环境中。
(4)call()、apply()、bind()等方法不能改变箭头函数中this的指向
+var id = 'Global';
+let fun1 = () => {
+ console.log(this.id)
+};
+fun1(); // 'Global'
+fun1.call({id: 'Obj'}); // 'Global'
+fun1.apply({id: 'Obj'}); // 'Global'
+fun1.bind({id: 'Obj'})(); // 'Global'
+(5)箭头函数不能作为构造函数使用
+构造函数在new的步骤在上面已经说过了,实际上第二步就是将函数中的this指向该对象。 但是由于箭头函数时没有自己的this的,且this指向外层的执行环境,且不能改变指向,所以不能当做构造函数使用。
+(6)箭头函数没有自己的arguments
+箭头函数没有自己的arguments对象。在箭头函数中访问arguments实际上获得的是它外层函数的arguments值。
+(7)箭头函数没有prototype
+(8)箭头函数不能用作Generator函数,不能使用yeild关键字
+箭头函数不同于传统JavaScript中的函数,箭头函数并没有属于⾃⼰的this,它所谓的this是捕获其所在上下⽂的 this 值,作为⾃⼰的 this 值,并且由于没有属于⾃⼰的this,所以是不会被new调⽤的,这个所谓的this也不会被改变。
+可以⽤Babel理解⼀下箭头函数:
+// ES6
+const obj = {
+ getArrow() {
+ return () => {
+ console.log(this === obj);
+ };
+ }
+}
+转化后:
+// ES5,由 Babel 转译
+var obj = {
+ getArrow: function getArrow() {
+ var _this = this;
+ return function () {
+ console.log(_this === obj);
+ };
+ }
+};
+(1)对象扩展运算符
+对象的扩展运算符(...)用于取出参数对象中的所有可遍历属性,拷贝到当前对象之中。
+let bar = { a: 1, b: 2 };
+let baz = { ...bar }; // { a: 1, b: 2 }
+上述方法实际上等价于:
+let bar = { a: 1, b: 2 };
+let baz = Object.assign({}, bar); // { a: 1, b: 2 }
+Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。Object.assign方法的第一个参数是目标对象,后面的参数都是源对象。(如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性)。
同样,如果用户自定义的属性,放在扩展运算符后面,则扩展运算符内部的同名属性会被覆盖掉。
+let bar = {a: 1, b: 2};
+let baz = {...bar, ...{a:2, b: 4}}; // {a: 2, b: 4}
+利用上述特性就可以很方便的修改对象的部分属性。在redux中的reducer函数规定必须是一个纯函数,reducer中的state对象要求不能直接修改,可以通过扩展运算符把修改路径的对象都复制一遍,然后产生一个新的对象返回。
需要注意:扩展运算符对对象实例的拷贝属于浅拷贝。
+(2)数组扩展运算符
+数组的扩展运算符可以将一个数组转为用逗号分隔的参数序列,且每次只能展开一层数组。
+console.log(...[1, 2, 3])
+// 1 2 3
+console.log(...[1, [2, 3, 4], 5])
+// 1 [2, 3, 4] 5
+下面是数组的扩展运算符的应用:
+function add(x, y) {
+ return x + y;
+}
+const numbers = [1, 2];
+add(...numbers) // 3
+const arr1 = [1, 2];
+const arr2 = [...arr1];
+要记住:扩展运算符(…)用于取出参数对象中的所有可遍历属性,拷贝到当前对象之中,这里参数对象是个数组,数组里面的所有对象都是基础数据类型,将所有基础数据类型重新拷贝到新的数组中。
+如果想在数组内合并数组,可以这样:
+const arr1 = ['two', 'three'];const arr2 = ['one', ...arr1, 'four', 'five'];// ["one", "two", "three", "four", "five"]
+const [first, ...rest] = [1, 2, 3, 4, 5];first // 1rest // [2, 3, 4, 5]
+需要注意:如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。
+const [...rest, last] = [1, 2, 3, 4, 5]; // 报错const [first, ...rest, last] = [1, 2, 3, 4, 5]; // 报错
+[...'hello'] // [ "h", "e", "l", "l", "o" ]
+比较常见的应用是可以将某些数据结构转为数组:
+// arguments对象
+function foo() {
+ const args = [...arguments];
+}
+用于替换es5中的Array.prototype.slice.call(arguments)写法。
Math函数获取数组中特定的值const numbers = [9, 4, 7, 1];
+Math.min(...numbers); // 1
+Math.max(...numbers); // 9
+解构是 ES6 提供的一种新的提取数据的模式,这种模式能够从对象或数组里有针对性地拿到想要的数值。 +1)数组的解构 +在解构数组时,以元素的位置为匹配条件来提取想要的数据的:
+const [a, b, c] = [1, 2, 3]
+最终,a、b、c分别被赋予了数组第0、1、2个索引位的值:
+
+数组里的0、1、2索引位的元素值,精准地被映射到了左侧的第0、1、2个变量里去,这就是数组解构的工作模式。还可以通过给左侧变量数组设置空占位的方式,实现对数组中某几个元素的精准提取:
const [a,,c] = [1,2,3]
+通过把中间位留空,可以顺利地把数组第一位和最后一位的值赋给 a、c 两个变量:
+
2)对象的解构 +对象解构比数组结构稍微复杂一些,也更显强大。在解构对象时,是以属性的名称为匹配条件,来提取想要的数据的。现在定义一个对象:
+const stu = {
+ name: 'Bob',
+ age: 24
+}
+假如想要解构它的两个自有属性,可以这样:
+const { name, age } = stu
+这样就得到了 name 和 age 两个和 stu 平级的变量:
+
注意,对象解构严格以属性名作为定位依据,所以就算调换了 name 和 age 的位置,结果也是一样的:
+const { age, name } = stu
+有时会遇到一些嵌套程度非常深的对象:
+const school = {
+ classes: {
+ stu: {
+ name: 'Bob',
+ age: 24,
+ }
+ }
+}
+像此处的 name 这个变量,嵌套了四层,此时如果仍然尝试老方法来提取它:
+const { name } = school
+显然是不奏效的,因为 school 这个对象本身是没有 name 这个属性的,name 位于 school 对象的“儿子的儿子”对象里面。要想把 name 提取出来,一种比较笨的方法是逐层解构:
+const { classes } = school
+const { stu } = classes
+const { name } = stu
+name // 'Bob'
+但是还有一种更标准的做法,可以用一行代码来解决这个问题:
+const { classes: { stu: { name } }} = school
+
+console.log(name) // 'Bob'
+可以在解构出来的变量名右侧,通过冒号+{目标属性名}这种形式,进一步解构它,一直解构到拿到目标数据为止。
+扩展运算符被用在函数形参上时,它还可以把一个分离的参数序列整合成一个数组:
+function mutiple(...args) {
+ let result = 1;
+ for (var val of args) {
+ result *= val;
+ }
+ return result;
+}
+mutiple(1, 2, 3, 4) // 24
+这里,传入 mutiple 的是四个分离的参数,但是如果在 mutiple 函数里尝试输出 args 的值,会发现它是一个数组:
+function mutiple(...args) {
+ console.log(args)
+}
+mutiple(1, 2, 3, 4) // [1, 2, 3, 4]
+这就是 … rest运算符的又一层威力了,它可以把函数的多个入参收敛进一个数组里。这一点经常用于获取函数的多余参数,或者像上面这样处理函数参数个数不确定的情况。
+ES6 提出了“模板语法”的概念。在 ES6 以前,拼接字符串是很麻烦的事情:
+var name = 'css'
+var career = 'coder'
+var hobby = ['coding', 'writing']
+var finalString = 'my name is ' + name + ', I work as a ' + career + ', I love ' + hobby[0] + ' and ' + hobby[1]
+仅仅几个变量,写了这么多加号,还要时刻小心里面的空格和标点符号有没有跟错地方。但是有了模板字符串,拼接难度直线下降:
+var name = 'css'
+var career = 'coder'
+var hobby = ['coding', 'writing']
+var finalString = `my name is ${name}, I work as a ${career} I love ${hobby[0]} and ${hobby[1]}`
+字符串不仅更容易拼了,也更易读了,代码整体的质量都变高了。这就是模板字符串的第一个优势——允许用${}的方式嵌入变量。但这还不是问题的关键,模板字符串的关键优势有两个:
+基于第一点,可以在模板字符串里无障碍地直接写 html 代码:
+let list = `
+ <ul>
+ <li>列表项1</li>
+ <li>列表项2</li>
+ </ul>
+`;
+console.log(message); // 正确输出,不存在报错
+基于第二点,可以把一些简单的计算和调用丢进 ${} 来做:
+function add(a, b) {
+ const finalString = `${a} + ${b} = ${a+b}`
+ console.log(finalString)
+}
+add(1, 2) // 输出 '1 + 2 = 3'
+除了模板语法外, ES6中还新增了一系列的字符串方法用于提升开发效率:
+(1)存在性判定:在过去,当判断一个字符/字符串是否在某字符串中时,只能用 indexOf > -1 来做。现在 ES6 提供了三个方法:includes、startsWith、endsWith,它们都会返回一个布尔值来告诉你是否存在。
+const son = 'haha'
+const father = 'xixi haha hehe'
+father.includes(son) // true
+const father = 'xixi haha hehe'
+father.startsWith('haha') // false
+father.startsWith('xixi') // true
+const father = 'xixi haha hehe'
+ father.endsWith('hehe') // true
+(2)自动重复:可以使用 repeat 方法来使同一个字符串输出多次(被连续复制多次):
+const sourceCode = 'repeat for 3 times;'
+const repeated = sourceCode.repeat(3)
+console.log(repeated) // repeat for 3 times;repeat for 3 times;repeat for 3 times;
+new操作符的执行过程:
+(1)首先创建了一个新的空对象
+(2)设置原型,将对象的原型设置为函数的 prototype 对象。
+(3)让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)
+(4)判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。
+具体实现:
+function objectFactory() {
+ let newObject = null;
+ let constructor = Array.prototype.shift.call(arguments);
+ let result = null;
+ // 判断参数是否是一个函数
+ if (typeof constructor !== "function") {
+ console.error("type error");
+ return;
+ }
+ // 新建一个空对象,对象的原型为构造函数的 prototype 对象
+ newObject = Object.create(constructor.prototype);
+ // 将 this 指向新建对象,并执行函数
+ result = constructor.apply(newObject, arguments);
+ // 判断返回对象
+ let flag = result && (typeof result === "object" || typeof result === "function");
+ // 判断返回结果
+ return flag ? result : newObject;
+}
+// 使用方法
+objectFactory(构造函数, 初始化参数);
+| Map | Object | |
|---|---|---|
| 意外的键 | Map默认情况不包含任何键,只包含显式插入的键。 | Object 有一个原型, 原型链上的键名有可能和自己在对象上的设置的键名产生冲突。 |
| 键的类型 | Map的键可以是任意值,包括函数、对象或任意基本类型。 | Object 的键必须是 String 或是Symbol。 |
| 键的顺序 | Map 中的 key 是有序的。因此,当迭代的时候, Map 对象以插入的顺序返回键值。 | Object 的键是无序的 |
| Size | Map 的键值对个数可以轻易地通过size 属性获取 | Object 的键值对个数只能手动计算 |
| 迭代 | Map 是 iterable 的,所以可以直接被迭代。 | 迭代Object需要以某种方式获取它的键然后才能迭代。 |
| 性能 | 在频繁增删键值对的场景下表现更好。 | 在频繁添加和删除键值对的场景下未作出优化。 |
(1)Map +map本质上就是键值对的集合,但是普通的Object中的键值对中的键只能是字符串。而ES6提供的Map数据结构类似于对象,但是它的键不限制范围,可以是任意类型,是一种更加完善的Hash结构。如果Map的键是一个原始数据类型,只要两个键严格相同,就视为是同一个键。
+实际上Map是一个数组,它的每一个数据也都是一个数组,其形式如下:
+const map = [
+ ["name","张三"],
+ ["age",18],
+]
+Map数据结构有以下操作方法:
+map.size 返回Map结构的成员总数。Map结构原生提供是三个遍历器生成函数和一个遍历方法
+const map = new Map([
+ ["foo",1],
+ ["bar",2],
+])
+for(let key of map.keys()){
+ console.log(key); // foo bar
+}
+for(let value of map.values()){
+ console.log(value); // 1 2
+}
+for(let items of map.entries()){
+ console.log(items); // ["foo",1] ["bar",2]
+}
+map.forEach( (value,key,map) => {
+ console.log(key,value); // foo 1 bar 2
+})
+(2)WeakMap +WeakMap 对象也是一组键值对的集合,其中的键是弱引用的。其键必须是对象,原始数据类型不能作为key值,而值可以是任意的。
+该对象也有以下几种方法:
+其clear()方法已经被弃用,所以可以通过创建一个空的WeakMap并替换原对象来实现清除。
+WeakMap的设计目的在于,有时想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。一旦不再需要这两个对象,就必须手动删除这个引用,否则垃圾回收机制就不会释放对象占用的内存。
+而WeakMap的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。
+总结:
+全局的对象( global objects )或称标准内置对象,不要和 "全局对象(global object)" 混淆。这里说的全局的对象是说在 +全局作用域里的对象。全局作用域中的其他对象可以由用户的脚本创建或由宿主程序提供。
+标准内置对象的分类:
+(1)值属性,这些全局属性返回一个简单值,这些值没有自己的属性和方法。例如 Infinity、NaN、undefined、null 字面量
+(2)函数属性,全局函数可以直接调用,不需要在调用时指定所属对象,执行结束后会将结果直接返回给调用者。例如 eval()、parseFloat()、parseInt() 等
+(3)基本对象,基本对象是定义或使用其他对象的基础。基本对象包括一般对象、函数对象和错误对象。例如 Object、Function、Boolean、Symbol、Error 等
+(4)数字和日期对象,用来表示数字、日期和执行数学计算的对象。例如 Number、Math、Date
+(5)字符串,用来表示和操作字符串的对象。例如 String、RegExp
+(6)可索引的集合对象,这些对象表示按照索引值来排序的数据集合,包括数组和类型数组,以及类数组结构的对象。例如 Array
+(7)使用键的集合对象,这些集合对象在存储数据时会使用到键,支持按照插入顺序来迭代元素。 +例如 Map、Set、WeakMap、WeakSet
+(8)矢量集合,SIMD 矢量集合中的数据会被组织为一个数据序列。 +例如 SIMD 等
+(9)结构化数据,这些对象用来表示和操作结构化的缓冲区数据,或使用 JSON 编码的数据。例如 JSON 等
+(10)控制抽象对象 +例如 Promise、Generator 等
+(11)反射。例如 Reflect、Proxy
+(12)国际化,为了支持多语言处理而加入 ECMAScript 的对象。例如 Intl、Intl.Collator 等
+(13)WebAssembly
+(14)其他。例如 arguments
+总结: +js 中的内置对象主要指的是在程序执行前存在全局作用域里的由 js 定义的一些全局值属性、函数和用来实例化其他对象的构造函数对象。一般经常用到的如全局变量值 NaN、undefined,全局函数如 parseInt()、parseFloat() 用来实例化对象的构造函数如 Date、Object 等,还有提供数学计算的单体内置对象如 Math 对象。
+// (1)匹配 16 进制颜色值
+var regex = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g;
+
+// (2)匹配日期,如 yyyy-mm-dd 格式
+var regex = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/;
+
+// (3)匹配 qq 号
+var regex = /^[1-9][0-9]{4,10}$/g;
+
+// (4)手机号码正则
+var regex = /^1[34578]\d{9}$/g;
+
+// (5)用户名正则
+var regex = /^[a-zA-Z\$][a-zA-Z0-9_\$]{4,16}$/;
+JSON 是一种基于文本的轻量级的数据交换格式。它可以被任何的编程语言读取和作为数据格式来传递。
+在项目开发中,使用 JSON 作为前后端数据交换的方式。在前端通过将一个符合 JSON 格式的数据结构序列化为 +JSON 字符串,然后将它传递到后端,后端通过 JSON 格式的字符串解析后生成对应的数据结构,以此来实现前后端数据的一个传递。
+因为 JSON 的语法是基于 js 的,因此很容易将 JSON 和 js 中的对象弄混,但是应该注意的是 JSON 和 js 中的对象不是一回事,JSON 中对象格式更加严格,比如说在 JSON 中属性值不能为函数,不能出现 NaN 这样的属性值等,因此大多数的 js 对象是不符合 JSON 对象的格式的。
+在 js 中提供了两个函数来实现 js 数据结构和 JSON 格式的转换处理,
+延迟加载就是等页面加载完成之后再加载 JavaScript 文件。 js 延迟加载有助于提高页面加载速度。
+一般有以下几种方式:
+一个拥有 length 属性和若干索引属性的对象就可以被称为类数组对象,类数组对象和数组类似,但是不能调用数组的方法。常见的类数组对象有 arguments 和 DOM 方法的返回结果,还有一个函数也可以被看作是类数组对象,因为它含有 length 属性值,代表可接收的参数个数。
+常见的类数组转换为数组的方法有这样几种:
+(1)通过 call 调用数组的 slice 方法来实现转换
+Array.prototype.slice.call(arrayLike);
+(2)通过 call 调用数组的 splice 方法来实现转换
+Array.prototype.splice.call(arrayLike, 0);
+(3)通过 apply 调用数组的 concat 方法来实现转换
+Array.prototype.concat.apply([], arrayLike);
+(4)通过 Array.from 方法来实现转换
+Array.from(arrayLike);
+在说Unicode之前需要先了解一下ASCII码:ASCII 码(American Standard Code for Information Interchange)称为美国标准信息交换码。
ASCII码可以表示的编码有限,要想表示其他语言的编码,还是要使用Unicode来表示,可以说Unicode是ASCII 的超集。
Unicode全称 Unicode Translation Format,又叫做统一码、万国码、单一码。Unicode 是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。
Unicode的实现方式(也就是编码方式)有很多种,常见的是UTF-8、UTF-16、UTF-32和USC-2。
UTF-8是使用最广泛的Unicode编码方式,它是一种可变长的编码方式,可以是1—4个字节不等,它可以完全兼容ASCII码的128个字符。
注意: UTF-8 是一种编码方式,Unicode是一个字符集合。
UTF-8的编码规则:
Unicode编码,因此对于英文字母,它的Unicode编码和ACSII编码一样。Unicode码 。来看一下具体的Unicode编号范围与对应的UTF-8二进制格式 :
| 编码范围(编号对应的十进制数) | 二进制格式 |
|---|---|
| 0x00—0x7F (0-127) | 0xxxxxxx |
| 0x80—0x7FF (128-2047) | 110xxxxx 10xxxxxx |
| 0x800—0xFFFF (2048-65535) | 1110xxxx 10xxxxxx 10xxxxxx |
| 0x10000—0x10FFFF (65536以上) | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
那该如何通过具体的Unicode编码,进行具体的UTF-8编码呢?步骤如下:
Unicode编码的所在的编号范围,进而找到与之对应的二进制格式Unicode编码转换为二进制数(去掉最高位的0)X中,如果有X未填,就设为0来看一个实际的例子:
+“马” 字的Unicode编码是:0x9A6C,整数编号是39532
+(1)首选确定了该字符在第三个范围内,它的格式是 1110xxxx 10xxxxxx 10xxxxxx
+(2)39532对应的二进制数为1001 1010 0110 1100
+(3)将二进制数填入X中,结果是:11101001 10101001 10101100
1. 平面的概念
+在了解UTF-16之前,先看一下平面的概念:
+Unicode编码中有很多很多的字符,它并不是一次性定义的,而是分区进行定义的,每个区存放65536(216)个字符,这称为一个平面,目前总共有17 个平面。
最前面的一个平面称为基本平面,它的码点从0 — 216-1,写成16进制就是U+0000 — U+FFFF,那剩下的16个平面就是辅助平面,码点范围是 U+10000—U+10FFFF。
2. UTF-16 概念:
+UTF-16也是Unicode编码集的一种编码形式,把Unicode字符集的抽象码位映射为16位长的整数(即码元)的序列,用于数据存储或传递。Unicode字符的码位需要1个或者2个16位长的码元来表示,因此UTF-16也是用变长字节表示的。
3. UTF-16 编码规则:
+U+0000—U+FFFF 的字符(常用字符集),直接用两个字节表示。U+10000—U+10FFFF 之间的字符,需要用四个字节表示。4. 编码识别
+那么问题来了,当遇到两个字节时,怎么知道是把它当做一个字符还是和后面的两个字节一起当做一个字符呢?
+UTF-16 编码肯定也考虑到了这个问题,在基本平面内,从 U+D800 — U+DFFF 是一个空段,也就是说这个区间的码点不对应任何的字符,因此这些空段就可以用来映射辅助平面的字符。
辅助平面共有 220 个字符位,因此表示这些字符至少需要 20 个二进制位。UTF-16 将这 20 个二进制位分成两半,前 10 位映射在 U+D800 — U+DBFF,称为高位(H),后 10 位映射在 U+DC00 — U+DFFF,称为低位(L)。这就相当于,将一个辅助平面的字符拆成了两个基本平面的字符来表示。
因此,当遇到两个字节时,发现它的码点在 U+D800 —U+DBFF之间,就可以知道,它后面的两个字节的码点应该在 U+DC00 — U+DFFF 之间,这四个字节必须放在一起进行解读。
5. 举例说明
+以 "𡠀" 字为例,它的 Unicode 码点为 0x21800,该码点超出了基本平面的范围,因此需要用四个字节来表示,步骤如下:
0x21800 - 0x100000001000110 0000000000U+D800 对应的二进制数为 1101100000000000, 将0001000110填充在它的后10 个二进制位,得到 1101100001000110,转成 16 进制数为 0xD846。同理,低位为 0xDC00,所以这个字的UTF-16 编码为 0xD846 0xDC00UTF-32 就是字符所对应编号的整数二进制形式,每个字符占四个字节,这个是直接进行转换的。该编码方式占用的储存空间较多,所以使用较少。
比如“马” 字的Unicode编号是:U+9A6C,整数编号是39532,直接转化为二进制:1001 1010 0110 1100,这就是它的UTF-32编码。
Unicode、UTF-8、UTF-16、UTF-32有什么区别?
+Unicode 是编码字符集(字符集),而UTF-8、UTF-16、UTF-32是字符集编码(编码规则);UTF-16 使用变长码元序列的编码方式,相较于定长码元序列的UTF-32算法更复杂,甚至比同样是变长码元序列的UTF-8也更为复杂,因为其引入了独特的代理对这样的代理机制;UTF-8需要判断每个字节中的开头标志信息,所以如果某个字节在传送过程中出错了,就会导致后面的字节也会解析出错;而UTF-16不会判断开头标志,即使错也只会错一个字符,所以容错能力教强;UTF-8就比UTF-16节省了很多空间;而如果字符内容全部是中文这样类似的字符或者混合字符中中文占绝大多数,那么UTF-16就占优势了,可以节省很多空间;现代计算机中数据都是以二进制的形式存储的,即0、1两种状态,计算机对二进制数据进行的运算加减乘除等都是叫位运算,即将符号位共同参与运算的运算。
+常见的位运算有以下几种:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +| 运算符 | 描述 | 运算规则 | |
|---|---|---|---|
& | 与 | 两个位都为1时,结果才为1 | |
| ` | ` | 或 | 两个位都为0时,结果才为0 |
^ | 异或 | 两个位相同为0,相异为1 | |
~ | 取反 | 0变1,1变0 | |
<< | 左移 | 各二进制位全部左移若干位,高位丢弃,低位补0 | |
>> | 右移 | 各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃 |
定义: 参加运算的两个数据按二进制位进行“与”运算。 +运算规则:
+0 & 0 = 0
+0 & 1 = 0
+1 & 0 = 0
+1 & 1 = 1
+总结:两位同时为1,结果才为1,否则结果为0。 +例如:3&5 即:
+0000 0011
+ 0000 0101
+ = 0000 0001
+因此 3&5 的值为1。 +注意:负数按补码形式参加按位与运算。
+用途:
+(1)判断奇偶
+只要根据最未位是0还是1来决定,为0就是偶数,为1就是奇数。因此可以用if ((i & 1) == 0)代替if (i % 2 == 0)来判断a是不是偶数。
(2)清零
+如果想将一个单元清零,即使其全部二进制位为0,只要与一个各位都为零的数值相与,结果为零。
+定义: 参加运算的两个对象按二进制位进行“或”运算。
+运算规则:
+0 | 0 = 0
+0 | 1 = 1
+1 | 0 = 1
+1 | 1 = 1
+总结:参加运算的两个对象只要有一个为1,其值为1。 +例如:3|5即:
+0000 0011
+ 0000 0101
+= 0000 0111
+因此,3|5的值为7。 +注意:负数按补码形式参加按位或运算。
+定义: 参加运算的两个数据按二进制位进行“异或”运算。
+运算规则:
+0 ^ 0 = 0
+0 ^ 1 = 1
+1 ^ 0 = 1
+1 ^ 1 = 0
+总结:参加运算的两个对象,如果两个相应位相同为0,相异为1。 +例如:3|5即:
+0000 0011
+ 0000 0101
+= 0000 0110
+因此,3^5的值为6。 +异或运算的性质:
+(a^b)^c == a^(b^c)(a + b)^c == a^b + b^cx^x=0,x^0=xa^b^b=a^0=a;定义: 参加运算的一个数据按二进制进行“取反”运算。
+运算规则:
+~ 1 = 0~ 0 = 1
+总结:对一个二进制数按位取反,即将0变1,1变0。 +例如:~6 即:
+0000 0110= 1111 1001
+在计算机中,正数用原码表示,负数使用补码存储,首先看最高位,最高位1表示负数,0表示正数。此计算机二进制码为负数,最高位为符号位。 +当发现按位取反为负数时,就直接取其补码,变为十进制:
+0000 0110 = 1111 1001反码:1000 0110补码:1000 0111
+因此,~6的值为-7。
+定义: 将一个运算对象的各二进制位全部左移若干位,左边的二进制位丢弃,右边补0。 +设 a=1010 1110,a = a<< 2 将a的二进制位左移2位、右补0,即得a=1011 1000。 +若左移时舍弃的高位不包含1,则每左移一位,相当于该数乘以2。
+定义: 将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃。 +例如:a=a>>2 将a的二进制位右移2位,左补0 或者 左补1得看被移数是正还是负。 +操作数每右移一位,相当于该数除以2。
+上面提到了补码、反码等知识,这里就补充一下。 +计算机中的有符号数有三种表示方法,即原码、反码和补码。三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”,而数值位,三种表示方法各不相同。
+(1)原码
+原码就是一个数的二进制数。例如:10的原码为0000 1010
+(2)反码
+例如:-10
+原码:1000 1010
+反码:1111 0101
+(3)补码
+例如:-10
+原码:1000 1010
+反码:1111 0101
+补码:1111 0110
+arguments是一个对象,它的属性是从 0 开始依次递增的数字,还有callee和length等属性,与数组相似;但是它却没有数组常见的方法属性,如forEach, reduce等,所以叫它们类数组。
要遍历类数组,有三个方法:
+(1)将数组的方法应用到类数组上,这时候就可以使用call和apply方法,如:
function foo(){
+ Array.prototype.forEach.call(arguments, a => console.log(a))
+}
+(2)使用Array.from方法将类数组转化成数组:
+function foo(){
+ const arrArgs = Array.from(arguments)
+ arrArgs.forEach(a => console.log(a))
+}
+(3)使用展开运算符将类数组转化成数组
+function foo(){
+ const arrArgs = [...arguments]
+ arrArgs.forEach(a => console.log(a))
+}
+一个拥有 length 属性和若干索引属性的对象就可以被称为类数组对象,类数组对象和数组类似,但是不能调用数组的方法。常见的类数组对象有 arguments 和 DOM 方法的返回结果,函数参数也可以被看作是类数组对象,因为它含有 length属性值,代表可接收的参数个数。
+常见的类数组转换为数组的方法有这样几种:
+Array.prototype.slice.call(arrayLike);
+Array.prototype.splice.call(arrayLike, 0);
+Array.prototype.concat.apply([], arrayLike);
+Array.from(arrayLike);
+AJAX是 Asynchronous JavaScript and XML 的缩写,指的是通过 JavaScript 的 异步通信,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。
+创建AJAX请求的步骤:
+const SERVER_URL = "/server";
+let xhr = new XMLHttpRequest();
+// 创建 Http 请求
+xhr.open("GET", url, true);
+// 设置状态监听函数
+xhr.onreadystatechange = function() {
+ if (this.readyState !== 4) return;
+ // 当请求成功时
+ if (this.status === 200) {
+ handle(this.response);
+ } else {
+ console.error(this.statusText);
+ }
+};
+// 设置请求失败时的监听函数
+xhr.onerror = function() {
+ console.error(this.statusText);
+};
+// 设置请求头信息
+xhr.responseType = "json";
+xhr.setRequestHeader("Accept", "application/json");
+// 发送 Http 请求
+xhr.send(null);
+使用Promise封装AJAX:
+// promise 封装实现:
+function getJSON(url) {
+ // 创建一个 promise 对象
+ let promise = new Promise(function(resolve, reject) {
+ let xhr = new XMLHttpRequest();
+ // 新建一个 http 请求
+ xhr.open("GET", url, true);
+ // 设置状态的监听函数
+ xhr.onreadystatechange = function() {
+ if (this.readyState !== 4) return;
+ // 当请求成功或失败时,改变 promise 的状态
+ if (this.status === 200) {
+ resolve(this.response);
+ } else {
+ reject(new Error(this.statusText));
+ }
+ };
+ // 设置错误监听函数
+ xhr.onerror = function() {
+ reject(new Error(this.statusText));
+ };
+ // 设置响应的数据类型
+ xhr.responseType = "json";
+ // 设置请求头信息
+ xhr.setRequestHeader("Accept", "application/json");
+ // 发送 http 请求
+ xhr.send(null);
+ });
+ return promise;
+}
+变量提升的表现是,无论在函数中何处位置声明的变量,好像都被提升到了函数的首部,可以在变量声明前访问到而不会报错。
+造成变量声明提升的本质原因是 js 引擎在代码执行前有一个解析的过程,创建了执行上下文,初始化了一些代码执行时需要用到的对象。当访问一个变量时,会到当前执行上下文中的作用域链中去查找,而作用域链的首端指向的是当前执行上下文的变量对象,这个变量对象是执行上下文的一个属性,它包含了函数的形参、所有的函数和变量声明,这个对象的是在代码解析的时候创建的。
+首先要知道,JS在拿到一个变量或者一个函数的时候,会有两步操作,即解析和执行。
+那为什么会进行变量提升呢?主要有以下两个原因:
+(1)提高性能 +在JS代码执行之前,会进行语法检查和预编译,并且这一操作只进行一次。这么做就是为了提高性能,如果没有这一步,那么每次执行代码前都必须重新解析一遍该变量(函数),而这是没有必要的,因为变量(函数)的代码并不会改变,解析一遍就够了。
+在解析的过程中,还会为函数生成预编译代码。在预编译时,会统计声明了哪些变量、创建了哪些函数,并对函数的代码进行压缩,去除注释、不必要的空白等。这样做的好处就是每次执行函数时都可以直接为该函数分配栈空间(不需要再解析一遍去获取代码中声明了哪些变量,创建了哪些函数),并且因为代码压缩的原因,代码执行也更快了。
+(2)容错性更好
+变量提升可以在一定程度上提高JS的容错性,看下面的代码:
+a = 1;var a;console.log(a);
+如果没有变量提升,这两行代码就会报错,但是因为有了变量提升,这段代码就可以正常执行。
+虽然,在可以开发过程中,可以完全避免这样写,但是有时代码很复杂的时候。可能因为疏忽而先使用后定义了,这样也不会影响正常使用。由于变量提升的存在,而会正常运行。
+总结:
+变量提升虽然有一些优点,但是他也会造成一定的问题,在ES6中提出了let、const来定义变量,它们就没有变量提升的机制。下面看一下变量提升可能会导致的问题:
+var tmp = new Date();
+
+function fn(){
+ console.log(tmp);
+ if(false){
+ var tmp = 'hello world';
+ }
+}
+
+fn(); // undefined
+在这个函数中,原本是要打印出外层的tmp变量,但是因为变量提升的问题,内层定义的tmp被提到函数内部的最顶部,相当于覆盖了外层的tmp,所以打印结果为undefined。
+var tmp = 'hello world';
+
+for (var i = 0; i < tmp.length; i++) {
+ console.log(tmp[i]);
+}
+
+console.log(i); // 11
+由于遍历时定义的i会变量提升成为一个全局变量,在函数结束之后不会被销毁,所以打印出来11。
+尾调用指的是函数的最后一步调用另一个函数。代码执行是基于执行栈的,所以当在一个函数里调用另一个函数时,会保留当前的执行上下文,然后再新建另外一个执行上下文加入栈中。使用尾调用的话,因为已经是函数的最后一步,所以这时可以不必再保留当前的执行上下文,从而节省了内存,这就是尾调用优化。但是 ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。
+ES6 Module和CommonJS模块的区别:
+ES6 Module和CommonJS模块的共同点:
+DOM 节点的获取的API及使用:
+getElementById // 按照 id 查询
+getElementsByTagName // 按照标签名查询
+getElementsByClassName // 按照类名查询
+querySelectorAll // 按照 css 选择器查询
+
+// 按照 id 查询
+var imooc = document.getElementById('imooc') // 查询到 id 为 imooc 的元素
+// 按照标签名查询
+var pList = document.getElementsByTagName('p') // 查询到标签为 p 的集合
+console.log(divList.length)
+console.log(divList[0])
+// 按照类名查询
+var moocList = document.getElementsByClassName('mooc') // 查询到类名为 mooc 的集合
+// 按照 css 选择器查询
+var pList = document.querySelectorAll('.mooc') // 查询到类名为 mooc 的集合
+创建一个新节点,并把它添加到指定节点的后面。 已知的 HTML 结构如下:
+<html>
+ <head>
+ <title>DEMO</title>
+ </head>
+ <body>
+ <div id="container">
+ <h1 id="title">我是标题</h1>
+ </div>
+ </body>
+</html>
+要求添加一个有内容的 span 节点到 id 为 title 的节点后面,做法就是:
+// 首先获取父节点
+var container = document.getElementById('container')
+// 创建新节点
+var targetSpan = document.createElement('span')
+// 设置 span 节点的内容
+targetSpan.innerHTML = 'hello world'
+// 把新创建的元素塞进父节点里去
+container.appendChild(targetSpan)
+删除指定的 DOM 节点, 已知的 HTML 结构如下:
+<html>
+ <head>
+ <title>DEMO</title>
+ </head>
+ <body>
+ <div id="container">
+ <h1 id="title">我是标题</h1>
+ </div>
+ </body>
+</html>
+需要删除 id 为 title 的元素,做法是:
+// 获取目标元素的父元素
+var container = document.getElementById('container')
+// 获取目标元素
+var targetNode = document.getElementById('title')
+// 删除目标元素
+container.removeChild(targetNode)
+或者通过子节点数组来完成删除:
+// 获取目标元素的父元素var container = document.getElementById('container')// 获取目标元素var targetNode = container.childNodes[1]// 删除目标元素container.removeChild(targetNode)
+修改 DOM 元素这个动作可以分很多维度,比如说移动 DOM 元素的位置,修改 DOM 元素的属性等。
+将指定的两个 DOM 元素交换位置, 已知的 HTML 结构如下:
+<html>
+ <head>
+ <title>DEMO</title>
+ </head>
+ <body>
+ <div id="container">
+ <h1 id="title">我是标题</h1>
+ <p id="content">我是内容</p>
+ </div>
+ </body>
+</html>
+现在需要调换 title 和 content 的位置,可以考虑 insertBefore 或者 appendChild:
+// 获取父元素
+var container = document.getElementById('container')
+
+// 获取两个需要被交换的元素
+var title = document.getElementById('title')
+var content = document.getElementById('content')
+// 交换两个元素,把 content 置于 title 前面
+container.insertBefore(content, title)
+use strict 是一种 ECMAscript5 添加的(严格模式)运行模式,这种模式使得 Javascript 在更严格的条件下运行。设立严格模式的目的如下:
+区别:
+两者对比:强类型语言在速度上可能略逊色于弱类型语言,但是强类型语言带来的严谨性可以有效地帮助避免许多错误。
+(1)解释型语言 +使用专门的解释器对源程序逐行解释成特定平台的机器码并立即执行。是代码在执行时才被解释器一行行动态翻译和执行,而不是在执行之前就完成翻译。解释型语言不需要事先编译,其直接将源代码解释成机器码并立即执行,所以只要某一平台提供了相应的解释器即可运行该程序。其特点总结如下
+(2)编译型语言 +使用专门的编译器,针对特定的平台,将高级语言源代码一次性的编译成可被该平台硬件执行的机器码,并包装成该平台所能识别的可执行性程序的格式。在编译型语言写的程序执行之前,需要一个专门的编译过程,把源代码编译成机器语言的文件,如exe格式的文件,以后要再运行时,直接使用编译结果即可,如直接运行exe文件。因为只需编译一次,以后运行时不需要编译,所以编译型语言执行效率高。其特点总结如下:
+两者主要区别在于: 前者源程序编译后即可在该平台运行,后者是在运行期间才编译。所以前者运行速度快,后者跨平台性好。
+for…of 是ES6新增的遍历方式,允许遍历一个含有iterator接口的数据结构(数组、对象等)并且返回各项的值,和ES3中的for…in的区别如下
+总结: for...in 循环主要是为了遍历对象而生,不适用于遍历数组;for...of 循环可以用来遍历数组、类数组对象,字符串、Set、Map 以及 Generator 对象。
+for…of是作为ES6新增的遍历方式,允许遍历一个含有iterator接口的数据结构(数组、对象等)并且返回各项的值,普通的对象用for..of遍历是会报错的。
+如果需要遍历的对象是类数组对象,用Array.from转成数组即可。
+var obj = {
+ 0:'one',
+ 1:'two',
+ length: 2
+};
+obj = Array.from(obj);
+for(var k of obj){
+ console.log(k)
+}
+如果不是类数组对象,就给对象添加一个[Symbol.iterator]属性,并指向一个迭代器即可。
+//方法一:
+var obj = {
+ a:1,
+ b:2,
+ c:3
+};
+
+obj[Symbol.iterator] = function(){
+ var keys = Object.keys(this);
+ var count = 0;
+ return {
+ next(){
+ if(count<keys.length){
+ return {value: obj[keys[count++]],done:false};
+ }else{
+ return {value:undefined,done:true};
+ }
+ }
+ }
+};
+
+for(var k of obj){
+ console.log(k);
+}
+
+// 方法二
+var obj = {
+ a:1,
+ b:2,
+ c:3
+};
+obj[Symbol.iterator] = function*(){
+ var keys = Object.keys(obj);
+ for(var k of keys){
+ yield [k,obj[k]]
+ }
+};
+
+for(var [k,v] of obj){
+ console.log(k,v);
+}
+(1)AJAX +Ajax 即“AsynchronousJavascriptAndXML”(异步 JavaScript 和 XML),是指一种创建交互式网页应用的网页开发技术。它是一种在无需重新加载整个网页的情况下,能够更新部分网页的技术。通过在后台与服务器进行少量数据交换,Ajax 可以使网页实现异步更新。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。传统的网页(不使用 Ajax)如果需要更新内容,必须重载整个网页页面。其缺点如下:
+(2)Fetch +fetch号称是AJAX的替代品,是在ES6出现的,使用了ES6中的promise对象。Fetch是基于promise设计的。Fetch的代码结构比起ajax简单多。fetch不是ajax的进一步封装,而是原生js,没有使用XMLHttpRequest对象。
+fetch的优点:
+fetch的缺点:
+(3)Axios +Axios 是一种基于Promise封装的HTTP客户端,其特点如下:
+| 方法 | 是否改变原数组 | 特点 |
|---|---|---|
| forEach() | 否 | 数组方法,不改变原数组,没有返回值 |
| map() | 否 | 数组方法,不改变原数组,有返回值,可链式调用 |
| filter() | 否 | 数组方法,过滤数组,返回包含符合条件的元素的数组,可链式调用 |
| for...of | 否 | for...of遍历具有Iterator迭代器的对象的属性,返回的是数组的元素、对象的属性值,不能遍历普通的obj对象,将异步循环变成同步循环 |
| every() 和 some() | 否 | 数组方法,some()只要有一个是true,便返回true;而every()只要有一个是false,便返回false. |
| find() 和 findIndex() | 否 | 数组方法,find()返回的是第一个符合条件的值;findIndex()返回的是第一个返回条件的值的索引值 |
| reduce() 和 reduceRight() | 否 | 数组方法,reduce()对数组正序操作;reduceRight()对数组逆序操作 |
遍历方法的详细解释:《细数JavaScript中那些遍历和循环》
+这方法都是用来遍历数组的,两者区别如下:
+在JavaScript中是使用构造函数来新建一个对象的,每一个构造函数的内部都有一个 prototype 属性,它的属性值是一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。当使用构造函数新建一个对象后,在这个对象的内部将包含一个指针,这个指针指向构造函数的 prototype 属性对应的值,在 ES5 中这个指针被称为对象的原型。一般来说不应该能够获取到这个值的,但是现在浏览器中都实现了 proto 属性来访问这个属性,但是最好不要使用这个属性,因为它不是规范中规定的。ES5 中新增了一个 Object.getPrototypeOf() 方法,可以通过这个方法来获取对象的原型。
+当访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。原型链的尽头一般来说都是 Object.prototype 所以这就是新建的对象为什么能够使用 toString() 等方法的原因。
+特点: JavaScript 对象是通过引用来传递的,创建的每个新对象实体中并没有一份属于自己的原型副本。当修改原型时,与之相关的对象也会继承这一改变。
+
function Person(name) {
+ this.name = name
+}
+// 修改原型
+Person.prototype.getName = function() {}
+var p = new Person('hello')
+console.log(p.__proto__ === Person.prototype) // true
+console.log(p.__proto__ === p.constructor.prototype) // true
+// 重写原型
+Person.prototype = {
+ getName: function() {}
+}
+var p = new Person('hello')
+console.log(p.__proto__ === Person.prototype) // true
+console.log(p.__proto__ === p.constructor.prototype) // false
+可以看到修改原型的时候p的构造函数不是指向Person了,因为直接给Person的原型对象直接用对象赋值时,它的构造函数指向的了根构造函数Object,所以这时候p.constructor === Object ,而不是p.constructor === Person。要想成立,就要用constructor指回来:
Person.prototype = {
+ getName: function() {}
+}
+var p = new Person('hello')
+p.constructor = Person
+console.log(p.__proto__ === Person.prototype) // true
+console.log(p.__proto__ === p.constructor.prototype) // true
+p.__proto__ // Person.prototype
+Person.prototype.__proto__ // Object.prototype
+p.__proto__.__proto__ //Object.prototype
+p.__proto__.constructor.prototype.__proto__ // Object.prototype
+Person.prototype.constructor.prototype.__proto__ // Object.prototype
+p1.__proto__.constructor // Person
+Person.prototype.constructor // Person
+由于Object是构造函数,原型链终点是Object.prototype.__proto__,而Object.prototype.__proto__=== null // true,所以,原型链的终点是null。原型链上的所有原型都是对象,所有的对象最终都是由Object构造的,而Object.prototype的下一级是Object.prototype.__proto__。
+
使用后hasOwnProperty()方法来判断属性是否属于原型链的属性:
function iterate(obj){
+ var res=[];
+ for(var key in obj){
+ if(obj.hasOwnProperty(key))
+ res.push(key+': '+obj[key]);
+ }
+ return res;
+}
+闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。
+闭包有两个常用的用途;
+比如,函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包。
+function A() {
+ let a = 1
+ window.B = function () {
+ console.log(a)
+ }
+}
+A()
+B() // 1
+在 JS 中,闭包存在的意义就是让我们可以间接访问函数内部的变量。经典面试题:循环中使用闭包解决 var 定义函数的问题
+for (var i = 1; i <= 5; i++) {
+ setTimeout(function timer() {
+ console.log(i)
+ }, i * 1000)
+}
+首先因为 setTimeout 是个异步函数,所以会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6。解决办法有三种:
for (var i = 1; i <= 5; i++) { ;(function(j) { setTimeout(function timer() { console.log(j) }, j * 1000) })(i)}
+在上述代码中,首先使用了立即执行函数将 i 传入函数内部,这个时候值就被固定在了参数 j 上面不会改变,当下次执行 timer 这个闭包的时候,就可以使用外部函数的变量 j,从而达到目的。
setTimeout 的第三个参数,这个参数会被当成 timer 函数的参数传入。for (var i = 1; i <= 5; i++) {
+ setTimeout(
+ function timer(j) {
+ console.log(j)
+ },
+ i * 1000,
+ i
+ )
+}
+let 定义 i 了来解决问题了,这个也是最为推荐的方式for (let i = 1; i <= 5; i++) {
+ setTimeout(function timer() {
+ console.log(i)
+ }, i * 1000)
+}
+(1)全局作用域
+(2)函数作用域
+{ }包裹的代码片段)作用域链: +在当前作用域中查找所需变量,但是该作用域没有这个变量,那这个变量就是自由变量。如果在自己作用域找不到该变量就去父级作用域查找,依次向上级作用域查找,直到访问到window对象就被终止,这一层层的关系就是作用域链。
+作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,可以访问到外层环境的变量和函数。
+作用域链的本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。作用域链的前端始终都是当前执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象。
+当查找一个变量时,如果当前执行环境中没有找到,可以沿着作用域链向后查找。
+(1)全局执行上下文
+任何不在函数内部的都是全局执行上下文,它首先会创建一个全局的window对象,并且设置this的值等于这个全局对象,一个程序中只有一个全局执行上下文。
+(2)函数执行上下文
+当一个函数被调用时,就会为该函数创建一个新的执行上下文,函数的上下文可以有任意多个。
+(3)eval函数执行上下文
执行在eval函数中的代码会有属于他自己的执行上下文,不过eval函数不常使用,不做介绍。
+let a = 'Hello World!';
+function first() {
+ console.log('Inside first function');
+ second();
+ console.log('Again inside first function');
+}
+function second() {
+ console.log('Inside second function');
+}
+first();
+//执行顺序
+//先执行second(),在执行first()
+创建执行上下文有两个阶段:创建阶段和执行阶段
+1)创建阶段
+(1)this绑定
+(2)创建词法环境组件
+(3)创建变量环境组件
+2)执行阶段 +此阶段会完成对变量的分配,最后执行完代码。
+简单来说执行上下文就是指:
+在执行一点JS代码之前,需要先解析代码。解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为undefined,函数先声明好可使用。这一步执行完了,才开始正式的执行程序。
+在一个函数执行之前,也会创建一个函数执行上下文环境,跟全局执行上下文类似,不过函数执行上下文会多出this、arguments和函数的参数。
+this,arguments注: 由于字数限制,剩余内容在下篇进行总结哦。
\ No newline at end of file diff --git a/src/content/notes/frontend/javascript-part-2.html b/src/content/notes/frontend/javascript-part-2.html new file mode 100644 index 0000000..3533f30 --- /dev/null +++ b/src/content/notes/frontend/javascript-part-2.html @@ -0,0 +1,598 @@ +this 是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。在实际开发中,this 的指向可以通过四种调用模式来判断。
+这四种方式,使用构造器调用模式的优先级最高,然后是 apply、call 和 bind 调用模式,然后是方法调用模式,然后是函数调用模式。
+它们的作用一模一样,区别仅在于传入参数的形式的不同。
+(1)call 函数的实现步骤:
+Function.prototype.myCall = function(context) {
+ // 判断调用对象
+ if (typeof this !== "function") {
+ console.error("type error");
+ }
+ // 获取参数
+ let args = [...arguments].slice(1),
+ result = null;
+ // 判断 context 是否传入,如果未传入则设置为 window
+ context = context || window;
+ // 将调用函数设为对象的方法
+ context.fn = this;
+ // 调用函数
+ result = context.fn(...args);
+ // 将属性删除
+ delete context.fn;
+ return result;
+};
+(2)apply 函数的实现步骤:
+Function.prototype.myApply = function(context) {
+ // 判断调用对象是否为函数
+ if (typeof this !== "function") {
+ throw new TypeError("Error");
+ }
+ let result = null;
+ // 判断 context 是否存在,如果未传入则为 window
+ context = context || window;
+ // 将函数设为对象的方法
+ context.fn = this;
+ // 调用方法
+ if (arguments[1]) {
+ result = context.fn(...arguments[1]);
+ } else {
+ result = context.fn();
+ }
+ // 将属性删除
+ delete context.fn;
+ return result;
+};
+(3)bind 函数的实现步骤:
+Function.prototype.myBind = function(context) {
+ // 判断调用对象是否为函数
+ if (typeof this !== "function") {
+ throw new TypeError("Error");
+ }
+ // 获取参数
+ var args = [...arguments].slice(1),
+ fn = this;
+ return function Fn() {
+ // 根据调用方式,传入不同绑定值
+ return fn.apply(
+ this instanceof Fn ? this : context,
+ args.concat(...arguments)
+ );
+ };
+};
+JavaScript中的异步机制可以分为以下几种:
+console.log('script start') //1. 打印 script start
+setTimeout(function(){
+ console.log('settimeout') // 4. 打印 settimeout
+}) // 2. 调用 setTimeout 函数,并定义其完成后执行的回调函数
+console.log('script end') //3. 打印 script start
+// 输出顺序:script start->script end->settimeout
+Promise本身是同步的立即执行函数, 当在executor中执行resolve或者reject的时候, 此时是异步操作, 会先执行then/catch等,当主栈完成后,才会去调用resolve/reject中存放的方法执行,打印p的时候,是打印的返回结果,一个Promise实例。
+console.log('script start')
+let promise1 = new Promise(function (resolve) {
+ console.log('promise1')
+ resolve()
+ console.log('promise1 end')
+}).then(function () {
+ console.log('promise2')
+})
+setTimeout(function(){
+ console.log('settimeout')
+})
+console.log('script end')
+// 输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout
+当JS主线程执行到Promise对象时:
+async function async1(){
+ console.log('async1 start');
+ await async2();
+ console.log('async1 end')
+}
+async function async2(){
+ console.log('async2')
+}
+console.log('script start');
+async1();
+console.log('script end')
+// 输出顺序:script start->async1 start->async2->script end->async1 end
+async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。
+例如:
+async function func1() {
+ return 1
+}
+console.log(func1())
+
+func1的运行结果其实就是一个Promise对象。因此也可以使用then来处理后续逻辑。
func1().then(res => {
+ console.log(res); // 30
+})
+await的含义为等待,也就是 async 函数需要等待await后的函数执行完成并且有了返回结果(Promise对象)之后,才能继续执行下面的代码。await通过返回一个Promise对象来实现同步的效果。
+Promise是异步编程的一种解决方案,它是一个对象,可以获取异步操作的消息,他的出现大大改善了异步编程的困境,避免了地狱回调,它比传统的解决方案回调函数和事件更合理和更强大。
+所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
+(1)Promise的实例有三个状态:
+当把一件事情交给promise时,它的状态就是Pending,任务完成了状态就变成了Resolved、没有完成失败了就变成了Rejected。
+(2)Promise的实例有两个过程:
+注意:一旦从进行状态变成为其他状态就永远不能更改状态了。
+Promise的特点:
+pending(进行中)、fulfilled(已成功)、rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态,这也是promise这个名字的由来——“承诺”;pending变为fulfilled,从pending变为rejected。这时就称为resolved(已定型)。如果改变已经发生了,你再对promise对象添加回调函数,也会立即得到这个结果。这与事件(event)完全不同,事件的特点是:如果你错过了它,再去监听是得不到结果的。Promise的缺点:
+总结: +Promise 对象是异步编程的一种解决方案,最早由社区提出。Promise 是一个构造函数,接收一个函数作为参数,返回一个 Promise 实例。一个 Promise 实例有三种状态,分别是pending、resolved 和 rejected,分别代表了进行中、已成功和已失败。实例的状态只能由 pending 转变 resolved 或者rejected 状态,并且状态一经改变,就凝固了,无法再被改变了。
+状态的改变是通过 resolve() 和 reject() 函数来实现的,可以在异步操作结束后调用这两个函数改变 Promise 实例的状态,它的原型上定义了一个 then 方法,使用这个 then 方法可以为两个状态的改变注册回调函数。这个回调函数属于微任务,会在本轮事件循环的末尾执行。
+注意: 在构造 Promise 的时候,构造函数内部的代码是立即执行的
Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。
+Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。
const promise = new Promise(function(resolve, reject) {
+ // ... some code
+ if (/* 异步操作成功 */){
+ resolve(value);
+ } else {
+ reject(error);
+ }
+});
+一般情况下都会使用new Promise()来创建promise对象,但是也可以使用promise.resolve和promise.reject这两个方法:
Promise.resolve(value)的返回值也是一个promise对象,可以对返回值进行.then调用,代码如下:
Promise.resolve(11).then(function(value){
+ console.log(value); // 打印出11
+});
+resolve(11)代码中,会让promise对象进入确定(resolve状态),并将参数11传递给后面的then所指定的onFulfilled 函数;
创建promise对象可以使用new Promise的形式创建对象,也可以使用Promise.resolve(value)的形式创建promise对象;
Promise.reject 也是new Promise的快捷形式,也创建一个promise对象。代码如下:
Promise.reject(new Error(“我错了,请原谅俺!!”));
+就是下面的代码new Promise的简单形式:
+new Promise(function(resolve,reject){
+ reject(new Error("我错了!"));
+});
+下面是使用resolve方法和reject方法:
+function testPromise(ready) {
+ return new Promise(function(resolve,reject){
+ if(ready) {
+ resolve("hello world");
+ }else {
+ reject("No thanks");
+ }
+ });
+};
+// 方法调用
+testPromise(true).then(function(msg){
+ console.log(msg);
+},function(error){
+ console.log(error);
+});
+上面的代码的含义是给testPromise方法传递一个参数,返回一个promise对象,如果为true的话,那么调用promise对象中的resolve()方法,并且把其中的参数传递给后面的then第一个函数内,因此打印出 “hello world”, 如果为false的话,会调用promise对象中的reject()方法,则会进入then的第二个函数内,会打印No thanks;
Promise有五个常用的方法:then()、catch()、all()、race()、finally。下面就来看一下这些方法。
+当Promise执行的内容符合成功条件时,调用resolve函数,失败就调用reject函数。Promise创建完了,那该如何调用呢?
promise.then(function(value) {
+ // success
+}, function(error) {
+ // failure
+});
+then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。其中第二个参数可以省略。
+then方法返回的是一个新的Promise实例(不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。
当要写有顺序的异步事件时,需要串行时,可以这样写:
+let promise = new Promise((resolve,reject)=>{
+ ajax('first').success(function(res){
+ resolve(res);
+ })
+})
+promise.then(res=>{
+ return new Promise((resovle,reject)=>{
+ ajax('second').success(function(res){
+ resolve(res)
+ })
+ })
+}).then(res=>{
+ return new Promise((resovle,reject)=>{
+ ajax('second').success(function(res){
+ resolve(res)
+ })
+ })
+}).then(res=>{
+
+})
+那当要写的事件没有顺序或者关系时,还如何写呢?可以使用all 方法来解决。
2. catch()
+Promise对象除了有then方法,还有一个catch方法,该方法相当于then方法的第二个参数,指向reject的回调函数。不过catch方法还有一个作用,就是在执行resolve回调函数时,如果出现错误,抛出异常,不会停止运行,而是进入catch方法中。
p.then((data) => {
+ console.log('resolved',data);
+},(err) => {
+ console.log('rejected',err);
+ }
+);
+p.then((data) => {
+ console.log('resolved',data);
+}).catch((err) => {
+ console.log('rejected',err);
+});
+3. all()
+all方法可以完成并行任务, 它接收一个数组,数组的每一项都是一个promise对象。当数组中所有的promise的状态都达到resolved的时候,all方法的状态就会变成resolved,如果有一个状态变成了rejected,那么all方法的状态就会变成rejected。
javascript
+let promise1 = new Promise((resolve,reject)=>{
+ setTimeout(()=>{
+ resolve(1);
+ },2000)
+});
+let promise2 = new Promise((resolve,reject)=>{
+ setTimeout(()=>{
+ resolve(2);
+ },1000)
+});
+let promise3 = new Promise((resolve,reject)=>{
+ setTimeout(()=>{
+ resolve(3);
+ },3000)
+});
+Promise.all([promise1,promise2,promise3]).then(res=>{
+ console.log(res);
+ //结果为:[1,2,3]
+})
+调用all方法时的结果成功的时候是回调函数的参数也是一个数组,这个数组按顺序保存着每一个promise对象resolve执行时的值。
(4)race()
+race方法和all一样,接受的参数是一个每项都是promise的数组,但是与all不同的是,当最先执行完的事件执行完之后,就直接返回该promise对象的值。如果第一个promise对象状态变成resolved,那自身的状态变成了resolved;反之第一个promise变成rejected,那自身状态就会变成rejected。
let promise1 = new Promise((resolve,reject)=>{
+ setTimeout(()=>{
+ reject(1);
+ },2000)
+});
+let promise2 = new Promise((resolve,reject)=>{
+ setTimeout(()=>{
+ resolve(2);
+ },1000)
+});
+let promise3 = new Promise((resolve,reject)=>{
+ setTimeout(()=>{
+ resolve(3);
+ },3000)
+});
+Promise.race([promise1,promise2,promise3]).then(res=>{
+ console.log(res);
+ //结果:2
+},rej=>{
+ console.log(rej)};
+)
+那么race方法有什么实际作用呢?当要做一件事,超过多长时间就不做了,可以用这个方法来解决:
Promise.race([promise1,timeOutPromise(5000)]).then(res=>{})
+5. finally()
+finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。
promise
+.then(result => {···})
+.catch(error => {···})
+.finally(() => {···});
+上面代码中,不管promise最后的状态,在执行完then或catch指定的回调函数以后,都会执行finally方法指定的回调函数。
下面是一个例子,服务器使用 Promise 处理请求,然后使用finally方法关掉服务器。
server.listen(port)
+ .then(function () {
+ // ...
+ })
+ .finally(server.stop);
+finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。finally本质上是then方法的特例:
promise
+.finally(() => {
+ // 语句
+});
+// 等同于
+promise
+.then(
+ result => {
+ // 语句
+ return result;
+ },
+ error => {
+ // 语句
+ throw error;
+ }
+);
+上面代码中,如果不使用finally方法,同样的语句需要为成功和失败两种情况各写一次。有了finally方法,则只需要写一次。
在工作中经常会碰到这样一个需求,比如我使用ajax发一个A请求后,成功后拿到数据,需要把数据传给B请求;那么需要如下编写代码:
+let fs = require('fs')
+fs.readFile('./a.txt','utf8',function(err,data){
+ fs.readFile(data,'utf8',function(err,data){
+ fs.readFile(data,'utf8',function(err,data){
+ console.log(data)
+ })
+ })
+})
+上面的代码有如下缺点:
+Promise出现之后,代码变成这样:
let fs = require('fs')
+function read(url){
+ return new Promise((resolve,reject)=>{
+ fs.readFile(url,'utf8',function(error,data){
+ error && reject(error)
+ resolve(data)
+ })
+ })
+}
+read('./a.txt').then(data=>{
+ return read(data)
+}).then(data=>{
+ return read(data)
+}).then(data=>{
+ console.log(data)
+})
+这样代码看起了就简洁了很多,解决了地狱回调的问题。
+(1)Promise.all
+Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。
Promise.all中传入的是数组,返回的也是是数组,并且会将进行映射,传入的promise对象返回的值是按照顺序在数组中排列的,但是注意的是他们执行的顺序并不是按照顺序的,除非可迭代对象为空。
+需要注意,Promise.all获得的成功结果的数组里面的数据顺序和Promise.all接收到的数组顺序是一致的,这样当遇到发送多个请求并根据请求顺序获取和使用数据的场景,就可以使用Promise.all来解决。
+(2)Promise.race
+顾名思义,Promse.race就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。当要做一件事,超过多长时间就不做了,可以用这个方法来解决:
+Promise.race([promise1,timeOutPromise(5000)]).then(res=>{})
+async/await其实是Generator 的语法糖,它能实现的效果都能用then链来实现,它是为优化then链而开发出来的。从字面上来看,async是“异步”的简写,await则为等待,所以很好理解async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。当然语法上强制规定await只能出现在asnyc函数中,先来看看async函数返回了什么:
async function testAsy(){
+ return 'hello world';
+}
+let result = testAsy();
+console.log(result)
+所以,async 函数返回的是一个 Promise 对象。async 函数(包含函数语句、函数表达式、Lambda表达式)会返回一个 Promise 对象,如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。
async 函数返回的是一个 Promise 对象,所以在最外层不能用 await 获取其返回值的情况下,当然应该用原来的方式:then() 链来处理这个 Promise 对象,就像这样:
async function testAsy(){
+ return 'hello world'
+}
+let result = testAsy()
+console.log(result)
+result.then(v=>{
+ console.log(v) // hello world
+})
+那如果 async 函数没有返回值,又该如何?很容易想到,它会返回 Promise.resolve(undefined)。
联想一下 Promise 的特点——无等待,所以在没有 await 的情况下执行 async 函数,它会立即执行,返回一个 Promise 对象,并且,绝不会阻塞后面的语句。这和普通返回 Promise 对象的函数并无二致。
注意:Promise.resolve(x) 可以看作是 new Promise(resolve => resolve(x)) 的简写,可以用于快速封装字面量对象或其他对象,将其封装成 Promise 实例。
await 在等待什么呢? 一般来说,都认为 await 是在等待一个 async 函数完成。不过按语法说明,await 等待的是一个表达式,这个表达式的计算结果是 Promise 对象或者其它值(换句话说,就是没有特殊限定)。
+因为 async 函数返回一个 Promise 对象,所以 await 可以用于等待一个 async 函数的返回值——这也可以说是 await 在等 async 函数,但要清楚,它等的实际是一个返回值。注意到 await 不仅仅用于等 Promise 对象,它可以等任意表达式的结果,所以,await 后面实际是可以接普通函数调用或者直接量的。所以下面这个示例完全可以正确运行:
+function getSomething() {
+ return "something";
+}
+async function testAsync() {
+ return Promise.resolve("hello async");
+}
+async function test() {
+ const v1 = await getSomething();
+ const v2 = await testAsync();
+ console.log(v1, v2);
+}
+test();
+await 表达式的运算结果取决于它等的是什么。
+来看一个例子:
+function testAsy(x){
+ return new Promise(resolve=>{setTimeout(() => {
+ resolve(x);
+ }, 3000)
+ }
+ )
+}
+async function testAwt(){
+ let result = await testAsy('hello world');
+ console.log(result); // 3秒钟之后出现hello world
+ console.log('cuger') // 3秒钟之后出现cug
+}
+testAwt();
+console.log('cug') //立即输出cug
+这就是 await 必须用在 async 函数中的原因。async 函数调用不会造成阻塞,它内部所有的阻塞都被封装在一个 Promise 对象中异步执行。await暂停当前async的执行,所以'cug''最先输出,hello world'和‘cuger’是3秒钟后同时出现的。
+单一的 Promise 链并不能发现 async/await 的优势,但是,如果需要处理由多个 Promise 组成的 then 链的时候,优势就能体现出来了(很有意思,Promise 通过 then 链来解决多层回调的问题,现在又用 async/await 来进一步优化它)。
+假设一个业务,分多个步骤完成,每个步骤都是异步的,而且依赖于上一个步骤的结果。仍然用 setTimeout 来模拟异步操作:
/**
+ * 传入参数 n,表示这个函数执行的时间(毫秒)
+ * 执行的结果是 n + 200,这个值将用于下一步骤
+ */
+function takeLongTime(n) {
+ return new Promise(resolve => {
+ setTimeout(() => resolve(n + 200), n);
+ });
+}
+function step1(n) {
+ console.log(`step1 with ${n}`);
+ return takeLongTime(n);
+}
+function step2(n) {
+ console.log(`step2 with ${n}`);
+ return takeLongTime(n);
+}
+function step3(n) {
+ console.log(`step3 with ${n}`);
+ return takeLongTime(n);
+}
+现在用 Promise 方式来实现这三个步骤的处理:
+function doIt() {
+ console.time("doIt");
+ const time1 = 300;
+ step1(time1)
+ .then(time2 => step2(time2))
+ .then(time3 => step3(time3))
+ .then(result => {
+ console.log(`result is ${result}`);
+ console.timeEnd("doIt");
+ });
+}
+doIt();
+// c:\var\test>node --harmony_async_await .
+// step1 with 300
+// step2 with 500
+// step3 with 700
+// result is 900
+// doIt: 1507.251ms
+输出结果 result 是 step3() 的参数 700 + 200 = 900。doIt() 顺序执行了三个步骤,一共用了 300 + 500 + 700 = 1500 毫秒,和 console.time()/console.timeEnd() 计算的结果一致。
如果用 async/await 来实现呢,会是这样:
+async function doIt() {
+ console.time("doIt");
+ const time1 = 300;
+ const time2 = await step1(time1);
+ const time3 = await step2(time2);
+ const result = await step3(time3);
+ console.log(`result is ${result}`);
+ console.timeEnd("doIt");
+}
+doIt();
+结果和之前的 Promise 实现是一样的,但是这个代码看起来是不是清晰得多,几乎跟同步代码一样
+async function fn(){
+ try{
+ let a = await Promise.reject('error')
+ }catch(error){
+ console.log(error)
+ }
+}
+一般使用字面量的形式直接创建对象,但是这种创建方式对于创建大量相似对象的时候,会产生大量的重复代码。但 js和一般的面向对象的语言不同,在 ES6 之前它没有类的概念。但是可以使用函数来进行模拟,从而产生出可复用的对象创建方式,常见的有以下几种:
+(1)第一种是工厂模式,工厂模式的主要工作原理是用函数来封装创建对象的细节,从而通过调用函数来达到复用的目的。但是它有一个很大的问题就是创建出来的对象无法和某个类型联系起来,它只是简单的封装了复用代码,而没有建立起对象和类型间的关系。
+(2)第二种是构造函数模式。js 中每一个函数都可以作为构造函数,只要一个函数是通过 new 来调用的,那么就可以把它称为构造函数。执行构造函数首先会创建一个对象,然后将对象的原型指向构造函数的 prototype 属性,然后将执行上下文中的 this 指向这个对象,最后再执行整个函数,如果返回值不是对象,则返回新建的对象。因为 this 的值指向了新建的对象,因此可以使用 this 给对象赋值。构造函数模式相对于工厂模式的优点是,所创建的对象和构造函数建立起了联系,因此可以通过原型来识别对象的类型。但是构造函数存在一个缺点就是,造成了不必要的函数对象的创建,因为在 js 中函数也是一个对象,因此如果对象属性中如果包含函数的话,那么每次都会新建一个函数对象,浪费了不必要的内存空间,因为函数是所有的实例都可以通用的。
+(3)第三种模式是原型模式,因为每一个函数都有一个 prototype 属性,这个属性是一个对象,它包含了通过构造函数创建的所有实例都能共享的属性和方法。因此可以使用原型对象来添加公用属性和方法,从而实现代码的复用。这种方式相对于构造函数模式来说,解决了函数对象的复用问题。但是这种模式也存在一些问题,一个是没有办法通过传入参数来初始化值,另一个是如果存在一个引用类型如 Array 这样的值,那么所有的实例将共享一个对象,一个实例对引用类型值的改变会影响所有的实例。
+(4)第四种模式是组合使用构造函数模式和原型模式,这是创建自定义类型的最常见方式。因为构造函数模式和原型模式分开使用都存在一些问题,因此可以组合使用这两种模式,通过构造函数来初始化对象的属性,通过原型对象来实现函数方法的复用。这种方法很好的解决了两种模式单独使用时的缺点,但是有一点不足的就是,因为使用了两种不同的模式,所以对于代码的封装性不够好。
+(5)第五种模式是动态原型模式,这一种模式将原型方法赋值的创建过程移动到了构造函数的内部,通过对属性是否存在的判断,可以实现仅在第一次调用函数时对原型对象赋值一次的效果。这一种方式很好地对上面的混合模式进行了封装。
+(6)第六种模式是寄生构造函数模式,这一种模式和工厂模式的实现基本相同,我对这个模式的理解是,它主要是基于一个已有的类型,在实例化时对实例化的对象进行扩展。这样既不用修改原来的构造函数,也达到了扩展对象的目的。它的一个缺点和工厂模式一样,无法实现对象的识别。
+(1)第一种是以原型链的方式来实现继承,但是这种实现方式存在的缺点是,在包含有引用类型的数据时,会被所有的实例对象所共享,容易造成修改的混乱。还有就是在创建子类型的时候不能向超类型传递参数。
+(2)第二种方式是使用借用构造函数的方式,这种方式是通过在子类型的函数中调用超类型的构造函数来实现的,这一种方法解决了不能向超类型传递参数的缺点,但是它存在的一个问题就是无法实现函数方法的复用,并且超类型原型定义的方法子类型也没有办法访问到。
+(3)第三种方式是组合继承,组合继承是将原型链和借用构造函数组合起来使用的一种方式。通过借用构造函数的方式来实现类型的属性的继承,通过将子类型的原型设置为超类型的实例来实现方法的继承。这种方式解决了上面的两种模式单独使用时的问题,但是由于我们是以超类型的实例来作为子类型的原型,所以调用了两次超类的构造函数,造成了子类型的原型中多了很多不必要的属性。
+(4)第四种方式是原型式继承,原型式继承的主要思路就是基于已有的对象来创建新的对象,实现的原理是,向函数中传入一个对象,然后返回一个以这个对象为原型的对象。这种继承的思路主要不是为了实现创造一种新的类型,只是对某个对象实现一种简单继承,ES5 中定义的 Object.create() 方法就是原型式继承的实现。缺点与原型链方式相同。
+(5)第五种方式是寄生式继承,寄生式继承的思路是创建一个用于封装继承过程的函数,通过传入一个对象,然后复制一个对象的副本,然后对象进行扩展,最后返回这个对象。这个扩展的过程就可以理解是一种继承。这种继承的优点就是对一个简单对象实现继承,如果这个对象不是自定义类型时。缺点是没有办法实现函数的复用。
+(6)第六种方式是寄生式组合继承,组合继承的缺点就是使用超类型的实例做为子类型的原型,导致添加了不必要的原型属性。寄生式组合继承的方式是使用超类型的原型的副本来作为子类型的原型,这样就避免了创建不必要的属性。
+垃圾回收:JavaScript代码运行时,需要分配内存空间来储存变量和值。当变量不在参与运行时,就需要系统收回被占用的内存空间,这就是垃圾回收。
+回收机制:
+浏览器通常使用的垃圾回收方法有两种:标记清除,引用计数。 +1)标记清除
+2)引用计数
+ obj1和obj2通过属性进行相互引用,两个对象的引用次数都是2。当使用循环计数时,由于函数执行完后,两个对象都离开作用域,函数执行结束,obj1和obj2还将会继续存在,因此它们的引用次数永远不会是0,就会引起循环引用。function fun() {
+ let obj1 = {};
+ let obj2 = {};
+ obj1.a = obj2; // obj1 引用 obj2
+ obj2.a = obj1; // obj2 引用 obj1
+}
+这种情况下,就要手动释放变量占用的内存:
+obj1.a = null
+ obj2.a = null
+虽然浏览器可以进行垃圾自动回收,但是当代码比较复杂时,垃圾回收所带来的代价比较大,所以应该尽量减少垃圾回收。
+object进行优化: 对象尽量复用,对于不再使用的对象,就将其设置为null,尽快被回收。以下四种情况会造成内存的泄漏:
+Post 和 Get 是 HTTP 请求的两种方法,其区别如下:
+HTTP Request Header 常见的请求头:
+HTTP Responses Header 常见的响应头:
+常见的 Content-Type 属性值有以下四种:
+(1)application/x-www-form-urlencoded:浏览器的原生 form 表单,如果不设置 enctype 属性,那么最终就会以 application/x-www-form-urlencoded 方式提交数据。该种方式提交的数据放在 body 里面,数据按照 key1=val1&key2=val2 的方式进行编码,key 和 val 都进行了 URL转码。
+(2)multipart/form-data:该种方式也是一个常见的 POST 提交方式,通常表单上传文件时使用该种方式。
+(3)application/json:服务器消息主体是序列化后的 JSON 字符串。
+(4)text/xml:该种方式主要用来提交 XML 格式的数据。
+服务器为了提高网站访问速度,对之前访问的部分页面指定缓存机制,当客户端在此对这些页面进行请求,服务器会根据缓存内容判断页面与之前是否相同,若相同便直接返回304,此时客户端调用缓存内容,不必进行二次下载。
+状态码304不应该认为是一种错误,而是对客户端有缓存情况下服务端的一种响应。
+搜索引擎蜘蛛会更加青睐内容源更新频繁的网站。通过特定时间内对网站抓取返回的状态码来调节对该网站的抓取频次。若网站在一定时间内一直处于304的状态,那么蜘蛛可能会降低对网站的抓取次数。相反,若网站变化的频率非常之快,每次抓取都能获取新内容,那么日积月累,的回访率也会提高。
+产生较多304状态码的原因:
+304状态码出现过多会造成以下问题:
+OPTIONS是除了GET和POST之外的其中一种 HTTP请求方法。
+OPTIONS方法是用于请求获得由Request-URI标识的资源在请求/响应的通信过程中可以使用的功能选项。通过这个方法,客户端可以在采取具体资源请求之前,决定对该资源采取何种必要措施,或者了解服务器的性能。该请求方法的响应不能缓存。
OPTIONS请求方法的主要用途有两个:
+HTTP 1.0和 HTTP 1.1 有以下区别:
+【1】队头堵塞:
+++队头阻塞是由 HTTP 基本的“请求 - 应答”模型所导致的。HTTP 规定报文必须是“一发一收”,这就形成了一个先进先出的“串行”队列。队列里的请求是没有优先级的,只有入队的先后顺序,排在最前面的请求会被最优先处理。如果队首的请求因为处理的太慢耽误了时间,那么队列里后面的所有请求也不得不跟着一起等待,结果就是其他的请求承担了不应有的时间成本,造成了队头堵塞的现象。
+
HTTP和HTTPS协议的主要区别如下:
+实际上HTTP协议规范并没有对get方法请求的url长度进行限制,这个限制是特定的浏览器及服务器对它的限制。 +IE对URL长度的限制是2083字节(2K+35)。由于IE浏览器对URL长度的允许值是最小的,所以开发过程中,只要URL不超过2083字节,那么在所有浏览器中工作都不会有问题。
+GET的长度值 = URL(2083)- (你的Domain+Path)-2(2是get请求中?=两个字符的长度)
+下面看一下主流浏览器对get方法中url的长度限制范围:
+主流的服务器对get方法中url的长度限制范围:
+根据上面的数据,可以知道,get方法中的URL长度最长不超过2083个字符,这样所有的浏览器和服务器都可能正常工作。
+(1)解析URL: 首先会对 URL 进行解析,分析所需要使用的传输协议和请求的资源的路径。如果输入的 URL 中的协议或者主机名不合法,将会把地址栏中输入的内容传递给搜索引擎。如果没有问题,浏览器会检查 URL 中是否出现了非法字符,如果存在非法字符,则对非法字符进行转义后再进行下一过程。
+(2)缓存判断: 浏览器会判断所请求的资源是否在缓存里,如果请求的资源在缓存里并且没有失效,那么就直接使用,否则向服务器发起新的请求。
+(3)DNS解析: 下一步首先需要获取的是输入的 URL 中的域名的 IP 地址,首先会判断本地是否有该域名的 IP 地址的缓存,如果有则使用,如果没有则向本地 DNS 服务器发起请求。本地 DNS 服务器也会先检查是否存在缓存,如果没有就会先向根域名服务器发起请求,获得负责的顶级域名服务器的地址后,再向顶级域名服务器请求,然后获得负责的权威域名服务器的地址后,再向权威域名服务器发起请求,最终获得域名的 IP 地址后,本地 DNS 服务器再将这个 IP 地址返回给请求的用户。用户向本地 DNS 服务器发起请求属于递归请求,本地 DNS 服务器向各级域名服务器发起请求属于迭代请求。
+(4)获取MAC地址: 当浏览器得到 IP 地址后,数据传输还需要知道目的主机 MAC 地址,因为应用层下发数据给传输层,TCP 协议会指定源端口号和目的端口号,然后下发给网络层。网络层会将本机地址作为源地址,获取的 IP 地址作为目的地址。然后将下发给数据链路层,数据链路层的发送需要加入通信双方的 MAC 地址,本机的 MAC 地址作为源 MAC 地址,目的 MAC 地址需要分情况处理。通过将 IP 地址与本机的子网掩码相与,可以判断是否与请求主机在同一个子网里,如果在同一个子网里,可以使用 APR 协议获取到目的主机的 MAC 地址,如果不在一个子网里,那么请求应该转发给网关,由它代为转发,此时同样可以通过 ARP 协议来获取网关的 MAC 地址,此时目的主机的 MAC 地址应该为网关的地址。
+(5)TCP三次握手: 下面是 TCP 建立连接的三次握手的过程,首先客户端向服务器发送一个 SYN 连接请求报文段和一个随机序号,服务端接收到请求后向服务器端发送一个 SYN ACK报文段,确认连接请求,并且也向客户端发送一个随机序号。客户端接收服务器的确认应答后,进入连接建立的状态,同时向服务器也发送一个ACK 确认报文段,服务器端接收到确认后,也进入连接建立状态,此时双方的连接就建立起来了。
+(6)HTTPS握手: 如果使用的是 HTTPS 协议,在通信前还存在 TLS 的一个四次握手的过程。首先由客户端向服务器端发送使用的协议的版本号、一个随机数和可以使用的加密方法。服务器端收到后,确认加密的方法,也向客户端发送一个随机数和自己的数字证书。客户端收到后,首先检查数字证书是否有效,如果有效,则再生成一个随机数,并使用证书中的公钥对随机数加密,然后发送给服务器端,并且还会提供一个前面所有内容的 hash 值供服务器端检验。服务器端接收后,使用自己的私钥对数据解密,同时向客户端发送一个前面所有内容的 hash 值供客户端检验。这个时候双方都有了三个随机数,按照之前所约定的加密方法,使用这三个随机数生成一把秘钥,以后双方通信前,就使用这个秘钥对数据进行加密后再传输。
+(7)返回数据: 当页面请求发送到服务器端后,服务器端会返回一个 html 文件作为响应,浏览器接收到响应后,开始对 html 文件进行解析,开始页面的渲染过程。
+(8)页面渲染: 浏览器首先会根据 html 文件构建 DOM 树,根据解析到的 css 文件构建 CSSOM 树,如果遇到 script 标签,则判端是否含有 defer 或者 async 属性,要不然 script 的加载和执行会造成页面的渲染的阻塞。当 DOM 树和 CSSOM 树建立好后,根据它们来构建渲染树。渲染树构建好后,会根据渲染树来进行布局。布局完成后,最后使用浏览器的 UI 接口对页面进行绘制。这个时候整个页面就显示出来了。
+(9)TCP四次挥手: 最后一步是 TCP 断开连接的四次挥手过程。若客户端认为数据发送完成,则它需要向服务端发送连接释放请求。服务端收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,此时表明客户端到服务端的连接已经释放,不再接收客户端发的数据了。但是因为 TCP 连接是双向的,所以服务端仍旧可以发送数据给客户端。服务端如果此时还有没发完的数据会继续发送,完毕后会向客户端发送连接释放请求,然后服务端便进入 LAST-ACK 状态。客户端收到释放请求后,向服务端发送确认应答,此时客户端进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有服务端的重发请求的话,就进入 CLOSED 状态。当服务端收到确认应答后,也便进入 CLOSED 状态。
+HTTP1.0 中默认是在每次请求/应答,客户端和服务器都要新建一个连接,完成之后立即断开连接,这就是短连接。当使用Keep-Alive模式时,Keep-Alive功能使客户端到服务器端的连接持续有效,当出现对服务器的后继请求时,Keep-Alive功能避免了建立或者重新建立连接,这就是长连接。其使用方法如下:
+Connection: keep-alive字段。若想断开keep-alive连接,需发送Connection:close字段;Connection:close首部字段。Keep-Alive的建立过程:
+服务端自动断开过程(也就是没有keep-alive):
+客户端请求断开连接过程:
+开启Keep-Alive的优点:
+开启Keep-Alive的缺点:
+HTTP 1下,浏览器对一个域名下最大TCP连接数为6,所以会请求多次。可以用多域名部署解决。这样可以提高同时请求的数目,加快页面图片的获取速度。HTTP 2下,可以一瞬间加载出来很多资源,因为,HTTP2支持多路复用,可以在一个TCP连接中发送多个HTTP请求。HTTP2的头部压缩是HPACK算法。在客户端和服务器两端建立“字典”,用索引号表示重复的字符串,采用哈夫曼编码来压缩整数和字符串,可以达到50%~90%的高压缩率。
+具体来说:
+例如下图中的两个请求, 请求一发送了所有的头部字段,第二个请求则只需要发送差异数据,这样可以减少冗余数据,降低开销。
+
请求报⽂有4部分组成:
+
+其中:
+(1)请求⾏包括:请求⽅法字段、URL字段、HTTP协议版本字段。它们⽤空格分隔。例如,GET /index.html HTTP/1.1。
+(2)请求头部:请求头部由关键字/值对组成,每⾏⼀对,关键字和值⽤英⽂冒号“:”分隔
(3)请求体: post put等请求携带的数据
+
请求报⽂有4部分组成:
+HTTP 是超文本传输协议,它定义了客户端和服务器之间交换报文的格式和方式,默认使用 80 端口。它使用 TCP 作为传输层协议,保证了数据传输的可靠性。
+HTTP协议具有以下优点:
+HTTP协议具有以下缺点:
+(1)通信使用明文(不加密),内容可能会被窃听; +(2)不验证通信方的身份,因此有可能遭遇伪装; +(3)无法证明报文的完整性,所以有可能已遭篡改;
+HTTP/3基于UDP协议实现了类似于TCP的多路复用数据流、传输可靠性等功能,这套功能被称为QUIC协议。
+
HTTP 协议是基于 TCP/IP,并且使用了请求-应答的通信模式,所以性能的关键就在这两点里。
+HTTP协议有两种连接模式,一种是持续连接,一种非持续连接。 +(1)非持续连接指的是服务器必须为每一个请求的对象建立和维护一个全新的连接。 +(2)持续连接下,TCP 连接默认不关闭,可以被多个请求复用。采用持续连接的好处是可以避免每次建立 TCP 连接三次握手时所花费的时间。
+对于不同版本的采用不同的连接方式:
+HTTP/1.1 采用了长连接的方式,这使得管道(pipeline)网络传输成为了可能。
+管道(pipeline)网络传输是指:可以在同一个 TCP 连接里面,客户端可以发起多个请求,只要第一个请求发出去了,不必等其回来,就可以发第二个请求出去,可以减少整体的响应时间。但是服务器还是按照顺序回应请求。如果前面的回应特别慢,后面就会有许多请求排队等着。这称为队头堵塞。
+HTTP 传输的报文必须是一发一收,但是,里面的任务被放在一个任务队列中串行执行,一旦队首的请求处理太慢,就会阻塞后面请求的处理。这就是HTTP队头阻塞问题。
+队头阻塞的解决方案: +(1)并发连接:对于一个域名允许分配多个长连接,那么相当于增加了任务队列,不至于一个队伍的任务阻塞其它所有任务。 +(2)域名分片:将域名分出很多二级域名,它们都指向同样的一台服务器,能够并发的长连接数变多,解决了队头阻塞的问题。
+以下面的URL为例:www.aspxfans.com:8080/news/index.…
+从上面的URL可以看出,一个完整的URL包括以下几部分:
+强缓存:
+协商缓存:
+超文本传输安全协议(Hypertext Transfer Protocol Secure,简称:HTTPS)是一种通过计算机网络进行安全通信的传输协议。HTTPS经由HTTP进行通信,利用SSL/TLS来加密数据包。HTTPS的主要目的是提供对网站服务器的身份认证,保护交换数据的隐私与完整性。
+
+HTTP协议采用明文传输信息,存在信息窃听、信息篡改和信息劫持的风险,而协议TLS/SSL具有身份验证、信息加密和完整性校验的功能,可以避免此类问题发生。
安全层的主要职责就是对发起的HTTP请求的数据进行加密操作 和 对接收到的HTTP的内容进行解密操作。
+TLS/SSL全称安全传输层协议(Transport Layer Security), 是介于TCP和HTTP之间的一层安全协议,不影响原有的TCP协议和HTTP协议,所以使用HTTPS基本上不需要对HTTP页面进行太多的改造。
+TLS/SSL的功能实现主要依赖三类基本算法:散列函数hash、对称加密、非对称加密。这三类算法的作用如下:
+常见的散列函数有MD5、SHA1、SHA256。该函数的特点是单向不可逆,对输入数据非常敏感,输出的长度固定,任何数据的修改都会改变散列函数的结果,可以用于防止信息篡改并验证数据的完整性。
+特点: 在信息传输过程中,散列函数不能三都实现信息防篡改,由于传输是明文传输,中间人可以修改信息后重新计算信息的摘要,所以需要对传输的信息和信息摘要进行加密。
+对称加密的方法是,双方使用同一个秘钥对数据进行加密和解密。但是对称加密的存在一个问题,就是如何保证秘钥传输的安全性,因为秘钥还是会通过网络传输的,一旦秘钥被其他人获取到,那么整个加密过程就毫无作用了。 这就要用到非对称加密的方法。
+常见的对称加密算法有AES-CBC、DES、3DES、AES-GCM等。相同的秘钥可以用于信息的加密和解密。掌握秘钥才能获取信息,防止信息窃听,其通讯方式是一对一。
+特点: 对称加密的优势就是信息传输使用一对一,需要共享相同的密码,密码的安全是保证信息安全的基础,服务器和N个客户端通信,需要维持N个密码记录且不能修改密码。
+非对称加密的方法是,我们拥有两个秘钥,一个是公钥,一个是私钥。公钥是公开的,私钥是保密的。用私钥加密的数据,只有对应的公钥才能解密,用公钥加密的数据,只有对应的私钥才能解密。我们可以将公钥公布出去,任何想和我们通信的客户, 都可以使用我们提供的公钥对数据进行加密,这样我们就可以使用私钥进行解密,这样就能保证数据的安全了。但是非对称加密有一个缺点就是加密的过程很慢,因此如果每次通信都使用非对称加密的方式的话,反而会造成等待时间过长的问题。
+常见的非对称加密算法有RSA、ECC、DH等。秘钥成对出现,一般称为公钥(公开)和私钥(保密)。公钥加密的信息只有私钥可以解开,私钥加密的信息只能公钥解开,因此掌握公钥的不同客户端之间不能相互解密信息,只能和服务器进行加密通信,服务器可以实现一对多的的通信,客户端也可以用来验证掌握私钥的服务器的身份。
+特点: 非对称加密的特点就是信息一对多,服务器只需要维持一个私钥就可以和多个客户端进行通信,但服务器发出的信息能够被所有的客户端解密,且该算法的计算复杂,加密的速度慢。
+综合上述算法特点,TLS/SSL的工作方式就是客户端使用非对称加密与服务器进行通信,实现身份的验证并协商对称加密使用的秘钥。对称加密算法采用协商秘钥对信息以及信息摘要进行加密通信,不同节点之间采用的对称秘钥不同,从而保证信息只能通信双方获取。这样就解决了两个方法各自存在的问题。
+现在的方法也不一定是安全的,因为没有办法确定得到的公钥就一定是安全的公钥。可能存在一个中间人,截取了对方发给我们的公钥,然后将他自己的公钥发送给我们,当我们使用他的公钥加密后发送的信息,就可以被他用自己的私钥解密。然后他伪装成我们以同样的方法向对方发送信息,这样我们的信息就被窃取了,然而自己还不知道。为了解决这样的问题,可以使用数字证书。
+首先使用一种 Hash 算法来对公钥和其他信息进行加密,生成一个信息摘要,然后让有公信力的认证中心(简称 CA )用它的私钥对消息摘要加密,形成签名。最后将原始的信息和签名合在一起,称为数字证书。当接收方收到数字证书的时候,先根据原始信息使用同样的 Hash 算法生成一个摘要,然后使用公证处的公钥来对数字证书中的摘要进行解密,最后将解密的摘要和生成的摘要进行对比,就能发现得到的信息是否被更改了。
+这个方法最要的是认证中心的可靠性,一般浏览器里会内置一些顶层的认证中心的证书,相当于我们自动信任了他们,只有这样才能保证数据的安全。
+
HTTPS的通信过程如下:
+HTTPS的优点如下:
+HTTPS的缺点如下:
+先理解两个概念:
+⾮对称加密虽然安全性更⾼,但是带来的问题就是速度很慢,影响性能。
+解决⽅案:
+结合两种加密⽅式,将对称加密的密钥使⽤⾮对称加密的公钥进⾏加密,然后发送出去,接收⽅使⽤私钥进⾏解密得到对称加密的密钥,然后双⽅可以使⽤对称加密来进⾏沟通。
+此时⼜带来⼀个问题,中间⼈问题: +如果此时在客户端和服务器之间存在⼀个中间⼈,这个中间⼈只需要把原本双⽅通信互发的公钥,换成⾃⼰的公钥,这样中间⼈就可以轻松解密通信双⽅所发送的所有数据。
+所以这个时候需要⼀个安全的第三⽅颁发证书(CA),证明身份的身份,防⽌被中间⼈攻击。 证书中包括:签发者、证书⽤途、使⽤者公钥、使⽤者私钥、使⽤者的HASH算法、证书到期时间等。
+但是问题来了,如果中间⼈篡改了证书,那么身份证明是不是就⽆效了?这个证明就⽩买了,这个时候需要⼀个新的技术,数字签名。
+数字签名就是⽤CA⾃带的HASH算法对证书的内容进⾏HASH得到⼀个摘要,再⽤CA的私钥加密,最终组成数字签名。当别⼈把他的证书发过来的时候,我再⽤同样的Hash算法,再次⽣成消息摘要,然后⽤CA的公钥对数字签名解密,得到CA创建的消息摘要,两者⼀⽐,就知道中间有没有被⼈篡改了。这个时候就能最⼤程度保证通信的安全了。
+状态码的类别:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +| 类别 | 原因 | 描述 |
|---|---|---|
| 1xx | Informational(信息性状态码) | 接受的请求正在处理 |
| 2xx | Success(成功状态码) | 请求正常处理完毕 |
| 3xx | Redirection(重定向状态码) | 需要进行附加操作一完成请求 |
| 4xx | Client Error (客户端错误状态码) | 服务器无法处理请求 |
| 5xx | Server Error(服务器错误状态码) | 服务器处理请求出错 |
状态码2XX表示请求被正常处理了。
+200 OK表示客户端发来的请求被服务器端正常处理了。
+该状态码表示客户端发送的请求已经在服务器端正常处理了,但是没有返回的内容,响应报文中不包含实体的主体部分。一般在只需要从客户端往服务器端发送信息,而服务器端不需要往客户端发送内容时使用。
+该状态码表示客户端进行了范围请求,而服务器端执行了这部分的 GET 请求。响应报文中包含由 Content-Range 指定范围的实体内容。
+3XX 响应结果表明浏览器需要执行某些特殊的处理以正确处理请求。
+永久重定向。 +该状态码表示请求的资源已经被分配了新的 URI,以后应使用资源指定的 URI。新的 URI 会在 HTTP 响应头中的 Location 首部字段指定。若用户已经把原来的URI保存为书签,此时会按照 Location 中新的URI重新保存该书签。同时,搜索引擎在抓取新内容的同时也将旧的网址替换为重定向之后的网址。
+使用场景:
+临时重定向。 +该状态码表示请求的资源被分配到了新的 URI,希望用户(本次)能使用新的 URI 访问资源。和 301 Moved Permanently 状态码相似,但是 302 代表的资源不是被永久重定向,只是临时性质的。也就是说已移动的资源对应的 URI 将来还有可能发生改变。若用户把 URI 保存成书签,但不会像 301 状态码出现时那样去更新书签,而是仍旧保留返回 302 状态码的页面对应的 URI。同时,搜索引擎会抓取新的内容而保留旧的网址。因为服务器返回302代码,搜索引擎认为新的网址只是暂时的。
+使用场景:
+该状态码表示由于请求对应的资源存在着另一个 URI,应使用 GET 方法定向获取请求的资源。 +303 状态码和 302 Found 状态码有着相似的功能,但是 303 状态码明确表示客户端应当采用 GET 方法获取资源。
+303 状态码通常作为 PUT 或 POST 操作的返回结果,它表示重定向链接指向的不是新上传的资源,而是另外一个页面,比如消息确认页面或上传进度页面。而请求重定向页面的方法要总是使用 GET。
+注意:
+浏览器缓存相关。 +该状态码表示客户端发送附带条件的请求时,服务器端允许请求访问资源,但未满足条件的情况。304 状态码返回时,不包含任何响应的主体部分。304 虽然被划分在 3XX 类别中,但是和重定向没有关系。
+带条件的请求(Http 条件请求):使用 Get方法 请求,请求报文中包含(if-match、if-none-match、if-modified-since、if-unmodified-since、if-range)中任意首部。
状态码304并不是一种错误,而是告诉客户端有缓存,直接使用缓存中的数据。返回页面的只有头部信息,是没有内容部分的,这样在一定程度上提高了网页的性能。
+307表示临时重定向。 该状态码与 302 Found 有着相同含义,尽管 302 标准禁止 POST 变成 GET,但是实际使用时还是这样做了。
+307 会遵守浏览器标准,不会从 POST 变成 GET。但是对于处理请求的行为时,不同浏览器还是会出现不同的情况。规范要求浏览器继续向 Location 的地址 POST 内容。规范要求浏览器继续向 Location 的地址 POST 内容。
+4XX 的响应结果表明客户端是发生错误的原因所在。
+该状态码表示请求报文中存在语法错误。当错误发生时,需修改请求的内容后再次发送请求。另外,浏览器会像 200 OK 一样对待该状态码。
+该状态码表示发送的请求需要有通过 HTTP 认证(BASIC 认证、DIGEST 认证)的认证信息。若之前已进行过一次请求,则表示用户认证失败
+返回含有 401 的响应必须包含一个适用于被请求资源的 WWW-Authenticate 首部用以质询(challenge)用户信息。当浏览器初次接收到 401 响应,会弹出认证用的对话窗口。
+以下情况会出现401:
+该状态码表明请求资源的访问被服务器拒绝了,服务器端没有必要给出详细理由,但是可以在响应报文实体的主体中进行说明。进入该状态后,不能再继续进行验证。该访问是永久禁止的,并且与应用逻辑密切相关。
+IIS 定义了许多不同的 403 错误,它们指明更为具体的错误原因:
+该状态码表明服务器上无法找到请求的资源。除此之外,也可以在服务器端拒绝请求且不想说明理由时使用。 +以下情况会出现404:
+该状态码表示客户端请求的方法虽然能被服务器识别,但是服务器禁止使用该方法。GET 和 HEAD 方法,服务器应该总是允许客户端进行访问。客户端可以通过 OPTIONS 方法(预检)来查看服务器允许的访问方法, 如下
+Access-Control-Allow-Methods: GET,HEAD,PUT,PATCH,POST,DELETE
+5XX 的响应结果表明服务器本身发生错误.
+该状态码表明服务器端在执行请求时发生了错误。也有可能是 Web 应用存在的 bug 或某些临时的故障。
+该状态码表明扮演网关或代理角色的服务器,从上游服务器中接收到的响应是无效的。注意,502 错误通常不是客户端能够修复的,而是需要由途经的 Web 服务器或者代理服务器对其进行修复。以下情况会出现502:
+该状态码表明服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。如果事先得知解除以上状况需要的时间,最好写入 RetryAfter 首部字段再返回给客户端。
+使用场景:
+该状态码表示网关或者代理的服务器无法在规定的时间内获得想要的响应。他是HTTP 1.1中新加入的。
+使用场景:代码执行时间超时,或者发生了死循环。
+(1)2XX 成功
+(2)3XX 重定向
+(3)4XX 客户端错误
+(4)5XX 服务器错误
+302是http1.0的协议状态码,在http1.1版本的时候为了细化302状态码⼜出来了两个303和307。 303明确表示客户端应当采⽤get⽅法获取资源,他会把POST请求变为GET请求进⾏重定向。 307会遵照浏览器标准,不会从post变为get。
+概念: DNS 是域名系统 (Domain Name System) 的缩写,提供的是一种主机名到 IP 地址的转换服务,就是我们常说的域名系统。它是一个由分层的 DNS 服务器组成的分布式数据库,是定义了主机如何查询这个分布式数据库的方式的应用层协议。能够使人更方便的访问互联网,而不用去记住能够被机器直接读取的IP数串。
+作用: 将域名解析为IP地址,客户端向DNS服务器(DNS服务器有自己的IP地址)发送域名查询请求,DNS服务器告知客户机Web服务器的 IP 地址。
+DNS占用53号端口,同时使用TCP和UDP协议。 +(1)在区域传输的时候使用TCP协议
+(2)在域名解析的时候使用UDP协议
+DNS服务器解析域名的过程:
+比如要查询 www.baidu.com 的 IP 地址,首先会在浏览器的缓存中查找是否有该域名的缓存,如果不存在就将请求发送到本地的 DNS 服务器中,本地DNS服务器会判断是否存在该域名的缓存,如果不存在,则向根域名服务器发送一个请求,根域名服务器返回负责 .com 的顶级域名服务器的 IP 地址的列表。然后本地 DNS 服务器再向其中一个负责 .com 的顶级域名服务器发送一个请求,负责 .com 的顶级域名服务器返回负责 .baidu 的权威域名服务器的 IP 地址列表。然后本地 DNS 服务器再向其中一个权威域名服务器发送一个请求,最后权威域名服务器返回一个对应的主机名的 IP 地址列表。
+实际上,DNS解析是一个包含迭代查询和递归查询的过程。
+一般我们向本地 DNS 服务器发送请求的方式就是递归查询,因为我们只需要发出一次请求,然后本地 DNS 服务器返回给我 们最终的请求结果。而本地 DNS 服务器向其他域名服务器请求的过程是迭代查询的过程,因为每一次域名服务器只返回单次 查询的结果,下一级的查询由本地 DNS 服务器自己进行。
+DNS 服务器中以资源记录的形式存储信息,每一个 DNS 响应报文一般包含多条资源记录。一条资源记录的具体的格式为
+(Name,Value,Type,TTL)
+其中 TTL 是资源记录的生存时间,它定义了资源记录能够被其他的 DNS 服务器缓存多长时间。
+常用的一共有四种 Type 的值,分别是 A、NS、CNAME 和 MX ,不同 Type 的值,对应资源记录代表的意义不同:
+ISO为了更好的使网络应用更为普及,推出了OSI参考模型。
+
OSI参考模型中最靠近用户的一层,是为计算机用户提供应用接口,也为用户直接提供各种网络服务。我们常见应用层的网络服务协议有:HTTP,HTTPS,FTP,POP3、SMTP等。
http(hyper text transfer protocol)(超文本传输协议)或者https.在后端设计数据接口时,我们常常使用到这个协议。FTP是文件传输协议,在开发过程中,个人并没有涉及到,但是我想,在一些资源网站,比如百度网盘``迅雷应该是基于此协议的。SMTP是simple mail transfer protocol(简单邮件传输协议)。在一个项目中,在用户邮箱验证码登录的功能时,使用到了这个协议。表示层提供各种用于应用层数据的编码和转换功能,确保一个系统的应用层发送的数据能被另一个系统的应用层识别。如果必要,该层可提供一种标准表示形式,用于将计算机内部的多种数据格式转换成通信中采用的标准表示形式。数据压缩和加密也是表示层可提供的转换功能之一。
+在项目开发中,为了方便数据传输,可以使用base64对数据进行编解码。如果按功能来划分,base64应该是工作在表示层。
会话层就是负责建立、管理和终止表示层实体之间的通信会话。该层的通信由不同设备中的应用程序之间的服务请求和响应组成。
+传输层建立了主机端到端的链接,传输层的作用是为上层协议提供端到端的可靠和透明的数据传输服务,包括处理差错控制和流量控制等问题。该层向高层屏蔽了下层数据通信的细节,使高层用户看到的只是在两个传输实体间的一条主机到主机的、可由用户控制和设定的、可靠的数据通路。我们通常说的,TCP UDP就是在这一层。端口号既是这里的“端”。
本层通过IP寻址来建立两个节点之间的连接,为源端的运输层送来的分组,选择合适的路由和交换节点,正确无误地按照地址传送给目的端的运输层。就是通常说的IP层。这一层就是我们经常说的IP协议层。IP协议是Internet的基础。我们可以这样理解,网络层规定了数据包的传输路线,而传输层则规定了数据包的传输方式。
将比特组合成字节,再将字节组合成帧,使用链路层地址 (以太网使用MAC地址)来访问介质,并进行差错检测。 +网络层与数据链路层的对比,通过上面的描述,我们或许可以这样理解,网络层是规划了数据包的传输路线,而数据链路层就是传输路线。不过,在数据链路层上还增加了差错控制的功能。
+实际最终信号的传输是通过物理层实现的。通过物理介质传输比特流。规定了电平、速度和电缆针脚。常用设备有(各种物理设备)集线器、中继器、调制解调器、网线、双绞线、同轴电缆。这些都是物理层的传输介质。
+OSI七层模型通信特点:对等通信 +对等通信,为了使数据分组从源传送到目的地,源端OSI模型的每一层都必须与目的端的对等层进行通信,这种通信方式称为对等层通信。在每一层通信过程中,使用本层自己协议进行通信。
+TCP/IP五层协议和OSI的七层协议对应关系如下:
+
从上图中可以看出,TCP/IP模型比OSI模型更加简洁,它把应用层/表示层/会话层全部整合为了应用层。
在每一层都工作着不同的设备,比如我们常用的交换机就工作在数据链路层的,一般的路由器是工作在网络层的。
+
+在每一层实现的协议也各不同,即每一层的服务也不同,下图列出了每层主要的传输协议:
+
同样,TCP/IP五层协议的通信方式也是对等通信:
+
TCP 和 UDP都是传输层协议,他们都属于TCP/IP协议族:
+(1)UDP
+UDP的全称是用户数据报协议,在网络中它与TCP协议一样用于处理数据包,是一种无连接的协议。在OSI模型中,在传输层,处于IP协议的上一层。UDP有不提供数据包分组、组装和不能对数据包进行排序的缺点,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。
+它的特点如下:
+1)面向无连接
+首先 UDP 是不需要和 TCP一样在发送数据前进行三次握手建立连接的,想发数据就可以开始发送了。并且也只是数据报文的搬运工,不会对数据报文进行任何拆分和拼接操作。
+具体来说就是:
+2)有单播,多播,广播的功能
+UDP 不止支持一对一的传输方式,同样支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能。
+3)面向报文
+发送方的UDP对应用程序交下来的报文,在添加首部后就向下交付IP层。UDP对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。因此,应用程序必须选择合适大小的报文
+4)不可靠性
+首先不可靠性体现在无连接上,通信都不需要建立连接,想发就发,这样的情况肯定不可靠。
+并且收到什么数据就传递什么数据,并且也不会备份数据,发送数据也不会关心对方是否已经正确接收到数据了。
+再者网络环境时好时坏,但是 UDP 因为没有拥塞控制,一直会以恒定的速度发送数据。即使网络条件不好,也不会对发送速率进行调整。这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景(比如电话会议)就需要使用 UDP 而不是 TCP。
+5)头部开销小,传输数据报文时是很高效的。
+
UDP 头部包含了以下几个数据:
+因此 UDP 的头部开销小,只有8字节,相比 TCP 的至少20字节要少得多,在传输数据报文时是很高效的。
+(2)TCP +TCP的全称是传输控制协议是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP 是面向连接的、可靠的流协议(流就是指不间断的数据结构)。
+它有以下几个特点:
+1)面向连接
+面向连接,是指发送数据之前必须在两端建立连接。建立连接的方法是“三次握手”,这样能建立可靠的连接。建立连接,是为数据的可靠传输打下了基础。
+2)仅支持单播传输
+每条TCP传输连接只能有两个端点,只能进行点对点的数据传输,不支持多播和广播传输方式。
+3)面向字节流
+TCP不像UDP一样那样一个个报文独立地传输,而是在不保留报文边界的情况下以字节流方式进行传输。
+4)可靠传输
+对于可靠传输,判断丢包、误码靠的是TCP的段编号以及确认号。TCP为了保证报文传输的可靠,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的字节发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据(假设丢失了)将会被重传。
+5)提供拥塞控制
+当网络出现拥塞的时候,TCP能够减小向网络注入数据的速率和数量,缓解拥塞。
+6)提供全双工通信
+TCP允许通信双方的应用程序在任何时候都能发送数据,因为TCP连接的两端都设有缓存,用来临时存放双向通信的数据。当然,TCP可以立即发送一个数据段,也可以缓存一段时间以便一次发送更多的数据段(最大的数据段大小取决于MSS)
+| UDP | TCP | |
|---|---|---|
| 是否连接 | 无连接 | 面向连接 |
| 是否可靠 | 不可靠传输,不使用流量控制和拥塞控制 | 可靠传输(数据顺序和正确性),使用流量控制和拥塞控制 |
| 连接对象个数 | 支持一对一,一对多,多对一和多对多交互通信 | 只能是一对一通信 |
| 传输方式 | 面向报文 | 面向字节流 |
| 首部开销 | 首部开销小,仅8字节 | 首部最小20字节,最大60字节 |
| 适用场景 | 适用于实时应用,例如视频会议、直播 | 适用于要求可靠传输的应用,例如文件传输 |
UDP在传输数据之前不需要先建立连接,远地主机的运输层在接收到UDP报文后,不需要确认,提供不可靠交付。总结就以下四点:
+由于TCP的下层网络(网络层)可能出现丢失、重复或失序的情况,TCP协议提供可靠数据传输服务。为保证数据传输的正确性,TCP会重传其认为已丢失(包括报文中的比特错误)的包。TCP使用两套独立的机制来完成重传,一是基于时间,二是基于确认信息。
+TCP在发送一个数据之后,就开启一个定时器,若是在这个时间内没有收到发送数据的ACK确认报文,则对该报文进行重传,在达到一定次数还没有成功时放弃并发送一个复位信号。
+TCP的拥塞控制机制主要是以下四种机制:
+(1)慢启动(慢开始)
+(2)拥塞避免
+(3)快速重传
+(4)快速恢复
+一般来说,流量控制就是为了让发送方发送数据的速度不要太快,要让接收方来得及接收。TCP采用大小可变的滑动窗口进行流量控制,窗口大小的单位是字节。这里说的窗口大小其实就是每次传输的数据大小。
+TCP 的可靠传输机制是基于连续 ARQ 协议和滑动窗口协议的。
+TCP 协议在发送方维持了一个发送窗口,发送窗口以前的报文段是已经发送并确认了的报文段,发送窗口中包含了已经发送但 未确认的报文段和允许发送但还未发送的报文段,发送窗口以后的报文段是缓存中还不允许发送的报文段。当发送方向接收方发 送报文时,会依次发送窗口内的所有报文段,并且设置一个定时器,这个定时器可以理解为是最早发送但未收到确认的报文段。 如果在定时器的时间内收到某一个报文段的确认回答,则滑动窗口,将窗口的首部向后滑动到确认报文段的后一个位置,此时如 果还有已发送但没有确认的报文段,则重新设置定时器,如果没有了则关闭定时器。如果定时器超时,则重新发送所有已经发送 但还未收到确认的报文段,并将超时的间隔设置为以前的两倍。当发送方收到接收方的三个冗余的确认应答后,这是一种指示, 说明该报文段以后的报文段很有可能发生丢失了,那么发送方会启用快速重传的机制,就是当前定时器结束前,发送所有的已发 送但确认的报文段。
+接收方使用的是累计确认的机制,对于所有按序到达的报文段,接收方返回一个报文段的肯定回答。如果收到了一个乱序的报文 段,那么接方会直接丢弃,并返回一个最近的按序到达的报文段的肯定回答。使用累计确认保证了返回的确认号之前的报文段都 已经按序到达了,所以发送窗口可以移动到已确认报文段的后面。
+发送窗口的大小是变化的,它是由接收窗口剩余大小和网络中拥塞程度来决定的,TCP 就是通过控制发送窗口的长度来控制报文 段的发送速率。
+但是 TCP 协议并不完全和滑动窗口协议相同,因为许多的 TCP 实现会将失序的报文段给缓存起来,并且发生重传时,只会重 传一个报文段,因此 TCP 协议的可靠传输机制更像是窗口滑动协议和选择重传协议的一个混合体。
+
+三次握手(Three-way Handshake)其实就是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。进行三次握手的主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备。实质上其实就是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号,交换TCP窗口大小信息。
刚开始客户端处于 Closed 的状态,服务端处于 Listen 状态。
+++首部的同步位SYN=1,初始序号seq=x,SYN=1的报文段不能携带数据,但要消耗掉一个序号。
+
++在确认报文段中SYN=1,ACK=1,确认号ack=x+1,初始序号seq=y
+
++确认报文段ACK=1,确认号ack=y+1,序号seq=x+1(初始为seq=x,第二个报文段所以要+1),ACK报文段可以携带数据,不携带数据则不消耗序号。
+
那为什么要三次握手呢?两次不行吗?
+++如客户端发出连接请求,但因连接请求报文丢失而未收到确认,于是客户端再重传一次连接请求。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接,客户端共发出了两个连接请求报文段,其中第一个丢失,第二个到达了服务端,但是第一个丢失的报文段只是在某些网络结点长时间滞留了,延误到连接释放以后的某个时间才到达服务端,此时服务端误认为客户端又发出一次新的连接请求,于是就向客户端发出确认报文段,同意建立连接,不采用三次握手,只要服务端发出确认,就建立新的连接了,此时客户端忽略服务端发来的确认,也不发送数据,则服务端一致等待客户端发送数据,浪费资源。
+
简单来说就是以下三步:
+TCP 三次握手的建立连接的过程就是相互确认初始序号的过程,告诉对方,什么样序号的报文段能够被正确接收。 第三次握手的作用是客户端对服务器端的初始序号的确认。如果只使用两次握手,那么服务器就没有办法知道自己的序号是否 已被确认。同时这样也是为了防止失效的请求报文段被服务器接收,而出现错误的情况。
+
+刚开始双方都处于 ESTABLISHED 状态,假如是客户端先发起关闭请求。四次挥手的过程如下:
++即发出连接释放报文段(FIN=1,序号seq=u),并停止再发送数据,主动关闭TCP连接,进入FIN_WAIT1(终止等待1)状态,等待服务端的确认。
+
++即服务端收到连接释放报文段后即发出确认报文段(ACK=1,确认号ack=u+1,序号seq=v),服务端进入CLOSE_WAIT(关闭等待)状态,此时的TCP处于半关闭状态,客户端到服务端的连接释放。客户端收到服务端的确认后,进入FIN_WAIT2(终止等待2)状态,等待服务端发出的连接释放报文段。
+
++即服务端没有要向客户端发出的数据,服务端发出连接释放报文段(FIN=1,ACK=1,序号seq=w,确认号ack=u+1),服务端进入LAST_ACK(最后确认)状态,等待客户端的确认。
+
++即客户端收到服务端的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),客户端进入TIME_WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL后,客户端才进入CLOSED状态。
+
那为什么需要四次挥手呢?
+++因为当服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当服务端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉客户端,“你发的FIN报文我收到了”。只有等到我服务端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送,故需要四次挥手。
+
简单来说就是以下四步:
+TCP 使用四次挥手的原因是因为 TCP 的连接是全双工的,所以需要双方分别释放到对方的连接,单独一方的连接释放,只代 表不能再向对方发送数据,连接处于的是半释放的状态。
+最后一次挥手中,客户端会等待一段时间再关闭的原因,是为了防止发送给服务器的确认报文段丢失或者出错,从而导致服务器 端不能正常关闭。
+默认情况下, TCP 连接会启⽤延迟传送算法 (Nagle 算法), 在数据发送之前缓存他们. 如果短时间有多个数据发送, 会缓冲到⼀起作⼀次发送 (缓冲⼤⼩⻅ socket.bufferSize ), 这样可以减少 IO 消耗提⾼性能.
+如果是传输⽂件的话, 那么根本不⽤处理粘包的问题, 来⼀个包拼⼀个包就好了。但是如果是多条消息, 或者是别的⽤途的数据那么就需要处理粘包.
+下面看⼀个例⼦, 连续调⽤两次 send 分别发送两段数据 data1 和 data2, 在接收端有以下⼏种常⻅的情况: +A. 先接收到 data1, 然后接收到 data2 . +B. 先接收到 data1 的部分数据, 然后接收到 data1 余下的部分以及 data2 的全部. +C. 先接收到了 data1 的全部数据和 data2 的部分数据, 然后接收到了 data2 的余下的数据. +D. ⼀次性接收到了 data1 和 data2 的全部数据.
+其中的 BCD 就是我们常⻅的粘包的情况. ⽽对于处理粘包的问题, 常⻅的解决⽅案有:
+WebSocket是HTML5提供的一种浏览器与服务器进行全双工通讯的网络技术,属于应用层协议。它基于TCP传输协议,并复用HTTP的握手通道。浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接, 并进行双向数据传输。
+WebSocket 的出现就解决了半双工通信的弊端。它最大的特点是:服务器可以向客户端主动推动消息,客户端也可以主动向服务器推送消息。
+WebSocket原理:客户端向 WebSocket 服务器通知(notify)一个带有所有接收者ID(recipients IDs)的事件(event),服务器接收后立即通知所有活跃的(active)客户端,只有ID在接收者ID序列中的客户端才会处理这个事件。 +
+WebSocket 特点的如下:
+Websocket的使用方法如下: +
+在客户端中:
+// 在index.html中直接写WebSocket,设置服务端的端口号为 9999
+let ws = new WebSocket('ws://localhost:9999');
+// 在客户端与服务端建立连接后触发
+ws.onopen = function() {
+ console.log("Connection open.");
+ ws.send('hello');
+};
+// 在服务端给客户端发来消息的时候触发
+ws.onmessage = function(res) {
+ console.log(res); // 打印的是MessageEvent对象
+ console.log(res.data); // 打印的是收到的消息
+};
+// 在客户端与服务端建立关闭后触发
+ws.onclose = function(evt) {
+ console.log("Connection closed.");
+};
+短轮询和长轮询的目的都是用于实现客户端和服务器端的一个即时通讯。
+短轮询的基本思路: 浏览器每隔一段时间向浏览器发送 http 请求,服务器端在收到请求后,不论是否有数据更新,都直接进行响应。这种方式实现的即时通信,本质上还是浏览器发送请求,服务器接受请求的一个过程,通过让客户端不断的进行请求,使得客户端能够模拟实时地收到服务器端的数据的变化。这种方式的优点是比较简单,易于理解。缺点是这种方式由于需要不断的建立 http 连接,严重浪费了服务器端和客户端的资源。当用户增加时,服务器端的压力就会变大,这是很不合理的。
+长轮询的基本思路: 首先由客户端向服务器发起请求,当服务器收到客户端发来的请求后,服务器端不会直接进行响应,而是先将这个请求挂起,然后判断服务器端数据是否有更新。如果有更新,则进行响应,如果一直没有数据,则到达一定的时间限制才返回。客户端 JavaScript 响应处理函数会在处理完服务器返回的信息后,再次发出请求,重新建立连接。长轮询和短轮询比起来,它的优点是明显减少了很多不必要的 http 请求次数,相比之下节约了资源。长轮询的缺点在于,连接挂起也会导致资源的浪费。
+SSE 的基本思想: 服务器使用流信息向服务器推送信息。严格地说,http 协议无法做到服务器主动推送信息。但是,有一种变通方法,就是服务器向客户端声明,接下来要发送的是流信息。也就是说,发送的不是一次性的数据包,而是一个数据流,会连续不断地发送过来。这时,客户端不会关闭连接,会一直等着服务器发过来的新的数据流,视频播放就是这样的例子。SSE 就是利用这种机制,使用流信息向浏览器推送信息。它基于 http 协议,目前除了 IE/Edge,其他浏览器都支持。它相对于前面两种方式来说,不需要建立过多的 http 请求,相比之下节约了资源。
+WebSocket 是 HTML5 定义的一个新协议议,与传统的 http 协议不同,该协议允许由服务器主动的向客户端推送信息。使用 WebSocket 协议的缺点是在服务器端的配置比较复杂。WebSocket 是一个全双工的协议,也就是通信双方是平等的,可以相互发送消息,而 SSE 的方式是单向通信的,只能由服务器端向客户端推送信息,如果客户端需要发送信息就是属于下一个 http 请求了。
+上面的四个通信协议,前三个都是基于HTTP协议的。
+对于这四种即使通信协议,从性能的角度来看: +WebSocket > 长连接(SEE) > 长轮询 > 短轮询 +但是,我们如果考虑浏览器的兼容性问题,顺序就恰恰相反了: +短轮询 > 长轮询 > 长连接(SEE) > WebSocket +所以,还是要根据具体的使用场景来判断使用哪种方式。
\ No newline at end of file diff --git a/src/content/notes/frontend/performance.html b/src/content/notes/frontend/performance.html new file mode 100644 index 0000000..f5e25f4 --- /dev/null +++ b/src/content/notes/frontend/performance.html @@ -0,0 +1,415 @@ +CDN(Content Delivery Network,内容分发网络)是指一种通过互联网互相连接的电脑网络系统,利用最靠近每位用户的服务器,更快、更可靠地将音乐、图片、视频、应用程序及其他文件发送给用户,来提供高性能、可扩展性及低成本的网络内容传递给用户。
+典型的CDN系统由下面三个部分组成:
+CDN一般会用来托管Web资源(包括文本、图片和脚本等),可供下载的资源(媒体文件、软件、文档等),应用程序(门户网站等)。使用CDN来加速这些资源的访问。
+(1)在性能方面,引入CDN的作用在于:
+(2)在安全方面,CDN有助于防御DDoS、MITM等网络攻击:
+除此之外,CDN作为一种基础的云服务,同样具有资源托管、按需扩展(能够应对流量高峰)等方面的优势。
+CDN和DNS有着密不可分的联系,先来看一下DNS的解析域名过程,在浏览器输入 www.test.com 的解析过程如下: +(1) 检查浏览器缓存 +(2)检查操作系统缓存,常见的如hosts文件 +(3)检查路由器缓存 +(4)如果前几步都没没找到,会向ISP(网络服务提供商)的LDNS服务器查询 +(5)如果LDNS服务器没找到,会向根域名服务器(Root Server)请求解析,分为以下几步:
+.com,.cn,.org等的地址,该例子中会返回.com的地址.test的地址www.test.com的地址CDN的工作原理: +(1)用户未使用CDN缓存资源的过程:
+(2)用户使用CDN缓存资源的过程:
+如果缓存服务器没有用户想要的内容,那么缓存服务器就会向它的上一级缓存服务器请求内容,以此类推,直到获取到需要的资源。最后如果还是没有,就会回到自己的服务器去获取资源。
+
+CNAME(意为:别名):在域名解析中,实际上解析出来的指定域名对应的IP地址,或者该域名的一个CNAME,然后再根据这个CNAME来查找对应的IP地址。
懒加载也叫做延迟加载、按需加载,指的是在长网页中延迟加载图片数据,是一种较好的网页性能优化的方式。在比较长的网页或应用中,如果图片很多,所有的图片都被加载出来,而用户只能看到可视窗口的那一部分图片数据,这样就浪费了性能。
+如果使用图片的懒加载就可以解决以上问题。在滚动屏幕之前,可视化区域之外的图片不会进行加载,在滚动屏幕时才加载。这样使得网页的加载速度更快,减少了服务器的负载。懒加载适用于图片较多,页面列表较长(长列表)的场景中。
+图片的加载是由src引起的,当对src赋值时,浏览器就会请求图片资源。根据这个原理,我们使用HTML5 的data-xxx属性来储存图片的路径,在需要加载图片的时候,将data-xxx中图片的路径赋值给src,这样就实现了图片的按需加载,即懒加载。
注意:data-xxx 中的xxx可以自定义,这里我们使用data-src来定义。
懒加载的实现重点在于确定用户需要加载哪张图片,在浏览器中,可视区域内的资源就是用户需要的资源。所以当图片出现在可视区域时,获取图片的真实地址并赋值给图片即可。
+使用原生JavaScript实现懒加载:
+知识点:
+(1)window.innerHeight 是浏览器可视区的高度
(2)document.body.scrollTop || document.documentElement.scrollTop 是浏览器滚动的过的距离
(3)imgs.offsetTop 是元素顶部距离文档顶部的高度(包括滚动条的距离)
(4)图片加载条件:img.offsetTop < window.innerHeight + document.body.scrollTop;
图示:
+
+代码实现:
<div class="container">
+ <img src="loading.gif" data-src="pic.png">
+ <img src="loading.gif" data-src="pic.png">
+ <img src="loading.gif" data-src="pic.png">
+ <img src="loading.gif" data-src="pic.png">
+ <img src="loading.gif" data-src="pic.png">
+ <img src="loading.gif" data-src="pic.png">
+</div>
+<script>
+var imgs = document.querySelectorAll('img');
+function lozyLoad(){
+ var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
+ var winHeight= window.innerHeight;
+ for(var i=0;i < imgs.length;i++){
+ if(imgs[i].offsetTop < scrollTop + winHeight ){
+ imgs[i].src = imgs[i].getAttribute('data-src');
+ }
+ }
+ }
+ window.onscroll = lozyLoad();
+</script>
+这两种方式都是提高网页性能的方式,两者主要区别是一个是提前加载,一个是迟缓甚至不加载。懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。
+当渲染树中部分或者全部元素的尺寸、结构或者属性发生变化时,浏览器会重新渲染部分或者全部文档的过程就称为回流。
+下面这些操作会导致回流:
+在触发回流(重排)的时候,由于浏览器渲染页面是基于流式布局的,所以当触发回流时,会导致周围的DOM元素重新排列,它的影响范围有两种:
+当页面中某些元素的样式发生变化,但是不会影响其在文档流中的位置时,浏览器就会对元素进行重新绘制,这个过程就是重绘。
+下面这些操作会导致回流:
+注意: 当触发回流时,一定会触发重绘,但是重绘不一定会引发回流。
+减少回流与重绘的措施:
+table布局, 一个小的改动可能会使整个table进行重新布局documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中display: none,操作结束后再把它显示出来。因为在display属性为none的元素上进行的DOM操作不会引发回流和重绘。浏览器针对页面的回流与重绘,进行了自身的优化——渲染队列
+浏览器会将所有的回流、重绘的操作放在一个队列中,当队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会对队列进行批处理。这样就会让多次的回流、重绘变成一次回流重绘。
+上面,将多个读操作(或者写操作)放在一起,就会等所有的读操作进入队列之后执行,这样,原本应该是触发多次回流,变成了只触发一次回流。
+对于如何优化动画,我们知道,一般情况下,动画需要频繁的操作DOM,就就会导致页面的性能问题,我们可以将动画的position属性设置为absolute或者fixed,将动画脱离文档流,这样他的回流就不会影响到页面了。
MDN中对documentFragment的解释:
++DocumentFragment,文档片段接口,一个没有父对象的最小文档对象。它被作为一个轻量版的 Document使用,就像标准的document一样,存储由节点(nodes)组成的文档结构。与document相比,最大的区别是DocumentFragment不是真实 DOM 树的一部分,它的变化不会触发 DOM 树的重新渲染,且不会导致性能等问题。
+
当我们把一个 DocumentFragment 节点插入文档树时,插入的不是 DocumentFragment 自身,而是它的所有子孙节点。在频繁的DOM操作时,我们就可以将DOM元素插入DocumentFragment,之后一次性的将所有的子孙节点插入文档中。和直接操作DOM相比,将DocumentFragment 节点插入DOM树时,不会触发页面的重绘,这样就大大提高了页面的性能。
+防抖函数的应用场景:
+节流函数的适⽤场景:
+函数防抖的实现:
+function debounce(fn, wait) {
+ var timer = null;
+
+ return function() {
+ var context = this,
+ args = [...arguments];
+
+ // 如果此时存在定时器的话,则取消之前的定时器重新记时
+ if (timer) {
+ clearTimeout(timer);
+ timer = null;
+ }
+
+ // 设置定时器,使事件间隔指定事件后执行
+ timer = setTimeout(() => {
+ fn.apply(context, args);
+ }, wait);
+ };
+}
+函数节流的实现:
+// 时间戳版
+function throttle(fn, delay) {
+ var preTime = Date.now();
+
+ return function() {
+ var context = this,
+ args = [...arguments],
+ nowTime = Date.now();
+
+ // 如果两次时间间隔超过了指定时间,则执行函数。
+ if (nowTime - preTime >= delay) {
+ preTime = Date.now();
+ return fn.apply(context, args);
+ }
+ };
+}
+
+// 定时器版
+function throttle (fun, wait){
+ let timeout = null
+ return function(){
+ let context = this
+ let args = [...arguments]
+ if(!timeout){
+ timeout = setTimeout(() => {
+ fun.apply(context, args)
+ timeout = null
+ }, wait)
+ }
+ }
+}
+(1)BMP,是无损的、既支持索引色也支持直接色的点阵图。这种图片格式几乎没有对数据进行压缩,所以BMP格式的图片通常是较大的文件。
+(2)GIF是无损的、采用索引色的点阵图。采用LZW压缩算法进行编码。文件小,是GIF格式的优点,同时,GIF格式还具有支持动画以及透明的优点。但是GIF格式仅支持8bit的索引色,所以GIF格式适用于对色彩要求不高同时需要文件体积较小的场景。
+(3)JPEG是有损的、采用直接色的点阵图。JPEG的图片的优点是采用了直接色,得益于更丰富的色彩,JPEG非常适合用来存储照片,与GIF相比,JPEG不适合用来存储企业Logo、线框类的图。因为有损压缩会导致图片模糊,而直接色的选用,又会导致图片文件较GIF更大。
+(4)PNG-8是无损的、使用索引色的点阵图。PNG是一种比较新的图片格式,PNG-8是非常好的GIF格式替代者,在可能的情况下,应该尽可能的使用PNG-8而不是GIF,因为在相同的图片效果下,PNG-8具有更小的文件体积。除此之外,PNG-8还支持透明度的调节,而GIF并不支持。除非需要动画的支持,否则没有理由使用GIF而不是PNG-8。
+(5)PNG-24是无损的、使用直接色的点阵图。PNG-24的优点在于它压缩了图片的数据,使得同样效果的图片,PNG-24格式的文件大小要比BMP小得多。当然,PNG24的图片还是要比JPEG、GIF、PNG-8大得多。
+(6)SVG是无损的矢量图。SVG是矢量图意味着SVG图片由直线和曲线以及绘制它们的方法组成。当放大SVG图片时,看到的还是线和曲线,而不会出现像素点。这意味着SVG图片在放大时,不会失真,所以它非常适合用来绘制Logo、Icon等。
+(7)WebP是谷歌开发的一种新图片格式,WebP是同时支持有损和无损压缩的、使用直接色的点阵图。从名字就可以看出来它是为Web而生的,什么叫为Web而生呢?就是说相同质量的图片,WebP具有更小的文件体积。现在网站上充满了大量的图片,如果能够降低每一个图片的文件大小,那么将大大减少浏览器和服务器之间的数据传输量,进而降低访问延迟,提升访问体验。目前只有Chrome浏览器和Opera浏览器支持WebP格式,兼容性不太好。
+对于 Loader 来说,影响打包效率首当其冲必属 Babel 了。因为 Babel 会将代码转为字符串生成 AST,然后对 AST 继续进行转变最后再生成新的代码,项目越大,转换代码越多,效率就越低。当然了,这是可以优化的。
+首先我们优化 Loader 的文件搜索范围
+module.exports = {
+ module: {
+ rules: [
+ {
+ // js 文件才使用 babel
+ test: /\.js$/,
+ loader: 'babel-loader',
+ // 只在 src 文件夹下查找
+ include: [resolve('src')],
+ // 不会去查找的路径
+ exclude: /node_modules/
+ }
+ ]
+ }
+}
+对于 Babel 来说,希望只作用在 JS 代码上的,然后 node_modules 中使用的代码都是编译过的,所以完全没有必要再去处理一遍。
当然这样做还不够,还可以将 Babel 编译过的文件缓存起来,下次只需要编译更改过的代码文件即可,这样可以大幅度加快打包时间
+loader: 'babel-loader?cacheDirectory=true'
+受限于 Node 是单线程运行的,所以 Webpack 在打包的过程中也是单线程的,特别是在执行 Loader 的时候,长时间编译的任务很多,这样就会导致等待的情况。
+HappyPack 可以将 Loader 的同步执行转换为并行的,这样就能充分利用系统资源来加快打包效率了
+module: {
+ loaders: [
+ {
+ test: /\.js$/,
+ include: [resolve('src')],
+ exclude: /node_modules/,
+ // id 后面的内容对应下面
+ loader: 'happypack/loader?id=happybabel'
+ }
+ ]
+},
+plugins: [
+ new HappyPack({
+ id: 'happybabel',
+ loaders: ['babel-loader?cacheDirectory'],
+ // 开启 4 个线程
+ threads: 4
+ })
+]
+DllPlugin 可以将特定的类库提前打包然后引入。这种方式可以极大的减少打包类库的次数,只有当类库更新版本才有需要重新打包,并且也实现了将公共代码抽离成单独文件的优化方案。DllPlugin的使用方法如下:
+// 单独配置在一个文件中
+// webpack.dll.conf.js
+const path = require('path')
+const webpack = require('webpack')
+module.exports = {
+ entry: {
+ // 想统一打包的类库
+ vendor: ['react']
+ },
+ output: {
+ path: path.join(__dirname, 'dist'),
+ filename: '[name].dll.js',
+ library: '[name]-[hash]'
+ },
+ plugins: [
+ new webpack.DllPlugin({
+ // name 必须和 output.library 一致
+ name: '[name]-[hash]',
+ // 该属性需要与 DllReferencePlugin 中一致
+ context: __dirname,
+ path: path.join(__dirname, 'dist', '[name]-manifest.json')
+ })
+ ]
+}
+然后需要执行这个配置文件生成依赖文件,接下来需要使用 DllReferencePlugin 将依赖文件引入项目中
// webpack.conf.js
+module.exports = {
+ // ...省略其他配置
+ plugins: [
+ new webpack.DllReferencePlugin({
+ context: __dirname,
+ // manifest 就是之前打包出来的 json 文件
+ manifest: require('./dist/vendor-manifest.json'),
+ })
+ ]
+}
+在 Webpack3 中,一般使用 UglifyJS 来压缩代码,但是这个是单线程运行的,为了加快效率,可以使用 webpack-parallel-uglify-plugin 来并行运行 UglifyJS,从而提高效率。
在 Webpack4 中,不需要以上这些操作了,只需要将 mode 设置为 production 就可以默认开启以上功能。代码压缩也是我们必做的性能优化方案,当然我们不止可以压缩 JS 代码,还可以压缩 HTML、CSS 代码,并且在压缩 JS 代码的过程中,我们还可以通过配置实现比如删除 console.log 这类代码的功能。
可以通过一些小的优化点来加快打包速度
+resolve.extensions:用来表明文件后缀列表,默认查找顺序是 ['.js', '.json'],如果你的导入文件没有添加后缀就会按照这个顺序查找文件。我们应该尽可能减少后缀列表长度,然后将出现频率高的后缀排在前面resolve.alias:可以通过别名的方式来映射一个路径,能让 Webpack 更快找到路径module.noParse:如果你确定一个文件下没有其他依赖,就可以使用该属性让 Webpack 不扫描该文件,这种方式对于大型的类库很有帮助在开发 SPA 项目的时候,项目中都会存在很多路由页面。如果将这些页面全部打包进一个 JS 文件的话,虽然将多个请求合并了,但是同样也加载了很多并不需要的代码,耗费了更长的时间。那么为了首页能更快地呈现给用户,希望首页能加载的文件体积越小越好,这时候就可以使用按需加载,将每个路由页面单独打包为一个文件。当然不仅仅路由可以按需加载,对于 loadash 这种大型类库同样可以使用这个功能。
按需加载的代码实现这里就不详细展开了,因为鉴于用的框架不同,实现起来都是不一样的。当然了,虽然他们的用法可能不同,但是底层的机制都是一样的。都是当使用的时候再去下载对应文件,返回一个 Promise,当 Promise 成功以后去执行回调。
Scope Hoisting 会分析出模块之间的依赖关系,尽可能的把打包出来的模块合并到一个函数中去。
+比如希望打包两个文件:
+// test.js
+export const a = 1
+// index.js
+import { a } from './test.js'
+对于这种情况,打包出来的代码会类似这样:
+[
+ /* 0 */
+ function (module, exports, require) {
+ //...
+ },
+ /* 1 */
+ function (module, exports, require) {
+ //...
+ }
+]
+但是如果使用 Scope Hoisting ,代码就会尽可能的合并到一个函数中去,也就变成了这样的类似代码:
+[
+ /* 0 */
+ function (module, exports, require) {
+ //...
+ }
+]
+这样的打包方式生成的代码明显比之前的少多了。如果在 Webpack4 中你希望开启这个功能,只需要启用 optimization.concatenateModules 就可以了:
module.exports = {
+ optimization: {
+ concatenateModules: true
+ }
+}
+Tree Shaking 可以实现删除项目中未被引用的代码,比如:
+// test.js
+export const a = 1
+export const b = 2
+// index.js
+import { a } from './test.js'
+对于以上情况,test 文件中的变量 b 如果没有在项目中使用到的话,就不会被打包到文件中。
如果使用 Webpack 4 的话,开启生产环境就会自动启动这个优化功能。
+⽤webpack优化前端性能是指优化webpack的输出结果,让打包的最终结果在浏览器运⾏快速⾼效。
+<div onClick={this.handleClick.bind(this)}>点我</div>
+React并不是将click事件绑定到了div的真实DOM上,而是在document处监听了所有的事件,当事件发生并且冒泡到document处的时候,React将事件内容封装并交由真正的处理函数运行。这样的方式不仅仅减少了内存的消耗,还能在组件挂在销毁时统一订阅和移除事件。
+除此之外,冒泡到document上的事件也不是原生的浏览器事件,而是由react自己实现的合成事件(SyntheticEvent)。因此如果不想要是事件冒泡的话应该调用event.preventDefault()方法,而不是调用event.stopProppagation()方法。
+
+JSX 上写的事件并没有绑定在对应的真实 DOM 上,而是通过事件代理的方式,将所有的事件都统一绑定在了
document 上。这样的方式不仅减少了内存消耗,还能在组件挂载销毁时统一订阅和移除事件。
另外冒泡到 document 上的事件也不是原生浏览器事件,而是 React 自己实现的合成事件(SyntheticEvent)。因此我们如果不想要事件冒泡的话,调用 event.stopPropagation 是无效的,而应该调用 event.preventDefault。
实现合成事件的目的如下:
+区别:
+preventDefault()来阻止默认行为。合成事件是 react 模拟原生 DOM 事件所有能力的一个事件对象,其优点如下:
+事件的执行顺序为原生事件先执行,合成事件后执行,合成事件会冒泡绑定到 document 上,所以尽量避免原生事件与合成事件混用,如果原生事件阻止冒泡,可能会导致合成事件不执行,因为需要冒泡到document 上合成事件才会执行。
+React基于Virtual DOM实现了一个SyntheticEvent层(合成事件层),定义的事件处理器会接收到一个合成事件对象的实例,它符合W3C标准,且与原生的浏览器事件拥有同样的接口,支持冒泡机制,所有的事件都自动绑定在最外层上。
+在React底层,主要对合成事件做了两件事:
+这三者是目前react解决代码复用的主要方式:
+(1)HOC +官方解释∶
+++高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。
+
简言之,HOC是一种组件的设计模式,HOC接受一个组件和额外的参数(如果需要),返回一个新的组件。HOC 是纯函数,没有副作用。
+// hoc的定义
+function withSubscription(WrappedComponent, selectData) {
+ return class extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ data: selectData(DataSource, props)
+ };
+ }
+ // 一些通用的逻辑处理
+ render() {
+ // ... 并使用新数据渲染被包装的组件!
+ return <WrappedComponent data={this.state.data} {...this.props} />;
+ }
+ };
+
+// 使用
+const BlogPostWithSubscription = withSubscription(BlogPost,
+ (DataSource, props) => DataSource.getBlogPost(props.id));
+HOC的优缺点∶
+(2)Render props +官方解释∶
+++"render prop"是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术
+
具有render prop 的组件接受一个返回React元素的函数,将render的渲染逻辑注入到组件内部。在这里,"render"的命名可以是任何其他有效的标识符。
+// DataProvider组件内部的渲染逻辑如下
+class DataProvider extends React.Components {
+ state = {
+ name: 'Tom'
+ }
+
+ render() {
+ return (
+ <div>
+ <p>共享数据组件自己内部的渲染逻辑</p>
+ { this.props.render(this.state) }
+ </div>
+ );
+ }
+}
+
+// 调用方式
+<DataProvider render={data => (
+ <h1>Hello {data.name}</h1>
+)}/>
+由此可以看到,render props的优缺点也很明显∶
+(3)Hooks +官方解释∶
+++Hook是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。通过自定义hook,可以复用代码逻辑。
+
// 自定义一个获取订阅数据的hook
+function useSubscription() {
+ const data = DataSource.getComments();
+ return [data];
+}
+//
+function CommentList(props) {
+ const {data} = props;
+ const [subData] = useSubscription();
+ ...
+}
+// 使用
+<CommentList data='hello' />
+以上可以看出,hook解决了hoc的prop覆盖的问题,同时使用的方式解决了render props的嵌套地狱的问题。hook的优点如下∶
+需要注意的是:hook只能在组件顶层使用,不可在分支语句中使用。
+总结∶ +Hoc、render props和hook都是为了解决代码复用的问题,但是hoc和render props都有特定的使用场景和明显的缺点。hook是react16.8更新的新的API,让组件逻辑复用更简洁明了,同时也解决了hoc和render props的一些缺点。
+React V15 在渲染时,会递归比对 VirtualDOM 树,找出需要变动的节点,然后同步更新它们, 一气呵成。这个过程期间, React 会占据浏览器资源,这会导致用户触发的事件得不到响应,并且会导致掉帧,导致用户感觉到卡顿。
+为了给用户制造一种应用很快的“假象”,不能让一个任务长期霸占着资源。 可以将浏览器的渲染、布局、绘制、资源加载(例如 HTML 解析)、事件响应、脚本执行视作操作系统的“进程”,需要通过某些调度策略合理地分配 CPU 资源,从而提高浏览器的用户响应速率, 同时兼顾任务执行效率。
+所以 React 通过Fiber 架构,让这个执行过程变成可被中断。“适时”地让出 CPU 执行权,除了可以让浏览器及时地响应用户的交互,还有其他好处:
+核心思想: Fiber 也称协程或者纤程。它和线程并不一样,协程本身是没有并发或者并行能力的(需要配合线程),它只是一种控制流程的让出机制。让出 CPU 的执行权,让 CPU 能在这段时间执行其他的操作。渲染的过程可以被中断,可以将控制权交回浏览器,让位给高优先级的任务,浏览器空闲后再恢复渲染。
+PureComponent表示一个纯组件,可以用来优化React程序,减少render函数执行的次数,从而提高组件的性能。
+在React中,当prop或者state发生变化时,可以通过在shouldComponentUpdate生命周期函数中执行return false来阻止页面的更新,从而减少不必要的render执行。React.PureComponent会自动执行 shouldComponentUpdate。
+不过,pureComponent中的 shouldComponentUpdate() 进行的是浅比较,也就是说如果是引用数据类型的数据,只会比较不是同一个地址,而不会比较这个地址里面的数据是否一致。浅比较会忽略属性和或状态突变情况,其实也就是数据引用指针没有变化,而数据发生改变的时候render是不会执行的。如果需要重新渲染那么就需要重新开辟空间引用数据。PureComponent一般会用在一些纯展示组件上。
+使用pureComponent的好处:当组件更新时,如果组件的props或者state都没有改变,render函数就不会触发。省去虚拟DOM的生成和对比过程,达到提升性能的目的。这是因为react自动做了一层浅比较。
+element是一个普通对象(plain object),描述了对于一个DOM节点或者其他组件component,你想让它在屏幕上呈现成什么样子。元素element可以在它的属性props中包含其他元素(译注:用于形成元素树)。创建一个React元素element成本很低。元素element创建之后是不可变的。component可以通过多种方式声明。可以是带有一个render()方法的类,简单点也可以定义为一个函数。这两种情况下,它都把属性props作为输入,把返回的一棵元素树作为输出。instance是你在所写的组件类component class中使用关键字this所指向的东西(译注:组件实例)。它用来存储本地状态和响应生命周期事件很有用。函数式组件(Functional component)根本没有实例instance。类组件(Class component)有实例instance,但是永远也不需要直接创建一个组件的实例,因为React帮我们做了这些。
React.createClass和extends Component的bai区别主要在于:
+(1)语法区别
+(2)propType 和 getDefaultProps
+(3)状态的区别
+(4)this区别
+(5)Mixins
+React mixins 的特性将不能被使用了。官方解释∶
+++高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。
+
高阶组件(HOC)就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件,它只是一种组件的设计模式,这种设计模式是由react自身的组合性质必然产生的。我们将它们称为纯组件,因为它们可以接受任何动态提供的子组件,但它们不会修改或复制其输入组件中的任何行为。
+// hoc的定义
+function withSubscription(WrappedComponent, selectData) {
+ return class extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ data: selectData(DataSource, props)
+ };
+ }
+ // 一些通用的逻辑处理
+ render() {
+ // ... 并使用新数据渲染被包装的组件!
+ return <WrappedComponent data={this.state.data} {...this.props} />;
+ }
+ };
+
+// 使用
+const BlogPostWithSubscription = withSubscription(BlogPost,
+ (DataSource, props) => DataSource.getBlogPost(props.id));
+1)HOC的优缺点
+2)适用场景
+3)具体应用例子
+// HOC.js
+function withAdminAuth(WrappedComponent) {
+ return class extends React.Component {
+ state = {
+ isAdmin: false,
+ }
+ async UNSAFE_componentWillMount() {
+ const currentRole = await getCurrentUserRole();
+ this.setState({
+ isAdmin: currentRole === 'Admin',
+ });
+ }
+ render() {
+ if (this.state.isAdmin) {
+ return <WrappedComponent {...this.props} />;
+ } else {
+ return (<div>您没有权限查看该页面,请联系管理员!</div>);
+ }
+ }
+ };
+}
+
+// pages/page-a.js
+class PageA extends React.Component {
+ constructor(props) {
+ super(props);
+ // something here...
+ }
+ UNSAFE_componentWillMount() {
+ // fetching data
+ }
+ render() {
+ // render page with data
+ }
+}
+export default withAdminAuth(PageA);
+
+// pages/page-b.js
+class PageB extends React.Component {
+ constructor(props) {
+ super(props);
+ // something here...
+ }
+ UNSAFE_componentWillMount() {
+ // fetching data
+ }
+ render() {
+ // render page with data
+ }
+}
+export default withAdminAuth(PageB);
+class Home extends React.Component {
+ render() {
+ return (<h1>Hello World.</h1>);
+ }
+ }
+ function withTiming(WrappedComponent) {
+ return class extends WrappedComponent {
+ constructor(props) {
+ super(props);
+ this.start = 0;
+ this.end = 0;
+ }
+ UNSAFE_componentWillMount() {
+ super.componentWillMount && super.componentWillMount();
+ this.start = Date.now();
+ }
+ componentDidMount() {
+ super.componentDidMount && super.componentDidMount();
+ this.end = Date.now();
+ console.log(`${WrappedComponent.name} 组件渲染时间为 ${this.end - this.start} ms`);
+ }
+ render() {
+ return super.render();
+ }
+ };
+ }
+
+ export default withTiming(Home);
+注意:withTiming 是利用 反向继承 实现的一个高阶组件,功能是计算被包裹组件(这里是 Home 组件)的渲染时间。
+const withFetching = fetching => WrappedComponent => {
+ return class extends React.Component {
+ state = {
+ data: [],
+ }
+ async UNSAFE_componentWillMount() {
+ const data = await fetching();
+ this.setState({
+ data,
+ });
+ }
+ render() {
+ return <WrappedComponent data={this.state.data} {...this.props} />;
+ }
+ }
+}
+
+// pages/page-a.js
+export default withFetching(fetching('science-fiction'))(MovieList);
+// pages/page-b.js
+export default withFetching(fetching('action'))(MovieList);
+// pages/page-other.js
+export default withFetching(fetching('some-other-type'))(MovieList);
+该方法当props发生变化时执行,初始化render时不执行,在这个回调函数里面,你可以根据属性的变化,通过调用this.setState()来更新你的组件状态,旧的属性还是可以通过this.props来获取,这里调用更新状态是安全的,并不会触发额外的render调用。
使用好处: 在这个生命周期中,可以在子组件的render函数执行前获取新的props,从而更新子组件自己的state。 可以将数据请求放在这里进行执行,需要传的参数则从componentWillReceiveProps(nextProps)中获取。而不必将所有的请求都放在父组件中。于是该请求只会在该组件渲染时才会发出,从而减轻请求负担。
+componentWillReceiveProps在初始化render的时候不会执行,它会在Component接受到新的状态(Props)时被触发,一般用于父组件状态更新时子组件的重新渲染。
+(1)哪些方法会触发 react 重新渲染?
+setState 是 React 中最常用的命令,通常情况下,执行 setState 会触发 render。但是这里有个点值得关注,执行 setState 的时候不一定会重新渲染。当 setState 传入 null 时,并不会触发 render。
+class App extends React.Component {
+ state = {
+ a: 1
+ };
+
+ render() {
+ console.log("render");
+ return (
+ <React.Fragement>
+ <p>{this.state.a}</p>
+ <button
+ onClick={() => {
+ this.setState({ a: 1 }); // 这里并没有改变 a 的值
+ }}
+ >
+ Click me
+ </button>
+ <button onClick={() => this.setState(null)}>setState null</button>
+ <Child />
+ </React.Fragement>
+ );
+ }
+}
+只要父组件重新渲染了,即使传入子组件的 props 未发生变化,那么子组件也会重新渲染,进而触发 render
+(2)重新渲染 render 会做些什么?
+React 的处理 render 的基本思维模式是每次一有变动就会去重新渲染整个应用。在 Virtual DOM 没有出现之前,最简单的方法就是直接调用 innerHTML。Virtual DOM厉害的地方并不是说它比直接操作 DOM 快,而是说不管数据怎么变,都会尽量以最小的代价去更新 DOM。React 将 render 函数返回的虚拟 DOM 树与老的进行比较,从而确定 DOM 要不要更新、怎么更新。当 DOM 树很大时,遍历两棵树进行各种比对还是相当耗性能的,特别是在顶层 setState 一个微小的修改,默认会去遍历整棵树。尽管 React 使用高度优化的 Diff 算法,但是这个过程仍然会损耗性能.
+组件状态的改变可以因为props的改变,或者直接通过setState方法改变。组件获得新的状态,然后React决定是否应该重新渲染组件。只要组件的state发生变化,React就会对组件进行重新渲染。这是因为React中的shouldComponentUpdate方法默认返回true,这就是导致每次更新都重新渲染的原因。
当React将要渲染组件时会执行shouldComponentUpdate方法来看它是否返回true(组件应该更新,也就是重新渲染)。所以需要重写shouldComponentUpdate方法让它根据情况返回true或者false来告诉React什么时候重新渲染什么时候跳过重新渲染。
React 声明组件的三种方式:
+无状态组件React.createClass定义的组件extends React.Component定义的组件(1)无状态函数式组件 +它是为了创建纯展示组件,这种组件只负责根据传入的props来展示,不涉及到state状态的操作 +组件不会被实例化,整体渲染性能得到提升,不能访问this对象,不能访问生命周期的方法
+(2)ES5 原生方式 React.createClass // RFC +React.createClass会自绑定函数方法,导致不必要的性能开销,增加代码过时的可能性。
+(3)E6继承形式 React.Component // RCC +目前极为推荐的创建有状态组件的方式,最终会取代React.createClass形式;相对于 React.createClass可以更好实现代码复用。
+无状态组件相对于于后者的区别: +与无状态组件相比,React.createClass和React.Component都是创建有状态的组件,这些组件是要被实例化的,并且可以访问组件的生命周期方法。
+React.createClass与React.Component区别:
+① 函数this自绑定
+② 组件属性类型propTypes及其默认props属性defaultProps配置不同
+③ 组件初始状态state的配置不同
+(1)有状态组件
+特点:
+使用场景:
+总结: +类组件可以维护自身的状态变量,即组件的 state ,类组件还有不同的生命周期方法,可以让开发者能够在组件的不同阶段(挂载、更新、卸载),对组件做更多的控制。类组件则既可以充当无状态组件,也可以充当有状态组件。当一个类组件不需要管理自身状态时,也可称为无状态组件。
+(2)无状态组件 +特点:
+使用场景:
+优点:
+缺点:
+总结:
+组件内部状态且与外部无关的组件,可以考虑用状态组件,这样状态树就不会过于复杂,易于理解和管理。当一个组件不需要管理自身状态时,也就是无状态组件,应该优先设计为函数组件。比如自定义的 <Button/>、 <Input /> 等组件。
在React中,组件返回的元素只能有一个根元素。为了不添加多余的DOM节点,我们可以使用Fragment标签来包裹所有的元素,Fragment标签不会渲染出任何元素。React官方对Fragment的解释:
+++React 中的一个常见模式是一个组件返回多个元素。Fragments 允许你将子列表分组,而无需向 DOM 添加额外节点。
+
import React, { Component, Fragment } from 'react'
+
+// 一般形式
+render() {
+ return (
+ <React.Fragment>
+ <ChildA />
+ <ChildB />
+ <ChildC />
+ </React.Fragment>
+ );
+}
+// 也可以写成以下形式
+render() {
+ return (
+ <>
+ <ChildA />
+ <ChildB />
+ <ChildC />
+ </>
+ );
+}
+可以用ref来获取某个子节点的实例,然后通过当前class组件实例的一些特定属性来直接获取子节点实例。ref有三种实现方法:
+<p ref="info">span</p><p ref={ele => this.info = ele}></p><>
+ <span id="name" ref={this.spanRef}>{this.state.title}</span>
+ <span>{
+ this.spanRef.current ? '有值' : '无值'
+ }</span>
+</>
+不可以,render 阶段 DOM 还没有生成,无法获取 DOM。DOM 的获取需要在 pre-commit 阶段和 commit 阶段:
+
React 官方对 Portals 的定义:
+++Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案
+
Portals 是React 16提供的官方解决方案,使得组件可以脱离父组件层级挂载在DOM树的任何位置。通俗来讲,就是我们 render 一个组件,但这个组件的 DOM 结构并不在本组件内。
+Portals语法如下:
+ReactDOM.createPortal(child, container);
+一般情况下,组件的render函数返回的元素会被挂载在它的父级组件上:
+import DemoComponent from './DemoComponent';
+render() {
+ // DemoComponent元素会被挂载在id为parent的div的元素上
+ return (
+ <div id="parent">
+ <DemoComponent />
+ </div>
+ );
+}
+然而,有些元素需要被挂载在更高层级的位置。最典型的应用场景:当父组件具有overflow: hidden或者z-index的样式设置时,组件有可能被其他元素遮挡,这时就可以考虑要不要使用Portal使组件的挂载脱离父组件。例如:对话框,模态窗。
import DemoComponent from './DemoComponent';
+render() {
+ // DemoComponent元素会被挂载在id为parent的div的元素上
+ return (
+ <div id="parent">
+ <DemoComponent />
+ </div>
+ );
+}
+React 基于虚拟 DOM 和高效 Diff 算法的完美配合,实现了对 DOM 最小粒度的更新。大多数情况下,React 对 DOM 的渲染效率足以业务日常。但在个别复杂业务场景下,性能问题依然会困扰我们。此时需要采取一些措施来提升运行性能,其很重要的一个方向,就是避免不必要的渲染(Render)。这里提下优化的点:
+在 React 类组件中,可以利用 shouldComponentUpdate或者 PureComponent 来减少因父组件更新而触发子组件的 render,从而达到目的。shouldComponentUpdate 来决定是否组件是否重新渲染,如果不希望组件重新渲染,返回 false 即可。
+在函数组件中,并没有 shouldComponentUpdate 这个生命周期,可以利用高阶组件,封装一个类似 PureComponet 的功能
+React.memo 是 React 16.6 新的一个 API,用来缓存组件的渲染,避免不必要的更新,其实也是一个高阶组件,与 PureComponent 十分类似,但不同的是, React.memo只能用于函数组件。
+React-intl是雅虎的语言国际化开源项目FormatJS的一部分,通过其提供的组件和API可以与ReactJS绑定。
+React-intl提供了两种使用方法,一种是引用React组件,另一种是直接调取API,官方更加推荐在React项目中使用前者,只有在无法使用React组件的地方,才应该调用框架提供的API。它提供了一系列的React组件,包括数字格式化、字符串格式化、日期格式化等。
+在React-intl中,可以配置不同的语言包,他的工作原理就是根据需要,在语言包之间进行切换。
+在React中,数据传递一般使用props传递数据,维持单向数据流,这样可以让组件之间的关系变得简单且可预测,但是单项数据流在某些场景中并不适用。单纯一对的父子组件传递并无问题,但要是组件之间层层依赖深入,props就需要层层传递显然,这样做太繁琐了。
+Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。
+可以把context当做是特定一个组件树内共享的store,用来做数据传递。简单说就是,当你不想在组件树中通过逐层传递props或者state的方式来传递数据时,可以使用Context来实现跨层级的组件数据传递。
+JS的代码块在执行期间,会创建一个相应的作用域链,这个作用域链记录着运行时JS代码块执行期间所能访问的活动对象,包括变量和函数,JS程序通过作用域链访问到代码块内部或者外部的变量和函数。
+假如以JS的作用域链作为类比,React组件提供的Context对象其实就好比一个提供给子组件访问的作用域,而 Context对象的属性可以看成作用域上的活动对象。由于组件 的 Context 由其父节点链上所有组件通 过 getChildContext()返回的Context对象组合而成,所以,组件通过Context是可以访问到其父组件链上所有节点组件提供的Context的属性。
+(1)受控组件
+在使用表单来收集用户输入时,例如<input><select><textearea>等元素都要绑定一个change事件,当表单的状态发生变化,就会触发onChange事件,更新组件的state。这种组件在React中被称为受控组件,在受控组件中,组件渲染出的状态与它的value或checked属性相对应,react通过这种方式消除了组件的局部状态,使整个状态可控。react官方推荐使用受控表单组件。
受控组件更新state的流程:
+受控组件缺陷: +表单元素的值都是由React组件进行管理,当有多个输入框,或者多个这种组件时,如果想同时获取到全部的值就必须每个都要编写事件处理函数,这会让代码看着很臃肿,所以为了解决这种情况,出现了非受控组件。
+(2)非受控组件 +如果一个表单组件没有value props(单选和复选按钮对应的是checked props)时,就可以称为非受控组件。在非受控组件中,可以使用一个ref来从DOM获得表单值。而不是为每个状态更新编写一个事件处理程序。
+React官方的解释:
+++要编写一个非受控组件,而不是为每个状态更新都编写数据处理函数,你可以使用 ref来从 DOM 节点中获取表单数据。 +因为非受控组件将真实数据储存在 DOM 节点中,所以在使用非受控组件时,有时候反而更容易同时集成 React 和非 React 代码。如果你不介意代码美观性,并且希望快速编写代码,使用非受控组件往往可以减少你的代码量。否则,你应该使用受控组件。
+
例如,下面的代码在非受控组件中接收单个属性:
+class NameForm extends React.Component {
+ constructor(props) {
+ super(props);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ }
+ handleSubmit(event) {
+ alert('A name was submitted: ' + this.input.value);
+ event.preventDefault();
+ }
+ render() {
+ return (
+ <form onSubmit={this.handleSubmit}>
+ <label>
+ Name:
+ <input type="text" ref={(input) => this.input = input} />
+ </label>
+ <input type="submit" value="Submit" />
+ </form>
+ );
+ }
+}
+总结: 页面中所有输入类的DOM如果是现用现取的称为非受控组件,而通过setState将输入的值维护到了state中,需要时再从state中取出,这里的数据就受到了state的控制,称为受控组件。
+Refs 提供了一种方式,用于访问在 render 方法中创建的 React 元素或 DOM 节点。Refs 应该谨慎使用,如下场景使用 Refs 比较适合:
+Refs 是使用 React.createRef() 方法创建的,他通过 ref 属性附加到 React 元素上。要在整个组件中使用 Refs,需要将 ref 在构造函数中分配给其实例属性:
class MyComponent extends React.Component {
+ constructor(props) {
+ super(props)
+ this.myRef = React.createRef()
+ }
+ render() {
+ return <div ref={this.myRef} />
+ }
+}
+由于函数组件没有实例,因此不能在函数组件上直接使用 ref:
function MyFunctionalComponent() {
+ return <input />;
+}
+class Parent extends React.Component {
+ constructor(props) {
+ super(props);
+ this.textInput = React.createRef();
+ }
+ render() {
+ // 这将不会工作!
+ return (
+ <MyFunctionalComponent ref={this.textInput} />
+ );
+ }
+}
+但可以通过闭合的帮助在函数组件内部进行使用 Refs:
+function CustomTextInput(props) {
+ // 这里必须声明 textInput,这样 ref 回调才可以引用它
+ let textInput = null;
+ function handleClick() {
+ textInput.focus();
+ }
+ return (
+ <div>
+ <input
+ type="text"
+ ref={(input) => { textInput = input; }} />
+ <input
+ type="button"
+ value="Focus the text input"
+ onClick={handleClick}
+ />
+ </div>
+ );
+}
+注意:
+ref 的返回值取决于节点的类型:
+ref 属性被用于一个普通的 HTML 元素时,React.createRef() 将接收底层 DOM 元素作为他的 current 属性以创建 ref。ref 属性被用于一个自定义的类组件时,ref 对象将接收该组件已挂载的实例作为他的 current。ref 时可使用传递 Refs 或回调 Refs。构造函数主要用于两个目的:
+所以,当在React class中需要设置state的初始值或者绑定事件时,需要加上构造函数,官方Demo:
+class LikeButton extends React.Component {
+ constructor() {
+ super();
+ this.state = {
+ liked: false
+ };
+ this.handleClick = this.handleClick.bind(this);
+ }
+ handleClick() {
+ this.setState({liked: !this.state.liked});
+ }
+ render() {
+ const text = this.state.liked ? 'liked' : 'haven\'t liked';
+ return (
+ <div onClick={this.handleClick}>
+ You {text} this. Click to toggle.
+ </div>
+ );
+ }
+}
+ReactDOM.render(
+ <LikeButton />,
+ document.getElementById('example')
+);
+构造函数用来新建父类的this对象;子类必须在constructor方法中调用super方法;否则新建实例时会报错;因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法;子类就得不到this对象。
+注意:
+React.forwardRef 会创建一个React组件,这个组件能够将其接受的 ref 属性转发到其组件树下的另一个组件中。这种技术并不常见,但在以下两种场景中特别有用:
+相同点: +组件是 React 可复用的最小代码片段,它们会返回要在页面中渲染的 React 元素。也正因为组件是 React 的最小编码单位,所以无论是函数组件还是类组件,在使用方式和最终呈现效果上都是完全一致的。
+我们甚至可以将一个类组件改写成函数组件,或者把函数组件改写成一个类组件(虽然并不推荐这种重构行为)。从使用者的角度而言,很难从使用体验上区分两者,而且在现代浏览器中,闭包和类的性能只在极端场景下才会有明显的差别。所以,基本可认为两者作为组件是完全一致的。
+不同点:
+
+具体的执行过程如下(源码级解析):
setState 入口函数,入口函数在这里就是充当一个分发器的角色,根据入参的不同,将其分发到不同的功能函数中去;ReactComponent.prototype.setState = function (partialState, callback) {
+ this.updater.enqueueSetState(this, partialState);
+ if (callback) {
+ this.updater.enqueueCallback(this, callback, 'setState');
+ }
+};
+enqueueSetState 方法将新的 state 放进组件的状态队列里,并调用 enqueueUpdate 来处理将要更新的实例对象;enqueueSetState: function (publicInstance, partialState) {
+ // 根据 this 拿到对应的组件实例
+ var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
+ // 这个 queue 对应的就是一个组件实例的 state 数组
+ var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
+ queue.push(partialState);
+ // enqueueUpdate 用来处理当前的组件实例
+ enqueueUpdate(internalInstance);
+}
+enqueueUpdate 方法中引出了一个关键的对象——batchingStrategy,该对象所具备的isBatchingUpdates 属性直接决定了当下是要走更新流程,还是应该排队等待;如果轮到执行,就调用 batchedUpdates 方法来直接发起更新流程。由此可以推测,batchingStrategy 或许正是 React 内部专门用于管控批量更新的对象。function enqueueUpdate(component) {
+ ensureInjected();
+ // 注意这一句是问题的关键,isBatchingUpdates标识着当前是否处于批量创建/更新组件的阶段
+ if (!batchingStrategy.isBatchingUpdates) {
+ // 若当前没有处于批量创建/更新组件的阶段,则立即更新组件
+ batchingStrategy.batchedUpdates(enqueueUpdate, component);
+ return;
+ }
+ // 否则,先把组件塞入 dirtyComponents 队列里,让它“再等等”
+ dirtyComponents.push(component);
+ if (component._updateBatchNumber == null) {
+ component._updateBatchNumber = updateBatchNumber + 1;
+ }
+}
+注意:batchingStrategy 对象可以理解为“锁管理器”。这里的“锁”,是指 React 全局唯一的 isBatchingUpdates 变量,isBatchingUpdates 的初始值是 false,意味着“当前并未进行任何批量更新操作”。每当 React 调用 batchedUpdate 去执行更新动作时,会先把这个锁给“锁上”(置为 true),表明“现在正处于批量更新过程中”。当锁被“锁上”的时候,任何需要更新的组件都只能暂时进入 dirtyComponents 里排队等候下一次的批量更新,而不能随意“插队”。此处体现的“任务锁”的思想,是 React 面对大量状态仍然能够实现有序分批处理的基石。
(1)React中setState后发生了什么
+在代码中调用setState函数之后,React 会将传入的参数对象与组件当前的状态合并,然后触发调和过程(Reconciliation)。经过调和过程,React 会以相对高效的方式根据新的状态构建 React 元素树并且着手重新渲染整个UI界面。
+在 React 得到元素树之后,React 会自动计算出新的树与老树的节点差异,然后根据差异对界面进行最小化重渲染。在差异计算算法中,React 能够相对精确地知道哪些位置发生了改变以及应该如何改变,这就保证了按需更新,而不是全部重新渲染。
+如果在短时间内频繁setState。React会将state的改变压入栈中,在合适的时机,批量更新state和视图,达到提高性能的效果。
+(2)setState 是同步还是异步的
+假如所有setState是同步的,意味着每执行一次setState时(有可能一个同步代码中,多次setState),都重新vnode diff + dom修改,这对性能来说是极为不好的。如果是异步,则可以把一个同步代码中的多个setState合并成一次组件更新。所以默认是异步的,但是在一些情况下是同步的。
+setState 并不是单纯同步/异步的,它的表现会因调用场景的不同而不同。在源码中,通过 isBatchingUpdates 来判断setState 是先存进 state 队列还是直接更新,如果值为 true 则执行异步操作,为 false 则直接更新。
+一般认为,做异步设计是为了性能优化、减少渲染次数:
+setState设计为异步,可以显著的提升性能。如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的;最好的办法应该是获取到多个更新,之后进行批量更新;state,但是还没有执行render函数,那么state和props不能保持同步。state和props不能保持一致性,会在开发中产生很多的问题;调用 setState 时,组件的 state 并不会立即改变, setState 只是把要修改的 state 放入一个队列, React 会优化真正的执行时机,并出于性能原因,会将 React 事件处理程序中的多次React 事件处理程序中的多次 setState 的状态修改合并成一次状态修改。 最终更新只产生一次组件及其子组件的重新渲染,这对于大型应用程序中的性能提升至关重要。
this.setState({
+ count: this.state.count + 1 ===> 入队,[count+1的任务]
+});
+this.setState({
+ count: this.state.count + 1 ===> 入队,[count+1的任务,count+1的任务]
+});
+ ↓
+ 合并 state,[count+1的任务]
+ ↓
+ 执行 count+1的任务
+需要注意的是,只要同步代码还在执行,“攒起来”这个动作就不会停止。(注:这里之所以多次 +1 最终只有一次生效,是因为在同一个方法中多次 setState 的合并动作不是单纯地将更新累加。比如这里对于相同属性的设置,React 只会为其保留最后一次的更新)。
+通过实现组件的getDefaultProps,对属性设置默认值(ES5的写法):
+var ShowTitle = React.createClass({
+ getDefaultProps:function(){
+ return{
+ title : "React"
+ }
+ },
+ render : function(){
+ return <h1>{this.props.title}</h1>
+ }
+});
+setState 的第二个参数是一个可选的回调函数。这个回调函数将在组件重新渲染后执行。等价于在 componentDidUpdate 生命周期内执行。通常建议使用 componentDidUpdate 来代替此方式。在这个回调函数中你可以拿到更新后 state 的值:
this.setState({
+ key1: newState1,
+ key2: newState2,
+ ...
+}, callback) // 第二个参数是 state 更新完成后的回调函数
+(1)setState() +setState()用于设置状态对象,其语法如下:
+setState(object nextState[, function callback])
+合并nextState和当前state,并重新渲染组件。setState是React事件处理函数中和请求回调函数中触发UI更新的主要方法。
+(2)replaceState() +replaceState()方法与setState()类似,但是方法只会保留nextState中状态,原state不在nextState中的状态都会被删除。其语法如下:
+replaceState(object nextState[, function callback])
+总结: setState 是修改其中的部分状态,相当于 Object.assign,只是覆盖,不会减少原来的状态。而replaceState 是完全替换原来的状态,相当于赋值,将原来的 state 替换为另一个对象,如果新状态属性减少,那么 state 中就没有这个状态了。
+this.state通常是用来初始化state的,this.setState是用来修改state值的。如果初始化了state之后再使用this.state,之前的state会被覆盖掉,如果使用this.setState,只会替换掉相应的state值。所以,如果想要修改state的值,就需要使用setState,而不能直接修改state,直接修改state之后页面是不会更新的。
+通过connect和mapStateToProps将state注入到组件中:
+import { connect } from 'react-redux'
+import { setVisibilityFilter } from '@/reducers/Todo/actions'
+import Link from '@/containers/Todo/components/Link'
+
+const mapStateToProps = (state, ownProps) => ({
+ active: ownProps.filter === state.visibilityFilter
+})
+
+const mapDispatchToProps = (dispatch, ownProps) => ({
+ setFilter: () => {
+ dispatch(setVisibilityFilter(ownProps.filter))
+ }
+})
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(Link)
+上面代码中,active就是注入到Link组件中的状态。 mapStateToProps(state,ownProps)中带有两个参数,含义是∶
+reducer 到组件经历的过程:
+高阶组件实现源码∶
+import React from 'react'
+import PropTypes from 'prop-types'
+
+// 高阶组件 contect
+export const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => {
+ class Connect extends React.Component {
+ // 通过对context调用获取store
+ static contextTypes = {
+ store: PropTypes.object
+ }
+
+ constructor() {
+ super()
+ this.state = {
+ allProps: {}
+ }
+ }
+
+ // 第一遍需初始化所有组件初始状态
+ componentWillMount() {
+ const store = this.context.store
+ this._updateProps()
+ store.subscribe(() => this._updateProps()); // 加入_updateProps()至store里的监听事件列表
+ }
+
+ // 执行action后更新props,使组件可以更新至最新状态(类似于setState)
+ _updateProps() {
+ const store = this.context.store;
+ let stateProps = mapStateToProps ?
+ mapStateToProps(store.getState(), this.props) : {} // 防止 mapStateToProps 没有传入
+ let dispatchProps = mapDispatchToProps ?
+ mapDispatchToProps(store.dispatch, this.props) : {
+ dispatch: store.dispatch
+ } // 防止 mapDispatchToProps 没有传入
+ this.setState({
+ allProps: {
+ ...stateProps,
+ ...dispatchProps,
+ ...this.props
+ }
+ })
+ }
+
+ render() {
+ return <WrappedComponent {...this.state.allProps} />
+ }
+ }
+ return Connect
+}
+(1)props
+props是一个从外部传进组件的参数,主要作为就是从父组件向子组件传递数据,它具有可读性和不变性,只能通过外部组件主动传入新的props来重新渲染子组件,否则子组件的props以及展现形式不会改变。
+(2)state
+state的主要作用是用于组件保存、控制以及修改自己的状态,它只能在constructor中初始化,它算是组件的私有属性,不可通过外部访问和修改,只能通过组件内部的this.setState来修改,修改state属性会导致组件的重新渲染。
+(3)区别
+this.props是组件之间沟通的一个接口,原则上来讲,它只能从父组件流向子组件。React具有浓重的函数式编程的思想。
提到函数式编程就要提一个概念:纯函数。它有几个特点:
+this.props就是汲取了纯函数的思想。props的不可以变性就保证的相同的输入,页面显示的内容是一样的,并且不会产生副作用
在一个组件传入的props更新时重新渲染该组件常用的方法是在componentWillReceiveProps中将新的props更新到组件的state中(这种state被成为派生状态(Derived State)),从而实现重新渲染。React 16.3中还引入了一个新的钩子函数getDerivedStateFromProps来专门实现这一需求。
(1)componentWillReceiveProps(已废弃)
+在react的componentWillReceiveProps(nextProps)生命周期中,可以在子组件的render函数执行前,通过this.props获取旧的属性,通过nextProps获取新的props,对比两次props是否相同,从而更新子组件自己的state。
+这样的好处是,可以将数据请求放在这里进行执行,需要传的参数则从componentWillReceiveProps(nextProps)中获取。而不必将所有的请求都放在父组件中。于是该请求只会在该组件渲染时才会发出,从而减轻请求负担。
+(2)getDerivedStateFromProps(16.3引入)
+这个生命周期函数是为了替代componentWillReceiveProps存在的,所以在需要使用componentWillReceiveProps时,就可以考虑使用getDerivedStateFromProps来进行替代。
两者的参数是不相同的,而getDerivedStateFromProps是一个静态函数,也就是这个函数不能通过this访问到class的属性,也并不推荐直接访问属性。而是应该通过参数提供的nextProps以及prevState来进行判断,根据新传入的props来映射到state。
需要注意的是,如果props传入的内容不需要影响到你的state,那么就需要返回一个null,这个返回值是必须的,所以尽量将其写到函数的末尾:
+static getDerivedStateFromProps(nextProps, prevState) {
+ const {type} = nextProps;
+ // 当传入的type发生变化的时候,更新state
+ if (type !== prevState.type) {
+ return {
+ type,
+ };
+ }
+ // 否则,对于state不进行任何操作
+ return null;
+}
+React为我们提供了PropTypes以供验证使用。当我们向Props传入的数据无效(向Props传入的数据类型和验证的数据类型不符)就会在控制台发出警告信息。它可以避免随着应用越来越复杂从而出现的问题。并且,它还可以让程序变得更易读。
+import PropTypes from 'prop-types';
+
+class Greeting extends React.Component {
+ render() {
+ return (
+ <h1>Hello, {this.props.name}</h1>
+ );
+ }
+}
+
+Greeting.propTypes = {
+ name: PropTypes.string
+};
+当然,如果项目汇中使用了TypeScript,那么就可以不用PropTypes来校验,而使用TypeScript定义接口来校验props。
+React 通常将组件生命周期分为三个阶段:
+挂载阶段组件被创建,然后组件实例插入到 DOM 中,完成组件的第一次渲染,该过程只会发生一次,在此阶段会依次调用以下这些方法:
+组件的构造函数,第一个被执行,若没有显式定义它,会有一个默认的构造函数,但是若显式定义了构造函数,我们必须在构造函数中执行 super(props),否则无法在构造函数中拿到this。
如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数Constructor。
+constructor中通常只做两件事:
+constructor(props) {
+ super(props);
+ // 不要在构造函数中调用 setState,可以直接给 state 设置初始值
+ this.state = { counter: 0 }
+ this.handleClick = this.handleClick.bind(this)
+}
+static getDerivedStateFromProps(props, state)
+这是个静态方法,所以不能在这个函数里使用 this,有两个参数 props 和 state,分别指接收到的新参数和当前组件的 state 对象,这个函数会返回一个对象用来更新当前的 state 对象,如果不需要更新可以返回 null。
该函数会在装载时,接收到新的 props 或者调用了 setState 和 forceUpdate 时被调用。如当接收到新的属性想修改 state ,就可以使用。
// 当 props.counter 变化时,赋值给 state
+class App extends React.Component {
+ constructor(props) {
+ super(props)
+ this.state = {
+ counter: 0
+ }
+ }
+ static getDerivedStateFromProps(props, state) {
+ if (props.counter !== state.counter) {
+ return {
+ counter: props.counter
+ }
+ }
+ return null
+ }
+
+ handleClick = () => {
+ this.setState({
+ counter: this.state.counter + 1
+ })
+ }
+ render() {
+ return (
+ <div>
+ <h1 onClick={this.handleClick}>Hello, world!{this.state.counter}</h1>
+ </div>
+ )
+ }
+}
+现在可以显式传入 counter ,但是这里有个问题,如果想要通过点击实现 state.counter 的增加,但这时会发现值不会发生任何变化,一直保持 props 传进来的值。这是由于在 React 16.4^ 的版本中 setState 和 forceUpdate 也会触发这个生命周期,所以当组件内部 state 变化后,就会重新走这个方法,同时会把 state 值赋值为 props 的值。因此需要多加一个字段来记录之前的 props 值,这样就会解决上述问题。具体如下:
// 这里只列出需要变化的地方
+class App extends React.Component {
+ constructor(props) {
+ super(props)
+ this.state = {
+ // 增加一个 preCounter 来记录之前的 props 传来的值
+ preCounter: 0,
+ counter: 0
+ }
+ }
+ static getDerivedStateFromProps(props, state) {
+ // 跟 state.preCounter 进行比较
+ if (props.counter !== state.preCounter) {
+ return {
+ counter: props.counter,
+ preCounter: props.counter
+ }
+ }
+ return null
+ }
+ handleClick = () => {
+ this.setState({
+ counter: this.state.counter + 1
+ })
+ }
+ render() {
+ return (
+ <div>
+ <h1 onClick={this.handleClick}>Hello, world!{this.state.counter}</h1>
+ </div>
+ )
+ }
+}
+render是React 中最核心的方法,一个组件中必须要有这个方法,它会根据状态 state 和属性 props 渲染组件。这个函数只做一件事,就是返回需要渲染的内容,所以不要在这个函数内做其他业务逻辑,通常调用该方法会返回以下类型中一个:
componentDidMount()会在组件挂载后(插入 DOM 树中)立即调。该阶段通常进行以下操作:
+如果在 componentDidMount 中调用 setState ,就会触发一次额外的渲染,多调用了一次 render 函数,由于它是在浏览器刷新屏幕前执行的,所以用户对此是没有感知的,但是我应当避免这样使用,这样会带来一定的性能问题,尽量是在 constructor 中初始化 state 对象。
在组件装载之后,将计数数字变为1:
+class App extends React.Component {
+ constructor(props) {
+ super(props)
+ this.state = {
+ counter: 0
+ }
+ }
+ componentDidMount () {
+ this.setState({
+ counter: 1
+ })
+ }
+ render () {
+ return (
+ <div className="counter">
+ counter值: { this.state.counter }
+ </div>
+ )
+ }
+}
+当组件的 props 改变了,或组件内部调用了 setState/forceUpdate,会触发更新重新渲染,这个过程可能会发生多次。这个阶段会依次调用下面这些方法:
shouldComponentUpdate(nextProps, nextState)
+在说这个生命周期函数之前,来看两个问题:
+this.setState({number: this.state.number})
+第一个问题答案是 会 ,第二个问题如果是父组件重新渲染时,不管传入的 props 有没有变化,都会引起子组件的重新渲染。
+那么有没有什么方法解决在这两个场景下不让组件重新渲染进而提升性能呢?这个时候 shouldComponentUpdate 登场了,这个生命周期函数是用来提升速度的,它是在重新渲染组件开始前触发的,默认返回 true,可以比较 this.props 和 nextProps ,this.state 和 nextState 值是否变化,来确认返回 true 或者 false。当返回 false 时,组件的更新过程停止,后续的 render、componentDidUpdate 也不会被调用。
注意: 添加 shouldComponentUpdate 方法时,不建议使用深度相等检查(如使用 JSON.stringify()),因为深比较效率很低,可能会比重新渲染组件效率还低。而且该方法维护比较困难,建议使用该方法会产生明显的性能提升时使用。
getSnapshotBeforeUpdate(prevProps, prevState)
+这个方法在 render 之后,componentDidUpdate 之前调用,有两个参数 prevProps 和 prevState,表示更新之前的 props 和 state,这个函数必须要和 componentDidUpdate 一起使用,并且要有一个返回值,默认是 null,这个返回值作为第三个参数传给 componentDidUpdate。
componentDidUpdate() 会在更新后会被立即调用,首次渲染不会执行此方法。 该阶段通常进行以下操作:
+componentDidUpdate(prevProps, prevState, snapshot){}
+该方法有三个参数:
+卸载阶段只有一个生命周期函数,componentWillUnmount() 会在组件卸载及销毁之前直接调用。在此方法中执行必要的清理操作:
+这个生命周期在一个组件被卸载和销毁之前被调用,因此你不应该再这个方法中使用 setState,因为组件一旦被卸载,就不会再装载,也就不会重新渲染。
componentDidCatch(error, info),此生命周期在后代组件抛出错误后被调用。 它接收两个参数∶
+React常见的生命周期如下:
+
+React常见生命周期的过程大致如下:
React主要生命周期总结:
+被废弃的三个函数都是在render之前,因为fber的出现,很可能因为高优先级任务的出现而打断现有任务导致它们会被执行多次。另外的一个原因则是,React想约束使用者,好的框架能够让人不得已写出容易维护和扩展的代码,这一点又是从何谈起,可以从新增加以及即将废弃的生命周期分析入手
+1) componentWillMount
+首先这个函数的功能完全可以使用componentDidMount和 constructor来代替,异步获取的数据的情况上面已经说明了,而如果抛去异步获取数据,其余的即是初始化而已,这些功能都可以在constructor中执行,除此之外,如果在 willMount 中订阅事件,但在服务端这并不会执行 willUnMount事件,也就是说服务端会导致内存泄漏所以componentWilIMount完全可以不使用,但使用者有时候难免因为各 种各样的情况在 componentWilMount中做一些操作,那么React为了约束开发者,干脆就抛掉了这个API
+2) componentWillReceiveProps
+在老版本的 React 中,如果组件自身的某个 state 跟其 props 密切相关的话,一直都没有一种很优雅的处理方式去更新 state,而是需要在 componentWilReceiveProps 中判断前后两个 props 是否相同,如果不同再将新的 props更新到相应的 state 上去。这样做一来会破坏 state 数据的单一数据源,导致组件状态变得不可预测,另一方面也会增加组件的重绘次数。类似的业务需求也有很多,如一个可以横向滑动的列表,当前高亮的 Tab 显然隶属于列表自身的时,根据传入的某个值,直接定位到某个 Tab。为了解决这些问题,React引入了第一个新的生命周期:getDerivedStateFromProps。它有以下的优点∶
+3) componentWillUpdate
+与 componentWillReceiveProps 类似,许多开发者也会在 componentWillUpdate 中根据 props 的变化去触发一些回调 。 但不论是 componentWilReceiveProps 还 是 componentWilUpdate,都有可能在一次更新中被调用多次,也就是说写在这里的回调函数也有可能会被调用多次,这显然是不可取的。与 componentDidMount 类 似, componentDidUpdate 也不存在这样的问题,一次更新中 componentDidUpdate 只会被调用一次,所以将原先写在 componentWillUpdate 中 的 回 调 迁 移 至 componentDidUpdate 就可以解决这个问题。
+另外一种情况则是需要获取DOM元素状态,但是由于在fber中,render可打断,可能在wilMount中获取到的元素状态很可能与实际需要的不同,这个通常可以使用第二个新增的生命函数的解决 getSnapshotBeforeUpdate(prevProps, prevState)
+4) getSnapshotBeforeUpdate(prevProps, prevState)
+返回的值作为componentDidUpdate的第三个参数。与willMount不同的是,getSnapshotBeforeUpdate会在最终确定的render执行之前执行,也就是能保证其获取到的元素状态与didUpdate中获取到的元素状态相同。官方参考代码:
+class ScrollingList extends React.Component {
+ constructor(props) {
+ super(props);
+ this.listRef = React.createRef();
+ }
+
+ getSnapshotBeforeUpdate(prevProps, prevState) {
+ // 我们是否在 list 中添加新的 items ?
+ // 捕获滚动位置以便我们稍后调整滚动位置。
+ if (prevProps.list.length < this.props.list.length) {
+ const list = this.listRef.current;
+ return list.scrollHeight - list.scrollTop;
+ }
+ return null;
+ }
+
+ componentDidUpdate(prevProps, prevState, snapshot) {
+ // 如果我们 snapshot 有值,说明我们刚刚添加了新的 items,
+ // 调整滚动位置使得这些新 items 不会将旧的 items 推出视图。
+ //(这里的 snapshot 是 getSnapshotBeforeUpdate 的返回值)
+ if (snapshot !== null) {
+ const list = this.listRef.current;
+ list.scrollTop = list.scrollHeight - snapshot;
+ }
+ }
+
+ render() {
+ return (
+ <div ref={this.listRef}>{/* ...contents... */}</div>
+ );
+ }
+}
+在getDerivedStateFromProps中进行处理。
+这个生命周期函数是为了替代componentWillReceiveProps存在的,所以在需要使用componentWillReceiveProps时,就可以考虑使用getDerivedStateFromProps来进行替代。
两者的参数是不相同的,而getDerivedStateFromProps是一个静态函数,也就是这个函数不能通过this访问到class的属性,也并不推荐直接访问属性。而是应该通过参数提供的nextProps以及prevState来进行判断,根据新传入的props来映射到state。
需要注意的是,如果props传入的内容不需要影响到你的state,那么就需要返回一个null,这个返回值是必须的,所以尽量将其写到函数的末尾:
+static getDerivedStateFromProps(nextProps, prevState) {
+ const {type} = nextProps;
+ // 当传入的type发生变化的时候,更新state
+ if (type !== prevState.type) {
+ return {
+ type,
+ };
+ }
+ // 否则,对于state不进行任何操作
+ return null;
+}
+react的父级组件的render函数重新渲染会引起子组件的render方法的重新渲染。但是,有的时候子组件的接受父组件的数据没有变动。子组件render的执行会影响性能,这时就可以使用shouldComponentUpdate来解决这个问题。
+使用方法如下:
+shouldComponentUpdate(nexrProps) {
+ if (this.props.num === nexrProps.num) {
+ return false
+ }
+ return true;
+}
+shouldComponentUpdate提供了两个参数nextProps和nextState,表示下一次props和一次state的值,当函数返回false时候,render()方法不执行,组件也就不会渲染,返回true时,组件照常重渲染。此方法就是拿当前props中值和下一次props中的值进行对比,数据相等时,返回false,反之返回true。
+需要注意,在进行新旧对比的时候,是**浅对比,**也就是说如果比较的数据时引用数据类型,只要数据的引用的地址没变,即使内容变了,也会被判定为true。
+面对这个问题,可以使用如下方法进行解决: +(1)使用setState改变数据之前,先采用ES6中assgin进行拷贝,但是assgin只深拷贝的数据的第一层,所以说不是最完美的解决办法:
+const o2 = Object.assign({},this.state.obj)
+ o2.student.count = '00000';
+ this.setState({
+ obj: o2,
+ })
+(2)使用JSON.parse(JSON.stringfy())进行深拷贝,但是遇到数据为undefined和函数时就会错。
+const o2 = JSON.parse(JSON.stringify(this.state.obj))
+ o2.student.count = '00000';
+ this.setState({
+ obj: o2,
+ })
+state 更新流程:
+
+这个过程当中涉及的函数:
++注意:此方法仅作为性能优化的方式而存在。不要企图依靠此方法来“阻止”渲染,因为这可能会产生 bug。应该考虑使用内置的 PureComponent 组件,而不是手动编写
+shouldComponentUpdate()
+props 更新流程:
+
+相对于 state 更新,props 更新后唯一的区别是增加了对 componentWillReceiveProps 的调用。关于 componentWillReceiveProps,需要知道这些事情:
对于异步请求,最好放在componentDidMount中去操作,对于同步的状态改变,可以放在componentWillMount中,一般用的比较少。
+如果认为在componentWillMount里发起请求能提早获得结果,这种想法其实是错误的,通常componentWillMount比componentDidMount早不了多少微秒,网络上任何一点延迟,这一点差异都可忽略不计。
+react的生命周期: constructor() -> componentWillMount() -> render() -> componentDidMount()
+上面这些方法的调用是有次序的,由上而下依次调用。
+总结:
+关于 React16 开始应用的新生命周期:
+
+可以看出,React16 自上而下地对生命周期做了另一种维度的解读:
与此同时,新的生命周期在流程方面,仍然遵循“挂载”、“更新”、“卸载”这三个广义的划分方式。它们分别对应到:
+React组件间通信常见的几种情况:
+父组件向子组件通信:父组件通过 props 向子组件传递需要的信息。
+// 子组件: Child
+const Child = props =>{
+ return <p>{props.name}</p>
+}
+// 父组件 Parent
+const Parent = ()=>{
+ return <Child name="react"></Child>
+}
+子组件向父组件通信:: props+回调的方式。
+// 子组件: Child
+const Child = props =>{
+ const cb = msg =>{
+ return ()=>{
+ props.callback(msg)
+ }
+ }
+ return (
+ <button onClick={cb("你好!")}>你好</button>
+ )
+}
+// 父组件 Parent
+class Parent extends Component {
+ callback(msg){
+ console.log(msg)
+ }
+ render(){
+ return <Child callback={this.callback.bind(this)}></Child>
+ }
+}
+父组件向子组件的子组件通信,向更深层子组件通信:
+// context方式实现跨级组件通信
+// Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据
+const BatteryContext = createContext();
+// 子组件的子组件
+class GrandChild extends Component {
+ render(){
+ return (
+ <BatteryContext.Consumer>
+ {
+ color => <h1 style={{"color":color}}>我是红色的:{color}</h1>
+ }
+ </BatteryContext.Consumer>
+ )
+ }
+}
+// 子组件
+const Child = () =>{
+ return (
+ <GrandChild/>
+ )
+}
+// 父组件
+class Parent extends Component {
+ state = {
+ color:"red"
+ }
+ render(){
+ const {color} = this.state
+ return (
+ <BatteryContext.Provider value={color}>
+ <Child></Child>
+ </BatteryContext.Provider>
+ )
+ }
+}
+
+即没有任何包含关系的组件,包括兄弟组件以及不在同一个父级中的非兄弟组件。
+客户端路由实现的思想:
+hashchange事件,感知 hash 的变化
+history.go() 等 APIreact-router 实现的思想:
+history 库来实现上述不同的客户端路由实现思想,并且能够保存历史记录等,磨平浏览器差异,上层无感知(1)使用<Route> 组件
路由匹配是通过比较 <Route> 的 path 属性和当前地址的 pathname 来实现的。当一个 <Route> 匹配成功时,它将渲染其内容,当它不匹配时就会渲染 null。没有路径的 <Route> 将始终被匹配。
// when location = { pathname: '/about' }
+<Route path='/about' component={About}/> // renders <About/>
+<Route path='/contact' component={Contact}/> // renders null
+<Route component={Always}/> // renders <Always/>
+(2)结合使用 <Switch> 组件和 <Route> 组件
<Switch> 用于将 <Route> 分组。
<Switch>
+ <Route exact path="/" component={Home} />
+ <Route path="/about" component={About} />
+ <Route path="/contact" component={Contact} />
+</Switch>
+<Switch> 不是分组 <Route> 所必须的,但他通常很有用。 一个 <Switch> 会遍历其所有的子 <Route>元素,并仅渲染与当前地址匹配的第一个元素。
(3)使用 <Link>、 <NavLink>、<Redirect> 组件
<Link> 组件来在你的应用程序中创建链接。无论你在何处渲染一个<Link> ,都会在应用程序的 HTML 中渲染锚(<a>)。
<Link to="/">Home</Link>
+// <a href='/'>Home</a>
+是一种特殊类型的 当它的 to属性与当前地址匹配时,可以将其定义为"活跃的"。
+// location = { pathname: '/react' }
+<NavLink to="/react" activeClassName="hurray">
+ React
+</NavLink>
+// <a href='/react' className='hurray'>React</a>
+当我们想强制导航时,可以渲染一个<Redirect>,当一个<Redirect>渲染时,它将使用它的to属性进行定向。
使用<Redirect>组件实现路由的重定向:
<Switch>
+ <Redirect from='/users/:id' to='/users/profile/:id'/>
+ <Route path='/users/profile/:id' component={Profile}/>
+</Switch>
+当请求 /users/:id 被重定向去 '/users/profile/:id':
from: string:需要匹配的将要被重定向路径。to: string:重定向的 URL 字符串to: object:重定向的 location 对象push: bool:若为真,重定向操作将会把新地址加入到访问历史记录里面,并且无法回退到前面的页面。从最终渲染的 DOM 来看,这两者都是链接,都是 标签,区别是∶
+<Link>是react-router 里实现路由跳转的链接,一般配合<Route> 使用,react-router接管了其默认的链接跳转行为,区别于传统的页面跳转,<Link> 的“跳转”行为只会触发相匹配的<Route>对应的页面内容更新,而不会刷新整个页面。
<Link>做了3件事情:
<a>标签就是普通的超链接了,用于从当前页面跳转到href指向的另一 个页面(非锚点情况)。a标签默认事件禁掉之后做了什么才实现了跳转?
+let domArr = document.getElementsByTagName('a')
+[...domArr].forEach(item=>{
+ item.addEventListener('click',function () {
+ location.href = this.href
+ })
+})
+(1)获取URL的参数
+路由配置还是普通的配置,如:'admin',传参方式如:'admin?id='1111''。通过this.props.location.search获取url获取到一个字符串'?id='1111'
+可以用url,qs,querystring,浏览器提供的api URLSearchParams对象或者自己封装的方法去解析出id的值。
路由需要配置成动态路由:如path='/admin/:id',传参方式,如'admin/111'。通过this.props.match.params.id 取得url中的动态路由id部分的值,除此之外还可以通过useParams(Hooks)来获取
传参方式如:在Link组件的to属性中可以传递对象{pathname:'/admin',query:'111',state:'111'};。通过this.props.location.state或this.props.location.query来获取即可,传递的参数可以是对象、数组等,但是存在缺点就是只要刷新页面,参数就会丢失。
(2)获取历史对象
+import { useHistory } from "react-router-dom";
+let history = useHistory();
+2.使用this.props.history获取历史对象
+let history = this.props.history;
+当路由变化时,即组件的props发生了变化,会调用componentWillReceiveProps等生命周期钩子。那需要做的只是: 当路由改变时,根据路由,也去请求数据:
+class NewsList extends Component {
+ componentDidMount () {
+ this.fetchData(this.props.location);
+ }
+
+ fetchData(location) {
+ const type = location.pathname.replace('/', '') || 'top'
+ this.props.dispatch(fetchListData(type))
+ }
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.location.pathname != this.props.location.pathname) {
+ this.fetchData(nextProps.location);
+ }
+ }
+ render () {
+ ...
+ }
+}
+利用生命周期componentWillReceiveProps,进行重新render的预处理操作。
+React-Router 支持使用 hash(对应 HashRouter)和 browser(对应 BrowserRouter) 两种路由规则, react-router-dom 提供了 BrowserRouter 和 HashRouter 两个组件来实现应用的 UI 和 URL 同步:
+(1)BrowserRouter
+它使用 HTML5 提供的 history API(pushState、replaceState 和 popstate 事件)来保持 UI 和 URL 的同步。由此可以看出,BrowserRouter 是使用 HTML 5 的 history API 来控制路由跳转的:
+<BrowserRouter
+ basename={string}
+ forceRefresh={bool}
+ getUserConfirmation={func}
+ keyLength={number}
+/>
+其中的属性如下:
+<BrowserRouter basename="/calendar">
+ <Link to="/today" />
+</BrowserRouter>
+等同于
+<a href="/calendar/today" />
+// 这是默认的确认函数
+const getConfirmation = (message, callback) => {
+ const allowTransition = window.confirm(message);
+ callback(allowTransition);
+}
+<BrowserRouter getUserConfirmation={getConfirmation} />
+++需要配合
+<Prompt>一起使用。
(2)HashRouter
+使用 URL 的 hash 部分(即 window.location.hash)来保持 UI 和 URL 的同步。由此可以看出,HashRouter 是通过 URL 的 hash 属性来控制路由跳转的:
+<HashRouter
+ basename={string}
+ getUserConfirmation={func}
+ hashType={string}
+/>
+其参数如下:
+BrowserRouter 功能一样;Switch 通常被用来包裹 Route,用于渲染与路径匹配的第一个子 <Route> 或 <Redirect>,它里面不能放其他元素。
假如不加 <Switch> :
import { Route } from 'react-router-dom'
+
+<Route path="/" component={Home}></Route>
+<Route path="/login" component={Login}></Route>
+Route 组件的 path 属性用于匹配路径,因为需要匹配 / 到 Home,匹配 /login 到 Login,所以需要两个 Route,但是不能这么写。这样写的话,当 URL 的 path 为 “/login” 时,<Route path="/" />和<Route path="/login" /> 都会被匹配,因此页面会展示 Home 和 Login 两个组件。这时就需要借助 <Switch> 来做到只显示一个匹配组件:
import { Switch, Route} from 'react-router-dom'
+
+<Switch>
+ <Route path="/" component={Home}></Route>
+ <Route path="/login" component={Login}></Route>
+</Switch>
+此时,再访问 “/login” 路径时,却只显示了 Home 组件。这是就用到了exact属性,它的作用就是精确匹配路径,经常与<Switch> 联合使用。只有当 URL 和该 <Route> 的 path 属性完全一致的情况下才能匹配上:
import { Switch, Route} from 'react-router-dom'
+
+<Switch>
+ <Route exact path="/" component={Home}></Route>
+ <Route exact path="/login" component={Login}></Route>
+</Switch>
+注: 由于文章字数限制,剩余内容请见下篇。
\ No newline at end of file diff --git a/src/content/notes/frontend/react-part-2.html b/src/content/notes/frontend/react-part-2.html new file mode 100644 index 0000000..9d8a22b --- /dev/null +++ b/src/content/notes/frontend/react-part-2.html @@ -0,0 +1,1479 @@ +React是视图层框架。Redux是一个用来管理数据状态和UI状态的JavaScript应用工具。随着JavaScript单页应用(SPA)开发日趋复杂, JavaScript需要管理比任何时候都要多的state(状态), Redux就是降低管理难度的。(Redux支持React、Angular、jQuery甚至纯JavaScript)。
+在 React 中,UI 以组件的形式来搭建,组件之间可以嵌套组合。但 React 中组件间通信的数据流是单向的,顶层组件可以通过 props 属性向下层组件传递数据,而下层组件不能向上层组件传递数据,兄弟组件之间同样不能。这样简单的单向数据流支撑起了 React 中的数据可控性。
+当项目越来越大的时候,管理数据的事件或回调函数将越来越多,也将越来越不好管理。管理不断变化的 state 非常困难。如果一个 model 的变化会引起另一个 model 变化,那么当 view 变化时,就可能引起对应 model 以及另一个 model 的变化,依次地,可能会引起另一个 view 的变化。直至你搞不清楚到底发生了什么。state 在什么时候,由于什么原因,如何变化已然不受控制。 当系统变得错综复杂的时候,想重现问题或者添加新功能就会变得举步维艰。如果这还不够糟糕,考虑一些来自前端开发领域的新需求,如更新调优、服务端渲染、路由跳转前请求数据等。state 的管理在大项目中相当复杂。
+Redux 提供了一个叫 store 的统一仓储库,组件通过 dispatch 将 state 直接传入store,不用通过其他的组件。并且组件通过 subscribe 从 store获取到 state 的改变。使用了 Redux,所有的组件都可以从 store 中获取到所需的 state,他们也能从store 获取到 state 的改变。这比组件之间互相传递数据清晰明朗的多。
+主要解决的问题: +单纯的Redux只是一个状态机,是没有UI呈现的,react- redux作用是将Redux的状态机和React的UI呈现绑定在一起,当你dispatch action改变state的时候,会自动更新页面。
+(1)原理 +Redux源码主要分为以下几个模块文件
+const actionTypes = {
+ ADD: 'ADD',
+ CHANGEINFO: 'CHANGEINFO',
+}
+
+const initState = {
+ info: '初始化',
+}
+
+export default function initReducer(state=initState, action) {
+ switch(action.type) {
+ case actionTypes.CHANGEINFO:
+ return {
+ ...state,
+ info: action.preload.info || '',
+ }
+ default:
+ return { ...state };
+ }
+}
+
+export default function createStore(reducer, initialState, middleFunc) {
+
+ if (initialState && typeof initialState === 'function') {
+ middleFunc = initialState;
+ initialState = undefined;
+ }
+
+ let currentState = initialState;
+
+ const listeners = [];
+
+ if (middleFunc && typeof middleFunc === 'function') {
+ // 封装dispatch
+ return middleFunc(createStore)(reducer, initialState);
+ }
+
+ const getState = () => {
+ return currentState;
+ }
+
+ const dispatch = (action) => {
+ currentState = reducer(currentState, action);
+
+ listeners.forEach(listener => {
+ listener();
+ })
+ }
+
+ const subscribe = (listener) => {
+ listeners.push(listener);
+ }
+
+ return {
+ getState,
+ dispatch,
+ subscribe
+ }
+}
+(2)工作流程
+通俗点解释:
+以 store 为核心,可以把它看成数据存储中心,但是他要更改数据的时候不能直接修改,数据修改更新的角色由Reducers来担任,store只做存储,中间人,当Reducers的更新完成以后会通过store的订阅来通知react component,组件把新的状态重新获取渲染,组件中也能主动发送action,创建action后这个动作是不会执行的,所以要dispatch这个action,让store通过reducers去做更新React Component 就是react的每个组件。
+可以在 componentDidmount 中直接进⾏请求⽆须借助redux。但是在⼀定规模的项⽬中,上述⽅法很难进⾏异步流的管理,通常情况下我们会借助redux的异步中间件进⾏异步处理。redux异步流中间件其实有很多,当下主流的异步中间件有两种redux-thunk、redux-saga。
+(1)使用react-thunk中间件
+redux-thunk优点:
+redux-thunk缺陷:
+使用步骤:
+import {createStore, applyMiddleware, compose} from 'redux';
+import reducer from './reducer';
+import thunk from 'redux-thunk'
+
+// 设置调试工具
+const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;
+// 设置中间件
+const enhancer = composeEnhancers(
+ applyMiddleware(thunk)
+);
+
+const store = createStore(reducer, enhancer);
+
+export default store;
+/**
+ 发送get请求,并生成相应action,更新store的函数
+ @param url {string} 请求地址
+ @param func {function} 真正需要生成的action对应的actionCreator
+ @return {function}
+*/
+// dispatch为自动接收的store.dispatch函数
+export const getHttpAction = (url, func) => (dispatch) => {
+ axios.get(url).then(function(res){
+ const action = func(res.data)
+ dispatch(action)
+ })
+}
+componentDidMount(){
+ var action = getHttpAction('/getData', getInitTodoItemAction)
+ // 发送函数类型的action时,该action的函数体会自动执行
+ store.dispatch(action)
+}
+(2)使用redux-saga中间件
+redux-saga优点:
+redux-saga缺陷:
+redux-saga可以捕获action,然后执行一个函数,那么可以把异步代码放在这个函数中,使用步骤如下:
+import {createStore, applyMiddleware, compose} from 'redux';
+import reducer from './reducer';
+import createSagaMiddleware from 'redux-saga'
+import TodoListSaga from './sagas'
+
+const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;
+const sagaMiddleware = createSagaMiddleware()
+
+const enhancer = composeEnhancers(
+ applyMiddleware(sagaMiddleware)
+);
+
+const store = createStore(reducer, enhancer);
+sagaMiddleware.run(TodoListSaga)
+
+export default store;
+import {takeEvery, put} from 'redux-saga/effects'
+import {initTodoList} from './actionCreator'
+import {GET_INIT_ITEM} from './actionTypes'
+import axios from 'axios'
+
+function* func(){
+ try{
+ // 可以获取异步返回数据
+ const res = yield axios.get('/getData')
+ const action = initTodoList(res.data)
+ // 将action发送到reducer
+ yield put(action)
+ }catch(e){
+ console.log('网络请求失败')
+ }
+}
+
+function* mySaga(){
+ // 自动捕获GET_INIT_ITEM类型的action,并执行func
+ yield takeEvery(GET_INIT_ITEM, func)
+}
+
+export default mySaga
+componentDidMount(){
+ const action = getInitTodoItemAction()
+ store.dispatch(action)
+}
+react-redux 数据传输∶ view-->action-->reducer-->store-->view。看下点击事件的数据是如何通过redux传到view上:
+代码示例∶
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { createStore } from 'redux';
+import { Provider, connect } from 'react-redux';
+class App extends React.Component{
+ render(){
+ let { text, click, clickR } = this.props;
+ return(
+ <div>
+ <div>数据:已有人{text}</div>
+ <div onClick={click}>加人</div>
+ <div onClick={clickR}>减人</div>
+ </div>
+ )
+ }
+}
+const initialState = {
+ text:5
+}
+const reducer = function(state,action){
+ switch(action.type){
+ case 'ADD':
+ return {text:state.text+1}
+ case 'REMOVE':
+ return {text:state.text-1}
+ default:
+ return initialState;
+ }
+}
+
+let ADD = {
+ type:'ADD'
+}
+let Remove = {
+ type:'REMOVE'
+}
+
+const store = createStore(reducer);
+
+let mapStateToProps = function (state){
+ return{
+ text:state.text
+ }
+}
+
+let mapDispatchToProps = function(dispatch){
+ return{
+ click:()=>dispatch(ADD),
+ clickR:()=>dispatch(Remove)
+ }
+}
+
+const App1 = connect(mapStateToProps,mapDispatchToProps)(App);
+
+ReactDOM.render(
+ <Provider store = {store}>
+ <App1></App1>
+ </Provider>,document.getElementById('root')
+)
+Redux 的中间件提供的是位于 action 被发起之后,到达 reducer 之前的扩展点,换而言之,原本 view -→> action -> reducer -> store 的数据流加上中间件后变成了 view -> action -> middleware -> reducer -> store ,在这一环节可以做一些"副作用"的操作,如异步请求、打印日志等。
+applyMiddleware源码:
+export default function applyMiddleware(...middlewares) {
+ return createStore => (...args) => {
+ // 利用传入的createStore和reducer和创建一个store
+ const store = createStore(...args)
+ let dispatch = () => {
+ throw new Error()
+ }
+ const middlewareAPI = {
+ getState: store.getState,
+ dispatch: (...args) => dispatch(...args)
+ }
+ // 让每个 middleware 带着 middlewareAPI 这个参数分别执行一遍
+ const chain = middlewares.map(middleware => middleware(middlewareAPI))
+ // 接着 compose 将 chain 中的所有匿名函数,组装成一个新的函数,即新的 dispatch
+ dispatch = compose(...chain)(store.dispatch)
+ return {
+ ...store,
+ dispatch
+ }
+ }
+}
+从applyMiddleware中可以看出∶
+使用redux-Saga +redux-saga是一个管理redux应用异步操作的中间件,用于代替 redux-thunk 的。它通过创建 Sagas 将所有异步操作逻辑存放在一个地方进行集中处理,以此将react中的同步操作与异步操作区分开来,以便于后期的管理与维护。 redux-saga如何处理并发:
+可以让多个 saga 任务并行被 fork 执行。
+import {
+ fork,
+ take
+} from "redux-saga/effects"
+
+const takeEvery = (pattern, saga, ...args) => fork(function*() {
+ while (true) {
+ const action = yield take(pattern)
+ yield fork(saga, ...args.concat(action))
+ }
+})
+takeLatest 不允许多个 saga 任务并行地执行。一旦接收到新的发起的 action,它就会取消前面所有 fork 过的任务(如果这些任务还在执行的话)。 +在处理 AJAX 请求的时候,如果只希望获取最后那个请求的响应, takeLatest 就会非常有用。
+import {
+ cancel,
+ fork,
+ take
+} from "redux-saga/effects"
+
+const takeLatest = (pattern, saga, ...args) => fork(function*() {
+ let lastTask
+ while (true) {
+ const action = yield take(pattern)
+ if (lastTask) {
+ yield cancel(lastTask) // 如果任务已经结束,则 cancel 为空操作
+ }
+ lastTask = yield fork(saga, ...args.concat(action))
+ }
+})
+两者都是存储数据以供后期使用。但是Redux状态更改可回溯——Time travel,数据多了的时候可以很清晰的知道改动在哪里发生,完整的提供了一套状态管理模式。
+随着 JavaScript 单页应用开发日趋复杂,JavaScript 需要管理比任何时候都要多的 state (状态)。 这些 state 可能包括服务器响应、缓存数据、本地生成尚未持久化到服务器的数据,也包括 UI状态,如激活的路由,被选中的标签,是否显示加载动效或者分页器等等。
+管理不断变化的 state 非常困难。如果一个 model 的变化会引起另一个 model 变化,那么当 view 变化时,就可能引起对应 model 以及另一个model 的变化,依次地,可能会引起另一个 view 的变化。直至你搞不清楚到底发生了什么。state 在什么时候,由于什么原因,如何变化已然不受控制。 当系统变得错综复杂的时候,想重现问题或者添加新功能就会变得举步维艰。 +如果这还不够糟糕,考虑一些来自前端开发领域的新需求,如更新调优、服务端渲染、路由跳转前请求数据等等。前端开发者正在经受前所未有的复杂性,难道就这么放弃了吗?当然不是。
+这里的复杂性很大程度上来自于:我们总是将两个难以理清的概念混淆在一起:变化和异步。 可以称它们为曼妥思和可乐。如果把二者分开,能做的很好,但混到一起,就变得一团糟。一些库如 React 视图在视图层禁止异步和直接操作 DOM来解决这个问题。美中不足的是,React 依旧把处理 state 中数据的问题留给了你。Redux就是为了帮你解决这个问题。
+(1)共同点
+(2)区别 +Redux更多的是遵循Flux模式的一种实现,是一个 JavaScript库,它关注点主要是以下几方面∶
+Action∶ 一个JavaScript对象,描述动作相关信息,主要包含type属性和payload属性∶
+o type∶ action 类型; o payload∶ 负载数据;
+Reducer∶ 定义应用状态如何响应不同动作(action),如何更新状态;
+Store∶ 管理action和reducer及其关系的对象,主要提供以下功能∶
+o 维护应用状态并支持访问状态(getState());
+o 支持监听action的分发,更新状态(dispatch(action));
+o 支持订阅store的变更(subscribe(listener));
+异步流∶ 由于Redux所有对store状态的变更,都应该通过action触发,异步任务(通常都是业务或获取数据任务)也不例外,而为了不将业务或数据相关的任务混入React组件中,就需要使用其他框架配合管理异步任务流程,如redux-thunk,redux-saga等;
+Mobx是一个透明函数响应式编程的状态管理库,它使得状态管理简单可伸缩∶
+对比总结:
+(1)Redux 和 Vuex区别
+通俗点理解就是,vuex 弱化 dispatch,通过commit进行 store状态的一次更变;取消了action概念,不必传入特定的 action形式进行指定变更;弱化reducer,基于commit参数直接对数据进行转变,使得框架更加简易;
+(2)共同思想
+本质上∶ redux与vuex都是对mvvm思想的服务,将数据从视图中抽离的一种方案。
+redux中间件本质就是一个函数柯里化。redux applyMiddleware Api 源码中每个middleware 接受2个参数, Store 的getState 函数和dispatch 函数,分别获得store和action,最终返回一个函数。该函数会被传入 next 的下一个 middleware 的 dispatch 方法,并返回一个接收 action 的新函数,这个函数可以直接调用 next(action),或者在其他需要的时刻调用,甚至根本不去调用它。调用链中最后一个 middleware 会接受真实的 store的 dispatch 方法作为 next 参数,并借此结束调用链。所以,middleware 的函数签名是({ getState,dispatch })=> next => action。
+connect负责连接React和Redux
+(1)获取state
+connect 通过 context获取 Provider 中的 store,通过 store.getState() 获取整个store tree 上所有state
(2)包装原组件
+将state和action通过props的方式传入到原组件内部 wrapWithConnect 返回—个 ReactComponent 对 象 Connect,Connect 重 新 render 外部传入的原组件 WrappedComponent ,并把 connect 中传入的 mapStateToProps,mapDispatchToProps与组件上原有的 props合并后,通过属性的方式传给WrappedComponent
+(3)监听store tree变化
+connect缓存了store tree中state的状态,通过当前state状态 和变更前 state 状态进行比较,从而确定是否调用 this.setState()方法触发Connect及其子组件的重新渲染
React-Hooks 是 React 团队在 React 组件开发实践中,逐渐认知到的一个改进点,这背后其实涉及对类组件和函数组件两种组件形式的思考和侧重。
+(1)类组件: 所谓类组件,就是基于 ES6 Class 这种写法,通过继承 React.Component 得来的 React 组件。以下是一个类组件:
+class DemoClass extends React.Component {
+ state = {
+ text: ""
+ };
+ componentDidMount() {
+ //...
+ }
+ changeText = (newText) => {
+ this.setState({
+ text: newText
+ });
+ };
+
+ render() {
+ return (
+ <div className="demoClass">
+ <p>{this.state.text}</p>
+ <button onClick={this.changeText}>修改</button>
+ </div>
+ );
+ }
+}
+可以看出,React 类组件内部预置了相当多的“现成的东西”等着我们去调度/定制,state 和生命周期就是这些“现成东西”中的典型。要想得到这些东西,难度也不大,只需要继承一个 React.Component 即可。
+当然,这也是类组件的一个不便,它太繁杂了,对于解决许多问题来说,编写一个类组件实在是一个过于复杂的姿势。复杂的姿势必然带来高昂的理解成本,这也是我们所不想看到的。除此之外,由于开发者编写的逻辑在封装后是和组件粘在一起的,这就使得类组件内部的逻辑难以实现拆分和复用。
+(2)函数组件:函数组件就是以函数的形态存在的 React 组件。早期并没有 React-Hooks,函数组件内部无法定义和维护 state,因此它还有一个别名叫“无状态组件”。以下是一个函数组件:
+function DemoFunction(props) {
+ const { text } = props
+ return (
+ <div className="demoFunction">
+ <p>{`函数组件接收的内容:[${text}]`}</p>
+ </div>
+ );
+}
+相比于类组件,函数组件肉眼可见的特质自然包括轻量、灵活、易于组织和维护、较低的学习成本等。
+通过对比,从形态上可以对两种组件做区分,它们之间的区别如下:
+除此之外,还有一些其他的不同。通过上面的区别,我们不能说谁好谁坏,它们各有自己的优势。在 React-Hooks 出现之前,类组件的能力边界明显强于函数组件。
+实际上,类组件和函数组件之间,是面向对象和函数式编程这两套不同的设计思想之间的差异。而函数组件更加契合 React 框架的设计理念:
+
+React 组件本身的定位就是函数,一个输入数据、输出 UI 的函数。作为开发者,我们编写的是声明式的代码,而 React 框架的主要工作,就是及时地把声明式的代码转换为命令式的 DOM 操作,把数据层面的描述映射到用户可见的 UI 变化中去。这就意味着从原则上来讲,React 的数据应该总是紧紧地和渲染绑定在一起的,而类组件做不到这一点。函数组件就真正地将数据和渲染绑定到了一起。函数组件是一个更加匹配其设计理念、也更有利于逻辑拆分与重用的组件表达形式。
为了能让开发者更好的的去编写函数式组件。于是,React-Hooks 便应运而生。
+React-Hooks 是一套能够使函数组件更强大、更灵活的“钩子”。
+函数组件比起类组件少了很多东西,比如生命周期、对 state 的管理等。这就给函数组件的使用带来了非常多的局限性,导致我们并不能使用函数这种形式,写出一个真正的全功能的组件。而React-Hooks 的出现,就是为了帮助函数组件补齐这些(相对于类组件来说)缺失的能力。
+如果说函数组件是一台轻巧的快艇,那么 React-Hooks 就是一个内容丰富的零部件箱。“重装战舰”所预置的那些设备,这个箱子里基本全都有,同时它还不强制你全都要,而是允许你自由地选择和使用你需要的那些能力,然后将这些能力以 Hook(钩子)的形式“钩”进你的组件里,从而定制出一个最适合你的“专属战舰”。
+useState 的用法:
+const [count, setCount] = useState(0)
+可以看到 useState 返回的是一个数组,那么为什么是返回数组而不是返回对象呢?
+这里用到了解构赋值,所以先来看一下ES6 的解构赋值:
+const foo = [1, 2, 3];
+const [one, two, three] = foo;
+console.log(one); // 1
+console.log(two); // 2
+console.log(three); // 3
+const user = {
+ id: 888,
+ name: "xiaoxin"
+};
+const { id, name } = user;
+console.log(id); // 888
+console.log(name); // "xiaoxin"
+看完这两个例子,答案应该就出来了:
+下面来看看如果 useState 返回对象的情况:
+// 第一次使用
+const { state, setState } = useState(false);
+// 第二次使用
+const { state: counter, setState: setCounter } = useState(0)
+这里可以看到,返回对象的使用方式还是挺麻烦的,更何况实际项目中会使用的更频繁。 +总结:useState 返回的是 array 而不是 object 的原因就是为了降低使用的复杂度,返回数组的话可以直接根据顺序解构,而返回对象的话要想使用多次就需要定义别名了。
+React Hooks 主要解决了以下问题:
+(1)在组件之间复用状态逻辑很难
+React 没有提供将可复用性行为“附加”到组件的途径(例如,把组件连接到 store)解决此类问题可以使用 render props 和 高阶组件。但是这类方案需要重新组织组件结构,这可能会很麻烦,并且会使代码难以理解。由 providers,consumers,高阶组件,render props 等其他抽象层组成的组件会形成“嵌套地狱”。尽管可以在 DevTools 过滤掉它们,但这说明了一个更深层次的问题:React 需要为共享状态逻辑提供更好的原生途径。
+可以使用 Hook 从组件中提取状态逻辑,使得这些逻辑可以单独测试并复用。Hook 使我们在无需修改组件结构的情况下复用状态逻辑。 这使得在组件间或社区内共享 Hook 变得更便捷。
+(2)复杂组件变得难以理解
+在组件中,每个生命周期常常包含一些不相关的逻辑。例如,组件常常在 componentDidMount 和 componentDidUpdate 中获取数据。但是,同一个 componentDidMount 中可能也包含很多其它的逻辑,如设置事件监听,而之后需在 componentWillUnmount 中清除。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。如此很容易产生 bug,并且导致逻辑不一致。
+在多数情况下,不可能将组件拆分为更小的粒度,因为状态逻辑无处不在。这也给测试带来了一定挑战。同时,这也是很多人将 React 与状态管理库结合使用的原因之一。但是,这往往会引入了很多抽象概念,需要你在不同的文件之间来回切换,使得复用变得更加困难。
+为了解决这个问题,Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分。你还可以使用 reducer 来管理组件的内部状态,使其更加可预测。
+(3)难以理解的 class
+除了代码复用和代码管理会遇到困难外,class 是学习 React 的一大屏障。我们必须去理解 JavaScript 中 this 的工作方式,这与其他语言存在巨大差异。还不能忘记绑定事件处理器。没有稳定的语法提案,这些代码非常冗余。大家可以很好地理解 props,state 和自顶向下的数据流,但对 class 却一筹莫展。即便在有经验的 React 开发者之间,对于函数组件与 class 组件的差异也存在分歧,甚至还要区分两种组件的使用场景。
+为了解决这些问题,Hook 使你在非 class 的情况下可以使用更多的 React 特性。 从概念上讲,React 组件一直更像是函数。而 Hook 则拥抱了函数,同时也没有牺牲 React 的精神原则。Hook 提供了问题的解决方案,无需学习复杂的函数式或响应式编程技术
+React Hooks 的限制主要有两条:
+那为什么会有这样的限制呢?Hooks 的设计初衷是为了改进 React 组件的开发模式。在旧有的开发模式下遇到了三个问题。
+这三个问题在一定程度上阻碍了 React 的后续发展,所以为了解决这三个问题,Hooks 基于函数组件开始设计。然而第三个问题决定了 Hooks 只支持函数组件。
+那为什么不要在循环、条件或嵌套函数中调用 Hook 呢?因为 Hooks 的设计是基于数组实现。在调用时按顺序加入数组中,如果使用循环、条件或嵌套函数很有可能导致数组取值错位,执行错误的 Hook。当然,实质上 React 的源码里不是数组,是链表。
+这些限制会在编码上造成一定程度的心智负担,新手可能会写错,为了避免这样的情况,可以引入 ESLint 的 Hooks 检查插件进行预防。
+(1)共同点
+(2)不同点
+在未来的趋势上,两个 API 是会长期共存的,暂时没有删减合并的计划,需要开发者根据场景去自行选择。React 团队的建议非常实用,如果实在分不清,先用 useEffect,一般问题不大;如果页面有异常,再直接替换为 useLayoutEffect 即可。
+(1)不要在循环,条件或嵌套函数中调用Hook,必须始终在 React函数的顶层使用Hook
+这是因为React需要利用调用顺序来正确更新相应的状态,以及调用相应的钩子函数。一旦在循环或条件分支语句中调用Hook,就容易导致调用顺序的不一致性,从而产生难以预料到的后果。
+(2)使用useState时候,使用push,pop,splice等直接更改数组对象的坑
+使用push直接更改数组无法获取到新值,应该采用析构方式,但是在class里面不会有这个问题。代码示例:
+function Indicatorfilter() {
+ let [num,setNums] = useState([0,1,2,3])
+ const test = () => {
+ // 这里坑是直接采用push去更新num
+ // setNums(num)是无法更新num的
+ // 必须使用num = [...num ,1]
+ num.push(1)
+ // num = [...num ,1]
+ setNums(num)
+ }
+return (
+ <div className='filter'>
+ <div onClick={test}>测试</div>
+ <div>
+ {num.map((item,index) => (
+ <div key={index}>{item}</div>
+ ))}
+ </div>
+ </div>
+ )
+}
+
+class Indicatorfilter extends React.Component<any,any>{
+ constructor(props:any){
+ super(props)
+ this.state = {
+ nums:[1,2,3]
+ }
+ this.test = this.test.bind(this)
+ }
+
+ test(){
+ // class采用同样的方式是没有问题的
+ this.state.nums.push(1)
+ this.setState({
+ nums: this.state.nums
+ })
+ }
+
+ render(){
+ let {nums} = this.state
+ return(
+ <div>
+ <div onClick={this.test}>测试</div>
+ <div>
+ {nums.map((item:any,index:number) => (
+ <div key={index}>{item}</div>
+ ))}
+ </div>
+ </div>
+
+ )
+ }
+}
+(3)useState设置状态的时候,只有第一次生效,后期需要更新状态,必须通过useEffect
+TableDeail是一个公共组件,在调用它的父组件里面,我们通过set改变columns的值,以为传递给TableDeail 的 columns是最新的值,所以tabColumn每次也是最新的值,但是实际tabColumn是最开始的值,不会随着columns的更新而更新:
+const TableDeail = ({
+ columns,
+}:TableData) => {
+ const [tabColumn, setTabColumn] = useState(columns)
+}
+
+// 正确的做法是通过useEffect改变这个值
+const TableDeail = ({
+ columns,
+}:TableData) => {
+ const [tabColumn, setTabColumn] = useState(columns)
+ useEffect(() =>{setTabColumn(columns)},[columns])
+}
+(4)善用useCallback
+父组件传递给子组件事件句柄时,如果我们没有任何参数变动可能会选用useMemo。但是每一次父组件渲染子组件即使没变化也会跟着渲染一次。
+(5)不要滥用useContext
+可以使用基于 useContext 封装的状态管理工具。
+函数组件 的本质是函数,没有 state 的概念的,因此不存在生命周期一说,仅仅是一个 render 函数而已。
+但是引入 Hooks 之后就变得不同了,它能让组件在不使用 class 的情况下拥有 state,所以就有了生命周期的概念,所谓的生命周期其实就是 useState、 useEffect() 和 useLayoutEffect() 。
即:Hooks 组件(使用了Hooks的函数组件)有生命周期,而函数组件(未使用Hooks的函数组件)是没有生命周期的。
+下面是具体的 class 与 Hooks 的生命周期对应关系:
+constructor:函数组件不需要构造函数,可以通过调用 **useState 来初始化 state**。如果计算的代价比较昂贵,也可以传一个函数给 useState。const [num, UpdateNum] = useState(0)
+getDerivedStateFromProps:一般情况下,我们不需要使用它,可以在渲染过程中更新 state,以达到实现 getDerivedStateFromProps 的目的。function ScrollView({row}) {
+ let [isScrollingDown, setIsScrollingDown] = useState(false);
+ let [prevRow, setPrevRow] = useState(null);
+ if (row !== prevRow) {
+ // Row 自上次渲染以来发生过改变。更新 isScrollingDown。
+ setIsScrollingDown(prevRow !== null && row > prevRow);
+ setPrevRow(row);
+ }
+ return `Scrolling down: ${isScrollingDown}`;
+}
+React 会立即退出第一次渲染并用更新后的 state 重新运行组件以避免耗费太多性能。
+shouldComponentUpdate:可以用 **React.memo** 包裹一个组件来对它的 props 进行浅比较const Button = React.memo((props) => { // 具体的组件});
+注意:**React.memo 等效于 **``**PureComponent**,它只浅比较 props。这里也可以使用 useMemo 优化每一个节点。
render:这是函数组件体本身。componentDidMount, componentDidUpdate: useLayoutEffect 与它们两的调用阶段是一样的。但是,我们推荐你一开始先用 useEffect,只有当它出问题的时候再尝试使用 useLayoutEffect。useEffect 可以表达所有这些的组合。// componentDidMount
+useEffect(()=>{
+ // 需要在 componentDidMount 执行的内容
+}, [])
+useEffect(() => {
+ // 在 componentDidMount,以及 count 更改时 componentDidUpdate 执行的内容
+ document.title = `You clicked ${count} times`;
+ return () => {
+ // 需要在 count 更改时 componentDidUpdate(先于 document.title = ... 执行,遵守先清理后更新)
+ // 以及 componentWillUnmount 执行的内容
+ } // 当函数中 Cleanup 函数会按照在代码中定义的顺序先后执行,与函数本身的特性无关
+}, [count]); // 仅在 count 更改时更新
+请记得 React 会等待浏览器完成画面渲染之后才会延迟调用 ,因此会使得额外操作很方便
+componentWillUnmount:相当于 useEffect 里面返回的 cleanup 函数// componentDidMount/componentWillUnmount
+useEffect(()=>{
+ // 需要在 componentDidMount 执行的内容
+ return function cleanup() {
+ // 需要在 componentWillUnmount 执行的内容
+ }
+}, [])
+componentDidCatch and getDerivedStateFromError:目前还没有这些方法的 Hook 等价写法,但很快会加上。| class 组件 | Hooks 组件 |
|---|---|
| constructor | useState |
| getDerivedStateFromProps | useState 里面 update 函数 |
| shouldComponentUpdate | useMemo |
| render | 函数本身 |
| componentDidMount | useEffect |
| componentDidUpdate | useEffect |
| componentWillUnmount | useEffect 里面返回的函数 |
| componentDidCatch | 无 |
| getDerivedStateFromError | 无 |
从本质上来说,Virtual Dom是一个JavaScript对象,通过对象的方式来表示DOM结构。将页面的状态抽象为JS对象的形式,配合不同的渲染工具,使跨平台渲染成为可能。通过事务处理机制,将多次DOM修改的结果一次性的更新到页面上,从而有效的减少页面渲染的次数,减少修改DOM的重绘重排次数,提高渲染性能。
+虚拟DOM是对DOM的抽象,这个对象是更加轻量级的对DOM的描述。它设计的最初目的,就是更好的跨平台,比如node.js就没有DOM,如果想实现SSR,那么一个方式就是借助虚拟dom,因为虚拟dom本身是js对象。 在代码渲染到页面之前,vue或者react会把代码转换成一个对象(虚拟DOM)。以对象的形式来描述真实dom结构,最终渲染到页面。在每次数据发生变化前,虚拟dom都会缓存一份,变化之时,现在的虚拟dom会与缓存的虚拟dom进行比较。在vue或者react内部封装了diff算法,通过这个算法来进行比较,渲染时修改改变的变化,原先没有发生改变的通过原先的数据进行渲染。
+另外现代前端框架的一个基本要求就是无须手动操作DOM,一方面是因为手动操作DOM无法保证程序性能,多人协作的项目中如果review不严格,可能会有开发者写出性能较低的代码,另一方面更重要的是省略手动DOM操作可以大大提高开发效率。
+为什么要用 Virtual DOM:
+(1)保证性能下限,在不进行手动优化的情况下,提供过得去的性能
+下面对比一下修改DOM时真实DOM操作和Virtual DOM的过程,来看一下它们重排重绘的性能消耗∶
+Virtual DOM的更新DOM的准备工作耗费更多的时间,也就是JS层面,相比于更多的DOM操作它的消费是极其便宜的。尤雨溪在社区论坛中说道∶ 框架给你的保证是,你不需要手动优化的情况下,我依然可以给你提供过得去的性能。 +(2)跨平台 +Virtual DOM本质上是JavaScript的对象,它可以很方便的跨平台操作,比如服务端渲染、uniapp等。
+实际上,diff 算法探讨的就是虚拟 DOM 树发生变化后,生成 DOM 树更新补丁的方式。它通过对比新旧两株虚拟 DOM 树的变更差异,将更新补丁作用于真实 DOM,以最小成本完成视图更新。
+
+具体的流程如下:
+一个简单的例子:
import React from 'react'
+export default class ExampleComponent extends React.Component {
+ render() {
+ if(this.props.isVisible) {
+ return <div className="visible">visbile</div>;
+ }
+ return <div className="hidden">hidden</div>;
+ }
+}
+这里,首先假定 ExampleComponent 可见,然后再改变它的状态,让它不可见 。映射为真实的 DOM 操作是这样的,React 会创建一个 div 节点。
+<div class="visible">visbile</div>
+当把 visbile 的值变为 false 时,就会替换 class 属性为 hidden,并重写内部的 innerText 为 hidden。这样一个生成补丁、更新差异的过程统称为 diff 算法。
+diff算法可以总结为三个策略,分别从树、组件及元素三个层面进行复杂度的优化:
+策略一:忽略节点跨层级操作场景,提升比对效率。(基于树进行对比)
+这一策略需要进行树比对,即对树进行分层比较。树比对的处理手法是非常“暴力”的,即两棵树只对同一层次的节点进行比较,如果发现节点已经不存在了,则该节点及其子节点会被完全删除掉,不会用于进一步的比较,这就提升了比对效率。
+策略二:如果组件的 class 一致,则默认为相似的树结构,否则默认为不同的树结构。(基于组件进行对比)
+在组件比对的过程中:
+只要父组件类型不同,就会被重新渲染。这也就是为什么 shouldComponentUpdate、PureComponent 及 React.memo 可以提高性能的原因。
+策略三:同一层级的子节点,可以通过标记 key 的方式进行列表对比。(基于节点进行对比)
+元素比对主要发生在同层级中,通过标记节点操作生成补丁。节点操作包含了插入、移动、删除等。其中节点重新排序同时涉及插入、移动、删除三个操作,所以效率消耗最大,此时策略三起到了至关重要的作用。通过标记 key 的方式,React 可以直接移动 DOM 节点,降低内耗。
+Keys 是 React 用于追踪哪些列表中元素被修改、被添加或者被移除的辅助标识。在开发过程中,我们需要保证某个元素的 key 在其同级元素中具有唯一性。
+在 React Diff 算法中 React 会借助元素的 Key 值来判断该元素是新近创建的还是被移动而来的元素,从而减少不必要的元素重渲染此外,React 还需要借助 Key 值来判断元素与本地状态的关联关系。
+注意事项:
+虚拟DOM相对原生的DOM不一定是效率更高,如果只修改一个按钮的文案,那么虚拟 DOM 的操作无论如何都不可能比真实的 DOM 操作更快。在首次渲染大量DOM时,由于多了一层虚拟DOM的计算,虚拟DOM也会比innerHTML插入慢。它能保证性能下限,在真实DOM操作的时候进行针对性的优化时,还是更快的。所以要根据具体的场景进行探讨。
+在整个 DOM 操作的演化过程中,其实主要矛盾并不在于性能,而在于开发者写得爽不爽,在于研发体验/研发效率。虚拟 DOM 不是别的,正是前端开发们为了追求更好的研发体验和研发效率而创造出来的高阶产物。虚拟 DOM 并不一定会带来更好的性能,React 官方也从来没有把虚拟 DOM 作为性能层面的卖点对外输出过。虚拟 DOM 的优越之处在于,它能够在提供更爽、更高效的研发模式(也就是函数式的 UI 编程方式)的同时,仍然保持一个还不错的性能。
+diff 算法是指生成更新补丁的方式,主要应用于虚拟 DOM 树变化后,更新真实 DOM。所以 diff 算法一定存在这样一个过程:触发更新 → 生成补丁 → 应用补丁。
+React 的 diff 算法,触发更新的时机主要在 state 变化与 hooks 调用之后。此时触发虚拟 DOM 树变更遍历,采用了深度优先遍历算法。但传统的遍历方式,效率较低。为了优化效率,使用了分治的方式。将单一节点比对转化为了 3 种类型节点的比对,分别是树、组件及元素,以此提升效率。
+以上是经典的 React diff 算法内容。自 React 16 起,引入了 Fiber 架构。为了使整个更新过程可随时暂停恢复,节点与树分别采用了 FiberNode 与 FiberTree 进行重构。fiberNode 使用了双链表的结构,可以直接找到兄弟节点与子节点。整个更新过程由 current 与 workInProgress 两株树双缓冲完成。workInProgress 更新完成后,再通过修改 current 相关指针指向新节点。
+Vue 的整体 diff 策略与 React 对齐,虽然缺乏时间切片能力,但这并不意味着 Vue 的性能更差,因为在 Vue 3 初期引入过,后期因为收益不高移除掉了。除了高帧率动画,在 Vue 中其他的场景几乎都可以使用防抖和节流去提高响应性能。
+通过引用而不是使用来命名组件displayName。
+使用displayName命名组件:
+export default React.createClass({ displayName: 'TodoApp', // ...})
+React推荐的方法:
+export default class TodoApp extends React.Component { // ...}
+React 16.x的三大新特性 Time Slicing、Suspense、 hooks
+(1)React16.8 +加入hooks,让React函数式组件更加灵活,hooks之前,React存在很多问题:
+hooks很好的解决了上述问题,hooks提供了很多方法
+(2)React16.9
+(3)React16.13.0
+import React, { Component } from 'react';
+import { is, fromJS } from 'immutable';
+import ReactDOM from 'react-dom';
+import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
+import './dialog.css';
+let defaultState = {
+ alertStatus:false,
+ alertTip:"提示",
+ closeDialog:function(){},
+ childs:''
+}
+class Dialog extends Component{
+ state = {
+ ...defaultState
+ };
+ // css动画组件设置为目标组件
+ FirstChild = props => {
+ const childrenArray = React.Children.toArray(props.children);
+ return childrenArray[0] || null;
+ }
+ //打开弹窗
+ open =(options)=>{
+ options = options || {};
+ options.alertStatus = true;
+ var props = options.props || {};
+ var childs = this.renderChildren(props,options.childrens) || '';
+ console.log(childs);
+ this.setState({
+ ...defaultState,
+ ...options,
+ childs
+ })
+ }
+ //关闭弹窗
+ close(){
+ this.state.closeDialog();
+ this.setState({
+ ...defaultState
+ })
+ }
+ renderChildren(props,childrens) {
+ //遍历所有子组件
+ var childs = [];
+ childrens = childrens || [];
+ var ps = {
+ ...props, //给子组件绑定props
+ _close:this.close //给子组件也绑定一个关闭弹窗的事件
+ };
+ childrens.forEach((currentItem,index) => {
+ childs.push(React.createElement(
+ currentItem,
+ {
+ ...ps,
+ key:index
+ }
+ ));
+ })
+ return childs;
+ }
+ shouldComponentUpdate(nextProps, nextState){
+ return !is(fromJS(this.props), fromJS(nextProps)) || !is(fromJS(this.state), fromJS(nextState))
+ }
+
+ render(){
+ return (
+ <ReactCSSTransitionGroup
+ component={this.FirstChild}
+ transitionName='hide'
+ transitionEnterTimeout={300}
+ transitionLeaveTimeout={300}>
+ <div className="dialog-con" style={this.state.alertStatus? {display:'block'}:{display:'none'}}>
+ {this.state.childs}
+ </div>
+ </ReactCSSTransitionGroup>
+ );
+ }
+}
+let div = document.createElement('div');
+let props = {
+
+};
+document.body.appendChild(div);
+let Box = ReactD
+子类:
+//子类jsx
+import React, { Component } from 'react';
+class Child extends Component {
+ constructor(props){
+ super(props);
+ this.state = {date: new Date()};
+ }
+ showValue=()=>{
+ this.props.showValue && this.props.showValue()
+ }
+ render() {
+ return (
+ <div className="Child">
+ <div className="content">
+ Child
+ <button onClick={this.showValue}>调用父的方法</button>
+ </div>
+ </div>
+ );
+ }
+}
+export default Child;
+css:
+.dialog-con{
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.3);
+}
+封装数据持久化组件:
+】let storage={
+ // 增加
+ set(key, value){
+ localStorage.setItem(key, JSON.stringify(value));
+ },
+ // 获取
+ get(key){
+ return JSON.parse(localStorage.getItem(key));
+ },
+ // 删除
+ remove(key){
+ localStorage.removeItem(key);
+ }
+};
+export default Storage;
+在React项目中,通过redux存储全局数据时,会有一个问题,如果用户刷新了网页,那么通过redux存储的全局数据就会被全部清空,比如登录信息等。这时就会有全局数据持久化存储的需求。首先想到的就是localStorage,localStorage是没有时间限制的数据存储,可以通过它来实现数据的持久化存储。
+但是在已经使用redux来管理和存储全局数据的基础上,再去使用localStorage来读写数据,这样不仅是工作量巨大,还容易出错。那么有没有结合redux来达到持久数据存储功能的框架呢?当然,它就是redux-persist。redux-persist会将redux的store中的数据缓存到浏览器的localStorage中。其使用步骤如下:
+(1)首先要安装redux-persist:
+npm i redux-persist
+(2)对于reducer和action的处理不变,只需修改store的生成代码,修改如下:
+import {createStore} from 'redux'
+import reducers from '../reducers/index'
+import {persistStore, persistReducer} from 'redux-persist';
+import storage from 'redux-persist/lib/storage';
+import autoMergeLevel2 from 'redux-persist/lib/stateReconciler/autoMergeLevel2';
+const persistConfig = {
+ key: 'root',
+ storage: storage,
+ stateReconciler: autoMergeLevel2 // 查看 'Merge Process' 部分的具体情况
+};
+const myPersistReducer = persistReducer(persistConfig, reducers)
+const store = createStore(myPersistReducer)
+export const persistor = persistStore(store)
+export default store
+(3)在index.js中,将PersistGate标签作为网页内容的父标签:
+import React from 'react';
+import ReactDOM from 'react-dom';
+import {Provider} from 'react-redux'
+import store from './redux/store/store'
+import {persistor} from './redux/store/store'
+import {PersistGate} from 'redux-persist/lib/integration/react';
+ReactDOM.render(<Provider store={store}>
+ <PersistGate loading={null} persistor={persistor}>
+ {/*网页内容*/}
+ </PersistGate>
+ </Provider>, document.getElementById('root'));
+这就完成了通过redux-persist实现React持久化本地数据存储的简单应用。
+相似之处:
+不同之处:
+1)数据流
+Vue默认支持数据双向绑定,而React一直提倡单向数据流
+2)虚拟DOM
+Vue2.x开始引入"Virtual DOM",消除了和React在这方面的差异,但是在具体的细节还是有各自的特点。
+3)组件化
+React与Vue最大的不同是模板的编写。
+具体来讲:React中render函数是支持闭包特性的,所以我们import的组件在render中可以直接调用。但是在Vue中,由于模板中使用的数据都必须挂在 this 上进行一次中转,所以 import 完组件之后,还需要在 components 中再声明下。
+4)监听数据变化的实现原理不同
+5)高阶组件
+react可以通过高阶组件(Higher Order Components-- HOC)来扩展,而vue需要通过mixins来扩展。
+原因高阶组件就是高阶函数,而React的组件本身就是纯粹的函数,所以高阶函数对React来说易如反掌。相反Vue.js使用HTML模板创建视图组件,这时模板无法有效的编译,因此Vue不采用HOC来实现。
+6)构建工具
+两者都有自己的构建工具
+7)跨平台
+(1)如果还未创建 Create React App 项目
+ npx create-react-app demo --typescript
+(2)如果已经创建了 Create React App 项目,需要将 typescript 引入到已有项目中
+npm install --save typescript @types/node @types/react @types/react-dom @types/jest
+(1)编写简单直观的代码
+React最大的价值不是高性能的虚拟DOM、封装的事件机制、服务器端渲染,而是声明式的直观的编码方式。react文档第一条就是声明式,React 使创建交互式 UI 变得轻而易举。为应用的每一个状态设计简洁的视图,当数据改变时 React 能有效地更新并正确地渲染组件。 以声明式编写 UI,可以让代码更加可靠,且方便调试。
+(2)简化可复用的组件
+React框架里面使用了简化的组件模型,但更彻底地使用了组件化的概念。React将整个UI上的每一个功能模块定义成组件,然后将小的组件通过组合或者嵌套的方式构成更大的组件。React的组件具有如下的特性∶
+(3) Virtual DOM
+真实页面对应一个 DOM 树。在传统页面的开发模式中,每次需要更新页面时,都要手动操作 DOM 来进行更新。 DOM 操作非常昂贵。在前端开发中,性能消耗最大的就是 DOM 操作,而且这部分代码会让整体项目的代码变得难 以维护。React 把真实 DOM 树转换成 JavaScript 对象树,也就是 Virtual DOM,每次数据更新后,重新计算 Virtual DOM,并和上一次生成的 Virtual DOM 做对比,对发生变化的部分做批量更新。React 也提供了直观的 shouldComponentUpdate 生命周期回调,来减少数据变化后不必要的 Virtual DOM 对比过程,以保证性能。
+(4)函数式编程
+React 把过去不断重复构建 UI 的过程抽象成了组件,且在给定参数的情况下约定渲染对应的 UI 界面。React 能充分利用很多函数式方法去减少冗余代码。此外,由于它本身就是简单函数,所以易于测试。
+(5)一次学习,随处编写
+无论现在正在使用什么技术栈,都可以随时引入 React来开发新特性,而不需要重写现有代码。
+React 还可以使用 Node 进行服务器渲染,或使用 React Native 开发原生移动应用。因为 React 组件可以映射为对应的原生控件。在输出的时候,是输出 Web DOM,还是 Android 控件,还是 iOS 控件,就由平台本身决定了。所以,react很方便和其他平台集成
+在React中,当涉及组件嵌套,在父组件中使用props.children把所有子组件显示出来。如下:
function ParentComponent(props){
+ return (
+ <div>
+ {props.children}
+ </div>
+ )
+}
+如果想把父组件中的属性传给所有的子组件,需要使用React.Children方法。
比如,把几个Radio组合起来,合成一个RadioGroup,这就要求所有的Radio具有同样的name属性值。可以这样:把Radio看做子组件,RadioGroup看做父组件,name的属性值在RadioGroup这个父组件中设置。
+首先是子组件:
+//子组件
+function RadioOption(props) {
+ return (
+ <label>
+ <input type="radio" value={props.value} name={props.name} />
+ {props.label}
+ </label>
+ )
+}
+然后是父组件,不仅需要把它所有的子组件显示出来,还需要为每个子组件赋上name属性和值:
+//父组件用,props是指父组件的props
+function renderChildren(props) {
+
+ //遍历所有子组件
+ return React.Children.map(props.children, child => {
+ if (child.type === RadioOption)
+ return React.cloneElement(child, {
+ //把父组件的props.name赋值给每个子组件
+ name: props.name
+ })
+ else
+ return child
+ })
+}
+//父组件
+function RadioGroup(props) {
+ return (
+ <div>
+ {renderChildren(props)}
+ </div>
+ )
+}
+function App() {
+ return (
+ <RadioGroup name="hello">
+ <RadioOption label="选项一" value="1" />
+ <RadioOption label="选项二" value="2" />
+ <RadioOption label="选项三" value="3" />
+ </RadioGroup>
+ )
+}
+export default App;
+以上,React.Children.map让我们对父组件的所有子组件又更灵活的控制。
React的状态提升就是用户对子组件操作,子组件不改变自己的状态,通过自己的props把这个操作改变的数据传递给父组件,改变父组件的状态,从而改变受父组件控制的所有子组件的状态,这也是React单项数据流的特性决定的。官方的原话是:共享 state(状态) 是通过将其移动到需要它的组件的最接近的共同祖先组件来实现的。 这被称为“状态提升(Lifting State Up)”。
+概括来说就是将多个组件需要共享的状态提升到它们最近的父组件上,在父组件上改变这个状态然后通过props分发给子组件。
+一个简单的例子,父组件中有两个input子组件,如果想在第一个输入框输入数据,来改变第二个输入框的值,这就需要用到状态提升。
+class Father extends React.Component {
+ constructor(props) {
+ super(props)
+ this.state = {
+ Value1: '',
+ Value2: ''
+ }
+ }
+ value1Change(aa) {
+ this.setState({
+ Value1: aa
+ })
+ }
+ value2Change(bb) {
+ this.setState({
+ Value2: bb
+ })
+ }
+ render() {
+ return (
+ <div style={{ padding: "100px" }}>
+ <Child1 value1={this.state.Value1} onvalue1Change={this.value1Change.bind(this)} />
+
+
+ <Child2 value2={this.state.Value1} />
+ </div>
+ )
+ }
+}
+class Child1 extends React.Component {
+ constructor(props) {
+ super(props)
+ }
+ changeValue(e) {
+ this.props.onvalue1Change(e.target.value)
+ }
+ render() {
+ return (
+ <input value={this.props.Value1} onChange={this.changeValue.bind(this)} />
+ )
+ }
+}
+class Child2 extends React.Component {
+ constructor(props) {
+ super(props)
+ }
+ render() {
+ return (
+ <input value={this.props.value2} />
+ )
+ }
+}
+
+ReactDOM.render(
+ <Father />,
+ document.getElementById('root')
+)
+两者都是用来初始化state的。前者是ES6中的语法,后者是ES5中的语法,新版本的React中已经废弃了该方法。
+getInitialState是ES5中的方法,如果使用createClass方法创建一个Component组件,可以自动调用它的getInitialState方法来获取初始化的State对象,
+var APP = React.creatClass ({
+ getInitialState() {
+ return {
+ userName: 'hi',
+ userId: 0
+ };
+ }
+})
+React在ES6的实现中去掉了getInitialState这个hook函数,规定state在constructor中实现,如下:
+Class App extends React.Component{
+ constructor(props){
+ super(props);
+ this.state={};
+ }
+ }
+StrictMode 是一个用来突出显示应用程序中潜在问题的工具。与 Fragment 一样,StrictMode 不会渲染任何可见的 UI。它为其后代元素触发额外的检查和警告。
+可以为应用程序的任何部分启用严格模式。例如:
import React from 'react';
+function ExampleApplication() {
+ return (
+ <div>
+ <Header />
+ <React.StrictMode>
+ <div>
+ <ComponentOne />
+ <ComponentTwo />
+ </div>
+ </React.StrictMode>
+ <Footer />
+ </div>
+ );
+}
+在上述的示例中,不会对 Header 和 Footer 组件运行严格模式检查。但是,ComponentOne 和 ComponentTwo 以及它们的所有后代元素都将进行检查。
StrictMode 目前有助于:
(1)遍历数组:map && forEach
+import React from 'react';
+
+class App extends React.Component {
+ render() {
+ let arr = ['a', 'b', 'c', 'd'];
+ return (
+ <ul>
+ {
+ arr.map((item, index) => {
+ return <li key={index}>{item}</li>
+ })
+ }
+ </ul>
+ )
+ }
+}
+
+class App extends React.Component {
+ render() {
+ let arr = ['a', 'b', 'c', 'd'];
+ return (
+ <ul>
+ {
+ arr.forEach((item, index) => {
+ return <li key={index}>{item}</li>
+ })
+ }
+ </ul>
+ )
+ }
+}
+(2)遍历对象:map && for in
+class App extends React.Component {
+ render() {
+ let obj = {
+ a: 1,
+ b: 2,
+ c: 3
+ }
+ return (
+ <ul>
+ {
+ (() => {
+ let domArr = [];
+ for(const key in obj) {
+ if(obj.hasOwnProperty(key)) {
+ const value = obj[key]
+ domArr.push(<li key={key}>{value}</li>)
+ }
+ }
+ return domArr;
+ })()
+ }
+ </ul>
+ )
+ }
+}
+
+// Object.entries() 把对象转换成数组
+class App extends React.Component {
+ render() {
+ let obj = {
+ a: 1,
+ b: 2,
+ c: 3
+ }
+ return (
+ <ul>
+ {
+ Object.entries(obj).map(([key, value], index) => { // item是一个数组,把item解构,写法是[key, value]
+ return <li key={key}>{value}</li>
+ })
+ }
+ </ul>
+ )
+ }
+}
+这个问题就设计到了数据持久化, 主要的实现方式有以下几种:
+pushState 函数可以给历史记录关联一个任意的可序列化 state,所以可以在路由 push 的时候将当前页面的一些信息存到 state 中,下次返回到这个页面的时候就能从 state 里面取出离开前的数据重新渲染。react-router 直接可以支持。这个方法适合一些需要临时存储的场景。React 并不强制要求使用 JSX。当不想在构建环境中配置有关 JSX 编译时,不在 React 中使用 JSX 会更加方便。
+每个 JSX 元素只是调用 React.createElement(component, props, ...children) 的语法糖。因此,使用 JSX 可以完成的任何事情都可以通过纯 JavaScript 完成。
例如,用 JSX 编写的代码:
+class Hello extends React.Component {
+ render() {
+ return <div>Hello {this.props.toWhat}</div>;
+ }
+}
+ReactDOM.render(
+ <Hello toWhat="World" />,
+ document.getElementById('root')
+);
+可以编写为不使用 JSX 的代码:
+class Hello extends React.Component {
+ render() {
+ return React.createElement('div', null, `Hello ${this.props.toWhat}`);
+ }
+}
+ReactDOM.render(
+ React.createElement(Hello, {toWhat: 'World'}, null),
+ document.getElementById('root')
+);
+本质上来说JSX是React.createElement(component, props, ...children)方法的语法糖。在React 17之前,如果使用了JSX,其实就是在使用React, babel 会把组件转换为 CreateElement 形式。在React 17之后,就不再需要引入,因为 babel 已经可以帮我们自动引入react。
async/await是ES7标准中的新特性。如果是使用React官方的脚手架创建的项目,就可以直接使用。如果是在自己搭建的webpack配置的项目中使用,可能会遇到 regeneratorRuntime is not defined 的异常错误。那么我们就需要引入babel,并在babel中配置使用async/await。可以利用babel的 transform-async-to-module-method 插件来转换其成为浏览器支持的语法,虽然没有性能的提升,但对于代码编写体验要更好。
+JavaScript中的map不会对为null或者undefined的数据进行处理,而React.Children.map中的map可以处理React.Children为null或者undefined的情况。
+服务端渲染是数据与模版组成的html,即 HTML = 数据 + 模版。将组件或页面通过服务器生成html字符串,再发送到浏览器,最后将静态标记"混合"为客户端上完全交互的应用程序。页面没使用服务渲染,当请求页面时,返回的body里为空,之后执行js将html结构注入到body里,结合css显示出来;
+SSR的优势:
+1)更利于SEO
+不同爬虫工作原理类似,只会爬取源码,不会执行网站的任何脚本使用了React或者其它MVVM框架之后,页面大多数DOM元素都是在客户端根据js动态生成,可供爬虫抓取分析的内容大大减少。另外,浏览器爬虫不会等待我们的数据完成之后再去抓取页面数据。服务端渲染返回给客户端的是已经获取了异步数据并执行JavaScript脚本的最终HTML,网络爬中就可以抓取到完整页面的信息。
+2)更利于首屏渲染
+首屏的渲染是node发送过来的html字符串,并不依赖于js文件了,这就会使用户更快的看到页面的内容。尤其是针对大型单页应用,打包后文件体积比较大,普通客户端渲染加载所有所需文件时间较长,首页就会有一个很长的白屏等待时间。
+SSR的局限:
+1)服务端压力较大
+本来是通过客户端完成渲染,现在统一到服务端node服务去做。尤其是高并发访问的情况,会大量占用服务端CPU资源;
+2)开发条件受限
+在服务端渲染中,只会执行到componentDidMount之前的生命周期钩子,因此项目引用的第三方的库也不可用其它生命周期钩子,这对引用库的选择产生了很大的限制;
+3)学习成本相对较高 +除了对webpack、MVVM框架要熟悉,还需要掌握node、 Koa2等相关技术。相对于客户端渲染,项目构建、部署过程更加复杂。
+时间耗时比较:
+1)数据请求
+由服务端请求首屏数据,而不是客户端请求首屏数据,这是"快"的一个主要原因。服务端在内网进行请求,数据响应速度快。客户端在不同网络环境进行数据请求,且外网http请求开销大,导致时间差
+
+2)html渲染
+服务端渲染是先向后端服务器请求数据,然后生成完整首屏 html返回给浏览器;而客户端渲染是等js代码下载、加载、解析完成后再请求数据渲染,等待的过程页面是什么都没有的,就是用户看到的白屏。就是服务端渲染不需要等待js代码下载完成并请求数据,就可以返回一个已有完整数据的首屏页面。
JSX 是一个 JavaScript 的语法扩展,或者说是一个类似于 XML 的 ECMAScript 语法扩展。它本身没有太多的语法定义,也不期望引入更多的标准。
+其实 React 本身并不强制使用 JSX。在没有 JSX 的时候,React 实现一个组件依赖于使用 React.createElement 函数。代码如下:
+class Hello extends React.Component {
+ render() {
+ return React.createElement(
+ 'div',
+ null,
+ `Hello ${this.props.toWhat}`
+ );
+ }
+}
+ReactDOM.render(
+ React.createElement(Hello, {toWhat: 'World'}, null),
+ document.getElementById('root')
+);
+而 JSX 更像是一种语法糖,通过类似 XML 的描述方式,描写函数对象。在采用 JSX 之后,这段代码会这样写:
+class Hello extends React.Component {
+ render() {
+ return <div>Hello {this.props.toWhat}</div>;
+ }
+}
+ReactDOM.render(
+ <Hello toWhat="World" />,
+ document.getElementById('root')
+);
+通过对比,可以清晰地发现,代码变得更为简洁,而且代码结构层次更为清晰。
+因为 React 需要将组件转化为虚拟 DOM 树,所以在编写代码时,实际上是在手写一棵结构树。而XML 在树结构的描述上天生具有可读性强的优势。
+但这样可读性强的代码仅仅是给写程序的同学看的,实际上在运行的时候,会使用 Babel 插件将 JSX 语法的代码还原为 React.createElement 的代码。
+总结: +JSX 是一个 JavaScript 的语法扩展,结构类似 XML。JSX 主要用于声明 React 元素,但 React 中并不强制使用 JSX。即使使用了 JSX,也会在构建过程中,通过 Babel 插件编译为 React.createElement。所以 JSX 更像是 React.createElement 的一种语法糖。
+React 团队并不想引入 JavaScript 本身以外的开发体系。而是希望通过合理的关注点分离保持组件开发的纯粹性。
+使用了装饰模式,高阶组件的运用:
+function withWindowWidth(BaseComponent) {
+ class DerivedClass extends React.Component {
+ state = {
+ windowWidth: window.innerWidth,
+ }
+ onResize = () => {
+ this.setState({
+ windowWidth: window.innerWidth,
+ })
+ }
+ componentDidMount() {
+ window.addEventListener('resize', this.onResize)
+ }
+ componentWillUnmount() {
+ window.removeEventListener('resize', this.onResize);
+ }
+ render() {
+ return <BaseComponent {...this.props} {...this.state}/>
+ }
+ }
+ return DerivedClass;
+}
+const MyComponent = (props) => {
+ return <div>Window width is: {props.windowWidth}</div>
+};
+export default withWindowWidth(MyComponent);
+装饰模式的特点是不需要改变 被装饰对象 本身,而只是在外面套一个外壳接口。JavaScript 目前已经有了原生装饰器的提案,其用法如下:
+@testable
+ class MyTestableClass {
+}
\ No newline at end of file
diff --git a/src/content/notes/frontend/vue-part-1.html b/src/content/notes/frontend/vue-part-1.html
new file mode 100644
index 0000000..a33c771
--- /dev/null
+++ b/src/content/notes/frontend/vue-part-1.html
@@ -0,0 +1,1348 @@
+当一个Vue实例创建时,Vue会遍历data中的属性,用 Object.defineProperty(vue3.0使用proxy )将它们转为 getter/setter,并且在内部追踪相关依赖,在属性被访问和修改时通知变化。 每个组件实例都有相应的 watcher 程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter被调用时,会通知watcher重新计算,从而致使它关联的组件得以更新。
+
Vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。主要分为以下几个步骤:
+在对一些属性进行操作时,使用这种方法无法拦截,比如通过下标方式修改数组数据或者给对象新增属性,这都不能触发组件的重新渲染,因为 Object.defineProperty 不能拦截到这些操作。更精确的来说,对于数组而言,大部分操作都是拦截不到的,只是 Vue 内部通过重写函数的方式解决了这个问题。
+在 Vue3.0 中已经不使用这种方式了,而是通过使用 Proxy 对对象进行代理,从而实现数据劫持。使用Proxy 的好处是它可以完美的监听到任何方式的数据改变,唯一的缺点是兼容性的问题,因为 Proxy 是 ES6 的语法。
+MVC、MVP 和 MVVM 是三种常见的软件架构设计模式,主要通过分离关注点的方式来组织代码结构,优化开发效率。
+在开发单页面应用时,往往一个路由页面对应了一个脚本文件,所有的页面逻辑都在一个脚本文件里。页面的渲染、数据的获取,对用户事件的响应所有的应用逻辑都混合在一起,这样在开发简单项目时,可能看不出什么问题,如果项目变得复杂,那么整个文件就会变得冗长、混乱,这样对项目开发和后期的项目维护是非常不利的。
+(1)MVC
+MVC 通过分离 Model、View 和 Controller 的方式来组织代码结构。其中 View 负责页面的显示逻辑,Model 负责存储页面的业务数据,以及对相应数据的操作。并且 View 和 Model 应用了观察者模式,当 Model 层发生改变的时候它会通知有关 View 层更新页面。Controller 层是 View 层和 Model 层的纽带,它主要负责用户与应用的响应操作,当用户与页面产生交互的时候,Controller 中的事件触发器就开始工作了,通过调用 Model 层,来完成对 Model 的修改,然后 Model 层再去通知 View 层更新。
+
(2)MVVM
+MVVM 分为 Model、View、ViewModel:
+Model和View并无直接关联,而是通过ViewModel来进行联系的,Model和ViewModel之间有着双向数据绑定的联系。因此当Model中的数据改变时会触发View层的刷新,View中由于用户交互操作而改变的数据也会在Model中同步。
+这种模式实现了 Model和View的数据自动同步,因此开发者只需要专注于数据的维护操作即可,而不需要自己操作DOM。
+
(3)MVP
+MVP 模式与 MVC 唯一不同的在于 Presenter 和 Controller。在 MVC 模式中使用观察者模式,来实现当 Model 层数据发生变化的时候,通知 View 层的更新。这样 View 层和 Model 层耦合在一起,当项目逻辑变得复杂的时候,可能会造成代码的混乱,并且可能会对代码的复用性造成一些问题。MVP 的模式通过使用 Presenter 来实现对 View 层和 Model 层的解耦。MVC 中的Controller 只知道 Model 的接口,因此它没有办法控制 View 层的更新,MVP 模式中,View 层的接口暴露给了 Presenter 因此可以在 Presenter 中将 Model 的变化和 View 的变化绑定在一起,以此来实现 View 和 Model 的同步更新。这样就实现了对 View 和 Model 的解耦,Presenter 还包含了其他的响应逻辑。
+对于Computed:
+对于Watch:
+当想要执行异步或者昂贵的操作以响应不断的变化时,就需要使用watch。
+总结:
+运用场景:
+可以将同一函数定义为一个 method 或者一个计算属性。对于最终的结果,两种方式是相同的
+不同点:
+slot又名插槽,是Vue的内容分发机制,组件内部的模板引擎使用slot元素作为承载分发内容的出口。插槽slot是子组件的一个模板标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决定的。slot又分三类,默认插槽,具名插槽和作用域插槽。
+实现原理:当子组件vm实例化时,获取到父组件传入的slot标签的内容,存放在vm.$slot中,默认插槽为vm.$slot.default,具名插槽为vm.$slot.xxx,xxx 为插槽名,当组件执行渲染函数时候,遇到slot标签,使用$slot中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。
根据过滤器的名称,过滤器是用来过滤数据的,在Vue中使用filters来过滤数据,filters不会修改数据,而是过滤数据,改变用户看到的输出(计算属性 computed ,方法 methods 都是通过修改数据来处理数据格式的输出显示)。
使用场景:
+fliters过滤器来处理数据。过滤器是一个函数,它会把表达式中的值始终当作函数的第一个参数。过滤器用在插值表达式 {{ }} 和 v-bind 表达式 中,然后放在操作符“ | ”后面进行指示。
例如,在显示金额,给商品价格添加单位:
+<li>商品价格:{{item.price | filterPrice}}</li>
+
+ filters: {
+ filterPrice (price) {
+ return price ? ('¥' + price) : '--'
+ }
+ }
+既然是要保持页面的状态(其实也就是组件的状态),那么会出现以下两种情况:
+那么可以按照这两种情况分别得到以下方法:
+组件会被卸载:
+(1)将状态存储在LocalStorage / SessionStorage
+只需要在组件即将被销毁的生命周期 componentWillUnmount (react)中在 LocalStorage / SessionStorage 中把当前组件的 state 通过 JSON.stringify() 储存下来就可以了。在这里面需要注意的是组件更新状态的时机。
比如从 B 组件跳转到 A 组件的时候,A 组件需要更新自身的状态。但是如果从别的组件跳转到 B 组件的时候,实际上是希望 B 组件重新渲染的,也就是不要从 Storage 中读取信息。所以需要在 Storage 中的状态加入一个 flag 属性,用来控制 A 组件是否读取 Storage 中的状态。
+优点:
+缺点:
+(2)路由传值
+通过 react-router 的 Link 组件的 prop —— to 可以实现路由间传递参数的效果。
+在这里需要用到 state 参数,在 B 组件中通过 history.location.state 就可以拿到 state 值,保存它。返回 A 组件时再次携带 state 达到路由状态保持的效果。
+优点:
+缺点:
+组件不会被卸载:
+(1)单页面渲染
+要切换的组件作为子组件全屏渲染,父组件中正常储存页面状态。
+优点:
+缺点:
+除此之外,在Vue中,还可以是用keep-alive来缓存页面,当组件在keep-alive内被切换时组件的activated、deactivated这两个生命周期钩子函数会被执行 +被包裹在keep-alive中的组件的状态将会被保留:
+<keep-alive>
+ <router-view v-if="$route.meta.keepAlive"></router-view>
+</kepp-alive>
+router.js
+{
+ path: '/',
+ name: 'xxx',
+ component: ()=>import('../src/views/xxx.vue'),
+ meta:{
+ keepAlive: true // 需要被缓存
+ }
+},
+.stop:等同于 JavaScript 中的 event.stopPropagation() ,防止事件冒泡;.prevent :等同于 JavaScript 中的 event.preventDefault() ,防止执行预设的行为(如果事件可取消,则取消该事件,而不停止事件的进一步传播);.capture :与事件冒泡的方向相反,事件捕获由外到内;.self :只会触发自己范围内的事件,不包含子元素;.once :只会触发一次。(1)作用在表单元素上 +动态绑定了 input 的 value 指向了 messgae 变量,并且在触发 input 事件的时候去动态把 message设置为目标值:
+<input v-model="sth" />
+// 等同于
+<input
+ v-bind:value="message"
+ v-on:input="message=$event.target.value"
+>
+//$event 指代当前触发的事件对象;
+//$event.target 指代当前触发的事件对象的dom;
+//$event.target.value 就是当前dom的value值;
+//在@input方法中,value => sth;
+//在:value中,sth => value;
+(2)作用在组件上 +在自定义组件中,v-model 默认会利用名为 value 的 prop和名为 input 的事件
+本质是一个父子组件通信的语法糖,通过prop和$.emit实现。 因此父组件 v-model 语法糖本质上可以修改为:
+<child :value="message" @input="function(e){message = e}"></child>
+在组件的实现中,可以通过 v-model属性来配置子组件接收的prop名称,以及派发的事件名称。 +例子:
+// 父组件
+<aa-input v-model="aa"></aa-input>
+// 等价于
+<aa-input v-bind:value="aa" v-on:input="aa=$event.target.value"></aa-input>
+
+// 子组件:
+<input v-bind:value="aa" v-on:input="onmessage"></aa-input>
+
+props:{value:aa,}
+methods:{
+ onmessage(e){
+ $emit('input',e.target.value)
+ }
+}
+默认情况下,一个组件上的v-model 会把 value 用作 prop且把 input 用作 event。但是一些输入类型比如单选框和复选框按钮可能想使用 value prop 来达到不同的目的。使用 model 选项可以回避这些情况产生的冲突。js 监听input 输入框输入数据改变,用oninput,数据改变以后就会立刻出发这个事件。通过input事件把数据$emit 出去,在父组件接受。父组件设置v-model的值为input $emit过来的值。
可以。v-model 实际上是一个语法糖,如:
+<input v-model="searchText">
+实际上相当于:
+<input
+ v-bind:value="searchText"
+ v-on:input="searchText = $event.target.value"
+>
+用在自定义组件上也是同理:
+<custom-input v-model="searchText">
+相当于:
+<custom-input
+ v-bind:value="searchText"
+ v-on:input="searchText = $event"
+></custom-input>
+显然,custom-input 与父组件的交互如下:
+searchText变量传入custom-input 组件,使用的 prop 名为value;input的事件,父组件将接收到的值赋值给searchText;所以,custom-input 组件的实现应该类似于这样:
+Vue.component('custom-input', {
+ props: ['value'],
+ template: `
+ <input
+ v-bind:value="value"
+ v-on:input="$emit('input', $event.target.value)"
+ >
+ `
+})
+JavaScript中的对象是引用类型的数据,当多个实例引用同一个对象时,只要一个实例对这个对象进行操作,其他实例中的数据也会发生变化。
+而在Vue中,更多的是想要复用组件,那就需要每个组件都有自己的数据,这样组件之间才不会相互干扰。
+所以组件的数据不能写成对象的形式,而是要写成函数的形式。数据以函数返回值的形式定义,这样当每次复用组件的时候,就会返回一个新的data,也就是说每个组件都有自己的私有数据空间,它们各自维护自己的数据,不会干扰其他组件的正常运行。
+如果需要在组件切换的时候,保存一些组件的状态防止多次渲染,就可以使用 keep-alive 组件包裹需要保存的组件。
+(1)keep-alive
+keep-alive有以下三个属性:
+注意:keep-alive 包裹动态组件时,会缓存不活动的组件实例。
+主要流程
+(2)keep-alive 的实现
+const patternTypes: Array<Function> = [String, RegExp, Array] // 接收:字符串,正则,数组
+
+export default {
+ name: 'keep-alive',
+ abstract: true, // 抽象组件,是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在父组件链中。
+
+ props: {
+ include: patternTypes, // 匹配的组件,缓存
+ exclude: patternTypes, // 不去匹配的组件,不缓存
+ max: [String, Number], // 缓存组件的最大实例数量, 由于缓存的是组件实例(vnode),数量过多的时候,会占用过多的内存,可以用max指定上限
+ },
+
+ created() {
+ // 用于初始化缓存虚拟DOM数组和vnode的key
+ this.cache = Object.create(null)
+ this.keys = []
+ },
+
+ destroyed() {
+ // 销毁缓存cache的组件实例
+ for (const key in this.cache) {
+ pruneCacheEntry(this.cache, key, this.keys)
+ }
+ },
+
+ mounted() {
+ // prune 削减精简[v.]
+ // 去监控include和exclude的改变,根据最新的include和exclude的内容,来实时削减缓存的组件的内容
+ this.$watch('include', (val) => {
+ pruneCache(this, (name) => matches(val, name))
+ })
+ this.$watch('exclude', (val) => {
+ pruneCache(this, (name) => !matches(val, name))
+ })
+ },
+}
+render函数:
+render () {
+ //
+ function getFirstComponentChild (children: ?Array<VNode>): ?VNode {
+ if (Array.isArray(children)) {
+ for (let i = 0; i < children.length; i++) {
+ const c = children[i]
+ if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {
+ return c
+ }
+ }
+ }
+ }
+ const slot = this.$slots.default // 获取默认插槽
+ const vnode: VNode = getFirstComponentChild(slot)// 获取第一个子组件
+ const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions // 组件参数
+ if (componentOptions) { // 是否有组件参数
+ // check pattern
+ const name: ?string = getComponentName(componentOptions) // 获取组件名
+ const { include, exclude } = this
+ if (
+ // not included
+ (include && (!name || !matches(include, name))) ||
+ // excluded
+ (exclude && name && matches(exclude, name))
+ ) {
+ // 如果不匹配当前组件的名字和include以及exclude
+ // 那么直接返回组件的实例
+ return vnode
+ }
+
+ const { cache, keys } = this
+
+ // 获取这个组件的key
+ const key: ?string = vnode.key == null
+ // same constructor may get registered as different local components
+ // so cid alone is not enough (#3269)
+ ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
+ : vnode.key
+
+ if (cache[key]) {
+ // LRU缓存策略执行
+ vnode.componentInstance = cache[key].componentInstance // 组件初次渲染的时候componentInstance为undefined
+
+ // make current key freshest
+ remove(keys, key)
+ keys.push(key)
+ // 根据LRU缓存策略执行,将key从原来的位置移除,然后将这个key值放到最后面
+ } else {
+ // 在缓存列表里面没有的话,则加入,同时判断当前加入之后,是否超过了max所设定的范围,如果是,则去除
+ // 使用时间间隔最长的一个
+ cache[key] = vnode
+ keys.push(key)
+ // prune oldest entry
+ if (this.max && keys.length > parseInt(this.max)) {
+ pruneCacheEntry(cache, keys[0], keys, this._vnode)
+ }
+ }
+ // 将组件的keepAlive属性设置为true
+ vnode.data.keepAlive = true // 作用:判断是否要执行组件的created、mounted生命周期函数
+ }
+ return vnode || (slot && slot[0])
+}
+keep-alive 具体是通过 cache 数组缓存所有组件的 vnode 实例。当 cache 内原有组件被使用时会将该组件 key 从 keys 数组中删除,然后 push 到 keys数组最后,以便清除最不常用组件。
+实现步骤:
+(3)keep-alive 本身的创建过程和 patch 过程
+缓存渲染的时候,会根据 vnode.componentInstance(首次渲染 vnode.componentInstance 为 undefined) 和 keepAlive 属性判断不会执行组件的 created、mounted 等钩子函数,而是对缓存的组件执行 patch 过程∶ 直接把缓存的 DOM 对象直接插入到目标元素中,完成了数据更新的情况下的渲染过程。
+首次渲染
+// core/instance/lifecycle
+function initLifecycle (vm: Component) {
+ const options = vm.$options
+
+ // locate first non-abstract parent
+ let parent = options.parent
+ if (parent && !options.abstract) { // 判断组件的abstract属性,才往父组件里面挂载DOM
+ while (parent.$options.abstract && parent.$parent) {
+ parent = parent.$parent
+ }
+ parent.$children.push(vm)
+ }
+
+ vm.$parent = parent
+ vm.$root = parent ? parent.$root : vm
+
+ vm.$children = []
+ vm.$refs = {}
+
+ vm._watcher = null
+ vm._inactive = null
+ vm._directInactive = false
+ vm._isMounted = false
+ vm._isDestroyed = false
+ vm._isBeingDestroyed = false
+}
+// core/vdom/create-component
+init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
+ if (
+ vnode.componentInstance &&
+ !vnode.componentInstance._isDestroyed &&
+ vnode.data.keepAlive
+ ) { // componentInstance在初次是undefined!!!
+ // kept-alive components, treat as a patch
+ const mountedNode: any = vnode // work around flow
+ componentVNodeHooks.prepatch(mountedNode, mountedNode) // prepatch函数执行的是组件更新的过程
+ } else {
+ const child = vnode.componentInstance = createComponentInstanceForVnode(
+ vnode,
+ activeInstance
+ )
+ child.$mount(hydrating ? vnode.elm : undefined, hydrating)
+ }
+ },
+prepatch 操作就不会在执行组件的 mounted 和 created 生命周期函数,而是直接将 DOM 插入
+(4)LRU (least recently used)缓存策略
+LRU 缓存策略∶ 从内存中找出最久未使用的数据并置换新的数据。 +LRU(Least rencently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是 "如果数据最近被访问过,那么将来被访问的几率也更高"。 最常见的实现是使用一个链表保存缓存数据,详细算法实现如下∶
+Vue 的 nextTick 其本质是对 JavaScript 执行原理 EventLoop 的一种应用。
+nextTick 的核心是利用了如 Promise 、MutationObserver、setImmediate、setTimeout的原生 JavaScript 方法来模拟对应的微/宏任务的实现,本质是为了利用 JavaScript 的这些异步回调任务队列来实现 Vue 框架中自己的异步回调队列。
+nextTick 不仅是 Vue 内部的异步队列的调用方法,同时也允许开发者在实际项目中使用这个方法来满足实际应用中对 DOM 更新数据时机的后续逻辑处理
+nextTick 是典型的将底层 JavaScript 执行原理应用到具体案例中的示例,引入异步更新队列机制的原因∶
+Vue采用了数据驱动视图的思想,但是在一些情况下,仍然需要操作DOM。有时候,可能遇到这样的情况,DOM1的数据发生了变化,而DOM2需要从DOM1中获取数据,那这时就会发现DOM2的视图并没有更新,这时就需要用到了nextTick了。
由于Vue的DOM操作是异步的,所以,在上面的情况中,就要将DOM2获取数据的操作写在$nextTick中。
this.$nextTick(() => { // 获取数据的操作...})
+所以,在以下情况下,会用到nextTick:
+nextTick()的回调函数中。nextTick()的回调函数中。因为在created()钩子函数中,页面的DOM还未渲染,这时候也没办法操作DOM,所以,此时如果想要操作DOM,必须将操作的代码放在nextTick()的回调函数中。
<template>
+ <div>
+ <ul>
+ <li v-for="value in obj" :key="value"> {{value}} </li>
+ </ul>
+ <button @click="addObjB">添加 obj.b</button>
+ </div>
+</template>
+
+<script>
+ export default {
+ data () {
+ return {
+ obj: {
+ a: 'obj.a'
+ }
+ }
+ },
+ methods: {
+ addObjB () {
+ this.obj.b = 'obj.b'
+ console.log(this.obj)
+ }
+ }
+ }
+</script>
+点击 button 会发现,obj.b 已经成功添加,但是视图并未刷新。这是因为在Vue实例创建时,obj.b并未声明,因此就没有被Vue转换为响应式的属性,自然就不会触发视图的更新,这时就需要使用Vue的全局 api $set():
+addObjB () (
+ this.$set(this.obj, 'b', 'obj.b')
+ console.log(this.obj)
+}
+$set()方法相当于手动的去把obj.b处理成一个响应式的属性,此时视图也会跟着改变了。
+在Vue中,对响应式处理利用的是Object.defineProperty对数据进行拦截,而这个方法并不能监听到数组内部变化,数组长度变化,数组的截取变化等,所以需要对这些操作进行hack,让Vue能监听到其中的变化。
+
+那Vue是如何实现让这些数组方法实现元素的实时更新的呢,下面是Vue中对这些方法的封装:
// 缓存数组原型
+const arrayProto = Array.prototype;
+// 实现 arrayMethods.__proto__ === Array.prototype
+export const arrayMethods = Object.create(arrayProto);
+// 需要进行功能拓展的方法
+const methodsToPatch = [
+ "push",
+ "pop",
+ "shift",
+ "unshift",
+ "splice",
+ "sort",
+ "reverse"
+];
+
+/**
+ * Intercept mutating methods and emit events
+ */
+methodsToPatch.forEach(function(method) {
+ // 缓存原生数组方法
+ const original = arrayProto[method];
+ def(arrayMethods, method, function mutator(...args) {
+ // 执行并缓存原生数组功能
+ const result = original.apply(this, args);
+ // 响应式处理
+ const ob = this.__ob__;
+ let inserted;
+ switch (method) {
+ // push、unshift会新增索引,所以要手动observer
+ case "push":
+ case "unshift":
+ inserted = args;
+ break;
+ // splice方法,如果传入了第三个参数,也会有索引加入,也要手动observer。
+ case "splice":
+ inserted = args.slice(2);
+ break;
+ }
+ //
+ if (inserted) ob.observeArray(inserted);// 获取插入的值,并设置响应式监听
+ // notify change
+ ob.dep.notify();// 通知依赖更新
+ // 返回原生数组方法的执行结果
+ return result;
+ });
+});
+简单来说就是,重写了数组中的那些原生方法,首先获取到这个数组的__ob__,也就是它的Observer对象,如果有新的值,就调用observeArray继续对新的值观察变化(也就是通过target__proto__ == arrayMethods来改变了数组实例的型),然后手动调用notify,通知渲染watcher,执行update。
概念:
+区别:
+
vue的模版编译过程主要如下:template -> ast -> render函数
+vue 在模版编译版本的码中会执行 compileToFunctions 将template转化为render函数:
+// 将模板编译为render函数const { render, staticRenderFns } = compileToFunctions(template,options//省略}, this)
+CompileToFunctions中的主要逻辑如下∶ +(1)调用parse方法将template转化为ast(抽象语法树)
+constast = parse(template.trim(), options)
+AST元素节点总共三种类型:type为1表示普通元素、2为表达式、3为纯文本
+(2)对静态节点做优化
+optimize(ast,options)
+这个过程主要分析出哪些是静态节点,给其打一个标记,为后续更新渲染可以直接跳过静态节点做优化
+深度遍历AST,查看每个子树的节点元素是否为静态节点或者静态节点根。如果为静态节点,他们生成的DOM永远不会改变,这对运行时模板更新起到了极大的优化作用。
+(3)生成代码
+const code = generate(ast, options)
+generate将ast抽象语法树编译成 render字符串并将静态部分放到 staticRenderFns 中,最后通过 new Function(`` render``) 生成render函数。
不会立即同步执行重新渲染。Vue 实现响应式并不是数据发生变化之后 DOM 立即变化,而是按一定的策略进行 DOM 的更新。Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化, Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。
+如果同一个watcher被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环tick中,Vue 刷新队列并执行实际(已去重的)工作。
+(1)mixin 和 extends +mixin 和 extends均是用于合并、拓展组件的,两者均通过 mergeOptions 方法实现合并。
+
+(2)mergeOptions 的执行过程
if(!child._base) { if(child.extends) { parent = mergeOptions(parent, child.extends, vm) } if(child.mixins) { for(let i = 0, l = child.mixins.length; i < l; i++){ parent = mergeOptions(parent, child.mixins[i], vm) } }}
+在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。 +一般需要对DOM元素进行底层操作时使用,尽量只用来操作 DOM展示,不修改内部的值。当使用自定义指令直接修改 value 值时绑定v-model的值也不会同步更新;如必须修改可以在自定义指令中使用keydown事件,在vue组件中使用 change事件,回调中修改vue数据;
+(1)自定义指令基本内容
+全局定义:Vue.directive("focus",{})
局部定义:directives:{focus:{}}
钩子函数:指令定义对象提供钩子函数
+o bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
+o inSerted:被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中)。
+o update:所在组件的VNode更新时调用,但是可能发生在其子VNode更新之前调用。指令的值可能发生了改变,也可能没有。但是可以通过比较更新前后的值来忽略不必要的模板更新。
+o ComponentUpdate:指令所在组件的 VNode及其子VNode全部更新后调用。
+o unbind:只调用一次,指令与元素解绑时调用。
+钩子函数参数 +o el:绑定元素
+o bing: 指令核心对象,描述指令全部信息属性
+o name
+o value
+o oldValue
+o expression
+o arg
+o modifers
+o vnode 虚拟节点
+o oldVnode:上一个虚拟节点(更新钩子函数中才有用)
+(2)使用场景
+普通DOM元素进行底层操作的时候,可以使用自定义指令
+自定义指令是用来操作DOM的。尽管Vue推崇数据驱动视图的理念,但并非所有情况都适合数据驱动。自定义指令就是一种有效的补充和扩展,不仅可用于定义任何的DOM操作,并且是可复用的。
+(3)使用案例
+初级应用:
+高级应用:
+子组件不可以直接改变父组件的数据。这样做主要是为了维护父子组件的单向数据流。每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。如果这样做了,Vue 会在浏览器的控制台中发出警告。
+Vue提倡单向数据流,即父级 props 的更新会流向子组件,但是反过来则不行。这是为了防止意外的改变父组件状态,使得应用的数据流变得难以理解,导致数据流混乱。如果破坏了单向数据流,当应用复杂时,debug 的成本会非常高。
+只能通过 $emit 派发一个自定义事件,父组件接收到后,由父组件修改。
在初始化 Vue 的每个组件时,会对组件的 data 进行初始化,就会将由普通对象变成响应式对象,在这个过程中便会进行依赖收集的相关逻辑,如下所示∶
+function defieneReactive (obj, key, val){
+ const dep = new Dep();
+ ...
+ Object.defineProperty(obj, key, {
+ ...
+ get: function reactiveGetter () {
+ if(Dep.target){
+ dep.depend();
+ ...
+ }
+ return val
+ }
+ ...
+ })
+}
+以上只保留了关键代码,主要就是 const dep = new Dep()实例化一个 Dep 的实例,然后在 get 函数中通过 dep.depend() 进行依赖收集。
+(1)Dep
+Dep是整个依赖收集的核心,其关键代码如下:
class Dep {
+ static target;
+ subs;
+
+ constructor () {
+ ...
+ this.subs = [];
+ }
+ addSub (sub) {
+ this.subs.push(sub)
+ }
+ removeSub (sub) {
+ remove(this.sub, sub)
+ }
+ depend () {
+ if(Dep.target){
+ Dep.target.addDep(this)
+ }
+ }
+ notify () {
+ const subs = this.subds.slice();
+ for(let i = 0;i < subs.length; i++){
+ subs[i].update()
+ }
+ }
+}
+Dep 是一个 class ,其中有一个关 键的静态属性 static,它指向了一个全局唯一 Watcher,保证了同一时间全局只有一个 watcher 被计算,另一个属性 subs 则是一个 Watcher 的数组,所以 Dep 实际上就是对 Watcher 的管理,再看看 Watcher 的相关代码∶
+(2)Watcher
+class Watcher {
+ getter;
+ ...
+ constructor (vm, expression){
+ ...
+ this.getter = expression;
+ this.get();
+ }
+ get () {
+ pushTarget(this);
+ value = this.getter.call(vm, vm)
+ ...
+ return value
+ }
+ addDep (dep){
+ ...
+ dep.addSub(this)
+ }
+ ...
+}
+function pushTarget (_target) {
+ Dep.target = _target
+}
+Watcher 是一个 class,它定义了一些方法,其中和依赖收集相关的主要有 get、addDep 等。
+(3)过程
+在实例化 Vue 时,依赖收集的相关过程如下∶ +初 始 化 状 态 initState , 这 中 间 便 会 通 过 defineReactive 将数据变成响应式对象,其中的 getter 部分便是用来依赖收集的。 +初始化最终会走 mount 过程,其中会实例化 Watcher ,进入 Watcher 中,便会执行 this.get() 方法,
+updateComponent = () => {
+ vm._update(vm._render())
+}
+new Watcher(vm, updateComponent)
+get 方法中的 pushTarget 实际上就是把 Dep.target 赋值为当前的 watcher。
+this.getter.call(vm,vm),这里的 getter 会执行 vm._render() 方法,在这个过程中便会触发数据对象的 getter。那么每个对象值的 getter 都持有一个 dep,在触发 getter 的时候会调用 dep.depend() 方法,也就会执行 Dep.target.addDep(this)。刚才 Dep.target 已经被赋值为 watcher,于是便会执行 addDep 方法,然后走到 dep.addSub() 方法,便将当前的 watcher 订阅到这个数据持有的 dep 的 subs 中,这个目的是为后续数据变化时候能通知到哪些 subs 做准备。所以在 vm._render() 过程中,会触发所有数据的 getter,这样便已经完成了一个依赖收集的过程。
+相似之处:
+不同之处 :
+1)数据流
+Vue默认支持数据双向绑定,而React一直提倡单向数据流
+2)虚拟DOM
+Vue2.x开始引入"Virtual DOM",消除了和React在这方面的差异,但是在具体的细节还是有各自的特点。
+3)组件化
+React与Vue最大的不同是模板的编写。
+具体来讲:React中render函数是支持闭包特性的,所以import的组件在render中可以直接调用。但是在Vue中,由于模板中使用的数据都必须挂在 this 上进行一次中转,所以 import 一个组件完了之后,还需要在 components 中再声明下。 +4)监听数据变化的实现原理不同
+5)高阶组件
+react可以通过高阶组件(HOC)来扩展,而Vue需要通过mixins来扩展。
+高阶组件就是高阶函数,而React的组件本身就是纯粹的函数,所以高阶函数对React来说易如反掌。相反Vue.js使用HTML模板创建视图组件,这时模板无法有效的编译,因此Vue不能采用HOC来实现。
+6)构建工具
+两者都有自己的构建工具:
+7)跨平台
+kb ;angular 的特点,在数据操作方面更为简单;react 的优点,实现了 html 的封装和重用,在构建单页面应用方面有着独特的优势;dom 操作是非常耗费性能的,不再使用原生的 dom 操作节点,极大解放 dom 操作,但具体操作的还是 dom 不过是换了另一种方式;react 而言,同样是操作虚拟 dom,就性能而言, vue 存在很大的优势。相同点: assets 和 static 两个都是存放静态资源文件。项目中所需要的资源文件图片,字体图标,样式文件等都可以放在这两个文件下,这是相同点
不相同点:assets 中存放的静态资源文件在项目打包时,也就是运行 npm run build 时会将 assets 中放置的静态资源文件进行打包上传,所谓打包简单点可以理解为压缩体积,代码格式化。而压缩后的静态资源文件最终也都会放置在 static 文件中跟着 index.html 一同上传至服务器。static 中放置的静态资源文件就不会要走打包压缩格式化等流程,而是直接进入打包好的目录,直接上传至服务器。因为避免了压缩直接进行上传,在打包时会提高一定的效率,但是 static 中的资源文件由于没有进行压缩等操作,所以文件的体积也就相对于 assets 中打包后的文件提交较大点。在服务器中就会占据更大的空间。
建议: 将项目中 template需要的样式文件js文件等都可以放置在 assets 中,走打包这一流程。减少体积。而项目中引入的第三方的资源文件如iconfoont.css 等文件可以放置在 static 中,因为这些引入的第三方文件已经经过处理,不再需要处理,直接上传。
delete 只是被删除的元素变成了 empty/undefined 其他的元素的键值还是不变。Vue.delete 直接删除了数组 改变了数组的键值。当在项目中直接设置数组的某一项的值,或者直接设置对象的某个属性值,这个时候,你会发现页面并没有更新。这是因为Object.defineProperty()限制,监听不到变化。
+解决方式:
+this.$set(this.arr, 0, "OBKoro1"); // 改变数组this.$set(this.obj, "c", "OBKoro1"); // 改变对象
+splice()、 push()、pop()、shift()、unshift()、sort()、reverse()
+vue源码里缓存了array的原型链,然后重写了这几个方法,触发这几个方法的时候会observer数据,意思是使用这些方法不用再进行额外的操作,视图自动进行更新。 推荐使用splice方法会比较好自定义,因为splice可以在数组的任何位置进行删除/添加操作
+vm.$set 的实现原理是:
vue中的模板template无法被浏览器解析并渲染,因为这不属于浏览器的标准,不是正确的HTML语法,所有需要将template转化成一个JavaScript函数,这样浏览器就可以执行这一个函数并渲染出对应的HTML元素,就可以让视图跑起来了,这一个转化的过程,就成为模板编译。模板编译又分三个阶段,解析parse,优化optimize,生成generate,最终生成可执行函数render。
+SSR也就是服务端渲染,也就是将Vue在客户端把标签渲染成HTML的工作放在服务端完成,然后再把html直接返回给客户端
+SSR的优势:
+SSR的缺点:
+(1)编码阶段
+(2)SEO优化
+(3)打包优化
+(4)用户体验
+SPA( single-page application )仅在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载。
+优点:
+缺点:
+对于 runtime 来说,只需要保证组件存在 render 函数即可,而有了预编译之后,只需要保证构建过程中生成 render 函数就可以。在 webpack 中,使用vue-loader编译.vue文件,内部依赖的vue-template-compiler模块,在 webpack 构建过程中,将template预编译成 render 函数。与 react 类似,在添加了jsx的语法糖解析器babel-plugin-transform-vue-jsx之后,就可以直接手写render函数。
所以,template和jsx的都是render的一种表现形式,不同的是:JSX相对于template而言,具有更高的灵活性,在复杂的组件中,更具有优势,而 template 虽然显得有些呆滞。但是 template 在代码结构上更符合视图与逻辑分离的习惯,更简单、更直观、更好维护。
+使用vue开发时,在vue初始化之前,由于div是不归vue管的,所以我们写的代码在还没有解析的情况下会容易出现花屏现象,看到类似于{{message}}的字样,虽然一般情况下这个时间很短暂,但是还是有必要让解决这个问题的。
+首先:在css里加上以下代码:
+[v-cloak] { display: none;}
+如果没有彻底解决问题,则在根元素加上style="display: none;" :style="{display: 'block'}"
这个 API 很少用到,作用是扩展组件生成一个构造器,通常会与 $mount 一起使用。
// 创建组件构造器let Component = Vue.extend({ template: '<div>test</div>'})// 挂载到 #app 上new Component().$mount('#app')// 除了上面的方式,还可以用来扩展已有的组件let SuperComponent = Vue.extend(Component)new SuperComponent({ created() { console.log(1) }})new SuperComponent().$mount('#app')
+优点:
+缺点:
+Vue 实例有⼀个完整的⽣命周期,也就是从开始创建、初始化数据、编译模版、挂载Dom -> 渲染、更新 -> 渲染、卸载 等⼀系列过程,称这是Vue的⽣命周期。
+$el 属性。this 仍能获取到实例。另外还有 keep-alive 独有的生命周期,分别为 activated 和 deactivated 。用 keep-alive 包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated 钩子函数,命中缓存渲染后会执行 activated 钩子函数。
加载渲染过程:
+更新过程:
+销毁过程:
+我们可以在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。 +
+推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:
+keep-alive是 Vue 提供的一个内置组件,用来对组件进行缓存——在组件切换过程中将状态保留在内存中,防止重复渲染DOM。
+如果为一个组件包裹了 keep-alive,那么它会多出两个生命周期:deactivated、activated。同时,beforeDestroy 和 destroyed 就不会再被触发了,因为组件不会被真正销毁。
+当组件被换掉时,会被缓存到内存中、触发 deactivated 生命周期;当组件被切回来时,再去缓存里找这个组件、触发 activated钩子函数。
+组件通信的方式如下:
+父组件通过props向子组件传递数据,子组件通过$emit和父组件通信
props只能是父组件向子组件进行传值,props使得父子组件之间形成了一个单向下行绑定。子组件的数据会随着父组件不断更新。props 可以显示定义一个或一个以上的数据,对于接收的数据,可以是各种数据类型,同样也可以传递一个函数。props属性名规则:若在props中使用驼峰形式,模板中需要使用短横线的形式// 父组件
+<template>
+ <div id="father">
+ <son :msg="msgData" :fn="myFunction"></son>
+ </div>
+</template>
+
+<script>
+import son from "./son.vue";
+export default {
+ name: father,
+ data() {
+ msgData: "父组件数据";
+ },
+ methods: {
+ myFunction() {
+ console.log("vue");
+ }
+ },
+ components: {
+ son
+ }
+};
+</script>
+// 子组件
+<template>
+ <div id="son">
+ <p>{{msg}}</p>
+ <button @click="fn">按钮</button>
+ </div>
+</template>
+<script>
+export default {
+ name: "son",
+ props: ["msg", "fn"]
+};
+</script>
+$emit绑定一个自定义事件,当这个事件被执行的时就会将参数传递给父组件,而父组件通过v-on监听并接收参数。// 父组件
+<template>
+ <div class="section">
+ <com-article :articles="articleList" @onEmitIndex="onEmitIndex"></com-article>
+ <p>{{currentIndex}}</p>
+ </div>
+</template>
+
+<script>
+import comArticle from './test/article.vue'
+export default {
+ name: 'comArticle',
+ components: { comArticle },
+ data() {
+ return {
+ currentIndex: -1,
+ articleList: ['红楼梦', '西游记', '三国演义']
+ }
+ },
+ methods: {
+ onEmitIndex(idx) {
+ this.currentIndex = idx
+ }
+ }
+}
+</script>
+//子组件
+<template>
+ <div>
+ <div v-for="(item, index) in articles" :key="index" @click="emitIndex(index)">{{item}}</div>
+ </div>
+</template>
+
+<script>
+export default {
+ props: ['articles'],
+ methods: {
+ emitIndex(index) {
+ this.$emit('onEmitIndex', index) // 触发父组件的方法,并传递参数index
+ }
+ }
+}
+</script>
+$emit / $on)eventBus事件总线适用于父子组件、非父子组件等之间的通信,使用步骤如下:
+(1)创建事件中心管理组件之间的通信
// event-bus.js
+
+import Vue from 'vue'
+export const EventBus = new Vue()
+(2)发送事件
+假设有两个兄弟组件firstCom和secondCom:
<template>
+ <div>
+ <first-com></first-com>
+ <second-com></second-com>
+ </div>
+</template>
+
+<script>
+import firstCom from './firstCom.vue'
+import secondCom from './secondCom.vue'
+export default {
+ components: { firstCom, secondCom }
+}
+</script>
+在firstCom组件中发送事件:
<template>
+ <div>
+ <button @click="add">加法</button>
+ </div>
+</template>
+
+<script>
+import {EventBus} from './event-bus.js' // 引入事件中心
+
+export default {
+ data(){
+ return{
+ num:0
+ }
+ },
+ methods:{
+ add(){
+ EventBus.$emit('addition', {
+ num:this.num++
+ })
+ }
+ }
+}
+</script>
+(3)接收事件
+在secondCom组件中发送事件:
<template>
+ <div>求和: {{count}}</div>
+</template>
+
+<script>
+import { EventBus } from './event-bus.js'
+export default {
+ data() {
+ return {
+ count: 0
+ }
+ },
+ mounted() {
+ EventBus.$on('addition', param => {
+ this.count = this.count + param.num;
+ })
+ }
+}
+</script>
+在上述代码中,这就相当于将num值存贮在了事件总线中,在其他组件中可以直接访问。事件总线就相当于一个桥梁,不用组件通过它来通信。
虽然看起来比较简单,但是这种方法也有不变之处,如果项目过大,使用这种方式进行通信,后期维护起来会很困难。
+这种方式就是Vue中的依赖注入,该方法用于父子组件之间的通信。当然这里所说的父子不一定是真正的父子,也可以是祖孙组件,在层数很深的情况下,可以使用这种方法来进行传值。就不用一层一层的传递了。
+provide / inject是Vue提供的两个钩子,和data、methods是同级的。并且provide的书写形式和data一样。
provide 钩子用来发送数据或方法inject钩子用来接收数据或方法在父组件中:
+provide() {
+ return {
+ num: this.num
+ };
+}
+在子组件中:
+inject: ['num']
+还可以这样写,这样写就可以访问父组件中的所有属性:
+provide() {
+ return {
+ app: this
+ };
+}
+data() {
+ return {
+ num: 1
+ };
+}
+
+inject: ['app']
+console.log(this.app.num)
+注意: 依赖注入所提供的属性是非响应式的。
+这种方式也是实现父子组件之间的通信。
+ref: 这个属性用在子组件上,它的引用就指向了子组件的实例。可以通过实例来访问组件的数据和方法。
在子组件中:
+export default {
+ data () {
+ return {
+ name: 'JavaScript'
+ }
+ },
+ methods: {
+ sayHello () {
+ console.log('hello')
+ }
+ }
+}
+在父组件中:
+<template>
+ <child ref="child"></component-a>
+</template>
+<script>
+ import child from './child.vue'
+ export default {
+ components: { child },
+ mounted () {
+ console.log(this.$refs.child.name); // JavaScript
+ this.$refs.child.sayHello(); // hello
+ }
+ }
+</script>
+$parent / $children$parent可以让组件访问父组件的实例(访问的是上一级父组件的属性和方法)$children可以让组件访问子组件的实例,但是,$children并不能保证顺序,并且访问的数据也不是响应式的。在子组件中:
+<template>
+ <div>
+ <span>{{message}}</span>
+ <p>获取父组件的值为: {{parentVal}}</p>
+ </div>
+</template>
+
+<script>
+export default {
+ data() {
+ return {
+ message: 'Vue'
+ }
+ },
+ computed:{
+ parentVal(){
+ return this.$parent.msg;
+ }
+ }
+}
+</script>
+在父组件中:
+// 父组件中
+<template>
+ <div class="hello_world">
+ <div>{{msg}}</div>
+ <child></child>
+ <button @click="change">点击改变子组件值</button>
+ </div>
+</template>
+
+<script>
+import child from './child.vue'
+export default {
+ components: { child },
+ data() {
+ return {
+ msg: 'Welcome'
+ }
+ },
+ methods: {
+ change() {
+ // 获取到子组件
+ this.$children[0].message = 'JavaScript'
+ }
+ }
+}
+</script>
+在上面的代码中,子组件获取到了父组件的parentVal值,父组件改变了子组件中message的值。
+需要注意:
$parent访问到的是上一级父组件的实例,可以使用$root来访问根组件的实例$children拿到的是所有的子组件的实例,它是一个数组,并且是无序的#app上拿$parent得到的是new Vue()的实例,在这实例上再拿$parent得到的是undefined,而在最底层的子组件拿$children是个空数组$children 的值是数组,而$parent是个对象$attrs / $listeners考虑一种场景,如果A是B组件的父组件,B是C组件的父组件。如果想要组件A给组件C传递数据,这种隔代的数据,该使用哪种方式呢?
+如果是用props/$emit来一级一级的传递,确实可以完成,但是比较复杂;如果使用事件总线,在多人开发或者项目较大的时候,维护起来很麻烦;如果使用Vuex,的确也可以,但是如果仅仅是传递数据,那可能就有点浪费了。
针对上述情况,Vue引入了$attrs / $listeners,实现组件之间的跨代通信。
先来看一下inheritAttrs,它的默认值true,继承所有的父组件属性除props之外的所有属性;inheritAttrs:false 只继承class属性 。
$attrs:继承所有的父组件属性(除了prop传递的属性、class 和 style ),一般用在子组件的子元素上$listeners:该属性是一个对象,里面包含了作用在这个组件上的所有监听器,可以配合 v-on="$listeners" 将所有的事件监听器指向这个组件的某个特定的子元素。(相当于子组件继承父组件的事件)A组件(APP.vue):
<template>
+ <div id="app">
+ //此处监听了两个事件,可以在B组件或者C组件中直接触发
+ <child1 :p-child1="child1" :p-child2="child2" @test1="onTest1" @test2="onTest2"></child1>
+ </div>
+</template>
+<script>
+import Child1 from './Child1.vue';
+export default {
+ components: { Child1 },
+ methods: {
+ onTest1() {
+ console.log('test1 running');
+ },
+ onTest2() {
+ console.log('test2 running');
+ }
+ }
+};
+</script>
+B组件(Child1.vue):
<template>
+ <div class="child-1">
+ <p>props: {{pChild1}}</p>
+ <p>$attrs: {{$attrs}}</p>
+ <child2 v-bind="$attrs" v-on="$listeners"></child2>
+ </div>
+</template>
+<script>
+import Child2 from './Child2.vue';
+export default {
+ props: ['pChild1'],
+ components: { Child2 },
+ inheritAttrs: false,
+ mounted() {
+ this.$emit('test1'); // 触发APP.vue中的test1方法
+ }
+};
+</script>
+C 组件 (Child2.vue):
<template>
+ <div class="child-2">
+ <p>props: {{pChild2}}</p>
+ <p>$attrs: {{$attrs}}</p>
+ </div>
+</template>
+<script>
+export default {
+ props: ['pChild2'],
+ inheritAttrs: false,
+ mounted() {
+ this.$emit('test2');// 触发APP.vue中的test2方法
+ }
+};
+</script>
+在上述代码中:
+$listeners 属性$attrs属性,C组件可以直接获取到A组件中传递下来的props(除了B组件中props声明的)(1)父子组件间通信
+$refs 组件名来获得子组件,子组件通过 $parent 获得父组件,这样也可以实现通信。(2)兄弟组件间通信
+$parent/$refs 来获取到兄弟组件,也可以进行通信。(3)任意组件之间
+如果业务逻辑复杂,很多组件之间需要同时处理一些公共的数据,这个时候采用上面这一些方法可能不利于项目的维护。这个时候可以使用 vuex ,vuex 的思想就是将这一些公共的数据抽离出来,将它作为一个全局的变量来管理,然后其他组件就可以对这个公共数据进行读写操作,这样达到了解耦的目的。
\ No newline at end of file diff --git a/src/content/notes/frontend/vue-part-2.html b/src/content/notes/frontend/vue-part-2.html new file mode 100644 index 0000000..d749f19 --- /dev/null +++ b/src/content/notes/frontend/vue-part-2.html @@ -0,0 +1,625 @@ +非懒加载:
+import List from '@/components/list.vue'
+const router = new VueRouter({
+ routes: [
+ { path: '/list', component: List }
+ ]
+})
+(1)方案一(常用):使用箭头函数+import动态加载
+const List = () => import('@/components/list.vue')
+const router = new VueRouter({
+ routes: [
+ { path: '/list', component: List }
+ ]
+})
+(2)方案二:使用箭头函数+require动态加载
+const router = new Router({
+ routes: [
+ {
+ path: '/list',
+ component: resolve => require(['@/components/list'], resolve)
+ }
+ ]
+})
+(3)方案三:使用webpack的require.ensure技术,也可以实现按需加载。 这种情况下,多个路由指定相同的chunkName,会合并打包成一个js文件。
+// r就是resolve
+const List = r => require.ensure([], () => r(require('@/components/list')), 'list');
+// 路由也是正常的写法 这种是官方推荐的写的 按模块划分懒加载
+const router = new Router({
+ routes: [
+ {
+ path: '/list',
+ component: List,
+ name: 'list'
+ }
+ ]
+}))
+Vue-Router有两种模式:hash模式和history模式。默认的路由模式是hash模式。
+简介: hash模式是开发中默认的模式,它的URL带着一个#,例如:www.abc.com/#/vue,它的hash值就是#/vue。
特点:hash值会出现在URL里面,但是不会出现在HTTP请求中,对后端完全没有影响。所以改变hash值,不会重新加载页面。这种模式的浏览器支持度很好,低版本的IE浏览器也支持这种模式。hash路由被称为是前端路由,已经成为SPA(单页面应用)的标配。
+原理: hash模式的主要原理就是onhashchange()事件:
+window.onhashchange = function(event){
+ console.log(event.oldURL, event.newURL);
+ let hash = location.hash.slice(1);
+}
+使用onhashchange()事件的好处就是,在页面的hash值发生变化时,无需向后端发起请求,window就可以监听事件的改变,并按规则加载相应的代码。除此之外,hash值变化对应的URL都会被浏览器记录下来,这样浏览器就能实现页面的前进和后退。虽然是没有请求后端服务器,但是页面的hash值和对应的URL关联起来了。
+简介: history模式的URL中没有#,它使用的是传统的路由分发模式,即用户在输入一个URL时,服务器会接收这个请求,并解析这个URL,然后做出相应的逻辑处理。 +特点: 当使用history模式时,URL就像这样:abc.com/user/id。相比hash模式更加好看。但是,history模式需要后台配置支持。如果后台没有正确配置,访问时会返回404。 +API: history api可以分为两大部分,切换历史状态和修改历史状态:
+pushState() 和 replaceState() 方法,这两个方法应用于浏览器的历史记录栈,提供了对历史记录进行修改的功能。只是当他们进行修改时,虽然修改了url,但浏览器不会立即向后端发送请求。如果要做到改变url但又不刷新页面的效果,就需要前端用上这两个API。forward()、back()、go()三个方法,对应浏览器的前进,后退,跳转操作。虽然history模式丢弃了丑陋的#。但是,它也有自己的缺点,就是在刷新页面的时候,如果没有相应的路由或资源,就会刷出404来。
+如果想要切换到history模式,就要进行以下配置(后端也要进行配置):
+const router = new VueRouter({
+ mode: 'history',
+ routes: [...]
+})
+调用 history.pushState() 相比于直接修改 hash,存在以下优势:
+hash模式和history模式都有各自的优势和缺陷,还是要根据实际情况选择性的使用。
+(1)监听$route的变化
+// 监听,当路由发生变化的时候执行
+watch: {
+ $route: {
+ handler: function(val, oldVal){
+ console.log(val);
+ },
+ // 深度观察监听
+ deep: true
+ }
+},
+(2)window.location.hash读取#值 +window.location.hash 的值可读可写,读取来判断状态是否改变,写入时可以在不重载网页的前提下,添加一条历史访问记录。
+$route 和$router 的区别(1)param方式
+/router/:id/router/1231)路由定义
+//在APP.vue中
+<router-link :to="'/user/'+userId" replace>用户</router-link>
+
+//在index.js
+{
+ path: '/user/:userid',
+ component: User,
+},
+2)路由跳转
+// 方法1:
+<router-link :to="{ name: 'users', params: { uname: wade }}">按钮</router-link
+
+// 方法2:
+this.$router.push({name:'users',params:{uname:wade}})
+
+// 方法3:
+this.$router.push('/user/' + wade)
+3)参数获取
+通过 $route.params.userid 获取传递的值
(2)query方式
+/router,也就是普通配置/route?id=1231)路由定义
+//方式1:直接在router-link 标签上以对象的形式
+<router-link :to="{path:'/profile',query:{name:'why',age:28,height:188}}">档案</router-link>
+
+// 方式2:写成按钮以点击事件形式
+<button @click='profileClick'>我的</button>
+
+profileClick(){
+ this.$router.push({
+ path: "/profile",
+ query: {
+ name: "kobi",
+ age: "28",
+ height: 198
+ }
+ });
+}
+2)跳转方法
+// 方法1:
+<router-link :to="{ name: 'users', query: { uname: james }}">按钮</router-link>
+
+// 方法2:
+this.$router.push({ name: 'users', query:{ uname:james }})
+
+// 方法3:
+<router-link :to="{ path: '/user', query: { uname:james }}">按钮</router-link>
+
+// 方法4:
+this.$router.push({ path: '/user', query:{ uname:james }})
+
+// 方法5:
+this.$router.push('/user?uname=' + jsmes)
+3)获取参数
+通过$route.query 获取传递的值
+一、Vue-Router导航守卫
+有的时候,需要通过路由来进行一些操作,比如最常见的登录权限验证,当用户满足条件时,才让其进入导航,否则就取消跳转,并跳到登录页面让其登录。 +为此有很多种方法可以植入路由的导航过程:全局的,单个路由独享的,或者组件级的
+vue-router全局有三个路由钩子;
+具体使用∶
+router.beforeEach((to, from, next) => {
+ let ifInfo = Vue.prototype.$common.getSession('userData'); // 判断是否登录的存储信息
+ if (!ifInfo) {
+ // sessionStorage里没有储存user信息
+ if (to.path == '/') {
+ //如果是登录页面路径,就直接next()
+ next();
+ } else {
+ //不然就跳转到登录
+ Message.warning("请重新登录!");
+ window.location.href = Vue.prototype.$loginUrl;
+ }
+ } else {
+ return next();
+ }
+})
+router.afterEach((to, from) => {
+ // 跳转之后滚动条回到顶部
+ window.scrollTo(0,0);
+});
+beforeEnter +如果不想全局配置守卫的话,可以为某些路由单独配置守卫,有三个参数∶ to、from、next
+export default [
+ {
+ path: '/',
+ name: 'login',
+ component: login,
+ beforeEnter: (to, from, next) => {
+ console.log('即将进入登录页面')
+ next()
+ }
+ }
+]
+beforeRouteUpdate、beforeRouteEnter、beforeRouteLeave
+这三个钩子都有三个参数∶to、from、next
+注意点,beforeRouteEnter组件内还访问不到this,因为该守卫执行前组件实例还没有被创建,需要传一个回调给 next来访问,例如:
+beforeRouteEnter(to, from, next) {
+ next(target => {
+ if (from.path == '/classProcess') {
+ target.isFromProcess = true
+ }
+ })
+}
+二、Vue路由钩子在生命周期函数的体现
+路由导航、keep-alive、和组件生命周期钩子结合起来的,触发顺序,假设是从a组件离开,第一次进入b组件∶
+location.href= /url 来跳转,简单方便,但是刷新了页面;history.pushState( /url ) ,无刷新页面,静态跳转;router.push( /url ) 来跳转,使用了 diff 算法,实现了按需加载,减少了 dom 的消耗。其实使用 router 跳转和使用 history.pushState() 没什么差别的,因为vue-router就是用了 history.pushState() ,尤其是在history模式下。用法:query要用path来引入,params要用name来引入,接收参数都是类似的,分别是 this.$route.query.name 和 this.$route.params.name 。
url地址显示:query更加类似于ajax中get传参,params则类似于post,说的再简单一点,前者在浏览器地址栏中显示参数,后者则不显示
+注意:query刷新不会丢失query里面的数据 params刷新会丢失 params里面的数据。
+在前端技术早期,一个 url 对应一个页面,如果要从 A 页面切换到 B 页面,那么必然伴随着页面的刷新。这个体验并不好,不过在最初也是无奈之举——用户只有在刷新页面的情况下,才可以重新去请求数据。
+后来,改变发生了——Ajax 出现了,它允许人们在不刷新页面的情况下发起请求;与之共生的,还有“不刷新页面即可更新页面内容”这种需求。在这样的背景下,出现了 SPA(单页面应用)。
+SPA极大地提升了用户体验,它允许页面在不刷新的情况下更新页面内容,使内容的切换更加流畅。但是在 SPA 诞生之初,人们并没有考虑到“定位”这个问题——在内容切换前后,页面的 URL 都是一样的,这就带来了两个问题:
+为了解决这个问题,前端路由出现了。
+前端路由可以帮助我们在仅有一个页面的情况下,“记住”用户当前走到了哪一步——为 SPA 中的各个视图匹配一个唯一标识。这意味着用户前进、后退触发的新内容,都会映射到不同的 URL 上去。此时即便他刷新页面,因为当前的 URL 可以标识出他所处的位置,因此内容也不会丢失。
+那么如何实现这个目的呢?首先要解决两个问题:
+从这两个问题来看,服务端已经完全救不了这个场景了。所以要靠咱们前端自力更生,不然怎么叫“前端路由”呢?作为前端,可以提供这样的解决思路:
+Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。
+
+Vuex为Vue Components建立起了一个完整的生态圈,包括开发中的API调用一环。
+(1)核心流程中的主要功能:
(2)各模块在核心流程中的主要功能:
+Vue Components∶ Vue组件。HTML页面上,负责接收用户操作等交互行为,执行dispatch方法触发对应action进行回应。dispatch∶操作行为触发方法,是唯一能执行action的方法。actions∶ 操作行为处理模块。负责处理Vue Components接收到的所有交互行为。包含同步/异步操作,支持多个同名方法,按照注册的顺序依次触发。向后台API请求的操作就在这个模块中进行,包括触发其他action以及提交mutation的操作。该模块提供了Promise的封装,以支持action的链式触发。commit∶状态改变提交操作方法。对mutation进行提交,是唯一能执行mutation的方法。mutations∶状态改变操作方法。是Vuex修改state的唯一推荐方法,其他修改方式在严格模式下将会报错。该方法只能进行同步操作,且方法名只能全局唯一。操作之中会有一些hook暴露出来,以进行state的监控等。state∶ 页面状态管理容器对象。集中存储Vuecomponents中data对象的零散数据,全局唯一,以进行统一的状态管理。页面显示所需的数据从该对象中进行读取,利用Vue的细粒度数据响应机制来进行高效的状态更新。getters∶ state对象读取方法。图中没有单独列出该模块,应该被包含在了render中,Vue Components通过该方法读取全局state对象。mutation中的操作是一系列的同步函数,用于修改state中的变量的的状态。当使用vuex时需要通过commit来提交需要操作的内容。mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是实际进行状态更改的地方,并且它会接受 state 作为第一个参数:
+const store = new Vuex.Store({
+ state: {
+ count: 1
+ },
+ mutations: {
+ increment (state) {
+ state.count++ // 变更状态
+ }
+ }
+})
+当触发一个类型为 increment 的 mutation 时,需要调用此函数:
+store.commit('increment')
+而Action类似于mutation,不同点在于:
+const store = new Vuex.Store({
+ state: {
+ count: 0
+ },
+ mutations: {
+ increment (state) {
+ state.count++
+ }
+ },
+ actions: {
+ increment (context) {
+ context.commit('increment')
+ }
+ }
+})
+Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters。 +所以,两者的不同点如下:
+(1)最重要的区别
+(2)应用场景
+(3)永久性
+刷新页面时vuex存储的值会丢失,localstorage不会。
+注意: 对于不变的数据确实可以用localstorage可以代替vuex,但是当两个组件共用一个数据源(对象或数组)时,如果其中一个组件改变了该数据源,希望另一个组件响应该变化时,localstorage无法做到,原因就是区别1。
+(1)Redux 和 Vuex区别
+通俗点理解就是,vuex 弱化 dispatch,通过commit进行 store状态的一次更变;取消了action概念,不必传入特定的 action形式进行指定变更;弱化reducer,基于commit参数直接对数据进行转变,使得框架更加简易;
+(2)共同思想
+本质上:redux与vuex都是对mvvm思想的服务,将数据从视图中抽离的一种方案; +形式上:vuex借鉴了redux,将store作为全局的数据中心,进行mode管理;
+由于传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致代码无法维护。
+所以需要把组件的共享状态抽取出来,以一个全局单例模式管理。在这种模式下,组件树构成了一个巨大的"视图",不管在树的哪个位置,任何组件都能获取状态或者触发行为。
+另外,通过定义和隔离状态管理中的各种概念并强制遵守一定的规则,代码将会变得更结构化且易维护。
+有五种,分别是 State、 Getter、Mutation 、Action、 Module
+在严格模式下,无论何时发生了状态变更且不是由mutation函数引起的,将会抛出错误。这能保证所有的状态变更都能被调试工具跟踪到。
+在Vuex.Store 构造器选项中开启,如下
+const store = new Vuex.Store({
+ strict:true,
+})
+使用mapGetters辅助函数, 利用对象展开运算符将getter混入computed 对象中
+import {mapGetters} from 'vuex'
+export default{
+ computed:{
+ ...mapGetters(['total','discountTotal'])
+ }
+}
+使用mapMutations辅助函数,在组件中这么使用
+import { mapMutations } from 'vuex'
+methods:{
+ ...mapMutations({
+ setNumber:'SET_NUMBER',
+ })
+}
+然后调用this.setNumber(10)相当调用this.$store.commit('SET_NUMBER',10)
(1)监测机制的改变
+(2)只能监测属性,不能监测对象
+(3)模板
+(4)对象式的组件声明方式
+(5)其它方面的更改
+Vue 在实例初始化时遍历 data 中的所有属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。这样当追踪数据发生变化时,setter 会被自动调用。
+Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。
+但是这样做有以下问题:
+$set 来调用Object.defineProperty()处理。Vue3 使用 Proxy 来监控数据的变化。Proxy 是 ES6 中提供的功能,其作用为:用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。相对于Object.defineProperty(),其有以下特点:
在 Vue2 中, 0bject.defineProperty 会改变原始数据,而 Proxy 是创建对象的虚拟表示,并提供 set 、get 和 deleteProperty 等处理器,这些处理器可在访问或修改原始对象上的属性时进行拦截,有以下特点∶
+Vue.$set 或 Vue.$delete 触发响应式。Proxy 实现的响应式原理与 Vue2的实现原理相同,实现方式大同小异∶
+在 Vue2 中,代码是 Options API 风格的,也就是通过填充 (option) data、methods、computed 等属性来完成一个 Vue 组件。这种风格使得 Vue 相对于 React极为容易上手,同时也造成了几个问题:
+this上下文,Vue 背后的一些小技巧使得 Vue 组件的开发看起来与 JavaScript 的开发原则相悖,比如在methods 中的this竟然指向组件实例来不指向methods所在的对象。这也使得 TypeScript 在Vue2 中很不好用。于是在 Vue3 中,舍弃了 Options API,转而投向 Composition API。Composition API本质上是将 Options API 背后的机制暴露给用户直接使用,这样用户就拥有了更多的灵活性,也使得 Vue3 更适合于 TypeScript 结合。
+如下,是一个使用了 Vue Composition API 的 Vue3 组件:
+<template>
+ <button @click="increment">
+ Count: {{ count }}
+ </button>
+</template>
+
+<script>
+// Composition API 将组件属性暴露为函数,因此第一步是导入所需的函数
+import { ref, computed, onMounted } from 'vue'
+
+export default {
+ setup() {
+// 使用 ref 函数声明了称为 count 的响应属性,对应于Vue2中的data函数
+ const count = ref(0)
+
+// Vue2中需要在methods option中声明的函数,现在直接声明
+ function increment() {
+ count.value++
+ }
+ // 对应于Vue2中的mounted声明周期
+ onMounted(() => console.log('component mounted!'))
+
+ return {
+ count,
+ increment
+ }
+ }
+}
+</script>
+显而易见,Vue Composition API 使得 Vue3 的开发风格更接近于原生 JavaScript,带给开发者更多地灵活性
+从React Hook的实现角度看,React Hook是根据useState调用的顺序来确定下一次重渲染时的state是来源于哪个useState,所以出现了以下限制
+而Composition API是基于Vue的响应式系统实现的,与React Hook的相比
+虽然Compositon API看起来比React Hook好用,但是其设计思想也是借鉴React Hook的。
+从本质上来说,Virtual Dom是一个JavaScript对象,通过对象的方式来表示DOM结构。将页面的状态抽象为JS对象的形式,配合不同的渲染工具,使跨平台渲染成为可能。通过事务处理机制,将多次DOM修改的结果一次性的更新到页面上,从而有效的减少页面渲染的次数,减少修改DOM的重绘重排次数,提高渲染性能。
+虚拟DOM是对DOM的抽象,这个对象是更加轻量级的对 DOM的描述。它设计的最初目的,就是更好的跨平台,比如Node.js就没有DOM,如果想实现SSR,那么一个方式就是借助虚拟DOM,因为虚拟DOM本身是js对象。 在代码渲染到页面之前,vue会把代码转换成一个对象(虚拟 DOM)。以对象的形式来描述真实DOM结构,最终渲染到页面。在每次数据发生变化前,虚拟DOM都会缓存一份,变化之时,现在的虚拟DOM会与缓存的虚拟DOM进行比较。在vue内部封装了diff算法,通过这个算法来进行比较,渲染时修改改变的变化,原先没有发生改变的通过原先的数据进行渲染。
+另外现代前端框架的一个基本要求就是无须手动操作DOM,一方面是因为手动操作DOM无法保证程序性能,多人协作的项目中如果review不严格,可能会有开发者写出性能较低的代码,另一方面更重要的是省略手动DOM操作可以大大提高开发效率。
+虚拟DOM的解析过程:
+(1)保证性能下限,在不进行手动优化的情况下,提供过得去的性能 +看一下页面渲染的流程:解析HTML -> 生成DOM -> 生成 CSSOM -> Layout -> Paint -> Compiler +下面对比一下修改DOM时真实DOM操作和Virtual DOM的过程,来看一下它们重排重绘的性能消耗∶
+Virtual DOM的更新DOM的准备工作耗费更多的时间,也就是JS层面,相比于更多的DOM操作它的消费是极其便宜的。尤雨溪在社区论坛中说道∶ 框架给你的保证是,你不需要手动优化的情况下,依然可以给你提供过得去的性能。 +(2)跨平台 +Virtual DOM本质上是JavaScript的对象,它可以很方便的跨平台操作,比如服务端渲染、uniapp等。
+在新老虚拟DOM对比时:
+在diff中,只对同层的子节点进行比较,放弃跨级的节点比较,使得时间复杂从O(n3)降低值O(n),也就是说,只有当新旧children都为多个子节点时才需要用核心的Diff算法进行同层级比较。
+vue 中 key 值的作用可以分为两种情况来考虑:
+key 是为 Vue 中 vnode 的唯一标记,通过这个 key,diff 操作可以更准确、更快速
+使用index 作为 key和没写基本上没区别,因为不管数组的顺序怎么颠倒,index 都是 0, 1, 2...这样排列,导致 Vue 会复用错误的旧子节点,做很多额外的工作。
\ No newline at end of file diff --git a/src/lib/tech-article.ts b/src/lib/tech-article.ts new file mode 100644 index 0000000..525c271 --- /dev/null +++ b/src/lib/tech-article.ts @@ -0,0 +1,79 @@ +import { readFileSync } from "fs"; +import { join } from "path"; + +type TocItem = { + title: string; + href: string; +}; + +type TechArticle = { + contentHtml: string; + toc: TocItem[]; +}; + +const contentRoot = join(process.cwd(), "src", "content", "notes"); + +function decodeHtml(value: string) { + return value + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/&/g, "&") + .replace(/"/g, '"') + .replace(/'/g, "'"); +} + +function stripTags(value: string) { + return decodeHtml(value.replace(/<[^>]+>/g, "")).trim(); +} + +const headingPrefixPattern = + /^(?:\s| )*(?:(?:第?[一二三四五六七八九十百千万零〇]+|\d+)\s*[、..)]|[((]\s*(?:[一二三四五六七八九十百千万零〇]+|\d+)\s*[))]|[①②③④⑤⑥⑦⑧⑨⑩])\s*/; + +function normalizeHeadingTitle(value: string) { + return value.replace(headingPrefixPattern, "").trim(); +} + +function normalizeHeadingInnerHtml(value: string) { + let stripped = false; + + return value.replace(/(^|>)([^<]+)/g, (match, leadingToken: string, text: string) => { + if (stripped) { + return match; + } + + if (!text.replace(/(?:\s| )+/g, "")) { + return match; + } + + const nextText = text.replace(headingPrefixPattern, ""); + stripped = nextText !== text; + + return `${leadingToken}${nextText}`; + }); +} + +function normalizeArticleHeadings(contentHtml: string) { + return contentHtml.replace( + /(<(h[2-5])\b[^>]*>)([\s\S]*?)(<\/\2>)/g, + (_match, openTag: string, _tagName: string, innerHtml: string, closeTag: string) => + `${openTag}${normalizeHeadingInnerHtml(innerHtml)}${closeTag}`, + ); +} + +function getToc(contentHtml: string) { + return [...contentHtml.matchAll(/