前言
虽然源码写的很优雅,模块化分类也一目了然。但是如果一行一行的去深入了解源码的话,不但费时间还会让自己越看越懵,所以我们从常用到的一些特性来看对应的源码,这样还能加深印象。
就像一颗树,应当先了解树的主干,再慢慢深入了解分支。
本系列就围绕 VueJs渲染 来进行源码阅读。
Vue对象
Vue对象定义:\src\core\instance\index.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'
/**
* Vue对象
* @param options new Vue()的时候传的参数,比如router、vuex等
* @constructor Vue
*/
function Vue (options) {
// 开发环境下如果不是通过 new 关键字创建对象的话,提示错误
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 Vue2
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
这段源码中的Vue对象看着很简约,先从 this._init(options) 开始
初始化
对应的路径:\src\core\instance\init.js
可以找到 _init() 方法在 initMixin 函数中
export function initMixin(Vue: Class<Component>) {
// 在原型上创建初始化方法,传入new Vue时的参数
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// uid标识
vm._uid = uid++
// ... 省略了一些代码 ...
// 防止被设置为观察对象的一个标识
vm._isVue = true
if (options && options._isComponent) {
// 优化内部构件实例化
// 由于动态选项合并是相当缓慢的,并且内部组件选项中没有一个需要特殊处理。
initInternalComponent(vm, options)
} else {
// 合并
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// 初始化代理
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// 暴露真实的实例对象
vm._self = vm
initLifecycle(vm) // 生命周期
initEvents(vm) // 事件
initRender(vm) // 渲染
callHook(vm, 'beforeCreate') // 触发 beforeCreate 钩子
initInjections(vm) // 在 data/props 注入前做一些处理
initState(vm) // 初始化 data/props
initProvide(vm) // 在提供 data/props 后做一些处理
callHook(vm, 'created') // 触发 created 钩子
// ... 省略了一些代码 ...
// 如果options中传入了 el 字段
if (vm.$options.el) {
// 【重点】挂载实例对象到dom(下一章讲,见文末)
vm.$mount(vm.$options.el)
}
}
}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
通过 _init() 这个方法可以了解到Vue基础的初始化都在这里。
合并options
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)2
3
4
5
这一步的具体逻辑代码先不用管,通过测试可以发现,这一步的作用就是将 new Vue(options) 中传入的 options 合并到 $options 上,因为Vue也有自己的基础options,当合并完后,在代码中,就可以通过 this.$options 获取到 el、data、components和生命周期钩子 等等。
因此引出一个小技巧 如果想把某个data里定义的数据还原到初始化时的样子,那么就可以: Object.assign(this.$data, this.$options.data()) // 初始化全部data中定义的数据 Object.assign(this.$data.xxx, this.$options.data().xxx) // 初始化ata中定义的xxx
initState()
这个方法的作用是初始化 data 和 props,为什么在 methods 里或者生命周期钩子中可以通过 this.xxx 获取到 data 中定义的 xxx 呢?正常推理不应该是 this.data().xxx / this.data.xxx 这样吗?
找到 initState() 方法所在路径:\src\core\instance\state.js
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
// 初始化props
if (opts.props) initProps(vm, opts.props)
// 初始化methods
if (opts.methods) initMethods(vm, opts.methods)
// 初始化data
if (opts.data) {
initData(vm)
} else {
// 先不用管
observe(vm._data = {}, true /* asRootData */)
}
// 初始化Computed
if (opts.computed) initComputed(vm, opts.computed)
// 初始化watch
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
initData()
这个 data ,就是在new Vue()参数中传入的 data
function initData (vm: Component) {
// 从合并后的options中拿到data
let data = vm.$options.data
// 复制 data 到实例上的 _data 上
// 判断是 function 还是对象
// 如果是function,就通过 getData 获取,反之直接获取
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:
' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// ========= 代理一下 data (start)=========
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
// 这里遍历data的key,分别和props与methods对比,如果有重复命名就报警告
// 因为最终这三个对象中定义的变量都会通过下面的proxy方法挂载到vm上,所以不能重名
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)) {
// 【重点】代理 data ,将 _data 中的属性代理到vm实例上
// 也就是通过 vm.vinsea 就能获取到 vm._data.vinsea
proxy(vm, `_data`, key)
}
}
// ========= 代理一下 data (end)=========
// 对 data 做响应式
observe(data, true /* asRootData */)
}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
以上代码的重点就是
proxy(vm, `_data`, key)这个方法的作用就是将 data中定义的属性,都挂载到vm上,让你可以方便的通过 this.xxx 就获取到 data中定义的 xxx
proxy()
看一下这个 proxy 方法都做了什么
// 定义一个公用的对象属性
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function proxy (target: Object, sourceKey: string, key: string) {
// 重写get,当获取数据时,会从this的sourceKey中获取名为key的属性
// 结合initData(),也就是 当执行this.xxx,这个xxx属性会通过 vm._data中获取
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
// 初始化这个对象(vm实例)
Object.defineProperty(target, key, sharedPropertyDefinition)
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
所以
new Vue({
el: '#app',
data() {
return {
name: 'Vinsea'
}
},
mounted() {
console.log(this.name) // Vinsea
console.log(this._data.name) // Vinsea
}
})2
3
4
5
6
7
8
9
10
11
12
通过 _data 也能获取到data中的变量,但是众所周知,变量前面带下划线,代表的私有变量,所以我们平时就可以忽视这个 _data 对象。