Vue2的响应式原理解析,看了都懂的流程图

Vue 2响应式原理时序图与部分源码记录如下:

图很直观!

Vue 2 响应式原理时序图

官网对响应式的说明: v2.cn.vuejs.org/v2/guide/re...

当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。

这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。这里需要注意的是不同浏览器在控制台打印数据对象时对 getter/setter 的格式> 化并不同,所以建议安装 vue-devtools 来获取对检查数据更加友好的用户界面。

每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把"接触"过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

核心点1: Object.defineProperty劫持属性

第一步是对JavaScript对象进行监听,监听使用到的api就是 Object.defineProperty,通过它来重写对象属性的getter和setter,如何重写的,使用到的函数就是observer

主要逻辑如下:

observer.js:

javascript 复制代码
// 将一个对象转换为响应式对象
function observe(obj) {
  if (!obj || typeof obj !== 'object') {
    return;
  }
  
  // 为每个对象创建一个Observer实例
  return new Observer(obj);
}

// Observer类,将对象的所有属性转换为getter/setter
class Observer {
  constructor(value) {
    this.value = value;
    
    if (Array.isArray(value)) {
      // 数组响应式处理...
    } else {
      this.walk(value);
    }
  }
  
  // 遍历对象所有属性并转换为getter/setter
  walk(obj) {
    const keys = Object.keys(obj);
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i]);
    }
  }
}

// 核心方法:将对象的属性转换为响应式属性
function defineReactive(obj, key) {
  // 为每个属性创建一个Dep实例
  const dep = new Dep();
  
  // 获取对象当前属性的值,闭包存储
  let val = obj[key];
  
  // 递归观察属性值(如果是对象)
  let childOb = observe(val);
  
  // 使用Object.defineProperty劫持属性
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    
    get: function reactiveGetter() {
      // 如果当前有正在评估的Watcher实例
      if (Dep.target) {
        // 收集依赖
        dep.depend();
      }
      return val;
    },
    
    set: function reactiveSetter(newVal) {
      if (newVal === val) return;
      val = newVal;
      // 如果新值是对象,继续观察它
      childOb = observe(newVal);
      dep.notify();
    }
  });
}

核心点2: 依赖收集

vue是什么的响应式的原理??

vue是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter和getter,在数据发生改变的时候,发布消息给订阅者,出发响应的监听回调

其中谁是发布者?谁是订阅者?

我觉得数据是发布者,使用数据的地方就是订阅者。但是单独的数据和引用数据不足以实现数据改变,订阅数据的地方监听到数据改变。这就需要维护一个数据和订阅者之间的关系列表。这个关系就是依赖收集器Dep,收集的就是订阅者Watcher

数据劫持已经由 observer.js 中的 defineReactive 完成了,接下来就是依赖收集了,其中核心类:dep.js

javascript 复制代码
/**
 * 依赖收集器类
 */
class Dep {
  constructor() {
    // 存储所有依赖(订阅者watcher)
    this.subs = [];
  }
  
  addSub(sub) {
    this.subs.push(sub);
  }
  
  removeSub(sub) {
    remove(this.subs, sub);
  }
  
  // 收集依赖重点函数!!
  depend() {
    if (Dep.target) {
      Dep.target.addDep(this);
    }
  }
  
  notify() {
    const subs = this.subs.slice();
    for (let i = 0; i < subs.length; i++) {
      subs[i].update();
    }
  }
}

// 当前正在评估的watcher
// 这是一个全局唯一的变量,同一时间只能有一个watcher被评估!!JavaScript是单线程执行的!
Dep.target = null;
const targetStack = [];

// 设置当前watcher
function pushTarget(target) {
  targetStack.push(target);
  Dep.target = target;
}

核心点3: Watcher与数据更新

在上面的依赖收集中,每一个属性里面都定义了一个dep,记录着每一个使用到该属性的watcher实例。watcher的作用是什么?? watcher的作用就是在数据发生变化的时候,通知所有的watcher实例进行更新。watcher是一个中间层,连接着数据和视图

那么它就需要两个功能:

  1. 监听数据变化
  2. 更新视图

妙,太妙了,主逻辑如下:

watcher.js

javascript 复制代码
/**
 * Watcher类是连接Observer和View的中间层
 */
class Watcher {
  constructor(vm, expOrFn, cb, options) {
    this.vm = vm;
    
    // 存储表达式或函数
    if (typeof expOrFn === 'function') {
      // render函数或计算属性的getter或者watch监听函数
      this.getter = expOrFn;
    } else {
      // 表达式解析为函数
      this.getter = parsePath(expOrFn);
    }
    
    this.cb = cb;
    this.deps = [];
    this.depIds = new Set(); // 用于防止依赖重复收集
    
    this.isRenderWatcher = options && options.isRenderWatcher;
    
    // 立即执行getter,触发依赖收集,注意如果是render函数,这个value是undefined,没什么作用。重点是get()的依赖收集
    this.value = this.get();
  }
  
  // 获取值并收集依赖
  get() {
    // 设置当前活动的Watcher
    pushTarget(this);
    let value;
    
    try {
      // 执行getter函数,触发响应式数据的getter
      value = this.getter.call(this.vm, this.vm);
    } finally {
      // 清除当前Watcher
      popTarget();
    }
    
    return value;
  }
  
  // 添加依赖
  addDep(dep) {
    // dep是Dep实例在每一个响应式属性中都有
    const id = dep.id;
    if (!this.depIds.has(id)) {
      this.depIds.add(id);
      this.deps.push(dep);
      dep.addSub(this);
    }
  }
  
  // 更新方法,被Dep调用
  update() {
    // 简单实现:直接执行run
    // 实际Vue中会将watcher推入队列,进行异步更新
    queueWatcher(this);
  }
  
  // 实际执行更新的方法
  run() {
    const oldValue = this.value;
    this.value = this.get();
    
    // 调用回调,如组件的更新函数、计算属性的setter或watch的回调
    if (this.cb) {
      this.cb.call(this.vm, this.value, oldValue);
    }
  }
}

到这儿的时候有一个强烈的疑惑:

  • this.value = this.get() 这行代码的作用是什么?value会是是什么东西?watcher有几个??

在文末会有额外的疑惑解决


先看到下面,然后来回滚动着看源码,马上就能看完,非常简单,非常妙啊

已经看这么多了,已经很棒了,晚上必加鸡腿🍗

核心点4: 异步更新队列(nextTick)

照着逻辑到了,数据更新的核心内容,在watcher中是如何更新的?为什么我更新完数据之后,如果直接去访问数据,拿到的还是旧值??

这就是nextTick的作用了,nextTick的作用就是将回调函数放入一个队列中,等到下一个事件循环的时候再执行这个回调函数。这样就可以避免多次更新造成的性能浪费

watcher中同样会去调用nextTick将更新推入异步更新队列,很好的解释了下面vue中常见的逻辑👇🏻:

javascript 复制代码
// vue里面的一个逻辑
  data() {
    return {
      msg: ''
    }
  },
  methods: {
    handleClick() {
      this.msg = 'Hello World';
      this.$nextTick(() => {
        console.log(this.$el.innerHTML); // 'Hello World'
      });
    }
  }

实现的核心逻辑如下:

javascript 复制代码
/**
 * 将回调函数缓存起来,一起执行
 */
function queueWatcher(watcher) {
  // ...
  nextTick(flushSchedulerQueue);
}

/**
 * 实际批量处理队列的函数
 */
function flushSchedulerQueue() {
  // ...
  // 依次执行watcher的run方法
  for (let i = 0; i < queue.length; i++) {
    const watcher = queue[i];
    watcher.run();
  }
}

/**
 * nextTick实现原理
 * 将回调添加到任务队列,在下一个tick执行
 */
const callbacks = [];
let pending = false;
function nextTick(cb, ctx) {
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx);
      } catch (e) {
        console.error(e);
      }
    }
  });
  
  // 如果还没有处理队列,则启动处理
  if (!pending) {
    pending = true;
    // 在下一个tick执行回调队列
    // vue2 优先使用Promise,其次是MutationObserver,然后是setImmediate,最后是setTimeout,浏览器兼容问题
    Promise.resolve().then(flushCallbacks);
  }
}

/**
 * 执行所有回调
 */
function flushCallbacks() {
  // 重置状态,为下一次回调执行做准备
  pending = false;
  const copies = callbacks.slice(0);
  callbacks.length = 0;
  
  for (let i = 0; i < copies.length; i++) {
    copies[i]();
  }
}

完整响应式流程

javascript 复制代码
// 示例:Vue实例初始化并响应数据变化
function initData(vm) {
  let data = vm.$options.data;
  
  data = vm._data = data.call(vm)
  
  const keys = Object.keys(data);
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i];
    proxy(vm, '_data', key);
  }
  // 响应式化数据
  observe(data);
}

function proxy(target, sourceKey, key) {
  Object.defineProperty(target, key, {
    get() {
      return target[sourceKey][key]
    },
    set(val) {
      target[sourceKey][key] = val
    }
  })
}

// 组件渲染时 创建 渲染Watcher
function mountComponent(vm, el) {
  // 更新组件模版内容的函数
  const updateComponent = () => {
    vm._update(vm._render());
  };
  
  // 创建 渲染Watcher
  new Watcher(
    vm, 
    updateComponent, 
    noop,
    { isRenderWatcher: true } // 标识是渲染的watcher
  );
  
  return vm;
}

// 使用示例
function egFn() {
  const vm = new Vue({
    data() {
      return {
        message: 'Hello'
      }
    },
    render(h) {
      // 在这里访问this.message会触发getter,完成依赖收集
      return h('div', this.message);
    }
  });
  
  // 挂载组件,创建渲染watcher并执行首次渲染
  vm.$mount('#app');
  
  // 修改数据,触发更新
  vm.message = 'Updated'; 
  // 这会触发setter -> dep.notify() -> watcher.update() -> queueWatcher -> nextTick -> flushSchedulerQueue -> watcher.run() -> 重新渲染
}

整体流程总结

  1. 初始化阶段
  • Vue示例创建时,通过observe方法将data选项转换为响应式对象
  • 通过Observer类使用Object.defineProperty 劫持每个属性,定义getter和setter
  • 每个属性都创建一个Dep实例用于依赖收集
  1. 依赖收集阶段
  • 组件渲染时创建Watcher实例
  • 渲染过程中访问响应式属性时触发getter
  • Dep.target设为当前Watcher,并通过dep.depend()收集依赖
  • 每个属性的dep记录那些Watcher依赖它
  1. 数据更新阶段
    • 当响应式数据被修改,触发setter
    • setter调用 dep.notify()通知所有依赖该数据的watcher
    • watcher被添加到异步更新队列(queueWatcher)
    • 通过nextTick在下一个事件循环周期统一更新Dom

ok👌,看完啦,非常nice


越学当然问题越多,下面是额外内容:

额外疑惑解决

Watcher部分代码

javascript 复制代码
class Watcher {
  constructor(vm, expOrFn, cb, options) {
    this.vm = vm;
    
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn;
    } else {
      // 如果是字符串表达式,会将其转换为函数
      this.getter = parsePath(expOrFn);  // 返回一个函数,用于从对象中获取属性值
    }
    
    // 立即求值并收集依赖
    this.value = this.get();
  }
}

this.value = this.get() 这行代码的作用是什么?value会是是什么东西?watcher有几个??

Watcher实例中的getter和value

this.value = this.get() 的作用

这行代码主要有两个时机执行:

  1. Watcher初始化时(构造函数中)
  2. 数据更新时(run方法中)

它的作用是:

  • 执行get()方法,触发getter进行依赖收集
  • 将获取到的值保存在this.value中,便于后续比较新旧值的变化
  • 对于渲染watcher,这一步会触发组件的渲染过程
this.getter 是什么?作用是什么

this.getter的来源是创建Watcher时传入的expOrFn参数。根据不同类型的Watcher,它可能是:

  1. 渲染Watcher: 就是组件的render函数,执行的时候会触发模版中的响应式数据的读取
javascript 复制代码
// 渲染Watcher的初始化
const updateComponent = () => {
  vm._update(vm._render());  // 生成VNode并更新DOM
};

// 此时传给Watcher的expOrFn就是updateComponent
new Watcher(vm, updateComponent, noop, { isRenderWatcher: true });

对于渲染watcher,this.getter就是组件的render函数,它会在组件渲染时被调用,触发getter,收集依赖。返回的值是undefined,因为render函数本身不返回值

  1. 计算属性Watcher:
javascript 复制代码
// 计算属性的getter函数
const getter = () => {
  return this.count * 2;  // 例如一个简单的computed属性
};

// 创建计算属性Watcher
new Watcher(vm, getter, noop, { lazy: true });

计算属性watcher,this.getter就是计算属性的getter函数,它会在计算属性被访问时被调用,触发getter,收集依赖,返回的值是计算属性的值

  1. 监听器Watcher:
javascript 复制代码
 watch: {
    // 这会隐式创建一个Watcher
    'user.name': function(newVal, oldVal) {
      console.log('用户名变更为:', newVal);
    }
  }

// 创建Watcher
new Watcher(vm, parsePath('user.name'), callback);

监听器同理,区别在于watch会有新旧两个数据,也就是在run()函数中获取的新值和记录的旧值

结语

又理解了一遍原理,记录下来,方便后续查阅

希望对你有所帮助

祝在前端开发的道路上越走越远,技术精进,问题迎刃而解!💪✨

相关推荐
CUIYD_19892 分钟前
Vue 中组件命名与引用
javascript·vue.js·node.js
面朝大海,春不暖,花不开4 分钟前
Spring Boot MVC自动配置与Web应用开发详解
前端·spring boot·mvc
知否技术4 分钟前
2025微信小程序开发实战教程(一)
前端·微信小程序
玲小珑5 分钟前
Auto.js 入门指南(五)实战项目——自动脚本
android·前端
Sparkxuan5 分钟前
IntersectionObserver的用法
前端
玲小珑6 分钟前
Auto.js 入门指南(四)Auto.js 基础概念
android·前端
全栈技术负责人6 分钟前
Webpack性能优化:构建速度与体积优化策略
前端·webpack·node.js
爱吃肉的小鹿8 分钟前
浏览器渲染的核心流程及详细解析(2025.6月最新)
前端
贩卖纯净水.13 分钟前
webpack打包学习
前端·学习·webpack
敲键盘的小夜猫25 分钟前
RunnablePassthrough介绍和透传参数实战
java·服务器·前端