vue响应式的实现主要是通过数据拦截(拦截数据的set和get)以及发布者订阅者模式。主要有observer,dep,watcher这几个模块。首先创建vue实例时,会传入一个对象,内部包含el,data, method, computed等属性。
observer是咋来的呢?
observer主要是进行数据拦截,用defineProperty递归拦截传入对象的data属性的set和get,以确保每个对象的set,get都被拦截。
observer.js代码:
export class Observer {
constructor(obj) {
this.obj = obj;
this.transform(obj);
}
// 将 obj 里的所有层级的 key 都用 defineProperty 重新定义一遍, 使之 reactive
transform(obj) {
const _this = this;
for (let key in obj) {
const value = obj[key];
makeItReactive(obj, key, value);
}
}
}
function makeItReactive(obj, key, val) {
// 如果某个 key 对应的 val 是 object, 则重新迭代该 val, 使之 reactive
if (isObject(val)) {
const childObj = val;
new Observer(childObj);
}
// 如果某个 key 对应的 val 不是 Object, 而是基础类型,我们则对这个 key 进行 defineProperty 定义
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: () => {
console.info(`get ${key}-${val}`)
return val;
},
set: (newVal) => {
// 如果 newVal 和 val 相等,则不做任何操作(不执行渲染逻辑)
if (newVal === val) {
return;
}
// 如果 newVal 和 val 不相等,且因为 newVal 为 Object, 所以先用 Observer迭代 newVal, 使之 reactive, 再用 newVal 替换掉 val, 再执行对应操作(渲染逻辑)
else if (isObject(newVal)) {
console.info(`set ${key} - ${val} - ${newVal} - newVal is Object`);
new Observer(newVal);
val = newVal;
}
// 如果 newVal 和 val 不相等,且因为 newVal 为基础类型, 所以用 newVal 替换掉 val, 再执行对应操作(渲染逻辑)
else if (!isObject(newVal)) {
console.info(`set ${key} - ${val} - ${newVal} - newVal is Basic Value`);
val = newVal;
}
}
})
}
function isObject(data) {
if (typeof data === 'object' && data != 'null') {
return true;
}
return false;
}
dep是咋来的呢?
dep是订阅者,在observer阶段,每个从data里遍历出来的属性key都会创建新的dep实例,dep负责管理对应的订阅者,当key的get被某个watcher实例调用时,就将watcher加到对应dep实例创建的数组中,当set被调用时,就通知对应的dep,让dep通知对应的watcher实例更新。
watcher是咋来的呢?
在compile解析el阶段,解析到代码中包含{{mustache}},v-model,v-bind,computed,watch的语法,都会生成一个watcher实例。这时dep.target会指向这个新生成的对象,就能把这个watcher添加到对应的dep中:dep.addSub(Dep.target)。
watcher更新,执行各自的回调函数
代码(有些我也没看懂,先贴着慢慢看):
dep.js
export class Dep {
constructor() {
this.subs = [];
}
// 将 watcher 实例置入队列
addSub(sub) {
this.subs.push(sub);
}
// 通知队列里的所有 watcher 实例,告知该 key 的 对应的 val 被改变
notify() {
this.subs.forEach((sub, index, arr) => sub.update());
}
}
// Dep 类的的某个静态属性,用于指向某个特定的 watcher 实例.
Dep.target = null
observer.js
import {Dep} from './dep'
function makeItReactive(obj, key, val) {
var dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: () => {
// 收集依赖! 如果该 key 被某个 watcher 实例依赖,则将该 watcher 实例置入该 key 对应的 dep 实例里
if(Dep.target){
dep.addSub(Dep.target)
}
return val
},
set: (newVal) => {
if (newVal === val) {
return;
}
else if (isObject(newVal)) {
new Observer(newVal);
val = newVal;
// 通知 dep 实例, 该 key 被 set,让 dep 实例向所有收集到的该 key 的 watcher 实例发送通知
dep.notify()
}
else if (!isObject(newVal)) {
val = newVal;
// 通知 dep 实例, 该 key 被 set,让 dep 实例向所有收集到的该 key 的 watcher 发送通知
dep.notify()
}
}
})
}
watcher.js
import { Dep } from './Dep.js';
export class Watcher {
constructor(vm, expOrFn, cb) {
this.cb = cb;
this.vm = vm;
this.expOrFn = expOrFn;
this.value = this.get();
}
get() {
// 在实例化某个 watcher 的时候,会将Dep类的静态属性 Dep.target 指向这个 watcher 实例
Dep.target = this;
// 在这一步 this.vm._data[this.expOrFn] 调用了 data 里某个 key 的 getter, 然后 getter 判断类的静态属性 Dep.target 不为null, 而为 watcher 的实例, 从而把这个 watcher 实例添加到 这个 key 对应的 dep 实例里。 巧妙!
const value = this.vm._data[this.expOrFn];
// 重置类属性 Dep.target
Dep.target = null;
return value;
}
// 如果 data 里的某个 key 的 setter 被调用,则 key 会通知到 该 key 对应的 dep 实例, 该Dep实例, 该 dep 实例会调用所有 依赖于该 key 的 watcher 实例的 update 方法。
update() {
this.run();
}
run() {
const value = this.get();
if (value !== this.value) {
this.value = value;
// 执行 cb 回调
this.cb.call(this.vm);
}
}
}
贴张示意图方便理解:
小栗子1:简单实现响应式
<input type = 'text' id = 'input'>
<p id = 'text'></p>
<script>
var oinput = document.getElementById('input')
var op = document.getElementById('text')
var obj = {}
Object.defineProperty(obj,'text',{
get(e){
console.log(e)
}
set(newValue){
op.innerHTML = newValue
}
}
oinput.onKeyUp = function(e){
obj.text = event.target.value
}
小栗子二:
<!DOCTYPE html>
<html lang="en">
<head>
<title>双向绑定demo</title>
<meta charset="UTF-8">
</head>
<body>
<div id="app">
<div>双向绑定demo</div>
<input type="text" id="input">
<div>
<span>input:</span>
<span id="output"></span>
</div>
</div>
</body>
<script>
var obj={}
Object.defineProperty(obj,'input',{
get:function(){
return obj.input
},
set:function(value){
document.getElementById('input').value = value
document.getElementById('output').innerHTML = value
}
})
//view->model
document.addEventListener('keyup',function(e){
//model->view
obj.input = e.target.value
})
</script>
</html>
由以上两个例子可以看出都是通过定义一个中间变量空的object对象,先对这个对象进行数据劫持,然后通过DOM元素的监听,将输入的值赋值给被数据劫持的obj对象的属性,实现model的数据改变