Vue源码1-从初始化到响应式原理

首先介绍后面要用到的几个类:

  • Observer类:用于将一个数据变为响应式(可被观测)
  • Watcher类:一个依赖(一个指令对应一个依赖),一旦有一个指令用到了某个对象属性,那么就会新建一个Watcher作为订阅者。
  • Dep类:依赖管理器,一个对象属性对应一个Dep,其有一个内部属性subs用于存放依赖。

Vue初始化

Vue原型是在src/instance/idnex.js中定义的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//src/instance/idnex.js

import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

可以看到Vue原型是一个function。在这个函数只判断了是否是new出来的,否则报警告。然后直接调用了_init()方法,这个方法是在下面的initMixin(Vue)中混入的初始化方法。下面看一下这个方法中的重要部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//init.js 52-72 liens 

initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props

//state包括data,props,methods
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}

if (vm.$options.el) {
vm.$mount(vm.$options.el)
}

这里按顺序进行了一下操作(最简化,不考虑分支):

  1. 首先初始化了生命周期 initLifecycle(vm)

  2. 初始化了事件($on,$emit,$once…) initEvents(vm)

  3. 初始化了render initRender(vm)

  4. 调用了beforeCreate生命周期 callHook(vm, 'beforeCreate')

  5. 是初始化inject initInjections(vm)

  6. 接下来的initState中,初始化了data,props,methods。 initState(vm)

  7. 初始化provide initProvide(vm)

  8. 调用生命周期created, callHook(vm, 'created'),如同官网所说:

    在实例创建完成后被立即调用。在这一步,实例已完成以下的配置:数据观测 (data observer),属性和方法的运算,watch/event 事件回调。然而,挂载阶段还没开始,$el 属性目前尚不可用。

  9. 最后一步将el挂载到页面。vm.$mount(vm.$options.el)

响应式原理(以对象为例)

响应式的所有文件都放在observer文件夹下:

  • observer
    • array.js:数组的处理相关处理
    • dep.js:依赖管理器类的定义及其处理
    • index.js:整个observer的出口
    • scheduler.js:调度者相关文件
    • traverse.js:递归遍历一个对象,以唤醒所有转换getter,使每个嵌套的属性内的对象作为“深度”依赖项收集。
    • watcher.js:观测者,依赖的类定义与相关处理。

还是接着上面的第6步:initState

initState方法定义在state.js中,下面节选这一部分中内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//state.js

export function proxy (){
//代理处理
//比如当我们调用this.xxx 的时候实际上它是挂载在vm._data.xxx的,这里的代理同样使用了Object.defineProperty()来进行了代理。
}

export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}

可以看到intiState中,Vue进行了下面主要操作:

  • initProps(vm, opts.props)
  • initMethods(vm, opts.methods)
  • initData(vm)
  • initWatch(vm, opts.watch)

然后以data为例,分析对data的处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//state.js

function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// observe data
observe(data, true /* asRootData */)
}

在这个函数里

  1. 首先看data是一个对象还是一个函数,对其进行对应的处理
  2. 然后判断他不能与props,methods中的属性同名,因为最终这三部分都会被挂载到vm实例上。
  3. 最后调用observe()方法来使data变为可被观测的

然后我们看observe()方法,这个方法就位于observer下的index.js中了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//observer/index.js

export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}

作为整个将数据变为响应式的入口函数,它进行了一下操作:

  1. 判断传入的val如果不是一个对象或者是一个Vnode,就直接返回,不做处理

  2. 判断整个val是否有__ob__整个属性或者是不是Observer的子类,如果是的话,直接将val.__ob__返回

  3. 进行了一系列的其他的判断,比如是否应该被观测(shouldObserve这个对象定义在全局中,判

    标识此时是否应该处理数据)、是否处于服务端渲染模式、是一个数组或是一个对象、是不是可扩展的、是不是Vue本身。

  4. 新建一个Observer对象,并将value传入。

继续向下看,新建Observer对象过程,Observer对象也在index中定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//observer/index.js

export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data

constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}

/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}

/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}

可以看到Observer对象包含了三个私有属性:

  • value:当前观测对象
  • dep:依赖管理器
  • vmCount:将这个对象作为$data的数量

再看他的构造方法中,执行流程如下:

  1. 初始化了value,dep,vmCount
  2. 并给value的加上一个(不可枚举的)__ob__属性,可以联系上面判断__ob__的操作
  3. 判断value是否是一个数组,然后执行对数组的observer操作。现不看数组。
  4. 如果不是,则代表value是一个对象,则执行walk()方法对其进行处理。
  5. walk()中,可以看到Vue遍历了对象的所有属性并对其调用了defineReactive方法

我们再跟进defineReactive方法中(这个方法就差不多是核心了):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
//为每一个属性建立一个依赖收集器
const dep = new Dep()

const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}

// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}

let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}

还是分析一下他的执行流程:

  1. 首先建立了一个Dep对象,即每一个属性都有一个依赖管理器用来收集用到这个属性的依赖,这一部分后面在讲。
  2. 判断这个对象属性能否被修改,及判断其configurable属性。
  3. 判断这个对下给你属性是否自带了gettersetter,如果有的话就将其缓存下来。
  4. 判断是否在函数调用时为这个对下给你属性传入了customSetter,如果有的话,则先调用getter将其值存储下来。
  5. 判断有无子对象或者是否在函数调用时确定只观测表层属性(不循环处理),如果都不满足则递归进行子属性的响应式处理。
  6. 这里就使整个响应式的核心:Object.defineProperty,在这里Vue为其定义了enumerable,configurable,get,set
    1. get():
      1. 获取原始的属性值(通过原始getting或者直接获取)
      2. 判断Dep.target的值是否存在(这个值后面介绍Dep对象时介绍,代表的是当前的依赖),如果存在的话,就调用dep对象的depend()方法进行依赖收集。
      3. 接下来判断是否有子ob对下给你,如果有的话,也调用子的dep的depend方法进行依赖收集。同时判断对象属性原来的值是否是个数组,如果是的话,调用dependArray方法进行数组的依赖收集。
    2. set()
      1. 获取对象属性原本的值,调用原来的getter,如果没有,就接受传入的值。
      2. 判断有没有必要更新。
      3. 判断是否有customSetter,如果有的话,就调用
      4. 如果只有getter,没有setter,则直接返回。这里是为了修复#7981的BUG,问题大概是如果一个对象属性如果被其他插件修改后只有getter,但没有setter,也就是说整个插件的原意是将其变为一个不可写入的属性,但是如果不加这一句进行判断,那么Vue会直接调用val = newVal,对其进行赋值。这不符合预期,所以加了这一句判断,直接返回不进行处理。
      5. 判断如果原来有setter的话,就调用其setter。否则就直接赋值给val。
      6. 然后同样是对子属性的处理。
      7. 这一步进行依赖派发。

至此,defineReactive方法流程介绍完毕。但是我们还留下了两个坑

  • 依赖收集dep.depend()具体如何完成的
  • 依赖通知dep.notify() 具体如何完成的

接下来我们在进入这两个函数进行分析。

首先我们看一下Dep对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
//observer/dep.js

/* @flow */

import type Watcher from './watcher'
import { remove } from '../util/index'
import config from '../config'

let uid = 0

/**
* A dep is an observable that can have multiple
* directives subscribing to it.
* 一个dep可以有多个指令(包括"{{}}","v-text","v-html"...)订阅它。
* target属性用于表示正在处理的依赖,当确定为这个值的指令时,则将其添加到subs中(addSub函数)。
* subs属性用于存放所有的依赖,他的依赖是Watcher类
*/
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;

constructor () {
this.id = uid++
this.subs = []
}

addSub (sub: Watcher) {
this.subs.push(sub)
}

removeSub (sub: Watcher) {
remove(this.subs, sub)
}

depend () {
if (Dep.target) {
Dep.target.addDep(this/* 这个依赖管理器Dep实例 */)
}
}

notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}

// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []

export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}

export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}

可以看到Dep对象有三个属性,

  • target:一个静态属性,类型是Watcher,即当前执行的依赖
  • id:Dep的标识符
  • subs:整个dep中所有的依赖,是一个Watcherd数组

然后我们直接看depend()方法:

这个函数判断dep的target是否存在,如果存在的话,则调用当前依赖的addDep方法,我们知道这个target是一个Watcher。所以我们看一下Watcher:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
export default class Watcher {
//初始化了很多属性
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;
}

addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
//调用depaddSub方法把这个指令(this)添加到这个dep中
dep.addSub(this)
}
}
}

可以看到在addDep中,判断这个Watcher对应的新dep中是否含有这个传进来的Dep,如果没有就其push到新dep与新depIds中,然后判断原来的dep中是否含有这个watcher,如果没有,就push进来。

这里的newDep与dep是为了灵活的动态更新视图,思考以下场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<template>
<div>
<span v-if="nameShow">{{name}}</span>
<span v-if="ageShow">{{age}}</span>

<input :value="name"/>
<input :value="age"/>

<button @click="nameShow = !nameShow">切换name状态</button>
<button @click="ageShow = !ageShow">age</button>

</div>
</template>

<script>
export default {
name:"Register",
data () {
return {
nameShow: true,
ageShow: true,
name:"123",
age:"111"
}
}
}
</script>

当nameShow与ageShow都是true时,我们对表单机进行修改以修改name与age的值时,肯定会涉及到到依赖的分发。

但是当我们点击button将nameShow或ageShow的值切换为false时,视图上已经不显示对应信息,则讲道理应该不会在对这个依赖进行通知。这个newDep的存在就是为了这里。

至此,依赖收集的过程基本完成。下面看看如何进行依赖派发的:

当一个对象属性被改变时,其set方法就会被调用,由此调用dep.notify() ,进行依赖派发。我们看一下dep.notify() 这个函数内部:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//observer/dep.js

notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}

可以看到首先是对subs所有的依赖进行了排序,根据官方的注释是应为:如果subs不是异步运行的话,那么他们没有在调度者中进行排序,所以我们需要对其进行排序以保证其正确按序派发。

然后这里调用每一个watcher的update方法,进行DOM的更新。看一下update()方法

1
2
3
4
5
6
7
8
9
10
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}

可以看到首先判断了这个watcher是不是懒加载的,如果是的话,将其dirty属性变为true,Vue会在调用到它时进行加载,否则看他是不是同步的,如果是的话,立即调用run()进行DOM更新操作,否则就将其推入到queueWatcher队列中,等待调度者,进行调度。(这里就不再讲调度算法,后续再讲)

再进入run()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//observer/watcher.js  

/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}

由官方的注释也可以看出来,这个run()是由调度者进行执行的(除非他是一个同步的watcher)

run()函数的运行流程如下:

  1. 判断这个wacher是否的活动的,如果是才操作。
  2. 通过get()获取这个watcher对应的值,判断获取到的值与watcher中保存的值是否相同
  3. 如果不等,则把当前watcher中保存的值作为oldValue保存下来,将当前watcher中的value设为获取到的value,然后判断这个watcher是不是用户定义的(this.user)(根据调度者中注释,watcher分为user watcher与render watcher),如果使用定义的watcher则用try--catch预防错误,否则直接调用这个watcher的回调函数。这个回调函数就会进行真正操作,比如调用rrnder更新DOM。

数组的响应式如何实现

在上面我们介绍Dep对象时,在其构造方法中,我们只看了this.walk()对对象的操作,现在我们看一下对数组的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data

constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}

如果value是一个对象,那么会判断value有没有__ptoto__对象,因为部分浏览器不支持这个属性,如果有的话,则调用protoAugment(value, arrayMethods)arrayMethods挂载到value的__proto__上,我们再看一下arrayMethods,它放在

observer/array.js中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/*
* not type checking this file because flow doesn't play well with
* dynamically accessing methods on Array prototype
*/

import { def } from '../util/index'

const arrayProto = 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) {
// cache original 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) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})

可以看到

  1. 首先是以原生Array的prototype作为原型创建了一个新的对象arrayMethods
  2. 列举出需要被修改的数组方法methodsToPatch
  3. methodsToPatch进行forEach循环,并给arrayMethods定义每一个列举出的方法(不可枚举),如果这些方法中要为这个数组插值,我们必须也要探测这个值是否是一个引用类型(Araay或者Object),并也要将其变为响应式,所以后面判断了如果是push,unshift,splice则拿到要插入的值inserted
  4. 判断inserted是否存在,如果存在,也使用observeArray将其变为响应式。
  5. 进行依赖收集ob.dep.notify()
  6. 返回原始方法调用后返回的结果。

然后我们再看protoAugment方法:

1
2
3
4
5
function protoAugment (target, src: Object) {
/* eslint-disable no-proto */
target.__proto__ = src
/* eslint-enable no-proto */
}

可以看到很简单,就只是把第一个参数的__proto__修改为第二个参数,结合我们刚刚传入的参数,即:将这个数组的_proto_修改为arrayMethods,即上面我们分析的这个对象。

再看copyAugment方法,这个方法也很简单,是针对不支持__proto__属性的浏览器:

1
2
3
4
5
6
7
8
9
10
11
/**
* Augment a target Object or Array by defining
* hidden properties.
*/
/* istanbul ignore next */
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}

即遍历所有的方法名字,并将其设置为到目标数组上的不可枚举属性。

最后我们看一下observeArray方法:

1
2
3
4
5
6
7
8
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}

这里实际上就是将数组的每一个值变为响应式。

但是还有一点是:我们还可以通过下标的方式为数组赋值,但是JS中找不到方法检测整个操作,所以Vue也无法检测到,所以Vue提供了Vue.set和Vue.del这两个api,用来弥补这一点。

至此,响应式的整个流程就差不多完成了。

最后梳理以下整个流程(对象):

image-20200108185122998

这个流程主要是我通过分析源码,借助一定的网上资料整理出来的,可能其中会有错误。希望大家指出来,谢谢。

Powered by Hexo and Hexo-theme-hiker

Copyright © 2019 - 2024 My Wonderland All Rights Reserved.

UV : | PV :