學(xué)習(xí)筆記(十五)Vue.js源碼剖析 - 響應(yīng)式原理

Vue.js 源碼剖析 - 響應(yīng)式原理

準(zhǔn)備工作

Vue源碼獲取

這里主要分析 Vue 2.6版本的源碼率翅,使用Vue 3.0版本來開發(fā)項(xiàng)目還需要一段時(shí)間的過渡

  • 項(xiàng)目地址:

  • Fork一份到自己倉庫,克隆到本地炼幔,這樣可以自己寫注釋提交鸽心,如果直接從github clone太慢滚局,也可以先導(dǎo)入gitee,再從gitee clone到本地

  • 查看Vue源碼的目錄結(jié)構(gòu)

    src
    compiler 編譯相關(guān)
    core Vue 核心庫
    platforms 平臺(tái)相關(guān)代碼(web顽频、weex)
    server SSR 服務(wù)端渲染
    sfc 單文件組件 .vue 文件編譯為 js 對(duì)象
    shared 公共代碼

Flow

Vue 2.6 版本中使用了Flow藤肢,用于代碼的靜態(tài)類型檢查

Vue 3.0+ 版本已使用TypeScript進(jìn)行開發(fā),因此不再需要Flow

  • Flow是JavaScript的靜態(tài)類型檢查器

  • Flow的靜態(tài)類型檢查是通過靜態(tài)類型推斷來實(shí)現(xiàn)的

    • 安裝Flow以后糯景,在要進(jìn)行靜態(tài)類型檢查的文件頭中通過 // @flow/* @flow */ 注釋的方式來聲明啟用

    • 對(duì)于變量類型的指定携狭,與TypeScript類似

      /* @flow */
      function hello (s: string): string {
          return `hello ${s}`
      }
      hello(1) // Error
      

打包與調(diào)試

Vue 2.6中使用Rollup來打包

  • 打包工具Rollup

    • Rollup比webpack輕量
    • Rollup只處理js文件锰霜,更適合用來打包工具庫
    • Rollup打包不會(huì)生成冗余的代碼
  • 調(diào)試

    • 執(zhí)行yarn安裝依賴(有yarn.lock文件)
    • package.json文件dev script中添加 --sourcemap 參數(shù)來開啟sourcemap,以便調(diào)試過程中查看代碼

Vue不同構(gòu)建版本的區(qū)別

執(zhí)行yarn build可以構(gòu)建所有版本的打包文件

UMD CommonJS ES Module
Full vue.js vue.common.js vue.esm.js
Runtime-only vue.runtime.js vue.runtime.common.js vue.runtime.esm.js
Full(production) vue.min.js
Runtime-only(production) vue.runtime.min.js

不同版本之間的區(qū)別

  • 完整版:同時(shí)包含編譯器運(yùn)行時(shí)的版本
    • 編譯器:用來將模板字符串編譯為JavaScript渲染(render)函數(shù)的代碼,體積大引颈、效率低
    • 運(yùn)行時(shí):用來創(chuàng)建Vue實(shí)例,渲染并處理虛擬DOM等的代碼回季,體積小轮傍、效率高,即去除編譯器之后的代碼
    • 簡(jiǎn)單來說策治,運(yùn)行時(shí)版本不包含編譯器的代碼脓魏,無法直接使用template模板字符串,需要自行使用render函數(shù)
    • 通過Vue Cli創(chuàng)建的項(xiàng)目通惫,使用的是vue.runtime.esm.js版本
  • 運(yùn)行時(shí)版:只包含運(yùn)行時(shí)的版本
  • UMD:通用模塊版本茂翔,支持多種模塊方式。vue.js默認(rèn)就是運(yùn)行時(shí)+編譯器的UMD版本
  • CommonJS:CommonJS模塊規(guī)范的版本履腋,用來兼容老的打包工具珊燎,例如browserfy、webpack 1等
  • ES Module:從2.6版本開始,Vue會(huì)提供兩個(gè)esm構(gòu)建文件俐末,是為現(xiàn)代打包工具提供的版本
    • esm格式被設(shè)計(jì)為可以被靜態(tài)分析料按,所以打包工具可以利用這一點(diǎn)來進(jìn)行tree-shaking,精簡(jiǎn)調(diào)未被用到的代碼

尋找入口文件

通過查看構(gòu)建過程卓箫,來尋找對(duì)應(yīng)構(gòu)建版本的入口文件位置

  • 以vue.js版本的構(gòu)建為例载矿,通過rollup進(jìn)行構(gòu)建,指定了配置文件scripts/config.js烹卒,并設(shè)置了環(huán)境變量TARGET:web-full-dev

    "dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev",

  • 進(jìn)一步查看 scripts/config.js 配置文件

    module.exports導(dǎo)出的內(nèi)容來自genConfig()函數(shù)闷盔,并接收了環(huán)境變量TARGET作為參數(shù)

    // scripts/config.js
    if (process.env.TARGET) {
      module.exports = genConfig(process.env.TARGET)
    } else {
      exports.getBuild = genConfig
      exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
    }
    

    genConfig函數(shù)組裝生成了config配置對(duì)象,入口文件配置input: opts.entry旅急,配置項(xiàng)的值opts.entry來自builds[name]

    // scripts/config.js
    function genConfig (name) {
      const opts = builds[name]
      const config = {
        input: opts.entry,
        external: opts.external,
        plugins: [
          flow(),
          alias(Object.assign({}, aliases, opts.alias))
        ].concat(opts.plugins || []),
        output: {
          file: opts.dest,
          format: opts.format,
          banner: opts.banner,
          name: opts.moduleName || 'Vue'
        },
        onwarn: (msg, warn) => {
          if (!/Circular/.test(msg)) {
            warn(msg)
          }
        }
      }
      ...
    }
    

    通過傳入環(huán)境變量TARGET的值逢勾,可以找到web-full-dev相應(yīng)的配置,入口文件是web/entry-runtime-with-compiler.js

    const builds = {
      // Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify
      'web-runtime-cjs-dev': {
        entry: resolve('web/entry-runtime.js'),
        dest: resolve('dist/vue.runtime.common.dev.js'),
        format: 'cjs',
        env: 'development',
        banner
      },
      'web-runtime-cjs-prod': {
        entry: resolve('web/entry-runtime.js'),
        dest: resolve('dist/vue.runtime.common.prod.js'),
        format: 'cjs',
        env: 'production',
        banner
      },
      // Runtime+compiler CommonJS build (CommonJS)
      'web-full-cjs-dev': {
        entry: resolve('web/entry-runtime-with-compiler.js'),
        dest: resolve('dist/vue.common.dev.js'),
        format: 'cjs',
        env: 'development',
        alias: { he: './entity-decoder' },
        banner
      },
      // 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
      },
      ...
    }
    

使用VS Code查看Vue源碼的兩個(gè)問題

使用VSCode查看Vue源碼時(shí)通常會(huì)碰到兩個(gè)問題

  • 對(duì)于flow的語法顯示異常報(bào)錯(cuò)
    • 修改VSCode設(shè)置藐吮,在setting.json中增加"javascript.validate.enable": false配置
  • 對(duì)于使用了泛型的后續(xù)代碼溺拱,丟失高亮
    • 通過安裝Babel JavaScript插件解決

一切從入口開始

入口文件 entry-runtime-with-compiler.js 中,為Vue.prototype.$mount指定了函數(shù)實(shí)現(xiàn)

  • el可以是DOM元素谣辞,或者選擇器字符串

  • el不能是body或html

  • 選項(xiàng)中有render迫摔,則直接調(diào)用mount掛載DOM

  • 選項(xiàng)中如果沒有render,判斷是否有template泥从,沒有template則將el.outerHTML作為template句占,并嘗試將template轉(zhuǎn)換成render函數(shù)

    Vue.prototype.$mount = function (
      el?: string | Element,
      hydrating?: boolean
    ): Component {
      // 判斷el是否是 DOM 元素
      // 如果el是字符串,則當(dāng)成選擇器來查詢相應(yīng)的DOM元素躯嫉,查詢不到則創(chuàng)建一個(gè)div并返回
      el = el && query(el)
    
      /* istanbul ignore if */
      // 判斷el是否是body或者h(yuǎn)tml
      // Vue不允許直接掛載在body或html標(biāo)簽下
      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
      // resolve template/el and convert to render function
      // 判斷選項(xiàng)中是否包含render
      if (!options.render) {
        // 如果沒有render纱烘,判斷是否有template
        let template = options.template
        if (template) {
          ...
        } else if (el) {
          // 如果沒有template,則獲取el的outerHTML作為template
          template = getOuterHTML(el)
        }
        if (template) {
          ...
    
          // 將template轉(zhuǎn)換成render函數(shù)
          const { render, staticRenderFns } = compileToFunctions(template, {
            outputSourceRange: process.env.NODE_ENV !== 'production',
            shouldDecodeNewlines,
            shouldDecodeNewlinesForHref,
            delimiters: options.delimiters,
            comments: options.comments
          }, this)
          options.render = render
          options.staticRenderFns = staticRenderFns
    
          ...
        }
      }
      // 調(diào)用 mount 掛載 DOM
      return mount.call(this, el, hydrating)
    }
    

tips:$mount()函數(shù)在什么位置被調(diào)用祈餐?通過瀏覽器調(diào)試工具的Call Stack視圖擂啥,可以簡(jiǎn)單且清晰的查看一個(gè)函數(shù)被哪個(gè)位置的上層代碼所調(diào)用

Vue初始化

初始化相關(guān)的幾個(gè)主要文件

  • src/platforms/web/entry-runtime-with-compiler.js

    • 重寫了平臺(tái)相關(guān)的$mount()方法
    • 注冊(cè)了Vue.compile()方法,將HTML字符串轉(zhuǎn)換成render函數(shù)
  • src/platforms/web/runtime/index.js

    • 注冊(cè)了全局指令:v-model昼弟、v-show
    • 注冊(cè)了全局組件:v-transition啤它、v-transition-group
    • 為Vue原型添加了全局方法
      • _patch_:把虛擬DOM轉(zhuǎn)換成真實(shí)DOM(snabbdom的patch函數(shù))
      • $mount: 掛載方法
  • src/core/index.js

    • 調(diào)用initGlobalAPI(Vue)設(shè)置了Vue的全局靜態(tài)方法
  • src/core/instance/index.js - Vue構(gòu)造函數(shù)所在位置

    • 定義了Vue構(gòu)造函數(shù),調(diào)用了this._init(options)方法

    • 給Vue中混入了常用的實(shí)例成員和方法

靜態(tài)成員

通過 initGlobalAPI() 函數(shù)舱痘,實(shí)現(xiàn)了Vue靜態(tài)成員的初始化過程

  • Vue.config
  • Vue.util
    • 暴露了util對(duì)象变骡,util中的工具方法不視作全局API的一部分,應(yīng)當(dāng)避免依賴它們
  • Vue.set()
    • 用來添加響應(yīng)式屬性
  • Vue.delete()
    • 用來刪除響應(yīng)式屬性
  • Vue.nextTick()
    • 在下次 DOM 更新循環(huán)結(jié)束之后執(zhí)行延遲回調(diào)
  • Vue.observable()
    • 用來將對(duì)象轉(zhuǎn)換成響應(yīng)式數(shù)據(jù)
  • Vue.options
    • Vue.options.components 保存全局組件
      • Vue.options.components.KeepAlive 注冊(cè)了內(nèi)置的keep-alive組件到全局Vue.options.components
    • Vue.options.directives 保存全局指令
    • Vue.options.filters 保存全局過濾器
    • Vue.options._base 保存Vue構(gòu)造函數(shù)
  • Vue.use()
    • 用來注冊(cè)插件
  • Vue.mixin()
    • 用來實(shí)現(xiàn)混入
  • Vue.extend(options)
    • 使用基礎(chǔ) Vue 構(gòu)造器芭逝,創(chuàng)建一個(gè)子組件塌碌,參數(shù)是包含選項(xiàng)的對(duì)象
  • Vue.component()
    • 用來注冊(cè)或獲取全局組件
  • Vue.directive()
    • 用來注冊(cè)或獲取全局指令
  • Vue.filter()
    • 用來注冊(cè)或獲取全局過濾器
  • Vue.compile()
    • 將一個(gè)模板字符串編譯成 render 函數(shù)
    • 這個(gè)靜態(tài)成員方法是在入口js文件中添加的
// src/core/global-api/index.js
export function initGlobalAPI (Vue: GlobalAPI) {
  // config
  const configDef = {}
  configDef.get = () => config
  if (process.env.NODE_ENV !== 'production') {
    configDef.set = () => {
      warn(
        'Do not replace the Vue.config object, set individual fields instead.'
      )
    }
  }
  // 初始化 Vue.config 對(duì)象
  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.
  // 增加靜態(tài)成員util對(duì)象
  // util中的工具方法不視作全局API的一部分,應(yīng)當(dāng)避免依賴它們
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }
 
  // 增加靜態(tài)方法 set/delete/nextTick
  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick
 
  // 2.6 explicit observable API
  // 增加 observable 方法旬盯,用來將對(duì)象轉(zhuǎn)換成響應(yīng)式數(shù)據(jù)
  Vue.observable = <T>(obj: T): T => {
    observe(obj)
    return obj
  }
 
  // 初始化 options 對(duì)象
  Vue.options = Object.create(null)
  // ASSET_TYPES
  // 'component',
  // 'directive',
  // 'filter'
  // 為 Vue.options 初始化components/directives/filters
  // 分別保存全局的組件/指令/過濾器
  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.
  // 保存 Vue 構(gòu)造函數(shù)到 options._base
  Vue.options._base = Vue
 
  // 注冊(cè)內(nèi)置組件 keep-alive 到全局 components
  extend(Vue.options.components, builtInComponents)
 
  // 注冊(cè) Vue.use() 用來注冊(cè)插件
  initUse(Vue)
  // 注冊(cè) Vue.mixin() 實(shí)現(xiàn)混入
  initMixin(Vue)
  // 注冊(cè) Vue.extend() 基于傳入的 options 返回一個(gè)組件的構(gòu)造函數(shù)
  initExtend(Vue)
  // 注冊(cè) Vue.component()/Vue.directive()/Vue.filter()
  initAssetRegisters(Vue)
}

實(shí)例成員

src/core/instance/index.js 中初始化了絕大部分實(shí)例成員屬性和方法

  • property
    • vm.$data
    • vm.$props
    • ...
  • 方法 / 數(shù)據(jù)
    • vm.$watch()

      • $watch() 沒有對(duì)應(yīng)的全局靜態(tài)方法台妆,因?yàn)樾枰玫綄?shí)例對(duì)象vm

        Vue.prototype.$watch = function (
            expOrFn: string | Function,
            cb: any, // 可以是函數(shù)翎猛,也可以是對(duì)象
            options?: Object
          ): Function {
            // 獲取 vue 實(shí)例
            const vm: Component = this
            if (isPlainObject(cb)) {
              // 判斷 cb 如果是對(duì)象,執(zhí)行 createWatcher
              return createWatcher(vm, expOrFn, cb, options)
            }
            options = options || {}
            // 標(biāo)記是用戶 watcher
            options.user = true
            // 創(chuàng)建用戶 Watcher 對(duì)象
            const watcher = new Watcher(vm, expOrFn, cb, options)
            if (options.immediate) {
              // 判斷 immediate 選項(xiàng)是 true接剩,則立即執(zhí)行 cb 回調(diào)函數(shù)
              // 不確定 cb 是否能正常執(zhí)行切厘,使用 try catch 進(jìn)行異常處理
              try {
                cb.call(vm, watcher.value)
              } catch (error) {
                handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
              }
            }
            // 返回 unwatch 方法
            return function unwatchFn () {
              watcher.teardown()
            }
          }
        
    • vm.$set()

      • 同Vue.set()
    • vm.$delete()

      • 同Vue.delete()
  • 方法 / 事件
    • vm.$on()
      • 監(jiān)聽自定義事件
    • vm.$once()
      • 監(jiān)聽自定義事件,只觸發(fā)一次
    • vm.$off()
      • 取消自定義事件的監(jiān)聽
    • vm.$emit()
      • 觸發(fā)自定義事件
  • 方法 / 生命周期
    • vm.$mount()
      • 掛載DOM元素
      • runtime/index.js中添加懊缺,在入口js中重寫
    • vm.$forceUpdate()
      • 強(qiáng)制重新渲染
    • vm.$nextTick()
      • 將回調(diào)延遲到下次 DOM 更新循環(huán)之后執(zhí)行
    • vm.$destory()
      • 完全銷毀一個(gè)實(shí)例
  • 其他
    • vm._init()
      • Vue實(shí)例初始化方法
      • 在Vue構(gòu)造函數(shù)中調(diào)用了該初始化方法
    • vm._update()
      • 會(huì)調(diào)用vm._patch_方法更新 DOM 元素
    • vm._render()
      • 會(huì)調(diào)用用戶初始化時(shí)選項(xiàng)傳入的render函數(shù)(或者template轉(zhuǎn)換成的render函數(shù))
    • vm._patch_()
      • 用于把虛擬DOM轉(zhuǎn)換成真實(shí)DOM
      • runtime/index.js中添加了該方法
// src/code/instance/index.js
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)
}
 
// 注冊(cè) vm 的 _init() 方法疫稿,同時(shí)初始化 vm
initMixin(Vue)
// 注冊(cè) vm 的 $data/$props/$set()/$delete()/$watch()
stateMixin(Vue)
// 注冊(cè) vm 事件相關(guān)的成員及方法
// $on()/$off()/$once()/$emit()
eventsMixin(Vue)
// 注冊(cè) vm 生命周期相關(guān)的成員及方法
// _update()/$forceUpdate()/$destory()
lifecycleMixin(Vue)
// $nextTick()/_render()
renderMixin(Vue)
 
export default Vue
 

_init()

Vue的構(gòu)造函數(shù)中會(huì)調(diào)用Vue實(shí)例的_init()方法來完成一些實(shí)例相關(guān)的初始化工作,并觸發(fā)beforeCreatecreated生命周期鉤子函數(shù)

// src/core/instance/init.js
Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    // 設(shè)置實(shí)例的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
    // 給實(shí)例對(duì)象添加_isVue屬性鹃两,避免被轉(zhuǎn)換成響應(yīng)式對(duì)象
    vm._isVue = true
    // merge options
    // 合并構(gòu)造函數(shù)中的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
    // 初始化生命周期相關(guān)的屬性
    // $children/$parent/$root/$refs
    initLifecycle(vm)
    // 初始化事件監(jiān)聽遗座,父組件綁定在當(dāng)前組件上的事件
    initEvents(vm)
    // 初始化render相關(guān)的屬性與方法
    // $slots/$scopedSlots/_c/$createElement/$attrs/$listeners
    initRender(vm)
    // 觸發(fā) beforeCreate 生命周期鉤子函數(shù)
    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
    // 觸發(fā) created 生命周期鉤子函數(shù)
    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)
    }
 
    if (vm.$options.el) {
      // 如果提供了掛載的 DOM 元素 el
      // 調(diào)用$mount() 掛載 DOM元素
      vm.$mount(vm.$options.el)
    }
  }

initState()

初始化 vm 的_props/methods/_data/computed/watch

// src/core/instance/state.js
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  // 將 props 中成員轉(zhuǎn)換成響應(yīng)式的數(shù)據(jù),并注入到 Vue 實(shí)例 vm 中
  if (opts.props) initProps(vm, opts.props)
  // 將 methods 中的方法注入到 Vue 的實(shí)例 vm 中
  // 校驗(yàn) methods 中的方法名與 props 中的屬性是否重名
  // 校驗(yàn) methods 中的方法是否以 _ 或 $ 開頭
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    // 將 data 中的屬性轉(zhuǎn)換成響應(yīng)式的數(shù)據(jù)俊扳,并注入到 Vue 實(shí)例 vm 中
    // 校驗(yàn) data 中的屬性是否在 props 與 methods 中已經(jīng)存在
    initData(vm)
  } else {
    // 如果沒有提供 data 則初始化一個(gè)響應(yīng)式的空對(duì)象
    observe(vm._data = {}, true /* asRootData */)
  }
  // 初始化 computed
  if (opts.computed) initComputed(vm, opts.computed)
  // 初始化 watch
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

首次渲染過程

image-20201216233101117

數(shù)據(jù)響應(yīng)式原理

Vue的響應(yīng)式原理是基于觀察者模式來實(shí)現(xiàn)的

響應(yīng)式處理入口

Vue構(gòu)造函數(shù)中調(diào)用了vm._init()

_init()函數(shù)中調(diào)用了initState()

initState()函數(shù)中如果傳入的data有值途蒋,則調(diào)用initData(),并在最后調(diào)用了observe()

observe()函數(shù)會(huì)創(chuàng)建并返回一個(gè)Observer的實(shí)例馋记,并將data轉(zhuǎn)換成響應(yīng)式數(shù)據(jù)号坡,是響應(yīng)式處理的入口

// src/core/observer/index.js
/**
* 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.
*/
export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  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
  ) {
    // 創(chuàng)建 Observer 實(shí)例
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  // 返回 Observer 實(shí)例
  return ob
}

Observer

Observer是響應(yīng)式處理的核心類,用來對(duì)數(shù)組或?qū)ο笞鲰憫?yīng)式的處理

在它的構(gòu)造函數(shù)中初始化依賴對(duì)象梯醒,并將傳入的對(duì)象的所有屬性轉(zhuǎn)換成響應(yīng)式的getter/setter筋帖,如果傳入的是數(shù)組,則會(huì)遍歷數(shù)組的每一個(gè)元素冤馏,并調(diào)用observe() 創(chuàng)建observer實(shí)例

/**
* 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.
*/
export class Observer {
  // 觀察對(duì)象
  value: any;
  // 依賴對(duì)象
  dep: Dep;
  // 實(shí)例計(jì)數(shù)器
  vmCount: number; // number of vms that have this object as root $data
 
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    // 初始化實(shí)例計(jì)數(shù)器
    this.vmCount = 0
    // 使用 Object.defineProperty 將實(shí)例掛載到觀察對(duì)象的 __ob__ 屬性
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      // 數(shù)組的響應(yīng)式處理
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      // 為數(shù)組每一個(gè)對(duì)象創(chuàng)建一個(gè) observer 實(shí)例
      this.observeArray(value)
    } else {
      // 如果 value 是一個(gè)對(duì)象
      // 遍歷對(duì)象所有屬性并轉(zhuǎn)換成 getter/setter
      this.walk(value)
    }
  }
 
  /**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    // 遍歷對(duì)象所有屬性
    for (let i = 0; i < keys.length; i++) {
      // 轉(zhuǎn)換成 getter/setter
      defineReactive(obj, keys[i])
    }
  }
 
  /**
   * Observe a list of Array items.
   */
  observeArray (items: Array<any>) {
    // 遍歷數(shù)組所有元素,調(diào)用 observe
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

defineReactive

用來定義一個(gè)對(duì)象的響應(yīng)式的屬性寄啼,即使用Object.defineProperty來設(shè)置對(duì)象屬性的 getter/setter

/**
* Define a reactive property on an Object.
*/
export function defineReactive (
  // 目標(biāo)對(duì)象
  obj: Object,
  // 目標(biāo)屬性
  key: string,
  // 屬性值
  val: any,
  // 自定義 setter 方法
  customSetter?: ?Function,
  // 是否深度觀察
  // 為 false 時(shí)如果 val 是對(duì)象逮光,也將轉(zhuǎn)換成響應(yīng)式
  shallow?: boolean
) {
  // 創(chuàng)建依賴對(duì)象實(shí)例,用于收集依賴
  const dep = new Dep()
 
  // 獲取目標(biāo)對(duì)象 obj 的目標(biāo)屬性 key 的屬性描述符對(duì)象
  const property = Object.getOwnPropertyDescriptor(obj, key)
  // 如果屬性 key 存在墩划,且屬性描述符 configurable === false
  // 則該屬性無法通過 Object.defineProperty來重新定義
  if (property && property.configurable === false) {
    return
  }
 
  // cater for pre-defined getter/setters
  // 獲取用于預(yù)定義的 getter 與 setter
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    // 如果調(diào)用時(shí)只傳入了2個(gè)參數(shù)(即沒傳入val)涕刚,且沒有預(yù)定義的getter,則直接通過 obj[key] 獲取 val
    val = obj[key]
  }
 
  // 判斷是否深度觀察乙帮,并將子對(duì)象屬性全部轉(zhuǎn)換成 getter/setter杜漠,返回子觀察對(duì)象
  let childOb = !shallow && observe(val)
  // 使用 Object.defineProperty 定義 obj 對(duì)象 key 屬性的 getter 與 setter
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      // 如果存在預(yù)定義的 getter 則 value 等于 getter 調(diào)用的返回值
      // 否則 value 賦值為屬性值 val
      const value = getter ? getter.call(obj) : val     
      if (Dep.target) {
        // 當(dāng)前存在依賴目標(biāo)則建立依賴
        dep.depend()
        if (childOb) {
          // 如果子觀察目標(biāo)存在,則建立子依賴
          childOb.dep.depend()
          if (Array.isArray(value)) {
            // 如果屬性是數(shù)組察净,則處理數(shù)組元素的依賴收集
            // 調(diào)用數(shù)組元素 e.__ob__.dep.depend()
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      // 如果存在預(yù)定義的 getter 則 value 等于 getter 調(diào)用的返回值
      // 否則 value 賦值為屬性值 val
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      // 判斷新值舊值是否相等
      // (newVal !== newVal && value !== value) 是對(duì) NaN 這個(gè)特殊值的判斷處理(NaN !== NaN)
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      // 有預(yù)定義 getter 但沒有 setter 直接返回
      if (getter && !setter) return
      if (setter) {
        // 有預(yù)定義 setter 則調(diào)用 setter
        setter.call(obj, newVal)
      } else {
        // 否則直接更新新值
        val = newVal
      }
      // 判斷是否深度觀察驾茴,并將新賦值的子對(duì)象屬性全部轉(zhuǎn)換成 getter/setter,返回子觀察對(duì)象
      childOb = !shallow && observe(newVal)
      // 觸發(fā)依賴對(duì)象的 notify() 派發(fā)通知所有依賴更新
      dep.notify()
    }
  })
}

依賴收集

依賴收集由Dep對(duì)象來完成

每個(gè)需要收集依賴的對(duì)象屬性氢卡,都會(huì)創(chuàng)建一個(gè)相應(yīng)的dep實(shí)例锈至,并收集watchers保存到其subs數(shù)組中

對(duì)象響應(yīng)式屬性的依賴收集,主要是getter中的這部分代碼

    if (Dep.target) {
        // 當(dāng)前存在依賴目標(biāo)則建立依賴
        dep.depend()
        if (childOb) {
          // 如果子觀察目標(biāo)存在译秦,則建立子依賴
          childOb.dep.depend()
          if (Array.isArray(value)) {
            // 如果屬性是數(shù)組峡捡,則處理數(shù)組元素的依賴收集
            // 調(diào)用數(shù)組元素 e.__ob__.dep.depend()
            dependArray(value)
          }
        }
     }

這里有兩個(gè)問題

  • Dep.target 是何時(shí)賦值的击碗?

    在mountComponent()調(diào)用時(shí),Watcher被實(shí)例化

    Watcher構(gòu)造函數(shù)中調(diào)用了實(shí)例方法get()们拙,并通過pushTarget() 將Watcher實(shí)例賦值給Dep.target

  • dep.depend() 的依賴收集進(jìn)行了什么操作稍途?

    dep.depend()會(huì)調(diào)用Dep.target.addDep()方法,并調(diào)用dep.addSub()方法砚婆,將Watcher實(shí)例添加到觀察者列表subs中

    Watcher中會(huì)維護(hù)dep數(shù)組與dep.id集合械拍,當(dāng)調(diào)用addDep()方法時(shí),會(huì)先判斷dep.id是否已經(jīng)在集合中射沟,從而避免重復(fù)收集依賴

數(shù)組

數(shù)組的成員無法像對(duì)象屬性一樣通過Object.defineProperty()去設(shè)置 getter/setter 來監(jiān)視變化殊者,因此數(shù)組的響應(yīng)式需要進(jìn)行特殊的處理,通過對(duì)一系列會(huì)影響數(shù)組成員數(shù)量的原型方法進(jìn)行修補(bǔ)验夯,添加依賴收集與更新派發(fā)猖吴,來完成響應(yīng)式處理

影響數(shù)組的待修補(bǔ)方法arrayMethods

  • push
  • pop
  • shift
  • unshift
  • splice
  • sort
  • reverse
    // Observer 構(gòu)造函數(shù)中的處理
    if (Array.isArray(value)) {
      // 數(shù)組的響應(yīng)式處理
      if (hasProto) {
        // 如果支持原型, 替換原型指向 __prototype__ 為修補(bǔ)后的方法
        protoAugment(value, arrayMethods)
      } else {
        // 如果不支持原型,通過 Object.defineProperty 為數(shù)組重新定義修補(bǔ)后的方法
        copyAugment(value, arrayMethods, arrayKeys)
      }
      // 為數(shù)組每一個(gè)對(duì)象創(chuàng)建一個(gè) observer 實(shí)例
      this.observeArray(value)
    }
// src/core/observer/array.js
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
 
// 影響數(shù)組的待修補(bǔ)方法
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
 
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
  // cache original method
  // 緩存數(shù)組原型上原始的處理函數(shù)
  const original = arrayProto[method]
  // 通過 Object.defineProperty 為新創(chuàng)建的數(shù)組原型對(duì)象定義修補(bǔ)后的數(shù)組處理方法
  def(arrayMethods, method, function mutator (...args) {
    // 先執(zhí)行數(shù)組原型上原始的處理函數(shù)并將結(jié)果保存到 result 中
    const result = original.apply(this, args)
    const ob = this.__ob__
    // 是否新增
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    // 如果新增, 調(diào)用observe()將新增的成員轉(zhuǎn)化成響應(yīng)式
    if (inserted) ob.observeArray(inserted)
    // notify change
    // 調(diào)用依賴的 notify() 方法派發(fā)更新挥转,通知觀察者 Watcher 執(zhí)行相應(yīng)的更新操作
    ob.dep.notify()
    // 返回結(jié)果
    return result
  })
})

通過查看數(shù)組響應(yīng)式處理的源碼我們可以發(fā)現(xiàn)海蔽,除了通過修補(bǔ)過的七個(gè)原型方法來修改數(shù)組內(nèi)容外,其他方式修改數(shù)組將不能觸發(fā)響應(yīng)式更新绑谣,例如通過數(shù)組下標(biāo)來修改數(shù)組成員array[0] = xxx党窜,或者修改數(shù)組長度array.length = 1

Watcher

Vue中的Watcher有三種

  • Computed Watcher

    • Computed Watcher是在Vue構(gòu)造函數(shù)初始化調(diào)用_init() -> initState() -> initComputed() 中創(chuàng)建的
  • 用戶Watcher(偵聽器)

    • 用戶Watcher是在Vue構(gòu)造函數(shù)初始化調(diào)用_init() -> initState() -> initWatch() 中創(chuàng)建的(晚于Computed Watcher)
  • 渲染W(wǎng)atcher

    • 渲染W(wǎng)atcher是在Vue初始化調(diào)用_init() -> vm.$mount() -> mountComponent()的時(shí)候創(chuàng)建的(晚于用戶Watcher)

        // src/core/instance/lifecycle.js
      
        // 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 的創(chuàng)建
        // updateComponent 方法用于調(diào)用 render 函數(shù)并最終通過 patch 更新 DOM
        // isRenderWatcher 標(biāo)記參數(shù)為 true
        new Watcher(vm, updateComponent, noop, {
          before () {
            if (vm._isMounted && !vm._isDestroyed) {
              callHook(vm, 'beforeUpdate')
            }
          }
        }, true /* isRenderWatcher */)
      
  • Watcher的實(shí)現(xiàn)

    /**
     * A watcher parses an expression, collects dependencies,
     * and fires callback when the expression value changes.
     * This is used for both the $watch() api and directives.
     */
    export default class Watcher {
      vm: Component;
      expression: string;
      cb: Function;
      id: number;
      deep: boolean;
      user: boolean;
      lazy: boolean;
      sync: boolean;
      dirty: boolean;
      active: 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
        if (isRenderWatcher) {
          // 如果是渲染 watcher,記錄到 vm._watcher
          vm._watcher = this
        }
        // 記錄 watcher 實(shí)例到 vm._watchers 數(shù)組中
        vm._watchers.push(this)
        // options
        if (options) {
          this.deep = !!options.deep
          this.user = !!options.user
          this.lazy = !!options.lazy
          this.sync = !!options.sync
          this.before = options.before
        } else {
          // 渲染 watcher 不傳 options
          this.deep = this.user = this.lazy = this.sync = false
        }
        this.cb = cb
        this.id = ++uid // uid for batching
        this.active = true
        this.dirty = this.lazy // for lazy watchers
        // watcher 相關(guān) dep 依賴對(duì)象
        this.deps = []
        this.newDeps = []
        this.depIds = new Set()
        this.newDepIds = new Set()
        this.expression = process.env.NODE_ENV !== 'production'
          ? expOrFn.toString()
          : ''
        // parse expression for getter
        // expOrFn 的值是函數(shù)或字符串
        if (typeof expOrFn === 'function') {
          // 是函數(shù)時(shí)借宵,直接賦給 getter
          this.getter = expOrFn
        } else {
          // 是字符串時(shí)幌衣,是偵聽器中監(jiān)聽的屬性名,例如 watch: { 'person.name': function() {}}
          // parsePath('person.name') 返回一個(gè)獲取 person.name 的值的函數(shù)
          this.getter = parsePath(expOrFn)
          if (!this.getter) {
            this.getter = noop
            process.env.NODE_ENV !== 'production' && warn(
              `Failed watching path: "${expOrFn}" ` +
              'Watcher only accepts simple dot-delimited paths. ' +
              'For full control, use a function instead.',
              vm
            )
          }
        }
        // 渲染 watcher 的 lazy 是 false壤玫, 會(huì)立即執(zhí)行 get()
        // 計(jì)算屬性 watcher 的lazy 是 true豁护,在 render 時(shí)才會(huì)獲取值
        this.value = this.lazy
          ? undefined
          : this.get()
      }
    
      /**
       * Evaluate the getter, and re-collect dependencies.
       */
      get () {
        // 組件 watcher 入棧
        // 用于處理父子組件嵌套的情況
        pushTarget(this)
        let value
        const vm = this.vm
        try {
          // 執(zhí)行傳入的 expOrFn
          // 渲染 Watcher 傳入的是 updateComponent 函數(shù),會(huì)調(diào)用 render 函數(shù)并最終通過 patch 更新 DOM
          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)
          }
          // 組件 watcher 實(shí)例出棧
          popTarget()
          // 清空依賴對(duì)象相關(guān)的內(nèi)容
          this.cleanupDeps()
        }
        return value
      }
      ...
    }
    

總結(jié)

響應(yīng)式處理過程總結(jié)

image-20201228185240923
  • 整個(gè)響應(yīng)式處理過程是從Vue初始化_init()開始的

    • initState() 初始化vue實(shí)例的狀態(tài)欲间,并調(diào)用initData()初始化data屬性
    • initData() 將data屬性注入vm實(shí)例楚里,并調(diào)用observe()方法將data中的屬性轉(zhuǎn)換成響應(yīng)式的
    • observe() 是響應(yīng)式處理的入口
  • observe(value)

    • 判斷value是否是對(duì)象,如果不是對(duì)象直接返回
    • 判斷value對(duì)象是否有__ob__屬性猎贴,如果有直接返回(認(rèn)為已進(jìn)行過響應(yīng)式轉(zhuǎn)換)
    • 創(chuàng)建并返回observer對(duì)象
    image-20201228185835411
  • Observer

    • 為value對(duì)象(通過Object.defineProperty)定義不可枚舉的__ob__屬性班缎,用來記錄當(dāng)前的observer對(duì)象
    • 區(qū)分是數(shù)組還是對(duì)象,并進(jìn)行相應(yīng)的響應(yīng)式處理
    image-20201228185941768
  • defineReactive

    • 為每一個(gè)對(duì)象屬性創(chuàng)建dep對(duì)象來收集依賴
    • 如果當(dāng)前屬性值是對(duì)象她渴,調(diào)用observe將其轉(zhuǎn)換成響應(yīng)式
    • 為對(duì)象屬性定義getter與setter
    • getter
      • 通過dep收集依賴
      • 返回屬性值
    • setter
      • 保存新值
      • 調(diào)用observe()將新值轉(zhuǎn)換成響應(yīng)式
      • 調(diào)用dep.notify()派發(fā)更新通知給watcher达址,調(diào)用update()更新內(nèi)容到DOM
    image-20201228190416199
  • 依賴收集

    • 在watcher對(duì)象的get()方法中調(diào)用pushTarget
      • 將Dep.target賦值為當(dāng)前watcher實(shí)例
      • 將watcher實(shí)例入棧,用來處理父子組件嵌套的情況
    • 訪問data中的成員的時(shí)候趁耗,即defineReactive為屬性定義的getter中收集依賴
    • 將屬性對(duì)應(yīng)的watcher添加到dep的subs數(shù)組中
    • 如果有子觀察對(duì)象childOb苏携,給子觀察對(duì)象收集依賴
    image-20201228190600536
  • Watcher

    • 數(shù)據(jù)觸發(fā)響應(yīng)式更新時(shí),dep.notify()派發(fā)更新調(diào)用watcher的update()方法
    • queueWatcher()判斷watcher是否被處理对粪,如果沒有的話添加queue隊(duì)列中右冻,并調(diào)用flushSchedulerQueue()
    • flushSchedulerQueue()
      • 觸發(fā)beforeUpdate鉤子函數(shù)
      • 調(diào)用watcher.run()
        • run() -> get() -> getter() -> updateComponent()
      • 清空上一次的依賴
      • 觸發(fā)actived鉤子函數(shù)
      • 觸發(fā)updated鉤子函數(shù)
    QQ截圖20201228205942

全局API

為一個(gè)響應(yīng)式對(duì)象動(dòng)態(tài)添加一個(gè)屬性装蓬,該屬性是否是響應(yīng)式的?不是

為一個(gè)響應(yīng)式對(duì)象動(dòng)態(tài)添加一個(gè)響應(yīng)式屬性纱扭,可以使用Vue.set()vm.$set()來實(shí)現(xiàn)

Vue.set()

用于向一個(gè)響應(yīng)式對(duì)象添加一個(gè)屬性牍帚,并確保這個(gè)新屬性也是響應(yīng)式的,且觸發(fā)視圖更新

注意:對(duì)象不能是Vue實(shí)例vm乳蛾,或者Vue實(shí)例的根數(shù)據(jù)對(duì)象vm.$data

  • 示例

    // object
    Vue.set(object, 'name', 'hello') 
    // 或 
    vm.$set(object, 'name', 'hello')
    
    // array
    Vue.set(array, 0, 'world') 
    // 或 
    vm.$set(array, 0, 'world')
    
  • 定義位置

    core/global-api/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)}`)
      }
      // 判斷目標(biāo)對(duì)象 target 是否是數(shù)組暗赶,且參數(shù) key 是否是合法的數(shù)組索引
      if (Array.isArray(target) && isValidArrayIndex(key)) {
        target.length = Math.max(target.length, key)
        // 通過 splice 對(duì) key 位置的元素進(jìn)行替換
        // 數(shù)組的 splice 方法已經(jīng)在Vue初始化時(shí)中完成了響應(yīng)式補(bǔ)丁處理 (array.js)
        target.splice(key, 1, val)
        return val
      }
      // 如果 key 在目標(biāo)對(duì)象 target 中存在,且不是原型上的成員肃叶,則直接賦值(已經(jīng)是響應(yīng)式的)
      if (key in target && !(key in Object.prototype)) {
        target[key] = val
        return val
      }
      // 獲取目標(biāo)對(duì)象 target 的 __ob__ 屬性
      const ob = (target: any).__ob__
      // 判斷 target 是否是 Vue 實(shí)例蹂随,或者是否是 $data (vmCount === 1) 并拋出異常
      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
      }
      // 判斷 target 是否為響應(yīng)式對(duì)象 (ob是否存在)
      // 如果是普通對(duì)象則不做響應(yīng)式處理直接返回
      if (!ob) {
        target[key] = val
        return val
      }
      // 調(diào)用 defineReactive 為目標(biāo)對(duì)象添加響應(yīng)式屬性 key 值為 val
      defineReactive(ob.value, key, val)
      // 發(fā)送通知更新視圖
      ob.dep.notify()
      return val
    }
    

Vue.delete()

用于刪除對(duì)象的屬性,如果對(duì)象是響應(yīng)式的因惭,確保刪除能觸發(fā)視圖更新

主要用于避開Vue不能檢測(cè)到屬性被刪除的限制岳锁,但是很少會(huì)使用到

注意:對(duì)象不能是Vue實(shí)例vm,或者Vue實(shí)例的根數(shù)據(jù)對(duì)象vm.$data

  • 示例

    Vue.delete(object, 'name')
    // 或
    vm.$delete(object, 'name')
    
  • 定義位置

    core/global-api/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)}`)
      }
      // 判斷目標(biāo)對(duì)象 target 是否是數(shù)組蹦魔,且參數(shù) key 是否是合法的數(shù)組索引
      if (Array.isArray(target) && isValidArrayIndex(key)) {
        // 通過 splice 刪除 key 位置的元素
        // 數(shù)組的 splice 方法已經(jīng)在Vue初始化時(shí)中完成了響應(yīng)式補(bǔ)丁處理 (array.js)
        target.splice(key, 1)
        return
      }
      // 獲取目標(biāo)對(duì)象 target 的 __ob__ 屬性
      const ob = (target: any).__ob__
      // 判斷 target 是否是 Vue 實(shí)例激率,或者是否是 $data (vmCount === 1) 并拋出異常
      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
      }
      // 判斷目標(biāo)對(duì)象 target 是否包含屬性 key
      // 如果不包含則直接返回
      if (!hasOwn(target, key)) {
        return
      }
      // 刪除目標(biāo)對(duì)象 target 的屬性 key
      delete target[key]
      // 判斷 target 是否為響應(yīng)式對(duì)象 (ob是否存在)
      // 如果是普通對(duì)象則直接返回
      if (!ob) {
        return
      }
      // 發(fā)送通知更新視圖
      ob.dep.notify()
    }
    

Vue.nextTick()

Vue更新DOM是批量異步執(zhí)行的,當(dāng)通過響應(yīng)式方式觸發(fā)DOM更新但沒有完成時(shí)勿决,無法立即獲取更新后的DOM

在修改數(shù)據(jù)后立即使用nextTick()方法可以在下次DOM更新循環(huán)結(jié)束后乒躺,執(zhí)行延遲回調(diào),從而獲得更新后的DOM

  • 示例

    Vue.nextTick(function(){})
    // 或
    vm.$nextTick(function(){})
    
  • 定義位置

    • 實(shí)例方法

      core/instance/render.js -> core/util/next-tick.js

    • 靜態(tài)方法

      core/global-api/index.js -> core/util/next-tick.js

  • 源碼解析

    export function nextTick (cb?: Function, ctx?: Object) {
      // 聲明 _resolve 用來保存 cb 未定義時(shí)返回新創(chuàng)建的 Promise 的 resolve
      let _resolve
      // 將回調(diào)函數(shù) cb 加上 try catch 異常處理存入 callbacks 數(shù)組
      callbacks.push(() => {
        if (cb) {
          // 如果 cb 有定義低缩,則執(zhí)行回調(diào)
          try {
            cb.call(ctx)
          } catch (e) {
            handleError(e, ctx, 'nextTick')
          }
        } else if (_resolve) {
          // 如果 _resolve 有定義嘉冒,執(zhí)行_resolve
          _resolve(ctx)
        }
      })
      if (!pending) {
        pending = true
        // nextTick() 的核心
        // 嘗試在本次事件循環(huán)之后執(zhí)行 flushCallbacks
        // 如果支持 Promise 則優(yōu)先嘗試使用 Promsie.then() 的方式執(zhí)行微任務(wù)
        // 否則非IE瀏覽器環(huán)境判斷是否支持 MutationObserver 并使用 MutationObserver 來執(zhí)行微任務(wù)
        // 嘗試使用 setImmediate 來執(zhí)行宏任務(wù)(僅IE瀏覽器支持,但性能好于 setTimeout)
        // 最后嘗試使用 setTimeout 來執(zhí)行宏任務(wù)
        timerFunc()
      }
      // $flow-disable-line
      // cb 未定義且支持 Promise 則返回一個(gè)新的 Promise咆繁,并將 resolve 保存到 _resolve 備用
      if (!cb && typeof Promise !== 'undefined') {
        return new Promise(resolve => {
          _resolve = resolve
        })
      }
    }
    
    
    // timerFunc()
    
    // 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).
    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:
    /* 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.
        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)
      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.
      timerFunc = () => {
        setImmediate(flushCallbacks)
      }
    } else {
      // Fallback to setTimeout.
      timerFunc = () => {
        setTimeout(flushCallbacks, 0)
      }
    }
    
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末健爬,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子么介,更是在濱河造成了極大的恐慌,老刑警劉巖蜕衡,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件壤短,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡慨仿,警方通過查閱死者的電腦和手機(jī)久脯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來镰吆,“玉大人帘撰,你說我怎么就攤上這事⊥蛎螅” “怎么了摧找?”我有些...
    開封第一講書人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵核行,是天一觀的道長。 經(jīng)常有香客問我蹬耘,道長芝雪,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任综苔,我火速辦了婚禮惩系,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘如筛。我一直安慰自己堡牡,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開白布杨刨。 她就那樣靜靜地躺著晤柄,像睡著了一般。 火紅的嫁衣襯著肌膚如雪拭嫁。 梳的紋絲不亂的頭發(fā)上可免,一...
    開封第一講書人閱讀 48,970評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音做粤,去河邊找鬼浇借。 笑死,一個(gè)胖子當(dāng)著我的面吹牛怕品,可吹牛的內(nèi)容都是我干的妇垢。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼肉康,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼闯估!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起吼和,我...
    開封第一講書人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤涨薪,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后炫乓,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體刚夺,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年末捣,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了侠姑。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡箩做,死狀恐怖莽红,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情邦邦,我是刑警寧澤安吁,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布醉蚁,位于F島的核電站,受9級(jí)特大地震影響柳畔,放射性物質(zhì)發(fā)生泄漏馍管。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一薪韩、第九天 我趴在偏房一處隱蔽的房頂上張望确沸。 院中可真熱鬧,春花似錦俘陷、人聲如沸罗捎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽桨菜。三九已至,卻和暖如春捉偏,著一層夾襖步出監(jiān)牢的瞬間倒得,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來泰國打工夭禽, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留霞掺,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓讹躯,卻偏偏與公主長得像菩彬,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子潮梯,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容