最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 我想用大白话讲清楚watch和computed

    正文概述 掘金(FinGet)   2021-02-03   468

    背景

    一直以来我对vue中的watchcomputed都一知半解的,知道一点(例如:watchcomputed的本质都是new Watcher,computed有缓存,只有调用的时候才会执行,也只有当依赖的数据变化了,才会再次触发...),然后就没有然后了。

    也看了很多大佬写的文章,一大段一大段的源码列出来,着实让我这个菜鸡看的头大,自然也就不想看了。最近,我又开始学习vue源码,才真正理解了它们的实现原理。

    data() {
      return {
        msg: 'hello guys',
        info: {age:'18'},
        name: 'FinGet'
      }
    }
    

    watcher

    watcher 是什么?侦听器?它就是个类class!

    class Watcher{
      constructor(vm,exprOrFn,callback,options,isRenderWatcher){
      }
    }
    
    • vm vue实例
    • exprOrFn 可能是字符串或者回调函数(有点懵就往后看,现在它不重要)
    • options 各种配置项(配置啥,往后看)
    • isRenderWatcher 是否是渲染Wathcer

    initState

    Vue 初始化中 会执行一个 initState方法,其中有大家最熟悉的initData,就是Object.defineProperty数据劫持。

    export function initState(vm) {
      const opts = vm.$options;
      // vue 的数据来源 属性 方法 数据 计算属性 watch
      if(opts.props) {
        initProps(vm);
      }
      if(opts.methods) {
        initMethod(vm);
      }
      if(opts.data) {
        initData(vm);
      }
      if(opts.computed){
        initComputed(vm);
      }
      if(opts.watch) {
        initWatch(vm, opts.watch);
      }
    }
    

    在数据劫持中,Watcher的好基友Dep出现了,Dep就是为了把Watcher存起来。

    我想用大白话讲清楚watch和computed

    function defineReactive(data, key, val) {
      let dep = new Dep(); 
      Object.defineProperty(data, key, {
        get(){
          if(Dep.target) {
            dep.depend(); // 收集依赖
          }
          return val;
        },
        set(newVal) {
          if(newVal === val) return;
          val = newVal;
          dep.notify(); // 通知执行
        }
      })
    }
    

    但是当$mount之后,就不一样了。至于$mount中执行的什么compilegeneraterenderpatchdiff都不是本文关注的,不重要,绕过!

    你只需要知道一件事:会执行下面的代码

    new Watcher(vm, updateComponent, () => {}, {}, true); // true 表示他是一个渲染watcher
    

    updateComponent就是更新哈,不计较具体执行,它现在就是个会更新页面的回调函数,它会被存在Watchergetter中。它对应的就是最开始那个exprOrFn参数。

    嘿嘿嘿,这个时候就不一样了:

    1. 渲染页面就是调用了你定义的数据(别杠,定义了没调用),就会走get
    2. new Watcher 就会调用一个方法把这个实例放到Dep.target上。
    pushTarget(watcher) {
      Dep.target = watcher;
    }
    

    这两件事正好凑到一起,那么 dep.depend()就干活了。

    下图就是定义了msginfoname三个数据,它们都有个渲染Watcher我想用大白话讲清楚watch和computed

    眼尖的小伙伴应该看到了msg中还有两个watcher,一个是用户定义的watch,另一个也是用户定义的watch。啊,当然不是啦,vue是做了去重的,不会有重复的watcher,正如你所料,另一个是computed watcher

    用户watch

    我们一般是这样使用watch的:

    watch: {
      msg(newVal, oldVal){
        console.log('my watch',newVal, oldVal)
      }
      // or
      msg: {
        handler(newVal, oldVal) {
          console.log('my watch',newVal, oldVal)
        },
        immediate: true
      }
    }
    

    这里会执行一个initWatch,一顿操作之后,就是提取出exprOrFn(这个时候它就是个字符串了)、handleroptions,这就和Watcher莫名的契合了,然后就顺理成章的调用了vm.$watch方法。

     Vue.prototype.$watch = function(exprOrFn, cb, options = {}) {
        options.user = true; // 标记为用户watcher
        // 核心就是创建个watcher
        const watcher = new Watcher(this, exprOrFn, cb, options);
        if(options.immediate){
          cb.call(vm,watcher.value)
        }
     }
    

    来吧,避免不了看看这段代码(本来粘贴了好长一段,但说了大白话,我就把和这段关系不大的给删减了):

    class Watcher{
      constructor(vm,exprOrFn,callback,options,isRenderWatcher){
        this.vm = vm;
        this.callback = callback;
        this.options = options;
        if(options) {
          this.user = !!options.user;
        }
        this.id = id ++;
        if (typeof exprOrFn == 'function') {
          this.getter = exprOrFn; // 将内部传过来的回调函数 放到getter属性上
        } else {
          this.getter = parsePath(exprOrFn);
          if (!this.getter) {
            this.getter = (() => {});
          }
        }
        this.value = this.get();
      }
      get(){
        pushTarget(this); // 把当前watcher 存入dep中
        let result = this.getter.call(this.vm, this.vm); // 渲染watcher的执行 这里会走到observe的get方法,然后存下这个watcher
        popTarget(); // 再置空 当执行到这一步的时候 所以的依赖收集都完成了,都是同一个watcher
        return result;
      }
    }
    
    // 这个就是拿来把msg的值取到,取到的就是oldVal
    function parsePath(path) {
      if (!path) {
        return
      }
      var segments = path.split('.');
      return function(obj) {
        for (var i = 0; i < segments.length; i++) {
          if (!obj) { return }
          obj = obj[segments[i]];
        }
        return obj
      }
    }
    

    大家可以看到,new Watcher会执行一下get方法,当是渲染Watcher就会渲染页面,执行一次updateComponent,当它是用户Watcher就是执行parsePath中的返回的方法,然后得到一个值this.value也就是oldVal

    嘿嘿嘿,既然取值了,那又走到了msgget里面,这个时候dep.depend()又干活了,用户Watcher就存进去了。

    msg改变的时候,这过程中还有一些骚操作,不重要哈,最后会执行一个run方法,调用回调函数,把newValueoldValue传进去:

      run(){
        let oldValue = this.value;
        // 再执行一次就拿到了现在的值,会去重哈,watcher不会重复添加
        let newValue = this.get();
        this.value = newValue;
        if(this.user && oldValue != newValue) { 
          // 是用户watcher, 就调用callback 也就是 handler
          this.callback(newValue, oldValue)
        }
      }
    

    computed

    computed: {
      c_msg() {
        return this.msg + 'computed'
      }
      // or
      c_msg: {
        get() {
          return this.msg + 'computed'
        },
        set() {}
      }
    },
    

    computed有什么特点:

    1. 调用的时候才会执行
    2. 有缓存
    3. 依赖改变时会重新计算

    调用的时候执行,我怎么知道它在调用?嘿嘿嘿,Object.defineProperty不就是干这事的嘛,巧了不是。

    依赖的数据改变时会重新计算,那就需要收集依赖了。还是那个逻辑,调用了this.msg -> get -> dep.depend()

    function initComputed(vm) {
      let computed = vm.$options.computed;
      const watchers = vm._computedWatchers = {};
      for(let key in computed) {
        const userDef = computed[key];
        // 获取get方法
        const getter = typeof userDef === 'function' ? userDef : userDef.get;
        // 创建计算属性watcher lazy就是第一次不调用
        watchers[key] = new Watcher(vm, userDef, () => {}, { lazy: true });
        defineComputed(vm, key, userDef)
      }
    }
    
    const sharedPropertyDefinition = {
      enumerable: true,
      configurable: true,
      get: () => {},
      set: () => {}
    }
    function defineComputed(target, key, userDef) {
      if (typeof userDef === 'function') {
          sharedPropertyDefinition.get = createComputedGetter(key)
      } else {
          sharedPropertyDefinition.get = createComputedGetter(userDef.get);
          sharedPropertyDefinition.set = userDef.set;
      }
      // 使用defineProperty定义 这样才能做到使用才计算
      Object.defineProperty(target, key, sharedPropertyDefinition)
    }
    

    下面这一段最重要,上面的看一眼就好,上面做的就是把get方法找出来,用Object.defineProperty绑定一下。

    class Watcher{
      constructor(vm,exprOrFn,callback,options,isRenderWatcher){
      	... 
        this.dirty = this.lazy;
        // lazy 第一次不执行
        this.value = this.lazy ? undefined : this.get();
        ...
      }
      
      update(){
        if (this.lazy) {
          // 计算属性 需要更新
          this.dirty = true;
        } else if (this.sync) {
          this.run();
        } else {
          queueWatcher(this); // 这就是个陪衬 现在不管它
        }
      }
      evaluate() {
        this.value = this.get();
        this.dirty = false;
      }
    }
    

    缓存就在这里,执行get方法会拿到一个返回值this.value就是缓存的值,在用户Watcher中,它就是oldValue,写到这里的时候,对尤大神的佩服,又加深一层。??plus!

    function createComputedGetter(key) {
      return function computedGetter() {
        // this 指向vue 实例
        const watcher = this._computedWatchers[key];
        if (watcher) {
          if (watcher.dirty) { // 如果dirty为true
            watcher.evaluate();// 计算出新值,并将dirty 更新为false
          }
          // 如果依赖的值不发生变化,则返回上次计算的结果
          return watcher.value
        }
      }
    }
    

    watcherupdate是什么时候调用的?也就是数据更新调用dep.notify()dirty就需要变成true,但是计算属性还是不能马上计算,还是需要在调用的时候才计算,所以在update的时候只是改了dirty的状态!然后下次调用的时候就会重新计算。

    class Dep {
      constructor() {
        this.id = id ++;
        this.subs = [];
      }
      addSub(watcher) {
        this.subs.push(watcher);
      }
      depend() {
        Dep.target.addDep(this);
      }
      notify() {
        this.subs.forEach(watcher => watcher.update())
      }
    }
    

    总结

    1. watchcomputed 本质都是Watcher,都被存放在Dep中,当数据改变时,就执行dep.notify把当前对应Dep实例中存的Watcherrun一下,这样执行了渲染Watcher 页面就刷新了;
    2. 每一个数据都有自己的Dep,如果他在模版中被调用,那它一定有一个渲染Watcher
    3. initData时,是没有 Watcher 可以收集的;
    4. 发现没有,渲染WatcherComputed 中,exprOrFn都是函数,用户Watcher 中都是字符串。

    我想用大白话讲清楚watch和computed

    文章中的代码是简略版的,还有很多细枝末节的东西没说,不重要也只是针对本文不重要,大家可以去阅读源码更深入的理解。


    起源地下载网 » 我想用大白话讲清楚watch和computed

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元