Vue源碼之響應式原理

我的博客地址vue

Object的變化偵測

像Vue官網上面說的,vue是經過Object.defineProperty來偵測對象屬性值的變化。數組

function defineReactive (obj, key, val) {
    let dep = new Dep()
    Object.defineProperty(obj, key, {
         enumerable: true,
          configurable: true,
          get () {
            return val
          },
          set (newVal) {
            if (val === newVal) return
                val = newVal
          }
    })
}

函數 defineReactive 是對 Object.defineProperty 的封裝,做用是定義一個響應式的數據。瀏覽器

不過若是隻是這樣是沒有什麼用的,真正有用的是收集依賴。在getter中收集依賴,在setter觸發依賴。緩存

Dep (收集依賴)閉包

// 還有幾個方法沒寫,好比怎麼移除依賴。
class Dep {
    constructor () {
        // 依賴數組
        this.subs = []
      }
    addSub (sub) {
        this.subs.push(sub)
    }
    
    depend (target) {
        if (Dep.target) {
            // 這時的Dep.target是Watcher實例
            Dep.target.addDep(this)
        }
    }
    notity () {
        this.subs.forEach(val => {
            val.update()
        })
    }
    Dep.target = null
}

Watcher (依賴)app

// 原本在Watcher中也要記錄Dep,可是偷懶沒寫了,記錄了Dep後能夠通知收集了Watcher的Dep移除依賴。
class Watcher {
    constructor (vm, expOrFn, cb) {
        // vm: vue實例
        // expOrFn: 字符串或函數
        // cb: callback回調函數
        this.vm = vm
        this.cb = cb
        // 執行this.getter就能夠讀取expOrFn的數據,就會收集依賴
        if (typeof expOrFn === 'function') {
            this.getter = expOrFn
        } else {
            // parsePath是讀取字符串keypath的函數,具體的能夠去瀏覽Vue的源碼
            this.getter = parsePath(expOrFn)
        }
        this.value = this.get()
    }
    get () {
        Dep.target = this
        // 在這裏執行this.getter
        let value = this.getter(this.vm, this.vm)
        Dep.target = null
        return value
    }
    addDep (dep) {
        dep.addSub(this)
    }
    // 更新依賴
    update () {
        const oldValue = this.value
        this.value = this.get()
        this.cb.call(this.vm, this.value, oldValue)
    }
}

接下來再改一下剛開始定義的 defineReactive 函數函數

function defineReactive (obj, key, val) {
    let dep = new Dep() // 閉包
    Object.defineProperty(obj, key, {
          enumerable: true,
          configurable: true,
          get () {
            // 觸發getter時,收集依賴
            dep.addDep()
            return val
          },
          set (newVal) {
            if (val === newVal) return
                val = newVal
                // 觸發setter時,觸發Dep的notify,便利依賴
                 dep.notity()
          }
    })
}

這個時候已經能夠偵測數據的單獨一個屬性,最後再封裝一下:this

class Observer {
    constructor (value) {
        this.value = value
        // 偵測數據的變化和偵測對象的變化是有區別的
        if (!Array.isArray(value)) {
            this.walk(value)
        }
    }
    
    walk (value) {
        const keys = Object.keys(value)
        keys.forEach(key => {
            this.defineReactive(value, key, value[key])
        })
    }
}

最後總結一下:prototype

實例化 Watcher 時經過 get 方法把 Dep.target 賦值爲當前的 Wathcer 實例,並把 Watcher 實例添加在 Dep 中,當設置數據時,觸發 defineReactiveset 運行 Dep.notify() 遍歷 Dep 中收集的依賴 Watcher 實例,而後觸發 Watcher 實例的 update 方法。code

Array的變化偵測

Object 能夠經過 getter/setter 來偵測變化,可是數組是經過方法來變化,好比 push 。這樣就不能和對象同樣,只能經過攔截器來實現偵測變化。

定義一個攔截器來覆蓋 Array.prototype,每當使用數組原型上面的方法操做數組的時候,實際上執行的是攔截器上面的方法,而後再攔截器裏面使用 Array 的原型方法。

const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
methodsToPatch.forEach(function (method) {
  // 緩存原始方法
  const original = arrayProto[method]
  Object.defineProperty(arrayMethods, method, {
      enumerable: false,
      configurable: true,
      writable: true,
      value: function mutator (...args) {
          return original.apply(this, args)
      }
  })

而後就要覆蓋 Array 的原型:

// 看是否支持__proto__, 若是不支持__proto__,則直接把攔截器的方法直接掛載到value上。
const hasProto = "__proto__" in {}
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
class Observer {
    constructor (value) {
        this.value = value
        if (!Array.isArray(value)) {
            this.walk(value)
        } else {
            const augment = hasProto ? protoAugment : copyAugment
            augment(value, arrayMethods, arraykeys)
        }
    }
    
    walk (value) {
        const keys = Object.keys(value)
        keys.forEach(key => {
            this.defineReactive(value, key, value[key])
        })
    }
}

function protoAugment (target, src: Object) {
  target.__proto__ = src
}

function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}

Array 也是在 getter 中收集依賴,不過依賴存的地方有了變化。Vue.js 把依賴存在 Observer 中:

class Observer {
    constructor (value) {
        this.value = value
        this.dep = new Dep // 新增Dep
        if (!Array.isArray(value)) {
            this.walk(value)
        } else {
            const augment = hasProto ? protoAugment : copyAugment
            augment(value, arrayMethods, arraykeys)
        }
    }
    
    walk (value) {
        const keys = Object.keys(value)
        keys.forEach(key => {
            this.defineReactive(value, key, value[key])
        })
    }
}

至於爲何把 Dep 存在 Observer 是由於必須在 getter 和 攔截器中都能訪問到。

function defineReactive (data, key, val) {
    let childOb = observer(val) // 新增
    let dep = new Dep() 
    Object.defineProperty(obj, key, {
          enumerable: true,
          configurable: true,
          get () {
            dep.addDep()
            if (childOb) {
                // 在這裏收集數組依賴
                childOb.dep.depend()
            }
            return val
          },
          set (newVal) {
            if (val === newVal) return
                val = newVal
                 dep.notity()
          }
    })
    
}
// 若是value已是響應式數據,即有了__ob__屬性,則直接返回已經建立的Observer實例
// 若是不是響應式數據,則建立一個Observer實例
function observer (value, asRootData) {
    if (!isObject(value)) {
        return
    }
    let ob
    if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observe) {
        ob = value.__ob__
    } else {
        ob = new Observer(value)
    }
    return ob
}

由於攔截器是對 Array 原型的封裝,因此能夠在攔截器中訪問到this(當前正在被操做的數組),

dep保存在 Observer 實例中,因此須要在this上訪問到 Observer 實例:

function def (obj, key, val, enumerable) {
    Object.defineProperty(obj, key, {
        value: val,
        enumerable: !!enumerable,
        writable: true,
        configerable: true
    })
}
class Observer {
    constructor (value) {
        this.value = value
        this.dep = new Dep
        // 把value上新增一個不可枚舉的屬性__ob__,值爲當前的Observer實例
        // 這樣就能夠經過數組的__ob__屬性拿到Observer實例,而後就能夠拿到Observer的depp
        // __ob__不止是爲了拿到Observer實例,還能夠標記是不是響應式數據
        def(value, '__ob__', this) // 新增
        if (!Array.isArray(value)) {
            this.walk(value)
        } else {
            const augment = hasProto ? protoAugment : copyAugment
            augment(value, arrayMethods, arraykeys)
        }
    }
    
    ...
}

在攔截器中:

methodsToPatch.forEach(function (method) {
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
      const result = original.apply(this, args)
      const ob = this.__ob__ // 新增
      ob.dep.notify() // 新增 向依賴發送信息
      return resullt
  })
})

到這裏還只是偵測了數組的變化,還要偵測數組元素的變化:

class Observer {
    constructor (value) {
        this.value = value
        this.dep = new Dep
        def(value, '__ob__', this) 
        if (!Array.isArray(value)) {
            this.walk(value)
        } else {
            const augment = hasProto ? protoAugment : copyAugment
            augment(value, arrayMethods, arraykeys)
            // 偵測數組中的每一項
            this.observeArray(value) // 新增
        }
    }
    
    observeArray (items) {
        items.forEach(item => {
            observe(item)
        })
    }
    
    ...
}

而後還要偵測數組中的新增元素的變化:

methodsToPatch.forEach(function (method) {
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
      const result = original.apply(this, args)
      const ob = this.__ob__ 
      // 新增開始
      let inserted
      switch (method) {
              case 'push'
              case 'unshift'
                  inserted = args
                  breaak
              case 'splice'
                  inserted = args.slice(2)
                break
      }
      if (inserted) ob.observeArray(inserted)
      // 新增結束
      ob.dep.notify() 
      return resullt
  })
})

總結一下:

Array 追蹤變化的方式和 Object 不同,是經過攔截器去覆蓋數組原型的方法來追蹤變化。

爲了避免污染全局的 Array.prototype ,因此只針對那些須要偵測變化的數組,對於不支持 __proto__的瀏覽器則直接把攔截器佈置到數組自己上。

Observer 中,對每一個偵測了變化的數據都加了 __ob__ 屬性,而且把this(Observer實例) 保存在__ob__ 上,主要有兩個做用:

  • 標記數據是否被偵測了
  • 能夠經過數據拿到__ob__,進一步拿到 Observer 實例。

因此把數組的依賴存放在 Observer 中,當攔截到數組發生變化時,向依賴發送通知。

最後還要經過observeArray偵測數組子元素和數組新增元素的變化。

相關文章
相關標籤/搜索