<p>分析vue是如何实现数据响应的.</p> <h2>前记</h2> <p>如今回顾一下看数据响应的缘由. 以前看了vuex和vue-i18n的源码, 他们都有本身内部的vm, 也就是vue实例. 使用的都是vue的响应式数据特性及<code>$watch</code>api. 因此决定看一下vue的源码, 了解vue是如何实现响应式数据.</p> <p>本文叙事方式为树藤摸瓜, 顺着看源码的逻辑走一遍, 查看的vue的版本为2.5.2.</p> <h2>目的</h2> <p>明确调查方向才能直至目标, 先说一下目标行为:</p> <ol> <li>vue中的数据改变, 视图层面就能得到到通知并进行渲染.</li> <li> <code>$watch</code>api监听表达式的值, 在表达式中任何一个元素变化之后得到通知并执行回调.</li> </ol> <p>那么准备开始以这个方向为目标从vue源码的入口开始找答案.</p> <h2>入口开始</h2> <p>来到<code>src/core/index.js</code>, 调了<code>initGlobalAPI()</code>, 其余代码是ssr相关, 暂不关心.</p> <p>进入<code>initGlobalAPI</code>方法, 作了一些暴露全局属性和方法的事情, 最后有4个init, initUse是Vue的install方法, 前面vuex和vue-i18n的源码分析已经分析过了. initMixin是咱们要深刻的部分.</p> <p>在<code>initMixin</code>前面部分依旧作了一些变量的处理, 具体的init动做为:</p>html
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')
<p>vue启动的顺序已经看到了: 加载生命周期/时间/渲染的方法 => beforeCreate钩子 => 调用injection => 初始化state => 调用provide => created钩子.</p> <p>injection和provide都是比较新的api, 我还没用过. 咱们要研究的东西在initState中.</p> <p>来到initState:</p>vue
export function initState (vm: Component) { vm._watchers = [] const opts = vm.$options if (opts.props) initProps(vm, opts.props) if (opts.methods) initMethods(vm, opts.methods) if (opts.data) { initData(vm) } else { observe(vm._data = {}, true /* asRootData */) // 若是没有data, _data效果同样, 只是没作代理 } if (opts.computed) initComputed(vm, opts.computed) if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) } }
<p>作的事情很简单: 若是有props就处理props, 有methods就处理methods, …, 咱们直接看<code>initData(vm)</code>.</p> <h2>initData</h2> <p>initData作了两件事: proxy, observe.</p> <p>先贴代码, 前面作了小的事情写在注释里了.</p>react
function initData (vm: Component) { let data = vm.$options.data data = vm._data = typeof data === 'function' // 若是data是函数, 用vm做为this执行函数的结果做为data ? getData(data, vm) : data || {} if (!isPlainObject(data)) { // 过滤乱搞, data只接受对象, 若是乱搞会报警而且把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 const methods = vm.$options.methods let i = keys.length while (i--) { // 遍历data const key = keys[i] if (process.env.NODE_ENV !== 'production') { if (methods && hasOwn(methods, key)) { // 判断是否和methods重名 warn( `Method "${key}" has already been defined as a data property.`, vm ) } } if (props && hasOwn(props, key)) { // 判断是否和props重名 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)) { // 判断key是否以_或$开头 proxy(vm, `_data`, key) // 代理data } } // observe data observe(data, true /* asRootData */) }
<p>咱们来看一下proxy和observe是干吗的.</p> <p>proxy的参数: vue实例, <code>_data</code>, 键.</p> <p>做用: 把vm.key的setter和getter都代理到vm._data.key, 效果就是vm.a实际实际是vm._data.a, 设置vm.a也是设置vm._data.a.</p> <p>代码是:</p>vuex
const sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop } export function proxy (target: Object, sourceKey: string, key: string) { // 在initData中调用: proxy(vm, `_data`, key) // target: vm, sourceKey: _data, key: key. 这里的key为遍历data的key // 举例: data为{a: 'a value', b: 'b value'} // 那么这里执行的target: vm, sourceKey: _data, key: a sharedPropertyDefinition.get = function proxyGetter () { return this[sourceKey][key] // getter: vm._data.a } sharedPropertyDefinition.set = function proxySetter (val) { this[sourceKey][key] = val // setter: vm._data.a = val } Object.defineProperty(target, key, sharedPropertyDefinition) // 用Object.defineProperty来设置getter, setter // 第一个参数是vm, 也就是获取`vm.a`就获取到了`vm._data.a`, 设置也是如此. }
<p>代理完成以后是本文的核心, initData最后调用了<code>observe(data, true)</code>,来实现数据的响应.</p> <h2>observe</h2> <p>observe方法实际上是一个滤空和单例的入口, 最后行为是建立一个observe对象放到observe目标的<code>__ob__</code>属性里, 代码以下:</p>express
/** * Attempt to create an observer instance for a value, * returns the new observer if successfully observed, * or the existing observer if the value already has one. */ export function observe (value: any, asRootData: ?boolean): Observer | void { if (!isObject(value) || value instanceof VNode) { // 只能是监察对象, 过滤非法参数 return } let ob: Observer | void if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__ // 若是已被监察过, 返回存在的监察对象 } else if ( // 符合下面条件就新建一个监察对象, 若是不符合就返回undefined observerState.shouldConvert && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { ob = new Observer(value) } if (asRootData && ob) { ob.vmCount++ } return ob }
<p>那么关键是<code>new Observer(value)</code>了, 赶忙跳到Observe这个类看看是如何构造的.</p> <p>如下是Observer的构造函数:</p>segmentfault
constructor (value: any) { this.value = value // 保存值 this.dep = new Dep() // dep对象 this.vmCount = 0 def(value, '__ob__', this) // 本身的副本, 放到__ob__属性下, 做为单例依据的缓存 if (Array.isArray(value)) { // 判断是否为数组, 若是是数组的话劫持一些数组的方法, 在调用这些方法的时候进行通知. const augment = hasProto ? protoAugment : copyAugment augment(value, arrayMethods, arrayKeys) this.observeArray(value) // 遍历数组, 继续监察数组的每一个元素 } else { this.walk(value) // 直到再也不是数组(是对象了), 遍历对象, 劫持每一个对象来发出通知 } }
<p>作了几件事:</p> <ul> <li>创建内部Dep对象. (做用是以后在watcher中递归的时候把本身添加到依赖中)</li> <li>把目标的<code>__ob__</code>属性赋值成Observe对象, 做用是上面提过的单例.</li> <li>若是目标是数组, 进行方法的劫持. (下面来看)</li> <li>若是是数组就observeArray, 不然walk.</li> </ul> <p>那么咱们来看看observeArray和walk方法.</p>api
/** * Walk through each property and convert them into * getter/setters. This method should only be called when * value type is Object. */ walk (obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i], obj[keys[i]]) // 用'obj[keys[i]]'这种方式是为了在函数中直接给这个赋值就好了 } } /** * Observe a list of Array items. */ observeArray (items: Array<any>) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } }
<p>咱们发现, observeArray的做用是递归调用, 最后调用的方法是<strong><code>defineReactive</code></strong>, 能够说这个方法是最终的核心了.</p> <p>下面咱们先看一下数组方法劫持的目的和方法, 以后再看<code>defineReactive</code>的作法.</p> <h2>array劫持</h2> <p>以后会知道defineReactive的实现劫持的方法是<code>Object.defineProperty</code>来劫持对象的getter, setter, 那么数组的变化不会触发这些劫持器, 因此vue劫持了数组的一些方法, 代码比较零散就不贴了. </p> <p>最后的结果就是: array.prototype.push = function () {…}, 被劫持的方法有<code>['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']</code>, 也就是调用这些方法也会触发响应. 具体劫持之后的方法是:</p>数组
def(arrayMethods, method, function mutator (...args) { const result = original.apply(this, args) // 调用原生的数组方法 const ob = this.__ob__ // 获取observe对象 let inserted switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } if (inserted) ob.observeArray(inserted) // 继续递归 // notify change ob.dep.notify() // 出发notify return result })
<p>作了两件事:</p> <ol> <li>递归调用</li> <li>触发所属Dep的<code>notify()</code>方法.</li> </ol> <p>接下来就说最终的核心方法, defineReactive, 这个方法最后也调用了notify().</p> <h2>defineReactive</h2> <p>这里先贴整个代码:</p>缓存
/** * Define a reactive property on an Object. */ export function defineReactive ( // 这个方法是劫持对象key的动做 // 这里仍是举例: 对象为 {a: 'value a', b: 'value b'}, 当前遍历到a obj: Object, // {a: 'value a', b: 'value b'} key: string, // a val: any, // value a customSetter?: ?Function, shallow?: boolean ) { const dep = new Dep() const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { // 判断当前key的操做权限 return } // cater for pre-defined getter/setters // 获取对象原本的getter setter const getter = property && property.get const setter = property && property.set let childOb = !shallow && observe(val) // childOb是val的监察对象(就是new Observe(val), 也就是递归调用) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val // 若是自己有getter, 先调用 if (Dep.target) { // 若是有dep.target, 进行一些处理, 最后返回value, if里的代码咱们以后去dep的代码中研究 dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }, set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val // 若是自己有getter, 先调用 /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { // 若是值不变就不去作通知了, (或是某个值为Nan?) return } /* eslint-enable no-self-compare */ if (process.env.NODE_ENV !== 'production' && customSetter) { customSetter() // 根据"生产环境不执行"这个行为来看, 这个方法可能做用是log, 多是保留方法, 还没地方用? } if (setter) { // 若是自己有setter, 先调用, 没的话就直接赋值 setter.call(obj, newVal) } else { val = newVal // 由于传入参数的时候实际上是'obj[keys[i]]', 因此就等因而'obj[key] = newVal'了 } childOb = !shallow && observe(newVal) // 从新创建子监察 dep.notify() // 通知, 能够说是劫持的核心步骤 } }) }
<p>解释都在注释中了, 总结一下这个方法的作的几件重要的事:</p> <ul> <li>创建Dep对象. (下面会说调用的Dep的方法的具体做用)</li> <li>递归调用. 能够说很大部分代码都在递归调用, 分别在建立子observe对象, setter, getter中.</li> <li>getter中: 调用原来的getter, 收集依赖(Dep.depend(), 以后会解释收集的原理), 一样也是递归收集.</li> <li>setter中: 调用原来的setter, 并判断是否须要通知, 最后调用<code>dep.notify()</code>.</li> </ul> <p>总结一下, 总的来讲就是, 进入传入的data数据会被劫持, 在get的时候调用<code>Dep.depend()</code>, 在set的时候调用<code>Dep.notify()</code>. 那么Dep是什么, 这两个方法又干了什么, 带着疑问去看Dep对象.</p> <h2>Dep</h2> <p>Dep应该是dependencies的意思. dep.js整个文件只有62行, 因此贴一下:</p>app
/** * A dep is an observable that can have multiple * directives subscribing to it. */ export default class Dep { static target: ?Watcher; 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) } depend () { if (Dep.target) { Dep.target.addDep(this) } } notify () { // stabilize the subscriber list first const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } } // the current target watcher being evaluated. // this is globally unique because there could be only one // watcher being evaluated at any time. // 这是一个队列, 由于不容许有多个watcher的get方法同时调用 Dep.target = null const targetStack = [] export function pushTarget (_target: Watcher) { // 设置target, 把旧的放进stack if (Dep.target) targetStack.push(Dep.target) Dep.target = _target } export function popTarget () { // 从stack拿一个做为当前的 Dep.target = targetStack.pop() }
<p>首先来分析变量:</p> <ul> <li>全局Target. 这个实际上是用来跟watcher交互的, 也保证了普通get的时候没有target就不设置依赖, 后面会解释.</li> <li>id. 这是用来在watcher里依赖去重的, 也要到后面解释.</li> <li>subs: 是一个watcher数组. sub应该是subscribe的意思, 也就是当前dep(依赖)的订阅者列表.</li> </ul> <p>再来看方法:</p> <ul> <li>构造: 设uid, subs. addSub: 添加wathcer, removeSub: 移除watcher. 这3个好无聊.</li> <li>depend: 若是有Dep.target, 就把本身添加到Dep.target中(调用了<code>Dep.target.addDep(this)</code>).<p>那么何时有Dep.target呢, 就由<code>pushTarget()</code>和<code>popTarget()</code>来操做了, 这些方法在Dep中没有调用, 后面会分析是谁在操做Dep.target.(这个是重点)</p> </li> <li>notify: 这个是setter劫持之后调用的最终方法, 作了什么: 把当前Dep订阅中的每一个watcher都调用<code>update()</code>方法.</li> </ul> <p>Dep看完了, 咱们的疑问都转向了Watcher对象了. 如今看来有点糊涂, 看完Watcher就都明白了.</p> <h2>Watcher</h2> <p>watcher很是大(并且打watcher这个单词也很是容易手误, 心烦), 咱们先从构造看起:</p>
constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: Object ) { this.vm = vm // 保存vm vm._watchers.push(this) // 把watcher存到vm里 // options // 读取配置 或 设置默认值 if (options) { this.deep = !!options.deep this.user = !!options.user this.lazy = !!options.lazy this.sync = !!options.sync } else { this.deep = this.user = this.lazy = this.sync = false } this.cb = cb this.id = ++uid // uid for batching this.active = true this.dirty = this.lazy // for lazy watchers this.deps = [] this.newDeps = [] this.depIds = new Set() this.newDepIds = new Set() this.expression = process.env.NODE_ENV !== 'production' // 非生产环境就记录expOrFn ? expOrFn.toString() : '' // parse expression for getter // 设置getter, parse字符串, 并滤空滤错 if (typeof expOrFn === 'function') { this.getter = expOrFn } else { this.getter = parsePath(expOrFn) if (!this.getter) { this.getter = function () {} process.env.NODE_ENV !== 'production' && warn( `Failed watching path: "${expOrFn}" ` + 'Watcher only accepts simple dot-delimited paths. ' + 'For full control, use a function instead.', vm ) } } // 调用get得到值 this.value = this.lazy ? undefined : this.get() }
<p>注释都写了, 我来高度总结一下构造器作了什么事:</p> <ul> <li>处理传入的参数并设置成本身的属性.</li> <li>parse表达式. watcher表达式接受2种: 方法/字符串. 若是是方法就设为getter, 若是是字符串会进行处理:</li> </ul>
/** * Parse simple path. */ const bailRE = /[^\w.$]/ export function parsePath (path: string): any { if (bailRE.test(path)) { return } const segments = path.split('.') // 这里是vue如何分析watch的, 就是接受 '.' 分隔的变量. // 若是键是'a.b.c', 也就等于function () {return this.a.b.c} return function (obj) { for (let i = 0; i < segments.length; i++) { if (!obj) return obj = obj[segments[i]] } return obj } }
<p>处理的效果写在上面代码的注释里.</p> <ul><li>调用<code>get()</code>方法.</li></ul> <p>下面说一下get方法. <strong>get()方法是核心, 看完了就能把以前的碎片都串起来了</strong>. 贴get()的代码:</p>
/** * Evaluate the getter, and re-collect dependencies. */ get () { pushTarget(this) // 进入队列, 把当前watcher设置为Dep.target // 这样下面调用getter的时候出发的dep.append() (最后调用Dep.target.addDep()) 就会调用这个watcher的addDep. let value const vm = this.vm try { value = this.getter.call(vm, vm) // 调用getter的时候会走一遍表达式, // 若是是 this.a + this.b , 会在a和b的getter中调用Dep.target.addDep(), 最后结果就调用了当前watcher的addDep, // 当前watcher就有了this.a的dep和this.b的dep // addDep把当前watcher加入了dep的sub(subscribe)里, dep的notify()调用就会运行本watcher的run()方法. } catch (e) { if (this.user) { handleError(e, vm, `getter for watcher "${this.expression}"`) } else { throw e } } finally { // "touch" every property so they are all tracked as // dependencies for deep watching // 走到这里已经经过了getter得到到了value, 或者失败为undefined, 这个值返回做为watcher的valule // 处理deep选项 (待看) if (this.deep) { traverse(value) } popTarget() // 移除队列 this.cleanupDeps() // 清理依赖(addDep加到newDep数组, 这步作整理动做) } return value }
<p>注释都在代码中了, 这段理解了就对整个响应系统理解了. </p> <p>我来总结一下: (核心, 很是重要)</p> <ul> <li><strong>dep方面: 传入vue参数的data(实际是全部调用<code>defineReactive</code>的属性)都会产生本身的Dep对象.</strong></li> <li><strong>Watcher方面: 在全部new Watcher的地方产生Watcher对象.</strong></li> <li> <strong>dep与Watcher关系: Watcher的get方法创建了双方关系:</strong><p><strong>把本身设为target, 运行watcher的表达式(即调用相关数据的getter), 由于getter有钩子, 调用了Watcher的addDep, addDep方法把dep和Watcher互相推入互相的属性数组(分别是deps和subs)</strong></p> </li> <li><strong>dep与Watcher创建了多对多的关系: dep含有订阅的watcher的数组, watcher含有所依赖的变量的数组</strong></li> <li> <strong>当dep的数据调动setter, 调用notify, 最终调用Watcher的update方法</strong>.</li> <li><strong>前面提到dep与Watcher创建关系是经过<code>get()</code>方法, 这个方法在3个地方出现: 构造方法, run方法, evaluate方法. 也就是说, notify了之后会从新调用一次get()方法. (因此在lifycycle中调用的时候把依赖和触发方法都写到getter方法中了). </strong></li> </ul> <p>那么接下来要看一看watcher在什么地方调用的.</p> <p>找了一下, 一共三处:</p> <ul> <li> <p>initComputed的时候: (state.js)</p>
watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions )
</li> <li> <p>$watch api: (state.js)</p>
new Watcher(vm, expOrFn, cb, options)
</li> <li> <p>lifecycle的mount阶段: (lifecycle.js)</p>
new Watcher(vm, updateComponent, noop)
</li> </ul> <h2>总结</h2> <p>看完源码就不神秘了, 写得也算很清楚了. 固然还有不少细节没写, 由于冲着目标来.</p> <p>总结其实都在上一节的粗体里了.</p> <h2>甜点</h2> <p>咱们只从data看了, 那么props和computed应该也是这样的, 由于props应该与组建相关, 下回分解吧, 咱们来看看computed是咋回事吧.</p>
const computedWatcherOptions = { lazy: true } function initComputed (vm: Component, computed: Object) { const watchers = vm._computedWatchers = Object.create(null) // computed properties are just getters during SSR const isSSR = isServerRendering() for (const key in computed) { // 循环每一个computed // ------------ // 格式滤错滤空 const userDef = computed[key] const getter = typeof userDef === 'function' ? userDef : userDef.get if (process.env.NODE_ENV !== 'production' && getter == null) { warn( `Getter is missing for computed property "${key}".`, vm ) } if (!isSSR) { // create internal watcher for the computed property. // 为computed创建wathcer watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions ) } // component-defined computed properties are already defined on the // component prototype. We only need to define computed properties defined // at instantiation here. // 由于没有被代理, computed属性是不能经过vm.xx得到的, 若是能够得到说明重复定义, 抛出异常. if (!(key in vm)) { defineComputed(vm, key, userDef) } else if (process.env.NODE_ENV !== 'production') { if (key in vm.$data) { warn(`The computed property "${key}" is already defined in data.`, vm) } else if (vm.$options.props && key in vm.$options.props) { warn(`The computed property "${key}" is already defined as a prop.`, vm) } } } }
<p>已注释, 总结为:</p> <ul> <li>遍历每一个computed键值, 过滤错误语法.</li> <li>遍历每一个computed键值, 为他们创建watcher, options为<code>{ lazy: true}</code>.</li> <li>遍历每一个computed键值, 调用defineComputed.</li> </ul> <p>那么继续看defineComputed.</p>
export function defineComputed ( target: any, key: string, userDef: Object | Function ) { const shouldCache = !isServerRendering() // 由于computed除了function还有get set 字段的语法, 下面的代码是作api的兼容 if (typeof userDef === 'function') { sharedPropertyDefinition.get = shouldCache ? createComputedGetter(key) : userDef sharedPropertyDefinition.set = noop } else { sharedPropertyDefinition.get = userDef.get ? shouldCache && userDef.cache !== false ? createComputedGetter(key) : userDef.get : noop sharedPropertyDefinition.set = userDef.set ? userDef.set : noop } // 除非设置setter, computed属性是不能被修改的, 抛出异常 (evan说改变了自由哲学, 要控制低级用户) if (process.env.NODE_ENV !== 'production' && sharedPropertyDefinition.set === noop) { sharedPropertyDefinition.set = function () { warn( `Computed property "${key}" was assigned to but it has no setter.`, this ) } } // 其实核心就下面这步... 上面步骤的做用是和data同样添加一个getter, 增长append动做. 如今经过vm.xxx能够获取到computed属性啦! Object.defineProperty(target, key, sharedPropertyDefinition) } function createComputedGetter (key) { return function computedGetter () { const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { if (watcher.dirty) { watcher.evaluate() } if (Dep.target) { watcher.depend() } return watcher.value } } }
<p>由于computed能够设置getter, setter, 因此computed的值不必定是function, 能够为set和get的function, 很大部分代码是作这些处理, 核心的事情有2件:</p> <ul> <li>使用Object.defineProperty在vm上挂载computed属性.</li> <li>为属性设置getter, getter作了和data同样的事: depend. 可是多了一步: <code>watcher.evalueate()</code>.</li> </ul> <p>看到这里, computed注册核心一共作了两件事:</p> <ol> <li>为每一个computed创建watcher(lazy: true)</li> <li>创建一个getter来depend, 并挂到vm上.</li> </ol> <p>那么dirty成了疑问, 咱们回到watcher的代码中去看, lazy和dirty和evaluate是干什么的.</p> <p>精选相关代码:</p> <ul> <li>(构造函数中) <code>this.dirty = this.lazy</code> </li> <li>(构造函数中) <code>this.value = this.lazy ? undefined : this.get()</code> </li> <li> <p>(evaluate函数)</p>
evaluate () { this.value = this.get() this.dirty = false }
</li> </ul> <p>到这里已经很清楚了. 由于还没设置getter, 因此在创建watcher的时候不当即调用getter, 因此构造函数没有立刻调用get, 在设置好getter之后调用evaluate来进行依赖注册.</p> <p>总结: computed是watch+把属性挂到vm上的行为组合.</p>