感觉自己比较守承诺,写完【Vue原理】$nextTick源码分析,接着立马另一篇原理出世,每天上完班回来之后,晚上回来还得写到11点半,有人说都有孩子了为啥这么拼,我只知道“性格决定命运”,没有充实就没有快乐。
读完这篇的收获:
- Watch的使用方法
- Watch监听的原理
- 加深响应式原理的理解
相信很多人跟我一样,看到watch就想到computed,不管是面试上还是使用上,肯定要懂得他们之间的区别
哈哈,本着精华在前和孰能生巧(多看到几遍)的思想,所以正式写文章前先总结为有以下几点:
- 更新时依赖收集:computed更新的前提需要“渲染Watcher”的配合,因此依赖属性的 subs 中至少会存储两个 Watcher;Watcher不需要
- 触发时机:computed是依赖的值发生变化才会执行;watch是监听定义的属性,只要此属性发生变化,就会触发回调
- 缓存:computed 有缓存机制;watch没有,不管属性有没有变化都会执行
- 应用场景:computed一般应用于不经常变化的、可能高计算量、需要缓存的地方;watch应用于异步请求、可有中间变量(可以配合v-model等指令使用)
在此说下自己的计划下一篇是写响应式原理,其实这篇已经在我的vue双向绑定原理中提到,但是随着理解的深入,还想花时间更易让大家理解抽离出来。同时这篇文章还会更详细。
watch介绍
作用
监听属性的变化
写法
- 字符串声明
watch: {
message: 'handler'
},
methods: {
handler (newVal, oldVal) { /* ... */ }
}
- 函数声明
watch: {
dragList: {
handler: function(newValue, oldValue) {},
// 回调会在监听开始之后被立即调用
immediate: true,
// 对象深度监听 对象内任意一个属性改变都会触发回调
deep: true
}
}
- 对象声明
watch: {
// 使用handler,监听对象的某个属性,
'a.b': {
handler: function (newVal, oldVal) { /* ... */ }
}
}
省去function,直接接函数
watch: {
'a.b.c'() {
}
}
watch: {
a() {} 可以不使用引号
},
直接后面接函数
watch: {
"$props.src": function(val) {}
}
- 数组声明
// 传入回调数组,它们会被逐一调用
watch: {
'a.b': [
'handle',
function handle2 (newVal, oldVal) { /* ... */ },
{
handler: function handle3 (newVal, oldVal) { /* ... */ },
}
],
},
methods: {
handler (newVal, oldVal) { /* ... */ }
}
- vm.$watch( expOrFn, callback, [options] )
// 键路径
vm.$watch('a.b.c', function (newVal, oldVal) {
// 做点什么
})
// 函数
vm.$watch(
function () {
// 表达式 `this.a + this.b` 每次得出一个不同的结果时
// 处理函数都会被调用。
// 这就像监听一个未被定义的计算属性
return this.a + this.b
},
function (newVal, oldVal) {
// 做点什么
}
)
deep属性
为了发现对象内部值的变化,可以在选项参数中指定 deep: true。注意监听数组的变更不需要这么做。
vm.$watch('someObject', callback, {
deep: true
})
vm.someObject.nestedValue = 123
// callback is fired
注意:deep的意思就是深入观察,监听器会一层层的往下遍历,给对象的所有属性都加上这个监听器,但是这样性能开销就会非常大了,任何修改obj里面任何一个属性都会触发这个监听器里的 handler。
优化,我们可以是使用字符串形式监听。
watch: {
'obj.a': {
handler(newName, oldName) {
console.log('obj.a changed');
},
immediate: true,
// deep: true
}
}
复制代码这样Vue.js才会一层一层解析下去,直到遇到属性a,然后才给a设置监听函数。
immediate属性
watch 有一个特点是,最初绑定的时候是不会执行的,要等到 'a' 改变时才执行监听计算。 不过当写了immediate: true,一开始最初绑定的时候就执行callback
在选项参数中指定 immediate: true 将立即以表达式的当前值触发回调:
vm.$watch('a', callback, {
immediate: true
})
// 立即以 `a` 的当前值触发回调
注意在带有 immediate 选项时,你不能在第一次回调时取消侦听给定的 property。
// 这会导致报错
var unwatch = vm.$watch(
'value',
function () {
doSomething()
unwatch()
},
{ immediate: true }
)
如果你仍然希望在回调内部调用一个取消侦听的函数,你应该先检查其函数的可用性:
var unwatch = vm.$watch(
'value',
function () {
doSomething()
if (unwatch) {
unwatch()
}
},
{ immediate: true }
)
取消(注销)watch
vm.$watch 返回一个取消观察函数,用来停止触发回调:
var unwatch = vm.$watch('a', cb)
// 之后取消观察
unwatch() //直接调用
为什么要注销 watch?因为我们的组件是经常要被销毁的,比如我们跳一个路由,从一个页面跳到另外一个页面,那么原来的页面的 watch 其实就没用了,这时候我们应该注销掉原来页面的 watch 的,不然的话可能会导致内置溢出。好在我们平时 watch 都是写在组件的选项中的,他会随着组件的销毁而销毁。
watch源码分析
从入口到获取到$watch函数的执行流程
入口:
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:
源码:https://github.com/vuejs/vue/blob/dev/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++
let startTag, endTag
// a flag to avoid this being observed 一个避免被observed的标志
vm._isVue = true
// merge options
if (options && options._isComponent) {
//优化内部组件实例化,因为动态选项合并非常慢,而且没有一个内部组件选项需要特殊处理。
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = 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:
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) {
//初始化data
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
// 这里会初始化 watch
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
initData:
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(... )
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// observe data
observe(data, true /* asRootData */)
}
initWatch:
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
//1.数组声明的 watch 有多个回调,需要循环创建监听
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
//2.其他声明方式直接创建
createWatcher(vm, key, handler)
}
}
}
createWatcher:
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
// 1. 对象声明的 watch,从对象中取出对应回调
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
// 2. 字符串声明的 watch,直接取实例上的方法(注:methods 中声明的方法,可以在实例上直接获取)
if (typeof handler === 'string') {
handler = vm[handler]
}
// 3. expOrFn 是 watch 的 key 值,$watch 用于创建一个“用户Watcher”
return vm.$watch(expOrFn, handler, options)
}
所以在创建数据监听时,除了 watch 配置外,也可以vm.$watch 方法实现同样的效果。
stateMixin:
export function stateMixin (Vue: Class<Component>) {
// 在使用object.defineproperty时,流在直接声明定义对象方面存在一些问题,因此我们必须在这里逐步构建该对象。
const dataDef = {}
dataDef.get = function () { return this._data }
const propsDef = {}
propsDef.get = function () { return this._props }
Object.defineProperty(Vue.prototype, '$data', dataDef)
Object.defineProperty(Vue.prototype, '$props', propsDef)
Vue.prototype.$set = set
Vue.prototype.$delete = del
Vue.prototype.$watch = function (){
下面
...
}
}
stateMixin 在入口文件就已经调用了,为 Vue 的原型添加 $watch 方法。
$watch
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
// 对象声明的cb,直接返回createWatcher
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
// 所有“用户Watcher”的 options,都会带有 user 标识
options.user = true
// 创建 watcher,进行依赖收集
const watcher = new Watcher(vm, expOrFn, cb, options)
// immediate 为 true 时,会立即调用回调
if (options.immediate) {
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
// 取消 watch 监听
return function unwatchFn () {
watcher.teardown()
}
}
}
options.deep怎么没有处理呢?
watch监听的内容依赖收集
依赖收集到底是什么啊?到处都用到它,
根据$watch,会调用new Watch(),这里是依赖收集和更新的触发点。 响应式那里还会有这块的具体逻辑
// 源码位置:/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
// 1 start
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
// 1 end
// 2 start
this.value = this.lazy
? undefined
: this.get()
// 2 end
}
}
根据vm.$watch( expOrFn, callback, [options] )中写法
// 键路径
vm.$watch('a.b.c', function (newVal, oldVal) {
})
// 函数
vm.$watch(
function () {
return this.a + this.b
},
function (newVal, oldVal) {
})
分析以上代码中的代码段1,传进来的 expOrFn 是 watch 的键值;其他是键路径,也就是例中的'a.b.c',需要调用 parsePath 对键值解析。
这一步也是依赖收集的关键点。它执行后返回的是一个函数,先不着急 parsePath 做的是什么,先接着流程继续走。
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('.') //根据上面举例path为'a.b.c',所以segments为键名数组
//obj在get函数中的value = this.getter.call(vm, vm)传入,参数 obj 是 vm 实例
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]] //循环去获取每项键名的值,触发它们的“数据劫持get”
}
return obj
}
}
疑问?? 循环去获取每项键名的值,触发它们的“数据劫持get”,每个键名都能在data属性中找到吗? 突然间明白了,假如现有obj中没有x,但是执行obj['x']也是会触发get,也就是会执行dep.depend()
根据parsePath代码,看出它是返回一函数,函数中返回监听对象中的任何一部分
代码段2,默认调用this.get()
get () {
// 将当前的“用户Watcher”(即当前实例this) 挂到 Dep.target 上,在收集依赖时,找的就是 Dep.target
pushTarget(this)
let value
const vm = this.vm
try {
//调用this.getter函数,也就是会调用expOrFn或者parsePath
value = 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 的逻辑。
以上执行顺序是:执行vm.$watch, 执行const watcher = new Watcher(vm, expOrFn, cb, options),Watcher中会触发get(),再触发parsePath,也就是会触发“数据劫持get”,再收集依赖(再任何确实是什么?我的理解依赖就是被依赖的属性,也就是watch)
参数 obj 是 vm 实例,segments 是解析后的键值数组,循环去获取每项键值的值,触发它们的“数据劫持get”。接着触发 dep.depend 收集依赖(依赖就是挂在 Dep.target 的 Watcher)。
到这里依赖收集就完成了,从上面我们也得知,每一项键值都会被触发依赖收集,也就是说上面的任何一项键名的值发生改变都会触发 watch 回调。例如:
watch: {
'obj.a.b.c': function(){}
}
复制代码不仅修改 c 会触发回调,修改 b、a 以及 obj 同样触发回调。这个设计也是很妙,通过简单的循环去为每一项都收集到了依赖。
get()在Watcher 中调用
watch监听更新
在更新时首先触发的是“数据劫持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 = 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和set,get会收集依赖,set会触发更新
深度监听(deep为true)
深度监听是 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 depId 是每一个被观察属性都会有的唯一标识
const depId = val.__ob__.dep.id
// 2 去重,防止相同属性重复执行逻辑
if (seen.has(depId)) {
return
}
seen.add(depId)
}
// 3 根据数组和对象使用不同的策略,最终目的是递归获取每一项属性,
触发它们的“数据劫持get”收集依赖,和 parsePath 的效果是异曲同工
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)
}
}
从这里能得出,深度监听利用递归进行监听,肯定会有性能损耗。因为每一项属性都要走一遍依赖收集流程,所以在业务中尽量避免这类操作。
取消(注销)watch
unwatchFn不常用,但是也讲下其中原理
Vue.prototype.$watch中返回unwatchFn函数
return function unwatchFn () {
watcher.teardown()
}
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
}
}
遍历 deps 调用 removeSub 方法,移除当前 watcher 实例。在下一次属性更新时,也不会通知 watcher 更新了。deps 存储的是属性的 dep。
总结主要流程
initWatch(vm, opts.watch)
-->
createWatcher(vm, key, handler[i]) [key代表监听的键名,数组需要循环创建监听,其他直接创建,那对象呢?] return vm.watch 用于创建一个“用户Watcher”】
-->
Vue.prototype.$watch = function (){
const watcher = new Watcher(vm, expOrFn, cb, options)
}
创建watcher收集依赖,cb是A函数,意味着立即调用回调
if (options.immediate) {
try {
cb.call(vm, watcher.value}
}
}
--> 触发update ()函数,获取新值,调用B函数,这样把新旧值都传入
watch中如果有deep,所以在get()函数中需要调用traverse
当deep为true时,watch 监听实现利用遍历获取属性,触发“数据劫持get”逐个收集依赖,这样做的好处是其上级的属性发生修改也能执行回调。
if (this.deep) {
traverse(value)
}