vue面试题(vue2响应式源码剖析)

一、前言

这篇文章结合Vue2.7.16的源码和一个Vue2的项目,来详细讲解Vue2实现响应式数据的核心代码

1.1 准备

  1. 安装@vue/cli

    npm install -g @vue/cli
    
  2. 创建vue项目

    vue create vue2-test
    
  3. 修改Vue实例的配置对象

    image-20240318200957743

二、响应式处理的入口

  1. 通过 new Vue() 调用 Vue 构造函数,然后会执行里面的this._init(options)方法

    image-20240318201336829
  2. _init(options)方法定义在Vue.prototype上,定义是通过执行initMixin(Vue)实现的

    image-20240318201612226

  3. _init(options)方法中,做了一些初始化的操作。其中initState(vm)用来初始化状态。

    image-20240318201828392

  4. initState(vm),按照props、methods、data、computed、watch的顺序,对一些属性做了初始化操作。其中initData(vm)就是对配置对象中的data做响应式处理的。

    image-20240318202029673

  5. initData()方法中,做了以下处理:

    • 获取到配置对象上面的data配置项,如果是用方法定义的就通过调用方法获取,如果是用对象配置的,就直接访问。然后将data绑定到vm._data上。
    • 检查data中的属性名是否与methods、props中定义重复
    • 通过proxy(),将vm._data上的数据直接放在了vm身上。内部就是使用Object.defineProperty()实现通过vm.属性去访问vm._data.属性。这样访问要方便一点。
    • 调用observe(data)来对数据进行观测,这也是响应式处理的入口

    image-20240318202356812

    image-20240318203317526

三、入口函数observe

  1. observe接收一个value(第一次传入的就是data),然后判断其身上是否有_ob_属性,该属性用来标识是否已经被观测。如果已经被观测过了,就直接返回value身上的ob对象。

  2. 判断数据类型等条件,条件满足,则创建一个Observer实例并返回。

  3. 在上面initData()方法中,会拿到返回的ob对象,并将ob对象身上的vmCount++。它的作用就是用来区分我们操作的对象是根\(data还是其子属性。 在Vue中,应该避免直接在一个Vue实例或其根`\)data`对象上添加或删除响应式属性。

    image-20240318203541883

    image-20240318204737737

    image-20240318204810530

四、 Observer类

  1. Observer类有两个属性:depvmCountvmCount的作用已经说过了。dep是一个Dep实例,用来收集依赖和派发更新。

  2. Observer的构造器中对depvmCount进行了初始化,然后在def(value, '__ob__', this)方法中,通过Object.defineProperty为当前的value(第一次调用时是data)添加了一个_ob_属性,属性值为当前的Observer实例

  3. 根据当前的value是不是数组,来进行不同的操作。如果不是数组,则遍历当前value的所有属性,执行defineReactive()方法。是数组的情况,在后面单独说明。

    image-20240318205354618

五、defineReactive

  1. defineReactive()方法就是用来对当前的属性做响应式处理的,主要做了以下操作:

    • 创建一个Dep实例对象,用来收集当前属性的依赖和派发更新。
    • 通过Object.getOwnPropertyDescriptor(obj, key)来获取当前属性的所有自有描述信息,比如:是否可写、可枚举、可配置、get()、set()
    • 如果当前属性不可配置,就直接返回不做处理。
    • 获取当前属性的getter和setter,后面会用到。获取当前属性的属性值,后面会用到。
    • 根据是否深度观测,来决定是否调用observe(value)方法。调用该方法,就可以实现递归地对所有属性进行响应式处理,同时,该方法还会为当前属性身上添加一个_ob_属性,指向一个Observer实例,然后将该实例对象返回,即childOb
    • 使用Object.defineProperty为当前属性添加getter、setter、enumerable: true、configurable: true等属性,即在此做数据劫持。

    image-20240318210214193

  2. 在get方法中,首先会获取当前属性的属性值(有getter就通过getter,没有就使用前面获取到的val)。

    然后通过dep.depend()Dep.target(当前访问该属性的watcher)添加到当前属性的依赖列表中,同时也添加到childOb的依赖列表中。这里之所以添加到childOb的列表中,是为了在其它地方也能知道有哪些watcher依赖该属性。因为可以通过属性._ob_.dep来进行派发更新。这也是$set$delete实现数据响应式的前提。

    最后将数据返回。

    image-20240318211537805

  3. 在set方法中,首先获取到当前属性的属性值(有getter就通过getter,没有就使用前面获取到的val)。

    然后对比当前set方法接收到的值,如果没有变化,就不做处理。如果发生了变化,就修改当前的值(有setter就通过setter修改,没有就手动修改)。

    考虑到set方法接收的新值可能也是一个对象,所以需要对这个新的值再次调用observe(newValue)进行观测。

    最后调用dep.notify()派发更新(通知依赖列表中的所有watcher执行update方法)。

    image-20240318212315488

六、收集依赖和派发更新

  1. 在get方法中,会收集依赖当前属性的所有watcher

    image-20240318212558614

  2. Dep类的结构如下

    根据Dep类的定义,可以知道每一个Dep实例都有一个id(唯一标识)和一个subs(用来保存watcher)。

    此外还有两个最重要的方法,depend()(收集依赖)和notify()(派发更新)。

    同时Dep还有一个静态属性target,指向的就是当前的watcher。

    image-20240318212825867

  3. depend()

    需要注意的是depend()方法并没有直接将Dep.target添加到subs中,它反而是调用了Dep.targetaddDep(this)方法,将当前的Dep实例传给了watcher

    image-20240318212948340

    watcher的addDep()方法,将传过来的dep信息保存下来,并通过dep.addSub(this),将当前watcher添加到dep的subs中。

    这里保证了watcher不重复收集dep,dep不重复收集watcher。而且,当watcher被销毁的时候,就可以根据收集的dep信息,通知相应的dep将自己从subs中移除,以免后面进行派发更新的时候通知给一个已经不存在的watcher。

    image-20240318213154409

  4. notify()

    这里根据watcher的id进行了排序,因为watcher有三种:渲染watcher、计算属性watcher、用户watcher。这三者要保证计算属性watcher、用户watcher、渲染watcher的顺序执行。

    遍历调用watcher的update方法实现更新操作。

    image-20240318213335207

七、数组的响应式处理

  1. 对于数组的响应式处理,这里首先做了一个判断,在if (!mock)里面,对数组的七个方法进行了重写,准确来说,是为响应式的数组修改了原型对象。

    重写的七个方法,在原有方法的功能之上,实现了派发更新,就是通过数组身上的_ob_.dep实现的。

    image-20240318215518076

    这里的hasProto是如何定义的?就是判断对象是否支持原型,那一般情况都是支持的。所以就先不管下面的分支处理了。

    image-20240318221925392

    再来看一下arrayMethods是什么

    • 首先通过Object.create(arrayProto),将新创建的arrayMethods对象的_proto_属性指向arrayProto,也就是arrayMethods._proto_ = = Array.prototype
    • 对会改变数组的七个方法,进行了重写。具体实现是
      • 获取数组原始的方法original
      • 通过def()方法,调用Object.definePropertyarrayMethods身上添加新的方法。新方法在原始方法original的基础上进行增强
      • 对于push、unshift、splice等会插入新值的方法,需要获取到新的值,然后对这个值做观测。

    image-20240318220012088

    最后,再将数组的原型属性指向新的对象arrayMethods,即

    array._proto_ == arrayMethods

    arrayMethods._proto_ = = Array.prototype

    image-20240318224636418

  2. 此外,还在if (!shallow)这个判断中,调用了observeArray(value)方法。该方法遍历数组元素,对每一个元素执行observe(),就是为了将数组中那些对象元素身上的属性变成响应式的。而对于基本类型的元素不做任何处理。因为数组可能会有很多的元素,为每一个元素添加getter和setter是很耗费性能的。

    image-20240318220937633

posted @ 2024-03-18 23:36  平平丶淡淡  阅读(23)  评论(0编辑  收藏  举报