[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>
![](https://box.kancloud.cn/dee4be024ce54050fa58dd8a55c2c2a5_978x552.png)
<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 基础
- 样式设计
- 组件测试
- 图标解决方案
- Algorithm
- 排序算法及常见问题
- 剑指 offer
- 动态规划
- DataStruct
- 概述
- 树
- 链表
- Network
- Performance
- Webpack
- PWA
- Browser
- Safety
- 微信小程序
- mpvue 课程实战记录
- 服务器
- 操作系统基础知识
- Linux
- Nginx
- redis
- node.js
- 基础及原生模块
- express框架
- node.js操作数据库
- 《深入浅出 node.js》笔记
- 前半部分
- 后半部分
- 数据库
- SQL
- 面试题收集
- 智力题
- 面试题精选1
- 面试题精选2
- 问答篇
- Other
- markdown 书写
- Git
- LaTex 常用命令