zoukankan      html  css  js  c++  java
  • Vue的computed计算属性是如何实现的

    一个开始

    有如下代码,full是一个计算属性,开始,他的值是'hello world',1s后,msg变成了‘I like’, full的值同步变成了'I like world';其原理解析来看一下。

    <div id="app">
        <span :msg="msg"></span>
        <div> {{full}}</div>
    </div>
    <script src="./vue.js"></script>
    <script>
    new Vue({
        el: '#app',
        data: {
            msg: 'hello ',
        },
        computed: {
            full() {
                return this.msg + 'world';
            },
        },
        mounted() {
            setTimeout(() => {
                this.msg = 'I like ';
            }, 1000);
        }
    })
    
    </script>

    从入口开始

    new Vue时,首先vue执行了_init方法,在这里做了vue的初始化工作,其中执行了一个initState函数,该函数进行的数据处理。

    函数内容如下

    function initState (vm) {
      vm._watchers = [];
      var opts = vm.$options;
      if (opts.props) { initProps(vm, opts.props); }
      if (opts.methods) { initMethods(vm, opts.methods); }
      // data数据绑定,数据驱动核心
      if (opts.data) {
        initData(vm);
      } else {
        observe(vm._data = {}, true /* asRootData */);
      }
      // 计算属性绑定
      if (opts.computed) { initComputed(vm, opts.computed); }
      if (opts.watch && opts.watch !== nativeWatch) {
        initWatch(vm, opts.watch);
      }
    }

    其中有两个核心流程,data绑定和computed初始化。首先来看一下计算属性初始化干了什么事。

    计算属性初始化

    执行initComputed时,会执行以下操作,会为每一个computed属性创建watcher并且执行defineComputed,在开始的示例中,会给full属性new一个watcher。

    function initComputed (vm, computed) {
        // 创建watchers对象缓存watcher
        var watchers = vm._computedWatchers = Object.create(null);
        for (var key in computed) {
          // 计算属性的执行函数/get、set描述对象
          var userDef = computed[key];
          var getter = typeof userDef === 'function' ? userDef : userDef.get;
          watchers[key] = new Watcher(
            vm,
            getter || noop,
            noop,
            computedWatcherOptions
          );
          if (!(key in vm)) {
            defineComputed(vm, key, userDef);
          }
        }
      }

    defineComputed做了什么呢?

    function defineComputed (
        target,
        key,
        userDef
      ) {
        // 只论述传入的值为函数的情况
        if (typeof userDef === 'function') {
          sharedPropertyDefinition.get = function computedGetter () {
            var watcher = this._computedWatchers && this._computedWatchers[key];
            if (watcher) {
              watcher.depend();
              return watcher.evaluate()
            }
          }
          sharedPropertyDefinition.set = noop;
        }
        // 进行了属性的描述定义。
        Object.defineProperty(target, key, sharedPropertyDefinition);
    }

    defineComputed函数会给当前的vue实例挂载计算属性,defineProperty定义其描述,其中get执行的函数如上。

    那么现在回到开始。开始的示例中,定义了full计算属性,并且template中使用了full属性,当模板中渲染full时,做了什么(这是vnode解析并渲染部分)?我们假设会获取full的值并且填充到模板中

    ,因此我们会触发了full的get函数,就是以上get代码。

    首先获取当前vue实例中的计算属性对应的监听器Watcher,然后进行depend方法执行,然后执行evaluate()方法,接下来我们走进监听器Watcher。

    Vue中的Watcher

    简单介绍watcher的作用,watcher顾名思义,监听器,1.监听什么2.要干什么事,这是我们关心的。

    var Watcher = function Watcher (
      vm,
      expOrFn,
      cb,
      options,
      isRenderWatcher
    ) {
      this.vm = vm;
      if (isRenderWatcher) {
        vm._watcher = this;
      }
      vm._watchers.push(this);
      // options
      if (options) {
        this.deep = !!options.deep;
        this.computed = !!options.computed;
      } else {
        this.deep = this.user = this.computed = this.sync = false;
      }
      this.cb = cb;
      this.id = ++uid$1; // uid for batching
      this.active = true;
      this.dirty = this.computed; // for computed watchers
      this.deps = [];
      this.newDeps = [];
      this.depIds = new _Set();
      this.newDepIds = new _Set();
      // parse expression for getter
      if (typeof expOrFn === 'function') {
        this.getter = expOrFn;
      }
      // 如果是计算属性,执行if,如果不是执行else
      if (this.computed) {
        this.value = undefined;
        this.dep = new Dep();
      } else {
        this.value = this.get();
      }
    };

    以上代码是Watcher的入口,看我们关心的入参数:

    vm:当前vue对象

    expOrFn:监听器触发的函数(2.要干什么)

    options:其他参数

    计算属性在new Watcher时,会传入getter函数给expOrFn,从上面代码看,如果expOrFn是函数,就会给getter属性赋值expOrFn,这是没问题的。

    同时计算实行new Watcher时,传递{computed: true}给options, 从以上代码看出,如果是计算属性的watcher,会与其他watcher不同的逻辑。

    计算属性的Watcher会new Dep赋值给this.dep。

    那么Watcher到底是干嘛的,Watcher是监听器,Vue会提供观察者去订阅他,如果观察者发现需要更新某个操作,会通知Watcher,watcher会执行update进行更新。

    那么Dep是什么。

    Vue中的Dep

    Dep是个订阅器,观察者想要订阅监听器,需要订阅器Dep来实现。

    同时计算属性的Watcher也会有订阅器,那么他订阅什么呢?同样是其他的Watcher,比如render Watcher, 当计算属性发生变化时,他可以通知render Watcher进行view渲染。

    回到主链路

    现在我们知道了,计算属性初始化会new Watcher,并计算属性在渲染到视图层时会触发getter,getter中计算属性会触发自己的watcher的两个函数,depend和evaluate,

    depend函数会将当前的订阅对象添加到自己的订阅器中,此时的订阅对像则是render watcher,此步骤可以自己做详细了解。

    主要的逻辑在evaluate中,evaluate中如果是计算属性并且没有被污染则调用get方法,来看一下get方法

    Watcher.prototype.get = function get () {
      // 将自身设置为订阅对象
      pushTarget(this);
      var value;
      var vm = this.vm;
      try {
        value = this.getter.call(vm, vm);
      } catch (e) {
        if (this.user) {
          handleError(e, vm, ("getter for watcher "" + (this.expression) + """));
        } else {
          throw e
        }
      } finally {
        // "touch" every property so they are all tracked as
        // dependencies for deep watching
        if (this.deep) {
          traverse(value);
        }
        popTarget();
        this.cleanupDeps();
      }
      return value
    };

    get方法中,首先,计算属性的watcher会将自己设置为订阅对象,共观察者订阅。然后执行getter,那么this.getter我们前面提到了,是我们写的计算属性函数 () {return this.msg + 'world'};

    当此getter执行时,我们来想一下。this.msg触发了msg属性的get,那么我们前面提到vue启动了2个核心流程 ,那么这里computed的流程中进入到了data流程中。

    initData简介

    function initData (vm) {
        var data = vm.$options.data;
        data = vm._data = typeof data === 'function'
          ? getData(data, vm)
          : data || {};
        observe(data, true /* asRootData */);
    }

    简化后,initData就做了这个事情,将data包装为观察者,observe方法中最终会针对data中每一个属性做defineReactive操作,并且递归调用。

    defineReactive便是我们双向数据绑定的主要部分。vue将msg属性进行defineReactive重写get/set,并且将它作为一个观察者。

    Object.defineProperty(obj, key, {
          enumerable: true,
          configurable: true,
          get: function reactiveGetter () {
            var value = getter ? getter.call(obj) : val;
            if (Dep.target) {
              dep.depend();
              if (childOb) {
                childOb.dep.depend();
                if (Array.isArray(value)) {
                  dependArray(value);
                }
              }
            }
            return value
          },
          set: function reactiveSetter (newVal) {
            var value = getter ? getter.call(obj) : val;
            /* eslint-disable no-self-compare */
            if (newVal === value || (newVal !== newVal && value !== value)) {
              return
            }
            /* eslint-enable no-self-compare */
            if ("development" !== 'production' && customSetter) {
              customSetter();
            }
            if (setter) {
              setter.call(obj, newVal);
            } else {
              val = newVal;
            }
            childOb = !shallow && observe(newVal);
            dep.notify();
          }
        });

    当我们执行this.msg, 进行msg的get时,以上get方法执行,并且此时我们说过计算属性full将自己的watcher设置为订阅对象Dep.target,因此msg的get中会执行dep.depend,depend方法中会将当前的Dep.target添加到订阅器中,因此msg的订阅列表会有full的watcher。

    再次回到主链路

    前面说到,计算属性在初始化时会创建一个watcher,并且计算属性会被定义为vue实例的一个属性Object.defineProperty,并且其getter会触发自身watcher的两个方法。

    sharedPropertyDefinition.get = function computedGetter () {
            var watcher = this._computedWatchers && this._computedWatchers[key];
            if (watcher) {
              watcher.depend();
              return watcher.evaluate()
            }
    }

    getter的返回值是watcher.evaluate();

    Watcher.prototype.evaluate = function evaluate () {
      if (this.dirty) {
        this.value = this.get();
        this.dirty = false;
      }
      return this.value
    };

    evaluate方法返回了this.value,其实此时value就是计算好的值 hello world。计算的逻辑上面讲述了,连贯的叙述一遍:

    在full计算属性getter执行时,会使用this.msg的值,触发this.msg的get,在这里,发现目前拥有被观察对象Dep.target(也就是计算属性full的监听器),msg的订阅器会添加此观察对象进行观察,msg getter返回msg的值,因此full的getter执行完毕,返回了'hello world',这就是初始化的整个过程。


    计算属性的动态性实现

    计算属性的初始化已经讲述完成了。那么在msg改变时,full怎么动态改变的呢。

    大概你应该明白么,前面讲到了,msg作为双向数据绑定的属性,会包装为观察者,其有订阅器会订阅监听器。当full计算属性初始化时,msg的订阅器订阅了full的watcher,

    那么在msg值改变时,也就是setter时,我只需要通知full的watcher同步更新就好了。

    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
          var value = getter ? getter.call(obj) : val;
          if (Dep.target) {
            dep.depend();
            if (childOb) {
              childOb.dep.depend();
              if (Array.isArray(value)) {
                dependArray(value);
              }
            }
          }
          return value
        },
        set: function reactiveSetter (newVal) {
          var value = getter ? getter.call(obj) : val;
          /* eslint-disable no-self-compare */
          if (newVal === value || (newVal !== newVal && value !== value)) {
            return
          }
          /* eslint-enable no-self-compare */
          if ("development" !== 'production' && customSetter) {
            customSetter();
          }
          if (setter) {
            setter.call(obj, newVal);
          } else {
            val = newVal;
          }
          childOb = !shallow && observe(newVal);
          console.log(dep)
          dep.notify();
        }
      });

    dep.notify()执行后,会通知所有观察的watcher进行更新,因此full的watcher自然也会触发更新,

    Watcher.prototype.update = function update () {
      var this$1 = this;
      // 如果是计算属性,执行这里
      if (this.computed) {
        this.getAndInvoke(function () {
            this$1.dep.notify();
        });
      } else if (this.sync) {
        this.run();
      } else {
        queueWatcher(this);
      }
    };

    按照我们的代码走,执行到getAndInvoke。

    Watcher.prototype.getAndInvoke = function getAndInvoke (cb) {
        var value = this.get();
        if (
          value !== this.value ||
          isObject(value) ||
          this.deep
        ) {
          // set new value
          var oldValue = this.value;
          this.value = value;
          this.dirty = false;
          if (this.user) {
            try {
              cb.call(this.vm, value, oldValue);
            } catch (e) {
              handleError(e, this.vm, ("callback for watcher "" + (this.expression) + """));
            }
          } else {
            cb.call(this.vm, value, oldValue);
          }
        }
      };

    通过getAndInvoke方法,我们又一次执行了this.get,此时,msg值已经变为了'I like ',因此这里获取到了新的full值。并执行了cb,cb是什么呢,就是上一步的代码

    this.getAndInvoke(function () {
        this$1.dep.notify();
      });

    this$1指向计算属性full的watcher对象自己,this$1.dep是full watcher的订阅器,这段代码就是通知full watcher订阅的watcher进行update。前面说到,计算属性在初始getter时候,进行了

    watcher.depend并添加了订阅对象render watcher,所以在这里,计算属性通知更新的watcher也就是render watcher。 render watcher是什么,是整个vue实例的渲染watcher,承载着vnode

    渲染真实dom的角色。

    结尾

    到这里此次分析已经完成了,此次分析从computed初始化为入口,以双向数据绑定为辅助,完成的整个解析思路,以上代码片段均已删减。

    能力有限,源码比较庞大,有些错误的地方请指正。

    vue v2.5.17-beta.0版本。

  • 相关阅读:
    redux-devtools的使用
    Electron-builder打包详解
    HTML JAVASCRIPT CSS 大小写敏感问题
    electron/nodejs实现调用golang函数
    基于react开发package.json的配置
    Chrome插件(扩展)开发全攻略
    Chrome插件中 popup,background,contentscript消息传递机制
    Linux 开机引导和启动过程详解
    bash 的配置文件加载顺序
    常用Electron App打包工具
  • 原文地址:https://www.cnblogs.com/xujiazheng/p/10219601.html
Copyright © 2011-2022 走看看