1. 程式人生 > 程式設計 >詳細分析vue響應式原理

詳細分析vue響應式原理

前言

響應式原理作為 Vue 的核心,使用資料劫持實現資料驅動檢視。在面試中是經常考查的知識點,也是面試加分項。

本文將會循序漸進的解析響應式原理的工作流程,主要以下面結構進行:

  1. 分析主要成員,瞭解它們有助於理解流程
  2. 將流程拆分,理解其中的作用
  3. 結合以上的點,理解整體流程

文章稍長,但大部分是程式碼實現,還請耐心觀看。為了方便理解原理,文中的程式碼會進行簡化,如果可以請對照原始碼學習。

主要成員

響應式原理中,Observe、Watcher、Dep這三個類是構成完整原理的主要成員。

  • Observe,響應式原理的入口,根據資料型別處理觀測邏輯
  • Watcher,用於執行更新渲染,元件會擁有一個渲染Watcher,我們常說的收集依賴,就是收集 Watcher
  • Dep,依賴收集器,屬性都會有一個Dep,方便發生變化時能夠找到對應的依賴觸發更新

下面來看看這些類的實現,包含哪些主要屬性和方法。

Observe:我會對資料進行觀測

溫馨提示:程式碼裡的序號對應程式碼塊下面序號的講解

// 原始碼位置:/src/core/observer/index.js
class Observe {
 constructor(data) {
  this.dep = new Dep()
  // 1
  def(data,'__ob__',this)
  if (Array.isArray(data)) {
   // 2
   protoAugment(data,arrayMethods)
   // 3
   this.observeArray(data)
  } else {
   // 4
   this.walk(data)
  }
 }
 walk(data) {
  Object.keys(data).forEach(key => {
   defineReactive(data,key,data[key])
  })
 }
 observeArray(data) {
  data.forEach(item => {
   observe(item)
  })
 }
}
  1. 為觀測的屬性新增 __ob__ 屬性,它的值等於 this,即當前 Observe 的例項
  2. 為陣列新增重寫的陣列方法,比如:push、unshift、splice 等方法,重寫目的是在呼叫這些方法時,進行更新渲染
  3. 觀測陣列內的資料,observe 內部會呼叫 new Observe,形成遞迴觀測
  4. 觀測物件資料,defineReactive 為資料定義 get 和 set ,即資料劫持

Dep:我會為資料收集依賴

// 原始碼位置:/src/core/observer/dep.js
let id = 0
class Dep{
 constructor() {
  this.id = ++id // dep 唯一標識
  this.subs = [] // 儲存 Watcher
 }
 // 1
 depend() {
  Dep.target.addDep(this)
 }
 // 2
 addSub(watcher) {
  this.subs.push(watcher)
 }
 // 3
 notify() {
  this.subs.forEach(watcher => watcher.update())
 }
}

// 4
Dep.target = null

export function pushTarget(watcher) {
 Dep.target = watcher
} 

export function popTarget(){
 Dep.target = null
}

export default Dep
  1. 據收集依賴的主要方法,Dep.target 是一個 watcher 例項
  2. 新增 watcher 到陣列中,也就是新增依賴
  3. 屬性在變化時會呼叫 notify 方法,通知每一個依賴進行更新
  4. Dep.target 用來記錄 watcher 例項,是全域性唯一的,主要作用是為了在收集依賴的過程中找到相應的 watcher

pushTarget 和 popTarget 這兩個方法顯而易見是用來設定 Dep.target的。Dep.target 也是一個關鍵點,這個概念可能初次檢視原始碼會有些難以理解,在後面的流程中,會詳細講解它的作用,需要注意這部分的內容。

Watcher:我會觸發檢視更新

// 原始碼位置:/src/core/observer/watcher.js
let id = 0
export class Watcher {
 constructor(vm,exprOrFn,cb,options){
  this.id = ++id // watcher 唯一標識
  this.vm = vm
  this.cb = cb
  this.options = options
  // 1
  this.getter = exprOrFn
  this.deps = []
  this.depIds = new Set()

  this.get()
 }
 run() {
  this.get()
 }
 get() {
  pushTarget(this)
  this.getter()
  popTarget(this)
 }
 // 2
 addDep(dep) {
  // 防止重複新增 dep
  if (!this.depIds.has(dep.id)) {
   this.depIds.add(dep.id)
   this.deps.push(dep)
   dep.addSub(this)
  }
 }
 // 3
 update() {
  queueWatcher(this)
 }
}
  1. this.getter 儲存的是更新檢視的函式
  2. watcher 儲存 dep,同時 dep 也儲存 watcher,進行雙向記錄
  3. 觸發更新,queueWatcher 是為了進行非同步更新,非同步更新會呼叫 run 方法進行更新頁面

響應式原理流程

對於以上這些成員具有的功能,我們都有大概的瞭解。下面結合它們,來看看這些功能是如何在響應式原理流程中工作的。

資料觀測

資料在初始化時會通過 observe 方法來建立 Observe 類

// 原始碼位置:/src/core/observer/index.js
export function observe(data) {
 // 1
 if (!isObject(data)) {
  return
 }
 let ob;
 // 2
 if (data.hasOwnProperty('__ob__') && data.__ob__ instanceof Observe) {
  ob = data.__ob__
 } else {
  // 3
  ob = new Observe(data)
 }
 return ob
}

在初始化時,observe 拿到的 data 就是我們在 data 函式內返回的物件。

  1. observe 函式只對 object 型別資料進行觀測
  2. 觀測過的資料都會被新增上 __ob__ 屬性,通過判斷該屬性是否存在,防止重複觀測
  3. 建立 Observe 類,開始處理觀測邏輯

物件觀測

進入 Observe 內部,由於初始化的資料是一個物件,所以會呼叫 walk 方法:

walk(data) {
 Object.keys(data).forEach(key => {
  defineReactive(data,data[key])
 })
}

defineReactive 方法內部使用 Object.defineProperty 對資料進行劫持,是實現響應式原理最核心的地方。

function defineReactive(obj,value) {
 // 1
 let childOb = observe(value)
 // 2
 const dep = new Dep()
 Object.defineProperty(obj,{
  get() {
   if (Dep.target) {
    // 3
    dep.depend()
    if (childOb) {
     childOb.dep.depend()
    }
   }
   return value
  },set(newVal) {
   if (newVal === value) {
    return
   }
   value = newVal
   // 4
   childOb = observe(newVal)
   // 5
   dep.notify()
   return value
  }
 })
}
  1. 由於值可能是物件型別,這裡需要呼叫 observe 進行遞迴觀測
  2. 這裡的 dep 就是上面講到的每一個屬性都會有一個 dep,它是作為一個閉包的存在,負責收集依賴和通知更新
  3. 在初始化時,Dep.target 是元件的渲染 watcher,這裡 dep.depend 收集的依賴就是這個 watcher,childOb.dep.depend 主要是為陣列收集依賴
  4. 設定的新值可能是物件型別,需要對新值進行觀測
  5. 值發生改變,dep.notify 通知 watcher 更新,這是我們改變資料後能夠實時更新頁面的觸發點

通過 Object.defineProperty 對屬性定義後,屬性的獲取觸發 get 回撥,屬性的設定觸發 set 回撥,實現響應式更新。

通過上面的邏輯,也能得出為什麼 Vue3.0 要使用 Proxy 代替 Object.defineProperty 了。Object.defineProperty 只能對單個屬性進行定義,如果屬性是物件型別,還需要遞迴去觀測,會很消耗效能。而 Proxy 是代理整個物件,只要屬性發生變化就會觸發回撥。

陣列觀測

對於陣列型別觀測,會呼叫 observeArray 方法:

observeArray(data) {
 data.forEach(item => {
  observe(item)
 })
}

與物件不同,它執行 observe 對陣列內的物件型別進行觀測,並沒有對陣列的每一項進行 Object.defineProperty 的定義,也就是說陣列內的項是沒有 dep 的。

所以,我們通過陣列索引對項進行修改時,是不會觸發更新的。但可以通過 this.$set 來修改觸發更新。那麼問題來了,為什麼 Vue 要這樣設計?

結合實際場景,陣列中通常會存放多項資料,比如列表資料。這樣觀測起來會消耗效能。還有一點原因,一般修改陣列元素很少會直接通過索引將整個元素替換掉。例如:

export default {
  data() {
    return {
      list: [
        {id: 1,name: 'Jack'},{id: 2,name: 'Mike'}
      ]
    }
  },cretaed() {
    // 如果想要修改 name 的值,一般是這樣使用
    this.list[0].name = 'JOJO'
    // 而不是以下這樣
    // this.list[0] = {id:1,name: 'JOJO'}
    // 當然你可以這樣更新
    // this.$set(this.list,'0',{id:1,name: 'JOJO'})
  }
}

陣列方法重寫

當陣列元素新增或刪除,檢視會隨之更新。這並不是理所當然的,而是 Vue 內部重寫了陣列的方法,呼叫這些方法時,陣列會更新檢測,觸發檢視更新。這些方法包括:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

回到 Observe 的類中,當觀測的資料型別為陣列時,會呼叫 protoAugment 方法。

if (Array.isArray(data)) {
 protoAugment(data,arrayMethods)
 // 觀察陣列
 this.observeArray(data)
} else {
 // 觀察物件
 this.walk(data)
}

這個方法裡把陣列原型替換為 arrayMethods ,當呼叫改變陣列的方法時,優先使用重寫後的方法。

function protoAugment(data,arrayMethods) {
 data.__proto__ = arrayMethods
}

接下來看看 arrayMethods 是如何實現的:

// 原始碼位置:/src/core/observer/array.js
// 1
let arrayProto = Array.prototype
// 2
export let arrayMethods = Object.create(arrayProto)

let methods = [
 'push','pop','shift','unshift','reverse','sort','splice'
]

methods.forEach(method => {
 arrayMethods[method] = function(...args) {
  // 3
  let res = arrayProto[method].apply(this,args)
  let ob = this.__ob__
  let inserted = ''
  switch(method){
   case 'push':
   case 'unshift':
    inserted = args
    break;
   case 'splice':
    inserted = args.slice(2)
    break;
  }
  // 4
  inserted && ob.observeArray(inserted)
  // 5
  ob.dep.notify()
  return res
 }
})
  1. 將陣列的原型儲存起來,因為重寫的陣列方法裡,還是需要呼叫原生陣列方法的
  2. arrayMethods 是一個物件,用於儲存重寫的方法,這裡使用 Object.create(arrayProto) 建立物件是為了使用者在呼叫非重寫方法時,能夠繼承使用原生的方法
  3. 呼叫原生方法,儲存返回值,用於設定重寫函式的返回值
  4. inserted 儲存新增的值,若 inserted 存在,對新值進行觀測
  5. ob.dep.notify 觸發檢視更新

依賴收集

依賴收集是檢視更新的前提,也是響應式原理中至關重要的環節。

虛擬碼流程

為了方便理解,這裡寫一段虛擬碼,大概瞭解依賴收集的流程:

// data 資料
let data = {
  name: 'joe'
}

// 渲染watcher
let watcher = {
  run() {
    dep.tagret = watcher
    document.write(data.name)
  }
}

// dep
let dep = [] // 儲存依賴 
dep.tagret = null // 記錄 watcher

// 資料劫持
Object.defineProperty(data,'name',{
  get(){
    // 收集依賴
    dep.push(dep.tagret)
  },set(newVal){
    data.name = newVal
    dep.forEach(watcher => {
      watcher.run()
    })
  }
})

初始化:

  1. 首先會對 name 屬性定義 get 和 set
  2. 然後初始化會執行一次 watcher.run 渲染頁面
  3. 這時候獲取 data.name,觸發 get 函式收集依賴。

更新:

修改 data.name,觸發 set 函式,呼叫 run 更新檢視。

真正流程

下面來看看真正的依賴收集流程是如何進行的。

function defineReactive(obj,value) {
 let childOb = observe(value)
 const dep = new Dep()
 Object.defineProperty(obj,{
  get() {
   if (Dep.target) {
    dep.depend() // 收集依賴
    if (childOb) {
     childOb.dep.depend()
    }
   }
   return value
  },set(newVal) {
   if (newVal === value) {
    return
   }
   value = newVal
   childOb = observe(newVal)
   dep.notify()
   return value
  }
 })
}

首先初始化資料,呼叫 defineReactive 函式對資料進行劫持。

export class Watcher {
 constructor(vm,options){
  this.getter = exprOrFn
  this.get()
 }
 get() {
  pushTarget(this)
  this.getter()
  popTarget(this)
 }
}

初始化將 watcher 掛載到 Dep.target,this.getter 開始渲染頁面。渲染頁面需要對資料取值,觸發 get 回撥,dep.depend 收集依賴。

class Dep{
 constructor() {
  this.id = id++
  this.subs = []
 }
 depend() {
  Dep.target.addDep(this)
 }
}

Dep.target 為 watcher,呼叫 addDep 方法,並傳入 dep 例項。

export class Watcher {
 constructor(vm,options){
  this.deps = []
  this.depIds = new Set()
 }
 addDep(dep) {
  if (!this.depIds.has(dep.id)) {
   this.depIds.add(dep.id)
   this.deps.push(dep)
   dep.addSub(this)
  }
 }
}

addDep 中新增完 dep 後,呼叫 dep.addSub 並傳入當前 watcher 例項。

class Dep{
 constructor() {
  this.id = id++
  this.subs = []
 }
 addSub(watcher) {
  this.subs.push(watcher)
 }
}

將傳入的 watcher 收集起來,至此依賴收集流程完畢。

補充一點,通常頁面上會繫結很多屬性變數,渲染會對屬性取值,此時每個屬性收集的依賴都是同一個 watcher,即元件的渲染 watcher。

陣列的依賴收集

methods.forEach(method => {
 arrayMethods[method] = function(...args) {
  let res = arrayProto[method].apply(this,args)
  let ob = this.__ob__
  let inserted = ''
  switch(method){
   case 'push':
   case 'unshift':
    inserted = args
    break;
   case 'splice':
    inserted = args.slice(2)
    break;
  }
  // 對新增的值觀測
  inserted && ob.observeArray(inserted)
  // 更新檢視
  ob.dep.notify()
  return res
 }
})

還記得重寫的方法裡,會呼叫 ob.dep.notify 更新檢視,__ob__ 是我們在 Observe 為觀測資料定義的標識,值為 Observe 例項。那麼 ob.dep 的依賴是在哪裡收集的?

function defineReactive(obj,value) {
 // 1
 let childOb = observe(value)
 const dep = new Dep()
 Object.defineProperty(obj,{
  get() {
   if (Dep.target) {
    dep.depend()
    // 2
    if (childOb) {
     childOb.dep.depend()
    }
   }
   return value
  },set(newVal) {
   if (newVal === value) {
    return
   }
   value = newVal
   childOb = observe(newVal)
   dep.notify()
   return value
  }
 })
}
  1. observe 函式返回值為 Observe 例項
  2. childOb.dep.depend 執行,為 Observe 例項的 dep 新增依賴

所以在陣列更新時,ob.dep 內已經收集到依賴了。

整體流程

下面捋一遍初始化流程和更新流程,如果你是初次看原始碼,不知道從哪裡看起,也可以參照以下的順序。由於原始碼實現比較多,下面展示的原始碼會稍微刪減一些程式碼

初始化流程

入口檔案:

// 原始碼位置:/src/core/instance/index.js
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

function Vue (options) {
 this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

_init:

// 原始碼位置:/src/core/instance/init.js
export function initMixin (Vue: Class<Component>) {
 Vue.prototype._init = function (options?: Object) {
  const vm: Component = this
  // a uid
  vm._uid = uid++

  // merge options
  if (options && options._isComponent) {
   // optimize internal component instantiation
   // since dynamic options merging is pretty slow,and none of the
   // internal component options needs special treatment.
   initInternalComponent(vm,options)
  } else {
   // mergeOptions 對 mixin 選項和傳入的 options 選項進行合併
   // 這裡的 $options 可以理解為 new Vue 時傳入的物件
   vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),options || {},vm
   )
  }

  // expose real self
  vm._self = vm
  initLifecycle(vm)
  initEvents(vm)
  initRender(vm)
  callHook(vm,'beforeCreate')
  initInjections(vm) // resolve injections before data/props
  // 初始化資料
  initState(vm)
  initProvide(vm) // resolve provide after data/props
  callHook(vm,'created')

  if (vm.$options.el) {
   // 初始化渲染頁面 掛載元件
   vm.$mount(vm.$options.el)
  }
 }
}

上面主要關注兩個函式,initState 初始化資料,vm.$mount(vm.$options.el) 初始化渲染頁面。

先進入 initState:

// 原始碼位置:/src/core/instance/state.js 
export function initState (vm: Component) {
 vm._watchers = []
 const opts = vm.$options
 if (opts.props) initProps(vm,opts.props)
 if (opts.methods) initMethods(vm,opts.methods)
 if (opts.data) {
  // data 初始化
  initData(vm)
 } else {
  observe(vm._data = {},true /* asRootData */)
 }
 if (opts.computed) initComputed(vm,opts.computed)
 if (opts.watch && opts.watch !== nativeWatch) {
  initWatch(vm,opts.watch)
 }
}

function initData (vm: Component) {
 let data = vm.$options.data
 // data 為函式時,執行 data 函式,取出返回值
 data = vm._data = typeof data === 'function'
  ? getData(data,vm)
  : data || {}
 // proxy data on instance
 const keys = Object.keys(data)
 const props = vm.$options.props
 const methods = vm.$options.methods
 let i = keys.length
 while (i--) {
  const key = keys[i]
  if (props && hasOwn(props,key)) {
   process.env.NODE_ENV !== 'production' && warn(
    `The data property "${key}" is already declared as a prop. ` +
    `Use prop default value instead.`,vm
   )
  } else if (!isReserved(key)) {
   proxy(vm,`_data`,key)
  }
 }
 // observe data
 // 這裡就開始走觀測資料的邏輯了
 observe(data,true /* asRootData */)
}

observe 內部流程在上面已經講過,這裡再簡單過一遍:

  1. new Observe 觀測資料
  2. defineReactive 對資料進行劫持

initState 邏輯執行完畢,回到開頭,接下來執行 vm.$mount(vm.$options.el) 渲染頁面:

$mount:

// 原始碼位置:/src/platforms/web/runtime/index.js 
Vue.prototype.$mount = function (
 el?: string | Element,hydrating?: boolean
): Component {
 el = el && inBrowser ? query(el) : undefined
 return mountComponent(this,el,hydrating)
}

mountComponent:

// 原始碼位置:/src/core/instance/lifecycle.js
export function mountComponent (
 vm: Component,el: ?Element,hydrating?: boolean
): Component {
 vm.$el = el
 callHook(vm,'beforeMount')

 let updateComponent
 /* istanbul ignore if */
 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`,endTag)
  }
 } else {
  // 資料改變時 會呼叫此方法
  updateComponent = () => {
   // vm._render() 返回 vnode,這裡面會就對 data 資料進行取值
   // vm._update 將 vnode 轉為真實dom,渲染到頁面上
   vm._update(vm._render(),hydrating)
  }
 }
 
 // 執行 Watcher,這個就是上面所說的渲染wacther 
 new Watcher(vm,updateComponent,noop,{
  before () {
   if (vm._isMounted && !vm._isDestroyed) {
    callHook(vm,'beforeUpdate')
   }
  }
 },true /* isRenderWatcher */)
 hydrating = false

 // manually mounted instance,call mounted on self
 // mounted is called for render-created child components in its inserted hook
 if (vm.$vnode == null) {
  vm._isMounted = true
  callHook(vm,'mounted')
 }
 return vm
}

Watcher:

// 原始碼位置:/src/core/observer/watcher.js 
let uid = 0

export default class Watcher {
 constructor(vm,options){
  this.id = ++id
  this.vm = vm
  this.cb = cb
  this.options = options
  // exprOrFn 就是上面傳入的 updateComponent
  this.getter = exprOrFn

  this.deps = []
  this.depIds = new Set()

  this.get()
 }
 get() {
  // 1. pushTarget 將當前 watcher 記錄到 Dep.target,Dep.target 是全域性唯一的
  pushTarget(this)
  let value
  const vm = this.vm
  try {
  // 2. 呼叫 this.getter 相當於會執行 vm._render 函式,對例項上的屬性取值,
  //由此觸發 Object.defineProperty 的 get 方法,在 get 方法內進行依賴收集(dep.depend),這裡依賴收集就需要用到 Dep.target
   value = this.getter.call(vm,vm)
  } catch (e) {
   if (this.user) {
    handleError(e,vm,`getter for watcher "${this.expression}"`)
   } else {
    throw e
   }
  } finally {
   // "touch" every property so they are all tracked as
   // dependencies for deep watching
   if (this.deep) {
    traverse(value)
   }
   // 3. popTarget 將 Dep.target 置空
   popTarget()
   this.cleanupDeps()
  }
  return value
 }
}

至此初始化流程完畢,初始化流程的主要工作是資料劫持、渲染頁面和收集依賴。

更新流程

資料發生變化,觸發 set ,執行 dep.notify

// 原始碼位置:/src/core/observer/dep.js 
let uid = 0

/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
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()
  if (process.env.NODE_ENV !== 'production' && !config.async) {
   // subs aren't sorted in scheduler if not running async
   // we need to sort them now to make sure they fire in correct
   // order
   subs.sort((a,b) => a.id - b.id)
  }
  for (let i = 0,l = subs.length; i < l; i++) {
   // 執行 watcher 的 update 方法
   subs[i].update()
  }
 }
}

wathcer.update:

// 原始碼位置:/src/core/observer/watcher.js 
/**
 * Subscriber interface.
 * Will be called when a dependency changes.
 */
update () {
 /* istanbul ignore else */
 if (this.lazy) { // 計算屬性更新
  this.dirty = true
 } else if (this.sync) { // 同步更新
  this.run()
 } else {
  // 一般的資料都會進行非同步更新
  queueWatcher(this)
 }
}

queueWatcher:

// 原始碼位置:/src/core/observer/scheduler.js

// 用於儲存 watcher
const queue: Array<Watcher> = []
// 用於 watcher 去重
let has: { [key: number]: ?true } = {}
/**
 * Flush both queues and run the watchers.
 */
function flushSchedulerQueue () {
 let watcher,id

 // 對 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]
  id = watcher.id
  has[id] = null
  // run方法更新檢視
  watcher.run()
 }
}
/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 */
export function queueWatcher (watcher: Watcher) {
 const id = watcher.id
 if (has[id] == null) {
  has[id] = true
  // watcher 加入陣列
  queue.push(watcher)
  // 非同步更新
  nextTick(flushSchedulerQueue)
 }
}

nextTick:

// 原始碼位置:/src/core/util/next-tick.js

const callbacks = []
let pending = false

function flushCallbacks () {
 pending = false
 const copies = callbacks.slice(0)
 callbacks.length = 0
 // 遍歷回撥函式執行
 for (let i = 0; i < copies.length; i++) {
  copies[i]()
 }
}

let timerFunc

if (typeof Promise !== 'undefined' && isNative(Promise)) {
 const p = Promise.resolve()
 timerFunc = () => {
  p.then(flushCallbacks)
 }
}

export function nextTick (cb?: Function,ctx?: Object) {
 let _resolve
 // 將回調函式加入陣列
 callbacks.push(() => {
  if (cb) {
   cb.call(ctx)
  }
 })
 if (!pending) {
  pending = true
  // 遍歷回撥函式執行
  timerFunc()
 }
 // $flow-disable-line
 if (!cb && typeof Promise !== 'undefined') {
  return new Promise(resolve => {
   _resolve = resolve
  })
 }
}

這一步是為了使用微任務將回調函式非同步執行,也就是上面的p.then。最終,會呼叫 watcher.run 更新頁面。

至此更新流程完畢。

寫在最後

如果沒有接觸過原始碼的同學,我相信看完可能還是會有點懵的,這很正常。建議對照原始碼再自己多看幾遍就能知道流程了。對於有基礎的同學就當做是複習了。

想要變強,學會看原始碼是必經之路。在這過程中,不僅能學習框架的設計思想,還能培養自己的邏輯思維。萬事開頭難,遲早都要邁出這一步,不如就從今天開始。

簡化後的程式碼我已放在github,有需要的可以看看。

以上就是詳細分析vue響應式原理的詳細內容,更多關於Vue響應式原理的資料請關注我們其它相關文章!