通俗易懂的Vue响应式原理以及依赖收集
2020/7/2 11:25:31
本文主要是介绍通俗易懂的Vue响应式原理以及依赖收集,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
最近在看一些底层方面的知识。所以想做个系列尝试去聊聊这些比较复杂又很重要的知识点。学习就好比是座大山,只有自己去登山,才能看到不一样的风景,体会更加深刻。今天我们就来聊聊Vue中比较重要的响应式原理以及依赖收集。
响应式原理
Object.defineProperty() 和 Proxy 对象,都可以用来对数据的劫持操作。何为数据劫持呢?就是在我们访问或者修改某个对象的某个属性的时候,通过一段代码进行拦截,然后进行额外的操作,返回结果。vue中双向数据绑定就是一个典型的应用。
Vue2.x 是使用 Object.defindProperty(),来实现对对象的监听。
Vue3.x 版本之后就改用Proxy实现。
在MDN中是这样定义:
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
Object.defineProperty(obj, prop, descriptor)
- obj:要定义属性的对象
- prop:要定义或修改的属性名称
- descriptor:要定义或修改的属性描述符(configurable: 可改变的;writable:可写的;enumerable:可枚举的;get\set:设置或获取对象的某个属性的值)
const data = {} const name = 'zhangsan' Object.defineProperty(data, 'name', { writable: true, configurable: true, get: function () { console.log('get') return name }, set: function (newVal) { console.log('set') name = newVal } }) 复制代码
当把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。简单理解就是在data和用户之间做了一层代理中间层,在vue initData的时候,将_data上面的数据代理到vm上,通过observer类将所有的data变成可观察的,及对data定义的每一个属性进行getter\setter操作,这就是Vue实现响应式的基础。
Vue数据响应式变化主要涉及 Observer, Watcher , Dep 这三个主要的类。因此要弄清Vue响应式变化需要明白这个三个类之间是如何运作联系的;以及它们的原理,负责的逻辑操作。响应式原理(Observer)
Observer类是将每个目标对象(即data)的键值转换成getter/setter形式,用于进行依赖收集以及调度更新。那么在vue这个类是如何实现的:
- 1、observer实例绑定在data的ob属性上面,防止重复绑定;
- 2、若data为数组,先实现对应的变异方法(Vue重写了数组的7种原生方法)再将数组的每个成员进行observe,使之成响应式数据;
- 3、否则执行walk()方法,遍历data所有的数据,进行getter/setter绑定。这里的核心方法就是 defineReative(obj, keys[i], obj[keys[i]])
// 监听对象属性Observer类 class Observer { constructor(value) { this.value = value if (!value || (typeof value !== 'object')) { return } else { this.walk(value) } } walk(obj) { Object.keys(obj).forEach(key => { defineReactive(obj, key, obj[key]) }) } } 复制代码
function defineReactive(obj, key, val) { Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter() { return val }, set: function reactiveSetter(newVal) { // 注意:value一直在闭包中,此处设置完之后,再get时也是会得到最新的值 if (newVal === val) return updateView() } }) } function updateView() { console.log('视图更新了') } const data = { name: 'zhangsan', age: 20 } new Observer(data) data.name = 'lisi' // 打印‘视图更新了’ 复制代码
这就是简单的一个Observer类,这也是vue响应式的基本原理。但我们都知道 object.defineproperty的存在一些缺点:
1、对于复杂的对象需要深度监听,递归到底,一次性计算量大
2、无法监听新增属性/删除属性(Vue.set Vue.delete)
3、无法监听数组,需特殊处理,也就是上面说的变异方法
这也就是vue3改进的一方面,后文我们也会着重讲解vue3 proxy如何做响应式的。
扩展一、vue如何深度监听
上图中我们看到data中的一级目录name、age在值改变的时候,会出发视图更新,但在我们实际开发过程中,data可能会是比较复杂的对象,嵌套了好几层:
const data = { name: 'zhangsan', age: 20, info: { address: '北京' } } data.info.address = '上海' // 并没有执行。 复制代码
造成这种原因是,代码中defineReactive接收到的val是一个对象,为了避免这种复杂的对象vue采用递归的思想在defineReactive函数中在执行一次observer函数就行,递归将对象在遍历一次获取key/value值,new Observer(val)。同样在设置值的时候可能会把name也设置成一个对象,因此在data值更新的时候也需要进行判断深度监听
function defineReactive(obj, key, val) { new Observer(val) // 深度监听 Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter() { return val }, set: function reactiveSetter(newVal) { // 注意:value一直在闭包中,此处设置完之后,再get时也是会得到最新的值 if (newVal === val) return new Observer(val) // 深度监听 updateView() } }) } 复制代码
扩展二、vue数组的监听
object.defineproperty对数组是不起作用的,那么在vue中又是如何去监听数组的变化,其实Vue 将被侦听的数组的变更方法进行了包裹。接下来将用简单代码演示:
// 防止全局污染,重新定义数组原型 const oldArrayProperty = Array.prototype // 创建新对象,原型指向oldArrayProperty const arrProto = Object.create(oldArrayProperty); ['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => { arrProto[methodName] = function () { // 在定义数组的方法 updateView() oldArrayProperty[methodName].call(this, ...arguments) // 实际执行数组的方法 } }) // 在Observer函数中对数组进行处理 if (Array.isArray(value)) { value.__proto__ = arrProto } 复制代码
从代码中看到,在Observer函数有一层对数组进行拦截,将数组的__proto__指向了一个arrProto,arrProto是一个对象,这个对象指向数组的原型,因此arrProto拥有了数组原型上的方法,然后在这对象上重新自定义了数组的7中方法将其包裹,但又不会影响数组原型的方法,这就是变异,再将数组的每个成员进行observe,使之成响应式数据。
依赖收集(Watcher、Dep)
我们现在有这么一个Vue对象
new Vue({ template: `<div> <span>text1:</span> {{text1}} <div>`, data: { text1: 'text1', text2: 'text2' } }) 复制代码
我们可以从以上代码看出,data中text2并没有被模板实际用到,为了提高代码执行效率,我们没有必要对其进行响应式处理,因此,依赖收集简单理解就是收集只在实际页面中用到的data数据,那么Vue是如何进行依赖收集的,这也就是下面要讲的Watcher、Dep类了。
被Observer的data在触发 getter 时,Dep 就会收集依赖,然后打上标记,这里就是标记为Dep.target
Watcher是一个观察者对象。依赖收集以后的watcher对象被保存在Dep的subs中,数据变动的时候Dep会通知watcher实例,然后由watcher实例回调cb进行视图更新。
Watcher可以接受多个订阅者的订阅,当有data变动时,就会通过 Dep 给 Watcher 发通知进行更新。
我们可以用一些简单的代码去实现这个过程。
class Observer { constructor(value) { this.value = value if (!value || (typeof value !== 'object')) { return } else { this.walk(value) } } walk(obj) { Object.keys(obj).forEach(key => { defineReactive(obj, key, obj[key]) }) } } // 订阅者Dep,存放观察者对象 class Dep { constructor() { this.subs = [] } /*添加一个观察者对象*/ addSub (sub) { this.subs.push(sub) } /*依赖收集,当存在Dep.target的时候添加观察者对象*/ depend() { if (Dep.target) { Dep.target.addDep(this) } } // 通知所有watcher对象更新视图 notify () { this.subs.forEach((sub) => { sub.update() }) } } class Watcher { constructor() { /* 在new一个Watcher对象时将该对象赋值给Dep.target,在get中会用到 */ Dep.target = this; } update () { console.log('视图更新啦') } /*添加一个依赖关系到Deps集合中*/ addDep (dep) { dep.addSub(this) } } function defineReactive (obj, key, val) { const dep = new Dep() Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { dep.depend() /*进行依赖收集*/ return val }, set: function reactiveSetter (newVal) { if (newVal === val) return dep.notify() } }) } class Vue { constructor (options) { this._data = options.data new Observer(this._data) // 所有data变成可观察的 new Watcher() // 创建一个观察者实例 console.log('render~', this._data.test) } } let o = new Vue({ data: { test: 'hello vue.' } }) o._data.test = 'hello mvvm!' Dep.target = null 复制代码
总结
- 1、在Vue中模版编译过程中的指令或者数据绑定都会实例化一个Watcher实例,实例化过程中会触发get()将自身指向Dep.target;
- 2、data在Observer时执行getter会触发dep.depend()进行依赖收集,
- 3、当data中被 Observer的某个对象值变化后,触发subs中观察它的watcher执行 update() 方法,最后实际上是调用watcher的回调函数cb,进而更新视图。
Vue3-Proxy实现响应式
Proxy可以理解成在目标对象前架设一个拦截层,外界对该对象的出发必须先通过这层拦截层,因此提供了一种机制可以对外界的访问进行过滤和改写。
function reactive(value = {}) { if (!value || (typeof value !== 'object')) { return } // 代理配置 const proxyConf = { get(target, key,receiver) { // 只处理非原型的属性 let ownKeys = Reflect.ownKeys(target) if (ownKeys.includes(key)) { console.log('get', key) } const result = Reflect.get(target, key, receiver) // 深度监听 // 性能如何提升? 什么时候用什么时候递归 return reactive(result) }, set(target, key, val, receiver) { // 重复的数据不处理 const oldVal = target[key] if (val === oldVal) return true const ownKey = Reflect.ownKeys(target) if (ownKeys.include(key)) { console.log('已有的key', key) } else { console.log('新增的key', key) } const result = Reflect.set(target, key, val, receiver) console.log('set', key, val) return result }, deleteProperty(target, key) { const result = Reflect.deleteProperty(target, key) console.log('delete property', key) return result } } // 生成代理对象 const observed = new Proxy(value, proxyConf) return observed } const data = { name: 'zhangsan', age: 20, info: { address: '北京' }, num: [1, 2, 3] } const proxyData = reactive(data) proxyData.name ='lisi' // set name lisi 复制代码
proxy深度监听的性能提升,在proxy中对于复杂的对象,只会geter()的时候对当前层的监听,比如说在info中
info: { address: '北京', a: { b: { c: { d: 2 } } } } 复制代码
修改proxyData.info.a并不会把后面b、c、d递归出来,避免了object.defineProperty一次性全部递归计算完成。由于proxy原生对数组就能监听,所以也是对object.defineProperty缺点的一个改进。并且从代码中可以看出,在增加/删除时proxy也一样可以监听到,这就是proxy的优势。
扩展一、Reflect
reflect对象的方法和proxy对象的方法一一对应,只要是proxy对象的方法,就能在reflect对象找到对应的方法。这就使得proxy对象可以方便的调用对应的reflect方法来完成默认的行为,作为修改行为的基础。
Reflect有其实是对Object对象的规范化吧,将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty)放到Reflect对象上。
Reflect.get(target, name, receiver): 查到并返回target对象上的name属性,没有该属性会返回undefined
Reflect.set(target, name, value, receiver): 设置target对象的name属性等于value
Reflect.has(object, name): 判断对象上是否有name属性
Reflect.ownKeys(target): 返回对象的所有属性
扩展二、使用proxy实现观察者模式
// 使用proxy实现观察者模式 // 观察者模式指的是函数自动观察数据对象的模式,一旦数据有变化,数据就会自动执行 const queuedObservers = new Set() const observe = fn => queuedObservers.add(fn) const observable = obj => new Proxy(obj, {set}) function set(target, key, value, receiver) { const result = Reflect.set(target, key, value, receiver) queuedObservers.forEach(observe => observe()) return result } const person = observable({ // 观察对象 name: '张三', age: 20 }) function print() { // 观察者 console.log(`${person.name}, ${person.age}`) } observe(print) person.name = '李四' 复制代码
这篇关于通俗易懂的Vue响应式原理以及依赖收集的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-10-04package.json 文件位置在哪?-icode9专业技术文章分享
- 2024-10-01Craco.js学习:从入门到实践指南
- 2024-10-01Create-React-App学习:入门与实践指南
- 2024-10-01CSS-in-JS学习:从入门到实践指南
- 2024-09-30JSX语法学习:从入门到初步掌握
- 2024-09-30Mock.js学习:入门教程与实战演练
- 2024-09-30React Hooks学习:从入门到实践
- 2024-09-30受控组件学习:React中的基础入门教程
- 2024-09-29JS定时器教程:初学者必看指南
- 2024-09-29JS对象教程:初学者的全面指南