前言
watch 是由用戶定義的資料監聽,當監聽的屬性發生改變就會觸發回呼,這項配置在業務中是很常用,在面試時,也是必問知識點,一般會用作和 computed 進行比較,
那么本文就來帶大家從原始碼理解 watch 的作業流程,以及依賴收集和深度監聽的實作,在此之前,希望你能對回應式原理流程、依賴收集流程有一些了解,這樣理解起來會更加輕松,
往期文章:
手摸手帶你理解Vue回應式原理
手摸手帶你理解Vue的Computed原理
watch 用法
“知己知彼,才能百戰百勝”,分析原始碼之前,先要知道它如何使用,這對于后面理解有一定的輔助作用,
第一種,字串宣告:
var vm = new Vue({
el: '#example',
data: {
message: 'Hello'
},
watch: {
message: 'handler'
},
methods: {
handler (newVal, oldVal) { /* ... */ }
}
})
第二種,函式宣告:
var vm = new Vue({
el: '#example',
data: {
message: 'Hello'
},
watch: {
message: function (newVal, oldVal) { /* ... */ }
}
})
第三種,物件宣告:
var vm = new Vue({
el: '#example',
data: {
peopel: {
name: 'jojo',
age: 15
}
},
watch: {
// 欄位可使用點運算子 監聽物件的某個屬性
'people.name': {
handler: function (newVal, oldVal) { /* ... */ }
}
}
})
watch: {
people: {
handler: function (newVal, oldVal) { /* ... */ },
// 回呼會在監聽開始之后被立即呼叫
immediate: true,
// 物件深度監聽 物件內任意一個屬性改變都會觸發回呼
deep: true
}
}
第四種,陣列宣告:
var vm = new Vue({
el: '#example',
data: {
peopel: {
name: 'jojo',
age: 15
}
},
// 傳入回呼陣列,它們會被逐一呼叫
watch: {
'people.name': [
'handle',
function handle2 (newVal, oldVal) { /* ... */ },
{
handler: function handle3 (newVal, oldVal) { /* ... */ },
}
],
},
methods: {
handler (newVal, oldVal) { /* ... */ }
}
})
作業流程
入口檔案:
// 原始碼位置:/src/core/instance/index.js
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
function Vue (options) {
this._init(options)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
_init:
// 原始碼位置:/src/core/instance/init.js
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
// merge 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 {
// mergeOptions 對 mixin 選項和 new Vue 傳入的 options 選項進行合并
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
// 初始化資料
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
initState:
// 原始碼位置:/src/core/instance/state.js
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = https://www.cnblogs.com/chanwahfung/p/{}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
// 這里會初始化 watch
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
initWatch:
// 原始碼位置:/src/core/instance/state.js
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
// 1
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
// 2
createWatcher(vm, key, handler)
}
}
}
- 陣列宣告的
watch有多個回呼,需要回圈創建監聽 - 其他宣告方式直接創建
createWatcher:
// 原始碼位置:/src/core/instance/state.js
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
// 1
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
// 2
if (typeof handler === 'string') {
handler = vm[handler]
}
// 3
return vm.$watch(expOrFn, handler, options)
}
- 物件宣告的
watch,從物件中取出對應回呼 - 字串宣告的
watch,直接取實體上的方法(注:methods中宣告的方法,可以在實體上直接獲取) expOrFn是watch的key值,$watch用于創建一個“用戶Watcher”
所以在創建資料監聽時,除了 watch 配置外,也可以呼叫實體的 $watch 方法實作同樣的效果,
$watch:
// 原始碼位置:/src/core/instance/state.js
export function stateMixin (Vue: Class<Component>) {
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
// 1
options = options || {}
options.user = true
// 2
const watcher = new Watcher(vm, expOrFn, cb, options)
// 3
if (options.immediate) {
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
// 4
return function unwatchFn () {
watcher.teardown()
}
}
}
stateMixin 在入口檔案就已經呼叫了,為 Vue 的原型添加 $watch 方法,
- 所有“用戶
Watcher”的options,都會帶有user標識 - 創建
watcher,進行依賴收集 immediate為 true 時,立即呼叫回呼- 回傳的函式可以用于取消
watch監聽
依賴收集及更新流程
經過上面的流程后,最侄訓進入 new Watcher 的邏輯,這里面也是依賴收集和更新的觸發點,接下來看看這里面會有哪些操作,
依賴收集
// 原始碼位置:/src/core/observer/watcher.js
export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
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
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
this.value = https://www.cnblogs.com/chanwahfung/p/this.lazy
? undefined
: this.get()
}
}
在 Watcher 建構式內,對傳入的回呼和 options 都進行保存,這不是重點,讓我們來關注下這段代碼:
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
傳進來的 expOrFn 是 watch 的鍵值,因為鍵值可能是 obj.a.b,需要呼叫 parsePath 對鍵值決議,這一步也是依賴收集的關鍵點,它執行后回傳的是一個函式,先不著急 parsePath 做的是什么,先接著流程繼續走,
下一步就是呼叫 get:
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = https://www.cnblogs.com/chanwahfung/p/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)
}
popTarget()
this.cleanupDeps()
}
return value
}
pushTarget 將當前的“用戶Watcher”(即當前實體this) 掛到 Dep.target 上,在收集依賴時,找的就是 Dep.target,然后呼叫 getter 函式,這里就進入 parsePath 的邏輯,
// 原始碼位置:/src/core/util/lang.js
const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\\d]`)
export function parsePath (path: string): any {
if (bailRE.test(path)) {
return
}
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
引數 obj 是 vm 實體,segments 是決議后的鍵值陣列,回圈去獲取每項鍵值的值,觸發它們的“資料劫持get”,接著觸發 dep.depend 收集依賴(依賴就是掛在 Dep.target 的 Watcher),
到這里依賴收集就完成了,從上面我們也得知,每一項鍵值都會被觸發依賴收集,也就是說上面的任何一項鍵值的值發生改變都會觸發 watch 回呼,例如:
watch: {
'obj.a.b.c': function(){}
}
不僅修改 c 會觸發回呼,修改 b、a 以及 obj 同樣觸發回呼,這個設計也是很妙,通過簡單的回圈去為每一項都收集到了依賴,
更新
在更新時首先觸發的是“資料劫持set”,呼叫 dep.notify 通知每一個 watcher 的 update 方法,
update () {
if (this.lazy) { dirty置為true
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
接著就走 queueWatcher 進行異步更新,這里先不講異步更新,只需要知道它最后會呼叫的是 run 方法,
run () {
if (this.active) {
const value = https://www.cnblogs.com/chanwahfung/p/this.get()
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
this.get 獲取新值,呼叫 this.cb,將新值舊值傳入,
深度監聽
深度監聽是 watch 監聽中一項很重要的配置,它能為我們觀察物件中任何一個屬性的變化,
目光再拉回到 get 函式,其中有一段代碼是這樣的:
if (this.deep) {
traverse(value)
}
判斷是否需要深度監聽,呼叫 traverse 并將值傳入
// 原始碼位置:/src/core/observer/traverse.js
const seenObjects = new Set()
export function traverse (val: any) {
_traverse(val, seenObjects)
seenObjects.clear()
}
function _traverse (val: any, seen: SimpleSet) {
let i, keys
const isA = Array.isArray(val)
if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
return
}
if (val.__ob__) {
// 1
const depId = val.__ob__.dep.id
// 2
if (seen.has(depId)) {
return
}
seen.add(depId)
}
// 3
if (isA) {
i = val.length
while (i--) _traverse(val[i], seen)
} else {
keys = Object.keys(val)
i = keys.length
while (i--) _traverse(val[keys[i]], seen)
}
}
depId是每一個被觀察屬性都會有的唯一標識- 去重,防止相同屬性重復執行邏輯
- 根據陣列和物件使用不同的策略,最終目的是遞回獲取每一項屬性,觸發它們的“資料劫持
get”收集依賴,和parsePath的效果是異曲同工
從這里能得出,深度監聽利用遞回進行監聽,肯定會有性能損耗,因為每一項屬性都要走一遍依賴收集流程,所以在業務中盡量避免這類操作,
卸載監聽
這種手段在業務中基本很少用,也不算是重點,屬于那種少用但很有用的方法,它作為 watch 的一部分,這里也講下它的原理,
使用
先來看看它的用法:
data(){
return {
name: 'jojo'
}
}
mounted() {
let unwatchFn = this.$watch('name', () => {})
setTimeout(()=>{
unwatchFn()
}, 10000)
}
使用 $watch 監聽資料后,會回傳一個對應的卸載監聽函式,顧名思義,呼叫它當然就是不會再監聽資料,
原理
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
try {
// 立即呼叫 watch
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
return function unwatchFn () {
watcher.teardown()
}
}
可以看到回傳的 unwatchFn 里實際執行的是 teardown,
teardown () {
if (this.active) {
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}
teardown 里的操作也很簡單,遍歷 deps 呼叫 removeSub 方法,移除當前 watcher 實體,在下一次屬性更新時,也不會通知 watcher 更新了,deps 存盤的是屬性的 dep,
奇怪的地方
在看原始碼時,我發現 watch 有個奇怪的地方,導致它的用法是可以這樣的:
watch:{
name:{
handler: {
handler: {
handler: {
handler: {
handler: {
handler: {
handler: ()=>{console.log(123)},
immediate: true
}
}
}
}
}
}
}
}
一般 handler 是傳遞一個函式作為回呼,但是對于物件型別,內部會進行遞回去獲取,直到值為函式,所以你可以無限套娃傳物件,
遞回的點在 $watch 中的這段代碼:
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
如果你知道這段代碼的實際應用場景麻煩告訴我一下,嘿嘿~
總結
watch 監聽實作利用遍歷獲取屬性,觸發“資料劫持get”逐個收集依賴,這樣做的好處是其上級的屬性發生修改也能執行回呼,
與 data 和 computed 不同,watch 收集依賴的流程是發生在頁面渲染之前,而前兩者是在頁面渲染時進行取值才會收集依賴,
在面試時,如果被問到 computed 和 watch 的異同,我們可以從下面這些點進行回答:
- 一是
computed要依賴data上的屬性變化回傳一個值,watch則是觀察資料觸發回呼; - 二是
computed和watch依賴收集的發生點不同; - 三是
computed的更新需要“渲染Watcher”的輔助,watch不需要,這點在我的上一篇文章有提到,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/51744.html
標籤:JavaScript
上一篇:react 插入html
