美文网首页前端开发那些事儿
vue源码解读--侦听属性

vue源码解读--侦听属性

作者: 习惯水文的前端苏 | 来源:发表于2021-02-17 23:09 被阅读0次

    目录导航

    这一节,我们的示例代码是这样的

    当页面渲染结束后,控制台将首先输出"name in watch: default";当点击change name按钮执行onChangeName方法控制台将依次输出"用户输入手机号"、"new input is: [object Object]";当点击change index按钮执行onChangeIndex方法控制台将先输出"new saveIndex: 1"再打印"name in watch: 写bug哦 三岁就会";当多次点击change name按钮控制台无反应;当多次点击chang index按钮页面则只会输出"new saveIndex: 1++"

    提出几点疑问

    a-为什么页面渲染完后会直接有输出,要知道在页面渲染后只是定义了初始值,而初始值是作为改变的比较项存在,不在值"改变"的定义中的

    b-为什么会先new saveIndex后name in watch,明明我在定义的时候是将name写在后的

    c-为什么两次重复点击两个按钮的控制台输出不同

    \bullet 创建过程

        在组件initState过程中调用initWatch,入参为组件实例和组件中定义的watch对象

            initWatch实际上就是对watch进行了一次规范化以符合vue的预期,接着调用createWatcher,入参为组件实例、watch对象定义的key(如name)、key对应的处理函数

            再次进行一次规范化处理,因为vue既支持直接定义函数又支持定义一个对象,如果为对象,那么则需要获取对象中定义的handle函数作为处理函数,最终核心的方法其实是$watch函数,入参为:key(处理函数的name)、处理函数、undefined

    (由$watch定义在prototype可知,watch的定义不只是能在组件中作为属性存在,还可以在逻辑代码中通过this.$watch定义)

        \cdot options.user = true将当前的watcher标识为user watcher

        \cdot 实例化watcher,通过之前的分析watcher将作为订阅者接收dep派发更新通知重新渲染,入参为:组件实例、处理函数/对象的key name、处理函数、{}。user watcher的核心逻辑如下

    (将watcher标识为user watcher,将在派发更新时有用) (此时的expOrFn 不再是一个function,故走向else逻辑,调用parsePath 方法)

            --入参为key(name、input.tell等)

                    \ominus 首先对传入的path(即定义的key)做一次校验,bailRe其实就是一个预定义的正则

                    \ominus 通过 split方法对key进行分割并保存到数组中,如input.tell的情况

                    \ominus 最后返回一个匿名函数

            --回到watcher中,向下调用this.get()方法

                    \ominus 调用this.getter,即调用parsePath返回的匿名函数,入参为组件实例,并通过for循环去访问组件上定义的属性,如vm.input。由于input是在data中定义的,因此势必会通过Object.defineProperty变成响应式数据,故会触发get方法进行依赖收集

                    \ominus 判断deep存在,执行traverse,入参为vm.key,即在data中定义的值,seen则是一个Set实例,防止重复

                    \ominus 根据之前创建响应式的分析,如果有__ob__则说明是一个响应式数据,则向Set中add一位

                    \ominus 对于数组或者对象则递归调用_traverse,取到每一个值的id(由于__ob__的value指向observe实例,故能取到dep),而取id的目的则在于避免多次无意义触发get

                    \ominus 根据尤大的注释,这一段代码是为了去收集依赖,而依赖的收集需要去触发get,触发get的代码则在while (i--) _traverse(val[keys[i]], seen)。那我现在不禁有个疑问,既然input已经是一个响应式了,也就是说在initdata过程中已经收集过依赖了,为什么还要再次收集一次呢?这是因为每一次的依赖收集都会使不同的watcher去订阅dep,而dep在每一次render中都至少被一个watcher订阅,换言之,dep去notify的watcher每次都是不一样的!    

        \cdot immediate为true则立即执行一次,此时即执行我们在组件中定义的处理函数,输出name in watch。这恰好回答了疑问a

        \cdot 最后返回unwatchFn函数,该函数将执行watcher的销毁。通过查找代码执行逻辑,我发现它实际上是没有被vue调用的,也就是说vue不会主动去销毁。结合之前分析可以在组件中通过this.$watch的方式添加侦听,所以我认为它是提供给我们用户手动进行销毁的接口,应当在定义时使用变量接收,在beforedestory中调用手动销毁

    \bullet 侦听过程

        \bullet change name被click

        vue对watch的定义是当值改变时触发回调,故当点击change name改变lastName、input.sex、input.tell将触发对应的处理函数,那么我们现在就来窥探下这一实现过程

        由于这三个值均是在data中定义的,因此势必会通过Object.defineProperty变成响应式数据,但是是否会触发依赖收集是有待商榷的:lastName并不会触发get,因为在render过程中并没有被访问,而this.lastName="写bug哦"是在设置值,故调用change name只会触发set,也由于没有进行依赖收集故dep为空;this.input.sex则是先访问input再对其sex进行设值,故this.input.sex将会触发依赖收集。因此change name的调用会收集两次依赖。故将会notify通知到watcher进行update

                  \ominus 如果定义了sync(示例中的saveIndex,此时watcher将有四个:computed name watcher--),则直接run

                    \ominus 显然sync的定义是在saveIndex下,故在本地的update过程不会调用run方法,而是调用queueWatcher

                接着调用nextTick,根据之前相关分析,nextTick是在主线程后通过事件轮询的方式执行回调flushSchedulerQueue,故是异步

                    可以看到,在下一次tick中执行了run方法

                可以看到,分别在框红的1和2的位置执行了求值和回调函数。也就是说,如果定义是sync,则直接run,否则则在下一次事件轮询时run,即sync是同步否则是异步,同步先执行。故这回答了疑问b

        \bullet change index 被 click

            我们已经知道,change index对应的值在notify过程中会直接run,故将直接求值并调用回调函数执行console.log.当第二次点击,name in watch不会再次被执行,这是因为saveIndex所触发的render过程中再次访问name返回的值相同(写bug 三岁就会)且this.deep为false,求的值为Sting类型(疑问c

    相关文章

      网友评论

        本文标题:vue源码解读--侦听属性

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