Vue.js进阶之Vue源码解析(上)

前言

阅读Vue源码仅仅只是为了面试吗?我想,大概很多人都会这么觉得吧!但我并不这么想...为什么呢?(ps:后续再慢慢讲述...)当然,当你选择去阅读Vue源码也真的是需要一定的勇气,为什么会这么说?如果自己花了时间但却没有一点收获,心情肯定是失落的。好在功夫不负有心人,这段时间通过不断的思考自己学到了什么这样的课题伴随着...还算是有收获。

初始 _init

Vue构造函数从_init这个api下开始工作,在init.js这个文件下初始化了很多api,详细的可查看源码,在这里我只想分享我的心得...

初始initComputed 和 initWatch

平时常讲的computed具有缓存的效果以及它依赖某个数据的变化而变化;watch当开启immediate:true时,为什么会比mounted钩子执行的还早?并且它们两个有什么区别,在项目中应该如何取舍等等...

这一切的疑问,在initComputed 和 initWatch函数都告诉我了。

源码思路

  • initComputed:内部的原理是使用Watcher类实现

当我们给computed选项写的的每一个对象,都是通过new Watcher包装构造出computedWatcher实例并且通过传入一个computedLazy:true标识来实现缓存的机制。

  • Watcher类具体做了什么事呢?(ps:Watcher类的实现,后续具体讲述)

在Watcher类构造器内部会进行第一次的取值。那这样看起来computed的每一个对象被包装成了computedWatcher实例,再将每个computedWatcher实例通过Object.defineProperty进行再次包装,在使用时触发了get,get函数会拦截判断computedWatcher实例dirty的值,为true时则触发watcher类的evaluate函数,该函数用来获取值最后再关闭dirty,dirty是false直接返回第一次获取的值。

 

 

整理出这样的思路,我大概写了一份伪代码来实现上面所述的逻辑:

这里要明确的是computed的缓存机制与它是否依赖于响应式数据是无关的。

思考

后来我又思考了:如果我写了一个computed对象不依赖data选项的数据或者是props的数据,然后我更改computed值时怎么会不能被watch选项监听呢?(可以动手敲一下)

computed: {
    getC: {
      get: () => {
        return 1;
      },
      set: (newVal) => {
        return newVal;
      },
    },
  },
mounted() {
    setTimeout(() => {
      this.getC = 8;
    }, 800);
  },
watch: {
    getC: {
      handler: function () {
        console.log(this.getC, "c无法被监听");//无法打印
      },
    },
  },

computed内部Object.defineProperty中的set没有做任何通知依赖变化的操作。initComputed最终获取的是一个值,它可以依赖某个响应式数据,也可以不依赖。如果依赖于一个响应式数据或者多个响应式数据,那么当响应式数据发生变化了,它也跟着变化这是因为响应式数据中使用了new Dep创建了dep实例,并且在set函数通知了那些依赖它的watcher。

那为什么不被watch选项监听呢?

watch选项监听的是响应式数据,执行watch选项的回调函数是在dep通知更新变化的时候去执行的,那不依赖于响应式数据的computed对象怎能做到被通知变化了呢!这样子说watch是用来监听一个值的变化,接收的值是一个新变化的值合情合理了。

回到最初的疑问,immediate:true的实现原理什么?

在开启immediate:true时,会先执行watch的回调函数,这是因为js是单线程的,在_init初始化时initWatchmounted早。initWatch的关键是vm.$watch,在实现这个函数的时候,判断了immediate是否为true,如果是true就立即执行了callback函数。

通过这些内容,实现缓存机制的一种想法已经浮现在我的脑袋中了,合理利用好Object.defineProperty可以实现一些数据的拦截,也是相当ok的!

初始响应式数据

props和data选项都是响应式数据,Vue是如何将propsdata的数据变成可观察的?

对象是通过使用Object.defineProperty进行包装整合,数组是通过将数组的方法进行重写。

在Vue源码中,通过一个Observer类实现数据的劫持,也就是说当初始化data和props时,调用observer方法。该方法进行了数据类型的判断,决定是重新包装对象,还是重写方法。那这和响应式有什么关系?

疑惑

当我在data选项里定义了一个变量a之后,改变了变量a的值,为何所有用到它的地方都会自动更新值呢?

思考

响应式数据其关键的还是Watcher类和Dep类在中间起了关键的角色。那这两个类具体做了什么事情?

  • Watcher(被观察者)作用

    1、在自身实例化时往属性订阅器(dep)里面添加自己。

    2、自身必须有一个update()方法。

    3、待属性变动dep.notice()通知时,能调用自身的update()方法,

  • Dep(观察者)作用

    1、有个框子来收集着每一个watcher。

    2、有个收集watcher的addSub()方法。

    3、有个通知的方法,通知watcher进行更新。

  • 提炼了两者的作用之后,那它们又是如何工作的?

    Wtacher类在实例化时,通过定义Dep类身上的target属性将该watcher实例关联起来了。那么对于变量a,在data初始化使用Object.defineProperty进行包装时,通过new Dep类(ps:这个dep实例可以看出一个框子,通过框子去收集这些依赖变量a的watcher实例)实现依赖变量a的watcher实例被收集了。

    这个dep实例实际上是属于变量a的小框子,它将依赖它的watcher放进了dep定义的小框子里。等待变量a发生改变时,在set函数里触发dep的notify函数通知所有watcher调用update函数更新值。

再比如我在computed选择定义了一个getA的属性,在初始化computed时new Watcher会把自身getA设置为Dep.target。如果getA属性有依赖于data选项定义的变量a值,在获取变量a的值时会触发Object.defineProperty的get方法,在该方法里收集getA的watcher依赖,也就是会将getA丢到了变量a的dep框里。

所以Observer类的实现并不难,关键是Watcher类和Dep类。

  • 如何实现Watcher类和Dep类

    我通过模拟Vue源码写了一份伪代码,可供参考:

 

 

Watcher类和工具函数:

image
image

 

 

Dep类:

到这里,你应该清晰的了解到响应式数据是如何实现了吧,而不是仅仅因为Object.defineProperty~

回归到Vue的响应式数据data/props,在源码中还做了一层代理,因此我们写代码的时候就可以直接通过this.xxx来访问变量了。

写这段伪代码的时候,让我想起了es6的Proxy类。

初始nextTick

在了解nextTick之前,我们先来看一个案例吧。

案例

有一种情况:在mounted时,test的值会被++循环执行1000次。 每次++时,都会根据响应式触发setter->Dep->Watcher->update->patch。

思考:Vue如何是如何更新视图的以及和nextTick有什么关系?

Vue是异步更新视图的,Vue实现了一个queue队列。先执行主线程的代码,等下一个tick的时候会统一执行queue中Watcher的run。这里在说下一个tick那得先理解一下事件循环。可参考学习大佬写的:事件循环

同时,拥有相同id的Watcher不会被重复加入到该queue中去,所以不会执行1000次Watcher的run。最终更新视图只会直接将test对应的DOM的0变成1000。

而之所以会在nextTick回调函数中能获取到数据修改后的DOM变化,是因为nextTick函数中定义了timerFunc函数,这个函数使用PromiseMutationObserversetImmediatesetTimeout进行包装,开启了另外的一个宏任务,当前的宏任务执行完毕,都会清空当前宏任务所产生的微任务(ps:Watcher的run被执行了)。

那我们在异步更新视图之后(ps:数据早更新了),想要获取到数据修改后的DOM变化,要再开启一个tick,所以使用nextTick传入的回调函数,在回调函数就可操作修改后的DOM变化,可以在这里写你的代码逻辑了。这就是为什么操作更新完毕的数据的dom元素要在nextTick的原因。

在nextTick函数中使用 callbacks 而不是直接在 nextTick 中执行回调函数的原因是保证在同一个 tick 内多次执行 nextTick,不会开启多个异步任务,而把这些异步任务都压成一个同步任务,在下一个 tick 执行完毕。

似想如果这时候没有异步更新视图,那么每次++都会直接操作DOM更新视图,这是非常消耗性能的。

初始事件机制

Vue.js提供了四个事件API,分别是$on$once$off$emit

分析

  • $on订阅函数,传入事件名称,以及cb。一个事件可被订阅多次,所以一个事件通过队列的形式进行cb的收集。
  • $emit发布函数,通过对应的事件名称去执行刚刚收集的cb函数。
  • $once只订阅一次,这里的思想是使用函数包装:将传入的cb进行包装,然后订阅包装函数,包装函数逻辑是先调用cb再取消订阅。那什么时候会触发包装函数?就是当$emit时候。

还有一个小细节:利用函数的特性给包装函数绑定属性,将传入的cb绑定到包装函数的某个属性上。这个作用是什么?(ps:留个疑问,后续讲述)

  • $off是移除事件监听,源码中分了几种情况:

    1、参数lenght为0,清空所有事件。

    2、当事件传入的是数组,递归一个一个关闭事件。

    3、判断传入的事件名和fn,从事件名所对应的队列中删除。这里比较特殊的就是如果我先$once一个事件了,但我并没有触发,直接$off该事件了。这时候去删除fn的时候就利用到了上面说的利用函数的特性给包装函数绑定属性。

思考

订阅发布仅仅只能先订阅了才能发布吗

显然不是的,在这个Eevnt类我又实现了先发布后订阅的情况(ps:例如离线接收消息)。主要是思想就是发步放在发布的时候,我先将发布的这个函数包裹收集起来,等到订阅者订阅了全部清空,这里要注意的是发布方的生命周期应该是一次的。(ps:订阅方只能订阅一次)等到订阅的时候再将结果给订阅方。

 

 

具体实现的代码如下

针对事件机制,一般可用来通信传输数据。我又思考了一个问题:

如何在Vue中实现广播事件(ps:广播事件就是父一层一层的向子广播通知)和派发事件(子一层一层的向父派发)。

广播事件和派发事件也有2种情况:

针对每一层级都派发

指定一层级派发

 

利用好事件机制,会给项目的数据通信添加不一样的色彩哦~

总结

学习本身就是一个循进渐进的过程,每个人的学习方法都不一样,但我觉得自问自答的方式也是一份学习方法!

这就是我觉得为什么看源码不仅仅是为了面试的原因,学习框架重在一份思想并且知道内部api的实现机制也是一份收获。这段时间的源码学习笔记,希望能帮到各位~

另外,我学习过程中整理了一套Vue面试题:【建议收藏】2020大厂Vue面试题汇总,持续更新中~,有需要的朋友可以去看看。

望大家不要吝啬手中的👍👍

posted @ 2021-01-20 21:32  Android程序员吴彦祖  阅读(89)  评论(0编辑  收藏  举报