vue原理探索--響應式系統

Vue.js 是一款 MVVM 框架,數據模型僅僅是普通的 JavaScript 對象,可是對這些對象進行操做時,卻能影響對應視圖,它的核心實現就是「響應式系統」。vue

首先看一下 Object.defineProperty,Vue.js就是基於它實現「響應式系統」的。react

主要涉及屬性:express

  • enumerable,屬性是否可枚舉,默認 false。
  • configurable,屬性是否能夠被修改或者刪除,默認 false。
  • get,獲取屬性的方法。
  • set,設置屬性的方法。

響應式基本原理就是,在 Vue 的構造函數中,對 options 的 data 進行處理。即在初始化vue實例的時候,對data、props等對象的每個屬性都經過Object.defineProperty定義一次,在數據被set的時候,作一些操做,改變相應的視圖。數組

class Vue {
    /* Vue構造類 */
    constructor(options) {
        this._data = options.data;
        observer(this._data);
    }
}
function observer (value) {
    if (!value || (typeof value !== 'object')) {
        return;
    }
    
    Object.keys(value).forEach((key) => {
        defineReactive(value, key, value[key]);
    });
}
function defineReactive (obj, key, val) {
    Object.defineProperty(obj, key, {
        enumerable: true,       /* 屬性可枚舉 */
        configurable: true,     /* 屬性可被修改或刪除 */
        get: function reactiveGetter () {
            return val;         
        },
        set: function reactiveSetter (newVal) {
            if (newVal === val) return;
            cb(newVal);
        }
    });
}

實際應用中,各類系統複雜無比。假設咱們如今有一個全局的對象,咱們可能會在多個 Vue 對象中用到它進行展現。又或者寫在data中的數據並無應用到視圖中呢,這個時候去更新視圖就是多餘的了。這就須要依賴收集的過程。閉包

依賴收集app

 所謂依賴收集,就是把一個數據用到的地方收集起來,在這個數據發生改變的時候,統一去通知各個地方作對應的操做。「訂閱者」在VUE中基本模式以下:框架

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.addDep(this)
    }
  }

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

有了訂閱者,再來看看Watcher的實現。源碼Watcher比較多邏輯,簡化後的模型以下異步

class Watcher{
    constructor(vm,expOrFn,cb,options){
        //傳進來的對象 例如Vue
        this.vm = vm
        //在Vue中cb是更新視圖的核心,調用diff並更新視圖的過程
        this.cb = cb
        //收集Deps,用於移除監聽
        this.newDeps = []
        this.getter = expOrFn
        //設置Dep.target的值,依賴收集時的watcher對象
        this.value =this.get()
    }

    get(){
        //設置Dep.target值,用以依賴收集
        pushTarget(this)
        const vm = this.vm
        let value = this.getter.call(vm, vm)
        return value
    }

    //添加依賴
      addDep (dep) {
          // 這裏簡單處理,在Vue中作了重複篩選,即依賴只收集一次,不重複收集依賴
        this.newDeps.push(dep)
        dep.addSub(this)
      }

      //更新
      update () {
        this.run()
    }

    //更新視圖
    run(){
        //這裏只作簡單的console.log 處理,在Vue中會調用diff過程從而更新視圖
        console.log(`這裏會去執行Vue的diff相關方法,進而更新數據`)
    }
}

defineReactive:函數

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  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) && arguments.length === 2) {
    val = obj[key]
  }

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

因此響應式原理就是,咱們經過遞歸遍歷,把vue實例中data裏面定義的數據,用defineReactive(Object.defineProperty)從新定義。每一個數據內新建一個Dep實例,閉包中包含了這個 Dep 類的實例,用來收集 Watcher 對象。在對象被「讀」的時候,會觸發 reactiveGetter 函數把當前的 Watcher 對象(存放在 Dep.target 中)收集到 Dep 類中去。以後若是當該對象被「寫」的時候,則會觸發 reactiveSetter 方法,通知 Dep 類調用 notify 來觸發全部 Watcher 對象的 update 方法更新對應視圖。oop

在vue中,共有4種狀況會產生Watcher:

一、Vue實例對象上的watcher,觀測根數據,發生變化時從新渲染組件
updateComponent = () => {   vm._update(vm._render(), hydrating) }
vm._watcher = new Watcher(vm, updateComponent, noop)
二、用戶在vue對象內用watch屬性建立的watcher
三、用戶在vue對象內建立的計算屬性,本質上也是watcher
四、用戶使用vm.$watch建立的watcher

Wathcer會增減,也可能在render的時候新增。因此,必須有一個Schedule來進行Watcher的調度。部分主要代碼以下:

 queue.sort((a, b) => a.id - b.id)

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    // in dev build, check and stop circular updates.
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop ' + (
            watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`
          ),
          watcher.vm
        )
        break
      }
    }
  }

調度的做用:

一、去重,每一個Watcher有一個惟一的id。首先,若是id已經在隊列裏了,跳過,不必重複執行,若是id不在隊列裏,要看隊列是否正在執行中。若是不在執行中,則在下一個時間片執行隊列,所以隊列永遠是異步執行的。

二、排序,按解析渲染的前後順序執行,即Watcher小的先執行。Watcher裏面的id是自增的,先創鍵的比後建立的id小。因此會有以下規律:

  2.一、組件是容許嵌套的,並且解析必然是先解析了父組件再到子組件。因此父組件的id比子組件小。

  2.二、用戶建立的Watcher會比render時候建立的先解析。因此用戶建立的Watcher的id比render時候建立的小。

三、刪除Watcher,若是一個組件的Watcher在隊列中,而他的父組件被刪除了,這個時候也要刪掉這個Watcher。

四、隊列執行過程當中,存一個對象circular,裏面有每一個watcher的執行次數,若是哪一個watcher執行超過MAX_UPDATE_COUNT定義的次數就認爲是死循環,再也不執行,默認是100次。

總之,調用的做用就是管理Watcher。

補充:

 VUE中是如何用Object.defineProperty給數組對象從新定義的呢,爲何咱們直接修改數據中某項(arr[3] = 4)的時候,視圖並無響應式地變化呢。

答案是數組的響應式是不夠徹底的,VUE只重寫了有限的方法。重寫邏輯以下:

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

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  // cache original 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
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})
相關文章
相關標籤/搜索