Vue 响应式原理

Vue 响应式原理

响应式例子

		<div id="app">
			<div>Price :¥{{ price }}</div>
			<div>Total:¥{{ price * quantity }}</div>
			<div>Taxes: ¥{{ totalPriceWithTax }}</div>
			<button @click="changePrice">改变价格</button>
		</div>
		<script>
			var app = new Vue({
				el: '#app',
				data() {
					return {
						price: 5.0,
						quantity: 2
					};
				},
				computed: {
					totalPriceWithTax() {
						return this.price * this.quantity * 1.03;
					}
				},
				methods: {
					changePrice() {
						this.price = 10;
					}
				}
			})
		</script>

上例中当price 发生变化的时候,Vue就知道自己需要做三件事情:

  • 更新页面上price的值
  • 计算表达式 price*quantity 的值,更新页面
  • 调用totalPriceWithTax 函数,更新页面

数据发生变化后,会重新对页面渲染,这就是Vue响应式,那么这一切是怎么做到的呢?
想完成这个过程,我们需要:

  • 侦测数据的变化
  • 收集视图依赖了哪些数据
  • 数据变化时,自动“通知”需要更新的视图部分,并进行更新

对应专业俗语分别是:

  • 数据劫持 / 数据代理
  • 依赖收集
  • 发布订阅模式

如何侦测数据变化

方法一.Object.defineProperty

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

			function render() {
				console.log('模拟视图渲染')
			}
			let data = {
				name: '浪里行舟',
				location: {
					x: 100,
					y: 100
				}
			}
			observe(data)

			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)
					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) // 监听
								render()
								value = newVal
							}
						}
					})
				}
			}
			data.location = {
				x: 1000,
				y: 1000
			} //set {x: 1000,y: 1000} 模拟视图渲染
			data.name // get 浪里行舟

上面的方法存在几点问题:

  1. 无法检测到对象属性的添加或者删除
  • 可以使用vm.$delete 追踪删除属性;使用Vue.set()方法想嵌套对象添加响应式属性,也可以对这个对象重新赋值。
  1. 不能监听数组的变化,需要进行数组方法的重写
function render() {
  console.log('模拟视图渲染')
}
let obj = [1, 2, 3]
let methods = ['pop', 'shift', 'unshift', 'sort', 'reverse', 'splice', 'push']
// 先获取到原来的原型上的方法
let arrayProto = Array.prototype
// 创建一个自己的原型 并且重写methods这些方法
let proto = Object.create(arrayProto)
methods.forEach(method => {
  proto[method] = function() {
    // AOP
    arrayProto[method].call(this, ...arguments)
    render()
  }
})
function observer(obj) {
  // 把所有的属性定义成set/get的方式
  if (Array.isArray(obj)) {
    obj.__proto__ = proto
    return
  }
  if (typeof obj == 'object') {
    for (let key in obj) {
      defineReactive(obj, key, obj[key])
    }
  }
}
function defineReactive(data, key, value) {
  observer(value)
  Object.defineProperty(data, key, {
    get() {
      return value
    },
    set(newValue) {
      observer(newValue)
      if (newValue !== value) {
        render()
        value = newValue
      }
    }
  })
}
observer(obj)
function $set(data, key, value) {
  defineReactive(data, key, value)
}
obj.push(123, 55)
console.log(obj) //[1, 2, 3, 123,  55]

这种方法将数组的常用方法进行重写,进而覆盖掉原生的数组方法,重写后的数组能够进行拦截。

方法二.Proxy

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) {
    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) // 模拟视图的更新 ['浪里行舟', 2, 3 ]
proxy.arr.length-- // 无效

如何收集依赖

核心思想就是“事件发布订阅模式”–订阅者Dep和观察者Watcher。

订阅者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.addSub(()=>{
  console.log('emit here');
})
dp.notify();

观察者 Watcher

1.为什么引入Watcher

Vue 中定义一个 Watcher 类来表示观察订阅依赖。至于为啥引入Watcher,《深入浅出vue.js》给出了很好的解释:
当属性发生变化后,我们要通知用到数据的地方,而使用这个数据的地方有很多,而且类型还不一样,既有可能是模板,也有可能是用户写的一个watch,这时需要抽象出一个能集中处理这些情况的类。然后,我们在依赖收集阶段只收集这个封装好的类的实例进来,通知也只通知它一个,再由它负责通知其他地方。

依赖收集的目的是将观察者 Watcher 对象存放到当前闭包中的订阅者 Dep 的 subs 中:

image.png

watcher的实现
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)
  }
}

收集依赖
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: true, //可配置(比如可以删除)
      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 的 update 方法
          dp.notify() //新增
        }
      }
    })
  }
}
class Vue {
    constructor(options) {
        this._data = options.data;
        observer(this._data);
        /* 新建一个Watcher观察者对象,这时候Dep.target会指向这个Watcher对象 */
        new Watcher();
        console.log('模拟视图渲染');
    }
}

image.png

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值