理解 Vue 数据响应原理

发布于 2023-08-18 08:47:06 字数 4610 浏览 26 评论 0

Vue 的数据响应是通过数据劫持结合发布者和订阅者实现的。其主要是通过 Object.defineProperty() 来实现数据劫持的。本文的例子实现数据更新驱动视图更新是直接通过操作 DOM,且是通过直接分析 DOM 来定位依赖的。其实 Vue 内部机制不完全是这样。下面我补充一些 Vue 实现数据响应的一些大体细节。这块东西真的很多,我仔细说个大概,具体细节还需要大家自己去了解。

1 数据观察

实例化 Vue 实例的时候,Vue.prototype._init 方法被第一个执行,在 initState 函数内部使用 initData 函数初始化 data 选项。这是数据响应的开始。initData 函数的主要作用是保证 options 中的 data 选项是个函数且返回的对象。同时在 Vue 实例对象上添加代理访问数据对象的同名属性。最后 通过调用了 observe 函数观测数据,将 data 变成响应式的(data 数据类型 是对象或数组时处理的 方式是不同的)。实现对象类型的 data 的观测主要是通过 defineReactive 函数

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  // 省略...

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) { //  watcher
        // 这里闭包引用了上面的 dep 常量
        dep.depend() // 收集依赖 Watcher
        // 省略...
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      // 省略...
      if (setter) { // 如果属性原本存在 set 函数则调用
        setter.call(obj, newVal)
      } else {
        val = newVal
      }

      // 这里闭包引用了上面的 dep 常量
      dep.notify() // 触发依赖
    }
  })
}

在访问器属性的 getter/setter 中,通过闭包引用了前面定义的“筐”,即 dep 常量。每一个数据字段都通过闭包引用着属于自己的 dep 常量。

那么为数组类型该如何处理???

处理数组的方式与纯对象不同,数组是一个特殊的数据结构,它有很多实例方法,并且有些方法会改变数组自身的值,这些方法有:push、pop、shift、unshift、splice、sort 以及 reverse 等。其通过拦截数组变异方法的方式得知用户调用这些变异方法,从而触发依赖。数组本身也是一个对象,所以它实例的 proto 属性指向的就是数组构造函数的原型,即 arr.proto === Array.prototype 为真。其是通过设置 proto 属性的值为一个新的对象,且该新对象的原型是数组构造函数原来的原型对象。在新对象的变异方法里收集依赖

2 收集依赖

正是因为 watcher 对表达式的求值,触发了数据属性的拦截器函数,从而收集到了依赖,当数据变化时能够触发响应。

new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)
 updateComponent = () => {// 可以简单的认为把渲染函数生成的虚拟 DOM 渲染成真正的 DOM
    // vm._render 函数的作用是调用 vm.$options.render 函数并返回生成的虚拟节点(vnode)
    // vm._update 函数的作用是把 vm._render 函数生成的虚拟节点渲染成真正的 DOM
    vm._update(vm._render(), hydrating)
  }

在上面的代码中 Watcher 观察者实例将对 updateComponent() 函数求值, updateComponent 函数的执行会间接触发渲染函数(vm.$options.render)的执行,而渲染函数的执行则会触发数据属性的 get 拦截器函数,从而将依赖(观察者)收集,当数据变化时将重新执行 updateComponent 函数,这就完成了重新渲染。
Watcher constructor 的最后

if (this.computed) {
  this.value = undefined
  this.dep = new Dep()
} else {
  this.value = this.get() // 此时调用 Watcher 的实例方法 get
}

get () {  // get 为 Watcher 的实例方法
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    value = this.getter.call(vm, vm) // 此时 this.getter 指的就是 updateComponent
  } catch (e) {
    if (this.user) {
      handleError(e, vm, `getter for watcher "${this.expression}"`)
    } else {
      throw e
    }
  } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value)
    }
    popTarget()
    this.cleanupDeps()
  }
  return value
}

3 触发依赖

修改属性值时会触发属性的 set 拦截器函数,这样就会调用 Dep 实例对象的 noitfy 方法,

set: function reactiveSetter (newVal) {
  // 省略...
  dep.notify()
}
export default class Dep {
  // 省略...

  constructor () {
    this.id = uid++
    this.subs = []
  }

  // 省略...

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

notify 方法只做了一件事,就是遍历当前 Dep 实例对象的 subs 属性所保存的所有观察者对象,并逐个调 用观察者对象的 update 方法,这就是触发响应的实现机制,那么大家应该也猜到了,重新求值的操作应该是在 update 方法中进行的,那我们就找到观察者对象的 update 方法真正的更新变化操作都是通过调用 观察者实例对象的 run 方法完成的,run 方法内判断次 Watcher 是否是激活状态,若激活则调用实例方法 getAndInvoke。

getAndInvoke (cb: Function) {
// 重新求值其实等价于重新执行渲染函数,最终结果就是重新生成了虚拟 DOM 并更新真实 DOM,这样就完成了重新渲染的过程
  const value = this.get() 
  if (
    value !== this.value ||
    // Deep watchers and watchers on Object/Arrays should fire even
    // when the value is the same, because the value may
    // have mutated.
    isObject(value) ||
    this.deep
  ) {
    // set new value
    const oldValue = this.value
    this.value = value
    this.dirty = false
    if (this.user) {
      try {
        cb.call(this.vm, value, oldValue)
      } catch (e) {
        handleError(e, this.vm, `callback for watcher "${this.expression}"`)
      }
    } else {
      cb.call(this.vm, value, oldValue)
    }
  }
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

捶死心动

暂无简介

0 文章
0 评论
23 人气
更多

推荐作者

qq_E2Iff7

文章 0 评论 0

Archangel

文章 0 评论 0

freedog

文章 0 评论 0

Hunk

文章 0 评论 0

18819270189

文章 0 评论 0

wenkai

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文