手摸手帶你理解Vue響應式原理

前言

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

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

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

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

主要成員

在響應式原理中,ObserveDepWatcher 這三個類是構成完整原理的主要成員。

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

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

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. 為數組添加重寫的數組方法,比如:pushunshiftsplice 等方法,重寫目的是在調用這些方法時,進行更新渲染
  3. 觀測數組內的數據,observe 內部會調用 new Observe,形成遞歸觀測
  4. 觀測對象數據,defineReactive 為數據定義 getset ,即數據劫持

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

pushTargetpopTarget 這兩個方法顯而易見是用來設置 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, key, data[key])
  })
}

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

function defineReactive(obj, key, value) {
  // 1
  let childOb = observe(value)
  // 2
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    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 收集的依賴就是這個 watcherchildOb.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

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

初始化:

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

更新:

修改 data.name,觸發 set 函數,調用 run 更新視圖。

真正流程

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

function defineReactive(obj, key, value) {
  let childOb = observe(value)
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    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, exprOrFn, cb, options){
    this.getter = exprOrFn
    this.get()
  }
  get() {
    pushTarget(this)
    this.getter()
    popTarget(this)
  }
}

初始化將 watcher 掛載到 Dep.targetthis.getter 開始渲染頁面。渲染頁面需要對數據取值,觸發 get 回調,dep.depend 收集依賴。

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

Dep.targetwatcher,調用 addDep 方法,並傳入 dep 實例。

export class Watcher {
  constructor(vm, exprOrFn, cb, 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, key, value) {
  // 1
  let childOb = observe(value)
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    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`, startTag, 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, exprOrFn, cb, 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,有需要的可以看看。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

FB行銷專家,教你從零開始的技巧

哥倫比亞毒梟艾斯科巴的河馬 意外恢復1萬多年前部分生態系功能

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

野火滅村奪74命 雅典同時燃15場森林火 希臘總理:很奇怪

摘錄自2018年7月25日東森新聞台北報導

希臘首都雅典近日高溫達到40℃,野火23日下午起失控焚燒,強風助長下蔓延燒至林地和村落,死亡人數超過74人,至少有187人受傷,甚至有「滅村」情形。這是十年來發生最嚴重的森林大火,當局已經進入緊急狀態,忙著撤離居民和遊客。

希臘總理齊普拉斯(Alexis Tsipras)24日宣布為期三天的全國性哀悼:「這個國家正面臨一場無法用言語形容的悲劇。」希臘同時也呼籲歐盟能提供國際援助,目前當局已布署直升機降水下來,提供民眾逃難的時間。

齊普拉斯與政府官員也派了幾架由美國政府提供的無人駕駛飛機追查任何可疑的行動,他們認為雅典的東、北、西方等地區同時發生15場森林大火是一件很奇怪的事情,也不知道是什麼原因點燃野火。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

FB行銷專家,教你從零開始的技巧

世衛組織總幹事宣布剛果伊波拉疫情結束

摘錄自2018年7月24日聯合國新聞報導

世界衛生組織總幹事譚德賽今天(24日)宣布,由於剛果民主共和國和國際社會的一道努力,該國的伊波拉疫情已經結束。

6月12日,剛果民主共和國最後一名伊波拉患者在第二次病毒檢測呈陰性後出院。

世衛組織表示,這種疾病缺乏治愈方法的部分原因在於傳統的埋葬方法仍在繼續,在一些情況下,這牽涉到人們在曾經對死於伊波拉病毒感染的人進行清潔的水中沐浴。

伊波拉病毒於1976年在剛果民主共和國發現,主要通過接觸野生動物感染,並可在人際之間傳播。2014-16年間,西非地區暴發大規模伊波拉疫情,造成超過1萬1000人​​死亡。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

網頁設計最專業,超強功能平台可客製化

最天然的野溪整治 英國實測河狸治水法

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

澎湖傳統南淺漁場 淪中國鐵殼船抽砂場

摘錄自2018年07月26日蘋果日報報導

澎湖最大傳統漁場南淺漁場(台灣淺堆)近年淪為中國採砂場,驚見每天都聚集大批中國鐵殼船不斷抽砂,漁民憂這會破壞生態影響生計,找上澎湖地區兩岸交流協會創會理事長,也是無黨聯盟主席林炳坤,希望能制止可預見的海洋生態浩劫。
 
林炳坤今 (26)日邀集保育團體和海洋保育志工隊,包船趕赴現場,觸目所見約40艘中國鐵殼船忙碌抽砂,每艘船噸位動輒萬噸以上。林炳坤指出,中國去年宣布禁止沿海採取海砂政策之後,這些抽砂船就轉移到澎湖人俗稱的「南淺漁場」海域抽取海砂,每天24小時不間斷,估計每天抽走約幾十萬噸的海砂,恐嚴重影響澎湖海洋生態及海底砂盤的穩定。

根據海洋保育志工以船上衛星定位這個海域位在北緯22度57分13秒、東經118度06分39秒,比較接近澎湖七美腳,偏台灣這一側,雖不是領海,但在國際上屬於200海浬經濟海域重疊區。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

FB行銷專家,教你從零開始的技巧

日產結盟三菱 衝刺北美及新興電動車市場

日產和三菱汽車周二(5)宣布策略結盟,將藉由共同開發電動車,共享工廠、產能和技術,並提升北美和新興市場汽車銷量。日產在聲明中表示,「日產已與三菱達成策略結盟,此舉將擴大雷諾與日產的盟友關係。」

三菱加入後,雷諾與日產的盟友將增加至5名,還包括德國戴姆勒和俄羅斯的AvtoVAZ,共同抵禦德國福斯和日本豐田的競爭。此外,雷諾的本國對手標緻雪鐵龍(PSA Peugeot Citroen)也與通用結盟,以節省成本。

2011年,日產和三菱成立合資公司NMKV,共同研發迷你車,本次的聲明表示,「預期現有的NMKV公司將共同研發新款小車,其中包括預計在全球銷售、特定版本的電動車。」

雷諾、日產和三菱表示,結盟後的首批新車之一,將是由雷諾子公司-雷諾三星汽車在南韓生產的大型車,該車款將掛上三菱的品牌在北美市場販售。第2款較小型的車款也是為三菱設計,係由雷諾研發,生產地點尚未決定。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

中國新能源汽車補貼未來兩年將下調 地方政府伸出援手

據南方網報導,在針對私人市場新能源補貼新政《關於繼續開展新能源汽車推廣應用工作的通知》(以下簡稱《通知》)出台月餘之際,中國大陸地方政府終於按捺不住了。

按照《通知》中的目標,在2013年至2015年這3年間,特大型城市或重點區域新能源汽車累計推廣量不低於1萬輛,其他城市或區域累計推廣量不低於5,000輛。

此外,《通知》明確,2014年和2015年,純電動乘用車、插電式混合動力(含增程式)乘用車、純電動專用車、燃料電池汽車補助標準在2013年標準的基礎上分別下降10%和20%。在新政補貼標準降低的情況下,地方政府開始伸出援手。

深圳市政府已經著手出台新能源汽車補貼的相關細則,最早會在12月出台,具體的補貼細則為與中央補貼一比一的比例。此外,北京市政府於2013年10月30日出台了《北京市2013-2017年機動車排放污染控制工作方案》,其中亦提出了關於推廣新能源汽車的相關細則。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

FB行銷專家,教你從零開始的技巧

充電設施不足 雷諾日產150萬電動車銷量恐難達成

雷諾暨日產執行長高恩(Carlos Ghosn)在接受英國「金融時報」訪問時首度公開坦承,由於充電設施不足,雷諾日產無法達成在2016年全球電動車銷售150萬輛的目標,達到時間將較預期落後4年多。

雷諾與日產已在電動車上投資數十億美元,高恩也一向大力唱好電動車前景。但他受訪時承認,電動車市場不如他的預期。他預估,以目前銷售速度來看,要達成銷售150萬電動車目標,恐怕較預定2016年的時間,再往後延4年或5年。

雖然政府向汽車業施壓去降低廢氣排放量,但到目前為止,電動車生產成本高,難以銷售和行駛哩數有限。尤其主要市場缺乏充電設施,產生所謂「續航憂慮」現象。

雖然外界普遍認為電動車價格高昂,是讓其難以普及的主因,但高恩認為充電與支援等基礎設施不足,才是導致雷諾日產難以達成銷售目標原因。在挪威和美國加州,因為政府給電動車買家提供補貼,並廣泛建設充電據點,才刺激電動車買氣。

雷諾與日產過去5年共賣出逾12萬輛電動車,為業界最高。日產Leaf為全球賣最好的電動車,迄今賣出約8.5萬輛。此外,雷諾日本與三菱已同意合作全球銷售小電動車,外界看好能進一步降低技術成本。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

網頁設計最專業,超強功能平台可客製化

英國三季度電動汽車銷量環比增25%

據英國媒體報導,第三季度英國電動汽車的銷量環比增長25%,電動汽車補貼登記數量達到1149台,創下2011年1月以來的最高記錄。

2011年1月,英國政府曾宣佈,在隨後的14個月中,只要用戶購買一輛低碳電動汽車,即將獲得高達5000英鎊的補貼。2012年,政府決定把對電動汽車的補貼措施延長到2015年。

不久前,雷諾—日產公司首席執行官卡洛斯•戈恩表示,2016年底之前,將實現不了150萬輛電動汽車的銷售目標,實現目標有可能是在2020年或2021年。他表示,政府對充電站建設的投入不到位,影響了電動汽車的銷售。

日產公司的純電動汽車「聆風」是政府補貼計畫下第一個受益的主要車型,並避開了英國執政聯盟預算縮減的風潮。

聆風由日產公司桑德蘭工廠生產,今年4月在挪威上市以後一炮打響,成為市場上最暢銷的汽車。當然,這部分得益於挪威慷慨的電動汽車減稅政策。

但是聆風也有不少競爭者,如通用雪佛蘭沃藍達在歐洲大陸的版本Vauxhall Ampera、相對便宜的雷諾Zoe和寶馬i3。此外,還有福特福克斯的電動汽車及特斯拉的豪華電動汽車。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!