中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Vue的data、computed、watch是什么

發布時間:2021-03-11 16:47:16 來源:億速云 閱讀:205 作者:TREX 欄目:web開發

本篇內容主要講解“Vue的data、computed、watch是什么”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“Vue的data、computed、watch是什么”吧!

導讀

記得初學Vue源碼的時候,在defineReactive、Observer、Dep、Watcher等等內部設計源碼之間跳來跳去,發現再也繞不出來了。Vue發展了很久,很多fix和feature的增加讓內部源碼越來越龐大,太多的邊界情況和優化設計掩蓋了原本精簡的代碼設計,讓新手閱讀源碼變得越來越困難,但是面試的時候,Vue的響應式原理幾乎成了Vue技術棧的公司面試中高級前端必問的點之一。

這篇文章通過自己實現一個響應式系統,盡量還原和Vue內部源碼同樣結構,但是剔除掉和渲染、優化等等相關的代碼,來最低成本的學習Vue的響應式原理。

預覽

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

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

預覽地址:
https://sl1673495.github.io/vue-reactive/

reactive

Vue最常用的就是響應式的data了,通過在vue中定義

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

在data發生改變的時候,視圖也會更新,在這篇文章里我把對data部分的處理單獨提取成一個api:reactive,下面來一起實現這個api。

要實現的效果:

const data = reactive({
 msg: 'Hello World',
})

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

在data.msg發生改變的時候,我們需要這個app節點的innerHTML同步更新,這里新增加了一個概念Watcher,這也是Vue源碼內部的一個設計,想要實現響應式的系統,這個Watcher是必不可缺的。

在實現這兩個api之前,我們先來理清他們之間的關系,reactive這個api定義了一個響應式的數據,其實大家都知道響應式的數據就是在它的某個屬性(比如例中的data.msg)被讀取的時候,記錄下來這時候是誰在讀取他,讀取他的這個函數肯定依賴它。
在本例中,下面這段函數,因為讀取了data.msg并且展示在頁面上,所以可以說這段渲染函數依賴了data.msg。

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

這也就解釋清了,為什么我們需要用new Watcher來傳入這段渲染函數,我們已經可以分析出來Watcher是幫我們記錄下來這段渲染函數依賴的關鍵。

在js引擎執行渲染函數的途中,突然讀到了data.msg,data已經被定義成了響應式數據,讀取data.msg時所觸發的get函數已經被我們劫持,這個get函數中我們去記錄下data.msg被這個渲染函數所依賴,然后再返回data.msg的值。

這樣下次data.msg發生變化的時候,Watcher內部所做的一些邏輯就會通知到渲染函數去重新執行。這不就是響應式的原理嘛。

下面開始實現代碼

import Dep from './dep'
import { isObject } from '../utils'

// 將對象定義為響應式
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)
 }
}

代碼很簡單,就是去遍歷data的key,在defineReactive函數中對每個key進行get和set的劫持,Dep是一個新的概念,它主要用來做上面所說的dep.depend()去收集當前正在運行的渲染函數和dep.notify() 觸發渲染函數重新執行。

可以把dep看成一個收集依賴的小筐,每當運行渲染函數讀取到data的某個key的時候,就把這個渲染函數丟到這個key自己的小筐中,在這個key的值發生改變的時候,去key的筐中找到所有的渲染函數再執行一遍。

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())
 }
}

// 正在運行的watcher
Dep.target = null

這個類很簡單,利用Set去做存儲,在depend的時候把Dep.target加入到deps集合里,在notify的時候遍歷deps,觸發每個watcher的update。

沒錯Dep.target這個概念也是Vue中所引入的,它是一個掛在Dep類上的全局變量,js是單線程運行的,所以在渲染函數如:

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

運行之前,先把全局的Dep.target設置為存儲了這個渲染函數的watcher,也就是:

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

這樣在運行途中data.msg就可以通過Dep.target找到當前是哪個渲染函數的watcher正在運行,這樣也就可以把自身對應的依賴所收集起來了。

這里劃重點:Dep.target一定是一個Watcher的實例。

又因為渲染函數可以是嵌套運行的,比如在Vue中每個組件都會有自己用來存放渲染函數的一個watcher,那么在下面這種組件嵌套組件的情況下:

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

watcher的運行路徑就是: 開始 -> ParentWatcher -> SonWatcher -> ParentWatcher -> 結束。

是不是特別像函數運行中的入棧出棧,沒錯,Vue內部就是用了棧的數據結構來記錄watcher的運行軌跡。

// watcher棧
const targetStack = []

// 將上一個watcher推到棧里,更新Dep.target為傳入的_target變量。
export function pushTarget(_target) {
 if (Dep.target) targetStack.push(Dep.target)
 Dep.target = _target
}

// 取回上一個watcher作為Dep.target,并且棧里要彈出上一個watcher。
export function popTarget() {
 Dep.target = targetStack.pop()
}

有了這些輔助的工具,就可以來看看Watcher的具體實現了

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()
 }
}

回顧一下開頭示例中Watcher的使用。

const data = reactive({
 msg: 'Hello World',
})

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

傳入的getter函數就是

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

在構造函數中,記錄下getter函數,并且執行了一遍get

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

在這個函數中,this就是這個watcher實例,在執行get的開頭先把這個存儲了渲染函數的watcher設置為當前的Dep.target,然后執行this.getter()也就是渲染函數

在執行渲染函數的途中讀取到了data.msg,就觸發了defineReactive函數中劫持的get:

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

這時候的dep.depend函數:

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

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

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

這個watcher實例了。

此時我們假如執行了這樣一段賦值代碼:

data.msg = 'ssh'

就會運行到劫持的set函數里:

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

此時在控制臺中打印出dep這個變量,它內部的deps屬性果然存儲了一個Watcher的實例。

Vue的data、computed、watch是什么

運行了dep.notify以后,就會觸發這個watcher的update方法,也就會再去重新執行一遍渲染函數了,這個時候視圖就刷新了。

computed

在實現了reactive這個基礎api以后,就要開始實現computed這個api了,這個api的用法是這樣:

const data = reactive({
 number: 1
})

const numberPlusOne = computed(() => data.number + 1)

// 渲染函數watcher
new Watcher(() => {
 document.getElementById('app2').innerHTML = `
  computed: 1 + number 是 ${numberPlusOne.value}
 `
})

vue內部是把computed屬性定義在vm實例上的,這里我們沒有實例,所以就用一個對象來存儲computed的返回值,用.value來拿computed的真實值。

這里computed傳入的其實還是一個函數,這里我們回想一下Watcher的本質,其實就是存儲了一個需要在特定時機觸發的函數,在Vue內部,每個computed屬性也有自己的一個對應的watcher實例,下文中叫它computedWatcher

先看渲染函數:

// 渲染函數watcher
new Watcher(() => {
 document.getElementById('app2').innerHTML = `
  computed: 1 + number 是 ${numberPlusOne.value}
 `
})

這段渲染函數執行過程中,讀取到numberPlusOne的值的時候

首先會把Dep.target設置為numberPlusOne所對應的computedWatcher

computedWatcher的特殊之處在于

  1. 渲染watcher只能作為依賴被收集到其他的dep筐子里,而computedWatcher實例上有屬于自己的dep,它可以收集別的watcher作為自己的依賴。

  2. 惰性求值,初始化的時候先不去運行getter。

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()
  }
 }
}

其實computed實現的本質就是,computed在讀取value之前,Dep.target肯定此時是正在運行的渲染函數的watcher。

先把當前正在運行的渲染函數的watcher作為依賴收集到computedWatcher內部的dep筐子里。

把自身computedWatcher設置為 全局Dep.target,然后開始求值:

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

// Watcher
import 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初始化:

// computed
import 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
}

這里的邏輯比較繞,如果沒理清楚的話可以把代碼下載下來一步步斷點調試,data.number被劫持的set觸發以后,可以看一下number的dep到底存了什么。

Vue的data、computed、watch是什么

watch

watch的使用方式是這樣的:

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

傳入的第一個參數是個函數,里面需要讀取到響應式的屬性,確保依賴能被收集到,這樣下次這個響應式的屬性發生改變后,就會打印出對飲的新值和舊值。

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

如果data.msg更新了,則就會觸發watchWatcher的update方法

直接上代碼:

// watch
import Watcher from './watcher'

export default function watch(getter, callback) {
 new Watcher(getter, { watch: true, callback })
}

沒錯又是直接用了getter,只是這次傳入的選項是{ watch: true, callback },接下來看看Watcher內部進行了什么處理:

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()
  }
 }
}

首先是構造函數中,對watch選項和callback進行了保存,其他沒變。

然后在update方法中。

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

在調用this.get去更新值之前,先把舊值保存起來,然后把新值和舊值一起通過調用callback函數交給外部,就這么簡單。
我們僅僅是改動寥寥幾行代碼,就輕松實現了非常重要的api:watch。

總結。

有了精妙的Watcher和Dep的設計,Vue內部的響應式api實現的非常簡單,不得不再次感嘆一下尤大真是厲害啊!

到此,相信大家對“Vue的data、computed、watch是什么”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

海阳市| 宁安市| 双江| 平南县| 黄骅市| 泰兴市| 许昌市| 莱阳市| 孝感市| 南昌市| 四子王旗| 金湖县| 卢龙县| 修文县| 龙海市| 个旧市| 和平县| 绥芬河市| 凌海市| 中阳县| 平果县| 综艺| 姚安县| 辽阳市| 彭泽县| 和硕县| 鄂伦春自治旗| 青龙| 依安县| 灯塔市| 鄂尔多斯市| 兴仁县| 景洪市| 荥阳市| 颍上县| 克山县| 西青区| 大邑县| 西华县| 北票市| 兴海县|