本篇文章借鉴自《深入浅出 vue.js》,根据该书中变化侦测的内容,对 vue 变化侦测的原理进行了文本梳理和理解
# 变化侦测
什么是变化侦测?
页面在渲染的过程中,状态发生不同的变化,变化侦测就是这样的一个存在,确定在渲染过程中状态发生了什么。
对于 Angulars 和 React,他们变化侦测的方式更像是__“拉”__。
什么叫拉呢? 我觉得吧,就像是一棵苹果树,摘苹果的时候,你从树顶上去 " 拉 “他,从最上面开始摘,顺着树枝的节点,依次向下摘。
Angular 和 React 的状态发生变化的时候,他们都不知道是谁突然变化 ,没有办法只好 通过” 拉 “的方式,通过暴力比对的方式来查找。 在 Angular 里,他选择了脏检查的办法,而在 React 他选择虚拟 DOM 的方法,二者都是在 **“拉”** 的基础上进行。
但我今天看到书是《深入浅出 Vue.js》,说明 Vue 和以上二者都不太一样,他选择的是一种 **“推”** 的方法。
那么什么叫 “推”?同样和苹果数一样,“推” 就是把从下往上,一个苹果被摘了,我们就顺着这枝桠向上进行摘取,知道把这个枝桠的苹果摘完。
在 Vue 里,当状态发生改变的时候,vue 就马上知道了,并且在一定程度上,知道了,是哪些状态发生了变化。这样的状态变更信息他知道的更多之后,他就可以进行细粒度的更新了
__什么叫细粒度的更新?__就是:假如说有一个状态绑定了好多个依赖,每一个依赖表示一个具体的 DOM 节点,那么当这个状态发生改变的时候,对这个状态的所有依赖发出通知,让他们进行 DOM 的更新操作。
# 如何追踪变化?
作者说,学过 js 的都知道有两种方法都知道,侦测变化的方法有两种:
- Object.definProperty
- ES6 的 Proxy
我好惭愧,我不知道。。。(i am so vegetable), 但还好,我会百度和 google。
废话不多说,直接讲原理:
function defineReactive(data, key, val) { | |
Object.defineProperty(data, key, { | |
enumerable: true,// 可枚举嘛? | |
configurable: true,// 可配置嘛? | |
get: function () { | |
return val; | |
}, | |
set: function (newVal) { | |
if (val === newVal) { | |
return; | |
} | |
val = newVal; | |
} | |
}) | |
} |
defineProperty(data,key,descriptor)
干什么的?侦测对象变化!(扣题欸!!)
defineReactive(data,key,val)
干嘛的?一看就是定义一个响应式数据嘛~
把函数的存取 val
的方式封装是为什么?我觉得吧,是因为,在你取一个值的时候,存一个值的时候会有耦合,通过上述代码,会更清晰明确一点(反正我是这样想的)。
# 如何收集依赖?
如果只是讲封装的 defineReactive(data,key,val)
,那明显不清楚我们侦测的原理是什么嘛,我们更在意的是 ------- 这么做到的?怎么收集到依赖的?
因为我们观察数据,目的是为了,当数据发生变化的时候,可以通知哪些用过他的地方,进行修改。
__所以收集依赖的核心是什么?__就是把使用过数据的地方都收集起来,他们怎么使用的?-- 通过 getter 对咩? 那就好说啦~。简单来说就是:通过 getter 收集依赖,然后再通过 setter 依次循环依赖,再触发一遍
# 依赖收集在哪?
首先我们想到,每个 key(属性)都有自己的值,这个值,会别很多人(依赖)取来用,那么这些个依赖的集合,是专属于当前 key(属性)的,那么我们可以大胆猜想,是不是存在一个数组 dep[]
专门来收集这个属性的依赖,且这些个依赖的集合 dep 被存在 window.target
上面呢?
function definedReactive(data, key, val) { | |
let dep = [];//******** * 新增数组 dep 用来存储被收集的依赖 / | |
Object.defineProperty(data, key, { | |
enumerable: true, | |
configurable: true, | |
get: function () { | |
dep.push(window.target);//******* */ | |
return val; | |
}, | |
set: function (newVal) { | |
if (val === newVal) { | |
return; | |
} | |
for (let i = 0; i < dep.length; i++) { | |
dep[i](newVal, val); | |
//set 被触发的时,循环 dep,触发之前收集到的依赖,进行数据修改 | |
}//******* */ | |
val = newVal; | |
} | |
}) | |
} |
但这样写,还是耦合了,我们试试看,写一个专门的类,叫做 Dep
, 专门为我们来管理依赖们
export default class Dep{ | |
constructor(){ | |
this.sub = []; | |
} | |
addSub(subb){ | |
this.sub.push(subb); | |
} | |
removeSub(subbb){ | |
remove(this.sub,subbb); | |
} | |
depend(){ | |
if(window.target){ | |
this.addSub(window.target); | |
} | |
} | |
// 通知:大家伙们~,数据改啦!! | |
notify(){ | |
const subs = this.sub.slice(); | |
for(let i = 0,l = subs.length;i<l;i++){ | |
subs[i].update(); | |
} | |
} | |
} | |
function remove(arr,item){ | |
if(arr.length){ | |
const index = arr.indexOf(item); | |
if(index =-1){ | |
return arr.splice(index,1); | |
} | |
} | |
} |
于是之前的 definReactive
变成
function definedReactive(data, key, val) { | |
let dep = Dep();///***** */ | |
Object.defineProperty(data, key, { | |
enumerable: true, | |
configurable: true, | |
get: function () { | |
return val; | |
dep.depend();//**** */ | |
}, | |
set: function (newVal) { | |
if (val === newVal) { | |
return; | |
} | |
dep.notify();//***** */ | |
val = newVal; | |
} | |
}) | |
} |
这样就回答了 :依赖在哪?------Dep 里(扣题~)
# 依赖是谁?
在上面的代码里,我们收集的依赖是 window.target
,那么他到底长什么样?我们到底要收集谁呢?
收集谁?归根结底,就是,你状态发生改变,你要通知谁?
通知的东西就很多啦啊?有可能是模板,或者说,用户自己写的一个 watch,这些个东西太多了,所以我们就想,是不是要写一个类来管理,这个类用来通知这些对象。这样的话,我们收集依赖的时候,只需要把这个类引进来,然后他来通知其他地方。
我们姑且叫这个类为 Watcher
所以依赖是谁?----Watcher
# 什么是 Watcher?
watcher 是一个中介角色,数据发生变化的时候,通知他,然后他在通知其他地方。
// 关于 watcher 的例子 | |
vm.$watch('a.b.c',function(newVal,oldVal){ | |
// 做点什么 | |
}) | |
// 这段代码表示当某个 data 的 'a.b.c' 属性发生改变的时候,触发第二个函数 |
我们思考一下是怎么实现这个功能的,可以猜测,是不是,把 watcher
的实例添加到 a.b.c
属性里,然后属性里值变化时通知 watcher
, 然后他再通知给别人。
export default class Watcher{ | |
constructor (vm,expOrFn,cb){ | |
this.vm = vm ; | |
// 执行 this.getter (), 就可以读取 “data.a.b.c” 里面的内容 | |
this.getter = parsePath(expOrFn); | |
this.cb = cb; | |
this.value = this.get(); | |
} | |
get(){ | |
window.target = this; | |
let value = this.getter.call(this.vm,this.vm); | |
window.target = undefined; | |
return value; | |
} | |
update(){ | |
const oldValue = this.value; | |
this.value = this.get(); | |
this.cb.call(this.vm,this.value,oldValue) | |
} | |
} |
这段代码可以把自己加进 data.a.b.c
的 Dep 中去
为什么?
因为,在 get
方法里,我把 window.target = this,
,也就是说:this 所指的当前 watcher 实例(也就是依赖)被我变成了 window.target
然后在读取 data.a.b.c 属性的值的时候,我们肯定会触发 getter(),触发 getter,说明触发了收集依赖的逻辑,关于收集依赖的逻辑,我们上述说过,会在 window.target
里面读取一个依赖并添加到 dep
里。
这就导致,只要 window.target = this
然后在读取一下值,就会主动把这个 this(watcher 实例 ===》依赖)添加到 dep 收集依赖的数组里。
依赖注入到 dep
里面后,每当 data.a.b.c
的值发生了变化时,就会让依赖列表的所有依赖循环触发,自己所含有的 update()方法。
# 递归所有的 key(属性)
上述讲的只是怎么实现一个属性的变化侦测。
但我们希望把数据里面的所有属性(包括子属性)都侦测到,所以需要封装一个 Observe 类,他的任务时,把一个数据里面的所有属性都转化成 getter/setter 的形式。
/** | |
* Observer 会附加在每一个被侦测的 Object 对象上 | |
* 一旦被附加,Observe 会把 Object 的所有属性转换成 getter/setter | |
* 来收集属性的依赖,并且当属性发生变化时,会通知这些依赖 | |
*/ | |
export class Observer { | |
constructor(value) { | |
this.value = value; | |
if (!Array.isArray(value)) { | |
this.walk(value); | |
} | |
} | |
/** | |
* walk () 会将每一个属性转换成 setter/getter 的行式来侦测变化 | |
* 这个方法只有在数据类型为 Object 时调用 | |
*/ | |
walk(obj) { | |
const keys = Object.keys(obj);// 一个表示给定对象的所有可枚举属性的字符串数组。 | |
for (let i = 0; i < keys.length; i++) { | |
definedReactive(obj, keys[i], obj[keys[i]]); | |
} | |
} | |
} | |
function definedReactive(data, key, val) { | |
if (typeof val === 'object') { | |
new Observer(val); | |
} | |
let dep = new Dep(); | |
Object.defineProperty(data, key, { | |
enumerable: true, | |
configurable: true, | |
get: function () { | |
dep.depend(); | |
return val; | |
}, | |
set: function () { | |
if (val === newVal) { | |
return | |
} | |
val = newVal; | |
dep.notify(); | |
} | |
}) | |
} |
# 关于 Object 的问题
前面介绍了 Object 类型的变化侦测原理,了解了数据的变化是通过 getter/setter 来追踪的,
但也正是由于这种方式,有些语法里即使是数据发生了变化,vue 也追踪不到
var vm = new Vue({ | |
el:'#el', | |
tempalte:'#demo-template', | |
methods:{ | |
action:function(){ | |
this.obj.name = "awsl"; | |
} | |
}, | |
data:{ | |
obj:{} | |
} | |
})//vue 无法侦察 |
或者说
var vm = new Vue({ | |
el:'#el', | |
tempalte:'#demo-template', | |
methods:{ | |
action:function(){ | |
delete this.obj.name; | |
} | |
}, | |
data:{ | |
obj:{name:'awsl'} | |
} | |
})//vue 无法侦察 |
vue 都无法察觉数据变化
# 总结
- 变化侦测就是侦测数据的变化
- Object 可以通过
Object.definProperty
将属性转换成 settet/getter 的行式,读取数据的时候触发 getter,存储数据 的时候触发 setter; - Dep 收集依赖,Watcher 就是依赖,数据变化,通知他。然后遍历 Dep 里的依赖,实现其他依赖的数据更新。