Vue2的响应式原理
--------Vue2响应式原理----------原理:通过数据劫持 defineProperty + 发布订阅者模式,当 vue 实例初始化后 observer 会针对实例中的 data 中的每一个属性进行劫持并通过 defineProperty() 设置值后在 get() 中向发布者添加该属性的订阅者,这里在编译模板时就会初始化每一属性的 watcher,在数据发生更新后调用 set 时会
--------Vue2响应式原理----------
原理:通过数据劫持 defineProperty + 发布订阅者模式,当 vue 实例初始化后 observer 会针对实例中的 data 中的每一个属性进行劫持并通过 defineProperty() 设置值后在 get() 中向发布者添加该属性的订阅者,这里在编译模板时就会初始化每一属性的 watcher,在数据发生更新后调用 set 时会通知发布者 notify 通知对应的订阅者做出数据更新,同时将新的数据根性到视图上显示。
缺陷:只能够监听初始化实例中的 data 数据,动态添加值不能响应,要使用对应的 Vue.set()。
1.第一步:对数据做数据劫持:
1.1对象类型做数据劫持
使用 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 实例。
过程:
- 首先将需要做监听的对象传入 observe 方法内,如果传进去的不是对象(第一次传入的数据毫无疑问是对象,但是后续的子属性还会再次调用 observe 函数,子属性的类型就很复杂了,因此需要有这层判断),就会直接return。如果是对象(或者数组因为 typeof(array) 返回的也是 object),就往下走。
- 此时已经确定了,传入的是个对象(数组),紧接着会判断这个对象(数组)有没有 ob 属性,有则代表已做过监视了,如果没有,就用它 new 一个 Observer 实例。
- 在new Observer 实例的过程中,会调用 def 方法给该实例添加一个 ob 属性(做个标记)。然后如果是对象则调用 walk 方法,walk 会遍历该对象中的每一项并用 defineReactive 方法加工。如果是数组则修改它的原型(里边有重写好的 API)为 arrayMethods,随即调用 observeArray 方法。
- defineReactive 方法用于对传入的属性做数据劫持(重写 get/set 方法)。因为是对属性进行操作的,因此即使传入的是数组,它也一定有 get/set 方法。
- 在 defineReactive 做数据劫持前,仍需再调用一次 observe 方法,去判断当前属性是否还是一个对象,如果是,就会再重复 1-3 的过程。此时这几个文件就形成了递归,直到某一次传入的属性不再是一个对象时,结束递归。当递归结束时,这个对象内的所有属性就都做好了数据劫持。
- 其中在 defineReactive 中还需要在 set 方法中将获取到的新值再一次使用 observe 方法,变成可监视的,因为这个新值也有可能是对象(或数组),如果不是,那么就会在 observe.js 文件中中直接 return 了。
图解:
几个文件的调用关系:
1.2.数组类型做数据劫持:
思路:
- 在 array.js 文件中以 Array.prototype 为原型,复制出一个新原型 arrayMethods,再把这个新原型上的 7 个可以改变数组的 API 方法全部重写,分别是:push、pop、shift、unshift、splice、sort、reverse。
- 这 7 个方法在保留原功能的基础上增加一些数据劫持的代码(也就是将数据变为可监控的),最后把 arrayMethods 暴露出去。
- 当在 new Observer 时判断传入的数据为数组后,使用 Object.setPrototypeOf() 方法强制让传入的那个数组的原型指向 arrayMthods,这样一来,这个数组以后就会使用我们重写好的 API,就达成响应式的目的。
- 随后要再一次对数组内的数据进行遍历,因为数组内部,或许还会有对象类型(或者数组类型)的数据。这就相当于用一个拦截器覆盖 Array.prototype,每当使用 Array 原型上的方法操作数组时,先执行 arrayMthods 拦截器中的数据劫持方法,再执行原生 Array 的方法去操作数组。
和上述对象的文件嵌套相比,增加了一个 array.js 文件。
2.第二步:依赖收集
需要用到数据的地方,称为依赖
Vue1.x, 细粒度依赖, 用到数据的 D0M 都是依赖;
Vue2.x, 中等粒度依赖, 用到数据的 组件 是依赖;
之所以要劫持数据,目的是当数据的属性发生变化时,可以通知那些曾经用到的该数据的地方。所以要先收集依赖,把用到这个数据的地方收集起来,等属性改变后,在之前收集好的依赖中循环触发一遍就好了,达到响应式的目的。
针对不同的类型:
在 getter() 中收集依赖,在 setter() 中触发依赖 // 对象类型
在 getter() 中收集依赖,在 拦截器 中触发依赖 // 数组类型
2.1前提:
此时已经进行过数据劫持了。
把 new Watcher 这个过程看作是 Vue 解析到了 {{ }} 模板的时候。
Dep.target 的值存在时,表示正处于依赖收集阶段。
Vue 在模板编译过程中遇到的指令和数据绑定都会生成 Watcher 实例,实例中的 Watch 属性也会成生成 Watcher 实例。
2.2过程:
- 在创建 Observer 实例的同时还会创建 Dep 实例,用于保存依赖项。因此每个数据都有 Observer 的实例,每个 Observer 实例中又都有一个 Dep 的实例。
- 当 Vue 解析到 {{ }} 中数据时,就会去创建 Watcher 实例,在 constructor 时会调用自身的 get 方法,该方法不仅将当前的 Watcher 实例赋值给了 Dep.target(表示此时处于依赖收集阶段),还让这个新实例去读取一下 {{ }} 中的数据,一旦读取,就会触发这个数据的 getter 方法。因为此时正在进行收集依赖,Dep.target 一定是为 true 的,于是顺利地把当前的这个 Watcher 实例记录到了 dep 中的 subs 数组里。再然后将 Dep.target 的值重新赋值为 null,表示退出依赖收集阶段。
- 为什么能记录到 subs 数组呢?因为在 defineReactive 文件的 17 行新 new 了一个 Dep 实例,这个实例只是一个工具人,通过调用工具人身上的 depend 函数,就将当前时刻的 Watcher 实例添加进去。这样一来当模板解析完毕,dep 实例就掌握这个数据的所有订阅者。
- 当数据的 set 方法被调用时,就执工具人的 dep.notify 方法,他会遍历 dep 实例身上的 subs 数组,这个数组存放了当前数据的所有订阅者,即许多 Watcher 实例,调用每一个 Watcher 实例身上的 update 方法,执行传入过来的回调函数,然后 Vue 接下来通过这个回调函数去进行 diff 算法,对比新旧模板,然后重新渲染页面,至此算是达到了响应式的目的。
因此,这个 Watcher 实际上是 Vue 的主程序在用。更新视图的代码应该是要写在传入过去的回调函数里。
3.最后总结:
3.1前置知识:
首先要了解三个最重要的对象:
Observer 对象:将 Vue 中的数据对象在初始化过程中转换为 Observer 对象。
Watcher 对象:将模板和 Observer 对象结合在一起生成 Watcher 实例,Watcher 是订阅者中的订阅者。
Dep对象:Watcher 对象和 Observer 对象之间纽带,每一个 Observe r都有一个 Dep 实例,用来存储订阅者 Watcher。
3.2过程:
- 在生命周期的 initState 方法中将 data,prop,method,computed,watch等所有数据全部进行数据劫持,将所有数据变为 Observer 实例,并且每个数据身上还有 Dep 实例。
- 然后在 initRender 方法中也就是模板编译过程,遇到的指令和数据绑定都会生成 Watcher 实例,并且把这个实例存入对应数据的 Dep 实例中的 subs 数组里。这样每一个数据的 Dep 实例里就都存放了依赖关系。
- 当数据变化时,数据的 setter 方法被调用,触发 dep.notify 方法,就会通知 Dep 实例依赖列表,执行 update 方法通知 Watcher,Watcher 会执行 run 方法去更新视图。
- 更新视图的过程,我猜是 Vue 接下来要进行 diff 算法,对比新旧模板,然后重新渲染页面。
Vue 是无法检测到对象属性的添加和删除,但是可以使用全局 Vue.set 方法(或 vm.$set 实例方法)。
Vue 无法检测利用索引设置数组,但是可以使用全局 Vue.set方法(或 vm.$set 实例方法)。
无法检测直接修改数组长度,但是可以使用 splice。
代码文件:
1.index.js:入口文件
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);
2.observe.js
用于判断某一属性是否为对象或者数组,因为 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;
}
3.Observer.js
对传入的属性做类型判断,然后分别转化为可被监测的属性。
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])
}
}
}
4.def.js
为属性添加 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
})
}
5.defineRective.js
给传入的属性做数据劫持(即添加 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()
}
})
}
6.array.js
该文件将 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
7.Dep.js
在依赖收集阶段,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);
}
}
}
8.Watcher.js
当解析到模板字符串 {{ }} 时,会默认去 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
// }
更多推荐
所有评论(0)