当前位置:首页 >> 网络编程

Vue 2.0的数据依赖实现原理代码简析

首先让我们从最简单的一个实例Vue入手:

  const app = new Vue({
    // options 传入一个选项obj.这个obj即对于这个vue实例的初始化
  })

通过查阅文档,我们可以知道这个options可以接受:

  1. 选项/数据
    1. data
    2. props
    3. propsData(方便测试使用)
    4. computed
    5. methods
    6. watch
  2. 选项 / DOM
  3. 选项 / 生命周期钩子
  4. 选项 / 资源
  5. 选项 / 杂项

具体未展开的内容请自行查阅相关文档,接下来让我们来看看传入的选项/数据是如何管理数据之间的相互依赖的。

  const app = new Vue({
    el: '#app',
    props: {
     a: {
      type: Object,
      default () {
       return {
        key1: 'a',
        key2: {
          a: 'b'
        }
       }
      }
     }
    },
    data: {
     msg1: 'Hello world!',
     arr: {
      arr1: 1
     }
    },
    watch: {
     a (newVal, oldVal) {
      console.log(newVal, oldVal)
     }
    },
    methods: {
     go () {
      console.log('This is simple demo')
     }
    }
  })

我们使用Vue这个构造函数去实例化了一个vue实例app。传入了props, data, watch, methods等属性。在实例化的过程中,Vue提供的构造函数就使用我们传入的options去完成数据的依赖管理,初始化的过程只有一次,但是在你自己的程序当中,数据的依赖管理的次数不止一次。

那Vue的构造函数到底是怎么实现的呢?Vue

// 构造函数
function Vue (options) {
 if (process.env.NODE_ENV !== 'production' &&
  !(this instanceof Vue)) {
  warn('Vue is a constructor and should be called with the `new` keyword')
 }
 this._init(options)
}

// 对Vue这个class进行mixin,即在原型上添加方法
// Vue.prototype.* = function () {}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

当我们调用new Vue的时候,事实上就调用的Vue原型上的_init方法.

// 原型上提供_init方法,新建一个vue实例并传入options参数
 Vue.prototype._init = function (options"htmlcode">
export function initState (vm: Component) {
 // 首先在vm上初始化一个_watchers数组,缓存这个vm上的所有watcher
 vm._watchers = []
 // 获取options,包括在new Vue传入的,同时还包括了Vue所继承的options
 const opts = vm.$options
 // 初始化props属性
 if (opts.props) initProps(vm, opts.props)
 // 初始化methods属性
 if (opts.methods) initMethods(vm, opts.methods)
 // 初始化data属性
 if (opts.data) {
  initData(vm)
 } else {
  observe(vm._data = {}, true /* asRootData */)
 }
 // 初始化computed属性
 if (opts.computed) initComputed(vm, opts.computed)
 // 初始化watch属性
 if (opts.watch) initWatch(vm, opts.watch)
}

initProps

我们在实例化app的时候,在构造函数里面传入的options中有props属性:

  props: {
   a: {
    type: Object,
    default () {
     return {
      key1: 'a',
      key2: {
        a: 'b'
      }
     }
    }
   }
  }
function initProps (vm: Component, propsOptions: Object) {
 // propsData主要是为了方便测试使用
 const propsData = vm.$options.propsData || {}
 // 新建vm._props对象,可以通过app实例去访问
 const props = vm._props = {}
 // cache prop keys so that future props updates can iterate using Array
 // instead of dynamic object key enumeration.
 // 缓存的prop key
 const keys = vm.$options._propKeys = []
 const isRoot = !vm.$parent
 // root instance props should be converted
 observerState.shouldConvert = isRoot
 for (const key in propsOptions) {
  // this._init传入的options中的props属性
  keys.push(key)
  // 注意这个validateProp方法,不仅完成了prop属性类型验证的,同时将prop的值都转化为了getter/setter,并返回一个observer
  const value = validateProp(key, propsOptions, propsData, vm)
  
  // 将这个key对应的值转化为getter/setter
   defineReactive(props, key, value)
  // static props are already proxied on the component's prototype
  // during Vue.extend(). We only need to proxy props defined at
  // instantiation here.
  // 如果在vm这个实例上没有key属性,那么就通过proxy转化为proxyGetter/proxySetter, 并挂载到vm实例上,可以通过app._props[key]这种形式去访问
  if (!(key in vm)) {
   proxy(vm, `_props`, key)
  }
 }
 observerState.shouldConvert = true
}

接下来看下validateProp(key, propsOptions, propsData, vm)方法内部到底发生了什么。

export function validateProp (
 key: string,
 propOptions: Object,  // $options.props属性
 propsData: Object,   // $options.propsData属性
 vm"htmlcode">
// 获取prop的默认值
function getPropDefaultValue (vm: "htmlcode">
  props: {
   a: {
    type: Object,
    default () {
     return {
      key1: 'a',
      key2: {
        a: 'b'
      }
     }
    }
   }
  }

在往上数的第二段代码里面的方法obervse(value),即对{key1: 'a', key2: {a: 'b'}}进行依赖的管理,同时将这个obj所有的属性值都转化为getter/setter形式。此外,Vue还会将props属性都代理到vm实例上,通过vm.key1,vm.key2就可以访问到这个属性。

此外,还需要了解下在Vue中管理依赖的一个非常重要的类: Dep

export default class Dep { 
 constructor () {
  this.id = uid++
  this.subs = []
 }
 addSub () {...} // 添加订阅者(依赖)
 removeSub () {...} // 删除订阅者(依赖)
 depend () {...} // 检查当前Dep.target是否存在以及判断这个watcher已经被添加到了相应的依赖当中,如果没有则添加订阅者(依赖),如果已经被添加了那么就不做处理
 notify () {...} // 通知订阅者(依赖)更新
}

在Vue的整个生命周期当中,你所定义的响应式的数据上都会绑定一个Dep实例去管理其依赖。它实际上就是观察者和订阅者联系的一个桥梁。

刚才谈到了对于依赖的管理,它的核心之一就是观察者Observer这个类:

export class Observer {
 value: any;
 dep: Dep;
 vmCount: number; // number of vms that has this object as root $data

 constructor (value: any) {
  this.value = value
  // dep记录了和这个value值的相关依赖
  this.dep = new Dep()
  this.vmCount = 0
  // value其实就是vm._data, 即在vm._data上添加__ob__属性
  def(value, '__ob__', this)
  // 如果是数组
  if (Array.isArray(value)) {
   // 首先判断是否能使用__proto__属性
   const augment = hasProto
    "htmlcode">
/**
 * Define a reactive property on an Object.
 */
export function defineReactive (
 obj: Object,
 key: string,
 val: any,
 customSetter"htmlcode">
Dep.target = null
const targetStack = []

export function pushTarget (_target: Watcher) {
 if (Dep.target) targetStack.push(Dep.target)
 Dep.target = _target
}

export function popTarget () {
 Dep.target = targetStack.pop()
}

那么Vue是如何来实现订阅者的呢?Vue里面定义了一个类: Watcher,在Vue的整个生命周期当中,会有4类地方会实例化Watcher:

  1. Vue实例化的过程中有watch选项
  2. Vue实例化的过程中有computed计算属性选项
  3. Vue原型上有挂载$watch方法: Vue.prototype.$watch,可以直接通过实例调用this.$watch方法
  4. Vue生成了render函数,更新视图时
constructor (
  vm: Component,
  expOrFn: string | Function,
  cb: Function,
  options"htmlcode">
get () {
 // pushTarget即设置当前的需要被执行的watcher
  pushTarget(this)
  let value
  const vm = this.vm
  if (this.user) {
   try {
    // $watch(function () {})
    // 调用this.getter的时候,触发了属性的getter函数
    // 在getter中进行了依赖的管理
    value = this.getter.call(vm, vm)
    console.log(value)
   } catch (e) {
    handleError(e, vm, `getter for watcher "${this.expression}"`)
   }
  } else {
   // 如果是新建模板函数,则会动态计算模板与data中绑定的变量,这个时候就调用了getter函数,那么就完成了dep的收集
   // 调用getter函数,则同时会调用函数内部的getter的函数,进行dep收集工作
   value = this.getter.call(vm, vm)
  }
  // "touch" every property so they are all tracked as
  // dependencies for deep watching
  // 让每个属性都被作为dependencies而tracked, 这样是为了deep watching
  if (this.deep) {
   traverse(value)
  }
  popTarget()
  this.cleanupDeps()
  return value  
}

一进入get方法,首先进行pushTarget(this)的操作,此时Vue当中Dep.target = 当前这个watcher,接下来进行value = this.getter.call(vm, vm)操作,在这个操作中就完成了依赖的收集工作。还是拿文章一开始的demo来说,在vue实例化的时候传入了watch选项:

  props: {
   a: {
    type: Object,
    default () {
     return {
      key1: 'a',
      key2: {
        a: 'b'
      }
     }
    }
   }
  },
  watch: {
    a (newVal, oldVal) {
      console.log(newVal, oldVal)
    }
  }, 

在Vue的initState()开始执行后,首先会初始化props的属性为getter/setter函数,然后在进行initWatch初始化的时候,这个时候初始化watcher实例,并调用get()方法,设置Dep.target = 当前这个watcher实例,进而到value = this.getter.call(vm, vm)的操作。在调用this.getter.call(vm, vm)的方法中,便会访问props选项中的a属性即其getter函数。在a属性的getter函数执行过程中,因为Dep.target已经存在,那么就进入了依赖收集的过程:

if (Dep.target) {
  // Dep.target.addDep(this)
  // 即添加watch函数
  // dep.depend()及调用了dep.addSub()只不过中间需要判断是否这个id的dep已经被包含在内了
  dep.depend()
  // childOb也添加依赖
  if (childOb) {
   childOb.dep.depend()
  }
  if (Array.isArray(value)) {
   dependArray(value)
  }
 }

dep是一开始初始化的过程中,这个属性上的dep属性。调用dep.depend()函数:

 depend () {
  if (Dep.target) {
   // Dep.target为一个watcher
   Dep.target.addDep(this)
  }
 }

Dep.target也就刚才的那个watcher实例,这里也就相当于调用了watcher实例的addDep方法: watcher.addDep(this),并将dep观察者传入。在addDep方法中完成依赖收集:

addDep (dep: Dep) {
  const id = dep.id
  if (!this.newDepIds.has(id)) {
   this.newDepIds.add(id)
   this.newDeps.push(dep)
   if (!this.depIds.has(id)) {
    dep.addSub(this)
   }
  }
 }

这个时候依赖完成了收集,当你去修改a属性的值时,会调用a属性的setter函数,里面会执行dep.notify(),它会遍历所有的订阅者,然后调用订阅者上的update函数。

initData过程和initProps类似,具体可参见源码。

initComputed

以上就是在initProps过程中Vue是如何进行依赖收集的,initData的过程和initProps类似,下来再来看看initComputed的过程.
在computed属性初始化的过程当中,会为每个属性实例化一个watcher:

const computedWatcherOptions = { lazy: true }

function initComputed (vm: Component, computed: Object) {
 // 新建_computedWatchers属性
 const watchers = vm._computedWatchers = Object.create(null)

 for (const key in computed) {
  const userDef = computed[key]
  // 如果computed为funtion,即取这个function为getter函数
  // 如果computed为非function.则可以单独为这个属性定义getter/setter属性
  let getter = typeof userDef === 'function' "htmlcode">
function createComputedGetter (key) {
 return function computedGetter () {
  const watcher = this._computedWatchers && this._computedWatchers[key]
  if (watcher) {
   // 是否需要重新计算
   if (watcher.dirty) {
    watcher.evaluate()
   }
   // 管理依赖
   if (Dep.target) {
    watcher.depend()
   }
   return watcher.value
  }
 }
}

在watcher存在的情况下,首先判断watcher.dirty属性,这个属性主要是用于判断这个computed属性是否需要重新求值,因为在上一轮的依赖收集的过程当中,观察者已经将这个watcher添加到依赖数组当中了,如果观察者发生了变化,就会dep.notify(),通知所有的watcher,而对于computed的watcher接收到变化的请求后,会将watcher.dirty = true即表明观察者发生了变化,当再次调用computed属性的getter函数的时候便会重新计算,否则还是使用之前缓存的值。

initWatch

initWatch的过程中其实就是实例化new Watcher完成观察者的依赖收集的过程,在内部的实现当中是调用了原型上的Vue.prototype.$watch方法。这个方法也适用于vm实例,即在vm实例内部调用this.$watch方法去实例化watcher,完成依赖的收集,同时监听expOrFn的变化。

总结:

以上就是在Vue实例初始化的过程中实现依赖管理的分析。大致的总结下就是:

  1. initState的过程中,将props,computed,data等属性通过Object.defineProperty来改造其getter/setter属性,并为每一个响应式属性实例化一个observer观察者。这个observer内部dep记录了这个响应式属性的所有依赖。
  2. 当响应式属性调用setter函数时,通过dep.notify()方法去遍历所有的依赖,调用watcher.update()去完成数据的动态响应。

这篇文章主要从初始化的数据层面上分析了Vue是如何管理依赖来到达数据的动态响应。下一篇文章来分析下Vue中模板中的指令和响应式数据是如何关联来实现由数据驱动视图,以及数据是如何响应视图变化的。

感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!