踩了Vue2运行机制的坑-响应式原理

最近遇到一个很奇怪的bug:

前置:后端接口返回的数据是这样的:

①首先在store中取出后端返回的数据A=res.data,在这里打印输出是正常的

②然后在vue页面上再取出A.data也就是res.data.data,以及其它几个字段即res.data.XXX,在这里打印输出是空白的

③结果在图表页中取出的一直是undefined(读取属性不存在),但是第一次加载不出来,再刷新一下却又能拿到数据了。

最后的解决方案:

在定义data/computed的时候,就先对A的所有属性进行枚举,需要用到后端data里的哪一层就枚举几层,于是就没问题了!

不仅仅是这个地方,在其他地方也能遇到这种bug!!!比如在展示一块信息的时候,已经写好了遍历,根据后端返回的字段自己去渲染,但是,如果不在data里面把每一个字段都枚举一下,就会导致在表单进行编辑修改的时候,数据明明数据变化了可是视图没有更新,于是乎,后端每次新增一个字段,前端也需要在枚举中增加一个字段才行。

在问了大佬以及看了文档之后,才知道原来这是Vue2响应式原理的一个问题......

因为第一次加载的时候,找不到softwareDeadline里面的属性,没有执行defineReactive(),所以取的时候是undefined,但是ctrl+S的时候,已经是从后端取过数据放在缓存里了,所以它又显示数据正常了。

一、Vue2的响应式流程

1、核心思路

当创建Vue实例时,vue会遍历data选项的属性,利用Object.defineProperty为属性添加getter和setter对数据的读取进行劫持(其中getter用来依赖收集,setter用来派发更新),并且在内部追踪依赖,在属性被访问和修改时通知变化。

每个组件实例会有相应的watcher实例,会在组件渲染的过程中记录依赖的所有数据属性(进行依赖收集,还有computed watcher、user watcher实例),之后依赖项被改动时,setter方法会通知依赖于此data的watcher实例重新计算(派发更新),从而使它关联的组件重新渲染。

  • 在init数据初始化的时候,对象内部通过 defineReactive 方法,使用 Object.defineProperty 将属性进行劫持(这时候只会劫持已经存在的属性)。如果数据是数组类型, Vue2中是通过重写数组方法来实现。多层对象是通过递归来实现劫持的。

  • 在初始化流程中的编译阶段,当render function 被渲染的时候,会读取Vue实例中和视图相关的响应式数据,此时会触发 getter 函数进行 依赖收集 (将观察者Watcher对象存放到当前闭包的订阅者Dep的subs中)。

  • 当数据发生变化或者视图导致的数据发生变化时,会触发数据劫持的setter函数,setter会通知初始化依赖收集中的Dep中和视图相应的 Watcher ,告知需要重新渲染视图,Watcher 就会再次通过 update 方法来更新视图。

2、关键角色

如上图所示:一个属性可能有多个依赖,每个响应式数据都有一个Dep来管理它的依赖。

Observer

  • 通过Observer对象将数据对象转换为响应式数据。
  • 通过Object.defineProperty()给对象的属性添加getter和 setter,用于依赖收集和派发更新。

getter():进行依赖收集。当访问响应式数据时,会将当前的Watcher对象添加到依赖列表中。

setter():数据发生变化时,setter函数会被触发,并通知相关的Watcher对象进行更新。

由于 Object.defineProperty 无法监听对象的变化,所以 Vue2 中设置了一个 Observer 类来管理对象的响应式依赖,同时也会递归侦测对象中子数据的变化。

Dep

  • 相当于一个管家,负责添加或删除相关的依赖和通知相关的依赖进行相关操作。
  • 用于收集当前响应式对象的依赖关系,每个响应式对象包括子对象都拥有一个Dep实例(里面subs是Watcher实例数组),当数据有变更时,会通过dep.notify()通知各个watcher。

Vue会为响应式对象中的每个属性、对象本身、数组本身创建一个Dep实例,每个Dep实例都有能力做以下两件事:

  • 记录依赖:是谁在用我。(当有人访问这个数据时,把它记录下来。)
  • 派发更新:我变了,我要通知那些用到我的人。(当数据被修改时,通知所有的依赖数据更改了。)
  1. 当读取响应式对象的某个属性时,它会进行依赖收集:有人用到了我
  2. 当改变某个属性时,它会派发更新:那些用我的人听好了,我变了

Watcher

  • 用于存储数据变化后要执行的更新函数 ,调用更新函数可以使用新的数据更新视图。当数发生变化时触发依赖,Dep通知所有的Watcher更新视图
  • Watcher通过new关键字实例化的时候让Dep.target指向自己

只有 Watcher 触发的 getter才会进行依赖收集,哪个Watcher触发了getter,就把哪个 Watcher收集到Dep中。当响应式数据发生改变的时候,就会把收集到的 Watcher 都进行通知。(当Dep进行派发更新时,它会通知之前记录的所有watcher:我变了)

每一个vue组件实例,都至少对应一个watcher,该watcher中记录了该组件的render函数。

watcher首先会把render函数运行一次以收集依赖,于是那些在render中用到的响应式数据就会记录这个watcher。

当数据变化时,dep就会通知该watcher,而watcher将重新运行render函数,从而让界面重新渲染,同时重新记录当前的依赖。

3、代码

javascript 复制代码
const Observer = function(data) {
  // 循环修改为每个属性添加get set
  for (let key in data) {
    defineReactive(data, key);
  }
}

const defineReactive = function(obj, key) {
  // 局部变量dep,用于get set内部调用
  const dep = new Dep();
  // 获取当前值
  let val = obj[key];
  Object.defineProperty(obj, key, {
    // 设置当前描述属性为可被循环
    enumerable: true,
    // 设置当前描述属性可被修改
    configurable: true,
    get() {
      console.log('in get');
      // 调用依赖收集器中的addSub,用于收集当前属性与Watcher中的依赖关系
      dep.depend();
      return val;
    },
    set(newVal) {
      if (newVal === val) {
        return;
      }
      val = newVal;
      // 当值发生变更时,通知依赖收集器,更新每个需要更新的Watcher,
      // 这里每个需要更新通过什么断定?dep.subs
      dep.notify();
    }
  });
}

const observe = function(data) {
  return new Observer(data);
}

const Vue = function(options) {
  const self = this;
  // 将data赋值给this._data,源码这部分用的Proxy所以我们用最简单的方式临时实现
  if (options && typeof options.data === 'function') {
    this._data = options.data.apply(this);
  }
  // 挂载函数
  this.mount = function() {
    new Watcher(self, self.render);
  }
  // 渲染函数
  this.render = function() {
    with(self) {
      _data.text;
    }
  }
  // 监听this._data
  observe(this._data);  
}

const Watcher = function(vm, fn) {
  const self = this;
  this.vm = vm;
  // 将当前Dep.target指向自己
  Dep.target = this;
  // 向Dep方法添加当前Wathcer
  this.addDep = function(dep) {
    dep.addSub(self);
  }
  // 更新方法,用于触发vm._render
  this.update = function() {
    console.log('in watcher update');
    fn();
  }
  // 这里会首次调用vm._render,从而触发text的get
  // 从而将当前的Wathcer与Dep关联起来
  this.value = fn();
  // 这里清空了Dep.target,为了防止notify触发时,不停的绑定Watcher与Dep,
  // 造成代码死循环
  Dep.target = null;
}

const Dep = function() {
  const self = this;
  // 收集目标
  this.target = null;
  // 存储收集器中需要通知的Watcher
  this.subs = [];
  // 当有目标时,绑定Dep与Wathcer的关系
  this.depend = function() {
    if (Dep.target) {
      // 这里其实可以直接写self.addSub(Dep.target),
      // 没有这么写因为想还原源码的过程。
      Dep.target.addDep(self);
    }
  }
  // 为当前收集器添加Watcher
  this.addSub = function(watcher) {
    self.subs.push(watcher);
  }
  // 通知收集器中所的所有Wathcer,调用其update方法
  this.notify = function() {
    for (let i = 0; i < self.subs.length; i += 1) {
      self.subs[i].update();
    }
  }
}

const vue = new Vue({
  data() {
    return {
      text: 'hello world'
    };
  }
})

vue.mount(); // in get
vue._data.text = '123'; // in watcher update /n in get

相关推荐
陈随易24 分钟前
Postman 被曝自动往 Claude Code 和 Codex 装插件
前端·后端·程序员
Dream-Y.ocean2 小时前
鸿蒙PC平台宅匠 3D 室内设计适配实战:基于 Electron 壳方案的 Three.js 双渲染引擎与参数化户型编辑器开发
javascript·3d·harmonyos
Csvn3 小时前
性能监控
前端
Highcharts.js3 小时前
Highcharts Gantt使用符号的自定义数据标签开发甘特图示例
javascript·甘特图·highcharts·图表开发·实战代码·响应式图表
Csvn4 小时前
构建工具
前端
FRing034 小时前
vue2干货总结3(computed计算属性、watch监听器)
开发语言·前端·vue.js·vue2
Java小卷5 小时前
JuanForm vs FormCreate!Vue3 表单引擎联动配置 PK!
vue.js·低代码
kyriewen7 小时前
Skills 榜前 10 我全装了:只有 3 个能直接用
前端·javascript·程序员
IT_陈寒7 小时前
当Java的hashCode遇上Lombok,我的缓存被全清了
前端·人工智能·后端
工控丫丫7 小时前
帆软看板中经常使用的公式函数及经常搭的组合
前端·javascript·数据库