问题导读
最近在使用ElementUI做项目的时候,使用到Tree组件,但是因为需求中需要定制化的东西比较多,原生的Tree组件并不能很好的完成项目需求,于是就想着看看ElementUI的代码实现逻辑,然后针对可定制化的部分,进行调整。
其中,再看到Tree组件的watch部分的时候,第一眼竟然充满了疑惑,我承认估计是JavaScript部分知识的薄弱,导致有些东西,无法第一时间明白其中的逻辑,还好,在手动测试后,明白了其中的细节,遂成此篇文章,以记录整个思路。
我们先来看看ElementUI中Tree组件的watch选项。
watch: {
defaultCheckedKeys(newVal) {
this.store.setDefaultCheckedKey(newVal);
},
defaultExpandedKeys(newVal) {
this.store.defaultExpandedKeys = newVal;
this.store.setDefaultExpandedKeys(newVal);
},
data(newVal) {
this.store.setData(newVal);
},
checkboxItems(val) {
Array.prototype.forEach.call(val, (checkbox) => {
checkbox.setAttribute('tabindex', -1);
});
},
checkStrictly(newVal) {
this.store.checkStrictly = newVal;
}
},
说实话,我第一次看这段代码的时候,是没看明白的,我想,所谓的侦听器,不应该侦听具体的属性嘛,为啥直接是一个handler呢。key上哪去了。
于是我就是直接在console中,动手撸了一下代码
a = { b(c) {console.log(c)}}
随着结果的输出,我一下子就清楚了,不信你看
说实话,这块内容对我来说,是暴击呀,真的是为自己的JS买单了。
但是事情还远远没有结束,因为我们还没看watch的内容呢
基本概念
Vue 提供了一种更通用的方式来观察和响应 Vue 实例上的数据变动:侦听属性。当你有一些数据需要随着其它数据变动而变动时,你很容易滥用 watch
——特别是如果你之前使用过 AngularJS。然而,通常更好的做法是使用计算属性而不是命令式的 watch
回调。
虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。这就是为什么 Vue 通过 watch
选项提供了一个更通用的方法,来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。
接下来看一个简单的例子:
<div id="watch-example">
<p> Ask a yes/no question:
<input v-model="question">
</p>
<p>{{ answer }}</p>
</div>
<script>
var watchExampleVM = new Vue({
el: '#watch-example',
data: { question: '', answer: 'I cannot give you an answer until you ask a question!' },
watch: {
// 如果 `question` 发生改变,这个函数就会运行
question: function (newQuestion, oldQuestion) {
this.answer = 'Waiting for you to stop typing...'
this.debouncedGetAnswer()
}
},
created: function () {
// `_.debounce` 是一个通过 Lodash 限制操作频率的函数。
// 在这个例子中,我们希望限制访问 yesno.wtf/api 的频率
// AJAX 请求直到用户输入完毕才会发出。想要了解更多关于
// `_.debounce` 函数 (及其近亲 `_.throttle`) 的知识,
// 请参考:https://lodash.com/docs#debounce
this.debouncedGetAnswer = _.debounce(this.getAnswer, 500) },
methods: {
getAnswer: function () {
if (this.question.indexOf('?') === -1) {
this.answer = 'Questions usually contain a question mark. ;-)'
return
}
this.answer = 'Thinking...'
var vm = this
axios.get('https://yesno.wtf/api')
.then(function (response) {
vm.answer = _.capitalize(response.data.answer)
})
.catch(function (error) {
vm.answer = 'Error! Could not reach the API. ' + error
})
}
}
})
</script>
上面这段代码就是文档中的例子,这个例子很好的解释了watch的功能和应用场景。非常好,大家可以去Vue官方文档上去看,去体验watch的真实案例。
而接下来我要说的并不是仅仅上述的内容
vue中的watch
不知道大家有没有看过vue2的源码,我最近在看这块内容,因为我好奇,就比如这watch选项。
当然了,如何看源码我就不解释了,不同的人有不同的看法,我就送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) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
这里根据我们在vue实例中定义的不同选项,进行统一的初始化,比如,props, data, methods, computed,还有watch选项。
这篇文章我就仅仅学习watch所以,就看最后一部分,如果你在Vue实例中定义了watch选项,那么这里就会创建watch侦听器,如何创建的呢?
这里提一句,在判断opts.watch的时候,跟nativeWatch做了一个对比,这是因为,Firefox也有一个watch函数在Object.prototype.
// Firefox has a "watch" function on Object.prototype...
export const nativeWatch = ({}).watch
继续看watch.
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
我们一般在vue实例上定义watch选项都是使用Object.在Object里,使用需要侦听的data对象或者props对象里面的属性作为key,定义一个function来作为对应Key发生变化时的handler.
所以在initWatch函数里,我们遍历定义的watch属性,然后分别createWatcher
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
createWatcher
的功能可以用最后一行代码体现
vm.$watch( expOrFn, callback, [options] )
观察 Vue 实例上的一个表达式或者一个函数计算结果的变化。回调函数得到的参数为新值和旧值。表达式只接受简单的键路径。对于更复杂的表达式,用一个函数取代。
vm.$watch
返回一个取消观察函数,用来停止触发回调:
var unwatch = vm.$watch('a', cb)
// 之后取消观察
unwatch()
为了发现对象内部值的变化,可以在选项参数中指定 deep: true
。注意监听数组的变更不需要这么做。
vm.$watch('someObject', callback, {
deep: true
})
vm.someObject.nestedValue = 123
// callback is fired
在选项参数中指定 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 })
回顾
现在再来看看导读里面的问题,你看看ElementUI中的代码
watch: {
defaultCheckedKeys(newVal) {
this.store.setDefaultCheckedKey(newVal);
},
defaultExpandedKeys(newVal) {
this.store.defaultExpandedKeys = newVal;
this.store.setDefaultExpandedKeys(newVal);
},
data(newVal) {
this.store.setData(newVal);
},
checkboxItems(val) {
Array.prototype.forEach.call(val, (checkbox) => {
checkbox.setAttribute('tabindex', -1);
});
},
checkStrictly(newVal) {
this.store.checkStrictly = newVal;
}
},
首先这些函数的名会最为key来进行侦听,为什么可以呢,因为这些key在data,或者props选项中都是有定义的。完全可以这些写。而这些函数的参数,也就是回调函数的参数,新值和旧值。