Vue2源码,响应式原理-对象

读完本篇文章,可以回答如下内容:

  1. Vue2是如何拦截对象的,
  2. Vue2响应式原理的具体实现
  3. Dep类、Watcher类、Observer类分别有什么作用
  4. Vue2拦截对象有什么缺陷 5. Vue.setVue.delete的实现原理

参考《深入浅出Vue.js》,并引用了Vue2源码内容

1. 啥是变化侦测

定义:理解响应式原理,要先理解变化侦测的概念。变化侦测是指当某个变量值改变时,能够侦测到并且通知到使用这个变量的地方进行更新

进一步,需要明确,使用这个变量的地方还有一个名字叫做依赖。数据变化,需要通知依赖。

此外,Vue2的状态变化是通知到组件,不管组件里面有多少个数据,都是通知到组件,然后组件内部使用虚拟DOM进行比对。

2. 如何侦听对象数据

Vue2封装了defineReactive方法,里面使用Object.defineProperty()这个API进行对象侦听。当访问对象的属性时触发get,当修改对象的属性时触发set

js 复制代码
function defineReactive (data, key, val) {
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function () {
            // 读取触发get
            return val
        },
        set: function (newVal) {
            // 修改触发set
            if (val === newVal) {
                return
            }
            val = newVal
        }
    })
}
let xhg = {
    height: 180
}
defineReactive(xhg, 'height', 190)
console.log(xhg.height, 'xhg.height');

对比Vue3,是使用Proxy来拦截数据

js 复制代码
const obj = new Proxy(data, {
    get(target, key) {
        bucket.add(effect)
        return target[key]
    },
    set(target, key, newVal) {
        target[key] = newVal
        bucket.forEach(fn => fn())
        // 返回true,表示设置操作成功
        return true
    }
})

3. 如何收集依赖

定义:收集依赖是指收集所有使用了这个数据的地方,比如一个DOM访问了这个变量,那么这个DOM就是一个依赖。

如下声明了一个dep数组记录所有依赖信息,在触发get时,假设window.target里面存储了依赖的内容,把这个内容push到dep数组里面,在修改这个对象的这个数据时,把dep数组里面的依赖挨个拿出来执行一遍

diff 复制代码
function defineReactive (data, key, val) {
+    let dep = []
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function () {
+            dep.push(window.target)
            // 读取触发get
            return val
        },
        set: function (newVal) {
            // 修改触发set
            if (val === newVal) {
                return
            }
+            for (let i = 0; i < dep.length; i++) {
+                dep[i](newVal, val)
+            }
            val = newVal
        }
    })
}
let xhg = {
    height: 180
}
defineReactive(xhg, 'height', 190)
console.log(xhg.height, 'xhg.height');

但是这样不太灵活,在Vue2里面是封装了一个Dep类,它是收集依赖的类。在触发对象的get拦截函数时,收集所有的依赖执行dep.depend(),在set拦截函数里面执行dep.notify()触发所有的依赖

js 复制代码
class Dep {
    constructor () {
        this.subs = []
    }
    addSub (sub) {
        this.subs.push(sub)
    }
    removeSub (sub) {
        remove(this.subs, sub)
    }
    depend (sub) {
        if (window.target) {
            this.addSub(window.target)
        }
    }
    notify (subs) {
        // 通知依赖执行
        for (let i = 0; i < subs.length; i++) {
            subs[i].update()
        }
    }
}

// 移除函数
function remove (array, item) {
    if (array.length) {
        let index = array.indexOf(item)
        if (index > -1) {
            array.splice(index, 1)
        }
    }
}

在defineReactive函数中

diff 复制代码
function defineReactive (data, key, val) {
+    let dep = new Dep()
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function () {
            // 存储依赖
+            dep.depend()
            return val
        },
        set: function (newVal) {
            if (val === newVal) {
                return
            }
            // 通知依赖
+            dep.notify()
            val = newVal
        }
    })
}

对应源码

可以只关注高亮的部分

diff 复制代码
/src/core/observer/dep.ts
export default class Dep {
  static target?: DepTarget | null
  id: number
  subs: Array<DepTarget | null>
  // pending subs cleanup
  _pending = false

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

  addSub(sub: DepTarget) {
+    this.subs.push(sub)
  }

  removeSub(sub: DepTarget) {
    // #12696 deps with massive amount of subscribers are extremely slow to
    // clean up in Chromium
    // to workaround this, we unset the sub for now, and clear them on
    // next scheduler flush.
    this.subs[this.subs.indexOf(sub)] = null
    if (!this._pending) {
      this._pending = true
      pendingCleanupDeps.push(this)
    }
  }

+  depend(info?: DebuggerEventExtraInfo) {
    if (Dep.target) {
+      Dep.target.addDep(this)
      if (__DEV__ && info && Dep.target.onTrack) {
        Dep.target.onTrack({
          effect: Dep.target,
          ...info
        })
      }
    }
  }

  notify(info?: DebuggerEventExtraInfo) {
    // stabilize the subscriber list first
    const subs = this.subs.filter(s => s) as DepTarget[]
    if (__DEV__ && !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++) {
+      const sub = subs[i]
      if (__DEV__ && info) {
        sub.onTrigger &&
          sub.onTrigger({
            effect: subs[i],
            ...info
          })
      }
      sub.update()
    }
  }
}

其中请注意,源码中是使用了Dep.target,他代表Watcher实例,如果存在Watcher实例,则调用他的addDeps方法,这个方法的实现在Watcher类中是这样的

diff 复制代码
/src/core/observer/watcher.ts
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)
      }
    }
}

4. 依赖是谁

依赖是什么?依赖是数据变化时,我们要通知的地方!Vue2中给依赖取名为Watcher!数据变化,先通知Watcher,然后Watcher再去通知其他地方。

假设侦听器里面侦听了data.a.b.c的属性,当data.a.b.c属性值发生改变时,要通知这里的回调函数重新执行一次,那么这个回调他就是一个依赖

js 复制代码
watch('data.a.b.c', (newval) => {
    console.log(newVal)
})

Vue当中的Watcher类实例的实现方式:

js 复制代码
class Watcher {
    constructor (vm, expOrfn, cb) {
        this.vm = vm
        this.getter = parsePath(expOrfn)
        this.cb = cb
        this.value = this.get()
    }

    get () {
        window.target = this
        let value = this.getter.call(this.vm, this.vm)
        window.target = undefined
        return value
    }

    update () {
        const oldValue = this.value
        // 拿到getter里面最新的值
        this.value = this.get()
        this.cb.call(this.vm, this.value, oldValue)
    }

}

如上Watcher类中,在构造函数里面会执行this.get(),该方法会把this也就是当前的Watcher实例存储到window.target上;然后执行parsePath函数,该函数会返回一个函数赋值给this.getter;接着执行this.get函数

this.get函数中,会将this也就是当前的Watcher实例存储到window.target上,然后执行this.getter函数,这个函数会读取对象的属性进而触发他的getter,并收集依赖,把当前的Watcher实例收集到对应的dep里去

一旦对象的属性修改触发setter,会调用dep.notify,该方法会把所有依赖拿出来执行他们的update方法,update方法在Watcher实例里面,会执行对应的回调,传入新值和旧值

Watcher类中parsePath的实现方式是:

js 复制代码
const bailRE = /^[\w.$]/ // 匹配非法字符
function parsePath (path) {
    if (bailRE.test(path)) {
        return
    }
    let segments = path.split('.')
    return function (obj) {
        for (let i = 0; i < segments.length; i++) {
            if (!obj) return
            obj = obj[segments[i]]
        }
        return obj
    } 
}

该方法会首先排除特殊字符,如果有除了[a-zA-Z0-9_.]这些以外的字符出现,就会直接return;

接着他会返回一个函数,这个函数会去读取嵌套的对象的属性,比如传入data.a.b.c,他会读取data[a][b][c]的值,一旦读取就会自动触发getter拦截函数了

对应源码

暂时只用关注高亮的部分代码

diff 复制代码
export default class Watcher implements DepTarget {
    ......
    constructor(
+    vm: Component | null,
+    expOrFn: string | (() => any),
+    cb: Function,
    options?: WatcherOptions | null,
    isRenderWatcher?: boolean
  ) {
    ......省略了一部分代码
    if (isFunction(expOrFn)) {
      this.getter = expOrFn
    } else {
+      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        __DEV__ &&
          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()
  }
  
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
+      value = this.getter.call(vm, vm)
    } catch (e: any) {
      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
  }
  
  update() {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
+      this.run()
    } else {
      queueWatcher(this)
    }
  }

  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  run() {
    if (this.active) {
      const value = this.get()
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
+        const oldValue = this.value
+        this.value = value
        if (this.user) {
          const info = `callback for watcher "${this.expression}"`
          invokeWithErrorHandling(
            this.cb,
            this.vm,
            [value, oldValue],
            this.vm,
            info
          )
        } else {
+          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
}

5. 如何递归处理对象的所有Key

Vue2封装了一个Observer类,如果是对象类型的数据,才会利用Object.keys方法去遍历对象的所有属性,依次执行defineReactive方法

js 复制代码
class Observer {
    constructor (value) {
        this.value = value
        // 如果不是数组
        if (!Array.isArray(value)) {
            this.walk(value)
        }
    }

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

defineReactive方法的改造,如果数据还是对象,则递归处理

diff 复制代码
// 改造该函数
function defineReactive (data, key, val) {
+    if (typeof val === 'object') {
+        new Observer(val)
+    }
    let dep = new Dep()
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function () {
            // 存储依赖
            dep.depend()
            return val
        },
        set: function (newVal) {
            if (val === newVal) {
                return
            }
            debugger
            val = newVal
             // 通知依赖
            dep.notify()
            console.log(val, 'val');
        }
    })
}

对应具体源码

暂时先关注下面高亮的部分

diff 复制代码
/src/core/observer/index.js
export class Observer {
  dep: Dep
  vmCount: number // number of vms that have this object as root $data

  constructor(public value: any, public shallow = false, public mock = false) {
    // this.value = value
    this.dep = mock ? mockDep : new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (isArray(value)) {
      if (!mock) {
        if (hasProto) {
          /* eslint-disable no-proto */
          ;(value as any).__proto__ = arrayMethods
          /* eslint-enable no-proto */
        } else {
          for (let i = 0, l = arrayKeys.length; i < l; i++) {
            const key = arrayKeys[i]
            def(value, key, arrayMethods[key])
          }
        }
      }
      if (!shallow) {
        this.observeArray(value)
      }
    } else {
      /**
       * Walk through all properties and convert them into
       * getter/setters. This method should only be called when
       * value type is Object.
       */
+      const keys = Object.keys(value)
+      for (let i = 0; i < keys.length; i++) {
+        const key = keys[i]
+        defineReactive(value, key, NO_INITIAL_VALUE, undefined, shallow, mock)
+      }
    }
  }

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

defineReactive函数

diff 复制代码
export function defineReactive(
  obj: object,
  key: string,
  val?: any,
  customSetter?: Function | null,
  shallow?: boolean,
  mock?: boolean,
  observeEvenIfShallow = false
) {
  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) &&
    (val === NO_INITIAL_VALUE || arguments.length === 2)
  ) {
    val = obj[key]
  }

  let childOb = shallow ? val && val.__ob__ : observe(val, false, mock)
+  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
+    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        if (__DEV__) {
          dep.depend({
            target: obj,
            type: TrackOpTypes.GET,
            key
          })
        } else {
          dep.depend()
        }
        if (childOb) {
          childOb.dep.depend()
          if (isArray(value)) {
            dependArray(value)
          }
        }
      }
      return isRef(value) && !shallow ? value.value : value
    },
+    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val
      if (!hasChanged(value, newVal)) {
        return
      }
      if (__DEV__ && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else if (getter) {
        // #7981: for accessor properties without setter
        return
      } else if (!shallow && isRef(value) && !isRef(newVal)) {
        value.value = newVal
        return
      } else {
        val = newVal
      }
      childOb = shallow ? newVal && newVal.__ob__ : observe(newVal, false, mock)
      if (__DEV__) {
        dep.notify({
          type: TriggerOpTypes.SET,
          target: obj,
          key,
          newValue: newVal,
          oldValue: value
        })
      } else {
+        dep.notify()
      }
    }
  })

  return dep
}

6. 拦截对象的问题与Vue.set和Vue.delete方法

Vue2拦截对象的时候,无法处理新增属性删除属性的情况,这是因为Object.defineProperty这个API本身就无法拦截新增和删除属性的情形

Vue2有额外的API来处理这两个问题,比如Vue.set来设置属性并触发响应,Vue.delete来删除属性并触发响应

如下是Vue2的Vue.set的实现方式,重点关注第六步,对目标对象的key再次执行defineReactive也就是再次执行了Object.defineProperty,使得这个属性也具备了响应式能力

js 复制代码
export function set(target: Array<any> | Object, key: any, val: any): any {
  // 1. 处理数组
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  
  // 2. 对象已经有这个属性,直接赋值
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  
  // 3. 获取对象的 __ob__ 属性(Observer 实例)
  const ob = target.__ob__
  
  // 4. 不能向 Vue 实例或 $data 添加根级响应式属性,_isVue用于判断是不是Vue实例;ob.bmCount判断是否是根对象
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  
  // 5. 如果对象不是响应式对象,直接赋值
  if (!ob) {
    target[key] = val
    return val
  }
  
  // 6. 将新属性转换为响应式
  defineReactive(ob.value, key, val)
  
  // 7. 通知依赖更新
  ob.dep.notify()
  
  return val
}

Vue2的Vue.delete实现原理,重点关注第七点ob.dep.notify(),在删除完毕后会去通知所有的Watcher这个数据更新了。

js 复制代码
export function del(target: Array<any> | Object, key: any) {
  // 1. 处理数组,直接删掉对应索引值
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1)
    return
  }
  
  // 2. 获取对象的 __ob__ 属性,有__obj__属性表示是响应式数据
  const ob = target.__ob__
  
  // 3. 不能删除 Vue 实例或 $data 的根级属性
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid deleting properties on a Vue instance or its root $data ' +
      '- just set it to null.'
    )
    return
  }
  
  // 4. 如果key不是target的自有属性,直接返回
  if (!hasOwn(target, key)) {
    return
  }
  
  // 5. 删除属性
  delete target[key]
  
  // 6. 如果对象不是响应式的,直接返回
  if (!ob) {
    return
  }
  
  // 7. 通知依赖更新
  ob.dep.notify()
}

反观Vue3的Proxy能够直接检测到新增属性和删除属性的操作,然后执行对应的依赖

相关推荐
吃没吃5 分钟前
vue2.6-源码学习-Vue 核心入口文件分析
前端
Carlos_sam5 分钟前
Openlayers:海量图形渲染之图片渲染
前端·javascript
XH2766 分钟前
Android Retrofit用法详解
前端
鸭梨大大大8 分钟前
Spring Web MVC入门
前端·spring·mvc
吃没吃10 分钟前
vue2.6-源码学习-Vue 初始化流程分析 (src/core/instance/init.js)
前端
XH27612 分钟前
Android Room用法详解
前端
木木黄木木1 小时前
css炫酷的3D水波纹文字效果实现详解
前端·css·3d
郁大锤1 小时前
Flask与 FastAPI 对比:哪个更适合你的 Web 开发?
前端·flask·fastapi
HelloRevit2 小时前
React DndKit 实现类似slack 类别、频道拖动调整位置功能
前端·javascript·react.js
ohMyGod_1233 小时前
用React实现一个秒杀倒计时组件
前端·javascript·react.js