目录
响应式原理简析
初始化——init
数据劫持——Observer
订阅者——Watcher
解析器——Compile
MVVM总结
Vue数据双向绑定是通过数据劫持结合发布者-订阅者模式的方式来实现的。首先要对数据进行劫持监听,所以我们需要设置一个监听器Observer,用来监听所有属性。如果属性发上变化了,就需要告诉订阅者Watcher看是否需要更新。因为订阅者是有很多个,所以我们需要有一个消息订阅器Dep来专门收集这些订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理的。我们还需要有一个指令解析器Compile,对每个节点元素进行扫描和解析,将相关指令(如v-model,v-on)对应初始化成一个订阅者Watcher,并替换模板数据或者绑定相应的函数,此时当订阅者Watcher接收到相应属性的变化,就会执行对应的更新函数,从而更新视图。
根据Vue的生命周期我们知道,Vue首先进行初始化操作;源码在src/core/instance/init.js中
/*初始化生命周期*/
initLifecycle(vm)
/*初始化事件*/
initEvents(vm)
/*初始化render*/
initRender(vm)
/*调用beforeCreate钩子函数并且触发beforeCreate钩子事件*/
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
/*初始化props、methods、data、computed与watch*/
initState(vm)
initProvide(vm) // resolve provide after data/props
/*调用created钩子函数并且触发created钩子事件*/
callHook(vm, 'created')
以上代码展示了初始化所需要完成的工作,其中 initState(vm) 初始化props,methods,data,computed和watch
/*初始化props、methods、data、computed与watch*/
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
/*初始化props*/
if (opts.props) initProps(vm, opts.props)
/*初始化方法*/
if (opts.methods) initMethods(vm, opts.methods)
/*初始化data*/
if (opts.data) {
initData(vm)
} else {
/*该组件没有data的时候绑定一个空对象*/
observe(vm._data = {}, true /* asRootData */)
}
/*初始化computed*/
if (opts.computed) initComputed(vm, opts.computed)
/*初始化watchers*/
if (opts.watch) initWatch(vm, opts.watch)
}
...
/*初始化data*/
function initData (vm: Component) {
/*得到data数据*/
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}defi
...
//遍历data中的数据
while (i--) {
/*保证data中的key不与props中的key重复,props优先,如果有冲突会产生warning*/
if (props && hasOwn(props, keys[i])) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${keys[i]}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(keys[i])) {
/*判断是否是保留字段*/
/*这里是我们前面讲过的代理,将data上面的属性代理到了vm实例上*/
proxy(vm, `_data`, keys[i])
}
}
// observe data
/*这里通过observe实例化Observe对象,开始对数据进行绑定,asRootData用来根数据,用来计算实例化根数据的个数,下面会进行递归observe进行对深层对象的绑定。则asRootData为非true*/
observe(data, true /* asRootData */)
}
重点分析:initData主要做了两件事,一是将_data上面的数据代理到vm上;二是通过执行 Observe将data内的属性递归变成可观察的,即对data定义的每个属性进行getter/setter操作,这里就是Vue实现响应式的基础;
Observer是一个数据监听器,其实现核心方法就是Object.defineProperty( )。如果要对所有属性都进行监听的话,那么可以通过递归方法遍历所有属性值,并对其进行Object.defineProperty( )处理。
function Observer(data) {
this.data = data;
this.walk(data);
}
Observer.prototype = {
walk: function(data) {
var self = this;
//这里是通过对一个对象进行遍历,对这个对象的所有属性都进行监听
Object.keys(data).forEach(function(key) {
self.defineReactive(data, key, data[key]);
});
},
defineReactive: function(data, key, val) {
var dep = new Dep();
// 递归遍历所有子属性
var childObj = observe(val);
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function getter () {
if (Dep.target) {
// 在这里添加一个订阅者
console.log(Dep.target)
dep.addSub(Dep.target);
}
return val;
},
// setter,如果对一个对象属性值改变,就会触发setter中的dep.notify(),通知watcher(订阅者)数据变更,执行对应订阅者的更新函数,来更新视图。
set: function setter (newVal) {
if (newVal === val) {
return;
}
val = newVal;
// 新的值是object的话,进行监听
childObj = observe(newVal);
dep.notify();
}
});
}
};
function observe(value, vm) {
if (!value || typeof value !== 'object') {
return;
}
return new Observer(value);
};
// 消息订阅器Dep,订阅器Dep主要负责收集订阅者,然后在属性变化的时候执行对应订阅者的更新函数
function Dep () {
this.subs = [];
}
Dep.prototype = {
/**
* [订阅器添加订阅者]
* @param {[Watcher]} sub [订阅者]
*/
addSub: function(sub) {
this.subs.push(sub);
},
// 通知订阅者数据变更
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
});
}
};
Dep.target = null;
分析:在Observer中,在拦截getter内添加订阅器的时候出现了Dep.target不知道哪里冒出来的??当写到Watcher的时候,你就会发现,这个Dep.target是来源于Watcher,因为每一个Watcher强制执行Observer内的getter方法获取属性值用于渲染。
Watcher就是一个订阅者。用于将Observer发来的update消息处理,执行Watcher绑定的更新函数。
function Watcher(vm, exp, cb) {
this.cb = cb;
this.vm = vm;
this.exp = exp;
this.value = this.get(); // 将自己添加到订阅器的操作
}
Watcher.prototype = {
update: function() {
this.run();
},
run: function() {
var value = this.vm.data[this.exp];
var oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal);
}
},
get: function() {
Dep.target = this; // 缓存自己
var value = this.vm.data[this.exp] // 强制执行监听器里的get函数
Dep.target = null; // 释放自己
return value;
}
};
分析:vm就是SelfValue对象,相当于Vue中的new Vue的一个对象。exp是node节点的v-model或v-on:click等指令的属性值。如v-model="name",exp就是"name"。cb,就是Watcher绑定的更新函数。
在Watcher的getter函数中,Dep.target指向了自己,也就是Watcher对象。在getter函数中
var value = this.vm.data[this.exp] // 强制执行监听器里的get函数。
//这里获取vm.data[this.exp] 时,会调用Observer中Object.defineProperty中的get函数
get: function getter () {
if (Dep.target) {
// 在这里添加一个订阅者
console.log(Dep.target)
dep.addSub(Dep.target);
}
return val;
}
Compile主要的作用是把new SelfVue 绑定的dom节点,(也就是el标签绑定的id)遍历该节点的所有子节点,找出其中所有的v-指令和" { {}} ".
1.如果子节点含有v-指令,即是元素节点,则对这个元素添加监听事件。(如果是v-on,则node.addEventListener('click'),如果是v-model,则node.addEventListener('input'))。接着初始化模板元素,创建一个Watcher绑定这个元素节点。
2.如果子节点是文本节点,即" { { data }} ",则用正则表达式取出" { { data }} "中的data,然后var initText = this.vm[exp],用initText去替代其中的data
Vue是通过Observer、Dep、Watcher、Compile等实现MVVM。
a、Observer实现对数据劫持;主要是通过Object.defineProterty()对data内的属性进行递归拦截。在getter方法中将订阅器添加至订阅器的数组中;在setter方法中对新值进行再次拦截,并通知订阅者进行更新。
b、订阅器Dep包涵订阅器的收集数组,添加订阅者的方法addsub()和通知方法notify()。
c、订阅者Watcher 包涵获取属性值和数据更新update方法。在接收Observer通过dep传递过来的数据变化通知Compile进行view update。
d、Compile实现指令解析,初始化视图,并订阅数据变化,绑定好更新函数。
摘于文章:
https://funteas.com/topic/5a809f5847dc830a0e4690c2
https://www.jianshu.com/p/f194619f6f26