首先要知道vue2 是2013年 基于 ES5开发出来的
我们常说的重渲染就是重新运行render函数
vue2响应式原理简单来说就是vue官网上的这图片
通过 Object.defineProperty 遍历对象的每一个属性,把每一个属性变成一个 getter 和 setter 函数,读取属性的时候调用 getter,给属性赋值的时候就会调用 setter.
当运行 render 函数的时候,发现用到了响应式数据,这时候就会运行 getter 函数,然后 watcher(发布订阅)就会记录下来。当响应式数据发生变化的时候,就会调用 setter 函数,watcher 就会再记录下来这次的变化,然后通知 render 函数,数据发生了变化,然后就会重新运行 render 函数,重新生成虚拟 dom 树。
深入了解:
我们要明白,响应式的最终目标:是当对象本身或对象属性发生变化时,会运行一些函数,最常见的就是 render 函数。不是只有 render,只要数据发生了变化后运行了一些函数,就是响应式,比如 watch。
在具体实现上,vue 采用了几个核心部件:
1.Observer
2.Dep
3.Watcher
4.Scheduler
Observer
observer 要实现的目标非常简单,就是把一个普通的对象转换成响应式的对象
为了实现这一点,observer 把对象的每个属性通过 object.defineProperty 转换为带有 getter 和 setter 的属性,这样一来,当访问或者设置属性时,vue 就会有机会做一些别的事情。
在组件的生命周期中,这件事发生在 beforeCreate 之后,create 之前。
具体实现上,他会递归遍历对象的所有属性,以完成深度的属性转换。
但是由于遍历只能遍历到对象的当前属性,无法监测到将来动态添加或者删除的属性,因此 vue 提供了$set和$delete 两个实例方法,但是 vue 并不提倡这样使用,我讲到 dep 的时候我再说为什么。
对于数组的话,vue 会更改它的隐式原型,之所以这样做是因为 vue 需要监听那些可能改变数组内容的方法。
数组 --> vue 自定义的对象 --> Array.prototype
总之,observer 的目标,就是要让一个对象,它属性的读取,赋值,内部数组的变化都要能够被 vue 感知到。
Dep
这里有两个问题没解决,就是读取属性时要做什么事,属性变化时又要做什么事,这个问题就得需要 dep 来解决
dep 的含义是 dependency 表示依赖的意思。
vue 会为响应式对象中的每一个属性,对象本身,数组本身创建一个 dep 实例,每个 dep 实例都可以做两件事情:
1,记录依赖:是谁在用我
2,派发更新:我变了,我要通知那些用我的人
当读取响应式对象的某一个属性时,他会进行依赖收集,有人用到了我
当改变某个属性时,他会派发更新,那些用我的人听好了,我变了
为什么尽量不要使用$set $delete ?
因为如果模板上没有用到值的话,你凭空加了一个数据,理论上来说应该不会重新运行render函数,但是上一级的dep发现自身发生改变了,所以也会导致重新运行render函数。
所以vue不建议使用$set 和$delete,最好提前先写上数据,哪怕先给数据赋值为 null;
watcher
这里又出现了一个问题,就是 dep 如何知道是谁在用我呢
watcher 就解决了这个问题
当函数执行的过程中,用到了响应式数据,响应式数据是无法知道是谁在用自己的
所以,我们不要直接执行函数,而是把函数交给一个 watcher 的东西去执行,watch 是一个对象,每个函数执行时都应该创建一个 watcher,通过 wacher 去执行
watcher 会创建一个全局变量,让全局变量记录当前负责执行的 watcher 等于自己,然后再去执行函数,在函数执行的过程中,如果发生了依赖记录,那么 dep 就会把这个全局变量记录下来,表示有一个 wathcer 用到了我这个属性。
当 dep 进行派发更行时,他会通知之前记录的所有 watcher,我变了。
Scheduler
现在还剩下最后一个问题啊,就是 dep 通知 watcher 之后,如果 wathcer 执行重新运行对应的函数,就有可能导致频繁运行,从而导致效率低下,试想,如果一个交给 watcher 的函数,它里面用到了属性 a,b,c,d,那么 a,b,c,d 都会记录依赖,然后这四个值都以此重新赋值,那么就会触发四次更新,这样显然不行啊,所以当 watcher 收到派发更新的通知后,实际上并不是立即执行,而是通过一个叫做 nextTick 的工具方法,把这些需要执行的 watcher 放到事件循环的微队列,nextTick 是通过 Promise then 来完成的。
也就是说,在响应式数据发生变化时,render 函数执行是异步的,并且在微队列中。
https://blog.csdn.net/m0_46143427/article/details/124756831
https://blog.csdn.net/m0_54581080/article/details/126036155
--------Vue2响应式原理----------
原理:通过数据劫持 defineProperty + 发布订阅者模式,当 vue 实例初始化后 observer 会针对实例中的 data 中的每一个属性进行劫持并通过 defineProperty() 设置值后在 get() 中向发布者添加该属性的订阅者,这里在编译模板时就会初始化每一属性的 watcher,在数据发生更新后调用 set 时会通知发布者 notify 通知对应的订阅者做出数据更新,同时将新的数据根性到视图上显示。
缺陷:只能够监听初始化实例中的 data 数据,动态添加值不能响应,要使用对应的 Vue.set()。
1.第一步:对数据做数据劫持:
1.1对象类型做数据劫持
使用 Object.defineProperty 方法添加对象,重写了原有的 get 和 set 方法,这就是数据劫持。
defineRective 文件通过封装 Object.defineProperty 方法,把浅层次的对象中的某个数据变成具有 get 和 set 方法的属性。因为有闭包的存在,所以不需要临时变量进行周转了。
接下来是深层次:用递归侦测对象的全部属性
八个 JS 文件互相调用(文件代码,放在文章下方):
index.js:入口文件
observe.js:用于判断某一属性是否为对象或者数组,因为 typeof(array) 返回的也是 object,算是一个局限性。普通数据就直接 return,对象(数组)就给它调用 new Observer
Observer.js:对传入的属性做类型判断,然后分别转化为可被监测的属性。
def.js:为属性添加 ob 属性,做标记,而且可以通过 value.ob 来访问 Observer 的实例
defineRective.js:给传入的属性做数据劫持(即添加 set/get 方法),因为是对属性进行操作的,不做类型判断,因此不论这个传入过来的属性是数组还是对象,都会有 get/set 方法。
array.js:该文件将 JS 中能改变数组的 7 个方法重写,并在进行数据劫持的时候将,数组的原型指向该文件加工后的新原型。
Dep.js:在依赖收集阶段,Dep 对象是 Watcher 对象和 Observer 对象之间纽带,每一个 Observer 都有一个 Dep 实例,用来存储订阅者 Watcher
Watcher.js:当解析到模板字符串 {{ }} 时,会默认去 new Watcher 实例。
过程:
首先将需要做监听的对象传入 observe 方法内,如果传进去的不是对象(第一次传入的数据毫无疑问是对象,但是后续的子属性还会再次调用 observe 函数,子属性的类型就很复杂了,因此需要有这层判断),就会直接return。如果是对象(或者数组因为 typeof(array) 返回的也是 object),就往下走。
此时已经确定了,传入的是个对象(数组),紧接着会判断这个对象(数组)有没有 ob 属性,有则代表已做过监视了,如果没有,就用它 new 一个 Observer 实例。
在new Observer 实例的过程中,会调用 def 方法给该实例添加一个 ob 属性(做个标记)。然后如果是对象则调用 walk 方法,walk 会遍历该对象中的每一项并用 defineReactive 方法加工。如果是数组则修改它的原型(里边有重写好的 API)为 arrayMethods,随即调用 observeArray 方法。
defineReactive 方法用于对传入的属性做数据劫持(重写 get/set 方法)。因为是对属性进行操作的,因此即使传入的是数组,它也一定有 get/set 方法。
在 defineReactive 做数据劫持前,仍需再调用一次 observe 方法,去判断当前属性是否还是一个对象,如果是,就会再重复 1-3 的过程。此时这几个文件就形成了递归,直到某一次传入的属性不再是一个对象时,结束递归。当递归结束时,这个对象内的所有属性就都做好了数据劫持。
其中在 defineReactive 中还需要在 set 方法中将获取到的新值再一次使用 observe 方法,变成可监视的,因为这个新值也有可能是对象(或数组),如果不是,那么就会在 observe.js 文件中中直接 return 了。
1.2.数组类型做数据劫持:
思路:
在 array.js 文件中以 Array.prototype 为原型,复制出一个新原型 arrayMethods,再把这个新原型上的 7 个可以改变数组的 API 方法全部重写,分别是:push、pop、shift、unshift、splice、sort、reverse。
这 7 个方法在保留原功能的基础上增加一些数据劫持的代码(也就是将数据变为可监控的),最后把 arrayMethods 暴露出去。
当在 new Observer 时判断传入的数据为数组后,使用 Object.setPrototypeOf() 方法强制让传入的那个数组的原型指向 arrayMthods,这样一来,这个数组以后就会使用我们重写好的 API,就达成响应式的目的。
随后要再一次对数组内的数据进行遍历,因为数组内部,或许还会有对象类型(或者数组类型)的数据。这就相当于用一个拦截器覆盖 Array.prototype,每当使用 Array 原型上的方法操作数组时,先执行 arrayMthods 拦截器中的数据劫持方法,再执行原生 Array 的方法去操作数组。
和上述对象的文件嵌套相比,增加了一个 array.js 文件。
2.第二步:依赖收集
需要用到数据的地方,称为依赖
Vue1.x, 细粒度依赖, 用到数据的 D0M 都是依赖;
Vue2.x, 中等粒度依赖, 用到数据的 组件 是依赖;
之所以要劫持数据,目的是当数据的属性发生变化时,可以通知那些曾经用到的该数据的地方。所以要先收集依赖,把用到这个数据的地方收集起来,等属性改变后,在之前收集好的依赖中循环触发一遍就好了,达到响应式的目的。
针对不同的类型:
在 getter() 中收集依赖,在 setter() 中触发依赖 // 对象类型
在 getter() 中收集依赖,在 拦截器 中触发依赖 // 数组类型
1
2
2.1前提:
此时已经进行过数据劫持了。
把 new Watcher 这个过程看作是 Vue 解析到了 {{ }} 模板的时候。
Dep.target 的值存在时,表示正处于依赖收集阶段。
Vue 在模板编译过程中遇到的指令和数据绑定都会生成 Watcher 实例,实例中的 Watch 属性也会成生成 Watcher 实例。
2.2过程:
在创建 Observer 实例的同时还会创建 Dep 实例,用于保存依赖项。因此每个数据都有 Observer 的实例,每个 Observer 实例中又都有一个 Dep 的实例。
当 Vue 解析到 {{ }} 中数据时,就会去创建 Watcher 实例,在 constructor 时会调用自身的 get 方法,该方法不仅将当前的 Watcher 实例赋值给了 Dep.target(表示此时处于依赖收集阶段),还让这个新实例去读取一下 {{ }} 中的数据,一旦读取,就会触发这个数据的 getter 方法。因为此时正在进行收集依赖,Dep.target 一定是为 true 的,于是顺利地把当前的这个 Watcher 实例记录到了 dep 中的 subs 数组里。再然后将 Dep.target 的值重新赋值为 null,表示退出依赖收集阶段。
为什么能记录到 subs 数组呢?因为在 defineReactive 文件的 17 行新 new 了一个 Dep 实例,这个实例只是一个工具人,通过调用工具人身上的 depend 函数,就将当前时刻的 Watcher 实例添加进去。这样一来当模板解析完毕,dep 实例就掌握这个数据的所有订阅者。
当数据的 set 方法被调用时,就执工具人的 dep.notify 方法,他会遍历 dep 实例身上的 subs 数组,这个数组存放了当前数据的所有订阅者,即许多 Watcher 实例,调用每一个 Watcher 实例身上的 update 方法,执行传入过来的回调函数,然后 Vue 接下来通过这个回调函数去进行 diff 算法,对比新旧模板,然后重新渲染页面,至此算是达到了响应式的目的。
因此,这个 Watcher 实际上是 Vue 的主程序在用。更新视图的代码应该是要写在传入过去的回调函数里。
3.最后总结:
3.1前置知识:
首先要了解三个最重要的对象:
Observer 对象:将 Vue 中的数据对象在初始化过程中转换为 Observer 对象。
Watcher 对象:将模板和 Observer 对象结合在一起生成 Watcher 实例,Watcher 是订阅者中的订阅者。
Dep对象:Watcher 对象和 Observer 对象之间纽带,每一个 Observe r都有一个 Dep 实例,用来存储订阅者 Watcher。
3.2过程:
在生命周期的 initState 方法中将 data,prop,method,computed,watch等所有数据全部进行数据劫持,将所有数据变为 Observer 实例,并且每个数据身上还有 Dep 实例。
然后在 initRender 方法中也就是模板编译过程,遇到的指令和数据绑定都会生成 Watcher 实例,并且把这个实例存入对应数据的 Dep 实例中的 subs 数组里。这样每一个数据的 Dep 实例里就都存放了依赖关系。
当数据变化时,数据的 setter 方法被调用,触发 dep.notify 方法,就会通知 Dep 实例依赖列表,执行 update 方法通知 Watcher,Watcher 会执行 run 方法去更新视图。
更新视图的过程,我猜是 Vue 接下来要进行 diff 算法,对比新旧模板,然后重新渲染页面。
Vue 是无法检测到对象属性的添加和删除,但是可以使用全局 Vue.set 方法(或 vm.$set 实例方法)。
Vue 无法检测利用索引设置数组,但是可以使用全局 Vue.set方法(或 vm.$set 实例方法)。
无法检测直接修改数组长度,但是可以使用 splice。
————————————————
版权声明:本文为CSDN博主「高等数学真简单」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/m0_46143427/article/details/124756831
标签:数组,对象,Watcher,响应,实例,vue2,原理,数据,属性 From: https://www.cnblogs.com/niufang/p/17164075.html