Vue源码之数据响应系统

Vue源码之数据响应系统

响应系统的基本思路

接下来就是数据响应的基本思路,我们先看一下下面的例子:

Vue对象中,我们可以使用$watch来观察一个字段,当这个字段的值发生变化的时候执行制定的观察者。如下:

const ins = new Vue({
  data: {
    a: 1
  }
})

ins.$watch('a', () => {
  console.log('修改了 a')
})

如果我们修改了a的值,那么就会在控制台打印出修改了a这个字段。

我们用正常的原生js怎么实现读写事件监听呢?答案就是修改这个对象的getset方法,这也就是vue的核心之一数据劫持,给出一个例子来说明吧。

let a = {
  name: '123'
}
let value;

Object.defineProperty(a, 'name', {
  set(newValue) {
    console.log(newValue)
   	value = newValue
  },
  
  get() {
    return value;
  }
})

这样我们就实现了对属性 a 的设置和获取操作的拦截,有了它我们就可以大胆地思考一些事情,比如: 能不能在获取属性 a 的时候收集依赖,然后在设置属性 a 的时候触发之前收集的依赖呢? 嗯,这是一个好思路,不过既然要收集依赖,我们起码需要一个”筐“,然后将所有收集到的依赖通通放到这个”筐”里,当属性被设置的时候将“筐”里所有的依赖都拿出来执行就可以了

// 进行简单模拟Vue的数据响应系统
let dep = [];

let obj = {
  name: ''
}

Object.defineProperty(obj, 'name', {
  get() {
    dep.push(Target)
  },

  set() {
    dep.forEach(fn => fn());
  }
})

// 全局的Target
let Target = null

function $watch(pro, fn) {
  // 将传进来的回调函数传给Target
  Target = fn;

  // 调用obj的写入方法,进行添加依赖
  obj[pro]
}

很多人可能会有一些疑惑,为什么是在get的时候进行添加依赖而不是在set的时候添加依赖呢?

  • 数据响应系统的目的是数据的属性一旦发生改变,就通知$watch方法进行执行函数更新数据,所以要对get方法进行响应
  • 需要有一个方法来进行注册依赖回调函数,所以用数据的get方法。

我们可以通过以下的代码来进行测试。

$watch('name', () => {
  console.log('我是第一个依赖回调函数')
})

$watch('name', () => {
  console.log('我是第二个依赖回调函数')
})

obj.name = '123';
// 我是第一个依赖回调函数
// 我是第二个依赖回调函数

上面就是最简单的数据响应的一个小的demo。但是我们想,一个对象不可能只有一个属性吧,那么如果改为多属性呢?答案就是加个迭代循环就行了。

// 进行简单模拟Vue的数据响应系统
let obj = {
  name: '',
  value: 1
}

for (let key in obj) {
  let dep = [];
  
  Object.defineProperty(obj, key, {
    get() {
      dep.push(Target)
      
      return 
    },

    set() {
      dep.forEach(fn => fn());
    }
  })
}

// 全局的Target
let Target = null

function $watch(pro, fn) {
  // 将传进来的回调函数传给Target
  Target = fn;

  // 调用obj的写入方法,进行添加依赖
  obj[pro]
}

$watch('name', () => {
  console.log('name')
})
$watch('value', () => {
  console.log('value')
})

obj.name = '1';
obj.value = '1'

// name
//value

到此时的话,有一些细心的人可能会有一些疑惑了,比如下面这串代码:

obj.name;   // undefined

这样的处理会导致obj的属性没有返回任何的值,那么就要进行以下的处理

for (let key in obj) {
  let dep = [],
      val = obj[key];
  
  Object.defineProperty(obj, key, {
    get() {
      dep.push(Target)
      
      return obj[key]		// 错误示范,这个会不断触发obj对象的key属性的getter方法,导致死循环
      return val  // 正确的做法
    },

    set(newVal) {
      // 如果新的值跟原来的值一样的话,那么说明这个新的值
      if (newVal === val) {
        return ;
      }
      
      // 更新值
      val = newVal;
      dep.forEach(fn => fn());
    }
  })
}

很多人一定会这么想,就是直接调用上面的代码就好了,但是要想一下:

  • 我们是如何去拿到数据的呢?

    我们是通过obj[key]这个代码去触发里面的get方法并且返回val,所以我们要有一个变量来缓存这个属性的值。

我们这样的话是做了一个数据响应观察的一个小小的demo,但是还有许多问题没有解决,比如如何去观察嵌套的属性呢?

function detail(obj, key) {
	const dep = [];
	let val = obj[key];
  Object.defineProperty(obj, key, {
    get() {
      if (Target) {
        dep.push(Target)
      }

      Target = null
      
      return val  // 正确的做法
    },

    set(newVal) {
      // 如果新的值跟原来的值一样的话,那么说明这个新的值
      if (newVal === val) {
        return ;
      }
      
      // 更新值
      val = newVal;
      dep.forEach(fn => fn());
    }
  })
}

// 进行遍历属性
function iterateObj(obj) {
  for (let key in obj) {
    if (isPlainObject(obj[key])) {
      iterateObj(obj[key])
    } else {
      detail(obj, key);
    }
  }
}

function isPlainObject(obj) {
  return Object.prototype.toString.call(obj) === '[object Object]';
}

iterateObj(obj)

上面就是最简单的数据观察响应的系统,但是还是跟我们的Vue里面的watch差别还是比较大的,最大的明显是人家可以用字符串来代替对象的路径。

$watch(obj, 'b.name', () => {
  console.log('b.name')
})

解决办法要根据js的一个特性来:对象的属性或者方法可以用[key]来进行访问,并且支持变量字符串进行访问,那么我们的思路就是将这些路径推进一个数组中,然后按顺序进行查找,所以我们要对$watch进行改进。

/**
 * obj {Object}
 * pro {String}
 * fn {Function}
 */
function $watch(obj, pro, fn) {
  // 将传进来的回调函数传给Target
  Target = fn;
  
  let pathArr = pro.split('.'),
      baseObj = obj;
 	
  for (let i = 0; i < pathArr.length; i++) {
    if (!baseObj) {
     	// 说明填写的路径错误
      throw new Error('路径出错')
    }
    // 在最后一个下标的时候就会进行访问到obj.b.name
    baseObj = baseObj[pathArr[i]];
  }
}

我们举个例子进行测试吧。

$watch(obj, 'b.name', () => {
  console.log('newName');
})
obj.b.name = '123'

// newName

这样我们就完成了对字符串路径进行解析并且访问了。

我们再想想,这个$watch方法的思路是什么?这个方法就是要尽可能能够触发对象方法的拦截器get从而添加一下依赖。那么$watch的第二个函数可以是一个函数(只需要这个函数能够触发这个被监听的对象的属性就行了)

那么我们就需要对$watch方法进行修改。

function $watch(obj, pro, fn) {
  // 将传进来的回调函数传给Target
  Target = fn;
  
  if (typeof pro === 'Function') {
    pro();
    return ;
  }
  
  let pathArr = pro.split('.'),
      baseObj = obj;
 	
  for (let i = 0; i < pathArr.length; i++) {
    if (!baseObj) {
     	// 说明填写的路径错误
      throw new Error('路径出错')
    }
    // 在最后一个下标的时候就会进行访问到obj.b.name
    baseObj = baseObj[pathArr[i]];
  }
}

这样子就是实现了我们的需求。

当然这些是比较严谨的vue的观察者对象的一些语法了,但是还有个很明显的缺点,如果想要观察的对象上有1000个属性,那么我们就必须对这1000个属性添加gettersetter方法,先不说占用的空间,在运行的时候,添加依赖的时候必定会造成卡顿,那么有没有更好的办法呢?

ES5是没有了,但是有个ES6的代理却能够做到,我就不在这里介绍了,如何使用的话,请见ES6实现Vue的数据响应系统

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值