vue如何实现数据双向绑定?

前置知识

MVVM

MVVM

MVVM是前端视图层的分层开发思想。它把页面分成了M、V和VM。其中,VM是MVVM思想的核心;因为VM是M和V之间的调度者。M 是指数据层,V 是指视图层。

MVVM 框架实现了双向绑定,减少通过操纵 DOM 去更新视图。
通过ViewModel 对 Model 层 获取到的数据进行处理,展现到 View 层上。
这就解耦了 View 层和 Model 层,是前后端分离方案实施的重要一环。

发布/订阅设计模式

observer

首先先来看了解一下观察者模式。
观察者模式:一个被称作被观察者的对象,维护一组被称为观察者的对象,这些对象依赖于被观察者,被观察者自动将自身的状态的任何变化通知给它们。
它有几个重要的部分:

  • 被观察者:维护一组观察者, 提供用于增加和移除观察者的方法。
  • 观察者:提供一个更新接口,用于当被观察者状态变化时,得到通知。
  • 具体的被观察者:状态变化时广播通知给观察者,保持具体的观察者的信息。
  • 具体的观察者:保持一个指向具体被观察者的引用,实现一个更新接口,用于观察,以便保证自身状态总是和被观察者状态一致的。

发布/订阅模式

观察者模式确实很有用,但是在javascript实践里面,通常我们使用一种叫做发布/订阅模式的变体来实现观察者模式。

从图中也能看到,这两种模式很相似,但是也有一些值得注意的不同。

发布/订阅模式使用一个主题/事件频道,这个频道处于想要获取通知的订阅者和发起事件的发布者之间。这个事件系统允许代码定义应用相关的事件,这个事件可以传递特殊的参数,参数中包含有订阅者所需要的值。

观察者模式和发布订阅模式的不同点:

  1. 观察者模式要求想要接受相关通知的观察者必须到发起这个事件的被观察者上注册这个事件。

  2. 发布/订阅模式使用一个主题/事件频道(类似于中介/中间商),可以减少订阅者和发布者之间的依赖性。

  3. 发布/订阅模式中订阅者可以实现一个合适的事件处理函数,用于注册和接受由发布者广播的相关通知。

这两种模式的优缺点

优点:观察者和发布/订阅模式鼓励人们认真考虑应用不同部分之间的关系,同时帮助我们找出这样的层,该层中包含有直接的关系,这些关系可以通过一些列的观察者和被观察者来替换掉。这种方式可以有效地将一个应用程序切割成小块,这些小块耦合度低,从而改善代码的管理,以及用于潜在的代码复用。

使用观察者模式更深层次的动机是,当我们需要维护相关对象的一致性的时候,我们可以避免对象之间的紧密耦合。例如,一个对象可以通知另外一个对象,而不需要知道这个对象的信息。

两种模式下,观察者和被观察者之间都可以存在动态关系。这提供很好的灵活性,而当我们的应用中不同的部分之间紧密耦合的时候,是很难实现这种灵活性的。

然而,正是由于这些优点,这种模式也暴露出一些缺点:

在发布/订阅模式中,将发布者共订阅者上解耦,将会在一些情况下,导致很难确保我们应用中的特定部分按照我们预期的那样正常工作。

例如,发布者可以假设有一个或者多个订阅者正在监听它们。比如我们基于这样的假设,在某些应用处理过程中来记录或者输出错误日志。如果订阅者执行日志功能崩溃了(或者因为某些原因不能正常工作),因为系统本身的解耦本质,发布者没有办法感知到这些事情。

订阅者对彼此之间存在没有感知,对切换发布者的代价无从得知。因为订阅者和发布者之间的动态关系,更新依赖也很能去追踪。

以上知识可以帮助我们理解vue响应式的实现,接下来让我们正式进入正题吧。

vue 实现数据双向绑定

我们会通过实现以下几个部分,来实现数据的双向绑定,这里暂时不考虑对数组的监听,在接下来的讲解中会给出解释,等到下一章节,会对这部分内容进行重点分析。

  1. 实现一个监听器 Observer ,用来劫持并监听所有属性,如果属性发生变化,就通知订阅者;
  2. 实现一个订阅器 Dep,这个对象用来存放 Watcher 对象的实例,对监听器 Observer 和 订阅者 Watcher 进行统一管理;
  3. 实现一个观察者 Watcher,可以收到属性的变化通知并执行相应的方法,从而更新视图;

监听器 Observer

  1. 首先我们定义一个 cb 函数,这个函数用来模拟视图更新,调用它即代表更新视图,内部可以是一些更新视图的方法。

  2. 然后我们定义一个 defineReactive ,这个方法通过 Object.defineProperty 来实现对对象的「响应式」化,

  • 参数obj(需要绑定的对象)、key(obj的某一个属性),val(具体的值)。

  • 经过 defineReactive 处理以后,obj 的 key 属性在「读」的时候会触发 reactiveGetter 方法,

  • 而在该属性被「写」的时候则会触发 reactiveSetter 方法。

  1. 还需要在上面再封装一层 observer。

这个函数传入一个 obj(需要「响应式」化的对象),通过遍历所有属性的方式对该对象的每一个属性都通过 defineReactive 处理。(注:实际上 observer 会进行递归调用,为了便于理解去掉了递归的过程)

function cb (val) {
    /* 渲染视图 */
    console.log("视图更新啦~");
}

function defineReactive (obj, key, val) {
    Object.defineProperty(obj, key, {
        enumerable: true,       /* 属性可枚举 */
        configurable: true,     /* 属性可被修改或删除 */
        get: function reactiveGetter () {
            return val;         /* 实际上会依赖收集,下一小节会讲 */
        },
        set: function reactiveSetter (newVal) {
            if (newVal === val) return;
			val = newVal;
            cb(newVal);
        }
    });
}

function observer (obj) {
    if (!obj || (typeof obj !== 'object')) {
        return;
    }

    Object.keys(obj).forEach((key) => {
        defineReactive(obj, key, obj[key]);
    });
}
  1. 最后,可以用 observer 来封装一个 Vue 。

在 Vue 的构造函数中,对 options 的 data 进行处理,这里的 data 就是平时我们在写 Vue 项目时组件中的 data 属性(实际上是一个函数,这里当作一个对象来简单处理)。

  1. 这样我们只要 new 一个 Vue 对象,就会将 data 中的数据进行「响应式」化。

如果我们对 data 的属性进行下面的操作,就会触发 cb 方法更新视图。

class Vue {
    /* Vue构造类 */
    constructor(options) {
        this._data = options.data;
        observer(this._data);
    }
}
let o = new Vue({
    data: {
        test: "I am test."
    }
});
o._data.test = "hello,world.";  /* 视图更新啦~ */

订阅者 Dep

实现一个订阅者 Dep ,它的主要作用是用来存放 Watcher 观察者对象。

class Dep {
    constructor () {
        /* 用来存放Watcher对象的数组 */
        this.subs = [];
    }

    /* 在subs中添加一个Watcher对象 */
    addSub (sub) {
        this.subs.push(sub);
    }

    /* 通知所有Watcher对象更新视图 */
    notify () {
        this.subs.forEach((sub) => {
            sub.update();
        })
    }
}

为了便于理解我们只实现了添加的部分代码,主要是两件事情:

  1. 用 addSub 方法可以在目前的 Dep 对象中增加一个 Watcher 的订阅操作;
  2. 用 notify 方法通知目前 Dep 对象的 subs 中的所有 Watcher 对象触发更新操作。

观察者 Watcher

class Watcher {
    constructor () {
        /* 在new一个Watcher对象时将该对象赋值给Dep.target,在get中会用到 */
        Dep.target = this;
    }

    /* 更新视图的方法 */
    update () {
        console.log("视图更新啦~");
    }
}

Dep.target = null;

依赖收集

接下来我们修改一下 defineReactive 以及 Vue 的构造函数,来完成依赖收集。

我们在闭包中增加了一个 Dep 类的对象,用来收集 Watcher 对象。

在对象被「读」的时候,会触发 reactiveGetter 函数把当前的 Watcher 对象(存放在 Dep.target 中)收集到 Dep 类中去。

之后如果当该对象被「写」的时候,则会触发 reactiveSetter 方法,通知 Dep 类调用 notify 来触发所有 Watcher 对象的 update 方法更新对应视图。

function defineReactive (obj, key, val) {
    /* 一个Dep类对象 */
    const dep = new Dep();

    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
            /* 将Dep.target(即当前的Watcher对象存入dep的subs中) */
            dep.addSub(Dep.target);
            return val;         
        },
        set: function reactiveSetter (newVal) {
            if (newVal === val) return;
            /* 在set的时候触发dep的notify来通知所有的Watcher对象更新视图 */
            dep.notify();
        }
    });
}

class Vue {
    constructor(options) {
        this._data = options.data;
        observer(this._data);
        /* 新建一个Watcher观察者对象,这时候Dep.target会指向这个Watcher对象 */
        new Watcher();
        /* 在这里模拟render的过程,为了触发test属性的get函数 */
        console.log('render~', this._data.test);
    }
}

完整的代码及说明

class Dep {
	constructor() {
		/* 用来存放Watcher对象的数组 */
		this.subs = [];
	}

	/* 在subs中添加一个Watcher对象 */
	addSub(sub) {
		this.subs.push(sub);
	}

	/* 通知所有Watcher对象更新视图 */
	notify() {
		this.subs.forEach((sub) => {
			sub.update();
		})
	}
}
class Watcher {
	constructor() {
		/* 在new一个Watcher对象时将该对象赋值给Dep.target,在get中会用到 */
		Dep.target = this;
	}

	/* 更新视图的方法 */
	update() {
		console.log("视图更新啦~");
	}
}

Dep.target = null;

function defineReactive(obj, key, val) {
	/* 一个Dep类对象 */
	const dep = new Dep();

	Object.defineProperty(obj, key, {
		enumerable: true,
		configurable: true,
		get: function reactiveGetter() {
			/* 将Dep.target(即当前的Watcher对象存入dep的subs中) */
			dep.addSub(Dep.target);
			return val;
		},
		set: function reactiveSetter(newVal) {
			if (newVal === val) return;
			
			// 设置新值
			// 注意,val 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
			val = newVal
			
			/* 在set的时候触发dep的notify来通知所有的Watcher对象更新视图 */
			dep.notify();
		}
	});
}

function observer(obj) {
	if (!obj || typeof obj !== 'object') {
		return
	}
	Object.keys(obj).forEach((key) => {
		defineReactive(obj, key, obj[key])
	})
}
class Vue {
	constructor(options) {
		this._data = options.data;
		observer(this._data);
		/* 新建一个Watcher观察者对象,这时候Dep.target会指向这个Watcher对象 */
		new Watcher();
		/* 在这里模拟render的过程,为了触发test属性的get函数 */
		console.log('render~', this._data.test);
	}
}
let o = new Vue({
	data: {
		test: 'I am a test'
	}
})

vue

首先在 observer 的过程中会注册 get 方法,该方法用来进行「依赖收集」。

在它的闭包中会有一个 Dep 对象,这个对象用来存放 Watcher 对象的实例。

其实「依赖收集」的过程就是把 Watcher 实例存放到对应的 Dep 对象中去。

get 方法可以让当前的 Watcher 对象(Dep.target)存放到它的 subs 中(addSub)方法,

在数据变化时,set 会调用 Dep 对象的 notify 方法通知它内部所有的 Watcher 对象进行视图更新。

这是 Object.defineProperty 的 set/get 方法处理的事情,那么「依赖收集」的前提条件还有两个:

  1. 触发 get 方法;
  2. 新建一个 Watcher 对象。

这个我们在 Vue 的构造类中处理。

新建一个 Watcher 对象只需要 new 出来,这时候 Dep.target 已经指向了这个 new 出来的 Watcher 对象来。

而触发 get 方法也很简单,实际上只要把 render function 进行渲染,那么其中的依赖的对象都会被「读取」,

这里我们通过打印来模拟这个过程,读取 test 来触发 get 进行「依赖收集」。

vue 如何深度监听data变化

Object.defineProperty的缺点

  • 深度监听,需要递归到底,一次性计算量大
  • 无法监听新增属性/删除属性(需要 Vue.set Vue.delete)
  • 无法原生监听数组,需要特殊处理
class Dep {
	constructor() {
		/* 用来存放Watcher对象的数组 */
		this.subs = [];
	}

	/* 在subs中添加一个Watcher对象 */
	addSub(sub) {
		this.subs.push(sub);
	}

	/* 通知所有Watcher对象更新视图 */
	notify() {
		this.subs.forEach((sub) => {
			sub.update();
		})
	}
}
class Watcher {
	constructor() {
		/* 在new一个Watcher对象时将该对象赋值给Dep.target,在get中会用到 */
		Dep.target = this;
	}

	/* 更新视图的方法 */
	update() {
		console.log("视图更新啦~");
	}
}

Dep.target = null;

function defineReactive(obj, key, val) {
	/* 一个Dep类对象 */
	const dep = new Dep();
	
	// 深度监听
	observer(val)
	
	Object.defineProperty(obj, key, {
		enumerable: true,
		configurable: true,
		get: function reactiveGetter() {
			/* 将Dep.target(即当前的Watcher对象存入dep的subs中) */
			dep.addSub(Dep.target);
			return val;
		},
		set: function reactiveSetter(newVal) {
			if (newVal === val) return;
			
			// 深度监听
			observer(newVal)
			
			// 设置新值
			// 注意,val 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
			val = newVal
			
			/* 在set的时候触发dep的notify来通知所有的Watcher对象更新视图 */
			dep.notify();
		}
	});
}

function observer(obj) {
	if (!obj || typeof obj !== 'object') {
		return
	}
	Object.keys(obj).forEach((key) => {
		defineReactive(obj, key, obj[key])
	})
}
class Vue {
	constructor(options) {
		this._data = options.data;
		observer(this._data);
		/* 新建一个Watcher观察者对象,这时候Dep.target会指向这个Watcher对象 */
		new Watcher();
		
		// 在这里模拟render的过程
		// 测试 深度监听
		console.log('深度监听', this._data.info.address);
	}
}
let o = new Vue({
	data: {
		name: 'zhangsan',
		age: 20,
		test: 'I am a test',
		info: {
		    address: '北京' // 需要深度监听
		}
	}
})

// 测试 深度监听
o._data.info.address = '杭州';
console.log(o._data.info.address);

vue 如何监听数组变化

Vue 的 Observer 对数组做了单独的处理,对数组的方法进行编译,并赋值给数组属性的 proto 属性上,因为原型链的机制,找到对应的方法就不会继续往上找了。编译方法中会对一些会增加索引的方法(push,unshift,splice)进行手动 observe。

class Dep {
	constructor() {
		/* 用来存放Watcher对象的数组 */
		this.subs = [];
	}

	/* 在subs中添加一个Watcher对象 */
	addSub(sub) {
		this.subs.push(sub);
	}

	/* 通知所有Watcher对象更新视图 */
	notify() {
		this.subs.forEach((sub) => {
			sub.update();
		})
	}
}
class Watcher {
	constructor() {
		/* 在new一个Watcher对象时将该对象赋值给Dep.target,在get中会用到 */
		Dep.target = this;
	}

	/* 更新视图的方法 */
	update() {
		console.log("视图更新啦~");
	}
}

Dep.target = null;

// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向 oldArrayProperty ,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
	arrProto[methodName] = function() {
		console.log('数组相关更新')
		oldArrayProperty[methodName].call(this, ...arguments)
		// Array.prototype.push.call(this, ...arguments)
	}
})

function defineReactive(obj, key, val) {
	/* 一个Dep类对象 */
	const dep = new Dep();

	// 深度监听
	observer(val)

	Object.defineProperty(obj, key, {
		enumerable: true,
		configurable: true,
		get: function reactiveGetter() {
			/* 将Dep.target(即当前的Watcher对象存入dep的subs中) */
			dep.addSub(Dep.target);
			return val;
		},
		set: function reactiveSetter(newVal) {
			if (newVal === val) return;

			// 深度监听
			observer(newVal)

			// 设置新值
			// 注意,val 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
			val = newVal

			/* 在set的时候触发dep的notify来通知所有的Watcher对象更新视图 */
			dep.notify();
		}
	});
}

function observer(obj) {
	if (!obj || typeof obj !== 'object') {
		return
	}
	// 判断 如果是 数组
	if (Array.isArray(obj)) {
		obj.__proto__ = arrProto
	}

	Object.keys(obj).forEach((key) => {
		defineReactive(obj, key, obj[key])
	})
}
class Vue {
	constructor(options) {
		this._data = options.data;
		observer(this._data);
		/* 新建一个Watcher观察者对象,这时候Dep.target会指向这个Watcher对象 */
		new Watcher();


	}
}
let o = new Vue({
	data: {
		name: 'zhangsan',
		age: 20,
		test: 'I am a test',
		info: {
			address: '北京' // 需要深度监听
		},
		nums: [10, 20, 30]
	}
})

// 测试 监听数组
o._data.nums.push(7)
console.log('数组监听', o._data.nums);

参考文章:

JavaScript 观察者模式-W3C

0 到 1 掌握:Vue 核心之数据双向绑定

剖析Vue原理&实现双向绑定MVVM

剖析 Vue.js 内部运行机制

posted @ 2020-03-27 17:32  Chrislinlin  阅读(3318)  评论(0编辑  收藏  举报