学习记录_0831

VUE响应式原理

https://juejin.im/post/6850418111985352711VUE响应式原理

1.什么是VUE响应式?

当数据发生改变时,会重新渲染页面

2.完成这个过程 需要哪些步骤

a.侦测数据的变化 ========================》数据劫持/数据代理
b.收集视图依赖了哪些数据 ========================》依赖收集
c.数据发生变化时 自动通知需要更新的视图部分,并进行更新 =================》发布订阅模式

3.如何侦测数据变化

3.1 Object.Property
vue通过设定对象属性的 setter/getter 的方法拉监听数据的变化,通过 getter 方法进行依赖收集 而每个setter方法就是一个观察者 在数据变更的时候通知订阅者更新视图

function render(){
    
    
//set 时候走这里 重新渲染
	console.log("模拟视图渲染")
}
let data = {
    
    
	name:"张三"
	loction:{
    
     x:100,y:100}
}
observe(data)
// 定义核心函数
function observe(obj) {
    
    
	// 判断类型
	if( !obj || typeof !== 'object' ){
    
    
		return
	}
	Object.keys(obj).forEach( key => {
    
    
		defineReactive( obj,key,obj[key])
	})
	function defineReactive(obj,key,value){
    
    
		// 递归子属性
		observe(value)
		Object.defineProperty(obj,key,{
    
    
			enumerable:true, // 可枚举(可以遍历)
			configurable:true, // 可配置
			get: function reactiveGetter() {
    
    
				console.log( 'get',value)
				return value
			}
			set: function reactiveSetter( newVal )  {
    
    
				observe( newVal ) // 如果赋值是一个对象,也要递归子属性
				if( newVal !== value){
    
    
					console.log( 'set',newVal) //  
					return ()
					value = newVal
				}
			}
		})
	}
}

改变data的属性 会触发set 然后获取data的属性 会触发get

data.location = {
    
    
	x:1000,
	y:1000
}   // 打印    set{ x:1000,y:1000} 模拟视图渲染
data.name    // 打印  get 张三

observe函数传入一个需要被追踪变化的对象obj 通过遍历属性方式给对象的每个属性通过defineReactive方法处理 给每个属性添加set和get方法 以此达到实现侦测对象变化,observe会进行递归调用
侦测vue中data的数据:

class Vue {
    
    
// vue构造类
	constructor(options) {
    
    
		this._data = options.data
		observe(this._data)
	}
}

只要 new 一个 vue 对象 就将 data 中的数据进行追踪变化
但是以上代码无法检测对象属性的添加删除 是因为 vue 通过Object.defineProperty 将对象的 key 转换成getter/setter 的形式来追踪变化,但 getter/setter 只能追踪一个数据是否被修改,无法追踪新增属性和删除。 如果删除属性可以用 vm.$delete 实现 如果新增属性 可以用:
a.使用 Vue.set(location, key ,value) 方法向嵌套对象添加响应式属性
b.给这个对象重新赋值 data.location = {…data.location,key:value} Object.defineProperty 不能监听数组的变化,需要进行数组方法的重写

3.2 Proxy的实现
Proxy 是 js2015 的一个新特性。Proxy 的代理是针对整个对象的,而不是对象的某个属性,因此不同于Object.defineProperty 的必须遍历对象每个属性,Proxy 只需要做一层代理就能监听同级结构下的所有属性变化,对于深层结构,递归还是必要的。此外Proxy还支持代理数组的变化

function render() {
    
    
	console.log("模拟视图更新")
}
let obj = {
    
    
	name:"张三",
	age:{
    
     age:100 },
	arr:[ 1,2,3 ]
}
let handler = {
    
    
// 如果取的值是对象 就再对这个对象进行数据劫持
	get(target,key){
    
    
		if(typeof target[key] == 'object' && target[key] !== null){
    
    
			return new Proxy(target[key],handler)
		}
		return Reflect.get(target,key)
	}
	set(target,key,value){
    
    
	// key 为 length 时,表示遍历完了最后一个属性
		if( key === 'length') return true
		render()
		return Reflect.set(target,key,value)
	}
}
let proxy = new Proxy(obj,handler)
proxy.age.name = "李四"
console.log(proxy.age.name) // 模拟视图更新 李四
proxy.arr[0] = "李四"
console.log(proxy.arr) // 模拟视图更新 李四
proxy.arr.length--

优点:精简、一套代码对对象和数组都适用
缺点:兼容性不好

4.收集依赖

4.1 为什么要收集依赖
之所以要观察数据,是为了在数据属性发生变化时,可以通知那些曾经使用过该数据的地方 依赖收集是如何实现的 核心思想就是“事件发布订阅模式”
4.2 订阅者 Dep
收集依赖需要为依赖找一个存储依赖的地方,因此创建了Dep,用来收集依赖、删除依赖、向依赖发送消息。
先实现一个订阅者 Dep 类,用于解耦属性的依赖收集和派发更新操作,主要作用是用来存放 watcher 观察者对象。watcher 可以理解为一个中介角色,当数据发生变化时通知他,然后再通知其他地方。

class Dep {
    
    
	constructor () {
    
    
		// 用来存放 watcher 的数组
		this.subs = []
	}
	// 在 subs 中添加一个 watcher 对象
	addSub (sub) {
    
    
		this.subs.push(sub)
	}
	// 通知所有 watcher 对象更新视图
	notify () {
    
    
		this.subs.forEach( (sub) => {
    
    
			sub.update()
		})	
	}
}

以上代码主要做两件事:

  • 用 addSub 方法可以在目前的 Dep 对象中增加一个 watcher 的订阅操作
  • 用 notify 方法通知目前 Dep 对象的 subs 中的所有 watcher 对象触发更新操作。所以当需要收集依赖的时候调用addSub,当需要派发更新的时候调用 notify。
    调用:
let dp = new Dep()
dp.addSup( () => {
    
    
	// 收集依赖时
	console.log('emit here')
})
dp.notify() // 派发更新的时候

5.观察者 Watcher
Vue 中定义了一个 Watcher 类来表示观察订阅依赖。当属性发生变化后,需要通知用到数据的地方,而使用这个数据的地方有很多,而且类型还不一样,既有可能是模板,也有可能是用户自己写的一个 watch ,这时需要抽象出一个能集中处理这些情况的类。然后我们在收集阶段只收集这个封装好的类的实例进来,通知也只通知它一个,再由它负责通知其他地方。
收集依赖的目的是:将观察者 Watcher 对象存放到当前闭包的订阅者 Dep 的 subs 中。

class Watcher {
    
    
	constructor(obj,key,cb){
    
    
	// 将 Dep.target 指向自己
	// 然后触发属性的 getter 添加监听
	// 最后将 Dep.target 置空
		Dep.target = this
		this.cb = cb
		this.obj = obj
		this.key = key
		this.value = obj[key]
		Dep.target = null	
	}
	update () {
    
    
		// 获得新值
		this.value = this.obj[this.key]
		// 定义一个 cb 函数,用来模拟视图更新
		this.cb(this.value)
	}
}

在执行构造函数时将 Dep.target 指向自身,从而使得收集了对应的 Watcher,在派发更新时候取出对应的 Watcher,然后执行 update 函数。
依赖的本质就是 Watcher,如何收集依赖,总结起来就是一句话:在 getter 中收集依赖,在 setter 中触发依赖。先收集依赖,把用到该数据的地方收集起来,然后等属性发生变化时,把之前收集好的依赖循环触发一遍就行。
具体来说,当外界通过 Watcher 读取数据时,便会触发 getter 从而将 Watcher 添加到依赖中,哪个 Watcher 触发可 getter,就把哪个 Watcher 收集到 Dep 中。当数据发生变化时,会循环把所有 Watcher 都通知一遍。

改造 defineReactive 在自定义函数中添加依赖收集和派发更新相关代码,实现简易的数据响应式:

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) // 递归子属性
	let dp = new Dep() // 新增
	Object.defineProperty(obj,key,{
    
    
		enumerable:true// 可以枚举(遍历)
		configurable:ture // 可以配置
		get:function reactiveGetter () {
    
    
			console.log('get',value) // 监听
			// 将 Watcher 添加到订阅
			if(Dep.target) {
    
    
				dp.addSub(Dep.target)	// 新增
			}	
			return value
		}
		set:function reactiveSetter (newVal) {
    
    
			observe(newVal)  // 如果赋值是一个对象,也要递归子属性
			if(newVal !== value) {
    
    
				console.log('set',newVal)  // 监听
				render()
				value = newVal
				//  执行 watcher 的新方法
				dp.notify()	  // 新增
			}	
		}
	})
}
class Vue{
    
    
	constructor(options) {
    
    
		this._data = options.data;
		observe(this._data)
		//  新建一个 Watcher 对象,这时候 Dep.target 会指向这个 Watcher 对象
		new Watcher()
		console.log("模拟视图渲染")
	}
}

当 render function 被渲染的时候,读取所需对象的值,会触发 reactiveGetter 函数把当前的 Watcher 对象(存放在 Dep.target 中)收集到 Dep 类中,之后如果去修改对象的值,则会触发 reactiveSetter 方法,通知 Dep 类调用 nofity 来触发所有 Watcher 对象的 update 方法更新对应视图。

完整流程:

  • 在 new Vue() 后,Vue 会调用 _init 函数进行初始化,也就是 init 过程,在这个过程 Data 通过 Observe 转换成了 getter/setter 的形式,来对数据追踪变化,当被设置的对象被读取的时候执行 getter 函数,而在被赋值的时候执行 setter 函数。
  • 当外界通过 Watcher 读取数据时,会触发 getter 从而将 Watcher 添加到依赖中。
  • 在修改对象的值过程中,会触发对应的 setter,setter 通知之前依赖收集到得到的 Dep 中的每一个 Watcher,告诉他们自己的值改变了,需要重新渲染视图。这时候这些 Watcher 就会开始调用 update 来更新视图。

完整详细代码:

const Observe = function(data) {
    
    
	console.log(1)                          // 4 new Vue 开始执行
	// 循环修改为每个属性添加get set
	for(let key in data){
    
    
		defineReactive(data,key)	
	}
}

const defineReactive = function(obj,key){
    
    
	console.log(2)                            // 5 new Vue 开始执行
	// 局部变量dep,用于get set内部调用
	const dep = new Dep()
	// 获取当前值
	let val = obj[key]
	Object.defineProperty(obj,key,{
    
    

		// 设置当前描述属性为可被循环
		enumerable:true,
		// 设置当前描述属性可被修改
		configurable:true,
		get() {
    
    
			console.log(3)                             // 10 19
			console,log("in get")
			// 调用依赖收集器中的 addSub,用于收集当前属性与 Watcher 的中依赖关系
			dep.depend()	
			return val
		}
		set() {
    
    
			console.log(4)	                              // 15
			if( newVal === val ) {
    
    
				return	
			}
			val = newVal
			// 当值发生变更时,通知依赖收集器,更新每个需要更新的Watcher,
			// 这里每个需要更新通过什么断定?dep.subs
			dep.notify()
		}
	})
}

const observe = faunction(data) {
    
    
	console.log(5)                         // 3 new Vue 开始执行
	return new Observe(data)
}

const Vue = function(options) {
    
    
	console.log(6)               // 1 new Vue 开始执行
	const self = this
	// 将 data 赋值给 this._data,源码用的是 Proxy,这里用最简单的方式临时实现
	if(options && typeof options.data === 'function') {
    
    
		console.log(7)                // 2 new Vue 开始执行
		this._data = options.data.apply(this)	
	}
	// 挂载函数
	this.mount = function() {
    
    
		console.log(8)                     // 7 new Vue以后,执行 vue.mounted()
		new Watcher(self,self.render)	
	}
	// 渲染函数
	this.render = function() {
    
    
		console.log(9)                                        // 9 18 render函数执行后走到这里
		with(self) {
    
    
			_data.text	                                      // 这里取 data 值的时候,走 get 方法
		}	
	}
	// 监听this._data
	observe(this._data)                    // new Vue 时候执行,这里执行完,就表示 new Vue 的过程执行完了
}

const Watcher = function(vm,fn) {
    
    
	console.log(10)                    // 8 执行 vue.mounted()后走到这里
	const self = this
	this.vm = vm
	// 将当前 Dep.target 指向自己
	Dep.target = this
	// 向 Dep 方法添加当前 Watcher
	this.addDep = functhion(dep) {
    
    
		console.log(11)                           // 13
		dep.addSub(self)	
	}
	// 更新方法,用于触发 vm._render
	this.update = function() {
    
    
		console.log(12)                               // 17
		console.log("in watcher update")
		fn()	
	}
	// 这里会首次调用 vm._render,从而触发text 的 get
	// 从而将当前的Watcher与Dep关联起来
	this.value = fn()                                 // 9 fn 是 render 函数,这里 fn()就会赋值的时候执行
	// 这里清空了 Dep.target,防止 notify 触发时,不停地绑定 Watcher 与 Dep,造成代码死循环
	
	Dep.target = null
}

const Dep = function() {
    
    
	console.log(13)                        // 6 new Vue 时候会执行 new Dep,执行到这里
	const self = this
	// 收集目标
	this.target = null
	// 存储收集器中需要通知的 Watcher
	this.subs = []
	// 当有目标时,绑定 Dep 和 Watcher 的关系
	
	this.depend = function() {
    
    
		console.log(14)                              // 11 20 走了 get 获取属性后,就要进行依赖收集
		if(Dep.target) {
    
    
			console.log(15)                          // 12
			// 这里可以直接写 self.addSub(Dep.target)
			// 
			Dep.target.addDep(self)	
		}
	}
	// 为当前收集器添加Watcher
	this.addSub = function(watcher) {
    
    
		console.log(16)                              // 14
		self.subs.push(watcher)
	}
	// 通知收集器中所的所有Wathcer,调用其update方法
	this.notify = function() {
    
    
		console.log(17)                            // 16
		for(let i = 0.i<self.subs.length;i += 1) {
    
    
			self.subs[i].update()	
		}	
	}
}

const vue = new Vue({
    
    
	data() {
    
    
		return {
    
    
			text:"hello world"	
		}	
	}
})

vue.mount()
vue._data.text = "123"

解析:

  1. new Vue,执行 Vue 构造函数,打印6
  2. Vue 的入参 options 实际上是 { data(){} },是一个包含了 data 函数的对象,所以 options.data 是一个 data 函数,打印7,将 vue 中的 data 函数返回的数据赋值给 _data。
  3. 67行的observe,执行41行定义的地方
  4. 43行 new Observe 的时候执行第一行 Observe(关键函数),打印1。Observe 实际上就是给data数据都添加 get 和 set 方法,只不过添加方法被 defineReactive 抽离出去了
  5. 执行9行,执行 defineReactive,打印2,然后15行给每个属性加上 set 和 get 方法
  6. 执行12行,new Dep 的时候到95行执行 Dep,打印13。Dep 函数剩下的代码只是定义函数,不会执行,跳出 Dep 函数。回到13行,defineReactive 剩下的代码中的函数也不会执行,所以会回到67行 Observe,new Vue 就执行完了
  7. 执行135行的 vue.mount(),走到56行,打印8
  8. 执行 new Watcher 走到70行,打印10,然后Dep.target = this,这一步将 Watch 实例挂载到了 Dep的 target 属性上,关联起来
  9. 72到88行只是定义,没有执行,89行中 this.value = fn() 中:fn 实际是传进来的 render 函数(57行)然后后面又加了()就会立即执行。然后执行60行的 render 函数,打印9。Watcher 就执行完了,打印完9会继续往下走,读取_data.text。这一步会触发 get 方法(这一步目的就只是触发 get,所以获取值就可以了,并不需要其他操作)
  10. 执行21行的 get,打印3
  11. 执行25行的 dep.depend(),到104行,打印14
  12. 这时候判断 Dep.target,由于第8步将 Watch 挂载到了 Dep.target 上,所以为 true,打印15
  13. 执行110行,执行77行,打印11
  14. 执行114行,打印16,完成了依赖收集,然后会回到 Watch ,执行最后一行,Dep.target = null,避免进入死循环,然后 Watch 执行完(Vue.mount也执行完了)
  15. 136行赋值操作,执行28行 set,打印4
  16. 向下执行,36行,dep.notify(),执行119行,打印17
  17. 执行122行,触发 update,执行82行,打印12
  18. 执行 fn 函数,即 render 函数,执行60行,打印9
  19. 执行63行,取 data 值,走到 get,21行,打印3
  20. 执行25行,调到104行,打印14,Dep.target 为 null,15不会打印

猜你喜欢

转载自blog.csdn.net/vigorZ/article/details/108319170