vue 源码深入学习分析——史上超详细
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了vue 源码深入学习分析——史上超详细相关的知识,希望对你有一定的参考价值。
2017/6/2 15:27:50 第一次复习
vue 框架号称五分钟就能上手,半小时就能精通,这是因为其使用非常简单,就像下面一样:
let vm = new Vue({
el: ‘#app‘,
data: {
a: 1,
b: [1, 2, 3]
}
})
在最开始,我传递了两个选项 el 以及 data ,很简单,官网上也是这样写的。
你肯定注意到了,我使用了 new 操作符。这就很自然的想到,Vue 就是一个构造函数,vm是 Vue构造函数 生成的实例,我们的配置项是传入构造函数的参数,是一个包括 el 属性 和 data属性的对象,事实上在实例化 Vue 时,传入的选项对象可以包含数据、模板、挂载元素、方法、生命周期钩子等选项。全部的选项可以在 vue的官方API 文档中查看。;
那么我们下面就要受好奇心的驱动,来看看 Vue构造函数 是什么样的?
在 \node_modules\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‘
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 V
不用害怕,我带你捋一捋,我们首先关注第8行,我摘抄出来:
function Vue (options) {
if (process.env.NODE_ENV !== ‘production‘ && // 这个 if 判断,是当你不用new操作符来实例化Vue构造函数时,会爆出警告
!(this instanceof Vue)) {
warn(‘Vue is a constructor and should be called with the `new` keyword‘)
}
this._init(options) // 主要就是这一句,
}
发现了吧,Vue 的确是一个构造函数,和你平时使用的 Array, Object 等普普通通的构造函数,没有本质的区别。
在构造函数里面,我们要关心的是 this._init( options ) , 稍微我会详细的来讲,我们先看 \node_modules\vue\src\core\instance\index.js 文件中的第16行~20行:
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
上面的代码调用了五个方法,这五个方法都是把Vue构造函数作为参数传入,其目的都是在 Vue .prototype 上挂载方法或属性,这个概念很好理解,我们在js 的原型链继承的学习中,经常把属性和方法丢到构造函数的原型上作为公有的属性和方法。
// initMixin(Vue) src/core/instance/init.js **************************************************
Vue.prototype._init = function (options?: Object) {}
// stateMixin(Vue) src/core/instance/state.js **************************************************
Vue.prototype.$data
Vue.prototype.$set = set
Vue.prototype.$delete = del
Vue.prototype.$watch = function(){}
// renderMixin(Vue) src/core/instance/render.js **************************************************
Vue.prototype.$nextTick = function (fn: Function) {}
Vue.prototype._render = function (): VNode {}
Vue.prototype._s = _toString
Vue.prototype._v = createTextVNode
Vue.prototype._n = toNumber
Vue.prototype._e = createEmptyVNode
Vue.prototype._q = looseEqual
Vue.prototype._i = looseIndexOf
Vue.prototype._m = function(){}
Vue.prototype._o = function(){}
Vue.prototype._f = function resolveFilter (id) {}
Vue.prototype._l = function(){}
Vue.prototype._t = function(){}
Vue.prototype._b = function(){}
Vue.prototype._k = function(){}
// eventsMixin(Vue) src/core/instance/events.js **************************************************
Vue.prototype.$on = function (event: string, fn: Function): Component {}
Vue.prototype.$once = function (event: string, fn: Function): Component {}
Vue.prototype.$off = function (event?: string, fn?: Function): Component {}
Vue.prototype.$emit = function (event: string): Component {}
// lifecycleMixin(Vue) src/core/instance/lifecycle.js **************************************************
Vue.prototype._mount = function(){}
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {}
Vue.prototype._updateFromParent = function(){}
Vue.prototype.$forceUpdate = function () {}
Vue.prototype.$destroy = function () {}
经过上面5个方法对Vue构造函数的处理,vm实例上就可以使用这些属性和方法了。其实在其他地方,Vue 构造函数也被处理了:在src/core/index.js 文件中:
import Vue from ‘./instance/index‘
import { initGlobalAPI } from ‘./global-api/index‘
import { isServerRendering } from ‘core/util/env‘
initGlobalAPI(Vue)
Object.defineProperty(Vue.prototype, ‘$isServer‘, { //为 Vue.prototype 添加$isServer属性
get: isServerRendering
})
Vue.version = ‘__VERSION__‘ // 在VUE 身上挂载了 version的静态属性
export default Vue
initGlobalAPI() 的作用是在 Vue 构造函数上挂载静态属性和方法,Vue 在经过 initGlobalAPI 之后,会变成这样:
Vue.config
Vue.util = util
Vue.set = set
Vue.delete = del
Vue.nextTick = util.nextTick
Vue.options = {
components: {
KeepAlive
},
directives: {},
filters: {},
_base: Vue
}
Vue.use
Vue.mixin
Vue.cid = 0
Vue.extend
Vue.component = function(){}
Vue.directive = function(){}
Vue.filter = function(){}
Vue.prototype.$isServer
Vue.version = ‘__VERSION__‘
下一个就是 web-runtime.js 文件了,web-runtime.js 文件主要做了三件事儿:
1、覆盖 Vue.config 的属性,将其设置为平台特有的一些方法
2、Vue.options.directives 和 Vue.options.components 安装平台特有的指令和组件
3、在 Vue.prototype 上定义 __patch__ 和 $mount
经过 web-runtime.js 文件之后,Vue 变成下面这个样子:
// 安装平台特定的utils
Vue.config.isUnknownElement = isUnknownElement
Vue.config.isReservedTag = isReservedTag
Vue.config.getTagNamespace = getTagNamespace
Vue.config.mustUseProp = mustUseProp
// 安装平台特定的 指令 和 组件
Vue.options = {
components: {
KeepAlive,
Transition,
TransitionGroup
},
directives: {
model,
show
},
filters: {},
_base: Vue
}
Vue.prototype.__patch__
Vue.prototype.$mount
这里要注意的是Vue.options 的变化。
最后一个处理 Vue 的文件就是入口文件 web-runtime-with-compiler.js 了,该文件做了两件事:
1、缓存来自 web-runtime.js 文件的 $mount 函数
const mount = Vue.prototype.$mount
2、在 Vue 上挂载 compile
Vue.compile = compileToFunctions
上面 compileToFunctions 函数可以将模板 template 编译为render函数。
至此,我们算是还原了 Vue 构造函数,总结一下:
1、Vue.prototype 下的属性和方法的挂载主要是在 src/core/instance 目录中的代码处理的
2、Vue 下的静态属性和方法的挂载主要是在 src/core/global-api 目录下的代码处理的
3、web-runtime.js 主要是添加web平台特有的配置、组件和指令,web-runtime-with-compiler.js 给Vue的 $mount 方法添加 compiler 编译器,支持 template。
好了,我们再回过头来看 this._init() 方法,_init() 方法就是Vue调用的第一个方法,然后将我们的参数 options 传了过去。_init() 是在 \node_modules\vue\src\core\instance\init.js 文件中被声明的:
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== ‘production‘ && config.performance && mark) {
startTag = `vue-perf-init:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// a flag to avoid this being observed
vm._isVue = true
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else { // 大部分情况下是走了这个分支,也是vue第一步要做的事情,使用mergeOptions来合并参数选项
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== ‘production‘) {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, ‘beforeCreate‘)
initInjections(vm) // resolve injections before data/props
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(`${vm._name} init`, startTag, endTag)
}
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
好了,我们一开始不需要关心那么多边边角角,直接从23行代码开始看,因为大部分情况下是走了这条分支,也就是执行了下面的代码:
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
这里是执行了 mergeOptions 函数,并将返回值赋值给 vm.$options 属性。 mergeOptions 函数接受三个参数,分别是 resolveContructorOptions方法, 我们调用 vue 构造函数传入的配置对象(如果没有就是空对象),以及 vm 实例 本身。
我们先看 resovleContructorOptions 方法, 传入的参数是 vm.constructor 。 vm.constructor 代表的是啥? const vm: Component = this 人家_init() 函数第一行就定义了,是指向_init() 函数内部的this, _init( ) 函数是 Vue.prototype上的一个方法,所以在其身上调用的时候,this 指向本身 Vue.prototype, 那么 vm.constructor 也就是指向 Vue 构造函数.
export function resolveConstructorOptions (Ctor: Class<Component>) { //ctor 就是 VUE 构造函数
let options = Ctor.options // vue 构造函数身上的 options 属性
if (Ctor.super) { // 判断是否定义了 Vue.super ,这个是用来处理继承的,我们后续再讲
const superOptions = resolveConstructorOptions(Ctor.super)
const cachedSuperOptions = Ctor.superOptions
if (superOptions !== cachedSuperOptions) {
// super option changed,
// need to resolve new options.
Ctor.superOptions = superOptions
// check if there are any late-modified/attached options (#4976)
const modifiedOptions = resolveModifiedOptions(Ctor)
// update base extend options
if (modifiedOptions) {
extend(Ctor.extendOptions, modifiedOptions)
}
options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
if (options.name) {
options.components[options.name] = Ctor
}
}
}
return options
}
第22行,resolveConstructorOptions 方法直接返回了 Vue.options。也就是说,传递给 mergeOptions 方法的第一个参数其实是 Vue.options。那么,实际上原来的代码就变成了下面这样:
// 这是原来的代码
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
// 实际上传过去的参数是下面这些
vm.$options = mergeOptions(
// Vue.options
{
components: {
KeepAlive,
Transition,
TransitionGroup
},
directives: {
model,
show
},
filters: {},
_base: Vue
},
// 调用Vue构造函数时传入的参数选项 options
{
el: ‘#app‘,
data: {
a: 1,
b: [1, 2, 3]
}
},
// this
vm
)
为什么要使用 mergeOptions 方法呢? 是为了 合并策略, 对于子组件和父组件如果有相同的属性(option)时要进行合并,相关文章:
那么我们继续查看 _init() 方法在合并完选项之后,Vue 第二部做的事情就来了:初始化工作与Vue实例对象的设计:
通过initData 看vue的数据响应系统
Vue的数据响应系统包含三个部分: Observer 、 Dep 、 Watcher 。我们还是先看一下 initData 中的代码:
function initData (vm: Component) {
let data = vm.$options.data // 第一步还是要先拿到数据,vm.$options.data 这时候还是通过 mergeOptions 合并处理后的 mergedInstanceDataFn 函数
data = vm._data = typeof data === ‘function‘
? data.call(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
let i = keys.length
while (i--) {
if (props && hasOwn(props, keys[i])) {
process.env.NODE_ENV !== ‘production‘ && warn(
`The data property "${keys[i]}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else {
proxy(vm, keys[i]) // 目的是在实例对象上对数据进行代理,这样我们就能通过 this.a 来访问 data.a 了
}
}
// observe data
observe(data)
data.__ob__ && data.__ob__.vmCount++
}
上面 proxy 方法非常简单,仅仅是在实例对象上设置与 data 属性同名的访问器属性,然后使用 _data 做数据劫持,如下:
function proxy (vm: Component, key: string) {
if (!isReserved(key)) {
Object.defineProperty(vm, key, { // vm是实例,key是data属性上的属性,
configurable: true,
enumerable: true,
get: function proxyGetter () {
return vm._data[key]
},
set: function proxySetter (val) {
vm._data[key] = val
}
})
}
}
做完数据的代理,就正式进入响应系统:
observe(data)
我们说过,数据响应系统主要包含三部分: Observer 、Dep、Watcher,
我们首先思考,我们应该如何观察一个数据对象的变化?
vue.js和avalon.js 都是通过 Object.definedProperty() 方法来实现的, 下面我们主要来介绍一下这个方法为什么可以实现对对象属性改变的监听。
Object.defineProperty ( )有三个参数, 三个参数都需要,分别是对象,属性,属性的属性
var o = {};
Object.definedProperty(o, ‘a‘, {
value: ‘b‘
})
属性的属性有下面这些:
configurable:true | false,
enumerable:true | false,
value:任意类型的值,
writable:true | false
writable
该属性的值是否可以修改;如果设置为false,则不能被修改,修改不会报错,只是默默的不修改;
var obj = {}
//第一种情况:writable设置为false,不能重写。
Object.defineProperty(obj,"newKey",{
value:"hello",
writable:false
});
//更改newKey的值
obj.newKey = "change value";
console.log( obj.newKey ); //hello
//第二种情况:writable设置为true,可以重写
Object.defineProperty(obj,"newKey",{
value:"hello",
writable:true
});
//更改newKey的值
obj.newKey = "change value";
console.log( obj.newKey ); //change value
enumerable
是否该属性可以被for……in 或者Object.keys( ) 枚举
var obj = {}
//第一种情况:enumerable设置为false,不能被枚举。
Object.defineProperty(obj,"newKey",{
value:"hello",
writable:false,
enumerable:false
});
//枚举对象的属性
for( var attr in obj ){
console.log( attr );
}
//第二种情况:enumerable设置为true,可以被枚举。
Object.defineProperty(obj,"newKey",{
value:"hello",
writable:false,
enumerable:true
});
//枚举对象的属性
for( var attr in obj ){
console.log( attr ); //newKey
}
configurable
是否可以删除目标属性或是否可以再次修改属性的特性(writable, configurable, enumerable)。设置为true可以被删除或可以重新设置特性;设置为false,不能被可以被删除或不可以重新设置特性。默认为false。
这个属性起到两个作用:
1、 目标属性是否可以使用delete删除
2、目标属性是否可以再次设置特性
//-----------------测试目标属性是否能被删除------------------------
var obj = {}
//第一种情况:configurable设置为false,不能被删除。
Object.defineProperty(obj,"newKey",{
value:"hello",
writable:false,
enumerable:false,
configurable:false
});
//删除属性
delete obj.newKey; //可以用delete 关键字来删除某一个对象上的属性
console.log( obj.newKey ); //hello
//第二种情况:configurable设置为true,可以被删除。
Object.defineProperty(obj,"newKey",{
value:"hello",
writable:false,
enumerable:false,
configurable:true
});
//删除属性
delete obj.newKey;
console.log( obj.newKey ); //undefined
//-----------------测试是否可以再次修改特性------------------------
var obj = {}
//第一种情况:configurable设置为false,不能再次修改特性。
Object.defineProperty(obj,"newKey",{
value:"hello",
writable:false,
enumerable:false,
configurable:false
});
//重新修改特性
Object.defineProperty(obj,"newKey",{
value:"hello",
writable:true,
enumerable:true,
configurable:true
});
console.log( obj.newKey ); //报错:Uncaught TypeError: Cannot redefine property: newKey
//第二种情况:configurable设置为true,可以再次修改特性。
Object.defineProperty(obj,"newKey",{
value:"hello",
writable:false,
enumerable:false,
configurable:true
});
//重新修改特性
Object.defineProperty(obj,"newKey",{
value:"hello",
writable:true,
enumerable:true,
configurable:true
});
console.log( obj.newKey ); //hello
一旦使用 Object.defineProperty 给对象添加属性,那么如果不设置属性的特性,那么configurable、enumerable、writable这些值都为默认的false
存取器描述:get set
不能 同时设置访问器 (get 和 set) 和 wriable 或 value,否则会错,就是说想用(get 和 set),就不能用(wriable 或 value中的任何一个)
注意:get set是加在对象属性上面的,不是对象上面的;赋值或者修改该对象属性,会分别触发get 和 set 方法;
正规用法:
var o = {}; // 不能是O.name=" dudu "了
var val = ‘dudu‘; // o 对象上的属性是其他人家的一个变量
Object.definedProperty(o,‘name‘,{ // Object.definedProperty( ) 方法通过定set get 方法,强行给拉郎配
get:function(){ return val }; //get: return val 把人家变量给返回了,就是人家的人了
set;function(value){ val = value } //set: val = value 把人家变量赋值为传进来的参数,就是人间人了
})
实验性代码:
var O = {};
Object.definedProperty(o,"name",{
set:function(){console.log(‘set‘)}; //在获取对象该属性的时候触发,
get:function(){console.log(‘get‘)}; // 在设置对象该属性的时候触发 , 并不会真正的设置;因为冲突了value,默认是falue
})
所以,你看到这里,基本上就能够明白,通过Object.defineProperty()来重写对象的get, set 方法,就可以在对象属性被访问和修改的时候获知 ,从而触发响应的回调函数,但是同一个数据属性,很可能有多个 watcher 来订阅的 ,所触发的回调函数可能有很多,不可能都写在 get set 里面,我们更希望更通过这样的方式:
var data = {
a: 1,
b: {
c: 2
}
}
observer(data) // 在这里遍历改写了get,set
new Watch(‘a‘, () => {
alert(9)
})
new Watch(‘a‘, () => {
alert(90)
})
new Watch(‘b.c‘, () => {
alert(80)
})
现在的问题是, Watch 构造函数要怎么写?
在 Watch 构造函数里面,我们已经可以获取到 data,当我们访问的时候,就会触发 data 的改写的get 方法:
class Watch {
constructor (exp, fn) {
// ……
data[exp] // 触发了data 身上的get 方法
}
}
当我们每实例化一个 Watch来订阅data上的a属性 , data.a 上的get 方法就会被触发一次, data.a 就多了一个订阅器。那么问题来了,这么多的订阅器watcher,我们肯定希望放在一个数组上进行管理,同时我们还希望有,向数组中 push 新的订阅器watcher的方法, 逐个触发数组中各个watcher的方法等等。这样,我们的data 上的每一个属性,它都有一个数组来放订阅器,都有相应的方法来操作这个数组。根据面向对象中的思想,我们可以把这个数组和操作数组的方法放进一个对象中, 这个对象就叫dep吧 :
dep {
subs: [watcher1,watcher2,watcher3], // subs 属性是一个数组,用来维护众多订阅器
addSubs: function(){ this.subs.push( …… ) },
notify: function() {
for(let i = 0; i< this.subs.length; i++){
this.subs[i].fn()
}
}
}
dep 对象我们希望用构造函数来生成,这样会比较方便:
class Dep {
constructor () {
this.subs = []
}
addSub () {
this.subs.push(……)
}
notify () {
for(let i = 0; i < this.subs.length; i++){
this.subs[i].fn()
}
}
}
接下来,我们要在每一个data 属性上生成一个dep实例对象:
function defineReactive (data, key, val) { // 这个函数就是用来重写对象属性的get set 方法
observer(val) // 递归的调用从而遍历
let dep = new Dep() // 在这里实例化一个dep实例
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
dep.addSub() //每当有订阅者订阅,我就新增一个
return val
},
set: function (newVal) {
if(val === newVal){
return
}
observer(newVal)
dep.notify() // 新增
}
})
}
等等,在第8行,执行 dep.addSub , 我怎么知道是要push 进去哪个 watcher 呢? 我们需要改写一下 watch 的构造函数:
Dep.target = null //类似于全局变量的一个东西,用来放 这次实例化的watcher
function pushTarget(watch){
Dep.target = watch
}
class Watch {
constructor (exp, fn) {
this.exp = exp
this.fn = fn
pushTarget(this) // 让Dep.target赋值为本次实例化的实例
data[exp] //紧接着就触发get 方法
}
}
被触发的get 方法在下面:
get: function () {
dep.addSub() //好吧,我又被触发了一次,
return val
},
dep.addSub() 方法的庐山真面目:
class Dep {
constructor () {
this.subs = []
}
addSub () {
this.subs.push(Dep.target)
}
notify () {
for(let i = 0; i < this.subs.length; i++){
this.subs[i].fn()
}
}
}
以上是关于vue 源码深入学习分析——史上超详细的主要内容,如果未能解决你的问题,请参考以下文章
史上最全的 IDEA Debug 调试技巧(超详细!建议收藏!)