Vue源码解读——Vue响应式原理

序言

Vue是当前最流行的框架之一,现在很多项目都或多或少都会用到Vue。所以了解Vue的响应式原理对我们意义非凡,有利于…

我们直接开始吧

源码解读

从入口开始

Vue对数据进行响应式的处理的入口在src/core/instance/state.js文件下的initState函数

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  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)
  }
}

函数参数vm指的是Vue实例,我们可以把它看作是我们平常写vue代码是经常使用的this
函数首先给vm实例定义_watchers属性,这个什么作用我们暂时不用管,接着往下。
然后获取vm的 o p t i o n s 配 置 。 这 里 的 options配置。这里的 optionsoptions配置包括我们Vue预先定义的配置、mixins传入的配置,和我们自己实例话Vue时传入的配置。
比如我们经常这样实例化Vue

new Vue({
    data() {
        return {
            a: 1
        }
    }
})

通过$options就能拿到我们传入的配置。
后面的代码我们很容易就能理解了,Vue拿到我们传入的props、methods、data、computed、watch属性分别进行初始化。我们在这里主要关注data的初始化,所以接下来我们看看initData做了什么

initData

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}

initData中首先拿到data的数据,并将数据赋值为vm._data, 如果时data是个函数的话,就调用getData获取数据

export function getData (data: Function, vm: Component): any {
  // #7573 disable dep collection when invoking data getters
  pushTarget()
  try {
    return data.call(vm, vm)
  } catch (e) {
    handleError(e, vm, `data()`)
    return {}
  } finally {
    popTarget()
  }
}

然后判断data是否是个对象,接着判断data中属性的名字是否和props和methods中属性命名冲突,是否时Vue的保留字(_isReserve)

如果data中的数据的属性不和props和method命名冲突,也不是以$_开头(Vue自己定义的属性以$_开头),那么久调用proxy函数对data中数据进行代理

proxy函数

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

proxy函数非常简单,主要通过 Object.defineProperty 函数在实例对象 vm 上定义与 data 数据字段同名的访问器属性,并且这些属性代理的值是 vm._data 上对应属性的值,所以我们平常使用的this.a其实是this._data.a中的值

接下来调用observe函数对data中的数据进行响应式处理,所以说,从这里开始才是Vue响应式系统的开始,接下来我们开看看observe函数做了什么

observe函数

observe定义在src/core/observer/index.js文件中

function observe (value: any, asRootData: ?boolean): Observer | void {
 if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  //...
    ob = new Observer(value)
//..
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

为了方便理解,代码删除了一些条件判断。从上面的代码很容易就可以看出,observe函数首先判断传入的value是不是对象,如果是对象才进行响应式处理,毫无疑问,我们传入的data是个对象,接着定义了一个Oberser对象并返回,接下来我们看看Observer
是怎样的

Observer

Observer定义在通过一个目录下,是一个类(构造函数)


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

  constructor (value: any) {
    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)
    }
  }
  // ...
}

在connstructor方法中,首先将传入的value(也就是data)赋值为this.value,实例对象的 dep 属性,保存了一个新创建的 Dep实例对象。然后初始化vmCount,之后给我们的data定义一个__ob__属性,指向创建的实例。 也就是说,如果我们传入的data是

data = {
    a: 1
}

这样的话,经过def(value, '__ob__', this)就变成了

data = {
    a: 1
    __ob__: {
        vmCount: 0,
        dep: Dep实例,
        ...
    }
}

接下来就进入了分支判断,判断value是否是数据,因为Vue对普通对象和数组的响应式策略不同,所以需要进行判断,在这里我们主要关注Vue对普通对象的响应式处理

walk函数
walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
}

walk函数非常简单,遍历obj的每一个属性,然后调用defineReactive函数

defineReactive
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 为每个属性定义一个dep实例,作为依赖收集器
  const dep = new Dep()

  // 判断属性是否可以被配置
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // 获取getter和setter
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  // 如果val是对象,先递归对其属性进行响应式处理
  let childOb = !shallow && observe(val)
  // 对自己响应式处理
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    // 拦截获取值的操作,并收集依赖
    get: function reactiveGetter () {
      // 获取值
      const 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) {
      // 获取原来的值
      const value = getter ? getter.call(obj) : val
      // 判断是否需要更新
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      // 如果属性不可修改,直接返回
      if (getter && !setter) return
      // 修改数据
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // 对新的值进行响应式处处理
      childOb = !shallow && observe(newVal)
      // 通知所有的依赖,数据进行了更新
      dep.notify()
    }
  })
}

defineReactive函数有点复杂,不过主要值有三点

  • 为每个属性定义一个依赖收集器dep
  • 重新定义访问器属性,并在对数据进行访问时可以收集依赖
  • 重新定义修改器属性,在数据变更时可以通知依赖数据更新,这就是为什么我们可以使用this.a = xxx对对数据进行更新,从而使得视图得到更新

依赖收集

通过前面的讨论,我们已经知道了Vue会在获取data的值的会调用getter,进行执行dep.depend()时候进行依赖收集,那么依赖收集又是什么实现的呢?而且dep具体又是什么,接下来我们就讨论一下这部分内容

Watcher

我们下来了解一下数据响应系统中另一个很重要的部分——Watcher。其实Watcher就是我们前面所说的依赖,dep收集依赖就是收集watcher,当data更新是,会通知它所收集的watcher数据得到了更新,进而watcher就会执行相应的逻辑,更新视图。

每一个Vue实例都对应一个Watcher,所以每次data数据得到更新时,Vue实例对应的Watcher都会得到通知,进而触发视图更新
那么Vue实例对应的Watcher时什么时候创建的呢?

wacher的创建在Vue挂在模板时进行创建。我们可以在src/core/instance/lifecycly.js中的mountComponent函数中看到Watcher的创建

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  //...
  let updateComponent
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      // ...
      const vnode = vm._render()
      // ...
      vm._update(vnode, hydrating)
      // ...
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  ...
}

在面代码中,定义了updateComponent函数,从名字可以看出,这个是用来更新视图的,开始创建和数据更新时都会调用这个函数来更新视图。updateComponent中调用了vm._render函数,这个函数的作用就是将我们的返回一个VNode(虚拟DOM),也就是在这个函数中我们得到data的值,触发getter,进行依赖收集。我们可以通过一个例子来看一下,如果我们自己定义render函数的话:

new Vue({
    data() {
        return {
            a: 1
        }
    },
    render: function(createElemnet) {
        let a = this.a
        return createElement('div', a);
    }
})

vm._render会调用到这里的render函数,在render函数中,我们获取a的值,触发了a的getter,随之进行依赖收集

接下来我们看看Watcher的定义,在core/observer/watcher.js下。

export default class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    // ...
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } 
    this.value = this.lazy
      ? undefined
      : this.get()
    // ..
  }
  // ...
}

在构造函数中定义了一系列属性。因为options没有传入deep、user、lasy、sync,所以this.deep等的值都为falsethis.depsthis.depIds时用来保存收集器的,也就是该watcher被哪些收集器所收集,而newDepIdsnewDeps是为了解决重复依赖的问题的。将我们传入的expOrFn(这里是updateComponent函数)赋值给gettter(这里很重要)。最后调用get函数。

下面我们就看看get函数做了什么

get () {
    pushTarget(this)
    let value
    const 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 {
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

该函数首先调用了pushTarget函数,最后调用了popTarget,这两个函数就定义在core/observer/dep.js下

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

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

这里就很关键了,就是在这里Vue将Dep.target设为当前的Watcher,因为Vue中会存在多个Watcher(computed属性会对应一个Watcher,我们自定义的watch也会对应Watcher),所以在pushTarget函数中会先将和原来的Dep.target放入targetStack,在popTarget函数再将Dep.Target设为原来的Dep.Target

之后get函数就执行

value = this.getter.call(vm, vm)

就是刚刚定义updateComponent函数,updateComponent调用vm._render函数,在vm._render函数中,我们获取data中的数据,触发其getter

get: function reactiveGetter () {
  // 获取值
  const value = getter ? getter.call(obj) : val
  // 收集依赖
  if (Dep.target) {
    dep.depend()
    if (childOb) {
      childOb.dep.depend()
      if (Array.isArray(value)) {
        dependArray(value)
      }
    }
  }
  return value
},

这时Dep.target不为空,所以就进入dep.depend,然后就可以收集依赖了

 depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
}

depend函数很简单,调用了Watcher的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)
      }
    }
}

addDep函数首先获取dep的id,然后判断依赖是否被收集了,如果没有这里的判断,下面的情况会出现重复收集依赖。

render: function(createElement) {
    let a1 = this.a
    let a2 = this.a
}

然后就将当前Watcher,加入到dep的依赖收集器中

addSub (sub: Watcher) {
    this.subs.push(sub)
}

我们回到Watcher的get函数中,在执行了popState函数后,然后执行this.cleanupDeps()函数, 我们来看看这个函数的作用

cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
}

函数一开始会将dep中不依赖当前Watcher从sub中剔除, 然后就是简单的交换。将deps和depIds设置为最新的newDeps和newDepIds

依赖收集就到这里了,依赖收集了解之后,依赖更新就很容易理解了

依赖更新

我们使用this.a = Xxx后,会触发setter,然后触发依赖进行更新,触发依赖调用了Depnotify,我们看看这个函数做了什么,函数定义在core/observer/dep.js下

notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }

notify的功能很简单,就是遍历所有的watcher,然后调用它们的update函数进行更新进行更新

至此,Vue响应式的原理就看完了,Vue源码博大精深,还有更广阔的天地等着大家探索,少年们加油吧

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值