Vue響應式原理解析(附超詳細源碼註釋和原理解析)

來點雞湯

考59分比考0分更遺憾,最痛苦的不是曾經擁有,而是差一點就能夠。react

前言

上一篇咱們深刻分析了數據驅動視圖渲染(juejin.im/post/5e06b4…)的原理以及源碼解析,感興趣的能夠去瞅一眼,那麼這一次咱們接着套路,講一下數據是如何驅動視圖更新的,也就是Vue的響應式原理,let go!express

案例

<div id="app" @click="changeName">
  {{ name }}
</div>

var app = new Vue({
  el: '#app',
  data: {
    name: 'bo.yang'
  },
  methods: {
    changeName() {
      this.name = 'biaohui'
    }
  }
})
複製代碼

案例解析:當咱們修改this.name的時候,視圖會接着修改成biaohui。設計模式

響應式對象

用過Vue的童鞋都知道Vue實現響應式原理是經過Object.defineProperty()這個API,不熟悉這個API的童鞋自行查看MDN(developer.mozilla.org/zh-CN/docs/…數組

Object.defineProperty(obj, prop, descriptor)
複製代碼

其中核心參數是屬性描述符descriptor,它是一個對象,裏邊最重要的屬性就是
get(給屬性提供getter方法,當訪問該屬性的時候會觸發getter)
set(給屬性提供setter方法,當咱們去修改屬性的時候會觸發setter)瀏覽器

一旦對象有了getter和setter,那麼咱們就會把它叫作響應式對象。緩存

initState

Vue初始化的時候會執行_init()方法,上一篇文章講過,_init()方法會執行initState(),源碼地址:src/core/instance/state.jsbash

/**
 * 初始化狀態
 */
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  /*初始化props*/
  if (opts.props) initProps(vm, opts.props)
  /*初始化方法*/
  if (opts.methods) initMethods(vm, opts.methods)
  /*初始化data*/
  if (opts.data) {
    initData(vm)
  } else {
    /*該組件沒有data的時候綁定一個空對象*/
    observe(vm._data = {}, true /* asRootData */)
  }
  /*初始化computed*/
  if (opts.computed) initComputed(vm, opts.computed)
  /*初始化watchers*/
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
複製代碼

先來分析initProps和initDataapp

initProps

function initProps (vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {}
  const props = vm._props = {}
  /* 循環props,進行響應式綁定 */
  for (const key in propsOptions) {
    defineReactive(props, key, value)
    if (!(key in vm)) {
      /* 把vm._props.key全都代理到vm.key上 */
      proxy(vm, `_props`, key)
    }
  }
}
複製代碼

initData

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  //遍歷data中的數據
  while (i--) {
    const key = keys[i]
    /* 把vm._data.key全都代理到vm.key上 */
    proxy(vm, `_data`, key)
  }
  /*從這裏開始咱們要observe了,開始對數據進行綁定,這裏有尤大大的註釋asRootData,這步做爲根數據,下面會進行遞歸observe進行對深層對象的綁定。*/
  observe(data, true /* asRootData */)
}
複製代碼

咱們能夠看出,initProps和initData都是對props和data綁定到this上,對props進行響應式綁定,並監聽data。接下來看一下proxy和observe異步

proxy

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

export function proxy (target: Object, sourceKey: string, key: string) {
  /* proxy(vm, `_data`, key),能夠看出get直接輸出vm._data.key的值 */
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  /* 把val(新值)附給vm._data.key */
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  /* 最後用sharedPropertyDefinition去描述vm的key屬性 */
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
複製代碼

observe

/**
 * 監聽數據變化
 * @param {object} value    data||props等
 */
export function observe (value: any, asRootData: ?boolean): Observer | void {
  let ob: Observer | void
  /* 若是data自己存在觀察屬性,直接返回 */
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    // ...
  ) {
    /* 不然從新實例化一個Observer */
    ob = new Observer(value)
  }
  return ob
}
複製代碼

observe就是經過Observer這個類給data或props綁定監聽async

Observer

Observer 是一個類,它的做用是經過defineReactive給對象的屬性添加 getter 和 setter,用於依賴收集和派發更新

/**
 * 給對象的屬性添加getter和setter,用於依賴收集和派發更新
 */
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; 

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    /* 把自身實例(Observer)添加到value的__ob__屬性上 */
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      /*
          若是是數組,將修改後能夠截獲響應的數組方法替換掉該數組的原型中的原生方法,達到監聽數組數據變化響應的效果。
          這裏若是當前瀏覽器支持__proto__屬性,則直接覆蓋當前數組對象原型上的原生數組方法,若是不支持該屬性,則直接覆蓋數組對象的原型。
      */
      if (hasProto) {
        /* 直接覆蓋原型的方法來修改目標對象 */
        protoAugment(value, arrayMethods)
      } else {
        /*定義(覆蓋)目標對象或數組的某一個方法*/
        copyAugment(value, arrayMethods, arrayKeys)
      }

      /*若是是數組則須要遍歷數組的每個成員進行observe*/
      this.observeArray(value)
    } else {
      /*若是是對象則直接walk進行綁定*/
      this.walk(value)
    }
  }

  /* 給對象的每一個屬性添加響應式,注意這裏只循環綁定了對象的第一層屬性 */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
  /* 給數組的每一個成員添加監聽 */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}
複製代碼

defineReactive

defineReactive 的功能就是定義一個響應式對象,給對象動態添加 getter 和 setter
源碼: src/core/observer/index.js

/* 定義一個響應式對象,給對象添加setter和getter */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()
  const property = Object.getOwnPropertyDescriptor(obj, key)

  const getter = property && property.get
  const setter = property && property.set

  /* 遞歸調用observe,使得obj對象不管層次多麼深,都會將全部屬性變成響應式的 */
  let childOb = !shallow && observe(val) // 獲取具備__ob__屬性的數據
  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
      // 新老值相等,就再也不執行setter
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      /* 給新值添加__ob__屬性 */
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}
複製代碼

以上涉及到了Vue響應式原理的兩個概念,分別是依賴收集和派發更新,接下來咱們詳細說一下:

依賴收集(Dep)

咱們發現defineReactive首先實例化了一個類Dep,它是依賴收集的核心,它也是對Watcher的一種管理,脫離Watcher,Dep將沒啥意義,先看一下Dep定義:

Dep

源碼:src/core/observer/dep.js

export default class Dep {
  static target: ?Watcher; // 靜態屬性target(Watcher類型)
  id: number;
  subs: Array<Watcher>; // 一個(Watcher類型)數組
  
  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub: Watcher) {
    /* 爲後續數據變化時候能通知到哪些 subs 作準備 */
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    /* 全部Watcher的實例對象 */
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
複製代碼

分析:上篇在Vue mount的過程當中有一段邏輯

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
/* 實例化Watcher,觸發它的get方法 */
new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)
複製代碼

再看看Watcher是啥玩意兒,只看一些關鍵代碼
源碼:src/core/observer/watcher.js

Watcher

export default class Watcher {
  vm: Component;
  deep: boolean;
  sync: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    vm._watchers.push(this)
    /* 監聽器的options */
    if (options) {
      this.deep = !!options.deep // 深度監聽
      // ...
      this.sync = !!options.sync // 在當前 Tick 中同步執行 watcher 的回調函數,不然響應式數據發生變化以後,watcher回調會在nextTick後執行;
    }
    /* Watcher實例持有Dep的實例的數組 */
    this.deps = [] // 老的Dep集合
    this.newDeps = [] // 觸發更新生成的新的Dep集合
    this.depIds = new Set()
    this.newDepIds = new Set()

  get () {
    /* 收集Watcher實例,也就是Dep.target */
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      /* this.getter對應就是咱們上篇講到的vm._update(vm._render(), hydrating),_update會生成VNode,在這個過程當中會訪
      問vm上的data,這時候就觸發了數據對象的getter,defineReactive中能夠發現每一個getter都持有一個dep,
      所以在觸發getter的時候會觸發Dep的depend方法,也就觸發了Watcher的addDep方法 */
      value = this.getter.call(vm, vm)
    } finally {
      /* 把 Dep.target 恢復成上一個狀態,由於當前 vm 的數據依賴收集已經完成,那麼對應的渲染Dep.target 也須要改變 */
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

  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)) {
        /* 把當前的 watcher 訂閱到這個數據持有的 dep 的 subs, 目的是爲後續數據變化時候能通知到哪些 subs 作準備 */
        dep.addSub(this)
      }
    }
  }
}
複製代碼

分析: 收集依賴的目的是爲了當這些響應式數據發生變化,觸發它們的 setter 的時候,能知道應該通知哪些訂閱者去作相應的邏輯處理,咱們把這個過程叫派發更新,其實 Watcher 和 Dep 就是一個很是經典的觀察者設計模式的實現

派發更新(setter)

defineReactive方法裏的setter就是派發更新的,它的核心是dep.notify(),來看一下:
源碼:src/core/observer/dep.js

notify () {
    /* 全部Watcher的實例對象 */
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
複製代碼

遍歷全部watcher實例,並執行Watcher的update方法:
源碼:src/core/observer/watcher.js

update () {
    // ...
    queueWatcher(this)
  }
複製代碼

看一下queueWatcher定義:
源碼: src/core/observer/scheduler.js

/* 異步執行Dom更新的時候所執行的方法 */
/*將一個觀察者對象push進觀察者隊列,在隊列中已經存在相同的id則該觀察者對象將被跳過,除非它是在隊列被刷新時推送*/
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  /* 保證同一個 Watcher 只添加一次 */
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    if (!waiting) {
      waiting = true
      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      /* 異步執行flushSchedulerQueue */
      nextTick(flushSchedulerQueue)
    }
  }
}
複製代碼

解析: 這是 Vue 在作派發更新的時候的一個優化的點,它並不會每次數據改變都觸發 watcher 的回調,而是把這些 watcher 先添加到一個隊列裏,而後在 nextTick 後執行 flushSchedulerQueue,再來看一下flushSchedulerQueue定義:

/*
nextTick的回調函數,在下一個tick時flush掉兩個隊列同時運行watchers
主要目的是執行Watcher的run函數,用來更新視圖
*/
function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id
  /*
    給queue排序,這樣作能夠保證:
    1.組件更新的順序是從父組件到子組件的順序,由於父組件老是比子組件先建立。
    2.一個組件的用戶的自定義watcher(user watchers)比Vue內部的渲染watcher(render watcher)先運行,由於user watchers每每比render watcher更早建立
    3.若是一個組件在父組件watcher運行期間被銷燬,它的watcher執行將被跳過。
  */
  queue.sort((a, b) => a.id - b.id)
  
  /*這裏不緩存queue.length是由於在執行watcher.run()的時候,可能用戶會再次添加新的 watcher,
  更多的watcher對象可能會被push進queue*/
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    id = watcher.id
    /*將has的標記刪除*/
    has[id] = null
    /*執行watcher的run方法*/
    watcher.run()
    /*
      在測試環境中,檢測watch是否在死循環中
      好比這樣一種狀況
      watch: {
        test () {
          this.test++;
        }
      }
      持續執行了一百次watch表明可能存在死循環
    */
    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
      }
    }
  }

  /*獲得隊列的拷貝*/
  const updatedQueue = queue.slice()
  /*重置調度者的狀態*/
  resetSchedulerState()

  /*使子組件狀態都改編成active同時調用activated鉤子*/
  callActivatedHooks(activatedQueue)
  /*調用updated鉤子*/
  callUpdatedHooks(updatedQueue)
}
複製代碼

接下來看一下watcher.run()的定義:
源碼:src/core/observer/watcher.js

run () {
      // 新value值
      const value = this.get()
      // 老value值
      const oldValue = this.value

      // set new value
      this.value = value
      /* 觸發更新 */
      this.cb.call(this.vm, value, oldValue)
  }
複製代碼

解析:執行了Watcher的回調cb,並有新值value和舊值oldValue,這就是咱們watch的時候能拿到的那倆值的緣由啦。

總結

在組件初始化渲染的時候咱們去把它的data和props進行響應式綁定,並將全部視圖中用到的數據進行依賴收集,將這些數據添加訂閱屬性( __ob__),以便在更新的時候知道須要更新那些數據,當咱們去修改相應data的時候,會執行setter方法進行派發更新,緊接着觸發了全部觀察者(Watcher)的update方法,而後執行Watcher的run方法,在run方法裏,去執行每一個Watcher的回調觸發視圖更新。

下期預告

Vue的nextTick實現原理(juejin.im/post/5e1ae6…)

相關文章
相關標籤/搜索