1.引子
在了解vue中的监听器的详细知识前,我们需要先从Vue的一个实例创建来说起。
我们以一个例子作为引子。下面是一个vue组件的实例化:
new Vue({
el: '#root',
data: {
name: ''
},
watch: {
name : {
handler(newName, oldName) {
// ...
},
immediate: true
}
}
})
Vue 初始化主要就干了几件事情,合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等。
每当我们new一个新的Vue实例时,其实都调用了一个_init()函数:
(入口文件地址:src/core/instance/index.js)
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options) //调用_init
}
_init()函数存在于init.js中:地址(src/core/instance/init.js)
// init.js部分代码如下:
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
...
initLifecycle(vm)
initEvents(vm) // 初始化事件相关的属性
initRender(vm) // vm添加了一些虚拟dom、slot等相关的属性和方法
callHook(vm, 'beforeCreate') //钩子函数,创建之前
//下面initInjections,initProvide两个配套使用,用于将父组件_provided中定义的值,通过inject注入到子组件,且这些属性不会被观察
initInjections(vm) // resolve injections before data/props
initState(vm) //初始化状态,主要就是操作数据了,props、methods、data、computed、watch,从这里开始就涉及到了Observer、Dep和Watcher
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created') //钩子函数,创建完成
...
}
可以看出,在Vue实例初始化时,会调用一个初始化状态的函数initState(vm)。
2. 数据的初始化
initState()函数存在于state.js中。到了这里,我们终于可以看到有关watch方法的相关内容了。我们看一下state.js源码中是如何将watch方法与使用watch方法的组件、watch所监听的内容来相互联系的。(源码地址:vue/src/core/instance/state.js)
var nativeWatch = ({}).watch; //这里是为了兼容火狐, Firefox has a "watch" function on Object.prototype
export function initState(vm:Component) {
vm._watchers = [] //为当前组件创建了一个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) { //判断组件有watch属性 并没有nativeWatch( 兼容火狐)
initWatch(vm, opts.watch) //调用watch初始化
}
...
}
首先有一个初始化watch的名为initWatch的方法。其传入两个参数:当前使用watch的组件和watch监听的对象。这个init方法做了什么事呢?可以从代码中看出,其对watch对象中的每一个属性(也就是watch所监听的组件)进行了遍历。
再initWatch中,传入的第二个参数watch是整个Vue实例的watch对象。这个watch对象中的属性即为每个添加了watch对象的组件watch数组,数组中即为我们需要对象监听的组件的属性。对于组件中的需要被监听的组件属性,添加了一个createWatcher方法。
function initWatch ( vm: Component, watch: Object) { //这里的watch:全局保存着全部watch数组的对象
for(const key in watch) { //遍历全局watch对象,key即为单个组件中的watch
const handler = watch[key]
if (Array.isArray(handler)) { //如果key为数组
for(let i=0; i<handler.length; i++) { //遍历单个组件中的watch数组, handler[i]即为watch数组中的属性
createWatcher(vm, key, handler[i]) //每个需要被watch的属性,做createWatcher() 操作,创建监听器 (数组)
}
}else{
createWatcher(vm, key, handler) //为属性创建监听器 (字符串)
}
}
}
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) //调用组件的$watch方法
}
这里主要进行了两步预处理,代码上很好理解,主要做一些解释:
第一步,可以理解为用户设置的 watch 有可能是一个 options 对象,如果是这样的话则取 options 中的 handler 作为回调函数。(并且将options 传入下一步的 vm.$watch)
第二步,watch 有可能是之前定义过的 method,则获取该方法为 handler。
第三步,调用组件的$watch方法。
3. 组件的$watch方法
Vue.prototype.$watch = function( // 定义在Vue原型上的$watch
expOrFn: string | Function, // 接收数据类型(字符串/方法)
cb:any, // 任意类型的回调方法,也就是 createWatcher里的handler
options?: Object
): Function {
const vm: Component = this
if(isPlainObject(cb)) { // 如果cb不是回调方法,那就先创建监听器
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options) // 创建监听实例
if(options.immediate) { // immediate表示在watch中首次绑定的时候,是否执行handler,值为true则表示在watch中声明的时候,就立即执行handler方法,值为false,则和一般使用watch一样,在数据发生变化的时候才执行handler
try{
cb.call(vm, watcher.value) // 首次声明时就立即执行回调
}catch(error) {
handleError(error, vm,`callback for immediate watcher "${watcher.expression}"`)
}
}
return function unwatchFn() {
watcher.teardown()
}
}
初始化watch,就是为每个watch属性创建一个观察者对象,这个expOrFn解析取值表达式去取值,然后就会调用相关data/prop属性的get方法,get方法又会在他的观察者列表里加上该watcher,一旦这些依赖属性值变化就会通知该watcher执行update方法。即会执行他的回调方法cb,也就是watch属性的handler方法。
4. 组件的监听构造函数Watcher
前面在$watch中用到的Watcher构造函数,在源码/src/core/observer/watcher.js中:
class Watcher { // 当使用了$watch 方法之后,不管有没有监听,或者触发监听,都会执行以下方法
constructor(vm, expOrFn, cb) {
this.cb = cb //调用$watch时候传进来的回调
this.vm = vm
this.expOrFn = expOrFn //这里的expOrFn是你要监听的属性或方法也就是$watch方法的第一个参数
this.value = this.get() //调用自己的get方法,并拿到返回值
}
update(){ // 更新
this.run()
}
run(){ //这个方法并不是实例化Watcher的时候执行的,而是监听的变量变化的时候才执行的
const value = this.get()
if(value !== this.value){
this.value = value
this.cb.call(this.vm) //触发你穿进来的回调函数 expOrFn
}
}
get(){ //向Dep.target 赋值为 Watcher
Dep.target = this //将Dep身上的target 赋值为Watcher对象
const value = this.vm._data[this.expOrFn]; //这里拿到你要监听的值,在变化之前的数值
// 声明value,使用this.vm._data进行赋值,并且触发_data[a]的get事件
Dep.target = null
return value
}
}
5. 深度监听deep
设置deep: true 则可以监听到对象的变化,此时会给对象的所有属性都加上这个监听器,当对象属性较多时,每个属性值的变化都会执行handler。如果只需要监听对象中的一个属性值,则可以做以下优化:使用字符串的形式监听对象属性,这样只会给对象的某个特定的属性加监听器。
watch: {
'cityName.name': {
handler(newName, oldName) {
// ...
},
deep: true,
immediate: true
}
}
数组(一维、多维)的变化不需要通过深度监听,对象数组中对象的属性变化则需要deep深度监听。
参考文献:
vue中watch的详细用法:https://www.cnblogs.com/shiningly/p/9471067.html
vue的源码学习之五——2.数据驱动: https://blog.csdn.net/qishuixian/article/details/84964567