久久福利_99r_国产日韩在线视频_直接看av的网站_中文欧美日韩_久久一

您的位置:首頁(yè)技術(shù)文章
文章詳情頁(yè)

Vue的data、computed、watch源碼淺談

瀏覽:2日期:2023-01-28 18:09:32

導(dǎo)讀

記得初學(xué)Vue源碼的時(shí)候,在defineReactive、Observer、Dep、Watcher等等內(nèi)部設(shè)計(jì)源碼之間跳來(lái)跳去,發(fā)現(xiàn)再也繞不出來(lái)了。Vue發(fā)展了很久,很多fix和feature的增加讓內(nèi)部源碼越來(lái)越龐大,太多的邊界情況和優(yōu)化設(shè)計(jì)掩蓋了原本精簡(jiǎn)的代碼設(shè)計(jì),讓新手閱讀源碼變得越來(lái)越困難,但是面試的時(shí)候,Vue的響應(yīng)式原理幾乎成了Vue技術(shù)棧的公司面試中高級(jí)前端必問(wèn)的點(diǎn)之一。

這篇文章通過(guò)自己實(shí)現(xiàn)一個(gè)響應(yīng)式系統(tǒng),盡量還原和Vue內(nèi)部源碼同樣結(jié)構(gòu),但是剔除掉和渲染、優(yōu)化等等相關(guān)的代碼,來(lái)最低成本的學(xué)習(xí)Vue的響應(yīng)式原理。

預(yù)覽

源碼地址(ts):https://github.com/sl1673495/vue-reactive

源碼地址(js)https://github.com/sl1673495/vue-reactive/tree/js-version

預(yù)覽地址:https://sl1673495.github.io/vue-reactive/

reactive

Vue最常用的就是響應(yīng)式的data了,通過(guò)在vue中定義

new Vue({ data() { return { msg: ’Hello World’ } }})

在data發(fā)生改變的時(shí)候,視圖也會(huì)更新,在這篇文章里我把對(duì)data部分的處理單獨(dú)提取成一個(gè)api:reactive,下面來(lái)一起實(shí)現(xiàn)這個(gè)api。

要實(shí)現(xiàn)的效果:

const data = reactive({ msg: ’Hello World’,})new Watcher(() => { document.getElementById(’app’).innerHTML = `msg is ${data.msg}`})

在data.msg發(fā)生改變的時(shí)候,我們需要這個(gè)app節(jié)點(diǎn)的innerHTML同步更新,這里新增加了一個(gè)概念Watcher,這也是Vue源碼內(nèi)部的一個(gè)設(shè)計(jì),想要實(shí)現(xiàn)響應(yīng)式的系統(tǒng),這個(gè)Watcher是必不可缺的。

在實(shí)現(xiàn)這兩個(gè)api之前,我們先來(lái)理清他們之間的關(guān)系,reactive這個(gè)api定義了一個(gè)響應(yīng)式的數(shù)據(jù),其實(shí)大家都知道響應(yīng)式的數(shù)據(jù)就是在它的某個(gè)屬性(比如例中的data.msg)被讀取的時(shí)候,記錄下來(lái)這時(shí)候是誰(shuí)在讀取他,讀取他的這個(gè)函數(shù)肯定依賴它。在本例中,下面這段函數(shù),因?yàn)樽x取了data.msg并且展示在頁(yè)面上,所以可以說(shuō)這段渲染函數(shù)依賴了data.msg。

// 渲染函數(shù)document.getElementById(’app’).innerHTML = `msg is ${data.msg}`

這也就解釋清了,為什么我們需要用new Watcher來(lái)傳入這段渲染函數(shù),我們已經(jīng)可以分析出來(lái)Watcher是幫我們記錄下來(lái)這段渲染函數(shù)依賴的關(guān)鍵。

在js引擎執(zhí)行渲染函數(shù)的途中,突然讀到了data.msg,data已經(jīng)被定義成了響應(yīng)式數(shù)據(jù),讀取data.msg時(shí)所觸發(fā)的get函數(shù)已經(jīng)被我們劫持,這個(gè)get函數(shù)中我們?nèi)ビ涗浵耫ata.msg被這個(gè)渲染函數(shù)所依賴,然后再返回data.msg的值。

這樣下次data.msg發(fā)生變化的時(shí)候,Watcher內(nèi)部所做的一些邏輯就會(huì)通知到渲染函數(shù)去重新執(zhí)行。這不就是響應(yīng)式的原理嘛。

下面開(kāi)始實(shí)現(xiàn)代碼

import Dep from ’./dep’import { isObject } from ’../utils’// 將對(duì)象定義為響應(yīng)式export default function reactive(data) { if (isObject(data)) { Object.keys(data).forEach(key => { defineReactive(data, key) }) } return data}function defineReactive(data, key) { let val = data[key] // 收集依賴 const dep = new Dep() Object.defineProperty(data, key, { get() { dep.depend() return val }, set(newVal) { val = newVal dep.notify() } }) if (isObject(val)) { reactive(val) }}

代碼很簡(jiǎn)單,就是去遍歷data的key,在defineReactive函數(shù)中對(duì)每個(gè)key進(jìn)行g(shù)et和set的劫持,Dep是一個(gè)新的概念,它主要用來(lái)做上面所說(shuō)的dep.depend()去收集當(dāng)前正在運(yùn)行的渲染函數(shù)和dep.notify() 觸發(fā)渲染函數(shù)重新執(zhí)行。

可以把dep看成一個(gè)收集依賴的小筐,每當(dāng)運(yùn)行渲染函數(shù)讀取到data的某個(gè)key的時(shí)候,就把這個(gè)渲染函數(shù)丟到這個(gè)key自己的小筐中,在這個(gè)key的值發(fā)生改變的時(shí)候,去key的筐中找到所有的渲染函數(shù)再執(zhí)行一遍。

Dep

export default class Dep { constructor() { this.deps = new Set() } depend() { if (Dep.target) { this.deps.add(Dep.target) } } notify() { this.deps.forEach(watcher => watcher.update()) }}// 正在運(yùn)行的watcherDep.target = null

這個(gè)類(lèi)很簡(jiǎn)單,利用Set去做存儲(chǔ),在depend的時(shí)候把Dep.target加入到deps集合里,在notify的時(shí)候遍歷deps,觸發(fā)每個(gè)watcher的update。

沒(méi)錯(cuò)Dep.target這個(gè)概念也是Vue中所引入的,它是一個(gè)掛在Dep類(lèi)上的全局變量,js是單線程運(yùn)行的,所以在渲染函數(shù)如:

document.getElementById(’app’).innerHTML = `msg is ${data.msg}`

運(yùn)行之前,先把全局的Dep.target設(shè)置為存儲(chǔ)了這個(gè)渲染函數(shù)的watcher,也就是:

new Watcher(() => { document.getElementById(’app’).innerHTML = `msg is ${data.msg}`})

這樣在運(yùn)行途中data.msg就可以通過(guò)Dep.target找到當(dāng)前是哪個(gè)渲染函數(shù)的watcher正在運(yùn)行,這樣也就可以把自身對(duì)應(yīng)的依賴所收集起來(lái)了。

這里劃重點(diǎn):Dep.target一定是一個(gè)Watcher的實(shí)例。

又因?yàn)殇秩竞瘮?shù)可以是嵌套運(yùn)行的,比如在Vue中每個(gè)組件都會(huì)有自己用來(lái)存放渲染函數(shù)的一個(gè)watcher,那么在下面這種組件嵌套組件的情況下:

// Parent組件<template> <div> <Son組件 /> </div></template>

watcher的運(yùn)行路徑就是: 開(kāi)始 -> ParentWatcher -> SonWatcher -> ParentWatcher -> 結(jié)束。

是不是特別像函數(shù)運(yùn)行中的入棧出棧,沒(méi)錯(cuò),Vue內(nèi)部就是用了棧的數(shù)據(jù)結(jié)構(gòu)來(lái)記錄watcher的運(yùn)行軌跡。

// watcher棧const targetStack = []// 將上一個(gè)watcher推到棧里,更新Dep.target為傳入的_target變量。export function pushTarget(_target) { if (Dep.target) targetStack.push(Dep.target) Dep.target = _target}// 取回上一個(gè)watcher作為Dep.target,并且棧里要彈出上一個(gè)watcher。export function popTarget() { Dep.target = targetStack.pop()}

有了這些輔助的工具,就可以來(lái)看看Watcher的具體實(shí)現(xiàn)了

import Dep, { pushTarget, popTarget } from ’./dep’export default class Watcher { constructor(getter) { this.getter = getter this.get() } get() { pushTarget(this) this.value = this.getter() popTarget() return this.value } update() { this.get() }}

回顧一下開(kāi)頭示例中Watcher的使用。

const data = reactive({ msg: ’Hello World’,})new Watcher(() => { document.getElementById(’app’).innerHTML = `msg is ${data.msg}`})

傳入的getter函數(shù)就是

() => { document.getElementById(’app’).innerHTML = `msg is ${data.msg}`}

在構(gòu)造函數(shù)中,記錄下getter函數(shù),并且執(zhí)行了一遍get

get() { pushTarget(this) this.value = this.getter() popTarget() return this.value }

在這個(gè)函數(shù)中,this就是這個(gè)watcher實(shí)例,在執(zhí)行g(shù)et的開(kāi)頭先把這個(gè)存儲(chǔ)了渲染函數(shù)的watcher設(shè)置為當(dāng)前的Dep.target,然后執(zhí)行this.getter()也就是渲染函數(shù)

在執(zhí)行渲染函數(shù)的途中讀取到了data.msg,就觸發(fā)了defineReactive函數(shù)中劫持的get:

Object.defineProperty(data, key, { get() { dep.depend() return val } })

這時(shí)候的dep.depend函數(shù):

depend() { if (Dep.target) { this.deps.add(Dep.target) } }

所收集到的Dep.target,就是在get函數(shù)開(kāi)頭中pushTarget(this)所收集的

new Watcher(() => { document.getElementById(’app’).innerHTML = `msg is ${data.msg}`})

這個(gè)watcher實(shí)例了。

此時(shí)我們假如執(zhí)行了這樣一段賦值代碼:

data.msg = ’ssh’

就會(huì)運(yùn)行到劫持的set函數(shù)里:

Object.defineProperty(data, key, { set(newVal) { val = newVal dep.notify() } })

此時(shí)在控制臺(tái)中打印出dep這個(gè)變量,它內(nèi)部的deps屬性果然存儲(chǔ)了一個(gè)Watcher的實(shí)例。

Vue的data、computed、watch源碼淺談

運(yùn)行了dep.notify以后,就會(huì)觸發(fā)這個(gè)watcher的update方法,也就會(huì)再去重新執(zhí)行一遍渲染函數(shù)了,這個(gè)時(shí)候視圖就刷新了。

computed

在實(shí)現(xiàn)了reactive這個(gè)基礎(chǔ)api以后,就要開(kāi)始實(shí)現(xiàn)computed這個(gè)api了,這個(gè)api的用法是這樣:

const data = reactive({ number: 1})const numberPlusOne = computed(() => data.number + 1)// 渲染函數(shù)watchernew Watcher(() => { document.getElementById(’app2’).innerHTML = ` computed: 1 + number 是 ${numberPlusOne.value} `})

vue內(nèi)部是把computed屬性定義在vm實(shí)例上的,這里我們沒(méi)有實(shí)例,所以就用一個(gè)對(duì)象來(lái)存儲(chǔ)computed的返回值,用.value來(lái)拿computed的真實(shí)值。

這里computed傳入的其實(shí)還是一個(gè)函數(shù),這里我們回想一下Watcher的本質(zhì),其實(shí)就是存儲(chǔ)了一個(gè)需要在特定時(shí)機(jī)觸發(fā)的函數(shù),在Vue內(nèi)部,每個(gè)computed屬性也有自己的一個(gè)對(duì)應(yīng)的watcher實(shí)例,下文中叫它c(diǎn)omputedWatcher

先看渲染函數(shù):

// 渲染函數(shù)watchernew Watcher(() => { document.getElementById(’app2’).innerHTML = ` computed: 1 + number 是 ${numberPlusOne.value} `})

這段渲染函數(shù)執(zhí)行過(guò)程中,讀取到numberPlusOne的值的時(shí)候

首先會(huì)把Dep.target設(shè)置為numberPlusOne所對(duì)應(yīng)的computedWatcher

computedWatcher的特殊之處在于

渲染watcher只能作為依賴被收集到其他的dep筐子里,而computedWatcher實(shí)例上有屬于自己的dep,它可以收集別的watcher作為自己的依賴。 惰性求值,初始化的時(shí)候先不去運(yùn)行g(shù)etter。

export default class Watcher { constructor(getter, options = {}) { const { computed } = options this.getter = getter this.computed = computed if (computed) { this.dep = new Dep() } else { this.get() } }}

其實(shí)computed實(shí)現(xiàn)的本質(zhì)就是,computed在讀取value之前,Dep.target肯定此時(shí)是正在運(yùn)行的渲染函數(shù)的watcher。

先把當(dāng)前正在運(yùn)行的渲染函數(shù)的watcher作為依賴收集到computedWatcher內(nèi)部的dep筐子里。

把自身computedWatcher設(shè)置為 全局Dep.target,然后開(kāi)始求值:

求值函數(shù)會(huì)在運(yùn)行() => data.number + 1的途中遇到data.number的讀取,這時(shí)又會(huì)觸發(fā)’number’這個(gè)key的劫持get函數(shù),這時(shí)全局的Dep.target是computedWatcher,data.number的dep依賴筐子里丟進(jìn)去了computedWatcher。此時(shí)的依賴關(guān)系是 data.number的dep筐子里裝著computedWatcher,computedWatcher的dep筐子里裝著渲染watcher。此時(shí)如果更新data.number的話,會(huì)一級(jí)一級(jí)往上觸發(fā)更新。會(huì)觸發(fā)computedWatcher的update,我們肯定會(huì)對(duì)被設(shè)置為computed特性的watcher做特殊的處理,這個(gè)watcher的筐子里裝著渲染watcher,所以只需要觸發(fā) this.dep.notify(),就會(huì)觸發(fā)渲染watcher的update方法,從而更新視圖。下面來(lái)改造代碼:

// Watcherimport Dep, { pushTarget, popTarget } from ’./dep’export default class Watcher { constructor(getter, options = {}) { const { computed } = options this.getter = getter this.computed = computed if (computed) { this.dep = new Dep() } else { this.get() } } get() { pushTarget(this) this.value = this.getter() popTarget() return this.value } // 僅為computed使用 depend() { this.dep.depend() } update() { if (this.computed) { this.get() this.dep.notify() } else { this.get() } }}

computed初始化:

// computedimport Watcher from ’./watcher’export default function computed(getter) { let def = {} const computedWatcher = new Watcher(getter, { computed: true }) Object.defineProperty(def, ’value’, { get() { // 先讓computedWatcher收集渲染watcher作為自己的依賴。 computedWatcher.depend() return computedWatcher.get() } }) return def}

這里的邏輯比較繞,如果沒(méi)理清楚的話可以把代碼下載下來(lái)一步步斷點(diǎn)調(diào)試,data.number被劫持的set觸發(fā)以后,可以看一下number的dep到底存了什么。

Vue的data、computed、watch源碼淺談

watch

watch的使用方式是這樣的:

watch( () => data.msg, (newVal, oldVal) => { console.log(’newVal: ’, newVal) console.log(’old: ’, oldVal) })

傳入的第一個(gè)參數(shù)是個(gè)函數(shù),里面需要讀取到響應(yīng)式的屬性,確保依賴能被收集到,這樣下次這個(gè)響應(yīng)式的屬性發(fā)生改變后,就會(huì)打印出對(duì)飲的新值和舊值。

分析一下watch的實(shí)現(xiàn)原理,這里依然是利用Watcher類(lèi)去實(shí)現(xiàn),我們把用于watch的watcher叫做watchWatcher,傳入的getter函數(shù)也就是() => data.msg,Watcher在執(zhí)行它之前還是一樣會(huì)把自身(也就是watchWatcher)設(shè)為Dep.target,這時(shí)讀到data.msg,就會(huì)把watchWatcher丟進(jìn)data.msg的依賴筐子里。

如果data.msg更新了,則就會(huì)觸發(fā)watchWatcher的update方法

直接上代碼:

// watchimport Watcher from ’./watcher’export default function watch(getter, callback) { new Watcher(getter, { watch: true, callback })}

沒(méi)錯(cuò)又是直接用了getter,只是這次傳入的選項(xiàng)是{ watch: true, callback },接下來(lái)看看Watcher內(nèi)部進(jìn)行了什么處理:

export default class Watcher { constructor(getter, options = {}) { const { computed, watch, callback } = options this.getter = getter this.computed = computed this.watch = watch this.callback = callback this.value = undefined if (computed) { this.dep = new Dep() } else { this.get() } }}

首先是構(gòu)造函數(shù)中,對(duì)watch選項(xiàng)和callback進(jìn)行了保存,其他沒(méi)變。

然后在update方法中。

update() { if (this.computed) { ... } else if (this.watch) { const oldValue = this.value this.get() this.callback(oldValue, this.value) } else { ... } }

在調(diào)用this.get去更新值之前,先把舊值保存起來(lái),然后把新值和舊值一起通過(guò)調(diào)用callback函數(shù)交給外部,就這么簡(jiǎn)單。我們僅僅是改動(dòng)寥寥幾行代碼,就輕松實(shí)現(xiàn)了非常重要的api:watch。

總結(jié)。

有了精妙的Watcher和Dep的設(shè)計(jì),Vue內(nèi)部的響應(yīng)式api實(shí)現(xiàn)的非常簡(jiǎn)單,不得不再次感嘆一下尤大真是厲害啊!

到此這篇關(guān)于Vue的data、computed、watch源碼淺談的文章就介紹到這了,更多相關(guān)Vue data、computed、watch源碼內(nèi)容請(qǐng)搜索好吧啦網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持好吧啦網(wǎng)!

標(biāo)簽: Vue
相關(guān)文章:
主站蜘蛛池模板: 日韩中文字幕在线看 | 精品久久久久久久久久久久久 | 午夜精品亚洲日日做天天做 | 日本黄色免费播放 | 欧美午夜在线观看 | 99精品不卡 | 色香阁99久久精品久久久 | 日本成人小视频 | 久久精品色欧美aⅴ一区二区 | 日日摸天天爽天天爽视频 | 久久久精品欧美 | 国产精品亚洲一区二区三区 | 射久久 | 欧美日日干| 亚洲欧美一区二区三区在线 | 三级黄视频在线观看 | 日韩成人在线一区 | 国产精品一卡二卡三卡 | 亚洲精品久久久久久下一站 | 亚洲a在线观看 | 中文字幕亚洲一区二区三区 | 91精品国产高清自在线观看 | 国产精品香蕉 | 欧美激情欧美激情在线五月 | 欧美一级二级三级视频 | 欧日韩免费 | 久久香蕉国产视频 | 美国黄色毛片女人性生活片 | 国产在线观看一区二区三区 | 91精品国产欧美一区二区成人 | 激情小网站| 黄色成人在线网站 | 99热在线精品播放 | 黄色av网站免费 | 亚洲视频一区在线 | 精品一区二区三区免费视频 | 精久久| 久久婷婷国产麻豆91天堂 | 欧美日韩中文字幕 | 欧美激情综合五月色丁香小说 | 精品国产乱码久久久久久1区2区 |