vue响应式原理

响应式原理的核心点

  • Observer把所有数据转成响应式
  • 通过Compile来解析编译模板指令
  • 在解析指令过程中,new Watcher生成一个对某数据的依赖实例
  • 通过Dep依赖收集器,在new Watcher的时候会触发getter收集某个数据的依赖对象,当监听到数据修改时,会触发当前数据的dep去遍历每个依赖,执行每个依赖的callback更新函数

在这里插入图片描述

响应数据结构图:
每个对象/数组上都有一个Observer实例对象,这个实例对象里面又有一个Dep实例
在这里插入图片描述

把所有数据转为响应式

  1. 怎么把数据全部转为响应式(可被监听)
    1)给每个对象实例一个new Observer(value),如果observe(obj)监听的是对象,往对象上的挂载原型对象__ob__:new Observer(),如果传入的参数不是对象就return
    2)new Observer()的作用是:给每个数据对象实例一个Dep依赖收集器;判断是数据是对象还是数组,分别调用不同的方法去处理
    3)遍历对象时,都会使用Object.defineProperty()来对数据进行劫持,需要用到它里面的getter和setter方法,访问数据时,触发当前数据的dep对收集此数据的watcher依赖对象,当数据改变时,需要遍历当前的数据的dep里面的全部依赖对象,调用依赖对象的callback函数进行数据的更新
    4)遍历数组时,强行让数据使用新方法处理数组Object.setPrototypeOf(value, arrayMethods)
// 创建observe函数
function observe(value) { // b{}
	// 如果value不是对象,什么都不做
	if (typeof value != 'object') return
	// 定义ob
	var ob
	if (typeof value.__ob__ !== 'undefined') {
		ob = value.__ob__
	} else { 
		ob = new Observer(value)
	}
	return ob  // 最终结果 b{__ob__:new Observer(b)}
}

export default observe
  1. 对对象的处理
    遍历每个对象转响应式observe(obj)
    getter的时候遍历对象的孩子,如果它的孩子是对象,继续遍历它的每个属性转响应式observe(obj)
    setter时也要对新值进行一次转响应式(防止新值又是一个对象)
import observe from './observe.js'
import Dep from './Dep.js'

// 闭包环境
export default function defineReactive(data,key,val){
  const dep = new Dep();
  if(arguments.length == 2){
    val = data[key];
  }
  //子元素要进行observe,形成递归,多个函数循环调用
  let childOb = observe(val);
  Object.defineProperty(data,key,{ 
    //可枚举
    enumerable:true,
    //可以被配置,比如可以被delete
    configurable:true,

    //getter
    get() {
      console.log(`打开${key}属性`)
      //如果现在处于依赖收集阶段
      if(Dep.target){
        dep.depend();
        if(childOb){
          childOb.dep.depend()
        };
      }
      return val;
    },
    //setter
    set(newValue) {
      console.log(`改变obj的${key}属性`,newValue)
      if(val === newValue){
        return;
      }
      val = newValue;
      //当设置新值,这个新值也要被observe
      childOb = observe(newValue)
      //发布订阅模式,通知dep
      dep.notify();
    }
  });
}

在这里插入图片描述

  1. 对数组的处理
    1)实例化Observer时,检测到是数组,强行让数据使用新方法处理数组Object.setPrototypeOf(value, arrayMethods)
    2)以Array.prototype为原型,创建arrayMethods对象,遍历数组各个方法,为每个数组方法扩展回原数组的方法和对新插入数据转换为响应式并通知订阅
    • 调用Object.definedProperty(arrayMethods,methodName,{})给arrayMethods设置某个数组方法,调用这个数组方法(提前备份原数组方法)
    • push、unshift、splice等方法都可能加入新数据,需要遍历新数据依次给新数据增加响应式,然后调用notify方法
// array.js
import def from './util.js'

// 得到Array的prototype
const arrayPrototype = Array.prototype

// 以Array.prototype为原型创建arrayMethods
const arrayMethods = Object.create(arrayPrototype)

// 要被改写的七个数组方法
const methodsNeedChange = [
	'push',
	'pop',
	'shift',
	'unshift',
	'sort',
	'splice',
	'reverse'
]

methodsNeedChange.forEach(methodName => {
	// 备份原来的方法
	const original = arrayPrototype[methodName]

	//定义新的方法
	// Object.defineProperty(obj, key, {value,...} )
	def(arrayMethods, methodName, function(){
		const result = original.apply(this, arguments)
		// 把类数组转换成数组
		const args = [...arguments]
		// 把这个数组上的__ob__取出来
		const ob = this.__ob__
		let inserted = []
		switch (methodName) {
			case 'push':
			case 'unshift':
				inserted = arguments
				break
			case 'splice':
				inserted = args.slice(2)
				break
		}
		// 判断有没有要插入的新项
		if (inserted) {
			ob.observeArray(inserted)
		}
		ob.dep.notify()
		return result
	}, false)

})

缕清Watcher依赖和Dep依赖管器的关系

在这里插入图片描述

  1. Watcher主要用来干嘛?
    监听数据的变化(在vue的源码底层方法中都需要用到它),如果数据发生变化,执行Watcher
    比如当用户输入值或者更新值时obj.a.m.n=‘888’,会触发dep收集这个Watcher依赖,
    Watcher是一个个依赖,只有watcher触发的getter(即new Watcher(obj, 'a.m')这样触发的getter)才会去收集依赖,哪个watcher发出getter,就把那个watcher收集到Dep中去

so,有new Watcher()的地方才会进入依赖收集阶段

// Watcher的结构
{
	id: 1,
	value:'1111',
	callback:(){}
}
  1. 在哪些地方用到Watcher?
    比如在compiler编译器中,解析指令或绑定表达式{{}}中需要new Watcher(obj, 'a.m',(val)=>{...})去监听{{}}里面的数据,收集它的依赖,如果值有变化的时候,执行这个watcher的更新函数
// dir:要做的指令名称
// 一旦发现一个动态绑定,都要做两件事情,首先解析动态值;其次创建更新函数
// 未来如果对应的exp它的值发生变化,执行这个watcher的更新函数
 update(node, exp, dir) {
  // 初始化
  const fn = this[dir + 'Updater']
  fn && fn(node, this.$vm[exp])

  // 更新,创建一个Watcher实例
  new Watcher(this.$vm, exp, val => {
    fn && fn(node, val)
  })
}

textUpdater(node, val) {
  node.textContent = val
}

htmlUpdater(node, val) {
  node.innerHTML = val
}
  1. Watcher依赖怎么被收集到Dep?
    首先会设置一个全局变量Dep.target或者window.target都可以,用这个变量来挂载当前的Watcher实例出来的依赖
    每当new Watcher(obj, ‘a.m’)的时候就把当前实例赋值给Dep.target,然后去获取值a.m,这时候会调用getter,在getter里收集依赖this.addSub(Dep.target),把全局的Dep.target属性订阅到依赖管理器dep里,全部收集完事,清空Dep.target=null
// watcher.js
import Dep from './Dep.js'
var uid = 0
class Watcher {
	constructor(target, expression, callback) {
		this.id = uid++
		this.target = target
		this.getter = parsePath(expression)
		this.callback = callback
		this.value = this.get()
	}
	get() {
		// 进入依赖收集阶段,让全局的Dep.target设置为watcher本身,那么就是进入依赖收集阶段
		Dep.target = this   // 把当前实例this赋值给Dep.target
		const obj = this.target
		var value
		try {
			value = this.getter(obj) // 获取数据,相当与触发getter获取value值
		} finally {
			Dep.target = null // 等到最后收集完毕才清空
		}
		return value
	}
}

// defineReactive.js
const dep = new Dep()
if (arguments.length == 2) {
	val = data[key]
}
// 子元素要进行observe, 至此形成递归,这个递归不是函数自己调用自己,而是多个函数,类循环调用
let childOb = observe(val)
Object.defineProperty(data, key, {
	enumerable: true,
	configurable: true,
	get() {
		console.log("触发getter", key, val);
		// 判断全局的Dep.target有值
		if (Dep.target) {
			dep.addDepend()
			if (childOb) {
				childOb.dep.addDepend()
			}
		}
		return val
	},
	set(newVal) {//...}
})

// index.js
// 生成一个wather依赖对象,监听'a.m.n',当数据有变化时,调用回调
// 此时也相当于调用数据,会触发getter函数里面的dep收集当前数据对象的依赖
new Watcher(obj, 'a.m.n',(val)=>{
	console.log('ssssss', val)
})

总结:

  • watcher就是依赖,只有wacther触发的getter才会收集依赖,哪个数据的生成的watcher依赖,才会被收集到那个数据的dep中
  • dep使用了发布订阅模式,当数据发生变化时,会遍历依赖列表,把所有watcher的回调执行一遍
  • watcher把自己设置到全局的一个指定位置,然后读取数据,因为读取了数据,才会触发这个数据的getter,在getter中就能得到当前数据的watcher,并把watcher收集到dep中

源码下载

参考:邵山欢Vue之数据响应式原理

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值