透過vue源碼分析響應式原理、

用vue已經一段時間了,對於輪子雖然會用可是也要知道輪子的構造,知其然並知其因此然。今天咱們對照着vue的源碼一步一步進行分析,對vue的實現原理有一個初步的瞭解。
ps:第一次寫文章各位請多包含,若是有錯誤請指出謝謝vue

瞭解模式和思路

vue的數據驅動是在數據劫持+訂閱者-發佈者模式下創建的,主要的依賴Observer Dep Watcher Compilernode

簡單瞭解下這幾個東西的做用

Observer:劫持全部數據添加getter,setter;
Dep:發佈者,收集全部的依賴,當數據變化時通知依賴;
Watcher:訂閱者,同時鏈接數據變化時候調用的更新函數;
Compiler:模板解析,轉換成render生成vnode,對比新老vnode進行更新頁面;react

瞭解了大概,接下來咱們根據源碼分別分析這幾項內部是如何實現的同時他們的關係是如何進行關聯的;git

Observer

上面說道Observer是數據劫持和添加getter,setter,這裏咱們講解Observer是如何進行數據劫持,添加的getter和setter的做用是什麼。 當咱們進行new Vue()以後,這段代碼初始化Data (源碼連接)github

export function initState (vm: Component) {
 const opts = vm.$options
  省略無用的代碼----
  opts.data取得就是data return的對象,若是存在執行initData(vm)
  if (opts.data) {
    initData(vm)---->看這裏調用了initData()
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
    省略無用的代碼----
}
複製代碼

initData執行了observe對數據加工,添加getter和setter。express

function initData (vm: Component) {
  let data = vm.$options.data
   省略無用的代碼----
  // observe data
  observe(data, true /* asRootData */)---->這裏執行observe,接下來咱們去observe去看一下作了什麼
}
複製代碼

observer源碼連接編程

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data
  constructor (value: any) {
    /Observer裏對傳過來的數據進行判斷,數組和對象的處理方法不是同樣的/
    if (Array.isArray(value)) {
      刪除無用的代碼----
      this.observeArray(value)
    } else {
    //這裏是針對對象進行處理的執行了walk
      this.walk(value)
    }
   }
   walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
            walk遍歷了data的keys對每一個對象的key執行了下面的方法
            若是我再頁面聲明的是
            data:{
                name:"張三",
                age:"23"
            }
            那麼obj就是 data,
            keys[i]就是每一個key(name或者age等)
      defineReactive(obj, keys[i])
    }
  }
  }
複製代碼

這裏咱們總結一下上面的代碼邏輯,當初始化vue對象的時候vue會對data進行處理,循環data的key執行了walk中defineReactive方法傳了兩個參數,數據data,和每一個key。數組

重點是defineReactive,進入在下面代碼裏咱們能夠看到vue就是利用Object.defineProperty() 對數據進行劫持對每一個key添加了get和set方法,同時咱們發現代碼中有new Dep()這裏面針對對象中的每一個key都添加了dep,咱們以前說過dep是收集訂閱者的,當有數據更新的時候通知更新。(關於代碼中dep咱們下面講,先了解)bash

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
//這裏針對data中每個key都new了一個發佈者,收集全部訂閱者。
  const dep = new Dep()
  const getter = property && property.get
  const setter = property && property.set
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      //關於dep咱們下面講,先了解概念:Dep.target目標訂閱者;dep.depend()收集訂閱者
      //當咱們調用this.name的時候就會調用get方法當Dep.target存在的時候調用了dep.depend()
      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
      //這裏對值進行比較,若是修改的是相同的值就return不進行下面的執行
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
      //更新value
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      //dep.notify()通知的方法
      //當進行數據更新的時候進行了dep.notify()通知更新
      dep.notify()
    }
  })
}
複製代碼

小結

通過上面的分析咱們瞭解到vue進行數據劫持,經過Object.defineProperty() 添加get,set方法監控數據的調用和更新,當數據調用的時候觸發get方法同時調用dep.depend()收集依賴,當數據更新的時候觸發set方法,而後進行數據對比是不是相同值若是相同則return,若是是更新則更新val同時調用dep.notify()通知訂閱者更新。閉包

咱們這裏瞭解了observe是如何進行數據劫持和如何添加getter和setter的而且知道getter和setter的做用 到這裏有幾個疑問new Dep(),Dep.target,dep.depend(),dep.notify()這究竟是什麼東西怎麼運行的?

Dep

剛剛咱們留了幾個問題
Dep.target是目標訂閱者;
dep.depend()收集訂閱者的;
dep.notify()通知更新的;

接下來咱們看源碼解釋一下他是如何肯定訂閱者的,如何收集的,如何通知的。 Dep源碼連接
如下刪除了無用代碼以後的代碼,看下代碼註釋;

import type Watcher from './watcher'

//這裏有個uid,new的每一個Dep()的時候公用一個uid
let uid = 0
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;
//這裏進行了uid++因此保證了每一個data new Dep的uid都是不一樣的
  constructor () {
    this.id = uid++
    this.subs = []
  }
  //這裏出現了Watcher對象,訂閱者
addSub (sub: Watcher) {
    this.subs.push(sub)
  }
//這個就是上面調用dep.depend()收集的方法,添加了起來
  depend () {
    if (Dep.target) {
    //這裏調用的addDep()把這個依賴收集了起來
      Dep.target.addDep(this)
    }
  }
//dep.notify()這裏是通知的方法,通知了全部訂閱者調用了update()方法;
//這裏咱們先記着notify()通知了全部的訂閱者(Watcher)調用了Watcher的update();
  notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

//這就是目標訂閱者,默認是沒有訂閱者null;
//Dep.target同時只能存在一個保證惟一性
Dep.target = null

//這裏暴露了一個方法用來添加目標訂閱者;
export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}
複製代碼

小結

看完了Dep咱們知道這個源碼有兩個做用一是收集訂閱者,二是通知訂閱者進行更新,是數據和更新方法的一個紐帶;這裏再回憶一下,數據更新的時候調用了數據的get方法同時調用dep.depend()收集依賴;數據更新的時候調用set方法而且調用 dep.notify()通知更新,貼上代碼以下:

get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      //當咱們調用this.name的時候就會調用get方法當Dep.target存在的時候調用了dep.depend()
      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
      //這裏對值進行比較,若是修改的是相同的值就return不進行下面的執行
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
      //更新value
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      //當進行數據更新的時候進行了dep.notify()通知更新
      dep.notify()
    }
複製代碼

Watcher

說完了Dep發佈者,他是收集訂閱者,通知訂閱者更新的,有幾個疑問什麼那是何時纔有訂閱者的,訂閱者究竟是誰。剛剛說Dep.target是目標訂閱者那麼他是何時添加的呢,接下來咱們分析這幾個問題,訂閱者是誰,何時添加的。Dep.target是何時存在的; 咱們翻開vue Watcher的源碼

//咱們分析一下源碼new Watcher()構造函數裏接收了幾個參數
export default class Watcher {
 constructor (
    vm: Component, //vue對象
    expOrFn: string | Function,//傳入進來的key值或者函數
    cb: Function,//回調函數(多是更新函數,或者是其餘回調)
    options?: ?Object,//一些配置computed的參數這裏咱們先不考慮
    isRenderWatcher?: boolean//不考慮
  ) {
    this.vm = vm
    
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
    //這裏默認參數都是false
      this.deep = this.user = this.lazy = this.sync = false
    }
    //把傳入的回調給了this.cb;
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    //這裏對 傳入的 參數判斷是否是函數不是函數執行parsePaht();
    //parsePaht這裏也是返回了一個新函數
    源碼地址:(https://github.com/vuejs/vue/blob/dev/src/core/util/lang.js)
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
    //注意這裏當new Watcher(傳入相應的參數)執行這,this.lazy是false,調用this.get();看下面的get方法
    this.value = this.lazy
      ? undefined
      : this.get()
  }
  //執行get()
  get () {
     //還記的dep中暴露的pushTarget方法嗎
    pushTarget(this)
   -------------------------dep中的代碼-------------------------------
   //這裏pushTarget添加了目標訂閱者
   //此時Dep.target不是null了
            export function pushTarget (target: ?Watcher) {
              targetStack.push(target)
              Dep.target = target
        }
        繼續往下看
-----------------------------------------------------------------
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)//這裏用了.call傳入了vm vue對象
    這個getter就是new Watcher傳進來的函數或者是parsePath(key)值包裝後返回的函數
    一般調用的是parsePath(key)返回的函數
    --------------------這裏貼上parsePath(key)-----------------------------
    源碼連接:https://github.com/vuejs/vue/blob/dev/src/core/util/lang.js
            export function parsePath (path: string): any {
            這個path就是傳入進來的data中數據key值如‘name,age’或者是其餘的多個值key
          if (bailRE.test(path)) {
            return
          }
          //進行拆分紅數組
          const segments = path.split('.')
          //能夠看到這裏用了個閉包這個obj就是上面的vm
          return function (obj) {
            for (let i = 0; i < segments.length; i++) {
              if (!obj) return
              //這裏至關於調用this.name那麼就調用了get方法
              //到這Dep.target有了目標訂閱者,也調用了get方法get方法中就調用了dep.depend()收集了訂閱者
              obj = obj[segments[i]]
            }
            //返回了這個值
            return obj
          }
        }
    -----------------------------------------------------------------------
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    }
    //返回了value
    return value
  }
 }
}
複製代碼

上面watcher主動調用getter就是調用了數據的get.

get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      //此時Dep.target不是null
      if (Dep.target) {
      //dep收集了依賴調用了Dep.target.addDep(this)
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
複製代碼

小結

通過了這麼一大長串的代碼分析咱們瞭解到了當new Watcher()的時候會調用數據自己的get同時添加了new出來的訂閱者。那麼訂閱者是誰呢何時才能new Watcher()?最顯而易見的就是template模板當數據變化的時候模板會更新,那麼這個模板就是訂閱者。咱們能夠看一下源碼頁面初始化的時候進行初始化Watcher。

Compiler模板解析,頁面掛載,出現訂閱者

頁面掛載源碼

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
 //刪除一些無關的------
 //beforeMount的生命週期的回調
  callHook(vm, 'beforeMount')
  
//這裏聲明瞭一個更新組件的變量;
  let updateComponent
  //判斷當前開發和生產環境給updateComponent賦值成一個函數
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)

      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    //這裏updateComponent返回了vm._update()裏面傳入了render方法,其實就是頁面的渲染方法;
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }
//訂閱者在這生成了,傳入了參數
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  ----------------Watcher中的代碼片斷-------
   constructor (
    vm: Component, //vue對象
    expOrFn: string | Function, //updateComponent
    cb: Function,//回調函數 noop
    options?: ?Object,  //before對象
    isRenderWatcher?: boolean //ture
  ) {
     //把noop賦值給了cb
      this.cb = cb;
   //傳進來的expOrFn是函數因此執行了這段
    if (typeof expOrFn === 'function') {
      這個getter就是updateComponent 
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
    //這裏執行了get把返回值賦值給了this.value
    this.value = this.lazy
      ? undefined
      : this.get()
      
  }
  get () {
   //添加了訂閱者
    pushTarget(this)
    let value
    const vm = this.vm
    try {
    value = this.getter.call(vm, vm)
    //執行了getter,就是執行了
      <!--updateComponent = () => {
         vm._update(vm._render(), hydrating)
       }-->
      //那麼這個value就是vm._update(vm._render(), hydrating)渲染函數;
      //render函數中會解析模板中{{name}}語法,就是調用了data中的數據,每一個對象new了個dep,收集了這個依賴
      //那麼當name數據更新的時候就會觸發更新方法dep通知剛剛添加的訂閱者更新;
      
    } 
    return value
  }
  --------------------------------------------
  return vm
}
複製代碼

小結

通過以上的分析咱們知道了在頁面掛載的時候會模板解析同時new了一個Watcher(訂閱者),當模板中用到data的數據時每一個數據生成了本身的dep,dep收集訂閱者(能夠理解成模板),當data的數據發生變化的時候,這個數據的dep就會通知訂閱者更新,調用更新函數從新渲染頁面。

頁面更新

到這裏咱們知道了頁面初始化的時候就是一個訂閱者,這個訂閱者用到了data中的數據,data中的數據收集了他,當數據變化的時候通知訂閱者更新,那麼是如何更新的,咱們回頭看一下Object.defineProperty 這裏的set就是當數據更新的時候調用的

Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      //這裏對值進行比較,若是修改的是相同的值就return不進行下面的執行
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
      //更新value
        val = newVal
      }
      //數據更新的時候就會調用dep.notify()通知訂閱者更新
      dep.notify()
    }
  })
  
  -------------dep的notify()--------------------------
  notify () {
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      subs.sort((a, b) => a.id - b.id)
    }
    //這裏調用訂閱者的update()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
-------------------------------------------------
-----------------watcher的update()---------------------
 update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
    //那麼這裏調用了這個隊列方法進行更新頁面
      queueWatcher(this)
    }
  }
--------------------------------------------------------

export function queueWatcher (watcher: Watcher) {
 //這裏取了訂閱者的id判斷這個id是否存在若是不存在則加入隊列中;
 //若是存在則替換掉前一個訂閱者,這就是當咱們在代碼中對一個數據進行屢次修改的時候vue只取最後一次的更新;
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true
      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      //把更新方法放到下一個事件循環中執行;
      nextTick(flushSchedulerQueue)
         }
       }
     }
複製代碼

總結

到這裏vue數據響應式的原理就結束了,咱們最後總結一下:
vue進行數據劫持經過Object.defineProperty 給每一個數據添加get,set方法而且添加了dep(發佈者),當數據被使用的時候觸發get-->dep.depend()添加訂閱者,當數據改變的時候觸發set-->dep.notify()通知訂閱者更新。 當咱們重複對一個數據進行修改的時候如

this.age=12, this.age=13, this.age=14 queueWatcher()會生成更新隊列一個數據的屢次修改會先到隊列中檢查是否存在,若是存在則會替換掉以前的watcher,若是沒有則添加到隊列中。因此對一個值的屢次修改只取最後一次進行更新。

相關文章
相關標籤/搜索