Vue2 Observer实例dep和闭包中dep区别详解

 更新时间:2022年10月31日 17:09:35   作者:tomato7777  
这篇文章主要为大家介绍了Vue2 Observer实例dep和闭包中dep区别详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

start

此前学习 Vue2 源码。对 Vue 源码中两次出现的new Dep(),不清楚它们的区别,写一个文章记录一下。

Vue2 源码中有两处通过new Dep生成dep实例:

1. Observer实例上的dep

export class Observer {
  value;
  dep;
  vmCount;
  constructor(value) {
    this.value = value;
     /* Observer的实例上有一个 dep 属性 */
    this.dep = new Dep();
    def(value, "__ob__", this);
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods);
      } else {
        copyAugment(value, arrayMethods, arrayKeys);
      }
      this.observeArray(value);
    } else {
      this.walk(value);
    }
  }

2. 定义getter,setter中dep

export function defineReactive(obj, key, val, customSetter, shallown) {
  /* 这个地方也定义了一个dep */
  const dep = new Dep()
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable,
    configurable,
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter(newVal) {
      /* ... */
    },
  })
}

为了方便后续介绍,我对两种dep的名称做一下缩减。

1. Observer实例上的dep => Observer实例上的dep

2. 定义getter,setter中dep => 闭包中的dep

既然是 new Dep(),肯定是想要实现某些功能。想要弄懂这两种 dep 的区别,先看看在源码中它们如何工作的。

1. 依赖收集

在打包输出的dist文件夹中,找到完整的 Vue.js 源码,全局搜索一下dep.depend() (收集依赖的方法)。

仅三处做了依赖收集

// 第 1 种情况
// 当触发对象属性 getter 的时候,`闭包中的dep`会收集依赖。
dep.depend()
// 第 2 种情况
// 当触发对象属性 getter 的时候,若属性值有更深的子层级。`Observer实例上的dep`会收集依赖。
childOb.dep.depend()
// 第 3 种情况
// 当触发对象属性 getter的时候,数据有更深的子层级且子层级是数组类型。递归遍历数组的每一项,若数组项上存在`Observer的实例`,则对应的`Observer实例上的dep`会收集依赖。
function dependArray(value) {
  for (var e = void 0, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    e && e.__ob__ && e.__ob__.dep.depend()
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}

情况说明:

  • 当触发对象属性 getter 的时候,闭包中的dep会收集依赖。
  • 当触发对象属性 getter 的时候,若属性值有更深的子层级。Observer实例上的dep会收集依赖。

当触发对象属性 getter 的时候,数据有更深的子层级且子层级是数组类型。递归遍历数组的每一项,若数组项上存在Observer的实例,则对应的Observer实例上的dep会收集依赖。

写到这里我有点疑惑,为什么数组的情况,还要额外处理?

  • 因为数组中如果存储的不是对象或数组,对应数组项不会有Observer的实例;
  • 因为数组中如果存储的是对象或数组,对应数组项身上就会有Observer的实例;
  • (核心原因:数组的每一项并不是都会被设置 getter,所以这里需要递归处理一下数组)

举个例子

写一个 Html 页面测试

<!DOCTYPE html>
<html lang="zh">
  <head>
    <meta charset="UTF-8" />
    <title>lazy_tomato</title>
  </head>
  <body>
    <div id="app">
      <div>{{ c }}</div>
    </div>
    <script src="./vue.js"></script>
    <script>
      new Vue({
        el: '#app',
        data() {
          return {
            b: '你好呀',
            c: [{}],
            d: {},
          }
        },
      })
    </script>
  </body>
</html>

修改一下Vue源码,加入打印:

if (Dep.target) {
  dep.depend()
  /* 这里 */
  console.log('111')
  if (childOb) {
    childOb.dep.depend()
    /* 这里 */
    console.log('222')
    if (Array.isArray(value)) {
      dependArray(value)
    }
  }
}
function dependArray(value) {
  for (var e = void 0, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    e && e.__ob__ && e.__ob__.dep.depend()
    if (e && e.__ob__ && e.__ob__.dep) {
      /* 这里 */
      console.log('333')
    }
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}

实验结果:

  • 如果页面没有使用 data 中的数据,那么三种情况的 dep.depend() 都不会触发。(因为没有触发 getter)
  • 如果页面仅仅使用到了 b,b 是简单类型的数据,没有子层级, 1会触发, 2、3不触发;
  • 如果页面仅仅使用到了 c,c 是数组类型的数据,而且数组中的项是对象,1、2、3会触发 2不触发;
  • 如果页面仅仅使用到了 d,d 是对象类型的数据,有子层级,1、2会触发, 3不触发;

结论: 由上面的结果,可以得到结论:

  • 闭包中的dep 关注的是对象属性;
  • Observer实例上的dep,关注的是对象中属性值是对象或者是数组的情况;

2. 通知更新

在打包输出的dist文件夹中,找到完整的 Vue.js 源码,全局搜索一下dep.notify()(通知更新的方法)。仅四处做了依赖收集。

// 第 1 种情况
// 当触发对象属性 setter 的时候,`闭包中的dep`会通知更新。
dep.notify()
// 还有三处,都是使用 `ob.dep.notify()` 的方式会通知更新。
// 场景分别为:
// 1. 改写数组的七个方法
// 2. set 方法
// 3. del 方法

结论:

  • 闭包中的dep 用于由对象本身修改而触发 setter 函数导致闭包中的 Dep 通知所有的 Watcher 对象。
  • Observer实例上的dep 则是在对象本身增删属性或者数组变化的时候被触发的 Dep。

思考

看到上述的内容,可以想到两种 dep 其实是各司其职。

  • 闭包中的 dep 用于管理对象本身修改而触发的依赖。
  • Observer 实例上的 dep 用于对象本身增删属性或者数组变化的时候被触发的依赖。

它可以用于弥补 Object.defineProperty() 的缺陷。

  • 对象属性 新增或者删除;
  • 数组方法不支持响应式;
  • 通过数组索引修改数据项;

拓展一

如果把 Observer 中初始化 dep 的代码注释掉,那么:$set,$del,重写的七种数组方法 都将失效。

class Observer {
  constructor(value) {
    // this.dep = new Dep(); // 正确的写法
    this.dep = { depend() {}, notify() {} }
    /* ... */
  }
}

拓展二

既然源码中的 $set,$del,重写的七种数组方法 ,通知更新,使用的是ob.dep.notify()。 那我们是否可以自己手动通知吗?

理论是可行的,但是 Vue 源码相对来说,做了更多特殊场景的考虑。所以用官方提供的 API 更可靠。

手动通知的案例

<!DOCTYPE html>
<html lang="zh">
  <head>
    <meta charset="UTF-8" />
    <title>lazy_tomato</title>
  </head>
  <body>
    <div id="app">
      <ul>
        <li v-for="item in list" :key="item">{{item}}</li>
      </ul>
      <h2 @click="foo">点击我通过数组索引添加数据</h2>
      <h2 @click="bar">点击我手动更新通知依赖更新</h2>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.7.10/dist/vue.js"></script>
    <script>
      new Vue({
        el: '#app',
        data() {
          return {
            list: [1, 2, 3],
          }
        },
        methods: {
          // 点击我向数组中添加数据
          foo() {
            let length = this.list.length
            this.list[length] = 'tomato' + length // 通过数组索引修改数据,默认是无法通知依赖更新的。
            console.log(this.list)
          },
          // 点击我手动更新通知更新
          bar() {
            this.list.__ob__.dep.notify()
          },
        },
      })
    </script>
  </body>
</html>

效果图:

end

  • 本文就 Dep 的初始化和基础的使用场景做了学习。

再总结一下:

  • 闭包中的dep 用于由对象本身修改而触发 setter 函数导致闭包中的 Dep 通知所有的 Watcher 对象。
  • Observer实例上的dep 则是在对象本身增删属性或者数组变化的时候被触发的 Dep。

更多关于Vue2 Observer与dep闭包的资料请关注脚本之家其它相关文章!

相关文章

  • vue form表单post请求结合Servlet实现文件上传功能

    vue form表单post请求结合Servlet实现文件上传功能

    这篇文章主要介绍了vue form表单post请求结合Servlet实现文件上传功能,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-01-01
  • 基于 Vue.js 之 iView UI 框架非工程化实践记录(推荐)

    基于 Vue.js 之 iView UI 框架非工程化实践记录(推荐)

    为了快速体验 MVVM 模式,我选择了非工程化方式来起步,并选择使用 Vue.js,以及基于它构建的 iView UI 框架。本文给大家分享基于 Vue.js 之 iView UI 框架非工程化实践记录,需要的朋友参考下吧
    2017-11-11
  • vue父组件中获取子组件中的数据(实例讲解)

    vue父组件中获取子组件中的数据(实例讲解)

    下面小编就为大家带来一篇vue父组件中获取子组件中的数据(实例讲解)。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-09-09
  • Vue3全局属性app.config.globalProperties的实现

    Vue3全局属性app.config.globalProperties的实现

    Vue3中的app.config.globalProperties是一个强大的全局配置功能,允许我们在应用级别设置和访问属性,本文主要介绍了Vue3全局属性app.config.globalProperties的实现,具有一定的参考价值,感兴趣的可以了解一下
    2024-01-01
  • vue项目引入svg图标的完整步骤

    vue项目引入svg图标的完整步骤

    在实际的项目开发中,使用svg图标占用内存比图片更小,映入图片内存比较大,同时也适用于不同屏幕的尺寸,下面这篇文章主要给大家介绍了关于vue项目引入svg图标的完整步骤,需要的朋友可以参考下
    2022-10-10
  • vuex的简单使用教程

    vuex的简单使用教程

    vuex是一个专门为vue.js设计的集中式状态管理架构。这篇文章主要介绍了vuex的简单使用,需要的朋友可以参考下
    2018-02-02
  • 从Vuex中取出数组赋值给新的数组,新数组push时报错的解决方法

    从Vuex中取出数组赋值给新的数组,新数组push时报错的解决方法

    今天小编就为大家分享一篇从Vuex中取出数组赋值给新的数组,新数组push时报错的解决方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-09-09
  • 浅谈vue首屏加载优化

    浅谈vue首屏加载优化

    这篇文章主要介绍了浅谈vue首屏加载优化,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-06-06
  • vue实现el-menu与el-tabs联动的项目实践

    vue实现el-menu与el-tabs联动的项目实践

    本文讲述了如何使用Vue.js中的ElementUI组件库实现el-menu与el-tabs的联动,通过在el-menu中选择菜单项,可以切换el-tabs的内容区域,具有一定的参考价值,感兴趣的可以了解一下
    2023-11-11
  • 前端vue如何监听对象或者数组某个属性的变化详解

    前端vue如何监听对象或者数组某个属性的变化详解

    这篇文章主要给大家介绍了关于前端vue如何监听对象或者数组某个属性的变化的相关资料,在Vue中你可以使用watch或者computed来监听对象或数组某个属性的变化,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2024-03-03

最新评论