Vue中的watch

Vue可以说存在三种watcher

  • 第一种是维持数据响应式的render watcher

  • 第二种是computed watcher,是computed函数在自身内部维护的一个watcher,配合其内部的属性dirty开关来决定computed的值是需要重新计算还是直接复用之前的值;

  • 第三种就是watcher api了,就是用户自定义的export导出对象的watch属性

    当然实际上他们都是通过class Watcher类来实现的

Vue 的响应式原理(render watch)

Vue 的响应式系统通过观察者模式(Observer Pattern)实现,当数据发生变化时,依赖于这些数据的地方(如模板渲染或计算属性)会被通知并更新。

  • Observer: 这里的主要工作是递归地监听对象上的所有属性,在属性值改变的时候,触发相应的Watcher
  • Watcher: 观察者,当监听的数据值修改时,执行响应的回调函数,在Vue里面的更新模板内容。
  • Dep: 链接ObserverWatcher的桥梁,每一个Observer对应一个Dep,它内部维护一个数组,保存与该Observer相关的Watcher
  1. Observer 对象:(初始化时对对象属性的依赖追踪)

    __ob__ :当 Vue 初始化数据时,每个响应式对象都会被挂载一个 __ob__ 属性(Observer 实例),这个属性指向对象的 Observer 实例。Observer 会遍历对象的属性,并将这些属性转化为响应式,即使用 Object.defineProperty 将每个属性的 getter 和 setter 拦截,并在对象属性更新时,触发与该属性相关的 watcher 进行更新。

     function __observe(obj){
         for(let item in obj){
             let dep = new __dep(); //建立Dep ,管理和watch的连接
             let value = obj[item];
             if (Object.prototype.toString.call(value) === "[object Object]") __observe(value);
             Object.defineProperty(obj, item, {
                 configurable: true,
                 enumerable: true,
                 get: function reactiveGetter() {
                     if(__dep.target) dep.addSub(__dep.target);  //添加watch依赖
                     return value;
                 },
                 set: function reactiveSetter(newVal) {
                     if (value === newVal) return value;
                     value = newVal;
                     dep.notifyAll();
                 }
             });
         }
         return obj;
     }
    
    • getter:当访问该属性时,会触发 getter,Vue 会进行依赖收集。此时,当前的 watcher(如模板渲染的 watcher)会被添加到这个属性的 Dep 中,表示该 watcher 依赖于这个属性。
    • setter:当修改该属性时,会触发 setter,并通过 dep.notify() 通知所有依赖这个属性的 watcher,使其更新。
    2.Dep 对象

    Dep:每个响应式属性(或依赖)都有一个对应的 Dep 实例。这个 Dep 用来收集依赖该属性的所有 watcher,并在属性变化时通知它们

     function __dep(){
         this.subscribers = [];
         this.addSub = function(watcher){
             if(__dep.target && !this.subscribers.includes(__dep.target) )  this.subscribers.push(watcher); //如果检测到有新增的watch, 添加
         }
         this.notifyAll = function(){
             this.subscribers.forEach( watcher => watcher.update());
         }
     }
    

当响应式数据的 setter 被触发(例如用户改变数据),该属性的 Dep 会调用 notify 方法,通知所有收集的 watcher,执行它们的 update 方法,重新渲染或执行其他更新操作

3. Watcher 与 Dep 的关联

在 Vue 中,watcher 是观察者对象,它会订阅多个响应式属性。当某个响应式属性改变时,watcher 会被通知,执行对应的更新操作。

  • watcher 被创建时,会触发与它相关的依赖属性的 getter,从而让这些属性的 Dep 收集这个 watcher
  • 例如,当渲染模板时,一个 render watcher 被创建,模板中用到的所有数据属性都会触发 getter,并将 render watcher 记录到这些属性的 Dep 中。
 function __watcher(fn){
     this.update = function(){
         fn();
     }
     
     this.activeRun = function(){
         __dep.target = this;
         fn();
         __dep.target = null;
     }
     this.activeRun();
 }
总结流程
  1. 初始化响应式对象

    • Vue 在初始化时,使用 Observer 将对象转为响应式,并通过 __ob__ 标记这个对象已经被观察。
  2. 依赖收集

    • 每个响应式属性的 getter 被调用时,Vue 会将当前的 watcher(如渲染 watcher)添加到该属性的 Dep 中。此时,watcher 就依赖了该属性。
  3. 属性更新

    • 当响应式属性的 setter 被调用时,Vue 会触发该属性的 Dep.notify(),通知所有依赖这个属性的 watcher,并执行更新逻辑。
简化代码示意:
 <!DOCTYPE html>
 <html>
 <head>
     <title>数据绑定</title>
 </head>
 <body>
     <div id="app">
         <div>{{msg}}</div>
         <div>{{date}}</div>
     </div> 
 </body>
 <script type="text/javascript">
 ​
     var Mvvm = function(config) {
         this.$el = config.el;
         this.__root = document.querySelector(this.$el);
         this.__originHTML = this.__root.innerHTML;
 ​
         function __dep(){
             this.subscribers = [];
             this.addSub = function(watcher){
                 if(__dep.target && !this.subscribers.includes(__dep.target) ) this.subscribers.push(watcher);
             }
             this.notifyAll = function(){
                 this.subscribers.forEach( watcher => watcher.update());
             }
         }
 ​
 ​
         function __observe(obj){
             for(let item in obj){
                 let dep = new __dep();
                 let value = obj[item];
                 if (Object.prototype.toString.call(value) === "[object Object]") __observe(value);
                 Object.defineProperty(obj, item, {
                     configurable: true,
                     enumerable: true,
                     get: function reactiveGetter() {
                         if(__dep.target) dep.addSub(__dep.target);
                         return value;
                     },
                     set: function reactiveSetter(newVal) {
                         if (value === newVal) return value;
                         value = newVal;
                         dep.notifyAll();
                     }
                 });
             }
             return obj;
         }
 ​
         this.$data = __observe(config.data);
 ​
         function __proxy (target) {
             for(let item in target){
                 Object.defineProperty(this, item, {
                     configurable: true,
                     enumerable: true,
                     get: function proxyGetter() {
                         return this.$data[item];
                     },
                     set: function proxySetter(newVal) {
                         this.$data[item] = newVal;
                     }
                 });
             }
         }
 ​
         __proxy.call(this, config.data);
 ​
         function __watcher(fn){
             this.update = function(){
                 fn();
             }
             
             this.activeRun = function(){
                 __dep.target = this;
                 fn();
                 __dep.target = null;
             }
             this.activeRun();
         }
 ​
         new __watcher(() => {
             console.log(this.msg, this.date);
         })
 ​
         new __watcher(() => {
             var html = String(this.__originHTML||'').replace(/"/g,'\"').replace(/\s+|\r|\t|\n/g, ' ')
             .replace(/{{(.)*?}}/g, function(value){ 
                 return  value.replace("{{",'"+(').replace("}}",')+"');
             })
             html = `var targetHTML = "${html}";return targetHTML;`;
             var parsedHTML = new Function(...Object.keys(this.$data), html)(...Object.values(this.$data));
             this.__root.innerHTML = parsedHTML;
         })
 ​
     }
 ​
     var vm = new Mvvm({
         el: "#app",
         data: {
             msg: "1",
             date: new Date(),
             obj: {
                 a: 1,
                 b: 11
             }
         }
     })
 ​
 </script>
 </html>

上面主要是响应式数据的实现,watch为render watch 除此之外 ,watch还有两种

computed watcher

computed函数在自身内部维护的一个watcher,配合其内部的属性dirty开关来决定computed的值是需要重新计算还是直接复用之前的值。

computed计算属性可以定义两种方式的参数,{ [key: string]: Function | { get: Function, set: Function } }

计算属性直接定义在Vue实例中,所有gettersetterthis上下文自动地绑定为Vue实例

此外如果为一个计算属性使用了箭头函数,则this不会指向这个组件的实例,不过仍然可以将其实例(vn)作为函数的第一个参数来访问,计算属性的结果会被缓存,除非依赖的响应式property变化才会重新计算,注意如果某个依赖例如非响应式property在该实例范畴之外,则计算属性是不会被更新的。

watcher api

对于watch api,类型{ [key: string]: string | Function | Object | Array }

Vue实例将会在实例化时调用$watch(),遍历watch对象的每一个property

watch api中可以定义deepimmediate属性,分别为深度监听watch和最初绑定即执行回调的定义,在render watch中定义数组的每一项由于性能与效果的折衷是不会直接被监听的,但是使用deep就可以对其进行监听

当然在Vue3中使用Proxy就不存在这个问题了,这原本是Js引擎的内部能力,拦截行为使用了一个能够响应特定操作的函数,即通过Proxy去对一个对象进行代理之后,我们将得到一个和被代理对象几乎完全一样的对象,并且可以从底层实现对这个对象进行完全的监控

不应该使用箭头函数来定义watcher函数,例如searchQuery: newValue => this.updateAutocomplete(newValue),理由是箭头函数绑定了父级作用域的上下文,所以this将不会按照期望指向Vue实例,this.updateAutocomplete将是undefined

  watch: {
             a: function(n, o){ // 普通watcher
                 console.log("a", o, "->", n);
             },
             b: { // 可以指定immediate属性
                 handler: function(n, o){
                     console.log("b", o, "->", n);
                 },
                 immediate: true
             },
             c: [ // 逐单元执行
                 function handler(n, o){
                     console.log("c1", o, "->", n);
                 },{
                     handler: function(n, o){
                         console.log("c2", o, "->", n);
                     },
                     immediate: true
                 }
             ],
             d: {
                 handler: function(n, o){ // 因为是内部属性值 更改不会执行
                     console.log("d.e1", o, "->", n);
                 },
             },
             "d.e": { // 可以指定内部属性的值
                 handler: function(n, o){
                     console.log("d.e2", o, "->", n);
                 }
             },
             f: { // 深度绑定内部属性
                 handler: function(n){
                     console.log("f.g", n.g);
                 },
                 deep: true
             }
         }

参考

Vue中的三种Watcher - Blog (touchczy.top)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值