前言:
关于Vue2.0的双向数据绑定,会大致分为三个部分来讲解,分别是Vue对 对象 的监听,Vue对数组的监听以及和Vue双向数据绑定相关的API的分析。
希望大家在看这篇文章的时候,先下载一份源码,然后一边看文章,一边对照源码,这样可以建立一个比较清晰的架构出来。当然,文章里也是会附上源码分析的。
Vue源码下载:https://github.com/vuejs/vue
看源码的时候,直接上src文件夹下找就ok啦。
我下面来开始吧,这部分先讲解Vue对对象{}是怎么进行双向数据绑定的。
双向数据绑定主要是指:数据变化更新视图,视图变化更新数据,如下图所示:
双向数据绑定的核心就是:数据劫持 + 发布订阅模式。
下面就是讲对对象的数据劫持 + 发布订阅模式(Dep + Watcher)。
那么,我们怎么知道数据(当然,这里的数据指的是我们的对象{})发生变化了呢?
有两种方法:
一种是使用Object.definedProperty()来实现对数据的变化的监听
一种是使用Proxy
在Vue2.0时,是使用**Object.defineProperty()**来实现对对象的变化监听的,因为在Vue2.0开发时,浏览器对ES6的支持不理想,现在的Vue3.0时Proxy来实现的。
所以,我们可以写出如下代码来对对象进行一个数据的监听:
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function() {
return val
},
set: function(newVal) {
if(val === newVal) {
return;
}
val = newVal;
}
})
}
这个函数就是对Object.defineProperty()进行了简单的封装。使用这个函数来进行监听的对象里面的属性,如果这个属性被读取时,就会触发get函数;如果这个属性被设置值的话,就会触发set函数。
完成了对对象的监听,然后要干嘛呢?我们是不是可以触发get函数或者是set函数时,在这两个函数里添加一些操作呢?可以的。
下面要完成的对依赖的收集了,可能大家会问为什么啊?
为什么要进行依赖的收集?
怎么收集呢?
什么是依赖?
下面就来解决大家的疑惑:
1.为什么要进行依赖的收集?
我们要明确,我们观察数据的目的是:当数据发生变化的时候,可以通知到那些使用了这个数据(指:监听的对象上的某个属性)的组件,然后组件内部再通过虚拟DOM来进行重新渲染。
所以,我们要先进行依赖的收集,把那些使用到这个数据的组件给收集起来,然后等数据发生变化的时候,把之前收集好的依赖循环“通知”一遍就好了。
2.怎么收集依赖呢?
我们把上面的代码改一下:
我们添加了dep数组来存储我们的依赖,在get函数里面收集依赖,在set函数里面触发依赖。
function defineReactive(obj, key, val) {
let dep = []; // 新增 : 用来存储我们的依赖
Object.defineProperty(obj, 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);
}
val = newVal;
}
})
}
当然,我们的代码不能这样写,这样代码就耦合啦。应该把依赖管理和数据监听的功能代码给分开:
依赖管理:
export default class Dep {
constructor () {
this.subs = []
}
addSub (sub) { // 添加依赖
this.subs.push(sub)
}
removeSub (sub) { // 删除依赖
remove(this.subs, sub)
}
depend () { // 添加依赖
if (window.target) {
this.addSub(window.target)
}
}
notify () { // 触发更新
const subs = this.subs.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);
}
}
}
数据监听:
function defineReactive(obj, key, val) {
let dep = new Dep(); // 修改
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function() {
dep.depend() // 修改
return val
},
set: function(newVal) {
if(val === newVal) {
return;
}
val = newVal;
dep.notify() // 修改
}
})
}
这样,我们就完成了对数据的监听和对依赖的管理了。
那么,依赖是什么呢?
依赖就是当我们的属性发生变化的时候,我们要通知谁,这个谁就是依赖。
在Vue源码中,这个依赖就是一个Watcher实例,数据发生改变的时候就会通知这个实例,然后再它通知其他的地方。
Watcher构造函数的代码如下:
export default class Watcher{
constructor (vm, exOrFn, cb) {
this.vm = vm;
// 如果执行this.getter函数,就可以获取到exOrFn对应于对象上属性的值
this.getter = parsePath(exOrFn);
this.cb = cb;
this.value = this.get();
}
get() {
// 把当前Watcher实例(就是这个this),赋值到window.target上,等待触发getter函数,进行依赖收集
window.target = this;
// 获取obj上某个属性的值
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);
}
}
//下面代码功能: 解析简单路径
const bailRE = /[^\w.$]/;
export 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];
}
return obj;
}
}
我们使用构造函数的时候,首先会进行 new Watcher()来创建一个Watcher实例,会获取到this.getter方法,然后触发this.get方法执行。
this.get函数执行时,会把当前的Watcher实例设置到window.target上;然后触发this.getter函数,去读取我们exOrFn这个参数对应的属性值,因为时读取操作,所以会触发get函数,进行依赖收集(这个在上面介绍过)。这个样就完成了一次依赖的收集,我当前的Watcher实例收集到了某个属性的dep数组里面。
每当属性的值发生变化的话,就会让依赖列表(就是dep数组)中的所有依赖循环触发update方法(就是Watcher实例的update方法),update函数会执行参数中的回调函数(这里的回调是创建Watcher实例传进去的)。
上面的功能就可以实现对对象进行一个基本的变化监听,但是,这还不够,我们需要的是监听所有的对象中的属性(包括子属性),所以,我们需要封装一个Observer类。这个类可以帮我们把对象里面的属性(包括子属性)都转换为get函数和set函数的形式,然后我们就可以去追踪这些属性的变化了。
Observer类代码如下:
export class Observer {
constructor (value) {
this.value = value
if (!Array.isArray(value)) {
this.walk(value)
}
}
/**
* 功能:
*迭代对象的每一个属性,然后将这些属性转换为get函数和set函数
*/
walk (obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
}
export function defineReactive (obj, key, val) {
if(typeof val === 'object') { // 新增 递归,进行深度的监听(可以类比到深拷贝)
new Observe(val)
}
let dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function () {
dep.depend();
return value
},
set: function (newVal) {
if(val === newVal) reurn;
val = newVal
dep.notify();
}
})
}
上面代码不难理解,就是先进行了类型判断,只有对象才可以调用walk函数,将每一个属性转成get/set函数来监听变化。
在defineReactive中新增new Observer(val)来递归子属性,这样,我们就可以把对象中的所有属性(包括子属性)都转换为get/set函数的形式。
这样就完成了对对象的变化监听了。(很开心不??)
但是,
Vue使用Object.defienProperty()来监听一个对象的变化真的好吗?答案是肯定不太好的。
第一点:它只能监听到属性的读取操作和修改操作(即设置值的操);对于一个属性被删除了,被遍历了等其他的操作,它检测不到。
第二点:对于新增的的属性和被删除掉的属性,无法追踪到,这样的变化没办法给依赖发通知。
为了解决新增属性和被删除的属性没办法被追踪到,Vue提供了两个API----vm.
s
e
t
与
v
m
.
set 与 vm.
set与vm.delete,在第三部分,我们会来分析它们的原理。
源码解析部分:
下面,我们来看看源码是怎么写的,实际上,上面代码基本上就是源码中对一个对象的变化侦测的核心代码了。
(我会给出图片,框出对应的文件夹和代码部分,没有框出来的部分,不是文章应对的内容。
但是还需要大家自己到源码上找对于得部分阅读,因为我们这样主要是为了帮助大家理解,源码这个东西是需要结合上下文来理解的,要从宏观上先理解其原理,然后在深入细节研究)
首先的对我们new Vue({data:{}})里data属性进行观察:
调用new Observer(val)来监测我们对象的所有属性(包括子属下)
调用walk函数
定义的defineReactive函数,创建一个dep实例,来管理我们的依赖
数据劫持:get函数收集依赖,set函数触发依赖通知
dep管理依赖的构造函数
Watcher的更新操作
参考:
《深入浅出Vue.js》 + Vue源码
最后,希望这篇文章可以帮助呢理解Vue的双向数据绑定, 如果有错误的地方,希望大家指出,欢迎在评论区留言;如果喜欢,可能关注收藏点赞,谢谢。
我们一起成长。