深入了解Vue响应式系统

栏目: 编程语言 · 发布时间: 4年前

内容简介:前面几篇文章一直都以源码分析为主,其实枯燥无味,对于新手玩家来说很不友好。这篇文章主要讲讲不同吧,分析为主,源码为辅,如果能达到深入浅出的效果那就更好了。「响应式系统」一直以来都是我认为

前言

前面几篇文章一直都以源码分析为主,其实枯燥无味,对于新手玩家来说很不友好。这篇文章主要讲讲 Vue 的响应式系统,形式与前边的稍显

不同吧,分析为主,源码为辅,如果能达到深入浅出的效果那就更好了。

什么是响应式系统

「响应式系统」一直以来都是我认为 Vue 里最核心的几个概念之一。想深入理解 Vue ,首先要掌握「响应式系统」的原理。

从一个官方的例子开始

由于 Vue 不允许动态添加根级响应式属性,所以你必须在初始化实例前声明所有根级响应式属性,哪怕只是一个空值:

var vm = new Vue({
  data: {
    // 声明 message 为一个空值字符串
    message: ''
  },
  template: '<div>{{ message }}</div>'
})
// 之后设置 `message`
vm.message = 'Hello!'

如果你未在 data 选项中声明 message, Vue 将警告你渲染函数正在试图访问不存在的属性。

当然,仅仅从上面这个例子我们也只能知道, Vue 不允许动态添加根级响应式属性。这意味我们需要将使用到的变量先在 data 函数中声明。

抛砖 引玉

新建一个空白工程,加入以下代码

export default {
    name: 'JustForTest',
    data () {
        return {}
    },
    created () {
        this.b = 555
        console.log(this.observeB)
        this.b = 666
        console.log(this.observeB)
    },
    computed: {
        observeB () {
            return this.b
        }
    }
}

运行上述代码,结果如下:

在上面的代码中我们做了些什么?

  1. 没有在 data 函数中声明变量(意味着此时没有根级响应式属性)
  2. 定义了一个 computed 属性 —— observeB ,用来返回(监听)变量 b
  3. 使用了变量 b 同时赋值 555 ,打印 this.observeB
  4. 使用了变量 b 同时赋值 666 ,打印 this.observeB

打印结果为什么都是 555

有段简单的代码可以解释这个原因:

function createComputedGetter (key) {
  return function computedGetter () {
    var watcher = this._computedWatchers && this._computedWatchers[key];
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate();
      }
      if (Dep.target) {
        watcher.depend();
      }
      return watcher.value
    }
  }
}
...
Watcher.prototype.evaluate = function evaluate () {
  this.value = this.get();
  this.dirty = false;
};

createComputedGetter 函数返回一个闭包函数并挂载在 computed 属性的 getter 上,一旦触发 computed 属性的 getter

那么就会调用 computedGetter

显然,输出 555 是因为触发了 this.observeBgetter ,从而触发了 computedGetter ,最后执行 Watcher.evalute()

然而,决定 watcher.evalute() 函数执行与否与 watcherwatcher.dirty 的值是否为空有关

深入了解响应式系统

Object.defineProperty

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。

那么这个函数应该怎么使用呢?给个官方的源码当做例子:

function def (obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  });
}
def(value, '__ob__', this);

gettersetter

上面提到了 Object.defineProperty 函数,其实这个函数有个特别的参数 —— descriptor (属性描述符),简单看下 MDN
上的定义:

对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可能是可写的,也可能不是

其中需要特别提到的就是 gettersetter ,在 descriptor (属性描述符)中分别代表 get 方法和 set
方法

get

一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,

set

一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,

小结

getter
setter
getter
setter

依赖收集

Vue 基于 Object.defineProperty 函数,可以对变量进行依赖收集,从而在变量的值改变时触发视图的更新。简单点来讲就是:

Vue 需要知道用到了哪些变量,不用的变量就不管,在它(变量)变化时, Vue 就通知对应绑定的视图进行更新。

举个例子:

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 (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter();
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) { return }
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = !shallow && observe(newVal);
      dep.notify();
    }
  });

这段代码做了哪些事情呢?主要有以下几点:

  • 对于 obj[key] ,定义它的 getset 函数
  • obj[key] 被访问时,触发 get 函数,调用 dep.depend 函数收集依赖
  • obj[key] 被赋值时,调用 set 函数,调用 dep.notify 函数触发视图更新

如果你再深入探究下去,那么还会发现 dep.notify 函数里还调用了 update 函数,而它恰好就是 Watcher 类所属

的方法,上面所提到的 computed 属性的计算方法也恰好也属于 Watcher

Observer

前面所提到的 Object.defineProperty 函数到底是在哪里被调用的呢?答案就是 initData 函数和 Observer 类。

可以归纳出一个清晰的调用逻辑:

  • 初始化 data 函数,此时调用 initData 函数
  • 在调用 initData 函数时,执行 observe 函数,这个函数执行成功后会返回一个 ob 对象
  • observe 函数返回的 ob 对象依赖于 Observer 函数
  • Observer 分别对对象和数组做了处理,对于某一个属性,最后都要执行 walk 函数
  • walk 函数遍历传入的对象的 key 值,对于每个 key 值对应的属性,依次调用 defineReactive$$1 函数
  • defineReactive$$1 函数中执行 Object.defineProperty 函数
  • ...

感兴趣的可以看下主要的代码,其实逻辑跟上面描述的一样,只不过步骤比较繁琐,耐心阅读源码的话还是能看懂。

initData

function initData (vm) {
  var data = vm.$options.data;
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {};
  if (!isPlainObject(data)) {
    data = {};
    ...
  }
  // proxy data on instance
  var keys = Object.keys(data);
  var props = vm.$options.props;
  var methods = vm.$options.methods;
  var i = keys.length;
  while (i--) {
    var key = keys[i];
    ...
    if (props && hasOwn(props, key)) {
        ...
    } else if (!isReserved(key)) {
      proxy(vm, "_data", key);
    }
  }
  // observe data
  observe(data, true /* asRootData */);
}

observe

function observe (value, asRootData) {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  var ob;
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__;
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value);
  }
  if (asRootData && ob) {
    ob.vmCount++;
  }
  return ob
}

Observer

var Observer = function Observer (value) {
  this.value = value;
  this.dep = new Dep();
  this.vmCount = 0;
  def(value, '__ob__', this);
  if (Array.isArray(value)) {
    if (hasProto) {
      protoAugment(value, arrayMethods);
    } else {
      copyAugment(value, arrayMethods, arrayKeys);
    }
    this.observeArray(value);
  } else {
    this.walk(value);
  }
};

更加方便的定义响应式属性

文档中提到, Vue 建议在根级声明变量。通过上面的分析我们也知道,在 data 函数中

声明变量则使得变量变成「响应式」的,那么是不是所有的情况下,变量都只能在 data 函数中

事先声明呢?

$set

Vue 其实提供了一个 $set 的全局函数,通过 $set 就可以动态添加响应式属性了。

export default {
    data () {
        return {}
    },
    created () {
        this.$set(this, 'b', 666)
    },
}

然而,执行上面这段代码后控制台却报错了

<font color=Red> [Vue warn]: Avoid adding reactive properties to a Vue instance or its root $data at runtime - declare it upfront in the data option. </font>

其实,对于已经创建的实例, Vue 不允许动态添加根级别的响应式属性。

$set 函数的执行逻辑:

  • 判断实例是否是数组,如果是则将属性插入
  • 判断属性是否已定义,是则赋值后返回
  • 判断实例是否是 Vue 的实例或者是已经存在 ob 属性(其实也是判断了添加的属性是否属于根级别的属性),是则结束函数并返回
  • 执行 defineReactive$$1 ,使得属性成为响应式属性
  • 执行 ob.dep.notify() ,通知视图更新

相关代码:

function set (target, key, val) {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(("Cannot set reactive property on undefined, null, or primitive value: " + ((target))));
  }
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key);
    target.splice(key, 1, val);
    return val
  }
  if (key in target && !(key in Object.prototype)) {
    target[key] = val;
    return val
  }
  var ob = (target).__ob__;
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    );
    return val
  }
  if (!ob) {
    target[key] = val;
    return val
  }
  c(ob.value, key, val);
  ob.dep.notify();
  return val
}

数组操作

为了变量的响应式, Vue 重写了数组的操作。其中,重写的方法就有这些:

push
pop
shift
unshift
splice
sort
reverse

那么这些方法是怎么重写的呢?

首先,定义一个 arrayMethods 继承 Array

var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);

然后,利用 object.defineProperty ,将 mutator 函数绑定在数组操作上:

def(arrayMethods, method, function mutator () { ... })

最后在调用数组方法的时候,会直接执行 mutator 函数。源码中,对这三种方法做了特别

处理:

push
unshift
splice
因为这三种方法都会增加原数组的长度。当然如果调用了这三种方法,会再调用一次 observeArray

方法(这里的逻辑就跟前面提到的一样了)

最后的最后,调用 notify 函数

核心代码:

methodsToPatch.forEach(function (method) {
 // cache original method
 var original = arrayProto[method];
 def(arrayMethods, method, function mutator () {
   var args = [], len = arguments.length;
   while ( len-- ) args[ len ] = arguments[ len ];

   var result = original.apply(this, args);
   var ob = this.__ob__;
   var inserted;
   switch (method) {
     case 'push':
     case 'unshift':
       inserted = args;
       break
     case 'splice':
       inserted = args.slice(2);
   }
   if (inserted) { ob.observeArray(inserted); }
   // notify change
   ob.dep.notify();
   return result
 });
});

总结

「响应式原理」借助了这三个类来实现,分别是:

Watcher
Observer
Dep

初始化阶段,利用 getter 的特点,监听到变量被访问 ObserverDep 实现对变量的「依赖收集」,

赋值阶段利用 setter 的特点,监听到变量赋值,利用 Dep 通知 Watcher ,从而进行视图更新。

深入了解Vue响应式系统

参考资料

深入响应式原理


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

互联网寡头战争

互联网寡头战争

屈运栩 / 浙江大学出版社 / 2017-5-1 / CNY 49.00

本书意在复盘2015年下半年资本寒冬袭来之后,互联网行业发生的小巨头并购等连锁反应,揭示其背后推手——以BAT(百度、阿里巴巴、腾讯)为首的互联网巨头在零售、出行、本地生活、金融等行业的布局竞争,记录和呈现行业新贵的选择与博弈,深度剖析中国互联网生态的演进过程。一起来看看 《互联网寡头战争》 这本书的介绍吧!

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具