• Vue 源码解析:深入响应式原理(上)


    原文链接:http://www.imooc.com/article/14466

    Vue.js 最显著的功能就是响应式系统,它是一个典型的 MVVM 框架,模型(Model)只是普通的 JavaScript 对象,修改它则视图(View)会自动更新。这种设计让状态管理变得非常简单而直观,不过理解它的原理也很重要,可以避免一些常见问题。下面让我们深挖 Vue.js 响应式系统的细节,来看一看 Vue.js 是如何把模型和视图建立起关联关系的。

    如何追踪变化

    我们先来看一个简单的例子。代码示例如下:

    <div id="main">
      <h1>count: {{times}}</h1>
    </div>
    <script src="vue.js"></script>
    <script>
      var vm = new Vue({
        el: '#main',
        data: function () {
          return {
            times: 1
          };
        },
        created: function () {
          var me = this;
          setInterval(function () {
            me.times++;
          }, 1000);
        }
      });
    </script>

    运行后,我们可以从页面中看到,count 后面的 times 每隔 1s 递增 1,视图一直在更新。在代码中仅仅是通过 setInterval 方法每隔 1s 来修改 vm.times 的值,并没有任何 DOM 操作。那么 Vue.js 是如何实现这个过程的呢?我们可以通过一张图来看一下,如下图所示:
    image

    图中的模型(Model)就是 data 方法返回的{times:1},视图(View)是最终在浏览器中显示的DOM。模型通过Observer、Dep、Watcher、Directive等一系列对象的关联,最终和视图建立起关系。归纳起来,Vue.js在这里主要做了三件事:

    • 通过 Observer 对 data 做监听,并且提供了订阅某个数据项变化的能力。
    • 把 template 编译成一段 document fragment,然后解析其中的 Directive,得到每一个 Directive 所依赖的数据项和update方法。
    • 通过Watcher把上述两部分结合起来,即把Directive中的数据依赖通过Watcher订阅在对应数据的 Observer 的 Dep 上。当数据变化时,就会触发 Observer 的 Dep 上的 notify 方法通知对应的 Watcher 的 update,进而触发 Directive 的 update 方法来更新 DOM 视图,最后达到模型和视图关联起来。

    接下来我们就结合 Vue.js 的源码来详细介绍这三个过程。

    Observer

    首先来看一下 Vue.js 是如何给 data 对象添加 Observer 的。我们知道,Vue 实例创建的过程会有一个生命周期,其中有一个过程就是调用 vm.initData 方法处理 data 选项。initData 方法的源码定义如下:

    <!-源码目录:src/instance/internal/state.js-->
    Vue.prototype._initData = function () {
        var dataFn = this.$options.data
        var data = this._data = dataFn ? dataFn() : {}
        if (!isPlainObject(data)) {
          data = {}
          process.env.NODE_ENV !== 'production' && warn(
            'data functions should return an object.',
            this
          )
        }
        var props = this._props
        // proxy data on instance
        var keys = Object.keys(data)
        var i, key
        i = keys.length
        while (i--) {
          key = keys[i]
          // there are two scenarios where we can proxy a data key:
          // 1. it's not already defined as a prop
          // 2. it's provided via a instantiation option AND there are no
          //    template prop present
          if (!props || !hasOwn(props, key)) {
            this._proxy(key)
          } else if (process.env.NODE_ENV !== 'production') {
            warn(
              'Data field "' + key + '" is already defined ' +
              'as a prop. To provide default value for a prop, use the "default" ' +
              'prop option; if you want to pass prop values to an instantiation ' +
              'call, use the "propsData" option.',
              this
            )
          }
        }
        // observe data
        observe(data, this)
      }

    在 initData 中我们要特别注意 proxy 方法,它的功能就是遍历 data 的 key,把 data 上的属性代理到 vm 实例上。_proxy 方法的源码定义如下:

    <!-源码目录:src/instance/internal/state.js-->
    Vue.prototype._proxy = function (key) {
        if (!isReserved(key)) {
          // need to store ref to self here
          // because these getter/setters might
          // be called by child scopes via
          // prototype inheritance.
          var self = this
          Object.defineProperty(self, key, {
            configurable: true,
            enumerable: true,
            get: function proxyGetter () {
              return self._data[key]
            },
            set: function proxySetter (val) {
              self._data[key] = val
            }
          })
        }
      }

    proxy 方法主要通过 Object.defineProperty 的 getter 和 setter 方法实现了代理。在前面的例子中,我们调用 vm.times 就相当于访问了 vm.data.times。

    在 _initData 方法的最后,我们调用了 observe(data, this) 方法来对 data 做监听。observe 方法的源码定义如下:

    <!-源码目录:src/observer/index.js-->
    export function observe (value, vm) {
      if (!value || typeof value !== 'object') {
        return
      }
      var ob
      if (
        hasOwn(value, '__ob__') &&
        value.__ob__ instanceof Observer
      ) {
        ob = value.__ob__
      } else if (
        shouldConvert &&
        (isArray(value) || isPlainObject(value)) &&
        Object.isExtensible(value) &&
        !value._isVue
      ) {
        ob = new Observer(value)
      }
      if (ob && vm) {
        ob.addVm(vm)
      }
      return ob
    }

    observe 方法首先判断 value 是否已经添加了 ob 属性,它是一个 Observer 对象的实例。如果是就直接用,否则在 value 满足一些条件(数组或对象、可扩展、非 vue 组件等)的情况下创建一个 Observer 对象。接下来我们看一下 Observer 这个类,它的源码定义如下:

    <!-源码目录:src/observer/index.js-->
    export function Observer (value) {
      this.value = value
      this.dep = new Dep()
      def(value, '__ob__', this)
      if (isArray(value)) {
        var augment = hasProto
          ? protoAugment
          : copyAugment
        augment(value, arrayMethods, arrayKeys)
        this.observeArray(value)
      } else {
        this.walk(value)
      }
    }

    Observer 类的构造函数主要做了这么几件事:首先创建了一个 Dep 对象实例(关于 Dep 对象我们稍后作介绍);然后把自身 this 添加到 value 的 ob 属性上;最后对 value 的类型进行判断,如果是数组则观察数组,否则观察单个元素。其实 observeArray 方法就是对数组进行遍历,递归调用 observe 方法,最终都会调用 walk 方法观察单个元素。接下来我们看一下 walk 方法,它的源码定义如下:

    <!-源码目录:src/observer/index.js-->
    Observer.prototype.walk = function (obj) {
      var keys = Object.keys(obj)
      for (var i = 0, l = keys.length; i < l; i++) {
        this.convert(keys[i], obj[keys[i]])
      }
    }

    walk 方法是对 obj 的 key 进行遍历,依次调用 convert 方法,对 obj 的每一个属性进行转换,让它们拥有 getter、setter 方法。只有当 obj 是一个对象时,这个方法才能被调用。接下来我们看一下 convert 方法,它的源码定义如下:

    <!-源码目录:src/observer/index.js-->
    Observer.prototype.convert = function (key, val) {
      defineReactive(this.value, key, val)
    }

    convert 方法很简单,它调用了 defineReactive 方法。这里 this.value 就是要观察的 data 对象,key 是 data 对象的某个属性,val 则是这个属性的值。defineReactive 的功能是把要观察的 data 对象的每个属性都赋予 getter 和 setter 方法。这样一旦属性被访问或者更新,我们就可以追踪到这些变化。接下来我们看一下 defineReactive 方法,它的源码定义如下:

    <!-源码目录:src/observer/index.js-->
    export function defineReactive (obj, key, val) {
      var dep = new Dep()
      var property = Object.getOwnPropertyDescriptor(obj, key)
      if (property && property.configurable === false) {
        return
      }
      // cater for pre-defined getter/setters
      var getter = property && property.get
      var setter = property && property.set
      var childOb = observe(val)
      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 (isArray(value)) {
              for (var e, i = 0, l = value.length; i < l; i++) {
                e = value[i]
                e && e.__ob__ && e.__ob__.dep.depend()
              }
            }
          }
          return value
        },
        set: function reactiveSetter (newVal) {
          var value = getter ? getter.call(obj) : val
          if (newVal === value) {
            return
          }
          if (setter) {
            setter.call(obj, newVal)
          } else {
            val = newVal
          }
          childOb = observe(newVal)
          dep.notify()
        }
      })
    }

    defineReactive 方法最核心的部分就是通过调用 Object.defineProperty 给 data 的每个属性添加 getter 和setter 方法。当 data 的某个属性被访问时,则会调用 getter 方法,判断当 Dep.target 不为空时调用 dep.depend 和 childObj.dep.depend 方法做依赖收集。如果访问的属性是一个数组,则会遍历这个数组收集数组元素的依赖。当改变 data 的属性时,则会调用 setter 方法,这时调用 dep.notify 方法进行通知。这里我们提到了 dep,它是 Dep 对象的实例。接下来我们看一下 Dep 这个类,它的源码定义如下:

    <!-源码目录:src/observer/dep.js-->
    export default function Dep () {
      this.id = uid++
      this.subs = []
    }
    // the current target watcher being evaluated.
    // this is globally unique because there could be only one
    // watcher being evaluated at any time.
    Dep.target = null

    Dep 类是一个简单的观察者模式的实现。它的构造函数非常简单,初始化了 id 和 subs。其中 subs 用来存储所有订阅它的 Watcher,Watcher 的实现稍后我们会介绍。Dep.target 表示当前正在计算的 Watcher,它是全局唯一的,因为在同一时间只能有一个 Watcher 被计算。

    前面提到了在 getter 和 setter 方法调用时会分别调用 dep.depend 方法和 dep.notify 方法,接下来依次介绍这两个方法。depend 方法的源码定义如下:

    <!-源码目录:src/observer/dep.js-->
    Dep.prototype.depend = function () {
      Dep.target.addDep(this)
    }

    depend 方法很简单,它通过 Dep.target.addDep(this) 方法把当前 Dep 的实例添加到当前正在计算的Watcher 的依赖中。接下来我们看一下 notify 方法,它的源码定义如下:

    <!-源码目录:src/observer/dep.js-->
    Dep.prototype.notify = function () {
      // stablize the subscriber list first
      var subs = toArray(this.subs)
      for (var i = 0, l = subs.length; i < l; i++) {
        subs[i].update()
      }
    }

    notify 方法也很简单,它遍历了所有的订阅 Watcher,调用它们的 update 方法。

    至此,vm 实例中给 data 对象添加 Observer 的过程就结束了。接下来我们看一下 Vue.js 是如何进行指令解析的。


    作者: ustbhuangyi 
    链接:http://www.imooc.com/article/14466
    来源:慕课网
    本文原创发布于慕课网 ,转载请注明出处,谢谢合作!

  • 相关阅读:
    jProfiler远程连接Linux监控jvm的运行状态
    Linux性能监控分析命令(五)—free命令介绍
    Linux性能监控分析命令(三)—iostat命令介绍
    Python编程练习题学习汇总
    OSPF与ACL综合应用实验
    ACL基础知识点总结
    NAT基础知识点
    VLAN实验五:利用三层交换机实现VLAN间路由
    VLAN实验四:利用单臂路由实现VLAN间路由
    VLAN实验三:理解Hybrid接口
  • 原文地址:https://www.cnblogs.com/gavinyyb/p/6256185.html
Copyright © 2020-2023  润新知