前文中,已经分析了在vuejs源码中是如何定义Vue类,以及如何添加实例属性和静态方法:大数据进阶-读懂vuejs源码1。vue
Vue实例化时调用_init,本文将深刻该方法内部作了哪些事情及vuejs如何实现数据响应式。node
在core/instance/index.js
文件中定义了Vue的构造函数:react
function Vue (options) { // 执行_init方法,此方法在initMixin中定义 this._init(options) }
_init方法定义在core/instance/init.js
中:web
Vue.prototype._init = function (options?: Object) { // 。。。 // 1. 合并options if (options && options._isComponent) { // 此处有重要的事情作。 initInternalComponent(vm, options) } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } // 2. 初始化属性 // 初始化$root,$parent,$children initLifecycle(vm) // 初始化_events initEvents(vm) // 初始化$slots/$scopedSlots/_c/$createElement/$attrs/$listeners initRender(vm) // 执行生命周期钩子 callHook(vm, 'beforeCreate') // 注册inject成员到vue实例上 initInjections(vm) // resolve injections before data/props // 初始化_props/methods/_data/computed/watch initState(vm) // 初始化_provided initProvide(vm) // resolve provide after data/props // 执行生命周期钩子 callHook(vm, 'created') // 3. 调用$mount方法 if (vm.$options.el) { vm.$mount(vm.$options.el) } }
在合并options的时候,若是options表示一个组件(_isComponent)则调用了initInternalComponent
函数:express
export function initInternalComponent(vm: Component, options: InternalComponentOptions) { // 此处保留组件之间的父子关系, const parentVnode = options._parentVnode opts.parent = options.parent opts._parentVnode = parentVnode //... }
此方法中设置了组件之间的父子关系,在后续的注册及渲染组件的时候会用到。segmentfault
定义在core/instance/inject.js
文件中。数组
export function initProvide (vm: Component) { const provide = vm.$options.provide if (provide) { vm._provided = typeof provide === 'function' ? provide.call(vm) : provide } }
在上面的代码中能够看出,若是provide是一个函数,那么会调用这个函数,并将this指向vm实例。因为initProvide在_init方法中最后被调用,所以可以访问到实例的属性。闭包
定义在core/instance/inject.js
文件中。app
export function initInjections (vm: Component) { const result = resolveInject(vm.$options.inject, vm) if (result) { // 遍历result属性,利用Object.defineProperty将其添加到vue实例上 // ... } }
此方法调用resolveInject方法获取全部inject值。框架
export function resolveInject(inject: any, vm: Component): ?Object { if (inject) { const result = Object.create(null) const keys = hasSymbol ? Reflect.ownKeys(inject) : Object.keys(inject) for (let i = 0; i < keys.length; i++) { // .... const provideKey = inject[key].from let source = vm while (source) { if (source._provided && hasOwn(source._provided, provideKey)) { result[key] = source._provided[provideKey] break } source = source.$parent } // ... } return result } }
在resolveInject方法中会从当前实例出发,延着parent一直向上找,直到找到_provided中存在。
此时整个Vue定义和初始化流程能够总结为以下:
vuejs框架的整个数据响应式实现过程比较复杂,代码散落在各个文件中。咱们都知道,在定义组件的时候,组件会自动将data属性中的数据添加上响应式监听,所以咱们从_init方法中调用initState
函数开始。
在initState函数中:
export function initState (vm: Component) { // ... if (opts.data) { // 处理data数据 initData(vm) } else { observe(vm._data = {}, true /* asRootData */) } // ... }
options中的data数据会交由initData方法处理:
function initData(vm: Component) { // ... 1. 获取data数据,若是data是一个函数,但没有返回值,会提示错误。 // ... 2. 遍历data全部的属性,首先判断在props和methods是否同名,而后将其代理到vue实例上。 // 3. 添加响应式数据监听 observe(data, true /* asRootData */) }
定义在core/observer/index.js
文件中:
export function observe (value: any, asRootData: ?boolean): Observer | void { if (!isObject(value) || value instanceof VNode) { return } let ob: Observer | void // 经过__ob__属性判断该属性是否添加过响应式监听 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 ) { // 建立Observer实例,其为响应式的核心 ob = new Observer(value) } // 经过vmCount能够判断某个响应式数据是不是根数据,能够理解为data属性返回的对象是根数据,若是data对象的某个属性也是一个对象,那么就再也不是根数据。 // vmCount属性后续会被用到 if (asRootData && ob) { ob.vmCount++ } return ob }
该方法的核心就是为data数据建立Observer实例ob, ob对象会为data添加getter/setter方法,其能够用来收集依赖并在变化的时候触发dom更新。
定义在core/observer/index.js
文件中,在其构造函数中,根据传入data的类型(Array/Object),分别进行处理。
constructor(value: any) { this.value = value // Observer实例上包含dep属性,这个属性后续会有很大做用,有些没法监听的数据变化能够由此属性完成 this.dep = new Dep() this.vmCount = 0 // 为data添加__ob__属性 def(value, '__ob__', this) if (Array.isArray(value)) { // ... 处理数组 } else { // 处理对象 this.walk(value) } }
遍历data的全部属性,调用defineReactive
函数添加getter/setter。
walk(obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { // 添加数据拦截 defineReactive(obj, keys[i]) } }
数据响应式实现的核心方法,原理是经过Object.defineProperty为data添加getter/setter拦截,在拦截中实现依赖收集和触发更新。
export function defineReactive( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { // 1. 建立闭包做用域内的Dep对象,用于收集观察者,当数据发生变化的时候,会触发观察者进行update const dep = new Dep() const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } // 2. 获取对象描述中原有的get和set方法 const getter = property && property.get const setter = property && property.set if ((!getter || setter) && arguments.length === 2) { val = obj[key] } let childOb = !shallow && observe(val) // 3. 添加getter/setter Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter() { const value = getter ? getter.call(obj) : val // 静态属性target存储的是当前观察者。 if (Dep.target) { dep.depend() if (childOb) { // 将观察者添加到Obsetver实例属性dep中。 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 } // ... 一些判断,省略 // 当赋值的时候,若是值为对象,须要为新赋值的对象添加响应式 childOb = !shallow && observe(newVal) // 调用set就是为属性赋值,赋值说明有新的变化,因此要触发更新 dep.notify() } }) }
整个defineReactive有两个地方比较难以理解:
因为这个地方涉及到后面的编译部分,因此咱们把这部分逻辑单独拿出来,用一段简短的代码来描述整个过程,以下:
// 模拟Dep let Dep = {} Dep.target = null // 模拟变化数据 let data = { foo: 'foo' } Object.defineProperty(data, 'foo', { get() { if (Dep.target) { console.log(Dep.target) } } }) // 模拟编译 {{foo}} // 1. 解析到template中须要foo属性的值 const key = 'foo' // 2. 在foo属性对应的值渲染到页面以前,为Dep.target赋值 Dep.target = () => { console.log('观察foo的变化') } // 3. 获取foo属性的值,此时会触发get拦截 const value = data[key] // 4. 获取完成后,须要将Dep.target的值从新赋值null,这样下一轮解析的时候,可以存储新的观察者 Dep.target = null
其实,这是为了方便在其余手动触发更新,因为defineReactive方法内部的dep对象是闭包做用域,在外部没法直接访问,只能经过赋值方式触发。
若是在Observer对象上保存一份,那么就能够经过data.__ob__.dep
的方式访问到,直接手动调用notify方法就能够触发更新,在Vue.set方法内部实现就能够这种触发更新方式。
众所周知,Object.defineProperty是没法监控到经过push,pop等方法改变数组,此时,vuejs经过另一种方式实现了数组响应式。该方式修改了数组原生的push,pop等方法,在新定义的方法中,经过调用数组对象的__ob__
属性的notify方法,手动触发更新。
Observer构造函数中:
if (Array.isArray(value)) { if (hasProto) { // 支持__proto__,那么就经过obj.__proto__的方式修改原型 protoAugment(value, arrayMethods) } else { // 不支持,就将新定义的方法遍历添加到数组对象上,这样能够覆盖原型链上的原生方法 copyAugment(value, arrayMethods, arrayKeys) } // 遍历数组项,若是某项是对象,那么为该对象添加响应式 this.observeArray(value) }
其中arrayMethods就是从新定义的数组操做方法。
定义在core/Observer/array.js
文件中,该文件主要做了两件事情:
Array.prototype
的原型对象arrayMethods。const arrayProto = Array.prototype export const arrayMethods = Object.create(arrayProto)
// 定义全部会触发更新的方法 const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] methodsToPatch.forEach(function (method) { // 获取Array中原生的同名方法 const original = arrayProto[method] // 经过Object.defineProperty为方法调用添加拦截 def(arrayMethods, method, function mutator(...args) { // 调用原生方法获取本该获得的结果 const result = original.apply(this, args) const ob = this.__ob__ let inserted // push,unshift,splice三个方法会向数组中插入新值,此处根据状况获取新插入的值 switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } // 若是新插入的值是对象,那么须要为对象添加响应式,处理逻辑和data处理逻辑类似 if (inserted) ob.observeArray(inserted) // 手动触发更新 ob.dep.notify() return result }) })
从上面的处理逻辑能够看出,下面的数组操做能够触发自动更新:
// 修改数组项 [].push(1) [].pop() [].unshift(1) [].shift() [].splice() // 修改数组项顺序 [].sort() [].reverse()
而下面的操做不能触发:
// 修改数组项 [1, 2][0] = 3 [1, 2].length = 0
在添加数据监听的过程当中用到了Dep类,Dep类至关于观察者模式中的目标,用于存储全部的观察者和发生变化时调用观察者的update方进行更新。
export default class Dep { // 当前须要添加的观察者 static target: ?Watcher; // id,惟一标识 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) } // 调用观察者的addDep方法,将目标添加到每个观察者中,观察者会调用addSub方法 depend() { if (Dep.target) { Dep.target.addDep(this) } } // 将观察者排序,而后依次调用update 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() } } }
Watcher类是观察者模式中的观察者,当Dep触发变化的时候,会调用内部存储的全部Watcher实例的update方法进行更新操做。
在vuejs中,Watcher可大体分为三种:Computed Watcher, 用户Watcher(侦听器)和渲染Watcher(触发Dom更新)。
Watcher类包含大量的实例成员,在构造函数中,主要逻辑以下:
constructor( vm: Component, expOrFn: string | Function, cb: Function, options ?: ? Object, isRenderWatcher ?: boolean ) { // ... 根据参数为实例成员赋值 // 调用get方法 this.value = this.lazy ? undefined : this.get() }
在get方法中,获取初始值并将自身添加到Dep.target。
get() { // 1. 和下面的popTarget相对应,这里主要是为Dep.target赋值 // 因为存在组件之间的父子关系,因此在pushTarget中还会将当前对象存放到队列中,方便处理完成子组件后继续处理父组件 pushTarget(this) let value const vm = this.vm try { // 2. 获取初始值,并触发get监听,Dep会收集该Watcher value = this.getter.call(vm, vm) } catch (e) { if (this.user) { handleError(e, vm, `getter for watcher "${this.expression}"`) } else { throw e } } finally { // 实现deep深度监听 if (this.deep) { traverse(value) } // 3. 将Dep.target值变为null popTarget() this.cleanupDeps() } return value }
addDep方法用于将当前Watcher实例添加到Dep中。
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)) { // 调用dep的addSub方法,将Watcher实例添加到Dep中 dep.addSub(this) } } }
update主要处理两种状况:
update() { if (this.lazy) { this.dirty = true } else if (this.sync) { // 用户添加的监听器会执行run方法 this.run() } else { // 触发dom更新会执行此方法, 以队列方式执行update更新 queueWatcher(this) } }
run方法主要用于在数据变化后,执行用户传入的回调函数。
run() { if (this.active) { // 1. 经过get方法获取变化后的值 const value = this.get() if ( value !== this.value || isObject(value) || this.deep ) { // 2. 获取初始化时保存的值做为旧值 const oldValue = this.value this.value = value if (this.user) { try { // 3. 调用用户定义的回调函数 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) } } } }
在查找编译入口那部分讲到了platforms/web/runtime/index.js
文件定义了$mount方法,此方法用于首次渲染Dom。
Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && inBrowser ? query(el) : undefined return mountComponent(this, el, hydrating) }
其内部执行了mountComponent函数。
定义在core/instance/lifecycle.js
文件中,该函数主要执行三块内容:
beforeMount
,beforeUpdate
和mounted
生命周期钩子函数。updateComponent
方法。export function mountComponent( vm: Component, el: ?Element, hydrating?: boolean ): Component { // ... 1. 触发生命周期钩子 // 2. 定义updateComponent方法 let updateComponent if (process.env.NODE_ENV !== 'production' && config.performance && mark) { updateComponent = () => { // ... vm._update(vnode, hydrating) // ... } } else { updateComponent = () => { vm._update(vm._render(), hydrating) } } // 生成watcher实例 new Watcher(vm, updateComponent, noop, { before() { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) hydrating = false // ... 触发生命周期钩子 return vm }
_update, _render是Vue的实例方法, _render方法用于根据用户定义的render或者模板生成的render生成虚拟Dom。_update方法根据传入的虚拟Dom,执行patch,进行Dom对比更新。
至此,响应式处理的整个闭环脉络已经摸清。