vue源碼分析

瞭解 Flow
官網:https://flow.org/
JavaScript 的靜態類型檢查器,在編譯階段就進行檢查而不是執行時,最終也會編譯成JavaScript來運行
爲了保證代碼的可維護性和可讀性,因此vue2.x中使用flow,使得vue在代碼最小改動的狀況下使用靜態類型檢查
Flow 的靜態類型檢查錯誤是經過靜態類型推斷實現的
文件開頭經過 // @flow 或者 /* @flow */ 聲明
/* @flow */
function square(n: number): number {
return n * n;
}
square("2"); // Error!
​

調試設置
咱們能夠在閱讀源碼時經過打包調試代碼來驗證本身關於源碼的理解是否正確
打包工具 Rollup
Vue.js 源碼的打包工具使用的是 Rollup,比 Webpack 輕量
Webpack 把全部文件當作模塊,Rollup 只處理 js 文件更適合在 Vue.js 這樣的庫中使用
Rollup 打包不會生成冗餘的代碼javascript

開發項目適合使用webpack,開發庫時適合使用Rollup
調試流程
安裝依賴
npm ihtml

設置 sourcemap
package.json 文件中的 dev 腳本中新添加參數 --sourcemap,開啓代碼映射
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev"
執行dev
npm run dev 執行打包,用的是 rollup,-w 參數是監聽文件的變化,文件變化自動從新打包
此時生成的vue.js文件和vue.js.map文件,若是想要其餘版本的js文件能夠經過npm run build來執行vue

調試
examples 的示例中引入的 vue.min.js 改成 vue.js
打開 Chrome 的調試工具中的 source,咱們在開啓sourcemap後就能夠看到源碼src目錄了
這樣作的目的是若是不開啓sourcemap咱們只能調試打包後的vue.js文件,該文件中有1w多行代碼不方便調試
而開啓sourcemap後咱們能夠直接經過模塊的形式調試模塊源碼java

Vue 的不一樣構建版本
執行npm run build 從新打包全部文件
官方文檔 - 對不一樣構建版本的解釋
dist/README.md
完整版:同時包含編譯器和運行時的版本
編譯器:用來將模板字符串編譯成爲 JavaScript 渲染函數的代碼(將template轉換成render函數),體積大(3000行代碼)、效率低
運行時:用來建立 Vue 實例、渲染並處理虛擬 DOM 等的代碼,體積小、效率高。基本上就是除去編譯器的代碼
UMD:UMD 版本通用的模塊版本,支持多種模塊方式。 vue.js 默認文件就是運行時 + 編譯器的UMD 版本,還能夠把vue掛載到window對象上
CommonJS(cjs):CommonJS 版本用來配合老的打包工具好比 Browserify 或 webpack 1
ES Module
從 2.6 開始 Vue 會提供兩個 ES Modules (ESM) 構建文件,爲現代打包工具提供的版本,咱們腳手架就是這個版本
ESM 格式被設計爲能夠被靜態分析,因此打包工具能夠利用這一點來進行「tree-shaking」並將用不到的代碼排除出最終的包
ES6 模塊與 CommonJS 模塊的差別【參考阮一峯老師的文章】node

Runtime+Compiler 完整版
// <script src="../../dist/vue.js"></script>
// Compiler
// 須要編譯器,把 template 轉換成 render 函數
const vm = new Vue({
  el: '#app',
  template: '<h1>{{ msg }}</h1>',
  data: {
    msg: 'Hello Vue'
  }
})
Runtime-only
// <script src="../../dist/vue.runtime.js"></script>
// Runtime
// 不須要編譯器
const vm = new Vue({
  el: '#app',
  // template: '<h1>{{ msg }}</h1>', // 沒有編譯器後不能解析html語法須要使用下面的render函數
  render (h) {
    return h('h1', this.msg)
  },
  data: {
    msg: 'Hello Vue'
  }
})


使用vue-cli建立的項目是使用的vue.runtimr.esm.js
在vue中查看webpack的配置文件(因爲vue對webpack進行了深度封裝,因此須要使用其餘命令來查看)
使用inspect方法:react

vue inspect  // 這樣使用獲取到的webpack信息會打印到終端,不方便查看
vue inspect > output.js     // > 的做用是把當前webpack配置信息打印到output.js文件中
​
// output.js
  resolve: {
    alias: {
      ...
      vue$: 'vue/dist/vue.runtime.esm.js'
    }
  }
注意: *.vue 文件中的模板是在構建時預編譯的,已經準換成了render函數,最終打包後的結果是不須要編譯器,只須要運行時版本便可

尋找入口文件
查看 dist/vue.js 的構建過程來找入口文件
執行構建webpack

npm run dev
# "dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev"
# --environment TARGET:web-full-dev 設置環境變量 TARGET
​
# 其中-w 表示watch掛起監聽模式,-c後面是配置文件,--environment設置環境變量爲TARGET,其中web表示web端,full表示完整版,dev表示開發版不進行壓縮


script/config.js 的執行過程(文件末尾)
做用:生成 rollup 構建的配置文件
使用環境變量 TARGET = web-full-dev


getConfig(name)
根據環境變量 TARGET 獲取配置信息
const opts = builds[name]
builds[name] 獲取生成配置的信息
最終返回了config配置對象


const builds = {
  ...
  // Runtime+compiler development build (Browser)
  'web-full-dev': {
    // 入口
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.js'),
    format: 'umd',
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
  },
}
resolve()

獲取入口和出口文件的絕對路徑ios

const aliases = require('./alias')
const resolve = p => {
  // 根據路徑中的前半部分去alias中找別名
  const base = p.split('/')[0]
  if (aliases[base]) {
    return path.resolve(aliases[base], p.slice(base.length + 1))
  } else {
    return path.resolve(__dirname, '../', p)
  }
}
// scripts/alias
const path = require('path')
const resolve = p => path.resolve(__dirname, '../', p)
module.exports = {
  ...
  web: resolve('src/platforms/web'),
}

最終結果web

把 src/platforms/web/entry-runtime-with-compiler.js (入口文件)構建成 dist/vue.js,若是設置 --sourcemap 會生成 vue.js.map
src/platform 文件夾下是 Vue 能夠構建成不一樣平臺下使用的庫,目前有 weex 和 web,還有服務器端渲染的庫vue-cli

Vue源碼分析
Vue調試方法

若是同時用render函數和template模板會輸出哪一個?
const vm = new Vue({
  el: '#app',
  template: '<h1>Hello Template</h1>',
  render(h) {
    return h('h1', 'Hello Render')
  }
})
根據源碼可分析:

// 保留 Vue 實例的 $mount 方法
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  // 非ssr狀況下爲 false,ssr 時候爲true
  hydrating?: boolean
): Component {
  // 獲取 el 對象
  el = el && query(el)

  /* istanbul ignore if */
  // el 不能是 body 或者 html
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }

  const options = this.$options
  
  // 這個if的做用
  // 若是options裏面沒有render函數,就把template/el轉換成render函數
  // 若是傳入的有render函數就不會進入這個if語句,就直接將調用mount渲染DOM
  if (!options.render) {
    ...
  }
  // 調用 mount 方法,渲染 DOM
  return mount.call(this, el, hydrating)
}

el不能是body或者html標籤,此處會進行判斷,若是設置了兩者會報錯
若是options裏面沒有render函數,就把template/el轉換成render函數
若是傳入的有render函數就不會進入這個if語句,就直接將調用mount渲染DOM
因此會輸出render函數中的內容

$mount 在哪裏調用?被什麼調用的?
經過代碼調試來探究
注意:

若是使用 npm run build來輸出文件,此時的dist/vue.js 中的最後一行是沒有 sourceMap 映射的://# sourceMappingURL=vue.js.map 會被清除,因此若是想在調試過程看到 src 源碼,須要從新運行 npm run dev 生成vue.js文件來開啓代碼地圖。

在調試窗口Call Stack調用堆棧位置能夠知道,咱們能夠看到方法調用的過程,當前執行的是 Vue.$mount 方法,再往下能夠看到 是vue調用的_init,Vue._init

經過代碼調試能夠發現$mount 是 _init() 調用的
而且證明了,若是 new Vue 同時設置了 template 和 render() ,此時只會執行 render()

Vue初始化過程
從導出vue的文件開始入手:

entry-runtime.js文件
這個文件裏是運行時的入口文件,導入了runtime.js文件並將其以Vue的名稱導出

entry-runtime-with-compiler.js文件
這個是帶有編譯器的入口文件,重寫了$mount方法增長了渲染模板的功能,核心是上述的那個if語句,將模板編譯成渲染函數
給Vue增長了一個compile靜態方法,用來編譯HTML語法轉換成render函數
引入了runtime中的index.js即:src/platform/web/runtime/index.js
設置 Vue.config
設置平臺相關的指令和組件
指令 v-model、v-show
組件 transition、transition-group

設置平臺相關的__patch__ 方法(打補丁方法,對比新舊的 VNode)
設置 $mount 方法,掛載 DOM

import config from 'core/config'
...
// install platform runtime directives & components
// 設置平臺相關的指令和組件(運行時)
// extend() 將第二個參數對象成員 拷貝到 第一個參數對象中去
// 指令 v-model、v-show
extend(Vue.options.directives, platformDirectives)
// 組件 transition、transition-group
extend(Vue.options.components, platformComponents)

// install platform patch function
// 設置平臺相關的 __patch__ 方法 (虛擬DOM 轉換成 真實DOM)
// 判斷是不是瀏覽器環境(是 - 直接返回, 非 - 空函數 noop
Vue.prototype.__patch__ = inBrowser ? patch : noop

// public mount method
// 設置 $mount 方法,掛載 DOM
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
src/platform/web/runtime/index.js 中又引用了 'core/index',即:src/core/index.js

platform是平臺相關的,而core文件夾中的代碼是和平臺無關的
定義了 Vue 的靜態方法
定義了服務端渲染的方法啥的
initGlobalAPI(Vue) 給vue的構造函數增長一些靜態方法
初始化Vue.config對象
定義Vue.set、Vue.delete、Vue.nextTick
定義Observerable,讓對象具備響應式

src/core/index.js 中引用了 './instance/index'

src/core/index.js 中引用了 './instance/index',即src/core/instance/index.js
定義了 Vue 的構造函數
設置 Vue 實例的成員

// 此處不用 class 的緣由是由於方便後續給 Vue 實例混入實例成員,構造函數配合原型更清晰些
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')
  }
  // 調用 _init() 方法
  this._init(options)
}
// 註冊 vm 的 _init() 方法,初始化 vm
initMixin(Vue)
// 註冊 vm 的 $data/$props/$set/$delete/$watch
stateMixin(Vue)
// 初始化事件相關方法
// $on/$once/$off/$emit
eventsMixin(Vue)
// 初始化生命週期相關的混入方法
// _update/$forceUpdate/$destroy
lifecycleMixin(Vue)
// 混入 render
// $nextTick/_render
renderMixin(Vue)

export default Vue
四個導出 Vue 的模塊
src/platforms/web/entry-runtime-with-compiler.js
web 平臺相關的入口
重寫了平臺相關的 $mount()方法
註冊了 Vue.compile() 方法,傳遞一個 HTML 字符串返回 render 函數

src/platform/web/runtime/index.js
web 平臺相關
註冊和平臺相關的全局指令:v-model、v-show
註冊和平臺相關的全局組件: v-transition、v-transition-group
全局方法:
patch:把虛擬 DOM 轉換成真實 DOM
$mount:掛載方法

src/core/index.js
與平臺無關
設置了 Vue 的靜態方法,initGlobalAPI(Vue)

src/core/instance/index.js
與平臺無關
定義了構造函數,調用了 this._init(options) 方法
給 Vue 中混入了經常使用的實例成員

Vue 靜態成員初始化
兩個問題
去掉vscode中的語法檢查問題
由於vscode和ts都是微軟開發的,默認類型檢查都是在ts文件中才能使用,而vue源碼使用的flow進行的類型檢查,可是vscode會報錯,認爲只能在ts文件中使用接口、類型檢查等語法
在設置的json文件中添加以下代碼

"javascript.validate.enable": false,  // 不對js代碼進行校驗

當解析到下面語法是會發生錯誤,致使這段代碼後面的代碼失去高亮顯示
解決辦法是在vscode裏面安裝babel JavaScript插件便可
雖然具有了高亮,可是會丟失ctrl+左擊跳轉的功能

Vue.observable = <T>(obj: T): T => {
    observe(obj)
    return obj
  }

靜態成員的初始化
經過 src/core/index.js 的 initGlobalAPI(Vue) 來到 初始化 Vue 的靜態方法 所在文件:global-api/index

import { initGlobalAPI } from './global-api/index'
...
// 註冊 Vue 的靜態屬性/方法
initGlobalAPI(Vue)
src/core/global-api/index.js
初始化 Vue 的靜態方法
initUse() : src/core/global-api/use.js
initMixin() : src/core/global-api/mixin.js
initExtend() : src/core/global-api/extend.js
initAssetRegisters() : src/core/global-api/assets.js




export function initGlobalAPI (Vue: GlobalAPI) {
  ...
  // 初始化 Vue.config 對象
  Object.defineProperty(Vue, 'config', configDef)

  // exposed util methods.
  // NOTE: these are not considered part of the public API - avoid relying on
  // them unless you are aware of the risk.
  // 這些工具方法不視做全局API的一部分,除非你已經意識到某些風險,不然不要去依賴他們
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }
  // 靜態方法 set/delete/nextTick
  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick

  // 2.6 explicit observable API
  // 讓一個對象可響應
  Vue.observable = <T>(obj: T): T => {
    observe(obj)
    return obj
  }
  // 初始化 Vue.options 對象,並給其擴展,全局的指令、過濾器都會存儲到這裏
  // components/directives/filters
  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })

  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  // 這是用來標識 "base "構造函數,在Weex的多實例方案中,用它來擴展全部普通對象組件
  Vue.options._base = Vue

  // 設置 keep-alive 組件
  extend(Vue.options.components, builtInComponents)

  // 註冊 Vue.use() 用來註冊插件
  initUse(Vue)
  // 註冊 Vue.mixin() 實現混入
  initMixin(Vue)
  // 註冊 Vue.extend() 基於傳入的options返回一個組件的構造函數
  initExtend(Vue)
  // 註冊 Vue.directive()、Vue.component()、Vue.filter() 這三個參數基本一致因此能夠一塊兒定義
  initAssetRegisters(Vue)
}

路徑中不加 ./ 是在src目錄下,若是加了./就表明是在同級目錄下
Vue.use方法的實現:
初始化時經過initUse方法來實現,initUse方法內部定義use方法,而後判斷當前傳入的插件參數是對象仍是函數
若是是對象就調用對象的install方法(這個是Vue文檔的約定,插件必須具備install方法)
若是是個函數就直接調用這個函數
最後要把當前做爲參數傳遞進來的插件放到Vue的數組installedPlugins裏面保存起來

Vue 實例成員初始化
在文件src/core/instance/index.js中進行的實例化
定義 Vue 的構造函數
初始化 Vue 的實例成員

initMixin() : src/core/instance/init.js
stateMixin() : src/core/instance/state.js
eventsMixin() : src/core/instance/state.js
lifecycleMixin() : src/core/instance/state.js
renderMixin() : src/core/instance/state.js


// 此處不用 class 的緣由是由於方便後續給 Vue 實例混入實例成員
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')
  }
  // 調用 _init() 方法
  this._init(options)
}

// 註冊 vm 的 _init() 方法,初始化 vm
initMixin(Vue)

// 註冊 vm 的 $data/$props/$set/$delete/$watch
stateMixin(Vue)

// 初始化事件相關方法
// $on/$once/$off/$emit
eventsMixin(Vue)

// 初始化生命週期相關的混入方法
// _update/$forceUpdate/$destroy
lifecycleMixin(Vue)

// 混入 render
// $nextTick/_render
// $nextTick/_render
renderMixin(Vue)


實例成員 - init
initMixin(Vue)
註冊 vm 的 _init() 方法,初始化 vm
src/core/instance/init.js
可參考Vue 實例 文檔


export function initMixin (Vue: Class<Component>) {
  // 給 Vue 實例增長 _init() 方法
  // 合併 options / 初始化操做
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to avoid this being observed
    // 若是是 Vue 實例不須要被 observe
    vm._isVue = true
    // merge options
    // 合併 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 {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
      
    // vm 的生命週期相關變量初始化
    // $children/$parent/$root/$refs
    initLifecycle(vm)
      
    // vm 的事件監聽初始化, 父組件綁定在當前組件上的事件
    initEvents(vm)
      
    // vm 的編譯render初始化
    // $slots/$scopedSlots/_c/$createElement/$attrs/$listeners
    initRender(vm)
      
    // beforeCreate 生命鉤子的回調
    callHook(vm, 'beforeCreate')
      
    // 把 inject 的成員注入到 vm 上,實現依賴注入
    initInjections(vm) // resolve injections before data/props
      
    // 初始化 vm 的 _props/methods/_data/computed/watch
    initState(vm)
      
    // 初始化 provide
    initProvide(vm) // resolve provide after data/props
      
    // created 生命鉤子的回調
    callHook(vm, 'created')

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }
    // 調用 $mount() 掛載
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

咱們在render函數時調用的h函數其實就是$createElement 方法來把虛擬DOM轉換成真實DOM
實例成員 - initState
initState(vm)
初始化 vm 的 _props/methods/_data/computed/watch
src/core/instance/state.js
可參考Vue 實例 文檔

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  // 將props成員轉換成響應式數據,並注入到vue實例
  if (opts.props) initProps(vm, opts.props)
  // 初始化選項中的方法(methods)
  if (opts.methods) initMethods(vm, opts.methods)
  // 數據的初始化
  if (opts.data) {
    // 把data中的成員注入到Vue實例 並轉換爲響應式對象
    initData(vm)
  } else {
    // observe數據的響應式處理
    observe(vm._data = {}, true /* asRootData */)
  } 
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

在初始化data和props時會進行判斷,兩者之間有沒有重名的屬性,是不容許有重名的屬性的

初始化過程調試
初始化過程調試代碼
設置斷點

src/core/instance/index.js
initMixin(Vue)


src/core/index.js
initGlobal(Vue)


src/platforms/web/runtime/index.js
Vue.config.mustUseProp


src/platforms/web/entry-runtime-with-compiler.js
const mount = Vue.prototype.$mount

開始調試

F5 刷新瀏覽器卡到斷點
首先進入core/instance/index.js,core中的代碼與平臺無關的,在這裏調用了Mixin的一些函數,這些函數裏面給Vue的原型上增長了一些實例成員
經過initMixin(Vue)用來初始化Vue的實例vm
經過_init() 函數,初始化vm實例,判斷當前是否爲組件,合併Vue自身及用戶提供的options
經過stateMixin(),新增 $data / $props / $set / $delete / $watch 幾個成員。可是 $data 和 $props 此時都是 undefined,僅僅初始化了這兩個屬性,未來須要經過選項去賦值
經過函數 eventsMixin(),初始化事件相關的四個方法 $on / $once / $off / $emit
經過函數 lifecycleMixin(),它註冊了根生命週期相關的方法 _update / $forceUpdate / $destroy。其中_updata內部調用了 patch 方法,把 VNode 渲染成真實的 DOM
經過函數renderMixin(),其執行事後,會給原型掛載一些 _ 開頭的方法,這些方法的做用是當咱們把模板轉換成 render函數的時候,在render函數中調用,除此以外還註冊了 $nextTick / _render, _render的做用是調用用戶定義 或 模板渲染的render函數

F8 跳轉到下一個導出Vue的文件core/index.js,這個文件中執行了 initGlobalAPI(),給Vue的構造函數初始化了靜態成員
進入 initGlobalAPI(),F10執行到初始化 Vue.config 對象的地方,Vue的構造函數新增 config屬性,這是一個對象,而且這個對象已經初始化了不少內容,這些內容是經過給config對象增長一個get方法,在get方法中返回了../config中導入的config
繼續F10執行三個靜態方法 set / delete / nextTick
F10 初始化 observable
繼續F10 初始化 options對象,但此時options對象爲空,由於是經過Object.create(null)來初始化的,沒有原型,繼續F10 增添全局組件、指令以及過濾器 components / directives / filters,再F10初始化_base即Vue
F10爲options.compents設置keep-alive組件
F10 初始化靜態方法 Vue.use()、Vue.mixin()、Vue.extend()。以及Vue.directive()、Vue.component()、Vue.filter(),它們是用來註冊全局的指令、組件和過濾器,咱們調用這些方法的時候,它們會把指令、組件和過濾器分別幫咱們註冊到Vue.options中對應的屬性裏面來

再按F8進入platforms/web/runtime/index.js,此時咱們看到的代碼都是與平臺相關的,它首先給Vue.config中註冊了一些與平臺相關的一些公共的方法,當它執行完事後 又註冊了幾個與平臺相關的指令和組件
F10將其執行完觀察指令和組件的變化
繼續F10給Vue原型上註冊了patch和$mount,其執行是在Vue._init中調用的

F8 進入到最後一個文件platforms/web/runtime/entry-runtime-with-compiler.js的斷點,這個文件重寫了$mount,新增了把模板編譯成render函數的功能
在文件最後給Vue構造函數掛載了compile方法,這個方法的做用是讓咱們手共把模板轉換成render函數

Vue首次渲染
首次渲染過程調試
Vue 初始化完畢,開始真正的執行
調用 new Vue() 以前,已經初始化完畢
經過調試[代碼,記錄首次渲染過程
首次渲染過程:

數據響應式原理
響應式處理入口
整個響應式處理的過程是比較複雜的,下面咱們先從

src\core\instance\init.js
initState(vm) vm 狀態的初始化
初始化 vm 的 _props、methods、_data等




src\core\instance\state.js
export function initState (vm: Component) {
  ...
  // 數據的初始化
  if (opts.data) {
    // 把data中的成員進行遍歷注入到Vue實例,最後再調用observe方法將data換爲響應式對象
    initData(vm)
  } else {
    // observe數據的響應式處理入口,建立了observer對象
    observe(vm._data = {}, true /* asRootData */)
  }
  ...
}
initData代碼:vm 數據的初始化
function initData (vm: Component) {
  let data = vm.$options.data
  // 初始化 _data,組件中 data 是函數,調用函數返回結果
  // 不然直接返回 data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  ...
  // proxy data on instance
  // 獲取 data 中的全部屬性
  const keys = Object.keys(data)
  // 獲取 props / methods
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  // 判斷data成員是否和props/methods重名,若是重名開發模式會發送警告
  ...
  
  // observe data
  // 數據的響應式處理
  observe(data, true /* asRootData */)
}


src\core\observer\index.js
observe(value, asRootData)

目的是做爲響應式入口,負責爲每個 Object 類型的 value 建立一個 observer 實例

此處的__ob__定義在observer類中,起到一個緩存的功能,若是value這個對象已經存在這個__ob__,就說明這個對象已經進行了響應式處理,那麼直接返回ob,ob是value中的一個屬性,裏面存在當前對象對應的new observer實例

/**
 * Attempt to create an observer instance for a value,
 * returns the new observer if successfully observed,
 * or the existing observer if the value already has one.
 */
// 試圖爲一個value建立一個observer觀察者實例,
// 若是建立成功,則返回新的觀察者實例
// 若是該值已經有觀察者,則返回現有的觀察者
export function observe (value: any, asRootData: ?boolean): Observer | void {
  // 首先判斷 value 是不是對象 是不是 VNode虛擬DOM 的實例
  // 若是它不是對象或者是VNode實例,那麼就不須要作響應式處理 直接返回
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  // 若是 value 有 __ob__(observer對象) 屬性
  // 判斷 value.__ob__ 屬性是不是 observer 的實例
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    // 賦值 最終返回
    ob = value.__ob__
  } else if (
    // 判斷是否能進行響應式處理
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    // 建立一個 Observer 對象
    ob = new Observer(value)
  }
  // 處理爲根數據
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}


Observer
src\core\observer\index.js
定義了三個屬性
value -- 被觀察的對象
dep -- dep依賴對象
vmCount -- 計數器

定義了walk方法,遍歷每個屬性,頂用defineReactive設置爲響應式數據
對對象作響應化處理
對數組作響應化處理
觀察者類,附加到每一個被觀察對象上一旦被附加,觀察者就會將目標對象的屬性轉換爲getter/setter,以收集依賴關係並派發更新(發送通知)

/**
 * Observer class that is attached to each observed
 * object. Once attached, the observer converts the target
 * object's property keys into getter/setters that
 * collect dependencies and dispatch updates.
 */
// 觀察者類,附加到每一個被觀察對象上
// 一旦被附加,觀察者就會將目標對象的屬性轉換爲getter/setter,
// 以收集依賴關係並派發更新
export class Observer {
  // 觀測對象
  value: any;
  // 依賴對象
  dep: Dep;
  // 實例計數器
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    // 初始化實例的 vmCount 爲0
    this.vmCount = 0
    // def 調用 defineProperty 默認不可枚舉
    // 將實例掛載到觀察對象的 __ob__ 屬性,這個__ob__是不能夠枚舉的
    def(value, '__ob__', this)
    // 數組的響應式處理
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      // 爲數組中的每個對象建立一個 observer 實例
      this.observeArray(value)
    } else {
      // 遍歷對象中的每個屬性,轉換成 setter/getter
      this.walk(value)
    }
  }

  /**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  // 遍歷全部屬性,並將它們轉換爲getter/setter
  // 只有當值類型爲Object時,才應調用此方法
  walk (obj: Object) {
    // 獲取觀察對象的每個屬性
    const keys = Object.keys(obj)
    // 遍歷每個屬性,設置爲響應式數據
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  /**
   * Observe a list of Array items.
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}
walk(obj)

遍歷 obj 的全部屬性,爲每個屬性調用 defineReactive() 方法,設置 getter/setter

爲data對象設置的__ob__是不能夠枚舉的,緣由是後續須要遍歷data中的因此成員定義響應式,而ob的做用僅是用來存貯當前對象對應的observer實例對象的,不須要在遍歷時定義響應式,因此定義成不可枚舉,因此遍歷時忽視它,不爲其定義響應式
對象響應式處理 defineReactive
src\core\observer\index.js
defineReactive(obj, key, val, customSetter, shallow)
爲一個對象定義一個響應式的屬性,每個屬性對應一個 dep 對象
若是該屬性的值是對象,繼續調用 observe
若是給屬性賦新值,繼續調用 observe
若是數據更新發送通知

依賴收集
在 defineReactive() 的 getter 中實例化 dep 對象,並判斷 Dep.target 是否有值,這個target就是watcher對象,若是有, 調用 dep.depend(),它內部最調用 dep.addSub(this),把 watcher 對象添加到 dep.subs.push(watcher) 中,也就是把訂閱者添加到 dep 的 subs 數組中,當數據變化的時候調用 watcher 對象的 update() 方法
Dep.target的設置時機
當首次渲染時調用 mountComponent() 方法的時候,建立了渲染 watcher 對象,執行 watcher 中的 get() 方法
get() 方法內部調用 pushTarget(this),把當前 Dep.target = watcher,同時把當前 watcher 入棧,由於有父子組件嵌套的時候先把父組件對應的 watcher 入棧,再去處理子組件的 watcher,子組件的處理完畢後,再把父組件對應的 watcher 出棧,繼續操做
Dep.target 用來存放目前正在使用的watcher。全局惟一,而且一次也只能有一個 watcher 被使用

/**
 * Define a reactive property on an Object.
 */
// 爲一個對象定義一個響應式的屬性
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 建立依賴對象實例
  const dep = new Dep()
  // 獲取 obj 的屬性描述符對象
  const property = Object.getOwnPropertyDescriptor(obj, key)
  // 經過 configurable 指定當前屬性是否爲可配置的
  // 若是爲不可配置 意味不可用delete刪除而且不能用defineReactvie從新定義 直接返回
  if (property && property.configurable === false) {
    return
  }
  // 提供預約義的存取器函數
  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  // 參數爲兩個時 獲取value
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
  // 判斷是否遞歸觀察子對象,並將子對象屬性都轉換成 getter/setter,返回子觀察對象
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      // 若是預約義的 getter 存在則 value 等於getter 調用的返回值
      // 不然直接賦予屬性值
      const value = getter ? getter.call(obj) : val
      // 若是存在當前依賴目標,即 watcher 對象,則創建依賴
      if (Dep.target) {
        ...
      }
      // 返回屬性值
      return value
    },
    set: function reactiveSetter (newVal) {
      // 若是預約義的 getter 存在則 value 等於getter 調用的返回值
      // 不然直接賦予屬性值
      const value = getter ? getter.call(obj) : val
      // 若是新值等於舊值或者新值舊值爲NaN則不執行
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // 若是沒有 setter 直接返回
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      // 若是預約義setter存在則調用,不然直接更新新值
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // 若是新值是對象,觀察子對象並返回 子的 observer 對象
      childOb = !shallow && observe(newVal)
      // 派發更新(發佈更改通知)
      dep.notify()
    }
  })
}

Dep
依賴對象
記錄 watcher 對象
depend() -- watcher 記錄對應的 dep
發佈通知

let uid = 0
// dep 是個可觀察對象,能夠有多個指令訂閱它
/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
export default class Dep {
  // 靜態屬性,watcher 對象
  static target: ?Watcher;
  // dep 實例 Id
  id: number;
  // dep 實例對應的 watcher 對象/訂閱者數組
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }

  // 添加新的訂閱者 watcher 對象
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  // 移除訂閱者
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  // 將觀察對象和 watcher 創建依賴
  depend () {
    if (Dep.target) {
      // 若是 target 存在,把 dep 對象添加到 watcher 的依賴中
      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)
    }
    // 調用每一個訂閱者的update方法實現更新
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
// Dep.target 用來存放目前正在使用的watcher
// 全局惟一,而且一次也只能有一個watcher被使用
// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []
// 將watcher放入棧中,入棧並將當前 watcher 賦值給 Dep.target
// 父子組件嵌套的時候先把父組件對應的 watcher 入棧,
// 再去處理子組件的 watcher,子組件的處理完畢後,再把父組件對應的 watcher 出棧,繼續操做
export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget () {
  // 出棧操做
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

數組響應式處理

src\core\observer\index.js
Observer 的構造函數中
// 獲取 arrayMethods 特有的成員 返回的是包含名字的數組
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

export class Observer {
  ...
  constructor (value: any) {
    ...
    // 數組的響應式處理
    if (Array.isArray(value)) {
      // 判斷當前瀏覽器是否支持對象的原型屬性
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      // 爲數組中的每個對象建立一個 observer 實例
      this.observeArray(value)
    } else {
      ...
    }
  }
  /**
 * Augment a target Object or Array by intercepting
 * the prototype chain using __proto__
 */
  // 經過使用__proto__攔截原型鏈來加強目標對象或數組
  function protoAugment (target, src: Object) {
    /* eslint-disable no-proto */
    target.__proto__ = src
    /* eslint-enable no-proto */
  }

  /**
   * Augment a target Object or Array by defining
   * hidden properties.
   */
  // 經過定義隱藏屬性來加強目標對象或數組 
  /* istanbul ignore next */
  function copyAugment (target: Object, src: Object, keys: Array<string>) {
    for (let i = 0, l = keys.length; i < l; i++) {
      const key = keys[i]
      def(target, key, src[key])
    }
  }
}

對數組方法進行修補
數組中的某些方法會改變原數組中的元素,數組默認的方法時不會觸發setter的,因此須要進行修補,當數組調用了這些方法時調用時就調用dep.notify來通知watcher來更新視圖
還有就是調用observeArray方法,遍歷數組中的元素將數組中的元素轉換成響應式的

能使頁面中視圖更新的方法有:
vm.arr.push(100) 這個能夠,由於已經重寫了vue組件中的數組方法
vm.arr[0] = 100 這個不能夠(數據發生了變化,可是視圖不會同步更新),由於源碼並無處理數組對象自己的屬性(arr[0]其實就是調用arr的特殊屬性名0),由於數組對象自己有很是多的屬性,若是都處理了可能會帶來性能上的過分開銷
vm.arr.length = 0 這個也不能夠更新視圖,緣由同上
若是要清空數組或者更改數組中的某一個元素的話,可使用splice方法來實現
splice(0) --- 表示清空數組
splice (0, 1, 100) --- 表示在索引爲0的位置,刪除1個元素,替換成100

Watcher
Watcher 分爲三種,Computed Watcher(計算屬性)、用戶 Watcher (偵聽器)、渲染 Watcher,前兩種是在initState階段初始化的
渲染 Watcher 的建立時機
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) {
    ...
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  // 咱們在watcher的構造函數中設置爲vm._watcher,
  // 由於watcher的初始補丁可能會調用$forceUpdate(例如在子組件的掛載鉤子中),
  // 這依賴於vm._watcher已經被定義  
  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
}

整個流程概述:
當數據發生變化後,經過setter調用dep的notify方法去通知watcher
會先把watcher放到一個隊列裏面,而後遍歷這個隊列調用每一個watcher的run方法
最後run方法會調用渲染watcher的updateComponent方法

響應式處理過程總結
詳見腦圖

實例方法
vm.$set
功能
向響應式對象中添加一個屬性,並確保這個新屬性一樣是響應式的,且觸發視圖更新。它必須用於向響應式對象上添加新屬性,由於 Vue 沒法探測普通的新增屬性 (好比 this.myObject.newProperty = 'hi')

實例
// 使用vm.$set或者Vue.set均可以,一個是靜態方法,一個是實例方法
vm.$set(obj, 'foo', 'test') // 參數:對象,屬性名,屬性值
注意:不能給 Vue 實例,或者 Vue 實例的根數據對象($data)添加響應式對象,這樣會報錯,而且實現不了響應式。

Vue.set()
src/core/global-api/index.js


  // 靜態方法 set/delete/nextTick
  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick
vm.$set()
src/core/instance/index.js
src/core/instance/state.js


// instance/index.js
// 註冊 vm 的 $data/$props/$set/$delete/$watch
stateMixin(Vue)

// instance/state.js
Vue.prototype.$set = set
Vue.prototype.$delete = del

源碼

set()方法

src/core/observer/index.js
/**
 * Set a property on an object. Adds the new property and
 * triggers change notification if the property doesn't
 * already exist.
 */
// 設置對象的屬性。添加新的屬性,若是該屬性不存在,則觸發更改通知
export function set (target: Array<any> | Object, key: any, val: any): any {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  // 判斷 target 是不是數組,key 是不是合法的索引
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    // 判斷當前key和數組length的最大值給length
    // 當咱們調用$set傳遞的索引有可能超過數組的length屬性
    target.length = Math.max(target.length, key)
    // 經過 splice 對key位置的元素進行替換
    // splice 在 array.js 進行了響應化的處理
    target.splice(key, 1, val)
    return val
  }
  // 若是 key 在對象中已經存在且不是原型成員 直接賦值
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  // 獲取 target 中的 observer 對象
  const ob = (target: any).__ob__
  // 若是 target 是 vue 實例或者 $data 直接返回
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  // 若是 ob 不存在,target 不是響應式對象直接賦值
  if (!ob) {
    target[key] = val
    return val
  }
  // 若是 ob 存在,把 key 設置爲響應式屬性
  defineReactive(ob.value, key, val)
  // 發送通知
  ob.dep.notify()
  return val
}

set方法會處理數組的響應式也會處理對象的響應式
給數組設置值的時候會調用splice方法
給對象增長新的成員時會調用defineReactive方法,最終調用ob.dep.notify方法發送通知

vm.$delete
功能
刪除對象的屬性。若是對象是響應式的,確保刪除能觸發更新視圖。這個方法主要用於避開 Vue 不能檢測到屬性被刪除的限制,可是你應該不多會使用它。

注意:對象不能是 Vue 實例,或者 Vue 實例的根數據對象。

實例
 vm.$delete(vm.obj, 'msg')        // 刪除obj對象中的msg,並更新到視圖上


定義位置

Vue.delete()
src/core/global-api/index.j


  // 靜態方法 set/delete/nextTick
  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick


vm.$delete()
src/core/instance/index.js
src/core/instance/state.js


// instance/index.js
// 註冊 vm 的 $data/$props/$set/$delete/$watch
stateMixin(Vue)

// instance/state.js
Vue.prototype.$set = set
Vue.prototype.$delete = del


源碼

delete()方法
src/core/observer/index.js


/**
 * Delete a property and trigger change if necessary.
 */
// 刪除一個屬性並在必要時觸發更改
export function del (target: Array<any> | Object, key: any) {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  // 判斷是不是數組,以及 key 是否合法
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    // 若是是數組經過 splice 刪除
    // splice 作過響應式處理
    target.splice(key, 1)
    return
  }
  // 獲取 target 的 ob 對象
  const ob = (target: any).__ob__
  // target 若是是 Vue 實例或者 $data 對象,直接返回
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid deleting properties on a Vue instance or its root $data ' +
      '- just set it to null.'
    )
    return
  }
  // 若是 target 對象沒有 key 屬性直接返回
  if (!hasOwn(target, key)) {
    return
  }
  // 刪除屬性
  delete target[key]
  // 判斷是不是響應式的
  if (!ob) {
    return
  }
  // 經過 ob 發送通知
  ob.dep.notify()
}

vm.$watch
vm.$watch( expOrFn, callback, [options] ) // expOrFn是data中的屬性,callback是data中屬性變化時的回調,[options]是配置對象
功能
觀察 Vue 實例變化的一個表達式或計算屬性函數。回調函數獲得的參數爲新值和舊值。表達式只接受監督的鍵路徑。對於更復雜的表達式,用一個函數取代。

參數
expOrFn:要監視的 $data 中的屬性,能夠是表達式或函數
callback:數據變化後執行的函數
函數:回調函數
對象:具備 handler 屬性(字符串或者函數),若是該屬性爲字符串則 methods 中相應的定義

options:可選的選項
deep:布爾類型,深度監聽,能夠監聽子屬性的變化
immediate:布爾類型,是否當即執行一次回調函數

示例

const vm = new Vue({
  el: '#app',
  data: {
      a: '1',
      b: '2',
      msg: 'Hello vue',
    user: {
      firstName: '諸葛',
      lastName: '亮'
    }
  }
})
// expOrFn 是表達式
vm.$watch('msg', function (newVal, oldVal) {
  onsole.log(newVal, oldVal)
})
vm.$watch('user.firstName', function (newVal, oldVal) {
  console.log(newVal)
})
// expOrFn 是函數
vm.$watch(function () {
  return this.a + this.b
}, function (newVal, oldVal) {
  console.log(newVal)
})
// deep 是 true,消耗性能
vm.$watch('user', function (newVal, oldVal) {
  // 此時的 newVal 是 user 對象
  console.log(newVal === vm.user)
}, {
  deep: true  
 // 若是不設置這個deep,那麼將會致使只要當user對象變化時纔會更新視圖(好比將user置空),user對象中的屬性變化不會更新視圖
})
// immediate 是 true
vm.$watch('msg', function (newVal, oldVal) {
  console.log(newVal)
}, {
  immediate: true
})

三種類型的 Watcher 對象

沒有靜態方法,由於 $watch 方法中要使用 Vue 的實例
Watcher 分三種:計算屬性 Watcher、用戶 Watcher (偵聽器)、渲染 Watcher
建立順序:計算屬性 Watcher(id:1)、用戶 Watcher (偵聽器 id:2)、渲染 Watcher(id:3)
執行順序:按照 id 從小到大排序,與建立順序相同
watcher 實例

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>watcher</title>
</head>
<body>
  <div id="app">
    {{ reversedMessage }}
    <hr>
    {{ user.fullName }}
  </div>

  <script src="../../dist/vue.js"></script>
  <script>
    const vm = new Vue({
      el: '#app',
      data: {
        message: 'Hello Vue',
        user: {
          firstName: '諸葛',
          lastName: '亮',
          fullName: ''
        }
      },
      computed: { // 計算屬性
        reversedMessage: function () {
          return this.message.split('').reverse().join('')
        }
      },
      watch: {
        // 偵聽器寫法1:  // 這種寫法不容易設置配置對象
        // 'user.firstName': function (newValue, oldValue) {
        //   this.user.fullName = this.user.firstName + ' ' + this.user.lastName
        // },
        // 'user.lastName': function (newValue, oldValue) {
        //   this.user.fullName = this.user.firstName + ' ' + this.user.lastName
        // },
        // 偵聽器寫法2:
        'user': {
          handler: function (newValue, oldValue) {
            this.user.fullName = this.user.firstName + ' ' + this.user.lastName
          },
          deep: true,
          immediate: true
        }
      }
    })
  </script>
</body>
</html>


vm.$watch()
src\core\instance\state.js


Vue.prototype.$watch = function (
  expOrFn: string | Function,
  cb: any,
  options?: Object
): Function {
  // 獲取 Vue 實例 this
  const vm: Component = this
  if (isPlainObject(cb)) {
    // 判斷若是 cb 是對象執行 createWatcher
    return createWatcher(vm, expOrFn, cb, options)
  }
  options = options || {}
  // 標記爲用戶 watcher
  options.user = true
  // 建立用戶 watcher 對象
  const watcher = new Watcher(vm, expOrFn, cb, options)
  // 判斷 immediate 若是爲 true
  if (options.immediate) {
    // 當即執行一次 cb 回調,而且把當前值傳入
    try {
      cb.call(vm, watcher.value)
    } catch (error) {
      handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
    }
  }
  // 返回取消監聽的方法
  return function unwatchFn () {
    watcher.teardown()
  }
}

異步更新隊列 nextTick()
是在數據變化後,數據更新到DOM上以後纔會去執行nextTick中傳遞的這個回調函數
因爲Vue 更新 DOM 是異步執行的、批量的,因此當修改DOM中的數據後當即緊接着就去獲取或者操做剛剛修改的數據是獲取不到的,仍是以前的老數據。
爲了解決這個問題,全部引入了$nextTick方法,來實如今下次 DOM 更新循環結束以後執行延遲執行$nextTick中的回調。使得在修改數據以後當即使用這個方法,也能獲取更新後的 DOM

vm.$nextTick(function () { /* 操做 DOM */ }) / Vue.nextTick(function () {})
示例代碼
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>nextTick</title>
</head>
<body>
  <div id="app">
    <p id="p" ref="p1">{{ msg }}</p>
    {{ name }}<br>
    {{ title }}<br>
  </div>
  <script src="../../dist/vue.js"></script>
  <script>
    const vm = new Vue({
      el: '#app',
      data: {
        msg: 'Hello nextTick',
        name: 'Vue.js',
        title: 'Title'
      },
      // 當屬性的值被改變以後不會當即更新DOM
      // 這個更新是一個異步的過程,
      // 此時若是當即獲取p標籤的內容是獲取不到的
      mounted() {
        this.msg = 'Hello World'
        this.name = 'Hello snabbdom'
        this.title = 'Vue.js'
  
        // 若是想獲取界面上最新的值
        Vue.nextTick(() => {
          console.log(this.$refs.p1.textContent)
        })
      }
    })

  </script>
</body>
</html>


定義位置

src\core\instance\render.js
  Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this)
  }

源碼

手動調用 vm.$nextTick()
在 Watcher 的 queueWatcher 中執行 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]()
  }
}

// Here we have async deferring wrappers using microtasks.
// In 2.5 we used (macro) tasks (in combination with microtasks).
// However, it has subtle problems when state is changed right before repaint
// (e.g. #6813, out-in transitions).
// Also, using (macro) tasks in event handler would cause some weird behaviors
// that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
// So we now use microtasks everywhere, again.
// A major drawback of this tradeoff is that there are some scenarios
// where microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690, which have workarounds)
// or even between bubbling of the same event (#6566).
// 在這裏,咱們有使用微任務的異步延遲包裝器。
// 在2.5中,咱們使用了(宏)任務(與微任務相結合)。
// 然而,當狀態在重繪以前就被改變時,它有微妙的問題。
// 另外,在事件處理程序中使用(宏)任務會致使一些奇怪的行爲。
// 另外,在事件處理程序中使用(宏)任務會致使一些奇怪的行爲。
// 因此咱們如今又處處使用微任務。
// 這種權衡的一個主要缺點是,有些狀況下,
// 微任務的優先級過高,在所謂的順序事件之間開火,甚至在同一事件的冒泡之間開火
let timerFunc

// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
// nextTick行爲利用了微任務隊列,
// 能夠經過原生的Promise.then或MutationObserver訪問
// MutationObserver有更普遍的支持,然而在iOS >= 9.3.3的UIWebView中,
// 當在觸摸事件處理程序中觸發時,它有嚴重的bug。
// 觸發幾回後就徹底中止工做了......因此,若是原生Promise可用,咱們會使用它。
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    // In problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    // 在有問題的UIWebViews中,Promise.then並無徹底break,
    // 但它可能會卡在一個奇怪的狀態,即回調被推送到微任務隊列中,
    // 但隊列並無被刷新,直到瀏覽器須要作一些其餘工做,例如處理一個計時器
    // 所以,咱們能夠經過添加一個空的定時器來 "強制 "微任務隊列被刷新。
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // Use MutationObserver where native Promise is not available,
  // e.g. PhantomJS, iOS7, Android 4.4
  // (#6466 MutationObserver is unreliable in IE11)
  // 在沒有本地Promise的地方使用MutationObserver
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // Fallback to setImmediate.
  // Technically it leverages the (macro) task queue,
  // but it is still a better choice than setTimeout.
  // 降級到setImmediate
  // 從技術上講,它利用了(宏)任務隊列,
  // 但它仍然是比setTimeout更好的選擇
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  // 降級到 setTimeout
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // callbacks 存儲全部的回調函數
  // 把 cb 加上異常處理存入 callbacks 數組中
  callbacks.push(() => {
    if (cb) {
      try {
        // 調用 cb()
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  // 判斷隊列是否正在被處理
  if (!pending) {
    pending = true
    // 調用
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    // 返回 promise 對象
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}
相關文章
相關標籤/搜索