总览
Vue是一款MVVM(Model-View-ViewModel)前端框架,框架帮我们实现了数据和视图的双向绑定,我们不在需要操作DOM,只需要关心数据的变化,视图就会自动改变,我们就来探究一下Vue的数据绑定原理
先来贴一张官方的数据绑定原理的介绍图,还有一段官网的介绍
当你把一个普通的 JavaScript 对象传入 Vue 实例作为
data
选项,Vue 将遍历此对象所有的 property,并使用Object.defineProperty
把这些 property 全部转为 getter/setter。
总体来说,就是将Vue中的data通过Observer进行数据观察,在读取数据值的时候添加订阅,在值进行修改时发布通知,告诉这个数值所有的订阅者更新数据
源码分析
为了方便理解,代码中有所省略
defineReactive劫持数据
众所周知,Vue2使用的是Object.defineProperty()来实现的数据劫持,defineReactive就是对Object.defineProperty()进行的一次封装
export default function defineReactive(data,key,val){
// console.log('defineReactive',data,key)
if(arguments.length == 2){
val = data[key]
}
//子元素要进行observe,形成递归,多个函数循环调用
let childOb = observe(val)
Object.defineProperty(data,key,{
//可枚举
enumerable:true,
//可以被配置,比如可以被delete
configurable:true,
//getter
get() {
console.log(`打开${key}属性`)
return val
},
//setter
set(newValue) {
console.log(`改变obj的${key}属性`,newValue)
if(val === newValue){
return
}
val = newValue
childOb = observe(newValue)
}
});
}
将defineReactive暴露出去,再需要进行数据劫持的时候直接就可以用这个封装好的方法
Observer类
开始我们说了,Vue中的data通过Observer进行数据观测才可以进行劫持,那么Observer的作用是什么呢?Observer主要负责将基本数据类型进行我们上面所定义的defineReactive数据劫持,然后区分引用类型,将对象的属性递归劫持,如果是数组,就要改写数组的方法来实现数据劫持,之后会详细介绍。
Observer使用时会在外面进行类型检测,如果不是引用类型,直接返回,只有引用类型才进行数据侦测,并将Observer实例返回
// observe.js
export default function(value){
//如果value不是对象,什么都不做
if(typeof value != 'object') return;
//定义ob
var ob;
// __ob__就是对象是否真测过的标识,如果数据被Observer处理过,他的属性上就会有__ob__
if(typeof value.__ob__ !== 'undefined'){
ob = value.__ob__;
}else{
ob = new Observer(value)
}
return ob;
}
// Observer.js
export default class Observer {
constructor(value) {
//每一个Observer的实例身上,都有一个dep(下一部分会说到)
this.dep = new Dep();
//给实例添加了__ob__属性,值是这个new的实例
def(value, '__ob__', this, false)
console.log('我是Observer构造器', value)
//Observer将一个正常的object转换为每个层级的属性都是响应式(可以被侦测的)object
//检查它是数组还是对象
if (Array.isArray(value)) {
//如果是数组,就将这个数组的原型指向arrayMethods(后面会介绍)
Object.setPrototypeOf(value, arrayMethods);
//让这个数组变得observe
this.observeArray(value)
} else {
this.walk(value)
}
}
//遍历
walk(value) {
for (let k in value) {
defineReactive(value, k)
}
}
//数组的特殊遍历
observeArray(arr) {
for (let i = 0, l = arr.length; i < l; i++) {
observe(arr[i])
}
}
}
数组的响应式
由于通过索引来实现绑定太过浪费性能,所以,Vue在实现数组的响应式时重写了7个数组的API(为什么是7个?Array.prototype上可以改变原数组的API只有7个,其他的不会改变原数组),只要调用了这7个API就通知视图进行更新
//得到Array.prototype
const arrayPrototype = Array.prototype;
//以Array.prototype为原型创建arrayMethods对象,并暴露
export const arrayMethods = Object.create(arrayPrototype);
//要被改写的7个数组方法
const methodsNeedChange = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsNeedChange.forEach(methodName =>{
//备份原来的方法
const original = arrayPrototype[methodName];
def(arrayMethods,methodName,function(){
//恢复原来的方法
const result = original.apply(this,arguments)
//把类数组对象变为数组
const args = [...arguments]
//把这个数组身上的__ob__取出来,__ob__已经被添加了,因为数组肯定不是最高层,比如obj.g属性是数组,obj不能是数组,第一次遍历obj这个对象的第一层的时候,已经给g属性(就是这个数组)添加了__ob__属性。
const ob = this.__ob__;
// 要把插入的新项也要变为observe的
let inserted = [];
switch(methodName){
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
//splice新加入的参数是从第三项开始
inserted = args.slice(2)
break
}
//判断有没有要插入的新项,让新项也变成响应的
if(inserted){
ob.observeArray(inserted)
}
// 调用dep的notify方法通知更新
ob.dep.notify()
return result
},false)
})
依赖收集
依赖收集的过程就是将watcher实例添加到Dep的队列中的过程,Dep实例存在 __ob__
属性中,也就是上文Observer中的创建Dep对象实例的那一段代码。每一个Dep实例维护着一个watcher队列,当数据发生改变时,触发notify方法通知视图更新
var uid = 0;
export default class Dep{
constructor(){
this.id = uid++;
//用数组存储自己的订阅者。subs是subscribes订阅者的意思
this.subs = [];
}
//添加订阅
addSub(sub){
this.subs.push(sub)
}
//添加依赖
depend(){
//Dep.target我们自己指定的全局的位置,只要是全局唯一没有歧义就行
if(Dep.target){
this.addSub(Dep.target)
}
}
//通知更新
notify(){
//浅克隆一份
const subs = this.subs.slice();
//遍历
for(let i = 0,l = subs.length;i<l;i++){
subs[i].update();
}
}
}
这里面比较难理解的是Dep.target这一个东西,这个东西其实就是一个全局标识,如果你愿意,你也可以用window.target或者其他的变量名。了解他的作用继续看下一段
var uid = 0;
export default class Watcher {
constructor(target, expression, callback) {
console.log('我是watcher的构造器');
this.id = uid++;
this.target = target;
this.getter = parsePath(expression);
this.callback = callback;
this.value = this.get();
}
update() {
this.run();
}
get() {
//进入依赖收集阶段.让全局的Dep.target设置为Watcher本身,那么就是进入依赖收集阶段
Dep.target = this;
const obj = this.target;
var value;
//只要能找,就一直找
try {
value = this.getter(obj);
} finally {
Dep.target = null;
}
return value;
}
run() {
this.getAndInvoke(this.callback)
}
getAndInvoke(cb) {
const value = this.get();
if (value !== this.value || typeof value == 'object') {
const oldValue = this.value;
cb.call(this.target, value, oldValue)
}
}
}
function parsePath(str) {
var segments = str.split('.');
return (obj) => {
for (let i = 0; i < segments.length; i++) {
if (!obj) return;
obj = obj[segments[i]]
}
return obj;
}
}
在创建Watcher实例的时候,将Dep.target指向当前实例,然后触发被处理过的属性的get方法,调用dep的depend方法,将这个watcher实例添加到dep的subs队列中,这时候defineReactive方法就要发生一点改变了
export default function defineReactive(data,key,val){
const dep = new Dep();
if(arguments.length == 2){
val = data[key];
}
//子元素要进行observe,形成递归,多个函数循环调用
let childOb = observe(val);
Object.defineProperty(data,key,{
enumerable:true,
configurable:true,
//getter
get() {
console.log(`打开${key}属性`)
//如果现在处于依赖收集阶段
if(Dep.target){
dep.depend();
if(childOb){
childOb.dep.depend()
};
}
return val;
},
//setter
set(newValue) {
console.log(`改变obj的${key}属性`,newValue)
if(val === newValue){
return;
}
val = newValue;
childOb = observe(newValue)
dep.notify();
}
});
}
总结
Vue数据绑定的过程大致就是如此了,还有很多不完善的地方,比如直接通过数组索引改变数组项不会触发响应式,这里官方也是不建议使用下标直接修改,所以官方提供了$set这个API来完成操作
结束~~~