双向绑定
- 首先通过一次渲染操作触发Data的getter进行依赖收集
- 在data发生变化的时候会触发它的setter
- setter通知Watcher
- Watcher进行回调通知组件重新渲染的函数
- diff算法来决定是否发生视图的更新
Observe
- 每个数据都有一个标记,防止重复绑定
- Observer为数据加上响应式属性进行双向绑定,如果是对象,则进行深度遍历,为每一个子对象都绑定上方法,如果是数组,对每个成员进行遍历绑定方法
源码解析:
export class Observer {
value: any;
dep: Dep;
vmCount: number;
constructor (value: any) {
this.value = value
this.dep = new Dep() //建立发布者
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
//是数组对每个成员进行遍历绑定方法
if (hasProto) {
// __proto__指向重写过后的原型
protoAugment(value, arrayMethods)
} else {
//遍历arrayMethods把它身上的这些方法直接给value
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
//是对象,则进行深度遍历,为每一个子对象都绑定上方法
//defineReactive通过Object.defineProperty定义getter和setter收集依赖通知更新
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
}
Watcher
观察者对象
- 依赖收集后保存在deps里
- 变动的时候deps作为发布者通知watcher
- watcher进行回调渲染
Dep
- 发布者,可以订阅多个观察者
- 收集依赖后会有一个或者多个watcher
- 一旦有变动便通知所有watcher
Watch监听Array数组变化
//此为监听对象
data(){
return {
objVal: {
name: 'obj',
type: 'obj'
}
}
},
watch:{
objVal:{
handler(val,oldval){
},
deep: true,
immediate:true
}
},
methods:{
changeObj(){
this.objVal.name = 'newobj';
}
}
deep:
当需要监听一个对象的改变时,普通的watch方法无法监听到对象内部属性的改变,只有data中的数据才能够监听到变化,深入监听,即监听对象里面的值的变化
immediate:
watch默认当值第一次绑定的时候,不会执行监听函数,immediate的作用就是首次获取值也执行函数
以上demo是监听对象,如果换成数组的话,会出现vue不会响应数据变化而重新去渲染页面,则监听失败
解决方法:
// Vue.set
Vue.$set(vm.items, indexOfItem, newValue)
// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)
原理:
Object.defineProperty对数组进行响应式化是有缺陷的
Vue使用了重写原型的方案代替
- 先获取原生 Array 的原型方法,因为拦截后还是需要原生的方法帮我们实现数组的变化。
- 对 Array 的原型方法使用 Object.defineProperty 做一些拦截操作。
- 把需要被拦截的 Array 类型的数据原型指向改造后原型
const arrayProto = Array.prototype // 获取Array的原型
function def (obj, key) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
value: function(...args) {
console.log(key); // 控制台输出 push
console.log(args); // 控制台输出 [Array(2), 7, "hello!"]
// 获取原生的方法
let original = arrayProto[key];
// 将开发者的参数传给原生的方法,保证数组按照开发者的想法被改变
const result = original.apply(this, args);
// do something 比如通知Vue视图进行更新
console.log('我的数据被改变了,视图该更新啦');
this.text = 'hello Vue';
return result;
}
});
}
// 新的原型
let obj = {
push() {}
}
// 重写赋值
def(obj, 'push');
let arr = [0];
// 原型的指向重写
arr.__proto__ = obj;
// 执行push
arr.push([1, 2], 7, 'hello!');
console.log(arr);
源码解析
array.js
Vue在array.js中重写了methodsToPatch中七个方法,并将重写后的原型暴露出去。
// Object.defineProperty的封装
import { def } from '../util/index'
// 获得原型上的方法
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
// Vue拦截的方法
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];
// 1.拦截方法
// 2.将开发者的参数传给原生的方法
// 3.重写
// 4.视图更新
methodsToPatch.forEach(function (method) {
// 原型方法进行赋值,不会去重新改写Array.prototype
const original = arrayProto[method]
//ob为成员唯一标识
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
//判断方法
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
//判断后observeArray为每个成员绑定方法
if (inserted) ob.observeArray(inserted)
// 通知视图更新
ob.dep.notify()
return result
})
})