vue响应式源码解析——笔记总结

该文章以vue2框架为主,记录学习笔记。如有错误或需要改进代码的地方,欢迎各位大佬指点,共同学习进步。

该文章源码git地址:vue源码解析: vue响应式源码解析

前言

        vue的响应式数据是通过Object.defineProperty()将属性转换成getter/setter的形式来追踪变化。结合消息订阅和发布者模式,读取数据时会触发getter,修改数据时会触发setter。

  • Observer:它的作用是把一个Object中的所有数据(包括子数据)都转换成响应式的
  • Watcher:订阅一个数据,并读取数据内容,数据变化时执行回调函数,更新视图
  • Dep:将读取的数据内容收集到dep中,数据改变时给回调函数传参

1.如何追踪变化

        我们封装一个函数defineReactive函数,其作用是定义一个响应式数据,也就是说在这个函数中进行变化追踪,封装后只需要传递data,key和val就行了。每当从data的key中读取数据时,get函数被触发;每当设置data中的key属性时,set被触发。

// 监听数据变化
function defineReactive(data, key, val = data[key]) {
  Object.defineProperty(data, key, {
    get() {
      console.log('读取数据触发get-->', val);
      return val
    },
    set(newVal) {
      if (val === newVal) return
      console.log('修改数据触发set-->', newVal);
      val = newVal
    },
  })
}

2.如何收集依赖

        只对Object.defineProperty进行封装,其实没什么用,真正有用的是如何收集依赖,观察下面代码:

<template>
    <h1>
        {{name}}
    </h1>
</template>

         该模板使用了数据name,先把用到的数据name收集起来,然后当它发生变化时,要向使用了它的地方发送通知。总结起来就是在getter中收集依赖,在setter中更新依赖。代码如下:

// 创建Dep类,用来收集依赖,发布依赖(dep收集的是windows.target)
class Dep {
	constructor() {
		this.subs = []
	}
	// 添加依赖
	addSubs(val) {
		this.subs.push(val)
	}
	// 收集依赖
	collect() {
		// 为什么收集的是Window中target的值呢?后续Watcher类中会解释
		if (Window.target) {
			this.addSubs(Window.target)
		}
	}
	// 发布依赖
	notify() {
		const subs = [...this.subs]
		// 数据变化时循环数组,执行元素watcher实例的undata方法通知更新
		subs.forEach(item => item.undata())
	}
}

        创建好Dep类后,如何把dep和监听的数据关联起来呢?我们改造一下之前的defineReactive函数,如下:

// 监听数据变化
function defineReactive(data, key, val = data[key]) {
  let dep = new Dep() //新增
  Object.defineProperty(data, key, {
      get() {
          // 收集
          dep.collect() //新增
          return val
      },
      set(newVal) {
          if (val === newVal) return
          val = newVal
          // 发布
          dep.notify() //新增
      }
  })
}

3.什么是watcher

        watcher是一个中介的角色,数据发生变化时通知它,然后它在通知其他地方。

        关于watcher,先看一个经典的使用方式:

vm.$watch('a.b.c', function(newVal, oldVal){
    //做点什么
})

        思考一下,怎们实现这个功能?好像只要把这个watcher实例添加到data.a.b.c属性的Dep中就行了。然后当data.a.b.c的发生变化时,通知watcher执行参数中的回调函数。代码如下:

// 订阅数据,把自己放在一个全局的位置(例:Window.target)
class Watcher {
	constructor(vm, expOrFn, cb) {
		// vm: 数据对象
		// expOrFn,根据data和expression就可以获取watcher依赖的数据
		// cb:依赖变化时触发的回调函数
		this.vm = vm
		this.expOrFn = expOrFn
		this.cb = cb
		this.value = this.get()
	}
	// 读取属性的内容
	get() {
    /**
     * 为什么把this(Watcher实例)放在Window.target下面呢?
     * 因为需要把读取的数据值放在Dep中,我们需要Watcher和Dep进行通讯
     * 所以在全局对象Window下创建一个target属性,解决了一个问题
     */
		Window.target = this
    // 执行path方法,读取对象中属性的值后会触发getter,收集到dep中
		const value = path(this.vm, this.expOrFn)
    // 收集后需要对Window.target进行重置
		Window.target = null
		return value
	}
	// 当收到数据变化的消息时执行该方法,从而调用cb
	undata() {
    // 之前存下来旧的值
		const oldVal = this.value
    // 读取新的值
		this.value = this.get()
    // 执行回调函数,并更改this执行,让this指向对象data而不是Watcher实例
		this.cb.call(this.vm, this.value, oldVal)
	}
}
// 辅助函数:用来读取对象中key的值
function path(obj, path = '') {
  // 分割路径,例如:'a.b.c',以data[a][b][c]的方式取值
	const keys = path.split('.')
	return obj ? keys.reduce((newObj, item) => newObj[item], obj) : obj
}

    这段代码可以主动把自己主动添加data.a.b.c的Dep中去,我在get方法中先把window.target设置成this,也就是当前的watcher实例,然后再读取一下data.a.b.c的值,就会触发getter。

    触发了getter,就会触发收集依赖的逻辑。收集依赖上面已经介绍过了,会从window.target中读取一下依赖并添加到Dep中。

    依赖注入到Dep后,每当data.a.b.c的值发生变化时,就会让依赖列表中所有的依赖循环触发updata方法,也就是watcher中的updata方法。而updata方法会执行参数中的回调函数,将value和oldValue传到参数中。

4.递归侦测所有key

现在可以实现变化侦测功能了,但是前面的代码只能侦测数据中的某一个属性,我们需要把数据中的所有属性(包括子属性)都侦测,所以封装一个Observer类。代码如下:

// 监听对象的每个属性
class Observe {
	constructor(data) {
		this.value = data
		// 数据的监听方式不太一样,当前只考虑对象的监听方式
		if (!Array.isArray(data)) {
			this.walk(data)
		}
	}
	walk(data) {
    // 这里和defineReactive函数新增的代码形成递归,直到子属性的值为基本数据类型
		Object.keys(data).forEach(key => defineReactive(data, key))
	}
}
// 改造一下之前的defineReactive函数
function defineReactive(data, key, val = data[key]) {
  // 新增代码
	if (typeof val === 'object' && val !== null) {
		new Observe(val) 
	}

	let dep = new Dep()

	Object.defineProperty(data, key, {
		get() {
			// 收集
			dep.collect()
			return val
		},
		set(newVal) {
			if (val === newVal) return
			val = newVal
			// 发布
			dep.notify()
		},
	})
}

5.总结

        根据上面封装的几个类,以下面代码obj对象的执行做一个总结。我们创建一个Observe类通过递归把obj中所有的子属性都变成响应式的。接着我们创建一个Watcher实例,并订阅属性a.b.c。watcher内部会先把自己放在全局变量Window.target下,然后读取obj.a.b.c的值,读取后会触发getter中collect方法把它收集到dep中。当后续更改obj.a.b.c的值时,会触发setter中notify方法,它会把之前收集到的依赖循环调用实例上undata方法,最后执行回调函数。

const obj = {
  a: {
      b: {
          c: 1
      }
  }
}
new Observe(obj)

const watcher1 = new Watcher(obj, 'a.b.c', function (newVal, oldVal) {
	console.log('订阅1-->', newVal)
	console.log('订阅1-->', oldVal)
})

const watcher2 = new Watcher(obj, 'a.b.c', function (newVal, oldVal) {
	console.log('订阅2-->', newVal)
	console.log('订阅2-->', oldVal)
})

obj.a.b.c = 66

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值