vue实现数据响应式,是经过数据劫持侦测数据变化,发布订阅模式进行依赖收集与视图更新,换句话说是Observe,Watcher以及Compile三者相互配合。javascript
变量
换成数据
,绑定更新函数,添加订阅者,收到通知就执行更新函数实现响应式的第一步就是能侦测数r据的变化,在Vue2.x是经过ES5的方法Object.defineProperty()实现对象属性的侦听,在Vue3.x中使用了ES6提供的Proxy对对象进行代理。html
Object.definePropertyvue
function observe(obj) { if (!obj || typeof obj !== "object") { return; } Object.keys(obj).forEach((key) => { defineReactive(obj, key, obj[key]); }); function defineReactive(obj, key, value) { //递归子属性 observe(value); //订阅器 const dp = new Dep(); Object.defineProperty(obj, key, { configurable: true, //可删除 enumerable: true, //可枚举遍历 get: function () { /* 将Dep.target(即当前的Watcher对象存入dep的subs中) */ dp.addSub(Dep.target); return value; }, set: function (newValue) { //递归新的子属性 observe(newValue); if (value !== newValue) { value = newValue; /* 在set的时候触发dep的notify来通知全部的Watcher对象更新视图 */ dp.notify(); } }, }); } }
Proxy实现代理java
let target = { name: " xiao" }; let handler = { get(target, key) { if (typeof target[key] === "object" && target[key] !== "null") { return new Proxy(target[key], handler); } return target[key]; }, set: function (target, key, value) { target[key] = value; }, }; target = new Proxy(target, handler);
//Dep订阅者,依赖收集器 class Dep { constructor() { /* 用来存放Watcher对象的数组 */ this.subs = []; } /* 在subs中添加一个Watcher对象 */ addSub(sub) { this.subs.push(sub); } /* 在subs中添加一个Watcher对象 */ notify() { this.subs.forEach((sub) => { sub.update(); }); } } //用 addSub 方法能够在目前的 Dep 对象中增长一个 Watcher 的订阅操做; //用 notify 方法通知目前 Dep 对象的 subs 中的全部 Watcher 对象触发更新操做。
class Watcher { constructor(obj, key, cb) { /* 在new一个Watcher对象时将该对象赋值给Dep.target,在observe get中会用到 */ Dep.target = this; this.obj = obj; this.key = key; this.cb = cb; //触发getter,依赖收集 this.value = obj[key]; //收集完置空Dep.target,防止重复收集 Dep.target = null; } update() { //得到新值 this.value = obj[this.key]; console.log("视图更新"); } }
//指令处理类 const compileUtile = { getVal(expr,vm){ //reduce用的好啊 return expr.split('.').reduce((data,curentval)=>{ return data[curentval]; },vm.$data) }, html(node,expr,vm){ new Watcher(vm,expr,(newVal)=>{ this.updater.htmlUpdate(node,newVal); }) const value = this.getVal(expr,vm); this.updater.htmlUpdate(node,value); }, //更新函数 updater:{ htmlUpdate(node,value){ node.innerHTML= value; }, } } //Compile指令解析器 class Compile{ //各类正则匹配vue指令和表达式,替换数据 }
Object.defineProperty与Proxy的区别?node
为何要依赖收集?api
数据劫持的目的是在属性变化的时候触发视图更新,依赖收集能够收集到哪些地方使用到了相关属性,属性变化时,就能够通知到全部的地方去更新视图,对于没有使用的属性,也能够避免无用的数据比对更新数组
Dep和Watcher的关系(多对多)app
watcher和Dep什么时候建立ide
由于Object.defineProperty
不能监听数组长度变化,因此Vue使用了函数劫持
的方式,重写了数组的方法,Vue将data中的数组进行了原型链重写,指向了本身定义的数组原型方法。这样当调用数组api时,能够通知依赖更新。若是数组中包含着引用类型,会对数组中的引用类型再次递归遍历进行监控。这样就实现了监测数组变化。函数
1 // src/core/observer/array.js 2 3 // 获取数组的原型Array.prototype,上面有咱们经常使用的数组方法 4 const arrayProto = Array.prototype 5 // 建立一个空对象arrayMethods,并将arrayMethods的原型指向Array.prototype 6 export const arrayMethods = Object.create(arrayProto) 7 8 // 列出须要重写的数组方法名 9 const methodsToPatch = [ 10 'push', 11 'pop', 12 'shift', 13 'unshift', 14 'splice', 15 'sort', 16 'reverse' 17 ] 18 // 遍历上述数组方法名,依次将上述重写后的数组方法添加到arrayMethods对象上 19 methodsToPatch.forEach(function (method) { 20 // 保存一份当前的方法名对应的数组原始方法 21 const original = arrayProto[method] 22 // 将重写后的方法定义到arrayMethods对象上,function mutator() {}就是重写后的方法 23 def(arrayMethods, method, function mutator (...args) { 24 // 调用数组原始方法,并传入参数args,并将执行结果赋给result 25 const result = original.apply(this, args) 26 // 当数组调用重写后的方法时,this指向该数组,当该数组为响应式时,就能够获取到其__ob__属性 27 const ob = this.__ob__ 28 let inserted 29 switch (method) { 30 case 'push': 31 case 'unshift': 32 inserted = args 33 break 34 case 'splice': 35 inserted = args.slice(2) 36 break 37 } 38 if (inserted) ob.observeArray(inserted) 39 // 将当前数组的变动通知给其订阅者 40 ob.dep.notify() 41 // 最后返回执行结果result 42 return result 43 }) 44 })
def就是经过Object.defineProperty重写value,也就是自定义的几个数组方法
function def(obj,key,val,enumble){ Object.defineProperty(obj,key,{ enumble:!!enumble, configrable:true, writeble:true, val:val }) }
observe方法里面加入数组的处理,
__proto__
属性,就把__protp__
属性指向重写的方法__proto__
属性,就把重写的方法定义到对象上实例上// src/core/observer/index.js export class Observer { ... constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', this) if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } this.observeArray(value) } else { this.walk(value) } } ... } function protoAugment (target, src: Object) { /* eslint-disable no-proto */ target.__proto__ = src /* eslint-enable no-proto */ } function copyAugment (target: Object, src: Object, keys: Array<string>) { for (let i = 0, l = keys.length; i < l; i++) { const key = keys[i] def(target, key, src[key]) } }