Vue源码------------- 数据响应系统的基本思路
在 Vue
中,我们可以使用 $watch
观测一个字段,当字段的值发生变化的时候执行指定的观察者,如下:
1 var vm = new Vue({ 2 data: { 3 num:1 4 } 5 }) 6 vm.$watch('num',function() { 7 console.log('num被修改') 8 })
这时候,当我们去修改 num 数值的时候,就会打印出来 'num被修改'。这个到底是如何实现,怎么打印出来的呢?
现在我们先以另一种方式,讲解期中的道理。关键一个知识点: Object.definePropert; 不了解的先打开这先看下
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
假设我们 有下边的数据
1 var data = { 2 num: 1 3 }
我们还有一个叫做 $watch
的函数,同时函数接受两个参数;第一个参数是要观测的字段,第二个参数是当该字段的值发生变化后要执行的函数,如下:
1 function $watch () {...} 2 $watch('num', () => { 3 console.log('修改了 num') 4 })
下边通过 Object.defineProperty 实现下边的功能:
1 Object.defineProperty(data, 'num', { 2 set () { 3 console.log('设置了num') 4 }, 5 get () { 6 console.log('读取了 num') 7 } 8 })
通过 Object.defineProperty 我们可以轻松知道 num 被设置,和读取了。但问题是如何让$watch 方法知道,同时通知第二个参数函数呢?
有了上边的想法,我们就可以大胆地思考一些事情,比如: 能不能在获取属性 num 的时候收集依赖,然后在设置属性 num
的时候触发之前收集的依赖呢?
1 // dep 数组就是我们所谓的“筐” 2 const dep = [] 3 Object.defineProperty(data, 'num', { 4 set () { 5 // 当属性被设置的时候,将“筐”里的依赖都执行一次 6 dep.forEach(fn => fn()) 7 }, 8 get () { 9 // 当属性被获取的时候,把依赖放到“筐”里 10 dep.push(fn) 11 } 12 })
上边的 fn 来自哪里? 又是在什么时候出发num 属性的get() 呢?
接下来需要在$watch()上下手:
1 // fn 是全局变量 2 let fn= null 3 function $watch (exp, callback) { 4 // 将 fn 的值设置为 callback 5 fn = callback 6 // 读取字段值 exp,触发 get 函数 7 data[exp] 8 }
通过上边调用$watch 方法,先给全局变量fn 设置为回调函数,然后读取data的属性,num属性的get方法中,收集callback, 这样当num 变化时候可以通知callback方法;
上边的方法还有几个问题需要思考:
1. 实现多个属性监听;2. data 某个属性字段是对象时,3. 确定属性值发生变化,才去出发回调;
要解决上述问题又要怎么去做呢? 下边封装一个方法:
1 function observe(data) { 2 for (let key in data) { 3 const dep = [] 4 let val = data[key] 5 // 如果 val 是对象,递归调用 observe 函数将其转为访问器属性 6 const nativeString = Object.prototype.toString.call(val) 7 if (nativeString === '[object Object]') { 8 observe(val) 9 } 10 Object.defineProperty(data, key, { 11 set:function setter (newVal) { 12 if (newVal === val) return 13 val = newVal 14 dep.forEach(fn => fn()) 15 }, 16 get:function getter () { 17 dep.push(fn) 18 return val 19 } 20 }) 21 } 22 } 23 observe(data)
Vue中$watch方法第一个参数可以是 data 中的某个属性,function, 以及data属性中 对象的属性 ; 那么这个watch是如何实现呢? 下边我们改变下$watch();
1 function $watch (exp, callback) { 2 fn= fcallback 3 let pathArr, 4 obj = data 5 if (typeof exp === 'function') { 6 exp() 7 return 8 } 9 // 检查 exp 中是否包含 . 10 if (/\./.test(exp)) { 11 // 将字符串转为数组,例:'a.b' => ['a', 'b'] 12 pathArr = exp.split('.') 13 // 使用循环读取到 data.a.b 14 pathArr.forEach(p => { 15 obj = obj[p] 16 }) 17 return 18 } 19 data[exp] 20 }
先判断第一个参数 时候为function ,如果为function,则直接调用第一个参数;如果为obj.a 等形式;则进行split分割一层层出发,收集fn;
最后完整版下如下:
1 var fn = null; 2 var data = {names:"xiaoming", age:19,obj: {a:1,b:2,c:{c:1,d:2}}} 3 function observe (data) { 4 for (let key in data) { 5 const dep = [] 6 let val = data[key] 7 // 如果 val 是对象,递归调用 observe 函数将其转为访问器属性 8 const nativeString = Object.prototype.toString.call(val) 9 if (nativeString === '[object Object]') { 10 observe(val) 11 } 12 Object.defineProperty(data, key, { 13 set: setter(newVal) { 14 if (newVal === val) return 15 val = newVal 16 dep.forEach(fn => fn()) 17 }, 18 get: getter() { 19 dep.push(fn) 20 return val 21 } 22 }) 23 } 24 } 25 26 observe(data) 27 28 function $watch (exp, callback) { 29 fn = callback 30 let pathArr, 31 obj = data 32 if (typeof exp === 'function') { 33 exp() 34 return 35 } 36 // 检查 exp 中是否包含 . 37 if (/\./.test(exp)) { 38 // 将字符串转为数组,例:'a.b' => ['a', 'b'] 39 pathArr = exp.split('.') 40 // 使用循环读取到 data.a.b 41 pathArr.forEach(p => { 42 obj = obj[p] 43 }) 44 return 45 } 46 data[exp] 47 } 48 49 $watch('names',function() { 50 console.log('name change') 51 })
运行:在改变 data.names = '小明';
结果:
当然Vue实现肯定不会如此简单,接下来有空慢慢细讲,(*^▽^*)