这一节,我们的示例代码是这样的
当页面渲染结束后,控制台将首先输出"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-为什么两次重复点击两个按钮的控制台输出不同
创建过程
在组件initState过程中调用initWatch,入参为组件实例和组件中定义的watch对象
initWatch实际上就是对watch进行了一次规范化以符合vue的预期,接着调用createWatcher,入参为组件实例、watch对象定义的key(如name)、key对应的处理函数
再次进行一次规范化处理,因为vue既支持直接定义函数又支持定义一个对象,如果为对象,那么则需要获取对象中定义的handle函数作为处理函数,最终核心的方法其实是$watch函数,入参为:key(处理函数的name)、处理函数、undefined
(由$watch定义在prototype可知,watch的定义不只是能在组件中作为属性存在,还可以在逻辑代码中通过this.$watch定义)options.user = true将当前的watcher标识为user watcher
实例化watcher,通过之前的分析watcher将作为订阅者接收dep派发更新通知重新渲染,入参为:组件实例、处理函数/对象的key name、处理函数、{}。user watcher的核心逻辑如下
(将watcher标识为user watcher,将在派发更新时有用) (此时的expOrFn 不再是一个function,故走向else逻辑,调用parsePath 方法)--入参为key(name、input.tell等)
首先对传入的path(即定义的key)做一次校验,bailRe其实就是一个预定义的正则
通过 split方法对key进行分割并保存到数组中,如input.tell的情况
最后返回一个匿名函数
--回到watcher中,向下调用this.get()方法
调用this.getter,即调用parsePath返回的匿名函数,入参为组件实例,并通过for循环去访问组件上定义的属性,如vm.input。由于input是在data中定义的,因此势必会通过Object.defineProperty变成响应式数据,故会触发get方法进行依赖收集
判断deep存在,执行traverse,入参为vm.key,即在data中定义的值,seen则是一个Set实例,防止重复
根据之前创建响应式的分析,如果有__ob__则说明是一个响应式数据,则向Set中add一位
对于数组或者对象则递归调用_traverse,取到每一个值的id(由于__ob__的value指向observe实例,故能取到dep),而取id的目的则在于避免多次无意义触发get
根据尤大的注释,这一段代码是为了去收集依赖,而依赖的收集需要去触发get,触发get的代码则在while (i--) _traverse(val[keys[i]], seen)。那我现在不禁有个疑问,既然input已经是一个响应式了,也就是说在initdata过程中已经收集过依赖了,为什么还要再次收集一次呢?这是因为每一次的依赖收集都会使不同的watcher去订阅dep,而dep在每一次render中都至少被一个watcher订阅,换言之,dep去notify的watcher每次都是不一样的!
immediate为true则立即执行一次,此时即执行我们在组件中定义的处理函数,输出name in watch。这恰好回答了疑问a
最后返回unwatchFn函数,该函数将执行watcher的销毁。通过查找代码执行逻辑,我发现它实际上是没有被vue调用的,也就是说vue不会主动去销毁。结合之前分析可以在组件中通过this.$watch的方式添加侦听,所以我认为它是提供给我们用户手动进行销毁的接口,应当在定义时使用变量接收,在beforedestory中调用手动销毁
侦听过程
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
如果定义了sync(示例中的saveIndex,此时watcher将有四个:computed name watcher--),则直接run
显然sync的定义是在saveIndex下,故在本地的update过程不会调用run方法,而是调用queueWatcher
接着调用nextTick,根据之前相关分析,nextTick是在主线程后通过事件轮询的方式执行回调flushSchedulerQueue,故是异步
可以看到,在下一次tick中执行了run方法
可以看到,分别在框红的1和2的位置执行了求值和回调函数。也就是说,如果定义是sync,则直接run,否则则在下一次事件轮询时run,即sync是同步否则是异步,同步先执行。故这回答了疑问b
change index 被 click
我们已经知道,change index对应的值在notify过程中会直接run,故将直接求值并调用回调函数执行console.log.当第二次点击,name in watch不会再次被执行,这是因为saveIndex所触发的render过程中再次访问name返回的值相同(写bug 三岁就会)且this.deep为false,求的值为Sting类型(疑问c)
网友评论