vue源码解析响应式的流程实现细节(干货)【三】

前言:

上一节,我们介绍了依赖收集和依赖触发的原理,虽然知道是通过自定义属性(defineProperty)的get和set进行实现的,但还是不清楚具体实现的细节,以及怎么通过依赖收集和依赖触发实现响应式的,带着这样的疑问,开始探索

导读:

我将以图形化、流程化和代码层次逐一展开说明

官网的响应式原理:深入响应式原理

官网的解释:

1、普通的 JavaScript 对象传给 Vue 实例的 data 选项时,vue会遍历此对象所有的属性.
2、属性遍历的时候,会为每个属性添加getter/setter方法
3、属性被访问时,收集当前的依赖【观察者】
4、属性被修改时,执行依赖【观察者】,更新属性
5、watcher把组件渲染的时候,对应的属性记录为依赖
6、属性被修改时,通知watcher【观察者】重新计算,关联的组件更新
复制代码

官网只是宏观上的描述了这样的一个过程,所以必须撸一波源码才能真正的理解其实现原理

代码分析

依然我们还是从源头开始看起,这里主要看数据的初始化部分(因为这部分是最重要的),你可以在instance/state.js下找到initState这个方法,然后代码块里有个initData这个方法

function initData (vm: Component) {
  // 删减
  // observe data
  observe(data, true /* asRootData */)
}
复制代码

直接定位到最后一行

// observe data
observe(data, true /* asRootData */)
复制代码

observe工厂函数

调用了 observe 函数观测数据,observe 函数来自于 core/observer/index.js 文件,打开该文件找到 observe 函数:

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}
复制代码

上面的代码,主要做了几件事

1、判断当前的值是否为一个对象或节点
2、是否存在__ob__属性,这里就是Observer的实例
3、如果不存在,满足一些条件后,然后创建Observer实例,并返回这个实例对象
复制代码

Observe构造函数

其实真正将数据对象转换成响应式数据的是 Observer 函数,它是一个构造函数,同样定义在 core/observer/index.js 文件下,如下是简化后的代码:

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

  constructor (value: any) {
    // 省略...
  }

  walk (obj: Object) {
    // 省略...
  }
  
  observeArray (items: Array) {
    // 省略...
  }
}
复制代码

可以清晰的看到 Observer 类的实例对象将拥有三个实例属性

1、分别是 value、dep 和 vmCount 以及两个实例方法 walk 和 observeArray
2、Observer 类的构造函数接收一个参数,即数据对象。
复制代码

constructor

constructor (value: any) {
    this.value = value // 实例对象的 value 属性引用了数据对象
    this.dep = new Dep() // 实例对象的 dep 属性,保存了一个新创建的 Dep 实例对象
    this.vmCount = 0 // 
    def(value, '__ob__', this) // 重点 创建__ob__属性是不可枚举的,避免枚举时当做属性被遍历
    if (Array.isArray(value)) { // 如果是数组处理对象数组
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else { // 处理对象
      this.walk(value)
    }
  }
复制代码

主要来看看这个方法def

def(value, '__ob__', this) // 重点
复制代码

举个例子

假设我们的数据对象如下:
const data = {
  a: 1
}
那么经过 def 函数处理之后,data 对象应该变成如下这个样子:

const data = {
  a: 1,
  // __ob__ 是不可枚举的属性
  __ob__: {
    value: data, // value 属性指向 data 数据对象本身,这是一个循环引用
    dep: dep实例对象, // new Dep()
    vmCount: 0
  }
}
复制代码

目的:为多层属性遍历添加依赖 上面代码主要做了:

1、实例对象的 value 属性引用了数据对象
2、实例对象的 dep 属性,保存了一个新创建的 Dep 实例对象
3、创建__ob__属性是不可枚举的,避免枚举时当做属性被遍历
4、如果为数组走处理数组逻辑,否则走对象处理逻辑(walk)
复制代码

walk对象处理

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

后续就是依赖收集和触发的部分,可移步上一篇的分析.依赖收集和依赖触发我们暂时先跳过这部分,不做赘述,下面主要谈谈defineReactive方法下的get和set闭包内dep的引用

get方法下

if (Dep.target) {
    dep.depend() // 重点 调用depend方法,收集当前依赖【watcher】到dep
    if (childOb) {
      childOb.dep.depend()
      if (Array.isArray(value)) {
        dependArray(value)
      }
    }
}

// dep.depend() 方法时,addDep添加依赖 core/observer/watcher.js下
// 添加依赖
addDep (dep: Dep) {
const id = dep.id
// newDepIds避免本次get中重复收集依赖
if (!this.newDepIds.has(id)) {
  this.newDepIds.add(id)
  this.newDeps.push(dep)
  // 避免多次求值中重复收集依赖,每次求值之后newDepIds会被清空,因此需要depIds来判断。newDepIds中清空
  if (!this.depIds.has(id)) {
    dep.addSub(this)
  }
}
}
复制代码
1、在addDep中添加依赖,并避免对一个数据多次求值时,其观察者被重复收集。
2、newDepIds避免一次求值的过程中重复收集依赖
3、depIds 属性避免多次求值中重复收集依赖
复制代码

set方法下

dep.notify() // 执行收集的依赖
复制代码

Watcher类

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) {
      // 将渲染函数的观察者存入_watcher
      vm._watcher = this
    }
    //将所有观察者push到_watchers列表
    vm._watchers.push(this)
    // options
    if (options) {
      // 是否深度观测
      this.deep = !!options.deep
      // 是否为开发者定义的watcher(渲染函数观察者、计算属性观察者属于内部定义的watcher)
      this.user = !!options.user
      // 是否为计算属性的观察者
      this.computed = !!options.computed
      this.sync = !!options.sync
      //在数据变化之后、触发更新之前调用
      this.before = options.before
    } else {
      this.deep = this.user = this.computed = this.sync = false
    }
    // 定义一系列实例属性
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.computed // for computed watchers
    this.deps = []
    this.newDeps = []
    // depIds 和 newDepIds 用书避免重复收集依赖
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    // 兼容被观测数据,当被观测数据是function时,直接将其作为getter
    // 当被观测数据不是function时通过parsePath解析其真正的返回值
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = function () {}
        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
        )
      }
    }
    if (this.computed) {
      this.value = undefined
      this.dep = new Dep()
    } else {
      // 除计算属性的观察者以外的所有观察者调用this.get()方法
      this.value = this.get()
    }
  }

  // get方法
  get () {
    ...
  }
  // 添加依赖
  addDep (dep: Dep) {
    ...
  }
  // 移除废弃观察者;清空newDepIds 属性和 newDeps 属性的值
  cleanupDeps () {
    ...
  }
  // 当依赖变化时,触发更新
  update () {
    ...
  }
  // 数据变化函数的入口
  run () {
    ...
  }
  // 真正进行数据变化的函数
  getAndInvoke (cb: Function) {
    ...
  }
  //
  evaluate () {
    ...
  }
  //
  depend () {
    ...
  }

  //
  teardown () {
    ...
  }
}
复制代码

watcher构造函数

由以上代码可见,在watcher构造函数中做了如下几件事:

  • 将组件的渲染函数的观察者存入_watcher,将所有的观察者存入_watchers中,你可以通过$watchers访问到

  • 保存before函数,在数据变化之后、触发更新之前调用 定义一系列实例属性

  • 兼容被观测数据,当被观测数据是function时,直接将其作为getter;

  • 当被观测数据不是function时通过parsePath解析其真正的返回值,被观测

  • 数据是 'obj.name'时,通过parsePath拿到真正的obj.name的返回值

  • 除计算属性的观察者以外的所有观察者调用this.get()方法

总结一下流程:

  • 调用$mount()函数进入到挂载阶段

  • 检查是否有render()函数,根据上述模版创建render()函数

  • 调用了mountComponent()函数完成挂载,并在mountComponen()中定义并初始化updateComponent()

  • 为渲染函数添加观察者,在观察者中对渲染函数求值 在求值的过程中触发数据对象str的get,在str的get中收集str的观察者到数据的dep中

  • 修改str的值时,触发str的set,在set中调用数据的dep的notify触发响应 notify中对每一个观察者调用update方法

  • 在run中调用getAndInvoke函数,进行数据变化。

  • 在getAndInvoke函数中调用回调函数

  • 对于渲染函数的观察者来说getAndInvoke就相当于执行updateComponent函数

  • 在updateComponent函数中调用_render函数生成vnode虚拟节点,以虚拟节点vnode作为参数调用_update函数,生成真正的DOM

至此响应式流程已完结

转载于:https://juejin.im/post/5c8f800ae51d45542743af3d

你可能感兴趣的:(vue源码解析响应式的流程实现细节(干货)【三】)