首先欢迎你们关注个人Github博客,也算是对个人一点鼓励,毕竟写东西无法得到变现,能坚持下去也是靠的是本身的热情和你们的鼓励。接下来的日子我应该会着力写一系列关于Vue与React内部原理的文章,感兴趣的同窗点个关注或者Star。
以前的两篇文章响应式数据与数据依赖基本原理和从Vue数组响应化所引起的思考咱们介绍了响应式数据相关的内容,没有看的同窗能够点击上面的连接了解一下。若是你们都阅读过上面两篇文章的话,确定对这方面内容有了足够的知识储备,想来是时候来看看Vue内部是如何实现数据响应化。目前Vue的代码很是庞大,但其中包含了例如:服务器渲染等咱们不关心的内容,为了能集中于咱们想学习的部分,咱们此次阅读的是Vue的早期代码,你们能够checkout
到这里查看对应的代码。
以前零零碎碎的看过React的部分源码,当我看到Vue的源码,以为真的是很是优秀,各个模块之间解耦的很是好,可读性也很高。Vue响应式数据是在Observer
模块中实现的,咱们能够看看Observer
是如何实现的。
javascript
若是看过上两篇文章的同窗应该会发现一个问题:数据响应化的代码与其余的代码耦合太强了,好比说:
前端
//代码来源于文章:响应式数据与数据依赖基本原理 //定义对象的单个响应式属性 function defineReactive(obj, key, value){ observify(value); Object.defineProperty(obj, key, { configurable: true, enumerable: true, set: function(newValue){ var oldValue = value; value = newValue; //能够在修改数据时触发其余的操做 console.log("newValue: ", newValue, " oldValue: ", oldValue); }, get: function(){ return value; } }); }
好比上面的代码,set
内部的处理的代码就与整个数据响应化相耦合,若是下次咱们想要在set
中作其余的操做,就必需要修改set
函数内部的内容,这是很是不友好的,不符合开闭原则(OCP: Open Close Principle)。固然Vue不会采用这种方式去设计,为了解决这个问题,Vue引入了发布-订阅模式。其实发布-订阅模式是前端工程师很是熟悉的一种模式,又叫作观察者模式,它是一种定义对象间一种一对多的依赖关系,当一个对象的状态发生改变的时候,其余观察它的对象都会获得通知。咱们最多见的DOM事件就是一种发布-订阅模式。好比:
vue
document.body.addEventListener("click", function(){ console.log("click event"); });
在上面的代码中咱们监听了body
的click
事件,虽然咱们不知道click
事件何时会发生,可是咱们必定能保证,若是发生了body
的click
事件,咱们必定能获得通知,即回调函数被调用。在JavaScript中由于函数是一等公民,咱们不多使用传统的发布-订阅模式,多采用的是事件模型的方式实现。在Vue中也实现了一个事件模型,咱们能够看一下。由于Vue的模块之间解耦的很是好,所以在看代码以前,其实咱们能够先来看看对应的单元测试文件,你就知道这个模块要实现什么功能,甚至若是你愿意的话,也能够本身实现一个相似的模块放进Vue的源码中运行。java
Vue早期代码使用是jasmine
进行单元测试,emitter_spec.js
是事件模型的单元测试文件。首先简单介绍一下jasmine
用到的函数,能够对照下面的代码了解具体的功能:git
describe
是一个测试单元集合it
是一个测试用例beforeEach
会在每个测试用例it
执行前执行expect
指望函数,用做对指望值和实际值之间执行逻辑比较createSpy
用来建立spy,而spy的做用是监测函数的调用相关信息和函数执行参数
var Emitter = require('../../../src/emitter') var u = undefined // 代码有删减 describe('Emitter', function () { var e, spy beforeEach(function () { e = new Emitter() spy = jasmine.createSpy('emitter') }) it('on', function () { e.on('test', spy) e.emit('test', 1, 2 ,3) expect(spy.calls.count()).toBe(1) expect(spy).toHaveBeenCalledWith(1, 2, 3) }) it('once', function () { e.once('test', spy) e.emit('test', 1, 2 ,3) e.emit('test', 2, 3, 4) expect(spy.calls.count()).toBe(1) expect(spy).toHaveBeenCalledWith(1, 2, 3) }) it('off', function () { e.on('test1', spy) e.on('test2', spy) e.off() e.emit('test1') e.emit('test2') expect(spy.calls.count()).toBe(0) }) it('apply emit', function () { e.on('test', spy) e.applyEmit('test', 1) e.applyEmit('test', 1, 2, 3, 4, 5) expect(spy).toHaveBeenCalledWith(1) expect(spy).toHaveBeenCalledWith(1, 2, 3, 4, 5) }) })
能够看出Emitter
对象实例对外提供如下接口:github
on
: 注册监听接口,参数分别是事件名和监听函数 emit
: 触发事件函数,参数是事件名 off
: 取消对应事件的注册函数,参数分别是事件名和监听函数 once
: 与on
相似,仅会在第一次时通知监听函数,随后监听函数会被移除。看完了上面的单元测试代码,咱们如今已经基本了解了这个模块要干什么,如今让咱们看看对应的代码:数组
// 删去了注释而且对代码顺序有调整 // ctx是监听回调函数的执行做用域(this) function Emitter (ctx) { this._ctx = ctx || this } var p = Emitter.prototype p.on = function (event, fn) { this._cbs = this._cbs || {} ;(this._cbs[event] || (this._cbs[event] = [])) .push(fn) return this } // 三种模式 // 不传参状况清空全部监听函数 // 仅传事件名则清除该事件的全部监听函数 // 传递事件名和回调函数,则对应仅删除对应的监听事件 p.off = function (event, fn) { this._cbs = this._cbs || {} // all if (!arguments.length) { this._cbs = {} return this } // specific event var callbacks = this._cbs[event] if (!callbacks) return this // remove all handlers if (arguments.length === 1) { delete this._cbs[event] return this } // remove specific handler var cb for (var i = 0; i < callbacks.length; i++) { cb = callbacks[i] // 这边的代码之因此会有cb.fn === fn要结合once函数去看 // 给once传递的监听函数其实已经被wrapped过 // 可是仍然能够经过原来的监听函数去off掉 if (cb === fn || cb.fn === fn) { callbacks.splice(i, 1) break } } return this } // 触发对应事件的全部监听函数,注意最多只能用给监听函数传递三个参数(采用call) p.emit = function (event, a, b, c) { this._cbs = this._cbs || {} var callbacks = this._cbs[event] if (callbacks) { callbacks = callbacks.slice(0) for (var i = 0, len = callbacks.length; i < len; i++) { callbacks[i].call(this._ctx, a, b, c) } } return this } // 触发对应事件的全部监听函数,传递参数个数不受限制(采用apply) p.applyEmit = function (event) { this._cbs = this._cbs || {} var callbacks = this._cbs[event], args if (callbacks) { callbacks = callbacks.slice(0) args = callbacks.slice.call(arguments, 1) for (var i = 0, len = callbacks.length; i < len; i++) { callbacks[i].apply(this._ctx, args) } } return this } // 经过调用on与off事件事件,在第一次触发以后就`off`对应的监听事件 p.once = function (event, fn) { var self = this this._cbs = this._cbs || {} function on () { self.off(event, on) fn.apply(this, arguments) } on.fn = fn this.on(event, on) return this }
咱们能够看到上面的代码采用了原型模式建立了一个Emitter
类。配合Karma跑一下这个模块 ,测试用例所有经过,到如今咱们已经阅读完Emitter
了,这算是一个小小的热身吧,接下来让咱们正式看一下Observer
模块。
浏览器
按照上面的思路咱们先看看Observer
对应的测试用例observer_spec.js
,因为Observer
的测试用例很是长,我会在代码注释中作解释,并尽可能精简测试用例,能让咱们了解模块对应功能便可,但愿你能有耐心阅读下来。
服务器
//测试用例是精简版,不然太冗长 var Observer = require('../../../src/observe/observer') var _ = require('../../../src/util') //Vue内部使用工具方法 var u = undefined Observer.pathDelimiter = '.' //配置Observer路径分隔符 describe('Observer', function () { var spy beforeEach(function () { spy = jasmine.createSpy('observer') }) //咱们能够看到咱们经过Observer.create函数能够将数据变为可响应化, //而后咱们监听get事件能够在属性被读取时触发对应事件,注意对象嵌套的状况(例如b.c) it('get', function () { Observer.emitGet = true var obj = { a: 1, b: { c: 2 } } var ob = Observer.create(obj) ob.on('get', spy) var t = obj.b.c expect(spy).toHaveBeenCalledWith('b', u, u) expect(spy).toHaveBeenCalledWith('b.c', u, u) Observer.emitGet = false }) //咱们能够监听响应式数据的set事件,当响应式数据修改的时候,会触发对应的时间 it('set', function () { var obj = { a: 1, b: { c: 2 } } var ob = Observer.create(obj) ob.on('set', spy) obj.b.c = 4 expect(spy).toHaveBeenCalledWith('b.c', 4, u) }) //带有$与_开头的属性都不会被处理 it('ignore prefix', function () { var obj = { _test: 123, $test: 234 } var ob = Observer.create(obj) ob.on('set', spy) obj._test = 234 obj.$test = 345 expect(spy.calls.count()).toBe(0) }) //访问器属性也不会被处理 it('ignore accessors', function () { var obj = { a: 123, get b () { return this.a } } var ob = Observer.create(obj) obj.a = 234 expect(obj.b).toBe(234) }) // 对数属性的get监听,注意嵌套的状况 it('array get', function () { Observer.emitGet = true var obj = { arr: [{a:1}, {a:2}] } var ob = Observer.create(obj) ob.on('get', spy) var t = obj.arr[0].a expect(spy).toHaveBeenCalledWith('arr', u, u) expect(spy).toHaveBeenCalledWith('arr.0.a', u, u) expect(spy.calls.count()).toBe(2) Observer.emitGet = false }) // 对数属性的get监听,注意嵌套的状况 it('array set', function () { var obj = { arr: [{a:1}, {a:2}] } var ob = Observer.create(obj) ob.on('set', spy) obj.arr[0].a = 2 expect(spy).toHaveBeenCalledWith('arr.0.a', 2, u) }) // 咱们看到能够经过监听mutate事件,在push调用的时候对应触发事件 // 触发事件第一个参数是"",表明的是路径名,具体源码能够看出,对于数组变异方法都是空字符串 // 触发事件第二个参数是数组自己 // 触发事件第三个参数比较复杂,其中: // method属性: 表明触发的方法名称 // args属性: 表明触发方法传递参数 // result属性: 表明触发变异方法以后数组的结果 // index属性: 表明变异方法对数组发生变化的最开始元素 // inserted属性: 表明数组新增的元素 // remove属性: 表明数组删除的元素 // 其余的变异方法: pop、shift、unshift、splice、sort、reverse内容都是很是类似的 // 具体咱们就不一一列举的了,若是有疑问能够本身看到所有的单元测试代码 it('array push', function () { var arr = [{a:1}, {a:2}] var ob = Observer.create(arr) ob.on('mutate', spy) arr.push({a:3}) expect(spy.calls.mostRecent().args[0]).toBe('') expect(spy.calls.mostRecent().args[1]).toBe(arr) var mutation = spy.calls.mostRecent().args[2] expect(mutation).toBeDefined() expect(mutation.method).toBe('push') expect(mutation.index).toBe(2) expect(mutation.removed.length).toBe(0) expect(mutation.inserted.length).toBe(1) expect(mutation.inserted[0]).toBe(arr[2]) }) // 咱们能够看到响应式数据中存在$add方法,相似于Vue.set,能够监听add事件 // 能够向响应式对象中添加新一个属性,若是以前存在该属性则操做会被忽略 // 而且新赋值的对象也必须被响应化 // 咱们省略了对象数据$delete方法的单元测试,功能相似于Vue.delete,与$add方法相反,能够用于删除对象的属性 // 咱们省略了数组的$set方法的单元测试,功能也相似与Vue.set,能够用于设置数组对应数字下标的值 // 咱们省略了数组的$remove方法的单元测试,功能用于移除数组给定下标的值或者给定的值,例如: // var arr = [{a:1}, {a:2}] // var ob = Observer.create(arr) // arr.$remove(0) => 移除对应下标的值 或者 // arr.$remove(arr[0]) => 移除给定的值 it('object.$add', function () { var obj = {a:{b:1}} var ob = Observer.create(obj) ob.on('add', spy) // ignore existing keys obj.$add('a', 123) expect(spy.calls.count()).toBe(0) // add event var add = {d:2} obj.a.$add('c', add) expect(spy).toHaveBeenCalledWith('a.c', add, u) // check if add object is properly observed ob.on('set', spy) obj.a.c.d = 3 expect(spy).toHaveBeenCalledWith('a.c.d', 3, u) }) // 下面的测试用例用来表示若是两个不一样对象parentA、parentB的属性指向同一个对象obj,那么该对象obj改变时会分别parentA与parentB的监听事件 it('shared observe', function () { var obj = { a: 1 } var parentA = { child1: obj } var parentB = { child2: obj } var obA = Observer.create(parentA) var obB = Observer.create(parentB) obA.on('set', spy) obB.on('set', spy) obj.a = 2 expect(spy.calls.count()).toBe(2) expect(spy).toHaveBeenCalledWith('child1.a', 2, u) expect(spy).toHaveBeenCalledWith('child2.a', 2, u) // test unobserve parentA.child1 = null obj.a = 3 expect(spy.calls.count()).toBe(4) expect(spy).toHaveBeenCalledWith('child1', null, u) expect(spy).toHaveBeenCalledWith('child2.a', 3, u) }) })
能坚持看到这里,咱们的长征路就走过了一半了,咱们已经知道了Oberver
对外提供的功能了,如今咱们就来了解一下Oberver
内部的实现原理。
Oberver
模块实际上采用采用组合继承(借用构造函数+原型继承)方式继承了Emitter
,其目的就是继承Emitter
的on
, off
,emit
等方法。咱们在上面的测试用例发现,咱们并无用new
方法直接建立一个Oberver
的对象实例,而是采用一个工厂方法Oberver.create
方法来建立的,咱们接下来看源码,因为代码比较多我会尽可能去拆分红一个个小块来说:
前端工程师
// 代码出自于observe.js // 为了方便讲解我对代码顺序作了改变,要了解详细的状况能够查看具体的源码 var _ = require('../util') var Emitter = require('../emitter') var arrayAugmentations = require('./array-augmentations') var objectAugmentations = require('./object-augmentations') var uid = 0 /** * Type enums */ var ARRAY = 0 var OBJECT = 1 function Observer (value, type, options) { Emitter.call(this, options && options.callbackContext) this.id = ++uid this.value = value this.type = type this.parents = null if (value) { _.define(value, '$observer', this) if (type === ARRAY) { _.augment(value, arrayAugmentations) this.link(value) } else if (type === OBJECT) { if (options && options.doNotAlterProto) { _.deepMixin(value, objectAugmentations) } else { _.augment(value, objectAugmentations) } this.walk(value) } } } var p = Observer.prototype = Object.create(Emitter.prototype) Observer.pathDelimiter = '\b' Observer.emitGet = false Observer.create = function (value, options) { if (value && value.hasOwnProperty('$observer') && value.$observer instanceof Observer) { return value.$observer } if (_.isArray(value)) { return new Observer(value, ARRAY, options) } else if ( _.isObject(value) && !value.$scope // avoid Vue instance ) { return new Observer(value, OBJECT, options) } }
咱们首先从Observer.create
看起,若是value
值没有响应化过(经过是否含有$observer
属性去判断),则使用new操做符建立Obsever实例(区分对象OBJECT与数组ARRAY)。接下来咱们看Observer
的构造函数是怎么定义的,首先借用Emitter
构造函数:
Emitter.call(this, options && options.callbackContext)
配合原型继承
var p = Observer.prototype = Object.create(Emitter.prototype)
从而实现了组合继承Emitter
,所以Observer
继承了Emitter
的属性(ctx
)和方法(on
,emit
等)。咱们能够看到Observer
有如下属性:
id
: 响应式数据的惟一标识value
: 原始数据type
: 标识是数组仍是对象parents
: 标识响应式数据的父级,可能存在多个,好比var obj = { a : { b: 1}}
,在处理{b: 1}
的响应化过程当中parents
中某个属性指向的就是obj
的$observer
。 咱们接着看首先给该数据赋值$observer
属性,指向的是实例对象自己。_.define
内部是经过defineProperty
实现的:
define = function (obj, key, val, enumerable) { Object.defineProperty(obj, key, { value : val, enumerable : !!enumerable, writable : true, configurable : true }) }
下面咱们首先看看是怎么处理数组类型的数据的
if (type === ARRAY) { _.augment(value, arrayAugmentations) this.link(value) }
若是看过我前两篇文章的同窗,其实还记得咱们对数组响应化当时还作了一个着重的原理讲解,大概原理就是咱们经过给数组对象设置新的原型对象,从而遮蔽掉原生数组的变异方法,大概的原理能够是:
function observifyArray(array){ var aryMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']; var arrayAugmentations = Object.create(Array.prototype); aryMethods.forEach((method)=> { let original = Array.prototype[method]; arrayAugmentations[method] = function () { // 调用对应的原生方法并返回结果 // do everything you what do ! return original.apply(this, arguments); }; }); array.__proto__ = arrayAugmentations; }
回到Vue的源码,虽然咱们知道基本原理确定是相同的,可是咱们仍然须要看看arrayAugmentations
是什么?下面arrayAugmentations
代码比较长。咱们会在注释里面解释基本原理:
// 代码来自于array-augmentations.js var _ = require('../util') var arrayAugmentations = Object.create(Array.prototype) // 这边操做和咱们以前的实现方式很是类似 // 建立arrayAugmentations原型继承`Array.prototype`从而能够调用数组的原生方法 // 而后经过arrayAugmentations覆盖数组的变异方法,基本逻辑大体相同 ['push','pop','shift','unshift','splice','sort','reverse'].forEach(function (method) { var original = Array.prototype[method] // 覆盖arrayAugmentations中的变异方法 _.define(arrayAugmentations, method, function () { var args = _.toArray(arguments) // 这里调用了原生的数组变异方法,并得到结果 var result = original.apply(this, args) var ob = this.$observer var inserted, removed, index // 下面switch这一部分代码看起来很长,其实目的就是针对于不一样的变异方法生成: // insert removed inserted 具体的含义对照以前的解释,了解便可 switch (method) { case 'push': inserted = args index = this.length - args.length break case 'unshift': inserted = args index = 0 break case 'pop': removed = [result] index = this.length break case 'shift': removed = [result] index = 0 break case 'splice': inserted = args.slice(2) removed = result index = args[0] break } // 若是给数组中插入新的数据,则须要调用ob.link // link函数其实在上面的_.augment(value, arrayAugmentations)以后也被调用了 // 具体的实现咱们能够先无论 // 咱们只要知道其目的就是分别对插入的数据执行响应化 if (inserted) ob.link(inserted, index) // 其实从link咱们就能够猜出unlink是干什么的 // 主要就是对删除的数据解除响应化,具体实现逻辑后面解释 if (removed) ob.unlink(removed) // updateIndices咱们也先不讲是怎么实现的, // 目的就是更新子元素在parents的key // 由于push和pop是不会改变现有元素的位置,所以不须要调用 // 而诸如splce shift unshift等变异方法会改变对应下标值,所以须要调用 if (method !== 'push' && method !== 'pop') { ob.updateIndices() } // 一样咱们先不考虑propagate内部实现,咱们只要propagate函数的目的就是 // 触发自身及其递归触发父级的事件 // 若是数组中的数据有插入或者删除,则须要对外触发"length"被改变 if (inserted || removed) { ob.propagate('set', 'length', this.length) } // 对外触发mutate事件 // 能够对照咱们以前讲的测试用例'array push',就是在这里触发的,回头看看吧 ob.propagate('mutate', '', this, { method : method, args : args, result : result, index : index, inserted : inserted || [], removed : removed || [] }) return result }) }) // 能够回看一下测试用例 array set,目的就是设置对应下标的值 // 其实就是调用了splice变异方法, 其实咱们在Vue中国想要改变某个下标的值的时候 // 官网给出的建议无非是Vue.set或者就是splice,都是相同的原理 // 注意这里的代码忽略了超出下标范围的值 _.define(arrayAugmentations, '$set', function (index, val) { if (index >= this.length) { this.length = index + 1 } return this.splice(index, 1, val)[0] }) // $remove与$add都是一个道理,都是调用的是`splice`函数 _.define(arrayAugmentations, '$remove', function (index) { if (typeof index !== 'number') { index = this.indexOf(index) } if (index > -1) { return this.splice(index, 1)[0] } }) module.exports = arrayAugmentations
上面的代码相对比较长,具体的解释咱们在代码中已经注释。到这里咱们已经了解完arrayAugmentations
了,咱们接着看看_.augment
作了什么。咱们在文章从Vue数组响应化所引起的思考中讲过Vue是经过__proto__
来实现数组响应化的,可是因为__proto__
是个非标准属性,虽然普遍的浏览器厂商基本都实现了这个属性,可是仍是存在部分的安卓版本并不支持该属性,Vue必须对此作相关的处理,_.augment
就负责这个部分:
exports.augment = '__proto__' in {} ? function (target, proto) { target.__proto__ = proto } : exports.deepMixin exports.deepMixin = function (to, from) { Object.getOwnPropertyNames(from).forEach(function (key) { var desc =Object.getOwnPropertyDescriptor(from, key) Object.defineProperty(to, key, desc) }) }
咱们看到若是浏览器不支持__proto__
话调用deepMixin
函数。而deepMixin
的实现也是很是的简单,就是使用Object.defineProperty
将原对象的属性描述符赋值给目标对象。接着调用了函数:
this.link(value)
关于link
函数在上面的备注中咱们已经见过了:
if (inserted) ob.link(inserted, index)
当时咱们的解释是将新插入的数据响应化,知道了功能咱们看看代码的实现:
// p === Observer.prototype p.link = function (items, index) { index = index || 0 for (var i = 0, l = items.length; i < l; i++) { this.observe(i + index, items[i]) } } p.observe = function (key, val) { var ob = Observer.create(val) if (ob) { // register self as a parent of the child observer. var parents = ob.parents if (!parents) { ob.parents = parents = Object.create(null) } if (parents[this.id]) { _.warn('Observing duplicate key: ' + key) return } parents[this.id] = { ob: this, key: key } } }
其实代码逻辑很是简单,link
函数会对给定数组index(默认为0)以后的元素调用this.observe
, 而observe
其实也就是对给定的val
值递归调用Observer.create
,将数据响应化,并创建父级的Observer与当前实例的对应关系。前面其实咱们发现Vue不只仅会对插入的数据响应化,而且也会对删除的元素调用unlink
,具体的调用代码是:
if (removed) ob.unlink(removed)
以前咱们大体讲过其用做就是对删除的数据解除响应化,咱们来看看具体的实现:
p.unlink = function (items) { for (var i = 0, l = items.length; i < l; i++) { this.unobserve(items[i]) } } p.unobserve = function (val) { if (val && val.$observer) { val.$observer.parents[this.id] = null } }
代码很是简单,就是对数据调用unobserve
,而unobserve
函数的主要目的就是解除父级observer
与当前数据的关系而且再也不保留引用,让浏览器内核必要的时候可以回收内存空间。
在arrayAugmentations
中其实还调用过Observer
的两个原型方法,一个是:
ob.updateIndices()
另外一个是:
ob.propagate('set', 'length', this.length)
首先看看updateIndices
函数,当时的函数的做用是更新子元素在parents的key,来看看具体实现:
p.updateIndices = function () { var arr = this.value var i = arr.length var ob while (i--) { ob = arr[i] && arr[i].$observer if (ob) { ob.parents[this.id].key = i } } }
接着看函数propagate
:
p.propagate = function (event, path, val, mutation) { this.emit(event, path, val, mutation) if (!this.parents) return for (var id in this.parents) { var parent = this.parents[id] if (!parent) continue var key = parent.key var parentPath = path ? key + Observer.pathDelimiter + path : key parent.ob.propagate(event, parentPath, val, mutation) } }
咱们以前说过propagate
函数的做用的就是触发自身及其递归触发父级的事件,首先调用emit
函数对外触发时间,其参数分别是:事件名、路径、值、mutatin
对象。而后接着递归调用父级的事件,而且对应改变触发的path
参数。parentPath
等于parents[id].key
+ Observer.pathDelimiter
+ path
到此为止咱们已经学习完了Vue是如何处理数组的响应化的,如今须要来看看是如何处理对象的响应化的。
在Observer
的构造函数中关于对象处理的代码是:
if (type === OBJECT) { if (options && options.doNotAlterProto) { _.deepMixin(value, objectAugmentations) } else { _.augment(value, objectAugmentations) } this.walk(value) }
和数组同样,咱们首先要了解一下objectAugmentations
的内部实现:
var _ = require('../util') var objectAgumentations = Object.create(Object.prototype) _.define(objectAgumentations, '$add', function (key, val) { if (this.hasOwnProperty(key)) return _.define(this, key, val, true) var ob = this.$observer ob.observe(key, val) ob.convert(key, val) ob.emit('add:self', key, val) ob.propagate('add', key, val) }) _.define(objectAgumentations, '$delete', function (key) { if (!this.hasOwnProperty(key)) return delete this[key] var ob = this.$observer ob.emit('delete:self', key) ob.propagate('delete', key) })
相比于arrayAugmentations
,objectAgumentations
内部实现则简单的多,objectAgumentations
添加了两个方法: $add
与$delete
。
$add
用于给对象添加新的属性,若是该对象以前就存在键值为key
的属性则不作任何操做,不然首先使用_.define
赋值该属性,而后调用ob.observe
目的是递归调用使得val
值响应化。而convert
函数的做用是将该属性转换成访问器属性getter/setter
使得属性被访问或者被改变的时候咱们可以监听到,具体我能够看一下convert
函数的内部实现:
p.convert = function (key, val) { var ob = this Object.defineProperty(ob.value, key, { enumerable: true, configurable: true, get: function () { if (Observer.emitGet) { ob.propagate('get', key) } return val }, set: function (newVal) { if (newVal === val) return ob.unobserve(val) val = newVal ob.observe(key, newVal) ob.emit('set:self', key, newVal) ob.propagate('set', key, newVal) } }) }
convert
函数的内部实现也不复杂,在get
函数中,若是开启了全局的Observer.emitGet
开关,在该属性被访问的时候,会对调用propagate
触发自己以及父级的对应get
事件。在set
函数中,首先调用unobserve
对之间的值接触响应化,接着调用ob.observe
使得新赋值的数据响应化。最后首先触发自己的set:self
事件,接着调用propagate
触发自己以及父级的对应set
事件。
$delete
用于给删除对象的属性,若是不存在该属性则直接退出,不然先用delete
操做符删除对象的属性,而后对外触发自己的delete:self
事件,接着调用delete
触发自己以及父级对应的delete
事件。
看完了objectAgumentations
以后,咱们在Observer
构造函数中知道,若是传入的参数中存在op.doNotAlterProto
意味着不要改变对象的原型,则采用deepMixin
函数将$add
和$delete
函数添加到对象中,不然采用函数arguments
函数将$add
和$delete
添加到对象的原型中。最后调用了walk
函数,让咱们看看walk
是内部是怎么实现的:
p.walk = function (obj) { var key, val, descriptor, prefix for (key in obj) { prefix = key.charCodeAt(0) if ( prefix === 0x24 || // $ prefix === 0x5F // _ ) { continue } descriptor = Object.getOwnPropertyDescriptor(obj, key) // only process own non-accessor properties if (descriptor && !descriptor.get) { val = obj[key] this.observe(key, val) this.convert(key, val) } } }
首先遍历obj
中的各个属性,若是是以$
或者_
开头的属性名,则不作处理。接着获取该属性的描述符,若是不存在get
函数,则对该属性值调用observe
函数,使得数据响应化,而后调用convert
函数将该属性转换成访问器属性getter/setter
使得属性被访问或者被改变的时候能被够监听。
到此为止,咱们已经看完了整个Observer
模块的全部代码,其实基本原理和咱们以前设想都是差很少的,只不过Vue代码中各个函数分解粒度很是小,使得代码逻辑很是清晰。看到这里,我推荐你也clone一份Vue源码,checkout到对应的版本号,本身阅读一遍,跑跑测试用例,打个断点试着调试一下,应该会对你理解这个模块有所帮助。
最后若是对这个系列的文章感兴趣欢迎你们关注个人Github博客算是对我鼓励,感谢你们的支持!