筆者日前學習了 Vue 的 Observer 部分,簡單地谷歌了一下,由於沒有找到解釋地十分完全的中文資源,記下本身對其的理解並分享。html
轉載需註明出處 https://segmentfault.com/a/11... ,有幫助請點贊。vue
本文引用的 Vue 版本爲 v2.5.17-beta.0 。
不過 Vue 的 Observer 部分自2017年以來至今沒什麼大變化,v2.5.16 到 v2.5.17-beta.0 對 Observer 有個小小的 bugfix。node
本文介紹 Vue 響應式原理的實現過程,並試圖以之爲參照改造出一個便於移植的庫。這裏筆者把 Vue 的 observer 部分提出來獨立地講,讀者不須要對 Vue 其餘部分十分熟悉。ios
Vue 的響應式模型十分完善,實現地足夠巧妙,私覺得有學習的必要。本文準備從寫一個簡單的模型出發,一步步填充功能,演化成 Vue 源碼的形態,因此文章看起來彷佛巨長,但代碼多有重複;我認爲這樣寫,讀者看起來會比較輕鬆,因此請沒必要長文恐懼。盧瑟福說,「只有你能將一個理論講得連女僕都懂了,你纔算真正懂了」。雖然讀者可能不是女僕(?),我也會寫得儘可能明白的。git
本文對 Observer 介紹地很徹底,對象和數組的不一樣處理,deep watching,以及異步隊列都會講解。固然,也不會徹底整成源碼那麼麻煩,一些只和 Vue 有關的代碼刪除了,此外計算屬性(computed property)的部分只說明原理,省略了實現。es6
但通常的 JS 技巧,ECMAScript 6,閉包的知識,Object.defineProperty 的知識仍是須要具有的。github
Vue 源碼是用 Flow 寫的,本文改爲 TypeScript 了(同爲類型註解,畢竟後者更流行),未學習過的同窗只要把文中不像 JS 的部分去掉,當 JS 就好了。express
JS 中數組是對象的一種,由於 Observer 部分對數組與普通對象的對待區別很大,因此下文說到對象,都是指 constructor 爲 Object 的普通對象。npm
能夠先git clone git@github.com:vuejs/vue.git
一份源碼備看。observer 的部分在源碼的 src/core/observer 目錄下。segmentfault
本文代碼已經放在 https://github.com/xyzingh/le... ,運行 npm i && npm run test
能夠測試。
新建文件夾 learn-vue-observer,建立幾個文件。
util.ts
/* 一些經常使用函數的簡寫 */ export function def(obj: any, key: string, value: any, enumerable: boolean = false) { Object.defineProperty(obj, key, { value, enumerable, writable: true, configurable: true, }); } export function isObject(obj: any) { return obj !== null && typeof obj === "object"; } export function hasOwn(obj: any, key: string): boolean { return Object.prototype.hasOwnProperty.call(obj, key); } export function isPlainObject(obj: any): boolean { return Object.prototype.toString.call(obj) === "[object Object]"; } export function isNative(ctor: any): boolean { return typeof ctor === "function" && /native code/.test(ctor.toString()); } export function remove(arr: any[], item: any): any[] | void { if (arr.length) { const index = arr.indexOf(item); if (index > -1) { return arr.splice(index, 1); } } }
假設咱們要把下面這個對象轉變成響應式的。
let obj = { a: { aa: { aaa: 123, bbb: 456, }, bb: "obj.a.bb", }, b: "obj.b", };
怎樣算做是響應式的呢?若是將 obj 的任意鍵的值改變,都能執行一個相應的函數進行相關操做(好比更新DOM),那麼就算得上響應式了。爲此,咱們勢必爲 obj 的每一個鍵建立代理,使對 obj 的直接操做變成透過代理操做。代理的方式有許多,Object.observe,Proxy,getter/setter。但 Object.observe 已經被廢棄,Proxy 巨硬家從 Edge 纔開始支持,IE 全滅,因此可行的只有 getter/setter (IE9 開始支持)。然而 getter/setter 依然有很大的侷限性,即只能轉化已有屬性,所以須要爲用戶提供特別的函數來設置新屬性,這個函數咱們最後再提。
obj 的值都轉成 getter/setter 了,真實值存在哪呢?Vue 的作法是藏在閉包裏。
下面咱們定義3個函數/類,嘗試遞歸地設置 obj 的 getter/setter。
index.ts
import { def, hasOwn, isObject, isPlainObject } from "./util"; /** * 嘗試對 value 建立 Observer 實例, * value 若是不是對象或數組,什麼都不作。 * @param value 須要嘗試監視的目標 */ export function observe(value: any) { if (!isObject(value)) { return; } let ob: Observer | void; if (typeof value.__ob__ !== "undefined") { ob = value.__ob__; } else { ob = new Observer(value); } return ob; } export class Observer { constructor(value: any) { def(value, "__ob__", this); this.walk(value); } public walk(value: any) { for (const key of Object.keys(value)) { defineReactive(value, key); } } } function defineReactive(obj: any, key: string, val?: any) { // 閉包中的 val 藏着 obj[key] 的真實值 if (arguments.length === 2) { val = obj[key]; } let childOb = observe(val); // val 若是不是對象的話,是返回 undefined 的。 Object.defineProperty(obj, key, { enumerable: true, configurable: true, get() { //////////////// console.log("you get " + val); //////////////// return val; }, set(newVal) { if (newVal === val) { return; } //////////////// console.log("you set " + newVal); //////////////// val = newVal; childOb = observe(newVal); } }); }
咱們能夠試一下
observe(obj); console.log(obj.a.aa.aaa = 234);
輸出應爲
you get [object Object] you get [object Object] you set 234 234
可是,有個問題,咱們不該假設 obj 的每一個鍵就是簡單的值,萬一原本就是 getter/setter 呢?
let obj2 = {}; Object.defineProperty(obj2, "a", { configurable: true, enumerable: true, get() { return obj2._a; }, set(val) { obj2._a = val; }, }); Object.defineProperty(obj2, "_a", { enumerable: false, value: 123, writable: true, });
所以,須要修改 defineReactive ,繼續用閉包保存 getter/setter 。
function defineReactive(obj: any, key: string, val?: any) { const property = Object.getOwnPropertyDescriptor(obj, key); if (property && property.configurable === false) { return; } const getter = property!.get; // property! 的歎號是 TypeScript 語法,忽略便可 const setter = property!.set; // 爲何寫成 (!getter || setter) ?後面會討論。 if ((!getter || setter) && arguments.length === 2) { val = obj[key]; } let childOb = observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get() { const value = getter ? getter.call(obj) : val; //////////////// console.log("you get " + value); //////////////// return value; }, set(newVal) { const value = getter ? getter.call(obj) : val; if (newVal === value) { return; } //////////////// console.log("you set " + newVal); //////////////// if (setter) { setter.call(obj, newVal); } else { val = newVal; } childOb = observe(newVal); }, }); }
這樣就能夠成功地把 obj2 轉變成響應式的。
筆者在理解 if ((!getter || setter) && arguments.length === 2)
時遇到過障礙,這實際上是講:
這是 v2.5.17-beta.0 的一個 bugfix ,有關的討論原文來自↓
issue/7280
issue/7302
pull/7981
issue/8494
並非說以前的版本不支持數組,而是通常開發者使用數組與使用對象的方法有區別。數組在 JS 中常被看成棧,隊列,集合等數據結構的實現方式,會儲存批量的數據以待遍歷。編譯器對對象與數組的優化也有所不一樣。因此對數組的處理須要特化出來以提升性能。
首先,不能再對數組每一個鍵設置 getter/setter 了,而是修改覆蓋數組的 push, pop, ... 等方法。用戶要修改數組只能使用這些方法,不然不會是響應式的(除了 Vue.set, Vue.delete)。
所以,準備一個數組方法的替代品。哪些方法應當替代掉?那些不會干涉原數組的方法不須要修改;刪除數組元素的方法須要替代;增長或替換數組元素的方法須要替換,還要嘗試把新的值變成響應式的。
array.ts
import { def } from './util'; const arrayProto = Array.prototype as any; // 創建以 Array.prototype 爲原型的 arrayMethods 並導出 export const arrayMethods = Object.create(arrayProto); // 會干涉原數組的方法 const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse', ]; methodsToPatch.forEach((method: string) => { // 原方法的緩存 const original = arrayProto[method]; // 在 arrayMethods 上定義替代方法 def(arrayMethods, method, function (this: any, ...args: any[]) { const result = original.apply(this, args); const ob = this.__ob__; // 新增的元素 let inserted: any[] | void; switch (method) { // 會增長或替換元素的方法 case 'push': case 'unshift': inserted = args; break; case 'splice': inserted = args.slice(2); break; } if (inserted){ ob.observeArray(inserted); // Observer 上新增的方法 } /////////////////////////////// console.log("array is modified."); /////////////////////////////// return result; }); });
而後修改 Observer,區別對待數組。
export class Observer { constructor(value: any) { def(value, "__ob__", this); if (Array.isArray(value)) { // 替換原型(Object.setPrototype 這個方法執行地比較慢,並且支持狀況堪憂) Object.setPrototypeOf(value, arrayMethods); this.observeArray(value); } else { this.walk(value); } } public walk(value: any) { for (const key of Object.keys(value)) { defineReactive(value, key); } } public observeArray(items: any[]) { // 設置 l = items.length 防止遍歷過程當中 items 長度變化 for (let i = 0, l = items.length; i < l; i++) { // 直接觀察數組元素,省略在鍵上設置 getter/setter 的步驟 observe(items[i]); } } }
vm.$watch( expressionOrFunction, callback [, options] ) 是 Vue 最基礎的觀察自身 data 的方式。咱們參考這個函數,提出適用本文的一個函數:
watch( target, expression, callback )
觀察 target 這個對象的表達式 exp 的值,一旦發生變化時執行 callback (同步地)。callback 的第一個參數爲新的值,第二個參數爲舊的值,this 爲 target。
例如 watch(obj, "a.aa.bbb", val => console.log(val))
,當 obj.a.aa.bbb 改變時,控制檯會打印新的值。注意 obj 應該已經通過 observe(obj) 轉化過了。
以前版本咱們在 getter/setter 處留下了
/////////////////
console.log(XXX)
/////////////////
只要把這些替換成相應代碼,就能實現 watch 方法了。
如今來定義一下哪些狀況應執行 callback 。
假設 obj.a.aa.bbb = 456 ,咱們對這個鍵進行了 watch :
假設咱們還對 obj.a.aa 進行了 watch :
簡而言之,若是 target 沿着 expression 解析到的值與以前的不全等,就認爲須要執行 callback 。對於基礎類型來講,就是值的不全等。對於普通對象,就是引用不相同。但數組比較特殊,對數組元素進行了操做,就應執行 callback 。
怎麼組織代碼呢?Evan You (Vue 做者) 的方法比較巧妙。
建立兩個新的類,Dep, Watcher 。Dep 是 Dependency 的簡稱,每一個 Observer 的實例,成員中都有一個 Dep 的實例。
這個 Dep 的實質是個數組,放置着監聽這個 Observer 的 Watcher ,當這個 Observer 對應的值變化時,就通知 Dep 中的全部 Watcher 執行 callback 。
export class Observer { constructor(value: any) { this.dep = new Dep(); // 新增 def(value, "__ob__", this); if (Array.isArray(value)) { // .........................
Watcher 是調用 watch 函數產生的,它保存着 callback 而且維護了一個數組,數組存放了全部 存有這個 Watcher 的 Dep 。這樣當這個 Watcher 須要被刪除時,能夠遍歷數組,從各個 Dep 中刪去自身,也就是 unwatch 的過程。
Watcher 什麼時候被放入 Dep 中的先不談。先說說 Dep 都在什麼地方。
以上說得並不全對,應該說,原始的 Dep 是建立在 defineReactive 的閉包中,Observer 的 dep 成員只是這個原始的 Dep 的備份,始終一塊兒被維護,保持一致。另外,Observer 只會創建在對象或數組的 __ob__ 上,若是鍵的值不是對象或數組,只會有閉包中的 Dep 保存這個鍵的 Watcher 。
function defineReactive(obj: any, key: string, val?: any) { const dep = new Dep(); // 新增 const property = Object.getOwnPropertyDescriptor(obj, key); // ...........................
舉例來講,
let obj = { // obj.__ob__.dep: 保存 obj 的 dep a: { // 閉包中有 obj.a 的 dep // obj.a.__ob__.dep: 保存 obj.a 的 dep aa: { // 閉包中有 obj.a.aa 的 dep // obj.a.aa.__ob__.dep: 保存 obj.a.aa 的 dep aaa: 123, // 閉包中有 obj.a.aa.aaa 的 dep bbb: 456, // 閉包中有 obj.a.aa.bbb 的 dep }, bb: "obj.a.bb", // 閉包中有 obj.a.bb 的 dep }, b: "obj.b", // 閉包中有 obj.b 的 dep }; observe(obj);
數組特殊對待,不對數組的成員進行 defineReactive ,
let obj = { arr: [ // 閉包中 obj.arr 的 dep // obj.arr.__ob__.dep 2, // 沒有 dep ,沒有閉包 3, 5, 7, 11, { // 沒有閉包 // obj.arr[6].__ob__.dep 存在 }, [ // 沒有閉包 // obj.arr[7].__ob__.dep 存在 ], ], }; observe(obj);
複習一下,dep 實質是個數組,放着監聽這個鍵的 Watcher 。
當這個鍵的值被修改時,就應該通知相應 dep 的全部 Watcher ,咱們在 Dep 上設置 notify 方法,用來實現這個功能。
爲此,修改 setter 的部分。
function defineReactive(obj: any, key: string, val?: any) { const dep = new Dep(); // ................................. Object.defineProperty(obj, key, { enumerable: true, configurable: true, get() { // ............................. }, set(newVal) { // ............................. } childOb = observe(newVal); dep.notify(); }, }); }
數組的部分,
array.ts
// ................ def(arrayMethods, method, function (this: any, ...args: any[]) { const result = original.apply(this, args); const ob = this.__ob__; // ..................... if (inserted){ ob.observeArray(inserted); } ob.dep.notify(); return result; });
如此一來,每當修改值時,相應的 Watcher 都會被通知了。
如今的問題是,什麼時候怎麼把 Watcher 放入 dep 中。下面咱們先來嘗試實現 Dep 。
dep.ts
import { remove } from "./util"; import { Watcher } from "./watcher"; let uid = 0; export default class Dep { public id: number; public subs: Watcher[]; constructor() { this.id = uid++; this.subs = []; } public addSub(sub: Watcher) { this.subs.push(sub); } public removeSub(sub: Watcher) { remove(this.subs, sub); } public notify() { // 先複製一份,應對通知 Watcher 過程當中,this.subs 可能變化的狀況 const subs = this.subs.slice(); for (let i = 0, l = subs.length; i < l; i++) { // Watcher 上定義了 update 方法,用來被通知 subs[i].update(); } } }
假設用 watch(obj, "a.aa.bbb", val => console.log(val))
,建立了一個 Watcher ,這個 Watcher 應被放進哪些 Dep 中呢?
由於 obj.a
, obj.a.aa
改變時,obj.a.aa.bbb
的值可能改變,因此答案是 obj.a
, obj.a.aa
, obj.a.aa.bbb
的閉包中的 Dep, 前二者是對象,因此在 __ob__.dep 中再放一份。
由於在對錶達式 obj.a.aa.bbb
求值時,會依次執行 obj.a
, (obj.a).aa
, ((obj.a).aa).bbb
的 getter ,這也正好對應了應被放入 Watcher 的鍵,因此很天然的一個想法是,
規定一個全局變量,日常是 null ,當在決定某個 Watcher 該放入哪些 Dep 的時候(即 依賴收集 階段),讓這個全局變量指向這個 Watcher 。而後 touch 被監視的那個鍵,換言之,對那個鍵求值。途中會調用一連串的 getter ,往那些 getter 所對應的 Dep 裏放入這個 Watcher 就對了。以後再將全局變量改回 null 。
這個作法的妙處,還在於它能夠同時適用 deep watching 和 計算屬性(computed property)。deep watching 後面會再說,對於計算屬性,這使得用戶直接寫函數就行,無需顯式說明這個計算屬性所依賴的其餘屬性,十分優雅,由於在運算這個函數時,用到其餘屬性就會觸發 getter ,可能的依賴都會被收集起來。
咱們來嘗試實現,
export default class Dep { // Dep.target 即前文所謂的全局變量 public static target: Watcher | null = null; public id: number; public subs: Watcher[]; public depend() { if (Dep.target) { this.addSub(Dep.target); } } // ...................................................
function defineReactive(obj: any, key: string, val?: any) { const dep = new Dep(); // ................................................... let childOb = observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get() { const value = getter ? getter.call(obj) : val; // 若是處在依賴收集階段 if (Dep.target) { dep.depend(); if (childOb) { childOb.dep.depend(); } } return value; }, // .................................................... }
如今也該把一直談論的 Watcher 給實現了。根據前面說的,它應該有個 update 方法。
watcher.ts
import Dep from "./dep"; let uid = 0; export class Watcher { public id: number; public value: any; public target: any; public getter: (target: any) => any; public callback: (newVal: any, oldVal: any) => void; constructor( target: any, expression: string, callback: (newVal: any, oldVal: any) => void, ) { this.id = uid++; this.target = target; this.getter = parsePath(expression); this.callback = callback; this.value = this.get(); } public get() { // 進入依賴收集階段 Dep.target = this; let value: any; const obj = this.target; try { // 調用了一連串 getter ,對應的鍵的 dep 中放入了這個 watcher value = this.getter(obj); } finally { // 退出依賴收集階段 Dep.target = null; } return value; } public update() { this.run(); } public run() { this.getAndInvoke(this.callback); } public getAndInvoke(cb: (newVal: any, oldVal: any) => void) { const value = this.get(); if (value !== this.value || isObject(value) /* 監視目標爲對象或數組的話,仍應執行回調,由於值可能變異了 */) { const oldVal = this.value; this.value = value; cb.call(this.target, value, oldVal); } } } const bailRE = /[^\w.$]/; function parsePath(path: string): any { if (bailRE.test(path)) { return; } const segments = path.split("."); return (obj: any) => { for (const segment of segments) { if (!obj) { return; } obj = obj[segment]; } return obj; }; }
function defineReactive(obj: any, key: string, val?: any) { // ..................................... if (Dep.target) { dep.depend(); if (childOb) { childOb.dep.depend(); if (Array.isArray(value)) { dependArray(value); } } } // ...................................... } function dependArray(value: any[]) { for (let e, i = 0, l = value.length; i < l; i++) { e = value[i]; // 若爲多維數組,繼續遞歸監視 e && e.__ob__ && e.__ob__.dep.depend(); if (Array.isArray(e)) { dependArray(e); } } }
討論的原文來自 issue/3883 ,舉例而言,
let obj = { matrix: [ [2, 3, 5, 7, 11], [13, 17, 19, 23, 29], ], }; observe(obj); watch(obj, "matrix", val => console.log(val)); obj.matrix[0].push(1); // 致使 matrix[0].__ob__.dep.notify() ,因爲遞歸監視,這個 dep 裏也有上面的 Watcher
只有 watch 沒有 unwatch 天然是不合理的。前面提到,Watcher 也維護了一個數組 deps,存放全部 放了這個 Watcher 的 Dep ,當這個 Watcher 析構時,能夠從這些 Dep 中刪去自身。
咱們給 Watcher 增長 active, deps, depIds, newDeps, newDepIds 屬性,addDep, cleanupDeps, teardown 方法,其中 teardown 方法起的是析構的做用,active 標誌 Watcher 是否可用,其餘的都是圍繞着維護 deps 。
export class Watcher { // .............................. public active = true; public deps: Dep[] = []; public depIds = new Set<number>(); public newDeps: Dep[] = []; public newDepIds = new Set<number>(); public run() { if (this.active) { this.getAndInvoke(this.callback); } } // newDeps 是新一輪收集的依賴,deps 是以前一輪收集的依賴 public addDep(dep: Dep) { const id = dep.id; if (!this.newDepIds.has(id)) { this.newDepIds.add(id); this.newDeps.push(dep); if (!this.depIds.has(id)) { dep.addSub(this); } } } public get() { Dep.target = this; let value: any; const obj = this.target; try { value = this.getter(obj); } finally { Dep.target = null; this.cleanupDeps(); } return value; } // 清理依賴 // 以前收集的依賴 若是不出如今新一輪收集的依賴中,就清除掉 // 再交換 deps/newDeps, depIds/newDepIds public cleanupDeps() { let i = this.deps.length; while (i--) { const dep = this.deps[i]; if (!this.newDepIds.has(dep.id)) { dep.removeSub(this); } } const tmpIds = this.depIds; this.depIds = this.newDepIds; this.newDepIds = tmpIds; this.newDepIds.clear(); const tmp = this.deps; this.deps = this.newDeps; this.newDeps = tmp; this.newDeps.length = 0; } public teardown() { if (this.active) { let i = this.deps.length; while (i--) { this.deps[i].removeSub(this); } this.active = false; } } }
修改以前的 dep.ts
export default class Dep { public depend() { if (Dep.target) { // this.addSub(Dep.target); Dep.target.addDep(this); } } }
Deep watching 的原理很簡單,就是在用 touch 收集依賴的基礎上,遞歸遍歷並 touch 全部子元素,如此一來,全部子元素都被收集到依賴中。其中只有防止對象引用成環須要稍微注意一下,這個用一個集合記錄遍歷到的元素來解決。
咱們給 Watcher 構造函數增長一個 deep 選項。
直接貼代碼,
export class Watcher { public deep: boolean; constructor( target: any, expression: string, callback: (newVal: any, oldVal: any) => void, { deep = false, }, ) { this.deep = deep; // ................................ } public get() { Dep.target = this; let value: any; const obj = this.target; try { value = this.getter(obj); } finally { if (this.deep) { // touch 全部子元素,收集到依賴中 traverse(value); } Dep.target = null; this.cleanupDeps(); } return value; } public getAndInvoke(cb: (newVal: any, oldVal: any) => void) { const value = this.get(); if (value !== this.value || isObject(value) || this.deep /* deep watcher 始終執行 */ ) { const oldVal = this.value; this.value = value; cb.call(this.target, value, oldVal); } } }
traverse.ts
import { isObject } from "./util"; const seenObjects = new Set(); export function traverse(val: any) { _traverse(val, seenObjects); seenObjects.clear(); } function _traverse(val: any, seen: Set<any>) { let i; let keys; const isA = Array.isArray(val); if ((!isA && !isObject(val)) || Object.isFrozen(val)) { return; } if (val.__ob__) { const depId = val.__ob__.dep.id; if (seen.has(depId)) { return; } seen.add(depId); } if (isA) { i = val.length; while (i--) { _traverse(val[i] /* touch */, seen); } } else { keys = Object.keys(val); i = keys.length; while (i--) { _traverse(val[keys[i]] /* touch */, seen); } } }
使用異步 Watcher 能夠緩衝在同一次事件循環中發生的全部數據改變。若是在本次執行棧中同一個 Watcher 被屢次觸發,只會被推入到隊列中一次。這樣在緩衝時去除重複數據,可以避免沒必要要的計算,提升性能。
Vue 源碼中的異步隊列模型比下文中的複雜,由於 Vue 要保證
若是這些是你的興趣,請直接轉戰源碼 src/core/observer/scheduler.js 。
如今修改 watcher.ts ,
export class Watcher { public deep: boolean; public sync: boolean; constructor( target: any, expression: string, callback: (newVal: any, oldVal: any) => void, { deep = false, sync = false, // 增長同步選項 }, ) { this.deep = deep; this.sync = sync; // ............................ } public update() { if (this.sync) { this.run(); } else { queueWatcher(this); // 推入隊列 } } }
建立 scheduler.ts
/// <reference path="next-tick.d.ts" /> import { nextTick } from "./next-tick"; import { Watcher } from "./watcher"; const queue: Watcher[] = []; let has: { [key: number]: true | null } = {}; let waiting = false; let flushing = false; let index = 0; /** * 重置 scheduler 的狀態. */ function resetSchedulerState() { index = queue.length = 0; has = {}; waiting = flushing = false; } /** * 刷新隊列,並運行 watcher */ function flushSchedulerQueue() { flushing = true; let watcher; let id; queue.sort((a, b) => a.id - b.id); for (index = 0; index < queue.length /* 不緩存隊列長度,由於新的 watcher 可能在執行隊列時加進來 */; index++) { watcher = queue[index]; id = watcher.id; has[id] = null; watcher.run(); } resetSchedulerState(); } /** * 將一個 watcher 推入隊列 * 相同 ID 的 watcher 會被跳過 * 除非隊列中以前的相同ID的 watcher 已被處理掉 */ export function queueWatcher(watcher: Watcher) { const id = watcher.id; if (has[id] == null) { has[id] = true; if (!flushing) { queue.push(watcher); } else { let i = queue.length - 1; // 放到隊列中相應 ID 的位置 while (i > index && queue[i].id > watcher.id) { i--; } queue.splice(i + 1, 0, watcher); } if (!waiting) { waiting = true; // 放入微任務隊列 nextTick(flushSchedulerQueue); } } }
若是不清楚微任務隊列是什麼,能夠閱讀下 理解瀏覽器和node.js中的Event loop事件循環 。
下面貼一下 Vue 的 nextTick 實現。
next-tick.d.ts
// 本身給 next-tick 寫了下接口 export declare function nextTick(cb: () => void, ctx?: any): Promise<any> | void;
next-tick.js (注意這是 JS)
import { isNative } from "./util"; const inBrowser = typeof window !== "undefined"; const inWeex = typeof WXEnvironment == "undefined" && !!WXEnvironment.platform; const weexPlatform = inWeex && WXEnvironment.platform.toLowerCase(); const UA = inBrowser && window.navigator.userAgent.toLowerCase(); const isIOS = (UA && /iphone|ipad|ipod|ios/.test(UA)) || (weexPlatform === "ios"); function noop() {} function handleError() {} 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 both microtasks and (macro) tasks. // In < 2.4 we used microtasks everywhere, but there are some scenarios where // microtasks have too high a priority and fire in between supposedly // sequential events (e.g. #4521, #6690) or even between bubbling of the same // event (#6566). However, using (macro) tasks everywhere also has subtle problems // when state is changed right before repaint (e.g. #6813, out-in transitions). // Here we use microtask by default, but expose a way to force (macro) task when // needed (e.g. in event handlers attached by v-on). let microTimerFunc; let macroTimerFunc; let useMacroTask = false; // Determine (macro) task defer implementation. // Technically setImmediate should be the ideal choice, but it's only available // in IE. The only polyfill that consistently queues the callback after all DOM // events triggered in the same loop is by using MessageChannel. /* istanbul ignore if */ if (typeof setImmediate !== "undefined" && isNative(setImmediate)) { macroTimerFunc = () => { setImmediate(flushCallbacks); }; } else if (typeof MessageChannel !== "undefined" && ( isNative(MessageChannel) || // PhantomJS MessageChannel.toString() === "[object MessageChannelConstructor]" )) { const channel = new MessageChannel(); const port = channel.port2; channel.port1.onmessage = flushCallbacks; macroTimerFunc = () => { port.postMessage(1); }; } else { /* istanbul ignore next */ macroTimerFunc = () => { setTimeout(flushCallbacks, 0); }; } // Determine microtask defer implementation. /* istanbul ignore next, $flow-disable-line */ if (typeof Promise !== "undefined" && isNative(Promise)) { const p = Promise.resolve(); microTimerFunc = () => { 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); } }; } else { // fallback to macro microTimerFunc = macroTimerFunc; } /** * Wrap a function so that if any code inside triggers state change, * the changes are queued using a (macro) task instead of a microtask. */ export function withMacroTask(fn) { return fn._withTask || (fn._withTask = function() { useMacroTask = true; const res = fn.apply(null, arguments); useMacroTask = false; return res; }); } export function nextTick(cb, ctx) { let _resolve; callbacks.push(() => { if (cb) { try { cb.call(ctx); } catch (e) { handleError(e, ctx, "nextTick"); } } else if (_resolve) { _resolve(ctx); } }); if (!pending) { pending = true; if (useMacroTask) { macroTimerFunc(); } else { microTimerFunc(); } } // $flow-disable-line if (!cb && typeof Promise !== "undefined") { return new Promise((resolve) => { _resolve = resolve; }); } }
本文代碼已經放在 https://github.com/xyzingh/le... ,運行 npm i && npm run test
能夠測試。