前文中,曾经剖析了在vuejs源码中是如何定义Vue类,以及如何增加实例属性和静态方法:大数据进阶-读懂vuejs源码1。
Vue实例化时调用_init,本文将深刻该办法外部做了哪些事件及vuejs如何实现数据响应式。
Vue初始化
在core/instance/index.js
文件中定义了Vue的构造函数:
function Vue (options) { // 执行_init办法,此办法在initMixin中定义 this._init(options)}
_init办法定义在core/instance/init.js
中:
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
函数:
export function initInternalComponent(vm: Component, options: InternalComponentOptions) { // 此处保留组件之间的父子关系, const parentVnode = options._parentVnode opts.parent = options.parent opts._parentVnode = parentVnode //...}
此办法中设置了组件之间的父子关系,在后续的注册及渲染组件的时候会用到。
initProvide
定义在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办法中最初被调用,因而可能拜访到实例的属性。
initInjections
定义在core/instance/inject.js
文件中。
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 */)}
增加监听
observe函数
定义在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更新。
Observer类
定义在core/observer/index.js
文件中,在其构造函数中,依据传入data的类型(Array/Object),别离进行解决。
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) }}
- walk
遍历data的所有属性,调用defineReactive
函数增加getter/setter。
walk(obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { // 增加数据拦挡 defineReactive(obj, keys[i]) }}
- defineReactive
数据响应式实现的外围办法,原理是通过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.target获取依赖
因为这个中央波及到前面的编译局部,所以咱们把这部分逻辑独自拿进去,用一段简短的代码来形容整个过程,如下:
// 模仿Deplet 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
- 在闭包作用域内曾经蕴含了Dep对象,在set中通过此对象的notify办法触发更新,为什么还须要在get办法中,将依赖增加到Observer对象的实例属性dep中。
其实,这是为了不便在其余手动触发更新,因为defineReactive办法外部的dep对象是闭包作用域,在内部无奈间接拜访,只能通过赋值形式触发。
如果在Observer对象上保留一份,那么就能够通过data.__ob__.dep
的形式拜访到,间接手动调用notify办法就能够触发更新,在Vue.set办法外部实现就能够这种触发更新形式。
Array
家喻户晓,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就是从新定义的数组操作方法。
- arrayMethods
定义在core/Observer/array.js
文件中,该文件次要作了两件事件:
- 创立新的集成自
Array.prototype
的原型对象arrayMethods。
const arrayProto = Array.prototypeexport 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类,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
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
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次要解决两种状况:
- 如果是用户增加的监听器,在变动的时候会执行run办法。
- 如果是渲染Dom时增加的,在变动的时候会执行queueWatcher函数,在queueWatcher函数中,通过队列的形式批量执行更新。
update() { if (this.lazy) { this.dirty = true } else if (this.sync) { // 用户增加的监听器会执行run办法 this.run() } else { // 触发dom更新会执行此办法, 以队列形式执行update更新 queueWatcher(this) }}
run
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) } } }}
生成渲染Watcher
在查找编译入口那局部讲到了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函数。
mountComponent
定义在core/instance/lifecycle.js
文件中,该函数次要执行三块内容:
- 触发
beforeMount
,beforeUpdate
和mounted
生命周期钩子函数。 - 定义
updateComponent
办法。 - 生成Watcher实例,传入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
_update, _render是Vue的实例办法, _render办法用于依据用户定义的render或者模板生成的render生成虚构Dom。_update办法依据传入的虚构Dom,执行patch,进行Dom比照更新。
总结
至此,响应式解决的整个闭环脉络曾经摸清。