Vue.js 源碼剖析 - 響應(yīng)式原理
準(zhǔn)備工作
Vue源碼獲取
這里主要分析 Vue 2.6版本的源碼率翅,使用Vue 3.0版本來開發(fā)項(xiàng)目還需要一段時(shí)間的過渡
-
項(xiàng)目地址:
- Vue 2.6 https://github.com/vuejs/vue
- Vue 3.x https://github.com/vuejs/vue-next
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)試過程中查看代碼
- 執(zhí)行
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
配置
- 修改VSCode設(shè)置藐吮,在setting.json中增加
- 對(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)方法
- 調(diào)用
-
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.options.components 保存全局組件
- 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.$on()
- 方法 / 生命周期
- vm.$mount()
- 掛載DOM元素
- 在
runtime/index.js
中添加懊缺,在入口js中重寫
- vm.$forceUpdate()
- 強(qiáng)制重新渲染
- vm.$nextTick()
- 將回調(diào)延遲到下次 DOM 更新循環(huán)之后執(zhí)行
- vm.$destory()
- 完全銷毀一個(gè)實(shí)例
- vm.$mount()
- 其他
- 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
中添加了該方法
- vm._init()
// 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ā)
beforeCreate
和created
生命周期鉤子函數(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)
}
}
首次渲染過程
數(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)建的
- Computed Watcher是在Vue構(gòu)造函數(shù)初始化調(diào)用
-
用戶Watcher(偵聽器)
- 用戶Watcher是在Vue構(gòu)造函數(shù)初始化調(diào)用
_init()
->initState()
->initWatch()
中創(chuàng)建的(晚于Computed Watcher)
- 用戶Watcher是在Vue構(gòu)造函數(shù)初始化調(diào)用
-
渲染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é)
-
整個(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ì)象
-
Observer
- 為value對(duì)象(通過Object.defineProperty)定義不可枚舉的
__ob__
屬性班缎,用來記錄當(dāng)前的observer對(duì)象 - 區(qū)分是數(shù)組還是對(duì)象,并進(jìn)行相應(yīng)的響應(yīng)式處理
- 為value對(duì)象(通過Object.defineProperty)定義不可枚舉的
-
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
-
依賴收集
- 在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ì)象收集依賴
- 在watcher對(duì)象的get()方法中調(diào)用pushTarget
-
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ù)
全局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) } }