Vue2.0 源码解析 --- 响应式原理

题目 : Vue2.0 源码解析 — 响应式原理

前言:vue的响应式原理不仅是面试时的高频考点,也是Vue区别于其他框架的一个很重要的特点,本文尝试用文图结合的方式来剖析响应式原理。理解几个核心的类(Observer 、Watcher 、Dep 、VNode),以及诸如依赖收集 、派发更新 等核心的概念

一、Vue在执行时的流程

<1> 概括
首先来看一张图片
Vue2.0 源码解析 --- 响应式原理_第1张图片
当你通过npm下载vue时,模块化开发时的源码在src目录下,dist是通过rollup打包后,运行在生产环境下的js文件。

  • 找到 dist 下的 vue.js,将其提取出来,新建一个html文件引入这个vue.js用于测试可调试代码,就像下面这样。
    Vue2.0 源码解析 --- 响应式原理_第2张图片

  • 当你一旦引入这个文件,文件中的js代码就会执行,可以简单看一看vue.js中的内容。
    Vue2.0 源码解析 --- 响应式原理_第3张图片

  • 看到这个匿名函数自调回立马执行,如果是在浏览器环境下就会在window上挂载一个Vue的变量,这个变量是一个factory执行的结构,执行后其实返回的就是Vue(一个巨大个的构造函数).

  • 这个factory工厂函数在执行的时候,不仅创建了Vue的构造函数,而且还在Vue的构造函数身上 (注意不是原型对象上) 挂载了很多的方法和属性,我们来看一下。(这时候最好去src中去看,更加友好)

  • 在src 下的 core 下的 index.js 中
    Vue2.0 源码解析 --- 响应式原理_第4张图片

  • 点进那个initGlobalAPI中去看一下。

  • 这里就一个一个展示了对响应式帮助不大,总之这个文件会在Vue上添加很多的方法和属性,也就是全局的api
    Vue2.0 源码解析 --- 响应式原理_第5张图片

  • 所以此时Vue既是一个构造函数也是一个对象,身上有很多的全局api和属性。诸如Vue.version Vue.component Vue.mixin等等,这就是执行环境在加载到Vue源码立即就会做的事情。

  • 下面当我们作为程序员去写出 new Vue({ ... })类似这样的代码的时候,就是去调用Vue构造函数,并且会把options传进去。
    Vue2.0 源码解析 --- 响应式原理_第6张图片

  • 但Vue的构造函数却异常单调就执行了个this._init()的方法,并把参数传了进去。而vue实例上此时是没有_init方法的,就会去原型上面找,找啊找?
    Vue2.0 源码解析 --- 响应式原理_第7张图片

  • 发现还真有,为什么会有这么一个方法呢,什么时候添加的呀?

  • 原来在初始化Vue的构造函数的时候在initMixin(Vue)时就做了这件事情
    Vue2.0 源码解析 --- 响应式原理_第8张图片

  • 点进initMixin中去看一下!
    Vue2.0 源码解析 --- 响应式原理_第9张图片

  • 瞧,这不是么!_init就在初始化时添加在了Vue的原型对象上。然后在new Vue的调用,那就分析一下呗!

  • 这里面初始化了生命周期,state ,render ,事件模型等,但最重要的还是对我们传入的data进行响应式化,后期我的博客中会对这些代码进行一一分析,但今天我们先来认识响应式化。
    Vue2.0 源码解析 --- 响应式原理_第10张图片

  • 点进这个里面进去看一下。
    Vue2.0 源码解析 --- 响应式原理_第11张图片
    如果存在data ,一般情况下都是存在的,就去初始化Data.点进去。
    Vue2.0 源码解析 --- 响应式原理_第12张图片

  • 这里面好多东西,但observe(data)才是重点,请深深的记住,这个的意思是 将data进行关注,或者观察。既然已经到这里了就不得不用到observer模块了。走到这里前面都能说通,但是我们还是欠缺一个observe的方法,不知道这个到底做了什么,我们在下面的章节中看一下。

二、Observer模块

  • 从上面的分析当中我们其实留了一个悬念,就是最终代码会执行到observe(data)这里来,那这个函数到底做了什么呢?这就是observer模块做的事情了。

  • 找到core 中的 observer 文件夹,打开它,可以看到这几个文件
    Vue2.0 源码解析 --- 响应式原理_第13张图片

  • 可以看到这几个文件,首先找到index.js文件。因为这个里面就有observe()方法
    Vue2.0 源码解析 --- 响应式原理_第14张图片

  • 这里面做的事情就一个,就是去观察传进来的数据,并且生成一个Observer的实例。返回出去

所以各位注意啦!接下来是重点,那就是Observer这个类它到底做了什么。一起来看一下!

Vue2.0 源码解析 --- 响应式原理_第15张图片

  • 我划线的部分就都是核心代码了。每当一个数据传进来进行实例化的时候,我可以直接告诉你,它一定是一个对象类型(普通对象或者数组),他会给这个对象类型添加一个__ob__的属性,指向当前的observer实例对象。(所以各位有没有发现只要是响应式对象的数据,都有的__ob__的字样)其次数组我们暂时不考虑,如果是 对象,那么就会指向walk()方法,walk方法会循环这个对象的每个属性,为每个属性进行defineReactive的处理。下面是重点!!!
    Vue2.0 源码解析 --- 响应式原理_第16张图片

  • 这是整个响应式的核心部分。首先会创建一个实例化一个Dep对象,我们暂且先不管这个是干嘛的,但你一定要知道这里实例化了一个对象。

  • 其次它截取了原生的getter和setter,并且执行了它,也就是正常的获取和修改是不会有问题的,除此之外它扩展了setter和setter,增加了一部分其他的逻辑,增加的getter中的这部分逻辑我们称之为依赖收集,setter中的我们称之为派发更新

  • 上面我们提到过,每一次调用defineReactive,都会创建一个Dep实例对象,但我还想说的是,每一个响应式属性都拥有一个唯一属于自己的dep对象。并且将永久的保存在内存中,直到实例销毁。为什么呢?

  • 我刚开始会觉得,这里的dep是一个局部变量,那么函数调用完之后不是会释放调么,那么为什么会永久的保存呢,这里的技巧就是因为dep在Object.defineProperty(,{})中进行了引用,因此形成了一个闭包,并不会被释放,这就是原因,而所谓的依赖收集:

  • 当有人来访问响应式数据的属性的时候,就会触发相应的getter,然后在条件符合的时候,便会调用一个方法,将所有关心这个属性变化的所有关心这,收集到dep中,因此dep其实本质上就是一个收集盒子,收集着所有关心这个属性变化的人。

  • 所以派发更新其实就很好理解了,因为这个dep已经存在于响应式数据中了,当某个变量变化了,就会触发setter,在setter当中它要想尽办法去造成影响呀

  • 怎么办呢?想了半天,它发现,原来我身上有个dep对象呀,dep对象上,保存了一堆关心着我的人呀,我知道告诉他们我变了,剩下了他们自己就会知道怎么办了。

  • 以上便是响应式原理的一部分真正的原理。

  • Vue2.0 源码解析 --- 响应式原理_第17张图片

  • 但是看到这里可能我们还是比较懵,dep是啥?那些关心他们的人是啥?

  • 其实就是Dep类,和Watcher类,一一来认识一下。

Dep类

Vue2.0 源码解析 --- 响应式原理_第18张图片

  • 其实所谓了Dep类代码并不复杂,但是理解起来会比较抽象,但请记住,this.subs = [],它就是一个盒子,往盒子里面放的元素必须是Watcher实例,但我们需要关注一个方法depend(),为什么要关注它,因为在我们刚刚说到的那个defineReactive方法中的getter中它调用了这个方法,这个方法简单来讲,就一个作用,将自己(某个dep实例对象)添加到全局watcher中保存,仅此而已,但请深深记住这句话。

  • 一句话概括notify方法做了什么,循环遍历自己保存的每个watcher,依次调用他们的update()方法

  • 这就是Dep类,先不着急和之前对接走通,我们直接把watcher也直接来认识一下。
    Vue2.0 源码解析 --- 响应式原理_第19张图片

  • 这个watcher实在太长了,我一下截不完,我们直接看核心的构造器部分吧。其实每次实例化一个watcher我们至少都需要传入这么3个参数。vue实例 ,路径表达式或者渲染函数,回调函数 也就是上面的 vm , expOrFn , cb

  • 这里我要解释一下,watcher分为两种,一种是掌控DOM的渲染watcher,一种是执行回调函数的,我将其称为 回调watcher

  • 我们在Vue中可以这样使用

{
  watch:{
    'a.b.c'(newVal){
      console.log('c发生了变化',newVal)
    }
  }
}
  • 其中a.b.c就是路径表达式,在watcher中会把这个路径表达式转化称为一个函数,这个函数传入一个对象,就可以根据这个路径表达式找到目标值。这个过程会触发响应式的getter
    Vue2.0 源码解析 --- 响应式原理_第20张图片
  • 最终this.getter一定是一个函数,要么是渲染函数,要么是一个取值函数。
  • 每次实例化一个watcher通常情况下都会执行watchr的get方法。
  • 首先会执行pushTarge(this),你可以点进去看一下。
    Vue2.0 源码解析 --- 响应式原理_第21张图片
  • 这句话的作用就是把当前的watcher变成全局watcher,在任何地方都可以访问到。
  • 下面会尝试执行一下this.getter,我们讨论渲染watcher的场景,此时全局会有一个渲染watcher,由于执行渲染函数的过程中,它一定会访问到响应式数据,进而会执行getter。我再把这个代码贴出来。
    Vue2.0 源码解析 --- 响应式原理_第22张图片
    此时因为全局中有渲染watcher的存在,因此当前的dep对象会调用dep.depend()方法,调用之后
    Vue2.0 源码解析 --- 响应式原理_第23张图片
    会调用全局watcher的addDep()方法,将当前的dep传过去。
    Vue2.0 源码解析 --- 响应式原理_第24张图片
    这个是全局watcher的addDep方法,它做了两件事情,第一将当前的dep添加到了全局watcher当中,第二,调用当前dep实例的addSub方法,当前dep对象就把全局watcher保存到了自己身上,因此形成了一个互相引用的过程。完成了依赖收集的使命。
  • 再次回到上面。由于渲染函数的执行,页面上就把页面渲染出来了。
  • 然后再次的,将全局watcher置为null,以便下次的依赖收集。
  • 此时整个函数执行栈的最深层执行完毕,函数退出。初次渲染结束。
    Vue2.0 源码解析 --- 响应式原理_第25张图片

三、核心概念

一、函数劫持

  • vue2并不支持使用数组下标的方式的响应式处理,原因是因为,vue2确实只劫持了部分数组的方法,我把代码贴上
    Vue2.0 源码解析 --- 响应式原理_第26张图片

  • 在上面的代码当中。vue劫持了数组的这几个方法,在这几个方法当中。他会执行默认的数组行为,并且会对新增的元素做响应式的处理。一旦有数组数据的变化,同样的会通知相应的watcher执行变化的操作。

  • 这里面有个小几千就是那个__ob__.

  • 还记得么,之前在进行new Observer()的时候,我们为每一个响应式对象都添加了一个__ob__的属性指向当前的observer实例对象。为了就是在这里访问的到observer的observerArray方法,如果不这么做,会发现还真的不好拿到实例里面的方法去用。

四、部分Api原理

一、Vue.set ,Vue.delete

  • 在vue中有这个两个api,众所周知,如果当vue已经实例化完成之后,我们再临时给响应式对象添加一个属性,这个添加的动作,是不会触发页面更新的。但是Vue.set可以实现,这是怎么做的呢?看一下源码
    Vue2.0 源码解析 --- 响应式原理_第27张图片
  • 其实并不难理解,如果是数组就调用一下splice就好了,因为splice已经被vue劫持, 它是可以给新增的数据进行响应式处理的。
  • 如果是对象,那么就再次响应式化一下就可以了呗?
  • 同样的delete也是一样的
    Vue2.0 源码解析 --- 响应式原理_第28张图片
  • 核心还是如果是数组方法就还是调用splice,那里面会触发更新,如果是对象,就调用一下更新的方法。触发更新就好了。

二、Vue.nextTick()
Vue2.0 源码解析 --- 响应式原理_第29张图片

  • nextTick的源码在这里。接受一个回调作为参数。他会把这个回调的执行放在一个函数中,再把这个函数放进callbacks容器中。
  • Vue2.0 源码解析 --- 响应式原理_第30张图片
    timerFunc是一个全局的变量,保存了一个函数,而且调用nextTick通常情况下会执行,核心就是p是一个成功的promise所以会执行第一个回调,也就是callbacks
    Vue2.0 源码解析 --- 响应式原理_第31张图片
    这个回调执行是异步的,因此就是等DOM更新完毕后在执行的。然后循环遍历callbacks中的函数执行。原理就是这样。

三、Vue.use

  • 在Vue中我们可以使用各种各样的插件比如

  • Vue.use(ElementUI) 等等。它的原理是什么呢?
    Vue2.0 源码解析 --- 响应式原理_第32张图片

  • 这里可以看出,插件如果是一个对象的话,那么一定会有一个install方法,然后这个方法会将剩余的参数传进这个Install方法。
    如果是函数的会直接调用这个函数。

  • 因此插件要么是个带install方法的对象,要么是一个函数。

四、Vue.mixin

  • 有没有觉得mixin是个很好用的方法,可以在vue中混入一段独立的逻辑和状态。非常有利于代码复用。它复杂吗?
    Vue2.0 源码解析 --- 响应式原理_第33张图片

  • 答案是就这么一段代码。原来,一旦我们定义好了一个mixin,它就会将我们的mixin混入的配置项与真正的实例组件配置项进行一个合并,那么这样,真正的组件其实就拥有了所有的状态和逻辑,并且可以复用,因为,Vue实例的一切行为都是根据这些配置项去完成的。

后记:好了,关于Vue2源码响应式我就介绍到这里了,很有很多不足之处,希望评论区批评指正,关于Vue2源码的编译原理,组件化,diff算法,期待后面的文章,成为一名优秀的前端工程师,一直在路上,加油!

你可能感兴趣的:(源码,javascript,vue.js,javascript)