likes
comments
collection
share

Vue2源码 - 响应式系统1

作者站长头像
站长
· 阅读数 35

前言

本文手把手,换位到读者的视角,一字一句的带你阅读源码。尽量不说多余的废话,也不讲什么高深的术语,用线性的方式带你学习。如果本文对你有帮助,也请大家点点赞收藏推荐一下。

响应式系统的概念

  • 响应式的意思就是,根据一个东西,从而变成什么东西(这我自己领悟的)。比如,我现在在照镜子,我做了一个动作,镜子里面的我也跟着我做了同样的动作。
  • 那么把这个概念放到Vue中呢。就是,我的数据是什么,页面中所用到这个数据的地方,也应该是什么。

体验响应式

先体验一下,如果现在没有响应式,我们的编码是一个什么样子。

用代码举个例子

<div id="myDiv"><span id="str">haha</span>!</div>
<button onclick="change()">Change</button>
<script>
function change() {
  // 获取要修改的元素
  const nameElement = document.getElementById('str');
  // 修改元素的文本内容
  nameElement.textContent = 'xixi';
}
</script>

从这段代码中,我们可以看到。没有响应式是一件多么痛苦的事情,只是想把haha变成xixi,我们就需要先获取html对应的元素,然后在写一大些名字很长的属性,费时费力。

接着,我们在体验一下简单响应式。

上代码

<div id="myDiv"><span id="str">haha</span>!</div>
<button onclick="change()">Change</button>
<script>
  // 定义一个响应式对象
  const data = {
    str: 'haha'
  };

  // 使用 Object.defineProperty 定义 str 属性
  Object.defineProperty(data, 'str', {
    get() {
      return data._str;
    },
    set(newValue) {
      data._str = newValue;
      // 更新相关的 HTML 元素
      const nameElement = document.getElementById('str');
      nameElement.textContent = newValue;
    }
  });

  // 修改 str 属性的方法
  function change() {
    data.str = 'xixi';
  }
</script>

上述代码,带你体验了简单的响应式。也就是Vue2.X的响应式雏形,怎么说呢?还不如上面那段代码。还是我们手动的获取了DOM元素,并且代码量还增加了。

那么重头戏来了,体验一下Vue的响应式

上代码

<div id="app">
  <p>Count: {{ count }}</p>
  <button @click="increment">Increment</button>
</div>

const app = Vue.createApp({
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++
    }
  }
})

app.mount('#app')

这段代码,我们换个例子,用计数的方式体现。当点击按钮时,count 的值会自动更新,并且在页面上显示出来。你能感觉到吗?这段代码中,我们没有操作DOM元素! 没有写很长很长的属性。 就做到了页面随着数据的改变而改变。其实,这些脏活累活,不是没有做,只是Vue帮我们做了,并且做的很好。

剖析Vue的响应式系统

首先,学习Vue的响应式绕不开3个类,下面我们一一说明一下这3个类。

Observer

Observer 是 Vue 中的一个核心概念,它的主要任务是负责将一个普通的 JavaScript 对象转换为一个响应式对象。Observer 通过递归的方式遍历对象的所有属性,并使用 Object.defineProperty 方法将(就是上面我们举例子用到的那个方法)它们转换为 getter/setter,从而使得这个对象的每个属性都具有响应性。当我们访问或修改这个对象的属性时,getter/setter 会被触发,从而实现对数据变化的监听响应

比方: 假如你现在装修一个新房子,这个新房子可以比作一个对象(数据),Observer可以比作一个成,你安装在这个新房子的摄像头。当这个房子的装修工人发生变化时(刷漆,铺瓷砖...),你都会立即知道。因为你通过安装摄像头,赋予了这个房子随时了解变化的能力。

Dep

Dep(依赖)是 Vue 中用来管理依赖关系的类。每个响应式属性都有一个与之关联的 Dep 实例,用来存储所有依赖于该属性的 Watcher 实例。当一个属性被访问时,当前的 Watcher 实例会被添加到这个属性的 Dep 中。当一个属性被修改时,Dep 会通知所有依赖于该属性的 Watcher 实例,让它们执行相应的更新操作。

比方: 你可以把Dep当做一个手机APP,一个房子会有好几个房间,我们在好几个房间都安装了摄像头。然后我们通过一个APP来管理这些摄像头,当某一个房间的工人发生变化的时候,APP就会向你推送一条信息(xx房间的工人,正在xx)。

Watcher

Watcher 是 Vue 中的观察者类,它用来观察一个表达式或计算属性的变化。当我们创建一个 Watcher 实例时,需要提供一个回调函数,这个函数会在 Watcher 所观察的表达式或计算属性的值发生变化时被调用。Watcher 会在初始化时读取一次它所观察的表达式或计算属性的值,以便收集依赖。在后续的更新过程中,如果 Watcher 所观察的表达式或计算属性的值发生了变化,Watcher 会再次读取新的值,并调用提供的回调函数。

比方:Watcher可以比作是你本人,或者你的家人们。你或你的家人们,都很关注装修这个事情。一直盯着手机。一旦APP向你发了条推送,(xx房间的工人,正在刷漆)。你打开APP一看,我X,我要刷白漆,怎么给我刷成黑的了。然后赶紧,通过这个摄像头告诉工人。你刷成黑的了,换成白的!

源码中的响应式系统

  • 此篇文章针对于获取操作,关于修改操作,会在第二篇中讲解。

observer/index.ts - Observer

class Observer {
  dep: Dep
  vmCount: number // number of vms that have this object as root $data

  constructor(public value: any, public shallow = false, public mock = false) {
    // this.value = value
    this.dep = mock ? mockDep : new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (isArray(value)) {
      if (!mock) {
        if (hasProto) {
          /* eslint-disable no-proto */
          ;(value as any).__proto__ = arrayMethods
          /* eslint-enable no-proto */
        } else {
          for (let i = 0, l = arrayKeys.length; i < l; i++) {
            const key = arrayKeys[i]
            def(value, key, arrayMethods[key])
          }
        }
      }
      if (!shallow) {
        this.observeArray(value)
      }
    } else {
      /**
       * Walk through all properties and convert them into
       * getter/setters. This method should only be called when
       * value type is Object.
       */
      const keys = Object.keys(value)
      for (let i = 0; i < keys.length; i++) {
        const key = keys[i]
        defineReactive(value, key, NO_INITIAL_VALUE, undefined, shallow, mock)
      }
    }
  } 

  /**
   * Observe a list of Array items.
   */
  observeArray(value: any[]) {
    for (let i = 0, l = value.length; i < l; i++) {
      observe(value[i], false, this.mock)
    }
  }
}

上述代码就是 Vue源码中,定义的Observer类,先不要慌,结合上面举的例子,给他传入一个对象,把这个对象中的属性,变成响应式的。比如,程序运行的第一次先传入data对象,然后首先先把data中的属性都变成响应式的。处理数组有处理数组的方法,对象有对象的方法,暂且先不开考虑数组,只考虑对象的情况。然后通过调用defineReactive方法赋予数据响应式的能力。

observer/index.ts - defineReactive

function defineReactive(
  obj: object,
  key: string,
  val?: any,
  customSetter?: Function | null,
  shallow?: boolean,
  mock?: boolean
) {
  // 创建一个Dep实例,这个实例,所有的响应式数据,都会有
  const dep = new Dep()
  // 获取对象的
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ( (!getter || setter) && (val === NO_INITIAL_VALUE || arguments.length === 2) ) {
    val = obj[key]
  }
  // 默认深度观察,除了$attrs这种明确写了不深度检测的情况除外, 
  // 执行了observe,就想到当于默认执行,深度检测了。
  let childOb = !shallow && observe(val, false, mock)
  // 赋予响应式能力
  Object.defineProperty(obj, key, {
    enumerable: true,  // 属性描述符
    configurable: true, // 属性描述符
    get: function reactiveGetter() {
       // 这里是获取数据,首先先看一下这个属性有没有一个get方法。
       // 如果有,那么就调用这个get方法
       // 如果没有,就返回当前属性的值
      const value = getter ? getter.call(obj) : val
      // 这个判断,在程序第一次进入此函数的时候是不会执行的,
      // 因为在创建阶段,还没有需要计算的Watcher实例
      if (Dep.target) {
        // 这个判断的意思是分为两个模式,如果是true那么是开发者模式
        // false则是生产模式
        // 开发者模式,你可以想象成,主要是给开发者工具服务的即可
        if (__DEV__) {
        // 这里是开发模式调用的dep中的depend方法,用来搜集 watcher依赖
          dep.depend({
            target: obj,
            type: TrackOpTypes.GET,
            key
          })
        } else {
          // 这里是生产模式调用的dep中的depend方法,用来搜集 watcher依赖
          dep.depend()
        }
        if (childOb) {
          // 在观察的属性上调用Dep类中的depend方法。
          childOb.dep.depend()
          if (isArray(value)) {
            dependArray(value)
          }
        }
      }
      // 这段主要是为了兼容v3,暂且不谈,就知道这里是在返回当前属性的值即可
      return isRef(value) && !shallow ? value.value : value
    },
    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val
      if (!hasChanged(value, newVal)) {
        return
      }
      if (__DEV__ && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else if (getter) {
        // #7981: for accessor properties without setter
        return
      } else if (!shallow && isRef(value) && !isRef(newVal)) {
        value.value = newVal
        return
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal, false, mock)
      if (__DEV__) {
        dep.notify({
          type: TriggerOpTypes.SET,
          target: obj,
          key,
          newValue: newVal,
          oldValue: value
        })
      } else {
        dep.notify()
      }
    }
  })

  return dep
}

上述是源码。defineReactive方法是真正的安装那个摄像头的地方,也就是将数据赋予响应式能力的地方。ok,我们先看function reactiveGetter() {}这个函数,这个函数就是响应式数据的get方法的实现,直接看源码即可(代码中有注释)。

通过上述源码,我们了解到了,一个数据被赋予了响应式的能力以后,会有这个get方法,然后每当我们去使用的这个属性的时候,这个get方法就会被触发。我在举个例子。

<div id="app">
  <p>Count: {{ count }}</p>
</div>
const app = Vue.createApp({
  data() {
    return {
      count: 0
    }
  }
})
app.mount('#app')

拿这个代码进行举例,我在data中定义了,count这个属性,程序开始执行,首先Observer先遍历data对象,把需要赋予响应式能力的属性传递给defineReactive进行响应式的改造。改造完了以后,count就变成了一个响应式的属性了。然后,程序继续运行发现,在模板中使用到了<p>Count: {{ count }}</p>。因为count被获取了,那么get方法就会被调用。这样,源码中的Dep.target 设置为这个 Watcher实例。if (Dep.target) 就会变为true。这样程序就会执行条件判断的内部。我们以生产模式为例,就会执行dep.depend()这个方法。ok,到了这里,我们接着往下看源码的分析

observer/dep.ts - Dep

class Dep {
  static target?: DepTarget | null
  id: number
  subs: Array<DepTarget | null>
  // pending subs cleanup
  _pending = false

  constructor() {
    this.id = uid++
    this.subs = []
  }
  // 搜集订阅当前Dep实例的Watcher实例。
  addSub(sub: DepTarget) {
    this.subs.push(sub)
  }
  // 删除订阅当前Dep实例的Watcher实例。
  removeSub(sub: DepTarget) {
    this.subs[this.subs.indexOf(sub)] = null
    if (!this._pending) {
      this._pending = true
      pendingCleanupDeps.push(this)
    }
  }
  // 这个参数针对于开发者模式,我们暂且忽略。
  depend(info?: DebuggerEventExtraInfo) {
    // 先看一下这个watcher依赖函数是否有元素,
    if (Dep.target) {
      // 调用addDep方法,这个方法是在Watcher类中所实现,
      // 用于将这个Dep实例存放到Watcher的订阅者列表中
      Dep.target.addDep(this)
      // 在开发模式下调用,主要是针对于调试,开发者工具有用处。
      if (__DEV__ && info && Dep.target.onTrack) {
        Dep.target.onTrack({
          effect: Dep.target,
          ...info
        })
      }
    }
  }
 
  // 发布
  notify(info?: DebuggerEventExtraInfo) {
    const subs = this.subs.filter(s => s) as DepTarget[]
    if (__DEV__ && !config.async) {
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      const sub = subs[i]
      if (__DEV__ && info) {
        sub.onTrigger &&
          sub.onTrigger({
            effect: subs[i],
            ...info
          })
      }
      sub.update()
    }
  }
}

上述源码中,定义了Dep类,结合上面的例子,我们继续。 首先看到这个类中定义的depend方法,if (Dep.target)这个判断是true,因为不是true,我们也看不到这个方法的执行了。继续往下走,到了Dep.target.addDep(this)这句代码。先看一下这个方法的定义

observer/dep.ts - DepTarget

// 定义一个接口DepTarget,并继承了DebuggerOptions这个类,
interface DepTarget extends DebuggerOptions {
  id: number
  addDep(dep: Dep): void
  update(): void
}

这个addDep方法,虽然是在Dep中定义的,但是,这个函数的实现是在Watcher类中实现的,是的。其实从这能体现出,这两个类你中有我,我中有你。既然是在Watcher中实现的,那接着往下看,

observer/watcher.ts - addDep

  // 目前展示的代码是Watcher类中的addDep相关的代码片段,不是全部代码。
  newDeps: Array<Dep> // 定义newDeps
  this.newDeps = [] // 初始化newDeps
  // 传入的是一个Dep实例,此方法是在Dep中定义,在Watcher类中实现。
  addDep(dep: Dep) {
    // 获取一下Dep实例的ID值
    const id = dep.id
    // 如果这个实例的 ID 不存在 newDepIds 集合中
    if (!this.newDepIds.has(id)) {
      // 那么就把这个实例的ID添加进去
      this.newDepIds.add(id)
      // 根据上面的代码可得知,** **
      // newDeps是一个Dep类型的数组,所以只能存放Dep类型的数据
      this.newDeps.push(dep)
      // 首次调用次方法,这个if是true,因为deIds中还没有任何的数据
      if (!this.depIds.has(id)) {
        // 把当前的Watcher实例添加进addSub这个数组中
        dep.addSub(this)
      }
    }
  }

从上述源码的注释中看到,dep.addSub(this)这句代码。将当前的这个Dep实例添加到addSub数组中。addSub这个数组就是存放订阅这个Dep实例的观察者数组,里面会存放一个或者多个Watcher实例。其实到这里就可以大致的感受到了在代码中,这三者的关系了。

observer/watcher.ts - Watcher

// 在这里,只为了讲解其原理,值贴出了需要用到的get方法
class Watcher implements DepTarget {
  //可以理解为Watcher实例的第一个调用的方法
  get() {
    // 调用Dep中的 pushTarget,把watcher实例,放进Dep的依赖收集函数中
    // 并将这个 Watcher 实例设置为当前的目标观察者(Dep.target)
    pushTarget(this)
    let value
    const vm = this.vm // 存放Vue实例
    try {
      // 以 vm 为上下文(this 值),
      // 调用 getter 函数。getter 函数的参数也是 vm
      // 但是这个参数的vm一般用不到。就是,我可以不用,但你不能没有
      value = this.getter.call(vm, vm)
    } catch (e: any) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // 看一下侦听属性有没有深度监听的选项
      if (this.deep) {
        // 遍历所有属性,从而实现深度监听
        traverse(value)
      }
      // 这个方法一旦调用,那么Dep.target这个数组中也会随之删除一位元素。
      popTarget()
      this.cleanupDeps()
    }
    return value
  }
}

上述代码中,是Watcher类的执行代码。还是回到一开始的那个例子,页面中使用到了 <p>Count: {{ count }}</p>,那么这一刻Watcher也就开始工作了,首先 get 方法来获取数据的初始值,并在这个过程中收集依赖,记录哪些数据(Dep实例)被这个 Watcher 所依赖。在 get 方法中,Watcher 会调用 pushTarget(this) 将自己设置为当前的目标观察者,然后调用 getter 函数来获取数据的值。在调用 getter 函数时,如果访问了其他的响应式数据,这些数据的 Dep 实例就会将当前的目标观察者(也就是这个 Watcher)添加到它们的依赖列表中。这样,当这些数据的值发生变化时,就知道需要通知哪些 Watcher 进行更新。

总结

在 Vue 的响应式系统中,当我们访问一个响应式数据时,会触发 getter,这时 Vue 会将当前的 Watcher 添加到这个数据的 Dep 中。当数据发生变化时,会触发 setter,这时 Dep 会通知所有依赖于这个数据的 Watcher,然后 Watcher 会重新计算或重新渲染,从而更新视图。这个系统使得 Vue 能够自动追踪数据的变化并更新视图,无需我们手动操作 DOM,简化了开发的复杂性。

转载自:https://juejin.cn/post/7269385060612243471
评论
请登录