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能够直接检测到新增属性和删除属性的操作,然后执行对应的依赖

相关推荐
拉不动的猪23 分钟前
前端常见数组分析
前端·javascript·面试
小吕学编程40 分钟前
ES练习册
java·前端·elasticsearch
Asthenia04121 小时前
Netty编解码器详解与实战
前端
袁煦丞1 小时前
每天省2小时!这个网盘神器让我告别云存储混乱(附内网穿透神操作)
前端·程序员·远程工作
一个专注写代码的程序媛2 小时前
vue组件间通信
前端·javascript·vue.js
一笑code2 小时前
美团社招一面
前端·javascript·vue.js
懒懒是个程序员2 小时前
layui时间范围
前端·javascript·layui
NoneCoder3 小时前
HTML响应式网页设计与跨平台适配
前端·html
凯哥19703 小时前
在 Uni-app 做的后台中使用 Howler.js 实现强大的音频播放功能
前端
烛阴3 小时前
面试必考!一招教你区分JavaScript静态函数和普通函数,快收藏!
前端·javascript