美文网首页Vue知识点总结
【vue】源码解析(2)vue中的监听器watcher用法

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

作者: SophieRabbit | 来源:发表于2019-06-10 20:50 被阅读0次

    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

    相关文章

      网友评论

        本文标题:【vue】源码解析(2)vue中的监听器watcher用法

        本文链接:https://www.haomeiwen.com/subject/mlkdaqtx.html