演示Vue.js 是如何进行「依赖收集]

原创 前端开发者 随笔 前端 144阅读 23 天前 举报

初始化Vue

我们简单实例化一个Vue的实例, 下面的我们针对这个简单的实例进行深入的去思考:

initState

在上面我们有添加一个watch的属性配置:

从上面的代码我们可知,我们配置了一个key为newTodo的配置项, 我们从上面的代码可以理解为:

newTodo的值发生变化了,我们需要执行hander方法,所以我们来分析下具体是怎么实现的。

我们还是先从initState方法查看入手:

我们来具体分析下initWatch方法:

从上面的代码分析,我们可以发现watch 可以有多个hander,写法如下:

我们接下来分析createWatcher方法:

总结:

  1. 从这个方法可知,其实我们的hanlder还可以是一个string
  2. 并且这个handervm对象上的一个方法,我们之前已经分析methods里面的方法都最终挂载在vm 实例对象上,可以直接通过vm["method"]访问,所以我们又发现watch的另外一种写法, 直接给watchkey 直接赋值一个字符串名称, 这个名称可以是methods里面定一个的一个方法:

接下来调用$watch方法

在这个方法,我们看到有一个immediate的属性,中文意思就是立即, 如果我们配置了这个属性为true, 就会立即执行watchhander,也就是同步 执行, 如果没有设置, 则会这个watcher异步执行,下面会具体分析怎么去异步执行的。 所以这个属性可能在某些业务场景应该用的着。

在这个方法中new 了一个Watcher对象, 这个对象是一个重头戏,我们下面需要好好的分析下这个对象。 其代码如下(删除只保留了核心的代码):

主要做了如下几件事:

  1. watcher 对象保存在vm._watchers
  2. 获取getter,this.getter = parsePath(expOrFn);
  3. 执行this.get()去获取value

其中parsePath方法代码如下,返回的是一个函数:

在调用this.get()方法中去调用value = this.getter.call(vm, vm);

然后会调用上面通过obj = obj[segments[i]];去取值,如vm.newTodo, 我们从 深入了解 Vue 响应式原理(数据拦截),已经知道,Vue 会将data里面的所有的数据进行拦截,如下:

所以我们在调用vm.newTodo时,会触发getter,所以我们来深入的分析下getter的方法

getter

getter 的代码如下:

  1. 首先取到值var value = getter ? getter.call(obj) : val;
  2. 调用Dep对象的depend方法, 将dep对象保存在target属性中Dep.target.addDep(this);target是一个Watcher对象 其代码如下:

生成的Dep对象如下图:
image.png

3. 判断是否有自属性,如果有自属性,递归调用。

现在我们已经完成了依赖收集, 下面我们来分析当数据改变是,怎么去准确地追踪所有修改。

准确地追踪所有修改

我们可以尝试去修改data里面的一个属性值,如newTodo, 首先会进入set方法,其代码如下:

下面我来分析这个方法。

  1. 首先判断新的value 和旧的value ,如果相等,则就直接return
  2. 调用dep.notify();去通知所有的subs, subs是一个类型是Watcher对象的数组 而subs里面的数据,是我们上面分析的getter逻辑维护的watcher对象.

notify方法,就是去遍历整个subs数组里面的对象,然后去执行update()

上面有一个判断config.async,是否是异步,如果是异步,需要排序,先进先出, 然后去遍历执行update()方法,下面我们来看下update()方法。

上面的方法,分成三种情况:

  1. 如果watch配置了lazy(懒惰的),不会立即执行(后面会分析会什么时候执行)
  2. 如果配置了sync(同步)为true则会立即执行hander方法
  3. 第三种情况就是会将其添加到watcher队列(queue)中

我们会重点分析下第三种情况, 下面是queueWatcher源码

  1. 首先flushing默认是false, 所以将watcher保存在queue的数组中。
  2. 然后waiting默认是false, 所以会走if(waiting)分支
  3. configVue的全局配置, 其async(异步)值默认是true, 所以会执行nextTick函数。

下面我们来分析下nextTick函数

nextTick

nextTick 代码如下:

nextTick 主要做如下事情:

  1. 将传递的参数cb 的执行放在一个匿名函数中,然后保存在一个callbacks 的数组中
  2. pendinguseMacroTask的默认值都是false, 所以会执行microTimerFunc()(微Task) microTimerFunc()的定义如下:

其实就是用Promise函数(只分析Promise兼容的情况), 而Promise 是一个i额微Task 必须等所有的宏Task 执行完成后才会执行, 也就是主线程空闲的时候才会去执行微Task;

现在我们查看下flushCallbacks函数:

这个方法很简单,

  1. 第一个是变更pending的状态为false
  2. 遍历执行callbacks数组里面的函数,我们还记得在nextTick 函数中,将cb 保存在callbacks 中。

我们下面来看下cb 的定义,我们调用nextTick(flushSchedulerQueue);, 所以cb 指的就是flushSchedulerQueue 函数, 其代码如下:

  1. 首先将flushing 状态开关变成true
  2. queue 进行按照ID 升序排序,queue是在queueWatcher 方法中,将对应的Watcher 保存在其中的。
  3. 遍历queue去执行对应的watcherrun 方法。
  4. 执行resetSchedulerState()是去重置状态值,如waiting = flushing = false
  5. 执行callActivatedHooks(activatedQueue);更新组件 ToDO:
  6. 执行callUpdatedHooks(updatedQueue);调用生命周期函数updated
  7. 执行devtools.emit('flush');刷新调试工具。

我们在3. 遍历queue去执行对应的watcher的run 方法。, 发现queue中有两个watcher, 但是我们在我们的app.js中初始化Vue的 时候watch的代码如下:

从上面的代码上,我们只Watch了一个newTodo属性,按照上面的分析,我们应该只生成了一个watcher, 但是我们却生成了两个watcher了, 另外一个watcher到底是怎么来的呢?

总结:

  1. 在我们配置的watch属性中,生成的Watcher对象,只负责调用hanlder方法。不会负责UI的渲染
  2. 另外一个watch其实算是Vue内置的一个Watch(个人理解),而是在我们调用Vue$mount方法时生成的, 如我们在我们的app.js中直接调用了这个方法:app.$mount('.todoapp'). 另外一种方法不直接调用这个方法,而是在初始化Vue的配置中,添加了一个el: '.todoapp'属性就可以。这个Watcher 负责了UI的最终渲染,很重要,我们后面会深入分析这个Watcher
  3. $mount方法是最后执行的一个方法,所以他生成的Watcher对象的Id 是最大的,所以我们在遍历queue之前,我们会进行一个升序 排序, 限制性所有的Watch配置中生成的Watcher 对象,最后才执行$mount中生成的Watcher对象,去进行UI渲染。

$mount

我们现在来分析$mount方法中是怎么生成Watcher对象的,以及他的cb 是什么。其代码如下:

  1. 从上面的代码,我们可以看到最后一个参数isRenderWatcher设置的值是true , 表示是一个Render Watcher, 在watch 中配置的,生成的Watcher 这个值都是false, 我们在Watcher 的构造函数中可以看到:

如果isRenderWatchertrue 直接将这个特殊的Watcher 挂载在Vue 实例的_watcher属性上, 所以我们在flushSchedulerQueue 方法中调用callUpdatedHooks 函数中,只有这个watcher才会执行生命周期函数updated

  1. 第二个参数expOrFn , 也就是Watchergetter, 会在实例化Watcher 的时候调用get方法,然后执行value = this.getter.call(vm, vm);, 在这里就是会执行updateComponent方法,这个方法是UI 渲染的一个关键方法,我们在这里暂时不深入分析。
  2. 第三个参数是cb, 传入的是一个空的方法
  3. 第四个参数传递的是一个options对象,在这里传入一个before的function, 也就是,在UI重新渲染前会执行的一个生命中期函数beforeUpdate

上面我们已经分析了watch的一个工作过程,下面我们来分析下computed的工作过程,看其与watch 有什么不一样的地方。

computed

首先在实例化Vue 对象时,也是在initState 方法中,对computed 进行了处理,执行了initComputed方法, 其代码如下:

上面代码比较长,但是我们可以总结如下几点:

  1. var watchers = vm._computedWatchers = Object.create(null);vm实例对象上面挂载了一个_computedWatchers的属性,保存了由computed 生成的所有的watcher
  2. 然后遍历所有的key, 每一个key 都生成一个watcher
  3. var getter = typeof userDef === 'function' ? userDef : userDef.get; 从这个代码可以延伸computed 的两种写法,如下:
  1. 如果不是服务端渲染,就生成一个watcher 对象,并且保存在vm._computedWatchers属性中,但是这个与watch 生成的watcher 有一个重要的区别就是, 传递了一个属性computedWatcherOptions对象,这个对象就配置了一个lazy: ture

我们在Watcher的构造函数中,有如下逻辑:

因为this.lazytrue 所以不会执行this.get();, 也就不会立即执行computed 里面配置的对应的方法。

  1. defineComputed(vm, key, userDef);就是将computed 的属性,直接挂载在vm 上,可以直接通过vm.strLen去访问,不过在这个方法中,有针对是不是服务器渲染做了区别,服务器渲染会立即执行computed 的函数,获取值,但是在Web 则不会立即执行,而是给get 赋值一个函数:

如果我们在我们的template中引用了computed的属性,如:<div>{{strLen}}</div>, 会执行$mount去渲染模版的时候,会去调用strLen,然后就会执行上面的computedGetter的方法去获取值, 执行的就是:

执行了this.get() 就是上面分析watch 中的this.get().

思考:

我们上面基本已经分析了computed逻辑的基本过程,但是我们好像还是没有关联上, 当我们的data里面的值变了,怎么去通知computed 更新的呢?我们的computed如下:

当我们改变this.newTodo 的时候,会执行strLen的方法呢?

答案:

  1. 在上面我们已经分析了我们在我们的template 中有引用strLen,如<div>{{strLen}}</div>,在执行$mount去渲染模版的时候,会去调用strLen,然后就会执行的computedGetter的方法去获取值,然后调用get 方法,也就是我们computed 配置的函数:
  1. 在执行上面方法的时候,会引用this.newTodo , 就会进入reactiveGetter方法(深入了解 Vue 响应式原理(数据拦截))

会将当前的Watcher 对象添加到dep.subs队列中。

  1. this.newTodo值改变时,就会执行reactiveSetter方法,当执行dep.notify();时,也就会执行computed 里面的方法,从而达到当data里面的值改变时,其有引用这个data 属性的computed 也就会立即执行。
  2. 如果我们定义了computed 但是没有任何地方去引用这个computed , 即使对应的data 属性变更了,也不会执行computed 方法的, 即使手动执行computed 方法, 如:app.strLen也不会生效,因为在WatcheraddDep 方法,已经判断当前的watcher 不是一个新加入的watcher

结语

感谢您的观看,如有不足之处,欢迎批评指正。
获取资料👈👈👈
本次给大家推荐一个免费的学习群,里面概括移动应用网站开发,css,html,webpack,vue node angular以及面试资源等。
对web开发技术感兴趣的同学,欢迎加入Q群:👉👉👉582735936 👈👈👈,不管你是小白还是大牛我都欢迎,还有大牛整理的一套高效率学习路线和教程与您免费分享,同时每天更新视频资料。
最后,祝大家早日学有所成,拿到满意offer,快速升职加薪,走上人生巅峰。

评论 ( 0 )
最新评论
暂无评论

赶紧努力消灭 0 回复