vue-原始碼剖析-雙向繫結

寫不好程式碼的格子襯衫發表於2019-03-01

專案中vue比較多,大概知道實現,最近翻了一下雙向繫結的程式碼,這裡寫一下閱讀後的理解。

專案目錄

拉到vue的程式碼之後,首先來看一下專案目錄,因為本文講的是雙向繫結,所以這裡主要看雙向繫結這塊的程式碼。

vue-原始碼剖析-雙向繫結

入口

從入口開始:src/core/index.js

index.js 比較簡單,第一句就引用了Vue進來,看下Vue是啥

import Vue from `./instance/index`
複製程式碼

Vue建構函式

來到 src/core/instance/index.js

一進來,嗯,沒錯,定義了Vue 建構函式,然後呼叫了好幾個方法,這裡我們看第一個initMixin

import { initMixin } from `./init`

function Vue (options) {
  if (process.env.NODE_ENV !== `production` &&
    !(this instanceof Vue)
  ) {
    warn(`Vue is a constructor and should be called with the `new` keyword`)
  }
  this._init(options)
}
initMixin(Vue)
...
export default Vue
複製程式碼

初始化

來到 src/core/instance/init.js

這個給Vue建構函式定義了_init方法,每次new Vue初始化例項時都會呼叫該方法。

然後看到_init中間的程式碼,呼叫了好多初始化的函式,這裡我們只關注data的走向,所以這裡看一下initState

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    ...
    initState(vm)
    ...
  }
}
複製程式碼

來到 src/core/instance/state.js
initState呼叫了initDatainitData呼叫了observe,然後我們再往下找observe

import { observe } from `../observer/index`
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  ...
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  ...
}

function initData (vm: Component) {
  let data = vm.$options.data
  ...
  observe(data, true /* asRootData */)
}
複製程式碼

Observer(觀察者)

來到 src/core/observer/index.js
這裡,例項化Observer物件
首先,new Observer例項化一個物件,引數為data

export function observe (value: any, asRootData: ?boolean): Observer | void {
  let ob: Observer | void
  ob = new Observer(value)
  return ob
}
複製程式碼

然後我們來看下Observer建構函式裡面寫了什麼,這裡給每個物件加了value和例項化了一個Dep,然後data為陣列的話則遞迴,否則執行walk
walk這裡是對物件遍歷執行defineReactive

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that has this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    if (Array.isArray(value)) {
      ...
      this.observeArray(value)
    } else {
      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做了什麼,嗯,這裡就是Observer的核心。
Object.defineProperty對物件進行配置,重寫get&set

get:對原來get執行,然後執行dep.depend新增一個訂閱者
set:對原來set執行,然後執行dep.notify通知訂閱者
Dep是幹啥的呢?Dep其實是一個訂閱者的管理中心,管理著所有的訂閱者

import Dep from `./dep`
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

  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()
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      dep.notify()
    }
  })
}
複製程式碼

Dep(訂閱者管理中心)

那麼,到這裡了,疑問的是什麼時候會觸發Observerget方法來新增一個訂閱者呢?
這裡的條件是有Dep.target的時候,那麼我們找一下程式碼中哪裡會對Dep.target賦值,找到了Dep定義的地方

來到 src/core/observer/dep.js
pushTarget就對Dep.target賦值了,然後來看一下到底是哪裡呼叫了pushTarget

export default class Dep {
  ...
}
Dep.target = null
const targetStack = []

export function pushTarget (_target: ?Watcher) {
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}
複製程式碼

然後,找到了Watcher呼叫了pushTarget,那麼我們來看一下Watcher的實現
來到 src/core/observer/watcher.js
這裡可以看到每次new Watcher時,就會呼叫get方法
這裡執行兩步操作
第一:呼叫pushTarget
第二:呼叫getter方法觸發Observerget方法將自己加入訂閱者

export default class Watcher {
  vm: Component;
  constructor (
    vm: Component
  ) {
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    }
    return value
  }
}
複製程式碼

接著,Dep.target有了之後,接下來就要看一下dep.depend()這個方法,所以還是要到Dep來看下這裡的實現。
來到 src/core/observer/dep.js
這裡呼叫了Dep.target.addDep的方法,引數是Dep的例項物件,那麼我們看下addDep

export default class Dep {
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
}
複製程式碼

Watcher(訂閱者)

又來到 src/core/observer/watcher.js
到這,addDep其實又呼叫時Dep例項的addSub方法,引數也是把Watcher例項傳遞過去
然後,我們看上面的addSub,這裡就是把Watcher例項pushdepsubs陣列中儲存起來
到這裡,就完成了把Watcher加入到Dep這裡訂閱器管理中心這裡,後面的管理就由Dep來統一管理

export default class Watcher {
  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)
      }
    }
  }
}
複製程式碼

走完了新增訂閱器,接著再來看下Observerset方法,這裡呼叫了dep.notify,我們來看一下這個方法

來到 src/core/observer/dep.js
這裡,就是對subs中的所有Watcher,呼叫其update方法來更新資料

export default class Dep {
 notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
複製程式碼

這裡,我們就來看看Watcher是怎麼更新的
又來到 src/core/observer/watcher.js
update呼叫的是run方法,run方法這裡先用get拿到新的值,然後把新&舊值做為引數給cb呼叫

export default class Watcher {
  update () {
    this.run()
  }
  
  run () {
    if (this.active) {
      const value = this.get()
      const oldValue = this.value
      this.value = value
      this.cb.call(this.vm, value, oldValue)
    }
  }
}
複製程式碼

這裡的cb其實是例項化的時候傳進來的,這裡我們看一下什麼時候會例項化Watcher
回到一開始的initState:src/core/instance/state.js
initState的最後還呼叫了initWatch,然後再createWatcher,最後$watch的時候就例項化了Watcher物件,這裡就把cb傳到了Watcher例項中,當監聽的資料改變的時候就會觸發cb函式

import Watcher from `../observer/watcher`
export function initState (vm: Component) {
  ...
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    createWatcher(vm, key, handler)
  }
}

function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  return vm.$watch(expOrFn, handler, options)
}

Vue.prototype.$watch = function (
  expOrFn: string | Function,
  cb: any,
  options?: Object
): Function {
  const vm: Component = this
  const watcher = new Watcher(vm, expOrFn, cb, options)
}
複製程式碼

寫在最後

這裡的思路,其實就是翻著原始碼走的,寫的時候都是按自己的理解思路來的,存在問題的話歡迎指出~

相關文章