深入源码理解Vue的响应式原理(八)

前言

本章,我们将深入 Vue 源码,初步探索其响应式实现原理。如果本篇文章对您有所帮助,请不要吝啬您的点赞和关注。


初始化过程

我们使用 Vue 时都会写一句new Vue({ ... })进行初始化,那么现在我们的源码探索之旅也以此为起点去查找 Vue 内部对data到底做了些什么?

我们打开项目的依赖目录node_modules,找到vue包,定位其下面的vue/src/core/index文件:

image.png

import Vue from './instance/index'
import {
    
     initGlobalAPI } from './global-api/index'
import {
    
     isServerRendering } from 'core/util/env'
import {
    
     FunctionalRenderContext } from 'core/vdom/create-functional-component'

initGlobalAPI(Vue)

Object.defineProperty(Vue.prototype, '$isServer', {
    
    
  get: isServerRendering
})

Object.defineProperty(Vue.prototype, '$ssrContext', {
    
    
  get () {
    
    
    /* istanbul ignore next */
    return this.$vnode && this.$vnode.ssrContext
  }
})

// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
    
    
  value: FunctionalRenderContext
})

Vue.version = '__VERSION__'

export default Vue

vue/src/core/index的源码比较简单。根据其变量命名判断,它初始化了 Vue 的一些全局API、挂载了一些和服务器渲染相关的属性以及暴露给我们Vue进行使用。其中Vue来自于同目录的./instance/index,我们顺着去看./instance/index的源码:

import {
    
     initMixin } from './init'
import {
    
     stateMixin } from './state'
import {
    
     renderMixin } from './render'
import {
    
     eventsMixin } from './events'
import {
    
     lifecycleMixin } from './lifecycle'
import {
    
     warn } from '../util/index'

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)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

根据这里的源码,我们知道了在new Vue()时,内部会执行this._init(),而这个_init方法实际上是在initMixin(Vue)内定义的,我们现在继续前往./init文件去找initMixin方法(因为源码过长,我们后面只展示关键代码):

export function initMixin (Vue: Class<Component>) {
    
    
  Vue.prototype._init = function (options?: Object) {
    
    
    const vm: Component = this
    ......
    
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')
    
    ......
  }
}

看到这里,其实就可以解释为啥beforeCreate这个钩子是访问不到我们的data的,因为只有当执行到initState(vm)时才会对内部使用到的一些状态,如propsdatacomputedwatchmethods进行初始化。那么我们现在目标显然就是去看看initState(vm)写了啥,initState来自于同目录的./state文件:

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)
  }
}

就如上面所说,这里面进行各种状态的初始化,现在我们的目光停留在了initData(vm),看看你对我们的data做了什么:

function initData (vm: Component) {
    
    
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {
    
    }
    
  ......
  
  // 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 */)
}

因为在initState方法中,propsmethods是在data之前初始化的,所以这里在对循环判断data里的属性有没有重复命名的情况。检测通过后,这里执行了proxy(vm, '_data', key)将我们data里的属性直接挂载到当前实例上,以下为proxy方法源码:

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)
}

这里使用了Object.defineProperty把这些data的属性全部转为 getter/setter,这也是为啥我们在data中定义的属性能直接以this.xxx的形式使用。

回到initDate方法,里面实现响应式最重要的一行代码,便是执行observe(data, true /* asRootData */),现在我们终于来到响应式原理的门口。

数据劫持

紧接着,我们定位到observe的代码:

export function observe (value: any, asRootData: ?boolean): Observer | void {
    
    
  
  ......
  
    ob = new Observer(value)

  ......

  return ob
}

看上去observe本质上是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()
    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)
    }
  }

  /**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk (obj: Object) {
    
    
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
    
    
      defineReactive(obj, keys[i])
    }
  }

  /**
   * Observe a list of Array items.
   */
  observeArray (items: Array<any>) {
    
    
    for (let i = 0, l = items.length; i < l; i++) {
    
    
      observe(items[i])
    }
  }
}

可以看到,在Observer构造函数内部,进行了数组的判断,如果是数组的话,又要进行hasProto判断,分别执行protoAugmentcopyAugment,其实就是把带有副作用的7个数组方法给添加上去,也就是实现之前我们讲到的 Vue 中的数组更新检测。在这里protoAugmentcopyAugment的调用都传入了arrayMethods这个参数,那就先来分析这个参数是什么,ctrl+左键点击arrayMethods,来到src\core\observer\array.js,其实这个文件就是对数组方法进行响应式的源码:

import { def } from '../util/index'

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})

可以看到这里通过def(实际通过Object.defineProperty)给这7个数组方法设置副作用,针对push、unshift、splice会增加数组元素的方法,额外的对新增的元素即inserted不为空时,调用observeArray循环的进行observe,之后ob.dep.notify()通知更新,也就代表着这7个方法执行后,都会通知更新,也就成了响应式。

分析完数组的情况,现在回到Observer类,如果是对象则执行walk,遍历对象所有key值,通过defineReactive进行数据劫持:

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
    
    
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    
    
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    
    
    val = obj[key]
  }

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    
    
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
    
    
      
      ......
      
    },
    set: function reactiveSetter (newVal) {
    
    
        
      ......
      
    }
  })
}

首先,实例化一个Dep对象,这个dep其实是一个闭包引用,在下面的Object.defineProperty中我们再讲。之前调用defineReactive的时候shallow参数是没有传的,则let childOb = !shallow && observe(val)会对该对象进行深层的监测。 defineReactive内部使用了Object.defineProperty对所有的key进行设置了getter和setter,就是在此时完成了数据劫持,并添加相应的副作用。现在我们来看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
    },
    set: function reactiveSetter (newVal) {
    
    
      
      ......
      
      childOb = !shallow && observe(newVal)
      dep.notify()
    }

在getter中,闭包引用的dep实际就在做依赖收集的事情,如果存在子对象也进行子对象的依赖收集,是数组就循环收集。而在setter中,对新值同样进行observe进行监测,然后通过dep.notify()通知更新完成响应式。

依赖收集

上面说到,在getter中使用dep.depend()进行依赖收集,在setter中使用dep.notify()进行更新通知。那现在我们就需要前往Dep类的实现,看它如何进行依赖收集和派发更新的。Dep引用自同目录的./dep文件,点进行查看其源码:

export default class Dep {
    
    
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    
    
    this.id = uid++
    this.subs = []
  }

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

  removeSub (sub: Watcher) {
    
    
    remove(this.subs, sub)
  }

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

  notify () {
    
    
  
    .......
    
  }
}

......

Dep.target = null
const targetStack = []

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

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

这里定义了一个subs,看上去是通过addSub、removeSub进行Watcher实例的添加删除。 dep.depend方法判断Dep.target然后执行Dep.target.addDep。这里的Dep.target也是个Watcher实例,现在需要追踪其addDep方法,看一看Watcher又是个啥东西:

......

export default class Watcher {
    
    
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    
    
    this.vm = vm
    if (isRenderWatcher) {
    
    
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    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()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
    
    
      this.getter = expOrFn
    } else {
    
    
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
    
    
        this.getter = noop
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${
      
      expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  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 {
    
    
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
    
    
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }


  addDep (dep: Dep) {
    
    
    ......
        dep.addSub(this)
    ......
  }

  ......
  
}

好家伙,面对这密密麻麻的一片代码,我们还是只去关注关键语句。其中addDep方法实际上还是通过调用DepaddSub将当前Watcher实例记录到了Depsubs中。值得注意的是,在get方法中调用了来自./dep文件中暴露的pushTarget方法,往上翻一屏,就能看到pushTarget中的一句Dep.target = target。现在,一切都串起来了。还记得在defineReactive内部使用了Object.defineProperty对所有的key进行设置了getter的时候,先对Dep.target进行了判断,即Dep是否添加了Watcher实例,现在知道了是这里的get进行的操作,get中的value = this.getter.call(vm, vm)似乎是在记录监测的值。而get是在Watcher实例化的时候调用的,this.getter也是来自实例化的expOrFn参数。那么,什么时候进行的Watcher的实例化,是完成整个依赖收集的闭环的关键。

经过查找new Watcher的调用位置,发现在src\core\instance\state.jssrc\core\instance\lifecycle.js代码中进行了Watcher实例化,而src\core\instance\state.jsWatcher的实例化跟计算属性和监听属性有关。现在只需聚焦到src\core\instance\lifecycle.js的代码,因为这里的Watcher实例化是跟渲染有关:

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
    
    

  ......

  callHook(vm, 'beforeMount')

  let updateComponent
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    
    
    
    ......
    
  } else {
    
    
    updateComponent = () => {
    
    
      vm._update(vm._render(), hydrating)
    }
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    
    
    before () {
    
    
      if (vm._isMounted && !vm._isDestroyed) {
    
    
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  
  ......
}

这里的mountComponent实际上会被vm.$mount进行调用,也就是在模板挂载时会去进行实例化Watcher,其第二个参数接收了updateComponent作为WatcherexpOrFn,也就是说Watcher里的this.getter实际上是执行的vm._update(vm._render(), hydrating)。这里就涉及 Vue 中的render函数了,这里先不做展开,知道这行代码是将虚拟DOM转为真实DOM就行。这个时候如果render函数内有使用到data中已经转为了响应式的数据,就会触发get方法进行依赖的收集,闭环了之前依赖收集的逻辑。

这里给出一个简要的示意流程:

  • _init->initState->initData-> observe->new Observer->walk->defineReactive->Object.defineProperty定义getter和setter
  • _init->vm.$mount->mountComponent->new Watcher->this.getter->_render->触发Object.defineProperty定义的getter->dep.addSub(sub: Watcher)

更新通知

当我们修改定义在data中的属性时,该属性的setter触发,执行dep.notify(),现在我们看看它的实现:

  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()
    }
  }

我们之前就知道dep中的subsWatcher数组,那么现在实际就在遍历执行每个Watcherupdate方法:


......

    // options
    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
    }

.....

  update () {
    
    
    /* istanbul ignore else */
    if (this.lazy) {
    
    
      this.dirty = true
    } else if (this.sync) {
    
    
      this.run()
    } else {
    
    
      queueWatcher(this)
    }
  }

这里实际会去执行queueWatcher,因为lazysync都没有传值进来,如果你有印象,会记得在mountComponent里Watcher实例化时第四个参数只定义了before。我们继续去找queueWatcher的实现,它在同目录的./scheduler.js文件:

export function queueWatcher (watcher: Watcher) {
    
    
  const id = watcher.id
  if (has[id] == null) {
    
    
    has[id] = true
    if (!flushing) {
    
    
      queue.push(watcher)
    } else {
    
    
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
    
    
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
    
    
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) {
    
    
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}

这里看上去先是进行了Watcher的去重操作,最终都会去执行flushSchedulerQueue,只是执行nextTick(flushSchedulerQueue)再下一次tick时更新它,还是立即更新的区别:

function flushSchedulerQueue () {
    
    
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id

  ......

  for (index = 0; index < queue.length; index++) {
    
    
    watcher = queue[index]
    if (watcher.before) {
    
    
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    
    ......
  }
   
   ......
}

然后就是遍历这个队列,执行传入的before方法触发beforeUpdate钩子。最后执行watcher.run()方法,执行真正的更新通知:

  run () {
    if (this.active) {
      const value = this.get()
         
      ......
    }
  }

其实就是重新执行一次this.get()方法,让vm._update(vm._render())再走一遍而已。然后生成新旧VNode,最后进行比对以更新视图。到这整个响应式实现的流程就算是结束了,下面依旧给出简要的示意流程:

  • 数据更改触发Object.defineProperty定义的setter->dep.notify->遍历执行watcher.update->queueWatcher->flushSchedulerQueue->watcher.run->this.getter->_render

内容预告

本章我们深入 Vue 源码,初步探索其响应式实现原理。在下一章节,我们将对需要进行http网络通信的场景,介绍和演示axios的使用。

猜你喜欢

转载自blog.csdn.net/d2235405415/article/details/123712214