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.$dataVue.prototype.$set = setVue.prototype.$delete = delVue.prototype.$watch = function(){}// renderMixin(Vue) src/core/instance/render.js **************************************************Vue.prototype.$nextTick = function (fn: Function) {}Vue.prototype._render = function (): VNode {}Vue.prototype._s = _toStringVue.prototype._v = createTextVNodeVue.prototype._n = toNumberVue.prototype._e = createEmptyVNodeVue.prototype._q = looseEqualVue.prototype._i = looseIndexOfVue.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.configVue.util = utilVue.set = setVue.delete = delVue.nextTick = util.nextTickVue.options = { components: { KeepAlive }, directives: {}, filters: {}, _base: Vue}Vue.useVue.mixinVue.cid = 0Vue.extendVue.component = function(){}Vue.directive = function(){}Vue.filter = function(){}Vue.prototype.$isServerVue.version = '__VERSION__'
下一个就是 web-runtime.js 文件了, web-runtime.js 文件主要作了三件事儿:
    
    
    
    
1、覆盖 Vue.config 的属性,将其设置为平台特有的一些方法2Vue.options.directives Vue.options.components 安装平台特有的指令和组件3、在 Vue.prototype 上定义 __patch__ $mount
通过 web-runtime.js 文件以后,Vue 变成下面这个样子:
    
    
    
    
// 安装平台特定的utilsVue.config.isUnknownElement = isUnknownElementVue.config.isReservedTag = isReservedTagVue.config.getTagNamespace = getTagNamespaceVue.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 了,该文件作了两件事:
一、缓存来自 web-runtime.js 文件的 $mount 函数
    
    
    
    
const mount = Vue.prototype.$mount
二、在 Vue 上挂载 compile
    
    
    
    
Vue.compile = compileToFunctions
上面 compileToFunctions 函数能够将模板 template 编译为render函数。
至此,咱们算是还原了 Vue 构造函数,总结一下:
    
    
    
    
1Vue.prototype 下的属性和方法的挂载主要是在 src/core/instance 目录中的代码处理的2Vue 下的静态属性和方法的挂载主要是在 src/core/global-api 目录下的代码处理的3web-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。
这个属性起到两个做用:
 一、  目标属性是否可使用delete删除
 二、目标属性是否能够再次设置特性
    
    
    
    
//-----------------测试目标属性是否能被删除------------------------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() } }}













相关文章
相关标签/搜索