【vue】源码解析(2)vue中的监听器watcher用法

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深度监听。


watch的过程


参考文献:

vue中watch的详细用法:https://www.cnblogs.com/shiningly/p/9471067.html

vue的源码学习之五——2.数据驱动:   https://blog.csdn.net/qishuixian/article/details/84964567

https://www.jianshu.com/p/b4c257f19ce3

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容