写在前面:本文为个人在日常工作和学习中的一些总结,便于后来查漏补缺,非权威性资料,请带着自己的思考^-^。

说起响应式,首先会想到Vue实例中的data属性,例如:对data中的某一属性重新赋值,如果该属性用在了页面渲染上面,则页面会自动进行重新渲染,这里就以data作为切入点,来看一下Vue中的响应式是怎样的一个实现思路。

Vue实例创建阶段

在创建Vue实例的时候,执行到了一个核心方法:initState,该方法会对methods/props/methods/data/computed/watch进行初始化,此时我们只关注data的初始化:

function initData(vm) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  
  ...
  
  const keys = Object.keys(data)
  let i = keys.length
  while (i--) {
    const key = keys[i]
    
    ...
    proxy(vm, '_data', key)
    ...
    
  }
  
  ...
  
  observe(data, true)
}

代码中省略了一些和当前研究的内容无关的代码,用...表示;
可以看到这个方法主要做了两件事:

  1. 将data代理到vm._data,即:访问data中的属性key vm[key]将触发getter,返回vm._data[key],赋值同理;作用是显而易见的:以后我们想要访问/赋值data中的某个属性key时,直接this[key]这样就可以了,无需this._data[key]这样
  2. 执行observe(data, true)函数,遍历data中的每一个属性,通过defineObject将其设为响应式的,即:在正常使用场景中,我们访问this[key](其中key为data中的一个属性)时,会由于1中缘故触发getter进而访问this._data[key],这样就又触发了当前添加的getter执行某些操作

proxy代码:

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

observe代码:

observe (value: any, asRootData: ?boolean): Observer | void {
  let ob
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    observerState.shouldConvert && // 新添加属性转为reactive
    !isServerRendering() && // 非服务端渲染
    (Array.isArray(value) || isPlainObject(value)) && // 数组或者对象
    Object.isExtensible(value) && // 可扩展对象
    !value._isVue // 非Vue实例
  ) {
    ob = new Observer(value) // 创建Observer实例
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

class Observer {
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    def(value, '__ob__', this) // value.__ob__ = this, 且__ob__为不可枚举属性
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys) // value.__proto__ = Array.prototype
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

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

function defineReactive (
  obj: Object, // vm instance of Vue
  key: string, // '$attr' 等属性名
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()
  let childOb = !shallow && observe(val) // 对当前属性的值继续进行observe
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      
      ...
      
    },
    set: function reactiveSetter (newVal) {
      
      ...
      
    }
  })
}

仍然逃脱不了粘贴代码,但是找不出比代码更直观的解释了...
不过核心方法是defineReactive,它仍然是使用defineProperty对vm._data中的每一个属性设置了getter/setter,至于getter/setter中的内容,先不去管他。
到了这里,对于data的初始化已经告一段落。

模板编译/挂载阶段

这里是Vue.prototype.$mount的执行阶段,此阶段其实包含了对于模板的编译、对编译结果进行转化生成render函数、render函数的执行进行挂载
这个阶段对于data的操作只存在于render函数的执行进行挂载时,核心函数的执行:new Watcher(vm, updateComponent, noop, null, true)
Watcher代码:

 class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function, // 回调函数
    options?: ?Object,
    isRenderWatcher?: boolean // render时实例的Watcher
  ) {
    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
    } 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
    this.deps = [] // 依赖列表
    this.newDeps = [] // 新的依赖列表
    this.depIds = new Set() // 依赖ids
    this.newDepIds = new Set() // 新的依赖ids
    // parse expression for getter
    // 将表达式 expOrFn包装为getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = function () {}
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   * 源码其实已经给出了注释,这里是进行render和依赖收集
   */
  get () {
    pushTarget(this) // 为Dep.target赋值为当前Watcher实例
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm) // 这里是render函数
    } catch (e) {
      ...
      
    } finally {
      if (this.deep) {
        traverse(value)
      }
      popTarget() // 弹出Deptarget
      this.cleanupDeps() // 本次添加的依赖落入this.deps,同时清空this.newDeps
    }
    return value
  },
  addDep (dep: Dep) { // 将Dep实例添加至this.newDeps队列中,这里的Dep实例产生自通过defineReactive为data属性定义getter/setter时,也就是说这里的Dep实例对应一个data属性
    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)
      }
    }
  }
  ...
  
}

从上面的代码可以看出,响应式相关的核心在于所谓的“依赖收集”,也就是在render函数执行的过程中势必会对页面渲染需要的data属性进行读取,这就触发了响应data属性的getter,还记得之前省略掉的observe函数中执行defineReactive函数时有关data属性getter函数相关的代码吗?

defineReactive (
  obj: Object, // vm instance of Vue
  key: string, // '$attr' 等属性名
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {

  ...
  
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) { // 在$mount函数中new Watcher进行依赖收集的时候已经为Dep.target赋值为Watcher实例
        dep.depend() // 这里的Dep实例对应当前data属性,此处会将当前dep实例放入watcher的依赖列表中
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      // setter相关代码
      ...
      
    }
  })
 }
 
 // dep.depend 代码:
...
  depend () {
    if (Dep.target) { // 此处Dep.target已被赋值为Watcher实例
      Dep.target.addDep(this)
    }
  }
...

页面首次渲染时的小结

页面的首次渲染基本上包含上述两个大的过程,这里先主要基于data进行讨论
new Vue(options)中主要做了:

  1. 将data函数转为data对象赋值给vm._data,此时访问data属性可以通过vm._data[key];
  2. 通过defineProperty进行一层代理,访问vm[key] 将返回vm._data[key],方便使用;
  3. 遍历vm._data,执行defineReactive函数,为vm._data的每一个属性添加getter/setter,在getter中进行依赖收集,在setter进行通知响应;

$mount中主要做了:

  1. 编译模板/生成渲染render函数;
  2. 通过实例Watcher对象,在执行render函数时,页面渲染所用到的data属性会被访问,从而触发vm._data[key]的getter,

在getter中将当前属性对应的dep实例添加至Watcher实例的deps列表中,同时将Watcher实例添加进dep的subs观察者列表中;

当data属性值发生变化时

为什么data属性变化了,页面会重新渲染得到更新呢?前面做了很多铺垫,接下来看一下data属性的变更会进行哪些操作
还记得前面提到得通过defineReactive函数为vm._data[key]设置得setter吗?当data变化时会触发该setter

    ...
    
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      if (newVal === value || (newVal !== newVal && value !== value)) { // 如果更新前后值相同,则直接返回
        return
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal) // 如果newVal为引用类型,则对其属性也进行劫持
      dep.notify() // 这里才是属性更新触发操作得核心,它会通知Watcher进行相应得更新
    }
    
    ...
    
    // dep.notify方法
    
    ...
    notify () {
      const subs = this.subs.slice() // 这里存放的是观察者Watcher列表
      for (let i = 0, l = subs.length; i < l; i++) { // 通知每一个Watcher,执行其update方法,进行相应更新
        subs[i].update()
      }
    }
    ...
    
    // Watcher.prototype.update方法
    
    ...
    
    update () {
      if (this.lazy) {
        this.dirty = true
      } else if (this.sync) { // 同步更新
        this.run()
      } else {
        queueWatcher(this) // 这个方法是nextTick中去执行Watcher.prototype.run方法,也就是说data属性更新触发setter然后通知Watcher去update这个过程通常并非同步执行的,而是会先被放入一个队列,异步执行,落地到我们使用中:我们不用担心同时修改多个data属性带来严重的性能问题,因为其触发的更新并非同步执行的;还有一点是Watcher.prototype.run方法中会执行get方法(还记得在首次渲染进行依赖收集的时候有这个方法吗?)该方法中会执行render进行vnode生成,当然会访问到data中的属性,这样就是一个依赖更新的过程,是不是一个闭环?
      }
    }
  
    ...
    

queueWatcher(this)
这个方法是nextTick中去执行Watcher.prototype.run方法,也就是说data属性更新触发setter然后通知Watcher去update这个过程通常并非同步执行的,而是会先被放入一个队列,异步执行,落地到我们使用中:我们不用担心同时修改多个data属性带来严重的性能问题,因为其触发的更新并非同步执行的;
还有一点是Watcher.prototype.run方法中会执行get方法(还记得在首次渲染进行依赖收集的时候有这个方法吗?)该方法中会执行render进行vnode生成,当然会访问到data中的属性,这样就是一个依赖更新的过程,是不是一个闭环?
另外不能忽略的一点是,在这个方法执行中还会触发updated 钩子函数,当然这里不做深入研究,只做一个大致了解,因为Vue中的细节很多,但是它不影响我们了解主要流程。

最后

放两张在debugg源码时写的两张图,只有自己能看懂当初想到哪里。。。
image.png
image.png
THE END


innocence
11 声望1 粉丝

undefined