[TOC]
# Vue 雙向綁定原理
參考:
[鏈接1](https://juejin.im/post/5acc17cb51882555745a03f8)
[鏈接2](https://www.cnblogs.com/canfoo/p/6891868.html)
[鏈接3](https://yuchengkai.cn/docs/frontend/framework.html#%E6%95%B0%E6%8D%AE%E5%8A%AB%E6%8C%81)
[鏈接4](https://www.cnblogs.com/kidney/p/6052935.html?utm_source=gold_browser_extension)
首先明確下雙向綁定的概念:
- 單向綁定指的是 Model(模型)更新時,View(視圖)會自動更新
- 如果反過來 View 更新時 Model 的數據也能自動更新,那就是雙向綁定
也就是說,我們只要滿足上述條件就算實現雙向綁定了,那么下面的代碼就是最簡單的雙向綁定:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
</head>
<body>
<input type="text" id="a">
<span id="b"></span>
<script>
const obj = {}
Object.defineProperty(obj, 'attr', {
set: function (newVal) {
document.getElementById('a').value = newVal
document.getElementById('b').innerHTML = newVal
}
})
document.addEventListener('keyup', function (e) {
obj.attr = e.target.value
})
</script>
</body>
</html>
```
我們在輸入框輸入文字時,JavaScript 代碼中的數據會發生變化;在控制臺顯式地修改 obj.attr 的值,視圖也會相應地更新,所以說這是一個極簡的雙向綁定。
鏈接 4 還提到了 Object.defineProperty 的幾個要點:
- 讀取或設置訪問器屬性的值,實際上是調用其內部特性:get 和 set 函數
- get 和 set 方法內部的 this 都指向 obj,這意味著其可以操作對象內部的值
- 訪問器屬性的會"覆蓋"同名的普通屬性,因為訪問器屬性會被優先訪問,與其同名的普通屬性則會被忽略。
下面再看 vue 是如何雙向綁定的,這里不再做具體分析了(可以參考上面的鏈接)。我用一句話、一張圖、一段代碼來整理自己的思路:
*****
<span style="font-size: 20px; color:#42b383" >一句(比較長)的話</span>
vue 的雙向綁定采用數據劫持結合發布-訂閱模式實現,數據劫持即使用 Object.defineProperty 把傳入的 data 選項(一個 JavaScript 對象)的屬性轉換為 getter / setter,發布-訂閱即模板解析過程中,與渲染相關的數據屬性會添加相應的 Watcher,該屬性的 setter 觸發時就會通知對應的 Watcher 更新視圖。
<span style="font-size: 20px; color:#42b383" >盜一張圖 -.-</span>

<span style="font-size: 20px; color:#42b383" >再剽一段代碼 -.-</span>
代碼來源:[https://github.com/bison1994/two-way-data-binding/blob/master/index.html](https://github.com/bison1994/two-way-data-binding/blob/master/index.html)
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Two-way-data-binding</title>
</head>
<body>
<div id="app">
<input type="text" v-model="text">
{{ text }}
</div>
<script>
// 這里是劫持效果是 this.xxx -> this.$data.xxx
function observe (obj, vm) {
Object.keys(obj).forEach(function (key) {
defineReactive(vm, key, obj[key]);
})
}
function defineReactive (obj, key, val) {
var dep = new Dep();
Object.defineProperty(obj, key, {
get: function () {
// 添加訂閱者 watcher 到主題對象 Dep
if (Dep.target) dep.addSub(Dep.target);
return val
},
set: function (newVal) {
if (newVal === val) return
val = newVal;
// 作為發布者發出通知
dep.notify();
}
});
}
// 編譯 DOM 結構,用文檔片段的形式存儲,然后將編譯后的 DOM 掛載到綁定的 el 上
function nodeToFragment (node, vm) {
var flag = document.createDocumentFragment();
var child;
// appendChild 方法有個隱蔽的地方,就是調用以后 child 會從原來 DOM 中移除
// 所以,第二次循環時,node.firstChild 已經不再是之前的第一個子元素了
while (child = node.firstChild) {
compile(child, vm);
flag.appendChild(child);
}
return flag
}
function compile (node, vm) {
var reg = /\{\{(.*)\}\}/;
// 節點類型為元素
if (node.nodeType === 1) {
var attr = node.attributes;
// 解析屬性
for (var i = 0; i < attr.length; i++) {
if (attr[i].nodeName == 'v-model') { // 該特性的名稱
var name = attr[i].nodeValue; // 獲取 v-model 綁定的屬性名
node.addEventListener('input', function (e) {
// 給相應的 data 屬性賦值,進而觸發該屬性的 set 方法
vm[name] = e.target.value;
});
node.removeAttribute('v-model');
}
};
new Watcher(vm, node, name, 'input');
}
// 節點類型為 text
if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) { // 有{{}}
var name = RegExp.$1; // 獲取匹配到的字符串
name = name.trim();
new Watcher(vm, node, name, 'text');
}
}
}
// nodeType 準確來說應該是事件類型,比如 v-model v-bind 歸為一類
function Watcher (vm, node, name, nodeType) {
Dep.target = this; // 全局變量
this.name = name;
this.node = node;
this.vm = vm;
this.nodeType = nodeType;
this.update(); // 更新視圖,即修改相應其監聽的 DOM 節點的某個特性值
Dep.target = null;
}
Watcher.prototype = {
update: function () {
this.get();
if (this.nodeType == 'text') {
this.node.nodeValue = this.value;
}
if (this.nodeType == 'input') {
this.node.value = this.value;
}
},
// 獲取 data 中的屬性值
get: function () {
this.value = this.vm[this.name]; // 觸發相應屬性的 get
}
}
function Dep () {
this.subs = []
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
});
}
}
function Vue (options) {
this.data = options.data;
var data = this.data;
observe(data, this);
var id = options.el;
var dom = nodeToFragment(document.getElementById(id), this);
// 編譯完成后,將 dom 返回到 app 中
document.getElementById(id).appendChild(dom);
}
var vm = new Vue({
el: 'app',
data: {
text: 'hello world'
}
})
</script>
</body>
</html>
```
# vue 的 nextTick 是如何實現的?
參考鏈接:
[https://mp.weixin.qq.com/s/mCcW4OYj3p3471ghMBylBw](https://mp.weixin.qq.com/s/mCcW4OYj3p3471ghMBylBw)
[https://segmentfault.com/a/1190000013314893](https://segmentfault.com/a/1190000013314893)
- nextTick 的用途?
該 API 可以在 DOM 更新完畢后執行一個回調,其可以確保我們操作的是更新后的 DOM
```js
// 修改數據
vm.msg = 'Hello'
// DOM 還沒有更新
Vue.nextTick(function () {
// DOM 更新了
})
```
- 如何檢測 DOM 的更新并確保回調是在 DOM 更新后執行?
1. vue 用異步隊列的方式來控制 DOM 更新和 nextTick 回調先后執行
2. microtask 因為其高優先級特性,能確保隊列中的微任務在一次事件循環前被執行完畢
3. 因為兼容性問題,vue 不得不做了 microtask 向 macrotask 的降級方案
來看下面這段代碼:
```html
<div id="example">
<div ref="test">{{test}}</div>
<button @click="handleClick">tet</button>
</div>
```
```js
var vm = new Vue({
el: '#example',
data: {
test: 'begin',
},
methods: {
handleClick() {
this.test = 'end' + this.test; // 這里確保 DOM 更新,你可以試試 this.test = 'end' 會發現第二次點擊時會輸出 1 promise 2 3
console.log('1')
setTimeout(() => { // macroTask
console.log('3')
}, 0);
Promise.resolve().then(function() { //microTask
console.log('promise!')
})
this.$nextTick(function () {
console.log('2')
})
}
}
})
```
在 Chrome 下,這段代碼會輸出 `1、2、promise、3`
DOM 更新其實就是生成一個 Watcher 隊列,最后會調用我們的 nextTick 函數(具體見鏈接2的分析)
```js
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) { //通過 pending 來判斷是否已經有 timerFunc 這個函數在事件循環的任務隊列等待被執行
pending = true
timerFunc() // 把回調作為 microTask 或 macroTask 參與到事件循環
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
```
這里面通過對 `pending` 的判斷來檢測是否已經有 `timerFunc` 這個函數在事件循環的任務隊列等待被執行。如果存在的話,那么是不會再重復執行的。
最后異步執行 `flushCallbacks` 時又會把 `pending` 置為 `false`。
```js
// 執行所有 callbacks
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
```
所以回到我們的例子:
```js
handleClick() {
this.test = 'end';
console.log('1')
setTimeout(() => { // macroTask
console.log('3')
}, 0);
Promise.resolve().then(function() { //microTask
console.log('promise!')
});
this.$nextTick(function () {
console.log('2')
});
}
```
代碼中,`this.test = 'end'` 必然會觸發 `watcher` 進行視圖的重新渲染,而我們在文章的 `Watcher` 一節中(鏈接2)就已經有提到會調用 `nextTick` 函數,一開始 `pending` 變量肯定就是 `false`,因此它會被修改為 `true` 并且執行 `timerFunc`。之后執行 `this.$nextTick` 其實還是調用的 `nextTick` 函數,只不過此時的 `pending` 為 `true` 說明 `timerFunc` 已經被生成,所以 `this.$nextTick(fn)` 只是把傳入的 `fn` 置入 `callbacks` 之中。此時的 `callbacks` 有兩個 `function` 成員,一個是 `flushSchedulerQueue`,另外一個就是 `this.$nextTick()` 的回調。
因此,上面這段代碼中,在 `Chrome` 下,有一個 `macroTask` 和兩個 `microTask`。一個`macroTask`就是`setTimeout`,兩個`microTask`:分別是`Vue`的`timerFunc`(其中先后執行`flushSchedulerQueue`和`function() {console.log('2')}`)、代碼中的`Promise.resolve().then()`。
最后我們貼出 timeFunc 的代碼來看看其降級策略:
```js
// vue@2.6.10 /src/core/util/next-tick.js
// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
// In problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// Fallback to setImmediate.
// Techinically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
// 降級策略:Native Promise -> MutationObserver -> setImmediate -> setTimeout
// 雖然參考鏈接中有說用 MessageChannel 但是這里的 Vue 源碼中沒看到? 2019.7.31
```
# 聊聊 keep-alive
參考鏈接:[https://segmentfault.com/a/1190000011978825](https://segmentfault.com/a/1190000011978825)
[https://juejin.im/post/5cce49036fb9a031eb58a8f9](https://juejin.im/post/5cce49036fb9a031eb58a8f9)
## keep-alive 內置組件的用途?
`<keep-alive>` 包裹動態組件時,會緩存不活動的組件實例,而不是銷毀它們。`<keep-alive>`是一個抽象組件:它自身不會渲染一個 DOM 元素,也不會出現在父組件鏈中。
當組件在 `<keep-alive>` 內被切換,它的 `activated` 和 `deactivated` 這兩個生命周期鉤子函數將會被對應執行。
- Props:
- `include`\- 字符串或正則表達式。只有名稱匹配的組件會被緩存。
- `exclude`\- 字符串或正則表達式。任何名稱匹配的組件都不會被緩存。
- `max`\- 數字。最多可以緩存多少組件實例。
應用場景:避免組件的反復重建和渲染,保存用戶狀態等。
## 為什么 keep-alive 組件自身不會被渲染?
Vue 在初始化生命周期的時候,為組件實例建立父子關系會根據 `abstract` 屬性決定是否忽略某個組件。在 keep-alive 中,設置了 `abstract: true`,那 Vue 就會跳過該組件實例。
## keep-alive 組件包裹的組件是如何使用緩存的?
```js
// src/core/vdom/patch.js
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm) // 將緩存的DOM(vnode.elm)插入父元素中
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
```
在首次加載被包裹組件時,由 `keep-alive.js` 中的 `render` 函數可知,`vnode.componentInstance` 的值是 `undefined`,`keepAlive` 的值是 `true`,因為 keep-alive 組件作為父組件,它的 `render` 函數會先于被包裹組件執行;那么就只執行到 `i(vnode, false /* hydrating */)`,后面的邏輯不再執行;
*****
再次訪問被包裹組件時,`vnode.componentInstance` 的值就是已經緩存的組件實例,那么會執行 `insert(parentElm, vnode.elm, refElm)` 邏輯,這樣就直接把上一次的 DOM 插入到了父元素中。
## 如何做到避免組件的重復創建?
一般的組件,每一次加載都會有完整的生命周期,即生命周期里面對應的鉤子函數都會被觸發,為什么被 keep-alive 包裹的組件卻不是呢? 因為被緩存的組件實例會為其設置 keepAlive = true,而在初始化組件鉤子函數中:
```js
// src/core/vdom/create-component.js
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
}
// ...
}
```
可以看出,當 vnode.componentInstance 和 keepAlive 同時為 truly 值時,不再進入 $mount 過程,那 mounted 之前的所有鉤子函數(beforeCreate、created、mounted)都不再執行。
## activated 與 deactivated 鉤子
在 patch 的階段,最后會執行 invokeInsertHook 函數,而這個函數就是去調用組件實例(VNode)自身的 insert 鉤子:
```js
// src/core/vdom/patch.js
function invokeInsertHook (vnode, queue, initial) {
if (isTrue(initial) && isDef(vnode.parent)) {
vnode.parent.data.pendingInsert = queue
} else {
for (let i = 0; i < queue.length; ++i) {
queue[i].data.hook.insert(queue[i]) // 調用 VNode 自身的 insert 鉤子函數
}
}
}
```
再看`insert`鉤子:
```js
// src/core/vdom/create-component.js
const componentVNodeHooks = {
// init()
insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
if (vnode.data.keepAlive) {
if (context._isMounted) {
queueActivatedComponent(componentInstance)
} else {
activateChildComponent(componentInstance, true /* direct */)
}
}
// ...
}
```
在這個鉤子里面,調用了`activateChildComponent`函數遞歸地去執行所有子組件的`activated`鉤子函數:
```js
// src/core/instance/lifecycle.js
export function activateChildComponent (vm: Component, direct?: boolean) {
if (direct) {
vm._directInactive = false
if (isInInactiveTree(vm)) {
return
}
} else if (vm._directInactive) {
return
}
if (vm._inactive || vm._inactive === null) {
vm._inactive = false
for (let i = 0; i < vm.$children.length; i++) {
activateChildComponent(vm.$children[i])
}
callHook(vm, 'activated')
}
}
```
相反地,`deactivated`鉤子函數也是一樣的原理,在組件實例(VNode)的 `destroy` 鉤子函數中調用`deactivateChildComponent`函數。
# vue-router 實現淺析
參考:[https://zhuanlan.zhihu.com/p/27588422](https://zhuanlan.zhihu.com/p/27588422)
"更新視圖但不重新請求頁面" 是前端路由原理的核心之一,目前在瀏覽器環境中這一功能的實現主要有兩種方式:
* 利用 URL 中的 hash(“#”)
* 利用 History interface 在 HTML5 中新增的方法
## Hash 模式
`http://www.example.com/index.html#print`
\# 符號本身以及它后面的字符稱之為 hash,可通過 window.location.hash 屬性讀取。它具有如下特點:
- hash 雖然出現在 URL 中,但不會被包括在 HTTP 請求中。它是用來指導瀏覽器動作的,對服務器端完全無用,因此,改變 hash 不會重新加載頁面
- 可以為 hash 的改變添加監聽事件:
```js
window.addEventListener("hashchange", funcRef, false)
```
- 每一次改變 hash(window.location.hash),都會在瀏覽器的訪問歷史中增加一個記錄
路由操作主要就是 push 和 replace,push 是將新的路由添加到瀏覽器歷史記錄棧的棧頂,replace 是替換當前棧頂。
函數觸發順序:
```js
1 $router.push() // 調用方法
2 HashHistory.push() // 設置 hash 并添加到瀏覽器歷史記錄(添加到棧頂)(window.location.hash= XXX)
3 History.transitionTo() // 監測更新,更新則調用 History.updateRoute()
4 History.updateRoute() // 更新路由
5 {app._route= route} // 替換當前app路由
6 vm.render() // 更新視圖
```
## History 模式
更改了 API,可以直接操作瀏覽器歷史記錄棧
1.push:與 hash 模式類似,只是將 window.hash 改為 history.pushState
2.replace:與 hash 模式類似,只是將 window.replace 改為 history.replaceState
3.監聽地址變化:在 HTML5History 的構造函數中監聽 popState(window.onpopstate)
# vuex 實現淺析
參考:[https://www.jianshu.com/p/d95a7b8afa06](https://www.jianshu.com/p/d95a7b8afa06)
vuex 僅僅是作為 vue 的一個插件而存在,不像 Redux,MobX 等庫可以應用于所有框架, vuex 只能使用在 vue 上,很大的程度是因為其高度依賴于 vue 的 computed 依賴檢測系統以及其插件系統。
每一個 vue 插件都需要有一個公開的 install 方法,vuex 也不例外。其調用了一下 applyMixin 方法,該方法主要作用就是在所有組件的 **beforeCreate** 生命周期注入了設置 **this.$store** 這樣一個對象。
```js
// src/mixins.js
// 對應applyMixin方法
export default function (Vue) {
const version = Number(Vue.version.split('.')[0])
if (version >= 2) {
Vue.mixin({ beforeCreate: vuexInit })
} else {
const _init = Vue.prototype._init
Vue.prototype._init = function (options = {}) {
options.init = options.init
? [vuexInit].concat(options.init)
: vuexInit
_init.call(this, options)
}
}
/**
* Vuex init hook, injected into each instances init hooks list.
*/
function vuexInit () {
const options = this.$options
// store injection
if (options.store) {
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store
}
}
}
```
Vuex 的構造函數中有如下一個方法:
```js
// src/store.js
function resetStoreVM (store, state, hot) {
// 省略無關代碼
Vue.config.silent = true
store._vm = new Vue({
data: {
$$state: state
},
computed
})
}
```
其本質就是將我們傳入的 state 作為一個隱藏的 vue 組件的 data,也就是說,我們的 commit 操作,本質上其實是修改這個組件的 data 值,結合上文的 computed,修改被 **defineReactive** 代理的對象值后,會將其收集到的依賴的 **watcher** 中的 **dirty** 設置為 true,等到下一次訪問該 watcher 中的值后重新獲取最新值。
這樣就能解釋了為什么 vuex 中的 state 的對象屬性必須提前定義好,如果該 **state** 中途增加**一個屬性**,因為該**屬性**沒有被 **defineReactive**,所以其依賴系統沒有檢測到,自然不能更新。
- 序言 & 更新日志
- H5
- Canvas
- 序言
- Part1-直線、矩形、多邊形
- Part2-曲線圖形
- Part3-線條操作
- Part4-文本操作
- Part5-圖像操作
- Part6-變形操作
- Part7-像素操作
- Part8-漸變與陰影
- Part9-路徑與狀態
- Part10-物理動畫
- Part11-邊界檢測
- Part12-碰撞檢測
- Part13-用戶交互
- Part14-高級動畫
- CSS
- SCSS
- codePen
- 速查表
- 面試題
- 《CSS Secrets》
- SVG
- 移動端適配
- 濾鏡(filter)的使用
- JS
- 基礎概念
- 作用域、作用域鏈、閉包
- this
- 原型與繼承
- 數組、字符串、Map、Set方法整理
- 垃圾回收機制
- DOM
- BOM
- 事件循環
- 嚴格模式
- 正則表達式
- ES6部分
- 設計模式
- AJAX
- 模塊化
- 讀冴羽博客筆記
- 第一部分總結-深入JS系列
- 第二部分總結-專題系列
- 第三部分總結-ES6系列
- 網絡請求中的數據類型
- 事件
- 表單
- 函數式編程
- Tips
- JS-Coding
- Framework
- Vue
- 書寫規范
- 基礎
- vue-router & vuex
- 深入淺出 Vue
- 響應式原理及其他
- new Vue 發生了什么
- 組件化
- 編譯流程
- Vue Router
- Vuex
- 前端路由的簡單實現
- React
- 基礎
- 書寫規范
- Redux & react-router
- immutable.js
- CSS 管理
- React 16新特性-Fiber 與 Hook
- 《深入淺出React和Redux》筆記
- 前半部分
- 后半部分
- react-transition-group
- Vue 與 React 的對比
- 工程化與架構
- Hybird
- React Native
- 新手上路
- 內置組件
- 常用插件
- 問題記錄
- Echarts
- 基礎
- Electron
- 序言
- 配置 Electron 開發環境 & 基礎概念
- React + TypeScript 仿 Antd
- TypeScript 基礎
- React + ts
- 樣式設計
- 組件測試
- 圖標解決方案
- Storybook 的使用
- Input 組件
- 在線 mock server
- 打包與發布
- Algorithm
- 排序算法及常見問題
- 劍指 offer
- 動態規劃
- DataStruct
- 概述
- 樹
- 鏈表
- Network
- Performance
- Webpack
- PWA
- Browser
- Safety
- 微信小程序
- mpvue 課程實戰記錄
- 服務器
- 操作系統基礎知識
- Linux
- Nginx
- redis
- node.js
- 基礎及原生模塊
- express框架
- node.js操作數據庫
- 《深入淺出 node.js》筆記
- 前半部分
- 后半部分
- 數據庫
- SQL
- 面試題收集
- 智力題
- 面試題精選1
- 面試題精選2
- 問答篇
- 2025面試題收集
- Other
- markdown 書寫
- Git
- LaTex 常用命令
- Bugs