Vue2源码理解-Observer、Dep、Watcher
1.前言
Vue依靠数据驱动视图,本篇主要描述vue是如何侦测数据变化的。在Vue2中数据侦测主要是基于 Object.defineProperty() 实现的,数组的变化侦测主要依靠改写数组方法实现。
下面主要是一些自己理解源码和尝试还原的一些过程,及遇到的问题。实现的不全面,可以作为参考(欢迎各位大佬留评)。
主要涉及源码位置
src/core/observer/index.js
src/core/observer/dep.js
src/core/observer/watcher.js
2.数据侦测实现
-
Object.defineProperty()
- Object.defineProperty(obj, prop, descriptor) 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
- @param obj 要定义属性的对象
- @param prop 要定义或修改的属性的名称可为Symbol
- @param descriptor {{configurable?: boolean, enumerable?: boolean, value?: any, writable?: boolean, get?: Function, set?: Function}} 要定义或修改的属性描述符,及对象属性的描述对象字段
- -configurable:
- -作用:当且仅当该属性的 configurable 键值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。
- -默认值:false
- -enumerable:
- -作用:当且仅当该属性的 enumerable 键值为 true 时,该属性才会出现在对象的枚举属性中。
- -默认值:false
- -value:
- -作用:该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。
- -默认值:undefined
- -writable:
- -作用:当且仅当该属性的 writable 键值为 true 时,属性的值,也就是上面的 value,才能被赋值运算符 (=, *=等)改变。
- -默认值:false
- -get:
- -作用:属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。
- -默认值:undefined
- -set:
- -作用:属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。
- -默认值:undefined
- 此方法详情可见https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
-
实现数据的可侦测
我们先定义一个对象
var obj = { name: 'obj', value: 'obj_value', child: { name: 'child', value: 'child_value' } }这个对象能被我们读取到,但是当对其中的属性做修改时,我们无法监测到,我们要想能监测到就需要用到上面提到的 Object.defineProperty(),我们再看下面一段代码
var obj = {}; var val = 12; Object.defineProperty(obj, 'value', { enumerable: true, configurable: true, get: function () { console.log('value属性被获取了'); return val; }, set: function (newValue) { console.log('value属性被修改了'); val = newValue } }); var v = obj.value; // value属性被获取了 obj.value = 24; // value属性被修改了上面代码实现了一个属性可侦测获取及修改,下面将上面代码进行下改造,使传入一个定义好的对象使其变的可观测
/** * 循环对象的key使每个属性转变为可观测对象 * @param obj */ function walk (obj) { var keys = Object.keys(obj); for (var i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]); } } /** * 判断是否Object,是否需要转变为可观测对象 * @param obj */ function observe (obj) { // 本篇只针对对Object的观测,数组不做实现 if (Object.prototype.toString.call(val) === '[object Object]') { walk(val); } } /** * 单个属性可侦测 * @param obj * @param key * @param val */ function defineReactive (obj, key, val) { // 这里判断下是否传入了val值, 没传入则使用obj[key]; if (arguments.length === 2) { val = obj[key] } // 如果是Object做递归处理 observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function () { console.log(['获取', '"', key, '"', '的值'].join('')); return val; }, set: function (v) { // 判断新值是否和原值一样,一样不做操作 if (val === v) { return; } console.log(['修改', '"', key, '"', '的值'].join('')); val = v; // 如果新值为Object,转变为可观测对象 observe(v); } }) } var obj = { name: 'obj', value: 'obj_value', child: { name: 'child', value: 'child_value' } } // 调用 walk(obj);参照Vue源码再次改造实现Observer类
function observe (obj) { // 本篇只针对对Object的监测,数组不做实现 if (Object.prototype.toString.call(val) === '[object Object]') { new Observer(obj); } } function Observer(value) { this.value = value; if (Array.isArray(value)) { // 数组不做处理 } else { this.walk(value); } } Observer.prototype.walk = function (obj) { var keys = Object.keys(obj); for (var i = 0; i < keys.length; i++) { this.defineReactive(obj, keys[i]); } } Observer.prototype.defineReactive = function (obj, key, val) { if (arguments.length === 2) { val = obj[key] } observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function () { console.log(['获取', '"', key, '"', '的值'].join('')); return val; }, set: function (v) { // 判断新值是否和原值一样,一样不做操作 if (val === v) { return; } console.log(['修改', '"', key, '"', '的值'].join('')); val = v; observe(v); } }) }到这里我们通过get和set方法实现了将传入的json数据变为可侦测数据,但是这远远不够,虽然数据可观测了,但是没有任何意义,数据变更后,我们只是console一串文字,仍然不知道要做什么,我们还需要记录谁观测了这些数据(谁使用了这些数据),在数据发生变更时通知观测者,做出相应的动作。
在Vue中主要通过Dep和Watcher两个类实现,Watcher用来收集具体谁观测了(使用了)这些数据,Dep用来存取每条数据对应了多少个Watcher。这样在数据变更后触发Dep通知Watcher,Watcher回调具体使用数据的方法,使其做出相对应的操作。 -
Dep 和 Watcher
这两个类放在一起说,是因为他们之间有相互依赖的关系,共同实现数据变化的传递。
实现一个简单的Dep和Watcherfunction Dep () { this.sups = []; // 用于存取Watcher实例 } Dep.prototype.addSub = function (sub) { this.subs.push(sub); // 向sups中添加Watcher实例 } Dep.prototype.removeSub = function (sub) { // 从sups中删除Watcher实例 const index = this.subs.indexOf(sub); if (index > -1) { this.subs.splice(index, 1); } } Dep.prototype.depend = function () { // window.target 是暂存全局的Watcher实例,在此处将Dep和Watcher关联,在vue中不是放于全局的,为方便理解,放在全局更好理解 if (window.target) { // 调用 this.addSub(window.target); } } Dep.prototype.notify = function () { // 数据发生变化后调用,遍历当前更新数据对应的所有的Watcher对象,调用update方法,通知更新 const subs = this.subs.slice() for (var i = 0, l = subs.length; i < l; i++) { subs[i].update() } } /** * @param vm 观测对象 * @param expOrFn {String | Function} 观测的具体字段,如: 'child.name'、function () { return this.name + ':' + this.value }; * @param cb {Function} 数据变化后的回调函数 */ function Watcher (vm, expOrFn, cb) { this.vm = vm; this.cb = cb; if (typeof expOrFn === "function") { this.getter = expOrFn; } else { this.getter = parsePath(expOrFn); } // 这里调用一遍get方法,触发获取数据的getter,向Dep添加一个实例 this.value = this.get(); } Watcher.prototype.get = function () { // 这里将this赋值给window.target,上方Dep的depend方法被调用,就会向subs插入实例 window.target = this; var vm = this.vm; var value = this.getter.call(vm, vm); // call改变this指针,防止内部this指针错误 window.target = null; return value; } Watcher.prototype.update = function () { var oldValue = this.value this.value = this.get() // 触发回调 this.cb.call(this.vm, this.value, oldValue) } /** * Parse simple path. * 把一个形如'data.a.b.c'的字符串路径所表示的值,从真实的data对象中取出来 * 例如: * data = {a:{b:{c:2}}} * parsePath('a.b.c')(data) // 2 */ var bailRE = /[^\w.$]/; function parsePath (path) { if (bailRE.test(path)) { return } const segments = path.split('.') return function (obj) { for (let i = 0; i < segments.length; i++) { if (!obj) return obj = obj[segments[i]] } return obj } }这样简单的Dep和Watcher就实现了,下面将Dep 和 Watcher 用到 Observer中, 改写下 Observer.prototype.defineReactive 方法
Observer.prototype.defineReactive = function (obj, key, val) { if (arguments.length === 2) { val = obj[key] } observe(val); var dep = new Dep(); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function () { if (window.target) { dep.depend(); } return val; }, set: function (v) { // 判断新值是否和原值一样,一样不做操作 if (val === v) { return; } val = v; dep.notify(); observe(v); } }) } // 调用 var obj = { name: 'obj', value: 'obj_value', child: { name: 'child', value: 'child_value' } } new Observer(obj); new Watcher(obj, 'name', function (val, oldVal) { // ... console.log(val, oldVal); }) new Watcher(obj, 'child.name', function (val, oldVal) { // ... console.log(val, oldVal); }) obj.name = 'test'; obj.child.name = 'child_test';上面的赋值操作,会触发dep.notify,dep.notify触发实例Watcher的update方法触发传入的回调函数,在控制台就能看到打印信息,有兴趣的可以试下。
这里面感觉比较难理解的就是,Watcher和Dep是如何关联的,关联方式我梳理了下,不知到能不能说明白,先这么看着吧:
1、new Watcher()
2、触发 Watcher的get方法
3、window.target = this 向window.target赋值
4、this.getter.call(vm, vm) 获取对应的数据值,触发数据值的get方法
5、数据值的get方法执行存在window.target
6、调用Dep的depend方法,将Watcher实例传递给Dep实例,添加从观测成功
7、window.target = null 清除赋值,阻止后续别的值调用依赖关系错误到这里已经实现一大部分了,但是当你运行这段代码的时候会发现Watcher的回调函次数会更着你数据修改的数据的次数增加,就是回调被重复绑定了,修改一次多绑定一次,这里解决方式就是绑定使用一次后销毁重新绑定,下面改写了下Dep和Watcher的代码对此处做了修正:
function Dep () { this.subs = []; } Dep.prototype.addSub = function (sub) { this.subs.push(sub); } Dep.prototype.removeSub = function (sub) { const index = this.subs.indexOf(sub); if (index > -1) { this.subs.splice(index, 1); } } Dep.prototype.depend = function () { if (window.target) { window.target.addDep(this); } } Dep.prototype.notify = function () { const subs = this.subs.slice() for (var i = 0, l = subs.length; i < l; i++) { subs[i].update() } } function Watcher (vm, expOrFn, cb) { this.vm = vm; this.cb = cb; if (typeof expOrFn === "function") { this.getter = expOrFn; } else { this.getter = parsePath(expOrFn); } this.value = this.get(); this.dep = null; } Watcher.prototype.get = function () { window.target = this; var vm = this.vm; var value = this.getter.call(vm, vm); window.target = null; return value; } Watcher.prototype.update = function () { var oldValue = this.value this.value = this.get() this.cb.call(this.vm, this.value, oldValue) } Watcher.prototype.addDep = function (dep) { dep.removeSub(this); // 这里移除后再新增 this.dep = dep; dep.addSub(this); }这就是改造后的代码,参照Vue和自己理解写的,没有源码复杂,简化了很多,也修正了数据修改回调次数增多的问题。
上面就是本篇全部了,就是抽空看下,现在只看了源码这些部分,数组的更新没有去做实现,大致看了下主要依靠改写Array原型链方法实现。
浙公网安备 33010602011771号