赞
踩
原理:通过数据劫持 defineProperty + 发布订阅者模式,当 vue 实例初始化后 observer 会针对实例中的 data 中的每一个属性进行劫持并通过 defineProperty() 设置值后在 get() 中向发布者添加该属性的订阅者,这里在编译模板时就会初始化每一属性的 watcher,在数据发生更新后调用 set 时会通知发布者 notify 通知对应的订阅者做出数据更新,同时将新的数据根性到视图上显示。
缺陷:只能够监听初始化实例中的 data 数据,动态添加值不能响应,要使用对应的 Vue.set()。
使用 Object.defineProperty 方法添加对象,重写了原有的 get 和 set 方法,这就是数据劫持。
defineRective 文件通过封装 Object.defineProperty 方法,把浅层次的对象中的某个数据变成具有 get 和 set 方法的属性。因为有闭包的存在,所以不需要临时变量进行周转了。
接下来是深层次:用递归侦测对象的全部属性
八个 JS 文件互相调用(文件代码,放在文章下方):
index.js:入口文件
observe.js:用于判断某一属性是否为对象或者数组,因为 typeof(array) 返回的也是 object,算是一个局限性。普通数据就直接 return,对象(数组)就给它调用 new Observer
Observer.js:对传入的属性做类型判断,然后分别转化为可被监测的属性。
def.js:为属性添加 ob 属性,做标记,而且可以通过 value.ob 来访问 Observer 的实例
defineRective.js:给传入的属性做数据劫持(即添加 set/get 方法),因为是对属性进行操作的,不做类型判断,因此不论这个传入过来的属性是数组还是对象,都会有 get/set 方法。
array.js:该文件将 JS 中能改变数组的 7 个方法重写,并在进行数据劫持的时候将,数组的原型指向该文件加工后的新原型。
Dep.js:在依赖收集阶段,Dep 对象是 Watcher 对象和 Observer 对象之间纽带,每一个 Observer 都有一个 Dep 实例,用来存储订阅者 Watcher
Watcher.js:当解析到模板字符串 {{ }} 时,会默认去 new Watcher 实例。
过程:
图解:
几个文件的调用关系:
思路:
和上述对象的文件嵌套相比,增加了一个 array.js 文件。
需要用到数据的地方,称为依赖
Vue1.x, 细粒度依赖, 用到数据的 D0M 都是依赖;
Vue2.x, 中等粒度依赖, 用到数据的 组件 是依赖;
之所以要劫持数据,目的是当数据的属性发生变化时,可以通知那些曾经用到的该数据的地方。所以要先收集依赖,把用到这个数据的地方收集起来,等属性改变后,在之前收集好的依赖中循环触发一遍就好了,达到响应式的目的。
针对不同的类型:
在 getter() 中收集依赖,在 setter() 中触发依赖 // 对象类型
在 getter() 中收集依赖,在 拦截器 中触发依赖 // 数组类型
此时已经进行过数据劫持了。
把 new Watcher 这个过程看作是 Vue 解析到了 {{ }} 模板的时候。
Dep.target 的值存在时,表示正处于依赖收集阶段。
Vue 在模板编译过程中遇到的指令和数据绑定都会生成 Watcher 实例,实例中的 Watch 属性也会成生成 Watcher 实例。
因此,这个 Watcher 实际上是 Vue 的主程序在用。更新视图的代码应该是要写在传入过去的回调函数里。
首先要了解三个最重要的对象:
Observer 对象:将 Vue 中的数据对象在初始化过程中转换为 Observer 对象。
Watcher 对象:将模板和 Observer 对象结合在一起生成 Watcher 实例,Watcher 是订阅者中的订阅者。
Dep对象:Watcher 对象和 Observer 对象之间纽带,每一个 Observe r都有一个 Dep 实例,用来存储订阅者 Watcher。
Vue 是无法检测到对象属性的添加和删除,但是可以使用全局 Vue.set 方法(或 vm.$set 实例方法)。
Vue 无法检测利用索引设置数组,但是可以使用全局 Vue.set方法(或 vm.$set 实例方法)。
无法检测直接修改数组长度,但是可以使用 splice。
import observe from './observe'; import Watcher from './Watcher' // 因为 Vue 会把所有的数据都存放在 data 对象中,所以一切数据的最外层都是一个对象 let obj = { a: { m: { n: 5 } }, c: { d: { e: { f: 6666 } } }, g: [22, 33, 44, 55] } observe(obj) // new Watcher 的过程,看作 Vue 在解析到了 {{}} 的时候, new 一次,subs 数组里就多一个数据,表示对有三处模板(可以看到每一个 watcher 的id 是不同的)用到了 a.m.n 属性 // 因此当第二次修改数据时,有两个 watcher 实例在监视它,就会输出两次值,就代表这需要重新渲染两次 // 同理第三次修改,三个 watcher 实例,渲染3次 new Watcher(obj, 'a.m.n', (val,oldValue) => { console.log('#######', val,oldValue); }) console.log(obj); // 2s后修改值 setTimeout(() => { obj.a.m.n = 88 }, 2000);
用于判断某一属性是否为对象或者数组,因为 typeof(array) 返回的也是 object,算是一个局限性。普通数据就直接 return,对象(数组)就给它调用 new Observer
import Observer from "./Observer"; export default function (value){ // 如果 value 不是对象或者数组,就直接返回。此处因为 typeof 的局限性,typeof(数组) 仍会返回 object // 因为 Vue 中不会单独存放 int、float 等类型的数据,毕竟它们没法调用 Object.defineProperty // 况且在 Vue 中数据都是存放在对象中的,所以根本不考虑其他数据类型 if(typeof (value) !== 'object') return; // 定义ob,存储 observe 实例 var ob; if(typeof value.__ob__ !== 'undefined') { ob = value.__ob__; } else { ob = new Observer(value) } return ob; }
对传入的属性做类型判断,然后分别转化为可被监测的属性。
import { def } from './def' import defineRective from './defineReactive' import { arrayMethods } from './array' import observe from './observe' import Dep from './Dep' /** * 该类的作用:将一个正常的 object 的每个层级的属性都转化为可以被侦测的属性 */ export default class Observer { constructor(value) { this.dep = new Dep(); // 构造函数的this不是类本身,而是表示实例。 // 添加 __ob__ 属性,值是这次 new 的 Observer 的实例,不可枚举 // _ob__的作用可以用来标记当前value是否已经被Observer转换成了响应式数据了;而且可以通过value.__ob__来访问Observer的实例 console.log('我是 Observer 构造器,接下来要用 def 方法去给传入的对象值添加 __ob__ 属性', value) def(value, '__ob__', this, false) // 检查这个数据是数组还是对象 if (Array.isArray(value)) { console.log('传入的是数组,我将改变它的原型为 arrayMethods ') // 是数组,就让他的原型指向 arrayMethods。到此,数组的监控已经加工完毕 Object.setPrototypeOf(value, arrayMethods) // 随后再遍历这个数组,因为数组内部,或许还会有对象类型的数据,肯定也要变为可监控的 this.observeArray(value) } else { console.log('def 方法执行完毕,接下来要用 walk 方法去遍历这个对象', value) // 让对象数据变为可监控的 this.walk(value) } } // 对象的特殊遍历 walk(value) { for (let key in value) { console.log(`我是 walk 方法,这次遍历了对象中的 ${key} 属性,并用 defineReactive 方法给它加工一下 `); defineRective(value, key) } } // 数组的特殊遍历 observeArray(arr) { // 逐项进行 observe,因为数组内部,或许还会有对象类型的数据,肯定也要变为可监控的 for (let i = 0, l = arr.length; i < l; i++) { // console.log(arr[i]); observe(arr[i]) } } }
为属性添加 ob 属性,做标记,而且可以通过 value.ob 来访问 Observer 的实例
// 对传入过来的数据添加指定的属性 /** * 定义一个对象属性 * @param {*} obj * @param {*} key * @param {*} value * @param {*} enumerable */ export const def = function (obj, key, value, enumerable){ Object.defineProperty(obj, key,{ value, enumerable, writable: true, configurable: true }) }
给传入的属性做数据劫持(即添加 set/get 方法),因为是对属性进行操作的,不做类型判断,因此不论这个传入过来的属性是数组还是对象,都会有 get/set 方法。
// 该文件将传入过来的属性加工成具有 get 和 set 方法的响应式数据 // 因为是对属性进行操作的,不做类型判断,因此不论这个传入过来的属性是数组还是对象,都会有 get/set 方法 /** * 给对象data的属性key定义监听 * @param {*} data 传入的数据 * @param {*} key 监听的属性 * @param {*} val 闭包环境提供的周转变量 */ import observe from "./observe" import Dep from "./Dep" export default function defineRective(data, key, val) { // 这里 new Dep 实际上是个工具人,只起到一个调用 depend 函数的作用,他不保存在任何数据身上。 // 调用 depend 函数时,是将代码正在运行的那个时刻的 Watcher 实例添加进去,因此不会影响。 const dep = new Dep() // 如果传入两个参数,则直接取出值给 val if (arguments.length == 2) { val = data[key] } // 对传过去的每一项还要 observe 一下,如果不是对象(数组)了就会直接 return,代码往下走。 // 如果仍是对象(数组),那么就形成了递归,直到不是某一层不是对象(数组)为止。这个递归比较特殊不是函数自己调用自己,而是多个函数循环调用 let childOb = observe(val) // 这里接收的是子代属性创建的 Observer 的实例对象,用于后续做依赖收集 // val 构成了闭包:后续代码有调用到 val 的地方,因此 val 不会消失。 Object.defineProperty(data, key, { // 可枚举 enumerable: true, // 可以被配置,比如可以被delete configurable: true, // getter 触发这个方法,就会将数据添加到依赖中 get() { console.log(`访问了 obj 的 ${key} 属性,值为${val}`) // 如果现在处于依赖收集阶段,即在模板解析的时候,就会调用 setter 方法,就会往 subs 里添加东西 if(Dep.target){ console.log('访问了watcher'); // console.log(data.__ob__.dep.depend); // debugger; // 将此时的 Watcher 实例对象添加 dep 中的 subs 数组里 // 在这里为什么要重新 new Dep dep.depend() // 这里为什么执行不了 depend 函数??? // 既然17行 new的实例只是工具人,起到调用 depend 函数的作用,那我这里随便访问一个 dep 实例并调用他身上的 depend 不可以吗? // data.__ob__.dep.depend() // 给子元素也添加依赖 if(childOb){ childOb.dep.depend(); } } return val }, // setter set(newValue) { console.log(`改变了 obj 的 ${key} 属性,新的值为${newValue}`) if (val === newValue) { return } val = newValue // 当设置了新值,这个新值可能也包含对象或者数组,因此也要被 observe childOb = observe(newValue) // 这里我不理解为什么还要用 childOb 接收一下 // 发布订阅模式,通知 dep 对依赖进行修改 dep.notify() } }) }
该文件将 JS 中能改变数组的 7 个方法重写,并在进行数据劫持的时候将,数组的原型指向该文件加工后的新原型。
import { def } from './def.js' // 得到 Array.prototype const arrayPrototype = Array.prototype // 以 Array.prototype 为原型对象创建 arrayMethods 对象,并暴露出去 export const arrayMethods = Object.create(arrayPrototype) console.log(arrayMethods) // 需要改写的7个方法 const methodsNeedChange = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] // 通过遍历给新原型的7个方法内部都添加一些新的内容 methodsNeedChange.forEach((methodName) => { // 备份原来的方法,因为劫持后仍然需要原生的 API 去改变数据 const original = arrayPrototype[methodName] // 定义新的方法 def(arrayMethods, methodName, function () { // 这个重写函数分为两步走: // 1.使用原来的功能去操作数组: const result = original.apply(this, arguments) // 把类数组 arguments 变为数组,类数组没有 slice 方法,要不然下边无法正常切割了 const args = [...arguments] // 2. 把新添加的值(或修改后的值)都加工成可监控的: // 把这个数组身上的 __ob__ 取出来,此时 __ob__ 已经被添加了 /** 为什么这里会有 __ob__ 属性呢? * 因为当 walk 函数遍历到数组 g 时,会继续按照流程走: defineReactive 文件把 g 交给 observe 文件,observe 判断 g 没有 __ob__,就会再交给 Observer 文件 * 紧接着 Observer 在 16 行用 def 方法给 g 添加 __ob__ 属性。一直到这里才做是否为数组的判断,到这里才用到了 array 文件 * 因此在这里完全可以取到 __ob__ 属性,它的值就是 g 的 Observer 实例本体 */ const ob = this.__ob__ // 有三种方法 push/unshift/splice 能够插入新项,现在ob__要把插入的新项也要变为 observe 的 let inserted = []; switch (methodName) { case 'push': case 'unshift': inserted = args; break; case 'splice': // 因为 splice 方法的三个参数代表:(下标, 数量, 插入的新项) 因此用 slice 取到插入进去的那个数据 inserted = args.slice(2); break; } // 判断有没有要插入的新项,如果有,就调用 observeArray 方法(来自数组的 Observer 实例身上),因为新的数据可能也包含对象类型的 if (inserted) { ob.observeArray(inserted); } // 能输出这句话代表重写方法成功 console.log('能输出这句话代表重写你所使用的那个数组 API 重写成功'); // 通知依赖进行数据的更新 ob.dep.notify() // 必须要有返回值,因为一些 pop、splice 的方法会返回被操作的值 return result }, false); }) // export default arrayMethods
在依赖收集阶段,Dep 对象是 Watcher 对象和 Observer 对象之间纽带,每一个 Observer 都有一个 Dep 实例,用来存储订阅者 Watcher
var uid = 0 export default class Dep { constructor() { console.log('我是 Dep 构造器'); this.id = uid++; // 用数组存储自己的订阅者 实际存放的是许多的 Watcher 实例 this.subs = [] } // 添加订阅 addSub(sub) { this.subs.push(sub) } // 删除订阅 removeSub(sub) { remove(this.subs, sub); } // 添加依赖 depend() { // 在调用这个函数时,一定是出于依赖收集阶段,因此 Dep.target 是存在的 if (Dep.target) { this.addSub(Dep.target) } } // 通知更新 notify() { console.log('我是 notify'); // 浅克隆一份 const subs = this.subs.slice() // 遍历 for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } } /** * 从arr数组中删除元素item * @param {*} arr * @param {*} item * @returns */ function remove(arr, item) { if (arr.length) { const index = arr.indexOf(item); if (index > -1) { return arr.splice(index, 1); } } }
当解析到模板字符串 {{ }} 时,会默认去 new Watcher 实例。
/** * 每一次的 new Watcher 都是独立的,因此构造器接收的三个参数,虽然名字一样但确实不同的数据,就像是 vm.$watch() 接收的参数一样, * @param {*} target 需要监视的对象,当做修改时,他就是 * @param {*} expression 这个对象中的某个属性,它是一个表达式 比如 obj.a.b.c * @param {*} callback 回调函数,需要执行的操作 */ import Dep from "./Dep"; // 这个 uid 用于对每一个的 Watcher 实例添加唯一的 id var uid = 0 // 在这里哪一步算是调用了 get 方法???????,解析到模板的时候 export default class Watcher { constructor(target, expression, callback) { console.log('我是 Watcher 构造器'); this.id = uid++; // 模板字符串中的整个表达式 this.target = target; // 通过拆分表达式(对象中的对象...),获得需要 Watch 的那个数据。比如传入的是 a.b.c.d 我们需要监视属性 d,就需要拆分 this.getter = parsePath(expression) // 有两种方法供使用 parsePath 会返回一个函数;如果用 reduce 方法,那么 getter 就会是一个具体的值,此时一定要修改下边的 get 方法!!! this.callback = callback // 调用该方法,进入依赖收集阶段 this.value = this.get() } // 当更新 dep 中的依赖项时,会调用每一个 Watcher 实例身上的 update 方法 update() { console.log('我是Watcher实例身上的update方法'); this.run() } // 进入依赖收集阶段,让全局的 Dep.target 设置为 Watcher 本身 get(){ // Webpack 在打包的时候 Dep 是全局唯一的,不管多少个JS 文件在用 dep 的时候,都是这一个文件 // 因此执行到这里 console.log(this); // Watcher 实例 Dep.target = this; // debugger; const obj = this.target; var value; // 防止找不到,用try catch一下,只要能找,就一直找 try { value = this.getter(obj) // 获取需要监视的那个值。这里因为constructor 的时候 this.get() 返回的是一个函数 } finally { Dep.target = null // 清空全局 target 的指向,同时也表示退出依赖收集阶段 } return value } // 其实可以直接 getAndInvoke,但是 Vue 源码时这样写的 run(){ this.getAndInvoke(this.callback) } // getAndInvoke(callback){ // 获取到修改后的新值 旧值是 this.value const value = this.get() if(value !== this.value || typeof value == 'object'){ const oldValue = this.value; this.value = value; callback.call(this.target, value, oldValue) } } } // 拆分表达式: // 方法一:将 str 用 . 分割成数组 segments,然后循环数组,一层一层去读取数据,最后拿到的 obj 就是 str 中想要读的数据 // 假设 let o = {a:{b:{c:{d:55}}}},我想要取得 d 的值,经过拆分后的 segments 数组的值为 ['a', 'b', 'c', 'd'] // 第一次循环后 obj = {b:{c:{d:55}}}, 第二次 obj = {c:{d:55}}, 第三次 obj = {d:55}, 第四次 obj = 55 function parsePath(str) { let segments = str.split("."); return function (obj) { for (let key of segments) { if (!obj) return; // 当没有传入 obj 时,直接 return obj = obj[key]; } return obj; }; } // 方法二 用 reduce 方法实现 // function parsePathReduce(str) { // let segments = str.split("."); // let result = segments.reduce((total, item) => { // total = total[item] // return total // }, str) // return result // }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。