響應(yīng)式系統(tǒng)(五)

前言

上文還漏了一些重要的諸如異步更新授段、computed等細節(jié)耀找,本文補齊

正文

異步更新

上文講到Watcher里的this.sync是來控制同步與異步觸發(fā)依賴更新的。同步的話缺點很明顯洋幻,試想一下如下例子

new Vue({
    data: {
        a: 1,
        b: 2
    },
    template: `<div @click="change">
        {{a}}{讲竿}
        </div>`,
    methods: {
        change() {
            this.a = 2
            this.b = 3
        }
    }
}).$mount('#app')

同時改動了this.a、this.b庄涡,因為這倆屬性都收集了renderWatcher量承,若是同步的話那么就會執(zhí)行倆遍渲染函數(shù),這是不明智的穴店,所以若是異步的話可以將其更新回調(diào)放入異步更新隊列撕捍,就可以一次遍歷觸發(fā),現(xiàn)在我們看看異步的邏輯泣洞,即

update() {
    if (this.computed) {
       // ...
    } else if (this.sync) {
        // ...
    } else {
        queueWatcher(this)
    }
}

queueWatcher方法在scheduler .js里忧风,具體看下文

computed

首先看看initComputed,從此我們可得知initComputed就是劫持computed球凰,將其轉(zhuǎn)化為響應(yīng)式對象

計算屬性其實就是惰性求值watcher狮腿,它觀測get里的響應(yīng)式屬性(若是如Date.now()之類非響應(yīng)式是不會觸發(fā)變化的),一旦其變化這個get就會觸發(fā)(get作為WatcherexpOrFn)呕诉,如此一來該計算屬性的值也就重新求值了

和普通watcher區(qū)別就是它不會立即求值只有在被引用觸發(fā)其響應(yīng)式屬性get才會求值蚤霞,而普通watcher一旦創(chuàng)建就會求值

new Vue({
    data: {
        a: 1
    },
    computed: {
        b: {
            get() {
                return this.a + 1
            },
            set(val) {
                this.a = val - 1
            }
        }
    },
    template: `<div @click="change">
        {{a}}{}
        </div>`,
    methods: {
        change() {
            this.a = 2
        }
    }
}).$mount('#app')

以此為例义钉,轉(zhuǎn)化結(jié)果如下

Object.defineProperty(target, key, {
    get: function computedGetter() {
        const watcher = this._computedWatchers && this._computedWatchers[key]
        if (watcher) {
            watcher.depend()
            return watcher.evaluate()
        }
    },
    set: function set(val) {
        this.a = val - 1
    }
})

從這可見只有get被觸發(fā)才會開始其依賴收集昧绣,這和普通watcher創(chuàng)建就求值從而開始依賴收集是不一樣的

依賴收集

如此例所示,b是依賴a的捶闸,所以a的dep得收集到bwatcher(這樣子a變化可以通知b重新求值)夜畴,模板渲染依賴b,所以b的dep得收集到renderWatcher

也就是當計算屬性b被讀壬咀场(在此是模板引用{{ b }})贪绘,該get會被執(zhí)行
首先其定義了watcher變量來存儲this._computedWatchers[key],通過前文我們知道在initComputed里遍歷vm.$options.computed給每個都new Watcher央碟,所以該watcher就是計算屬性b的觀察者對象

watcher <==> new Watcher(vm, b.get, noop, { computed: true })

若該對象存在的話就會執(zhí)行該對象的depend税灌、evalute
首先我們看Watcher.depend

depend() {
    if (this.dep && Dep.target) {
        this.dep.depend()
    }
}

這個很簡單,就是判斷下this.dep && Dep.target,存在的話就調(diào)用this.dep.depend()菱涤。這個this.dep就是計算屬性的dep苞也,它初始化在Watcher constructor

constructor() {
    if (this.computed) {
        this.value = undefined
        this.dep = new Dep()
    } else {
        this.value = this.get()
    }
}

我們可見非計算屬性watcher是直接執(zhí)行this.get來觸發(fā)響應(yīng)式屬性的get從而收集依賴,計算屬性watcher就是初始化了this.dep也就是該響應(yīng)式計算屬性對應(yīng)的dep

前者直接開始求值粘秆,后者只有在訪問到的時候才求值

回到this.dep.depend()方法如迟,我們看上訴提到的a的depb的dep如何收集依賴

  • 我們知道這個就是收集依賴攻走,那么我們得知道Dep.target是什么殷勘,這個其實是renderWatcher,因為計算屬性brenderWatcher依賴昔搂,也就是這b.getrender觸發(fā)訪問的玲销,這就完成了b的dep收集

watcher.depend()完了之后還有return watcher.evaluate()

evaluate() {
    if (this.dirty) {
        this.value = this.get()
        this.dirty = false
    }
    return this.value
}

首先判斷dirty,我們之前就有說過true為未求值摘符、false為已求值贤斜,這個就是computed緩存來源
未求值的話就是執(zhí)行this.get()求值,其實這相當于執(zhí)行b.get议慰。注意這里Dep.target已經(jīng)變成了計算屬性bwatcher

get() {
    return this.a + 1
}
  • 關(guān)鍵到了,這里訪問了this.a就觸發(fā)了a.get奴曙,這樣子就會導(dǎo)致a的dep收集到計算屬性bwatcher

如此我們就完成了依賴收集

依賴觸發(fā)

我們現(xiàn)在觸發(fā)了例子里的change函數(shù)别凹,也就是修改this.a的值(其實修改this.b也一樣內(nèi)在還是修改this.a
我們知道

a.dep.subs <===> [renderWatcher, bWatcher]

那么修改a就會觸發(fā)這倆個watcher.update

update() {
    /* istanbul ignore else */
    if (this.computed) {
        // A computed property watcher has two modes: lazy and activated.
        // It initializes as lazy by default, and only becomes activated when
        // it is depended on by at least one subscriber, which is typically
        // another computed property or a component's render function.
        if (this.dep.subs.length === 0) {
            // In lazy mode, we don't want to perform computations until necessary,
            // so we simply mark the watcher as dirty. The actual computation is
            // performed just-in-time in this.evaluate() when the computed property
            // is accessed.
            this.dirty = true
        } else {
            // In activated mode, we want to proactively perform the computation
            // but only notify our subscribers when the value has indeed changed.
            this.getAndInvoke(() => {
                this.dep.notify()
            })
        }
    } else if (this.sync) {
        this.run()
    } else {
        // 這里是個優(yōu)化
        queueWatcher(this)
    }
}

這里除了computed我們都有講過,所以我們這里講computed

this.dep <==> b.dep
this.dep.subs.length <==> b.dep.subs.length <==> [renderWatcher].length === 1

首先判斷下這個this.dep.subs.length === 0洽糟,我們知道dirty === true是未求值炉菲,所以在該屬性未被依賴的時候(未被引用)將dirty置為true(其實就是重置默認值),這樣子當被依賴的時候(被引用)evaluate就會重新求值(dirty === false的話evaluate不會重新求值)
此例來看this.dep.subs.length === 1坤溃,所以走else分支拍霜,調(diào)用getAndInvoke方法重新求值設(shè)置新值之后執(zhí)行this.dep.notify(),通知訂閱了b變化的更新(也就是通知renderWatcher更新)

scheduler.js

這個文件里存儲的都是異步執(zhí)行更新的相關(guān)方法

queueWatcher

就是個watcher入隊的方法

const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let waiting = false
let flushing = false
let index = 0
/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 */
export function queueWatcher(watcher: Watcher) {
    const id = watcher.id
    if (has[id] == null) {
        has[id] = true
        if (!flushing) {
            queue.push(watcher)
        } else {
            // if already flushing, splice the watcher based on its id
            // if already past its id, it will be run next immediately.
            let i = queue.length - 1
            while (i > index && queue[i].id > watcher.id) {
                i--
            }
            queue.splice(i + 1, 0, watcher)
        }
        if (!waiting) {
            waiting = true
            nextTick(flushSchedulerQueue)
        }
    }
}

就像這個注釋所言該方法就是push一個watcher到觀察者隊列薪介,總體來看就是入隊列祠饺,然后調(diào)用nextTick在下一個Tick執(zhí)行flushSchedulerQueue,也就是在下一個Tick之前會入隊完畢汁政,接下來我我們看如何入隊的
首先就是獲取這個入隊的watcher.id道偷,我們定義了has這個對象用于紀錄入隊的watcher。先判斷下這個watcher是否已經(jīng)入隊记劈,已入隊的話就啥也不干勺鸦,未入隊的話就給此watcher標記在has
然后就是判斷下這個flushing。它是用于判斷是否執(zhí)行更新中目木,也就是更新隊列是否正在被執(zhí)行换途,默認是false

  • 若是未執(zhí)行更新中自然就是簡單入隊即可
  • 若是在執(zhí)行更新中卻有觀察者要入隊那么就得考慮好這個要入隊的watcher插在哪,也就是得插入到正在執(zhí)行的watcher后面军拟,假設(shè)已經(jīng)有倆[{id: 1}, {id: 2}]剃执,假設(shè)已經(jīng)循環(huán)到{id: 1}這個watcher,那么這時候index還是0吻谋,我們要插入的位置也是{id: 1}后面
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
    i--
}

很明顯這個尋找插入點是倒序查找忠蝗,這里判斷queue[i].id > watcher.id是因為flushSchedulerQueue里對queue做了針對id的排序
最后就是判斷下waiting,這個就是個防止多次觸發(fā)nextTick(flushSchedulerQueue)的一個標志漓拾,算是個小技巧

這個方法包含倆部分:

  • 觀察者入隊
  • 下一個tick執(zhí)行更新隊列
flushSchedulerQueue
export const MAX_UPDATE_COUNT = 100
const activatedChildren: Array<Component> = []
let circular: { [key: number]: number } = {}
function flushSchedulerQueue() {
    flushing = true
    let watcher, id

    queue.sort((a, b) => a.id - b.id)

    for (index = 0; index < queue.length; index++) {
        watcher = queue[index]
        if (watcher.before) {
            watcher.before()
        }
        id = watcher.id
        has[id] = null
        watcher.run()
        if (process.env.NODE_ENV !== 'production' && has[id] != null) {
            circular[id] = (circular[id] || 0) + 1
            if (circular[id] > MAX_UPDATE_COUNT) {
                warn(
                    'You may have an infinite update loop ' + (
                        watcher.user
                            ? `in watcher with expression "${watcher.expression}"`
                            : `in a component render function.`
                    ),
                    watcher.vm
                )
                break
            }
        }
    }

    const activatedQueue = activatedChildren.slice()
    const updatedQueue = queue.slice()
    resetSchedulerState()

    callActivatedHooks(activatedQueue)
    callUpdatedHooks(updatedQueue)

    if (devtools && config.devtools) {
        devtools.emit('flush')
    }
}

這個方法就是具體的執(zhí)行更新隊列的所在阁最,首先就是將flushing置為true,然后就是將queue隊列按照watcher.id從小到大排列這是有門道的主要是以下三點:

  • 組件更新是父到子的骇两,先創(chuàng)建父然后是子速种,所以需要父在前
  • userWatchrenderWatch之前,因為userWatch定義的更早
  • 若是一個組件在父組件的watcher執(zhí)行期間被銷毀低千,那么子組件的watcher自然也得跳過配阵,所以父組件的先執(zhí)行
for (index = 0; index < queue.length; index++) {
    // ...
}

這里就是存儲執(zhí)行更新時當前watcher的索引index的地方,這里有個點需要注意的是不存儲queue.length示血,因為在執(zhí)行更新中queue可能會變化

watcher = queue[index]
if (watcher.before) {
    watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()

這里就是before鉤子所在

new Watcher(vm, updateComponent, noop, {
    before() {
        if (vm._isMounted) {
            callHook(vm, 'beforeUpdate')
        }
    }
}, true /* isRenderWatcher */ )

就像這個renderWatcher就有傳入before

這就是beforeUpdate所在

然后就是給當前watcher移出has這個記錄表棋傍,表示這個idwatcher已經(jīng)處理了,可以繼續(xù)入隊难审,因為執(zhí)行更新中也可能有watcher入隊瘫拣,然后執(zhí)行watcher.run()

if (process.env.NODE_ENV !== 'production' && has[id] != null) {
    circular[id] = (circular[id] || 0) + 1
    if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
            'You may have an infinite update loop ' + (
                watcher.user
                    ? `in watcher with expression "${watcher.expression}"`
                    : `in a component render function.`
            ),
            watcher.vm
        )
        break
    }
}

這段很重要,就是在開發(fā)環(huán)境下對無限循環(huán)的一個提示告喊。因為watcher.runwatcher回調(diào))可能會導(dǎo)致has[id]有值麸拄,如下所示:

new Vue({
    data: {
        a: 1
    },
    watch: {
        'a': function aCallback(nVal, oVal) {
            this.a = Math.random()
        }
    },
    template: `<div @click="change">
        {{a}}
        </div>`,
    methods: {
        change() {
            this.a = 2
        }
    }
}).$mount('#app')

在執(zhí)行到flushSchedulerQueue時,queue會有倆個watchera:userWatcher黔姜、renderWatcher
首先是userWatcher拢切,在watcher.run()(也就是aCallback這個回調(diào)函數(shù))之前has[id] = null,然后執(zhí)行了這個aCallback又給this.a賦值秆吵,這樣子就是在執(zhí)行更新中watcher入隊(set() -> dep.notify() -> watcher.update() -> queueWatcher(this)
如此一來執(zhí)行到queue下一個項其實還是當前這個userWatcher淮椰,就沒完沒了了
所以這里定了個規(guī)矩,就是這同一個watcher執(zhí)行了MAX_UPDATE_COUNT也就是100次那說明這個有問題纳寂,可能就是無限循環(huán)了实苞。circular就是這么個標識變量

const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()

這里我們先看updatedQueue,它是queue的淺拷貝對象烈疚。這是因為緊隨其后調(diào)用了resetSchedulerState黔牵,若不淺拷貝的話queue就被置空了,這也杜絕了queue被影響

// 重置scheduler狀態(tài)
function resetSchedulerState() {
    index = queue.length = activatedChildren.length = 0
    has = {}
    // 若是開發(fā)環(huán)境爷肝,那么就每輪更新執(zhí)行之后置空這個無限循環(huán)檢測標志
    // 這是因為下面檢測也是開發(fā)環(huán)境下檢測的
    // 也就是默認生存環(huán)境下不會出現(xiàn)這種糟糕的代碼
    if (process.env.NODE_ENV !== 'production') {
        circular = {}
    }
    waiting = flushing = false
}

他就是重置scheduler里這么些方法所用到的標識變量

這里只在非生產(chǎn)環(huán)境重置了circular猾浦,這就代表生存環(huán)境下不會出現(xiàn)這種糟糕的代碼

callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)

然后就是callUpdatedHooks

// 執(zhí)行updated鉤子
function callUpdatedHooks(queue) {
    let i = queue.length
    while (i--) {
        const watcher = queue[i]
        const vm = watcher.vm
        // 要是當前這個watcher是渲染watcher陆错,而且已經(jīng)掛載了,那么觸發(fā)updated鉤子
        if (vm._watcher === watcher && vm._isMounted) {
            callHook(vm, 'updated')
        }
    }
}

這里就是統(tǒng)一調(diào)用update鉤子金赦,這個和before非統(tǒng)一調(diào)用不一樣音瓷,這里通過watcher獲取到vm也就是當前的實例對象,這樣子就可以取到vm. _watcher也就是renderWatcher夹抗,也可以取到vm._isMounted绳慎。有了這倆個條件就可以調(diào)用生命周期update

這就是updated所在

if (devtools && config.devtools) {
    devtools.emit('flush')
}

這里是開發(fā)者工具的事件傳遞flush

queueActivatedComponent

這個其實是入隊激活的組件,類似queueWatcher入隊watcher

export function queueActivatedComponent(vm: Component) {
    // setting _inactive to false here so that a render function can
    // rely on checking whether it's in an inactive tree (e.g. router-view)
    vm._inactive = false
    activatedChildren.push(vm)
}

這里就是將入隊的實例的激活狀態(tài)(_inactive)置為激活漠烧,然后入隊到activatedChildren以待后用

function callActivatedHooks(queue) {
    for (let i = 0; i < queue.length; i++) {
        queue[i]._inactive = true
        activateChildComponent(queue[i], true /* true */)
    }
}

這個其實就是統(tǒng)一遍歷調(diào)用actibated鉤子杏愤,給每一項實例的激活狀態(tài)(_inactive)置為未激活(從未激活到激活)
注意這里activateChildComponent傳入的第二參數(shù)是true,在講到keep-alive詳解

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末已脓,一起剝皮案震驚了整個濱河市珊楼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌度液,老刑警劉巖厕宗,帶你破解...
    沈念sama閱讀 218,640評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異堕担,居然都是意外死亡已慢,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,254評論 3 395
  • 文/潘曉璐 我一進店門霹购,熙熙樓的掌柜王于貴愁眉苦臉地迎上來佑惠,“玉大人,你說我怎么就攤上這事厕鹃【ぱ觯” “怎么了乍丈?”我有些...
    開封第一講書人閱讀 165,011評論 0 355
  • 文/不壞的土叔 我叫張陵剂碴,是天一觀的道長。 經(jīng)常有香客問我轻专,道長忆矛,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,755評論 1 294
  • 正文 為了忘掉前任请垛,我火速辦了婚禮催训,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘宗收。我一直安慰自己漫拭,他們只是感情好,可當我...
    茶點故事閱讀 67,774評論 6 392
  • 文/花漫 我一把揭開白布混稽。 她就那樣靜靜地躺著采驻,像睡著了一般审胚。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上礼旅,一...
    開封第一講書人閱讀 51,610評論 1 305
  • 那天膳叨,我揣著相機與錄音,去河邊找鬼痘系。 笑死菲嘴,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的汰翠。 我是一名探鬼主播龄坪,決...
    沈念sama閱讀 40,352評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼奴璃!你這毒婦竟也來了悉默?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,257評論 0 276
  • 序言:老撾萬榮一對情侶失蹤苟穆,失蹤者是張志新(化名)和其女友劉穎抄课,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體雳旅,經(jīng)...
    沈念sama閱讀 45,717評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡跟磨,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,894評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了攒盈。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片抵拘。...
    茶點故事閱讀 40,021評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖型豁,靈堂內(nèi)的尸體忽然破棺而出僵蛛,到底是詐尸還是另有隱情,我是刑警寧澤迎变,帶...
    沈念sama閱讀 35,735評論 5 346
  • 正文 年R本政府宣布充尉,位于F島的核電站,受9級特大地震影響衣形,放射性物質(zhì)發(fā)生泄漏驼侠。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,354評論 3 330
  • 文/蒙蒙 一谆吴、第九天 我趴在偏房一處隱蔽的房頂上張望倒源。 院中可真熱鬧,春花似錦句狼、人聲如沸笋熬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,936評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽胳螟。三九已至苫拍,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間旺隙,已是汗流浹背绒极。 一陣腳步聲響...
    開封第一講書人閱讀 33,054評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蔬捷,地道東北人垄提。 一個月前我還...
    沈念sama閱讀 48,224評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像周拐,于是被迫代替她去往敵國和親铡俐。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,974評論 2 355

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