vue 响应式源码版(偏收集依赖)

本文深入探讨Vue.js的响应式系统,从初始化组件、数据代理、依赖收集到更新派发,详细解析了`_init`、`_update`、`Observer`、`Dep`、`Watcher`等关键过程,揭示了Vue如何实现数据驱动视图的更新机制。
摘要由CSDN通过智能技术生成

该文章结合: computed计算属性源码解析 食用理解更佳

只有访问数据触发了 get 才会去收集依赖,一开始只是递归对数据进行响应式处理,无法收集依赖
触发依赖:用到响应式对象某个属性的,template模板上的内容,以及手写的watcher的回调等

收集依赖:

  • 会在组件挂载mounted前,调用updateComponent方法之后会生成渲染的虚拟dom,这个过程中会访问 vm 上的数据,就触发了数据对象的 getter,进行首次的依赖收集
  • 在之后的setter改变后,就会触发依赖更新视图
export function mountComponent (...): Component {
  // 调用生命周期钩子函数
  callHook(vm, 'beforeMount')
  let updateComponent
  updateComponent = () => {
    // 调用 _update 对 render 返回的虚拟 DOM 进行 patch(也就是 Diff )到真实DOM,这里是首次渲染
    vm._update(vm._render(), hydrating)
  }
  // 为当前组件实例设置观察者,监控 updateComponent 函数得到的数据,下面有介绍
  new Watcher(vm, updateComponent, noop, {
    // 当触发更新的时候,会在更新之前调用
    before () {
      // 判断 DOM 是否是挂载状态,就是说首次渲染和卸载的时候不会执行
      if (vm._isMounted && !vm._isDestroyed) {
        // 调用生命周期钩子函数
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  // 没有老的 vnode,说明是首次渲染
  if (vm.$vnode == null) {
    vm._isMounted = true
    // 调用生命周期钩子函数
    callHook(vm, 'mounted')
  }
  return vm
}

初始化

src/core/instance/init.js - 15export function initMixin (Vue: Class<Component>) {
  // 在原型上添加 _init 方法
  Vue.prototype._init = function (options?: Object) {
    ...
    vm._self = vm
    initLifecycle(vm) // 初始化实例的属性、数据:$parent, $children, $refs, $root, _watcher...等
    initEvents(vm) // 初始化事件:$on, $off, $emit, $once
    initRender(vm) // 初始化渲染: render, mixin
    callHook(vm, 'beforeCreate') // 调用生命周期钩子函数
    initInjections(vm) // 初始化 inject
    initState(vm) // 初始化组件数据:props, data, methods, watch, computed
    initProvide(vm) // 初始化 provide
    callHook(vm, 'created') // 调用生命周期钩子函数
    ...
  }
}

initState()

export function initState (vm: Component) {
  vm._watchers = []
  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 {
    // 没有 data 的话就默认赋值为空对象,并监听
    observe(vm._data = {}, true /* asRootData */)
  }
  // 初始化 computed
  if (opts.computed) initComputed(vm, opts.computed)
  // 初始化 watch
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

其中与响应式数据相关的,也就是 initProps()、initData()、observe()

initProps()

  • 遍历父组件传进来的 props 列表
  • 校验每个属性的命名、类型、default 属性等,都没有问题就调用 defineReactive 设置成响应式
  • 然后用 proxy() 把属性代理到当前实例上,如把 vm._props.xx 变成 vm.xx,就可以访问
src/core/instance/state.js - 65function initProps (vm: Component, propsOptions: Object) {
  // 父组件传入子组件的 props
  const propsData = vm.$options.propsData || {}
  // 经过转换后最终的 props
  const props = vm._props = {}
  // 存放 props 的 key,就算 props 值空了,key 也会在里面
  const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent
  // 转换非根实例的 props
  if (!isRoot) {
    toggleObserving(false)
  }
  for (const key in propsOptions) {
    keys.push(key)
    // 校验 props 类型、default 属性等
    const value = validateProp(key, propsOptions, propsData, vm)
    // 在非生产环境中
    if (process.env.NODE_ENV !== 'production') {
      const hyphenatedKey = hyphenate(key)
      if (isReservedAttribute(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {
        warn(`hyphenatedKey 是保留属性,不能用作组件 prop`)
      }
      // 把 props 设置成响应式的
      defineReactive(props, key, value, () => {
        // 如果用户修改 props 发出警告
        if (!isRoot && !isUpdatingChildComponent) {
          warn(`避免直接改变 prop`)
        }
      })
    } else {
      // 把 props 设置为响应式
      defineReactive(props, key, value)
    }
    // 把不在默认 vm 上的属性,代理到实例上
    // 可以让 vm._props.xx 通过 vm.xx 访问
    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}

initData()

  • 初始化一个 data,并拿到 keys 集合
  • 遍历 keys 集合,来判断有没有和 props 里的属性名或者 methods 里的方法名重名的
  • 没有问题就通过 proxy() 把 data 里的每一个属性都代理到当前实例上,就可以通过 this.xx 访问了
  • 最后再调用 observe 监听整个 data
function initData (vm: Component) {
  // 获取当前实例的 data 
  let data = vm.$options.data
  // 判断 data 的类型
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(`数据函数应该返回一个对象`)
  }
  // 获取当前实例的 data 属性名集合
  const keys = Object.keys(data)
  // 获取当前实例的 props 
  const props = vm.$options.props
  // 获取当前实例的 methods 对象
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    // 非生产环境下判断 methods 里的方法是否存在于 props 中
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(`Method 方法不能重复声明`)
      }
    }
    // 非生产环境下判断 data 里的属性是否存在于 props 中
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(`属性不能重复声明`)
    } else if (!isReserved(key)) {
      // 都不重名的情况下,代理到 vm 上
      // 可以让 vm._data.xx 通过 vm.xx 访问
      proxy(vm, `_data`, key)
    }
  }
  // 监听 data
  observe(data, true /* asRootData */)
}

observe()
这个方法主要就是用来给数据加上监听器的

这里主要做的是:

  • 如果是 vnode 的对象类型或者不是引用类型,就直接跳出
  • 否则就给没有添加 Observer 的数据添加一个 Observer,也就是监听者
src/core/observer/index.js - 110export function observe (value: any, asRootData: ?boolean): Observer | void {
  // 如果不是'object'类型 或者是 vnode 的对象类型就直接返回
  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
}

Observer
这是一个类,作用是把一个正常的数据成可观测的数据

这里主要做的是:

给当前 value 打上已经是响应式属性的标记,避免重复操作
然后判断数据类型

  • 如果是对象,就遍历对象,调用 defineReactive()创建响应式对象
  • 如果是数组,就遍历数组,调用 observe()对每一个元素进行监听
src/core/observer/index.js - 37export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // 根对象上的 vm 数量
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    // 给 value 添加 __ob__ 属性,值为value 的 Observe 实例
    // 表示已经变成响应式了,目的是对象遍历时就直接跳过,避免重复操作
    def(value, '__ob__', this)
    // 类型判断
    if (Array.isArray(value)) {
      // 判断数组是否有__proty__
      if (hasProto) {
        // 如果有就重写数组的方法
        protoAugment(value, arrayMethods)
      } else {
        // 没有就通过 def,也就是Object.defineProperty 去定义属性值
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
  // 如果是对象类型
  walk (obj: Object) {
    const keys = Object.keys(obj)
    // 遍历对象所有属性,转为响应式对象,也是动态添加 getter 和 setter,实现双向绑定
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
  // 监听数组
  observeArray (items: Array<any>) {
    // 遍历数组,对每一个元素进行监听
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

defineReactive()
这个方法的作用是定义响应式对象
这里主要做的是:

  • 先初始化一个 dep 实例

  • 如果是对象就调用 observe,递归监听,以保证不管结构嵌套多深,都能变成响应式对象

  • 然后调用 Object.defineProperty() 劫持对象属性的 getter 和 getter

  • 如果获取时,触发 getter 会调用 dep.depend() 把观察者 push 到依赖的数组 subs 里去,也就是依赖收集

  • 如果更新时,触发 setter 会做以下操作

    • 新值没有变化或者没有 setter 属性的直接跳出
    • 如果新值是对象就调用 observe() 递归监听
    • 然后调用 dep.notify() 派发更新
src/core/observer/index.js - 135export 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 是对象的话就递归监听
  // 递归调用 observe 就可以保证不管对象结构嵌套有多深,都能变成响应式对象
  //childOb为当前属性的值为对象时,返回这个对象的Observer实例
  let childOb = !shallow && observe(val)
  // 截持对象属性的 getter 和 setter
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    // 拦截 getter,当取值时会触发该函数
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      // 进行依赖收集
      // 初始化渲染 watcher 时访问到需要双向绑定的对象,从而触发 get 函数
      if (Dep.target) {
      	//给当前属性添加对应的watcher实例,每个属性都有一个Dep实例,来管理所有该属性的依赖watcher
        dep.depend()
        // 比如{a:[1,2,3]} 属性a对应的值是一个数组 观测数组的返回值就是对应数组的Observer实例对象,实现整个数组对象的观察,而非数组中的普通元素
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) { //如果数据结构类似 {a:[1,2,[3,4,[5,6]]]}
          
            dependArray(value) // 不停的进行依赖收集
          }
        }
      }
      return value
    },
    // 拦截 setter,当值改变时会触发该函数
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      // 判断是否发生变化
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // 没有 setter 的访问器属性
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // 如果新值是对象的话递归监听
      childOb = !shallow && observe(newVal)
      // 派发更新
      dep.notify()
    }
  })
}

// 递归收集数组依赖
function dependArray(value) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i];
    // e.__ob__代表e已经被响应式观测了 但是没有收集依赖 所以把他们收集到自己的Observer实例的dep里面
    e && e.__ob__ && e.__ob__.dep.depend();
    if (Array.isArray(e)) {
      // 如果数组里面还有数组  就递归去收集依赖
      dependArray(e);
    }
  }
}

//数组的派发更新
// src/observer/array.js
methodsToPatch.forEach((method) => {
  arrayMethods[method] = function (...args) {
    //   这里保留原型方法的执行结果
    const result = arrayProto[method].apply(this, args);
    // 这句话是关键
    // this代表的就是数据本身 比如数据是{a:[1,2,3]} 那么我们使用a.push(4)  this就是a  ob就是a.__ob__ 这个属性代表的是该数据已经被响应式观察过了 __ob__对象指的就是Observer实例
    const ob = this.__ob__;
    let inserted;
    switch (method) {
      case "push":
      case "unshift":
        inserted = args;
        break;
      case "splice":
        inserted = args.slice(2);
      default:
        break;
    }
    if (inserted) ob.observeArray(inserted); // 对新增的每一项进行观测
    ob.dep.notify(); //数组派发更新 ob指的就是数组对应的Observer实例 我们在get的时候判断如果属性的值还是对象那么就在Observer实例的dep收集依赖 所以这里是一一对应的  可以直接更新
    return result;
  };
});

依赖收集
Dep
这是一个类,它实际上就是对 Watcher 的一种管理
这里首先初始化一个 subs 数组,用来存放依赖,也就是观察者,谁依赖这个数据,谁就在这个数组里,然后定义几个方法来对依赖添加、删除、通知更新等

另外它有一个静态属性 target,这是一个全局的 Watcher,也表示同一时间只能存在一个全局的 Watcher,因为一个Vue实例(组件)对应一个Watcher类,然后其中的每个响应式属性都添加这个类的实例,又因为组件是按顺序加载,所以同一时间只有一个Watcher类,该类收集完后就去除掉

src/core/observer/dep.js

let uid = 0
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为当前组件对应的Watcher的实例
      // 调用 Watcher 实例的 addDep 函数,将当前Dep实例传进去,然后调用实例的addSub方法添加wacher实例
      Dep.target.addDep(this)
    }
  }
  // 派发更新
  notify () {
    ...
  }
}

管理当前Watcher实例的方法

// 同一时间只有一个观察者使用,赋值观察者
Dep.target = null
const targetStack = []

export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target //将Dep.target指向当前Watcher实例
}

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1] //清除当前Watcher实例
}

Watcher
Watcher 也是一个类,也叫观察者(订阅者),这里干的活还挺复杂的,而且还串连了渲染和编译

src/core/observer/watcher.js

let uid = 0
export default class Watcher {
  ...
  constructor (
    vm: Component,
    expOrFn: string | Function, //依赖对应的表达式路径或是一个方法
    cb: Function,   //当前依赖对应的回调函数,如$watcher('xx.x',fn)
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // Watcher 实例持有的 Dep 实例的数组
    //一个响应式属性对应一个Dep实例,实例中保存多个Watcher实例,然后在Watcher实例中获取该属性的Dep实例,就能拿到该属性所有的Watcher实例,从而可以做到后面的取消依赖
    //旧Dep实例,首次和报错setter之前的该属性的Dep实例
    this.deps = []
    //新Dep实例,每次setter调用之后的该属性的Dep实例
    this.newDeps = []
    
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.value = this.lazy
      ? undefined
      : this.get()
	
	//获取依赖对应的属性路径|方法
    if (typeof expOrFn === 'function') {  //如upcateComponent,第一次组件挂载前调用或者计算属性对应的回调
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn) //如"a.b.c",就是通过这个路径去获取data.a.b.c的内容
    }
  }
  get () 
    // 该函数用于缓存 Watcher
    // 因为在组件含有嵌套组件的情况下,需要恢复父组件的 Watcher
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 调用回调函数,也就是upcateComponent|表达式路径,即触发对应的getter,从而在defineReactive中触发dep.depend进行依赖收集
      value = this.getter.call(vm, vm)
    } catch (e) {
      ...
    } finally {
      // 深度监听
      if (this.deep) {
        traverse(value)
      }
      // 恢复Watcher
      popTarget()
      // 清理不需要了的依赖
      this.cleanupDeps()
    }
    return value
  }
  // 依赖收集时调用
  addDep (dep: Dep) {
    const id = dep.id
   	//把dep放到newDeps里面 同时保证同一个dep只被保存到watcher一次  同样的  同一个watcher也只会保存在dep一次
    if (!this.newDepIds.has(id)) { 
      this.newDepIds.add(id)
      this.newDeps.push(dep) //触发收集时,保存进当前Wacher实例的newDeps
      if (!this.depIds.has(id)) {
        // 把当前 Watcher实例 push 进数组
        dep.addSub(this)
      }
    }
  }
  // 清理不需要的依赖(下面有)
  cleanupDeps () {
    ...
  }
  // 派发更新时调用(下面有)
  update () {
    ...
  }
  // 执行 watcher 的回调,即传入的cb
  run () {
    ...
  }
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
}


  • 在 watcher.run() 里面会执行回调,传入新值和老值两个参数
  • 为什么要初始化两个 Dep 实例数组?因为 Vue 是数据驱动的,每次数据变化都会重新 render,也就是说 vm.render() 方法就又会重新执行,再次触发 getter,所以用两个数组表示,新添加的 Dep 实例数组 newDeps 和上一次添加的实例数组 deps

开始收集依赖
在首次渲染挂载的时候

src/core/instance/lifecycle.js - 141export function mountComponent (...): Component {
  // 调用生命周期钩子函数
  callHook(vm, 'beforeMount')
  let updateComponent
  updateComponent = () => {
    // 调用 _update 对 render 返回的虚拟 DOM 进行 patch(也就是 Diff )到真实DOM,这里是首次渲染
    vm._update(vm._render(), hydrating)
  }
  // 为当前组件实例设置观察者,监控 updateComponent 函数得到的数据,下面有介绍
  new Watcher(vm, updateComponent, noop, {
    // 当触发更新的时候,会在更新之前调用
    before () {
      // 判断 DOM 是否是挂载状态,就是说首次渲染和卸载的时候不会执行
      if (vm._isMounted && !vm._isDestroyed) {
        // 调用生命周期钩子函数
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  // 没有老的 vnode,说明是首次渲染
  if (vm.$vnode == null) {
    vm._isMounted = true
    // 调用生命周期钩子函数
    callHook(vm, 'mounted')
  }
  return vm
}


  • 挂载之前会实例化一个渲染 watcher ,进入 watcher 构造函数里就会执行 this.get() 方法
    然后就会执行 pushTarget(this),就是把 Dep.target 赋值为当前渲染 watcher 并压入栈(为了恢复用)
  • 然后执行 this.getter.call(vm, vm),也就是上面的 updateComponent() 函数,里面就执行了 vm._update(vm._render(), hydrating)
  • 接着执行 vm._render() 就会生成渲染 vnode,这个过程中会访问 vm 上的数据,就触发了数据对象的 getter
    每一个对象值的 getter 都有一个 dep,在触发 getter 的时候就会调用 dep.depend() 方法,也就会执行 Dep.target.addDep(this)
  • 然后这里会做一些判断,以确保同一数据不会被多次添加,接着把符合条件的数据 push 到 subs 里,到这就已经完成了依赖的收集,不过到这里还没执行完,如果是对象还会递归对象触发所有子项的getter,还要恢复 Dep.target 状态

移除订阅
移除订阅就是调用 cleanupDeps() 方法。比如在模板中有 v-if 我们收集了符合条件的模板 a 里的依赖。当条件改变时,模板 b 显示出来,模板 a 隐藏。这时就需要移除 a 的依赖

这里主要做的是:

  • 先遍历上一次添加的实例数组 deps,移除 dep.subs 数组中的 Watcher 的订阅
  • 然后把 newDepIds 和 depIds 交换,newDeps 和 deps 交换
  • 再把 newDepIds 和 newDeps 清空
// 清理不需要的依赖
  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
  }

派发更新
notify()

notify () {
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // 如果不是异步,需要排序以确保正确触发
      subs.sort((a, b) => a.id - b.id)
    }
    // 遍历所有 watcher 实例数组
    for (let i = 0, l = subs.length; i < l; i++) {
      // 触发更新
      subs[i].update()
    }
  }

update()

  update () {
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      // 组件数据更新会走这里
      queueWatcher(this)
    }
  }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值