【手把手教你搓Vue响应式原理】(五) Watcher 与 De
大家好,我是 辉夜真是太可爱啦 。这是我最近在写的【手把手教你搓Vue响应式原理】系列,本文将一步步地为你解开vue响应式原理的面纱。由于本人也是在写这篇文章的过程中不断试错,不断学习改进的,所以,本文同样很适合和我一样的初学者。和
Vue
的设计理念如出一辙,那就是渐进增强。
上文链接
【手把手教你搓Vue响应式原理】(三)observe 以及 ob
前言
之前已经将数据劫持已经全部完成了。
那么,接下来,主要的要点就是在于两点,依赖收集和触发依赖更新。
它的意义主要在于控制哪些地方使用了这个变量,然后,按照最小的开销来更新视图。
首先,要先明白,依赖是什么,比方说在我们的模板中有 {{a}}
,那么,这个地方就有对于变量 a 的依赖。
在模板编译的时候,就会触发 a 变量的 getter
。
然后,当我们执行 a++;
的时候,那么,我们就要触发依赖的更新,当初模板中 {{a}}
的地方,就要更新,是吧!
所以,我们都是在 getter
中收集依赖,在 setter
中触发依赖更新 。
这一节的内容,主要就是用来专门讲清楚这两件事情。
依赖收集和派发更新
依赖收集和触发依赖更新主要由两个类来完成, Dep
和 Watcher
。
Dep
和 Watcher
在设计模式中,就是发布-订阅者的模式。
而依赖,你可以理解为所谓的订阅者。
- Dep
Dep
说白了就是发布者,它的工作就是依赖管理,要知道哪些地方用到了这个变量,可能用到这个变量的地方有很多,所以,它会有多个订阅者。
然后,每个变量都应该有属于自己的 Dep
,因为每个变量所在的依赖位置是不一样的,所以他们的订阅者也不一样。
然后在变量更新之后,就去通知所有的订阅者(Watcher),我的变量更新了,你们该触发视图更新了。
- Watcher
Watcher
说白了就是订阅者,它接受 Dep
发过来的更新通知之后,就去执行视图更新了。
它其实就是所谓的 watch 监听器,变量改变之后,执行一个回调函数。
Dep
初始化我们的 Dep 类
我们先按照图例来创建我们的 Dep
类
根据我们的需求:
- 首先,它要在初始化的时候,新建一个
subs
数组,用来存储依赖,也就是 Watcher 的实例
class Dep{
constructor() {
// 用数组存储自己的订阅者 subs 是 subscribes 订阅者的意思
// 这个数组里放的是 Watcher 的实例
this.subs=[]
}
}
- 它需要有一个
depend()
方法,用于添加依赖,也就是将 Watcher 实例往subs
数组中 push
class Dep{
// 添加依赖
depend(){
// 判断当前是否有需要监听的目标,Dep.target 会被 Wacher 赋值
if(Dep.target){
// 将监听的目标推进 subs 数组
this.subs.push(Dep.target);
}
}
}
- 它需要有一个
notify()
方法,用于通知 Wacher 数据更新了,调用 Wacher 的update()
方法
class Dep{
// 通知所有订阅者
notify(){
// 浅克隆一份
const subs=this.subs.slice();
// 遍历
for(let i=0,l=subs.length;i<l;i++){
// 逐个更新
subs[i].update();
}
}
}
使用我们的 Dep 类
- 每个属性都要有自己的 Dep
Dep
我们在前面也说了,每个属性都应该有它自己的 Dep ,用来管理依赖。
所以,首先,如果我们在 Observer
中创建 Dep,那不就可以了。毕竟 Observer
会遍历到每一个对象。
class Observer{
constructor(obj){
this.dep=new Dep();
// ...
}
}
- 在 getter 中收集依赖
所以,很明显,我们可以在 defineReactive 的 get 中收集依赖
因为有了 if(Dep.target)
的判断,所以,只有绑定 Watcher 的变量触发 getter 时,才会添加依赖。
function defineReactive(obj,key,val) {
let dep=new Dep();
let childOb;
// 判断当前入参个数,两个的话直接返回当前层的对象
if(arguments.length===2){
val=obj[key];
childOb = observe(val)
}
Object.defineProperty(obj,key,{
get(){
// Dep.target 是我们弄的唯一标识,当有这个标识的时候,添加依赖
if(Dep.target){
// 添加依赖
dep.depend();
// 如果有子属性,也要将它加入依赖
if(childOb){
// 给子属性添加依赖
childOb.dep.depend();
}
}
return val;
},
})
}
这个 Dep.target
其实就是 Watcher 的实例
- 在 setter 中触发依赖更新
所以,很明显,我们可以在 defineReactive 的 set 中收调用 notify()
方法告知 Watcher 实例,数据更新了。
function defineReactive(obj,key,val) {
let dep=new Dep();
let childOb;
// 判断当前入参个数,两个的话直接返回当前层的对象
if(arguments.length===2){
val=obj[key];
childOb = observe(val)
}
Object.defineProperty(obj,key,{
// ...
set(newValue){
val=newValue;
childOb = observe(val)
// notify 切忌 val=newValue 之后,不然在 callback 回调中一直是旧值
dep.notify();
}
})
}
至此, Dep
的所有职责,我们已经帮它完成了。
其实照道理应该有一个删除依赖,我们这里就不再扩展了。
Watcher
初始化我们的 Watcher 类
首先, Watcher
实例应该大家会相对而言更加好理解点,因为,我们有一个 watch 侦听器,大家一定都很熟悉,这两个其实一样。
我们先按照图例来创建我们的 Watcher
类
根据我们的需求:
- 首先,它要在初始化的时候,需要传入目标对象
target
, 属性名expression
, 回调函数callback
class Watcher{
// target 目标对象
// expression 属性名
// callback 回调函数
// value 属性的值
constructor(target,expression,callback) {
this.target=target;
// parsePath 为一个高阶函数
this.getter=parsePath(expression);
this.callback=callback;
// get为我们之后要写的获取值的方法
this.value=this.get();
}
}
这个 parsePath
需要单独拎出来说一下,比方说我们现在有这么一个对象
let a={
b:{
c:{
d:10
}
}
}
我们要监听到 a.b.c.d
,所以,我们需要下面的这种格式
new Watcher(a,'b.c.d',val=>{
console.log('ok啦',val);
})
所以,这个 get 很明显就有点难度了。 我们需要通过循环 拿到 a.b 然后 .c 然后 .d。
我们将这个方法命名为 parsePath
。
function parsePath(str){
let segments = str.split('.');
return obj=> {
for(let i=0;i<segments.length;i++){
if(!obj) return;
obj=obj[segments[i]];
}
return obj;
}
}
入参接受我们的 b.c.d
,我们可以看到 第一句执行之后 segments=['b','c','d']
,然后进行第二层,这是返回了一个方法,按照循环,那就是 obj=obj.b
=> obj=obj.c
=> obj=obj.d
,所以,就是返回一个对象的 obj.b.c.d,相当于是遍历字符串中的属性树。
- 它需要有一个
get()
方法,用于获取当前的值,并将它更新,然后return
返回
class Watcher{
// 获取当前的值,并将它更新,然后 return 返回
get(){
// 进入依赖收集阶段,将 Dep.target 设为 Watcher 实例本身
Dep.target=this;
// 当前对象
const obj=this.target;
let value;
// 当对象不再使用的时候,我们需要将它清空
try{
value=this.getter(obj)
}finally {
Dep.target=null;
}
this.value=value
return value;
}
}
- 它需要有一个
update()
方法,用于执行数据触发更新之后,保存新的值和旧的值,将它返回给callback
回调函数
class Watcher{
// Dep发过来的通知,当前变量更新了,我们返回一个更新之后的回调函数
update(){
// this.value 由于还没触发更新,所以此时是旧的值
const oldValue=this.value;
// 通过我们的 getter 方法,直接获取最新的值
const newValue=this.get();
// 将新值和旧值返回给 callback 回调函数
this.callback(newValue,oldValue);
}
}
使用案例
let a={
b:{
c:{
d:10
}
}
}
observe(a);
new Watcher(a,'b.c.d',(val,oldValue)=>{
console.log('ok',val,oldValue);
})
a.b.c.d=55; // ok 55 10
在执行 a.b.c.d=55;
的同时,我们的控制台就会输出 ok 55 10 。
运行分析
observe(a)
- 首先,
observe(a)
会将 a 对象变为响应式对象
new Watcher
- 执行
new Watcher
之后,就会调用 Watcher 类的 constructor 。此时target
是 a ,expression
是 'b.c.d',callback
是(val,oldValue)=>{console.log('ok',val,oldValue); })
// target 目标对象
// expression 属性名
// callback 回调函数
// value 属性的值
constructor(target,expression,callback) {
this.target=target;
// parsePath 为一个高阶函数
this.getter=parsePath(expression);
this.callback=callback;
// get为我们之后要写的获取值的方法
this.value=this.get();
}
-
this.value=this.get()
又会执行get()
方法, 此时Dep.target
被赋值了,就是当前 Watcher 实例。
get(){
// 进入依赖收集阶段,将 Dep.target 设为 Watcher 实例本身
Dep.target=this;
// 当前对象
const obj=this.target;
let value;
// 当对象不再使用的时候,我们需要将它清空
try{
value=this.getter(obj)
}finally {
Dep.target=null;
}
this.value=value
return value;
}
-
value=this.getter(obj)
会触发defineReactive
中的get()
, 因为Dep.target
之前已经被赋值了,所以,现在有值,触发dep.depend
。
get(){
// Dep.target 是我们弄的唯一标识,当有这个标识的时候,添加依赖
if(Dep.target){
dep.depend(); // b 在这里触发
// 如果有子属性,也要将它加入依赖
if(childOb){
childOb.dep.depend(); // c d 在这里触发
}
}
return val;
},
- 将当前 Watcher 实例推进了
subs
数组中。
// 添加依赖
depend(){
// 判断当前是否有需要监听的目标,Dep.target 会被 Wacher 赋值
if(Dep.target){
// 将 Watcher 实例添加进 subs
this.subs.push(Dep.target)
}
}
a.b.c.d=55;
- 执行代码
a.b.c.d
触发defineReactive
中的set
方法,然后执行dep.notify();
set(newValue){
val=newValue;
childOb = observe(val)
// notify 切忌 val=newValue 之后,不然在 callback 回调中一直是旧值
dep.notify();
}
- 通过遍历
subs
列表,通知所有订阅者
// 通知所有订阅者
notify(){
// 浅克隆一份
const subs=this.subs.slice();
// 遍历
for(let i=0,l=subs.length;i<l;i++){
// 逐个更新
subs[i].update();
}
}
- 相应的订阅者执行
update()
,将新值和旧值获取,然后通过callback
回调函数返回
// Dep发过来的通知,当前变量更新了,我们返回一个更新之后的回调函数
update(){
// this.value 由于还没触发更新,所以此时是旧的值
const oldValue=this.value;
// 通过我们的 getter 方法,直接获取最新的值
const newValue=this.get();
// 将新值和旧值返回给 callback 回调函数
this.callback(newValue,oldValue);
}
- 最终
new Watcher
实例中的回调函数成功执行,并且成功拿到val
和oldValue
new Watcher(a,'b.c.d',(val,oldValue)=>{
console.log('ok',val,oldValue); // ok 10 5
})
所有代码
// 拷贝一份数组的原型
const arrayPrototype=Array.prototype;
// 以 Array.prototype 为原型创建 arrayMethods 对象
const arrayMethods=Object.create(arrayPrototype);
// 需要改写的数组方法列表
const methodsNeedChange=[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse',
]
for(let i=0;i<methodsNeedChange.length;i++){
// 备份原来的方法
const original=arrayMethods[methodsNeedChange[i]];
// 定义新的方法
def(arrayMethods,methodsNeedChange[i],function () {
// 用来保存新插入的值
let inserted=[];
// 由于 arguments 对象是类数组,所以先通过扩展运算符转为数组之后,再进行操作。
let args=[...arguments];
// 先判断 是否是 push shift splice ,如果是的话,先取出插入的新值,后面进行 observeArray
switch (methodsNeedChange[i]) {
case 'push':
case 'shift':
inserted=args;
break;
case ' ':
// splice(起始下标,删除个数,新添加的元素)
inserted=args.slice(2);
}
// 先判断 inserted 里面有东西,才执行 observeArray
inserted.length && observeArray(inserted);
// 将备份的方法进行执行,毕竟不能丢失数组方法原本的功能执行
original.apply(this,arguments)
// 写监听到之后更新视图
},false)
}
function defineReactive(obj,key,val) {
let dep=new Dep();
// eslint-disable-next-line no-unused-vars
let childOb;
// 判断当前入参个数,两个的话直接返回当前层的对象
if(arguments.length===2){
val=obj[key];
childOb = observe(val)
}
Object.defineProperty(obj,key,{
// 可枚举,默认为 false
enumerable:true,
// 属性的描述符能够被改变,或者是删除,默认为 false
configurable:true,
get(){
// Dep.target 是我们弄的唯一标识,当有这个标识的时候,添加依赖
if(Dep.target){
dep.depend();
// 如果有子属性,也要将它加入依赖
if(childOb){
childOb.dep.depend();
}
}
return val;
},
set(newValue){
val=newValue;
childOb = observe(val)
// notify 切忌 val=newValue 之后,不然在 callback 回调中一直是旧值
dep.notify();
}
})
}
function def(obj,key,value,enumerable) {
Object.defineProperty(obj,key,{
value,
//这个属性仅仅保存 Observer 实例,所以不需要遍历
enumerable
})
}
// 遍历对象当前层的所有属性,并且绑定 defineReactive
class Observer{
constructor(obj){
this.dep=new Dep();
def(obj,'__ob__',this,false)
if (Array.isArray(obj)){
// 遍历当前数组,给所有的元素绑定 observe 响应式
observeArray(obj)
// 将当前数组对象的原型链强行指向 arrayMethods
Object.setPrototypeOf(obj,arrayMethods);
}else{
this.walk(obj);
}
}
// 遍历对象的当前层的所有属性, 给他绑定 defineReactive 响应式
walk(obj){
let keys=Object.keys(obj);
for(let i =0;i<keys.length;i++){
defineReactive(obj,keys[i])
}
}
}
// 响应式的入口方法 ,主要用于先判断是否是对象 ,然后判断是否有 __ob__ 属性,没有的话,肯定没有 Observer 遍历过
function observe(value) {
// 判断传入的值是否是对象,不是对象直接返回,不进行后面的操作
if(typeof value !== 'object') return;
// 用来存储当前的 Observer 实例
let ob;
// 判定当前属性是否有 __ob__ ,并且该属性是否原型属于 Observer
// eslint-disable-next-line no-prototype-builtins
if(value.hasOwnProperty('__ob__') && value.__ob__ instanceof Observer){
ob=value.__ob__;
}else{
// 没有 __ob__ 属性代表没有遍历过,先执行 new Observer(value)
ob = new Observer(value);
}
return ob;
}
// 遍历数组,将他们 observe 进行响应式
function observeArray(list) {
for(let i=0,l=list.length;i<l;i++){
observe(list[i])
}
}
class Dep{
constructor() {
// 用数组存储自己的订阅者 subs 是 subscribes 订阅者的意思
// 这个数组里放的是 Watcher 的实例
this.subs=[]
}
// 添加依赖
depend(){
// 判断当前是否有需要监听的目标,Dep.target 会被 Wacher 赋值
if(Dep.target){
// 将 Watcher 实例添加进 subs
this.subs.push(Dep.target)
}
}
// 通知所有订阅者
notify(){
// 浅克隆一份
const subs=this.subs.slice();
// 遍历
for(let i=0,l=subs.length;i<l;i++){
// 逐个更新
subs[i].update();
}
}
}
class Watcher{
// target 目标对象
// expression 属性名
// callback 回调函数
// value 属性的值
constructor(target,expression,callback) {
this.target=target;
// parsePath 为一个高阶函数
this.getter=parsePath(expression);
this.callback=callback;
// get为我们之后要写的获取值的方法
this.value=this.get();
}
// Dep发过来的通知,当前变量更新了,我们返回一个更新之后的回调函数
update(){
// this.value 由于还没触发更新,所以此时是旧的值
const oldValue=this.value;
// 通过我们的 getter 方法,直接获取最新的值
const newValue=this.get();
// 将新值和旧值返回给 callback 回调函数
this.callback(newValue,oldValue);
}
// 获取当前的值,并将它更新,然后 return 返回
get(){
// 进入依赖收集阶段,将 Dep.target 设为 Watcher 实例本身
Dep.target=this;
// 当前对象
const obj=this.target;
let value;
// 当对象不再使用的时候,我们需要将它清空
try{
value=this.getter(obj)
}finally {
Dep.target=null;
}
this.value=value
return value;
}
}
function parsePath(str){
let segments = str.split('.');
return obj=> {
for(let i=0;i<segments.length;i++){
if(!obj) return;
obj=obj[segments[i]];
}
return obj;
}
}
let a={
b:{
c:{
d:10
}
}
}
observe(a)
new Watcher(a,'b.c.d',(val,oldValue)=>{
console.log('ok',val,oldValue); // ok 10 5
})
a.b.c.d=55;