资讯专栏INFORMATION COLUMN

源码学习VUE之Watcher

sutaking / 1539人阅读

摘要:这里面还有一些问题对应的回调函数这就是执行上下文收集依赖同步异步更新所谓的同步更新是指当观察的主体改变时立刻触发更新。

我们在前面推导过程中实现了一个简单版的watcher。这里面还有一些问题

class Watcher {
    constructors(component, getter, cb){
        this.cb = cb // 对应的回调函数,callback
        this.getter = getter;
        this.component = component; //这就是执行上下文
    }
    
    //收集依赖
    get(){
        Dep.target = this;        
        this.getter.call(this.component)   
        if (this.deep) {
            traverse(value)
        }
        Dep.target = null;
    }
    
    update(){
        this.cb()
    }
}
同步异步更新

所谓的同步更新是指当观察的主体改变时立刻触发更新。而实际开发中这种需求并不多,同一事件循环中可能需要改变好几次state状态,但视图view只需要根据最后一次计算结果同步渲染就行(react中的setState就是典型)。如果一直做同步更新无疑是个很大的性能损耗。
这就要求watcher在接收到更新通知时不能全都立刻执行callback。我们对代码做出相应调整

constructors(component, getter, cb, options){
        this.cb = cb // 对应的回调函数,callback
        this.getter = getter;
        this.id = UUID() // 生成一个唯一id
        this.sync = options.sync; //默认一般为false
        this.vm = component; //这就是执行上下文
        this.value = this.getter() // 这边既收集了依赖,又保存了旧的值
    }
        
    update(){
        if(this.sync){ //如果是同步那就立刻执行回调
            this.run();
        }else{
            // 否则把这次更新缓存起来
            //但是就像上面说的,异步更新往往是同一事件循环中多次修改同一个值,
            // 那么一个wather就会被缓存多次。因为需要一个id来判断一下,
            queueWatcher(this)
        }
    }
    
    run: function(){
        //获取新的值
        var newValue = this.getter();
        this.cb.call(this.vm, newValue, this.value)
    }

这里的一个要注意的地方是,考虑到极限情况,如果正在更新队列中wather时,又塞入进来该怎么处理。因此,加入一个flushing来表示队列的更新状态。
如果加入的时候队列正在更新状态,这时候分两种情况:

这个watcher已经更新过, 就把这个watcher再放到当前执行的下一位,当前watcher处理完,立即处理这个最新的。

这个watcher还没有处理,就找到这个wather在队列中现有的位置,并再把新的放在后面。

let flushing = false;
let has = {}; // 简单用个对象保存一下wather是否已存在
function queueWatcher (watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true // 如果之前没有,那么就塞进去吧,如果有了就不用管了
    if (!flushing) {
      queue.push(watcher)
    } else {
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
   // ... 等同一事件循环结束后再依次处理队列里的watcher。具体代码放到后面nexttick部分再说
    }
  }
}

这么设计不无道理。我们之所以为了将wather放入队列中,就是为了较少不必要的操作。考虑如下代码

data: {
    a: 1
},
computed: {
    b: function(){
        this.a + 1
    }
}

methods: {
    act: function(){
        this.a = 2;
        // do someting
        this.a = 1
    }
}

在act操作中,我们先改变a,再把它变回来。我们理想状况下是a没变,b也不重新计算。这就要求,b的wather执行update的时候要拿到a最新的值来计算。这里就是1。如果队列中a的watehr已经更新过,那么就应该把后面的a的wather放到当前更新的wather后面,立即更新。这样可以保证后面的wather用到a是可以拿到最新的值。
同理,如果a的wather还没有更新,那么把新的a的wather放的之前的a的wather的下一位,也是为了保证后面的wather用到a是可以拿到最新的值。

computed

之所以把计算属性拿出爱多带带讲,是因为

计算属性存在按需加载的情况

与render和$watcher相比,计算属性a可能依赖另一个计算属性b。

按需加载

所谓的按需计算顾名思义就是用到了才会计算,即调用了某个计算属性的get方法。在前面的方法中,我们在class Watcher的constructor中直接调用了getter方法收集依赖,这显然是不符合按需加载的原则的。

依赖收集

实际开发中,我们发现一个计算属性往往由另一个计算属性得来。如,

computed: {
    a: function(){
        return this.name;
    },
    b: function(){
        return this.a + "123"; 
    }
}

对于a而言,它是b的依赖,因此有必要在a的wather执行update操作时也更新b,也就意味着,a的watcher里需要收集着b的依赖。而收集的时机是执行b的回调时,this.a调用了a的get方法的时候
在computed部分,已经对计算属性的get方法进行了改写

function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      //调用一个计算属性的get方法时,会在watcher中收集依赖。
      watcher.depend() 
      return watcher.evaluate()
    }
  }

我们再修改一下wather代码:

class Watcher {
    constructors(component, getter, cb, options){
         this.cb = cb 
        this.getter = getter;
        this.id = UUID() 
        this.sync = options.sync; 
        this.vm = component; 
        if(options){
            this.computed = options.computed //由于是对计算属性特殊处理,那肯定要给个标识符以便判断
        }
        this.dirty = this.computed // for computed watchers
        this.value = this.lazy ? undefined : this.get();
    }
    
    update(){
        if (this.lazy) {
          this.dirty = true
        } else if (this.sync) {
          this.run()
        } else {
          queueWatcher(this)
        }
    }
    
    run: function(){
         //拿到新值
        const value = this.get()
        if (value !== this.value || //基本类型的值直接比较
          // 对象没办法直接比较,因此都进行计算
          isObject(value)) {
          // set new value
          const oldValue = this.value
          this.value = value
          this.dirty = false
          cb.call(this.vm, value, oldValue)
        }
    }
    
    // 新增depend方法,收集计算属性的依赖
    depend () {
        if (this.dep && Dep.target) {
          this.dep.depend()
        }
      }
}
  
  //不要忘了还要返回当前computed的最新的值
  //由于可能不是立即更新的,因此根据dirty再判断一下,如果数据脏了,调用get再获取一下
  evaluate () {
    if (this.dirty) {
      this.value = this.get()
      this.dirty = false
    }
    return this.value
  }

在绑定依赖之前(computed的get被触发一次),computed用到的data数据改变是不会触发computed的重新计算的。

路径解析

对于render和computed想要收集依赖,我们只需要执行一遍回调函数就行,但是对于$watch方法,我们并不关心他的回调是什么,而更关心我们需要监听哪个值。
这里的需求多种多样,
比如单个值监听,监听对象的某个属性(.),比如多个值混合监听(&&, ||)等。这就需要对监听的路径进行解析。

 constructors(component, expOrFn, cb, options){
         this.cb = cb 
        this.id = UUID() 
        this.sync = options.sync; 
        this.vm = component; 
        if(options){
            this.computed = options.computed
        }
        if(typeof expOrFn === "function"){
            // render or computed
            this.getter = expOrFn 
        }else{
            this.getter = this.parsePath();
        }
        if(this.computed){
            this.value = undefined
            this.dep = new Dep() 
        }else{
            this.value = this.get(); //非计算属性是通过调用getter方法收集依赖。
        }
    }
    
    parsePath: function(){
        // 简单的路径解析,如果都是字符串则不需要解析
         if (/[^w.$]/.test(path)) {
            return
          }
        // 这边只是简单解析了子属性的情况
          const segments = path.split(".")
          return function (obj) {
            for (let i = 0; i < segments.length; i++) {
              if (!obj) return
              obj = obj[segments[i]]
            }
            return obj
          }
    }
总结

我们在watcher乞丐版的基础上,根据实际需求推导出了更健全的watcher版本。下面是完整代码

class Watcher {
    constructors(component, getter, cb, options){
         this.cb = cb 
        this.getter = getter;
        this.id = UUID() 
        this.sync = options.sync; 
        this.vm = component; 
        if(options){
            this.computed = options.computed //由于是对计算属性特殊处理,那肯定要给个标识符以便判断
        }
        if(typeof expOrFn === "function"){
            // render or computed
            this.getter = expOrFn 
        }else{
            this.getter = this.parsePath();
        }
        this.dirty = this.computed // for computed watchers
        if(this.computed){
            // 对于计算属性computed而言,我们需要关心preValue吗?   *********************
            this.value = undefined
            // 如果是计算属性,就要收集依赖
            //同时根据按需加载的原则,这边不会手机依赖,主动执行回调函数。
            this.dep = new Dep() 
        }else{
            this.value = this.get(); //非计算属性是通过调用getter方法收集依赖。
        }
    }
    
    update(){
        if (this.lazy) {
          this.dirty = true
        } else if (this.sync) {
          this.run()
        } else {
          queueWatcher(this)
        }
    }
    
    run: function(){
         //拿到新值
        const value = this.get()
        if (value !== this.value || //基本类型的值直接比较
          // 对象没办法直接比较,因此都进行计算
          isObject(value)) {
          // set new value
          const oldValue = this.value
          this.value = value
          this.dirty = false
          cb.call(this.vm, value, oldValue)
        }
    }

    
    // 新增depend方法,收集计算属性的依赖
    depend () {
        if (this.dep && Dep.target) {
          this.dep.depend()
        }
      }
}
  
  //不要忘了还要返回当前computed的最新的值
  //由于可能不是立即更新的,因此根据dirty再判断一下,如果数据脏了,调用get再获取一下
  evaluate () {
    if (this.dirty) {
      this.value = this.get()
      this.dirty = false
    }
    return this.value
  }

可以看到,基本vue的实现一样了。VUE中有些代码,比如teardown方法,清除自身的订阅信息我并没有加进来,因为没有想到合适的应用场景。
这种逆推的过程我觉得比直接读源码更有意思。直接读源码并不难,但很容易造成似是而非的情况。逻辑很容易理解,但是真正为什么这么写,一些细节原因很容易漏掉。但是不管什么框架都是为了解决实际问题的,从需求出发,才能更好的学习一个框架,并在自己的工作中加以借鉴。
借VUE的生命周期图进行展示

局部图:

从局部图里可以看出,vue收集依赖的入口只有两个,一个是在加载之前处理$wacth方法,一个是render生成虚拟dom。
而对于computed,只有在使用到时才会收集依赖。如果我们在watch和render中都没有使用,而是在methods中使用,那么加载的过程中是不会计算这个computed的,只有在调用methods中方法时才会计算。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/97953.html

相关文章

  • Vue原理】NextTick - 源码 服务Vue

    写文章不容易,点个赞呗兄弟专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于 Vue版本 【2.5.17】 如果你觉得排版难看,请点击 下面链接 或者 拉到 下面关注公众号也可以吧 【Vue原理】NextTick - 源码版 之 服务Vue 初次看的兄弟可以先看 【Vue原理】NextTick - 白话版 简单了解下...

    Acceml 评论0 收藏0
  • vue源码分析系列响应式数据(四)

    摘要:执行当时传入的回调,并将新值与旧值一并传入。文章链接源码分析系列源码分析系列之环境搭建源码分析系列之入口文件分析源码分析系列之响应式数据一源码分析系列之响应式数据二源码分析系列之响应式数据三 前言 上一节着重讲述了initComputed中的代码,以及数据是如何从computed中到视图层的,以及data修改后如何作用于computed。这一节主要记录initWatcher中的内容。 ...

    GHOST_349178 评论0 收藏0
  • Vue原理】依赖更新 - 源码

    摘要:写文章不容易,点个赞呗兄弟专注源码分享,文章分为白话版和源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于版本如果你觉得排版难看,请点击下面链接或者拉到下面关注公众号也可以吧原理依赖更新源码版如果对依赖收集完 写文章不容易,点个赞呗兄弟专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于...

    moven_j 评论0 收藏0
  • Vue原理】依赖收集 - 源码基本数据类型

    摘要:当东西发售时,就会打你的电话通知你,让你来领取完成更新。其中涉及的几个步骤,按上面的例子来转化一下你买东西,就是你要使用数据你把电话给老板,电话就是你的,用于通知老板记下电话在电话本,就是把保存在中。剩下的步骤属于依赖更新 写文章不容易,点个赞呗兄弟专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于 Vue版本 【...

    VincentFF 评论0 收藏0
  • Vue源码分析Observer

    摘要:中的观察者模式观察者模式一般包含发布者和订阅者两种角色顾名思义发布者负责发布消息,订阅者通过订阅消息响应动作了。中主要有两种类型的,一种是另外一种是是通过或者中的属性定义的。结束好了,基本结束,如有错漏,望指正。 碎碎念 四月份真是慵懒无比的一个月份,看着手头上没啥事干,只好翻翻代码啥的,看了一会Vue的源码,忽而有点感悟,于是便记录一下。 Vue中的观察者模式 观察者模式一般包含发布...

    CoderBear 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<