Vue响应式原理详解:从零实现一个迷你Vue

前言

在现代前端开发中,Vue.js因其简洁的API和强大的响应式系统而广受欢迎。但你是否曾好奇,Vue是如何实现数据变化时自动更新视图的?今天,我们将深入Vue响应式系统的核心,通过实现一个迷你版的Vue来理解其背后的原理。

什么是响应式系统?

简单来说,响应式系统就是当数据发生变化时,能够自动更新依赖该数据的视图部分。就像Excel表格中的公式,当引用的单元格值改变时,公式的结果会自动重新计算。

核心架构

我们的迷你Vue包含四个核心类:

  • Vue:入口类,负责初始化
  • Observer:数据观察者,实现数据劫持
  • Dep:依赖管理器,收集和管理依赖关系
  • Watcher:观察者,连接数据和视图
  • Compile:编译器,解析模板和指令

让我们逐一深入分析每个部分。

1. Vue类:应用程序的入口

javascript 复制代码
class Vue {
  constructor(options) {
    this.$options = options || {};
    this.$data = options.data || {};
    const el = options.el;
    this.$el = typeof el === "string" ? document.querySelector(el) : el;

    // 检查挂载元素是否存在
    if (!this.$el) {
      console.error('挂载元素不存在:', options.el);
      return;
    }

    // 将属性注入Vue实例
    this._proxyData(this.$data);
    // 创建Observer进行data属性变化的观察
    new Observer(this.$data);
    // 视图解析
    new Compile(this);
  }

  _proxyData(data) {
    Object.keys(data).forEach((key) => {
      Object.defineProperty(this, key, {
        enumerable: true,
        configurable: true,
        get() {
          return data[key];
        },
        set(newValue) {
          data[key] = newValue;
        },
      });
    });
  }
}

关键点解析:

  • 数据代理_proxyData方法将$data中的属性代理到Vue实例上,这样我们可以直接使用vm.message而不是vm.$data.message
  • 初始化流程:Vue实例化时依次完成数据代理、创建观察者、编译模板这三个关键步骤

2. Observer类:数据劫持的核心

javascript 复制代码
class Observer {
  constructor(data) {
    this.data = data
    this.walk(data)
  }

  walk(data) {
    Object.keys(data).forEach(key => {
      this.defineReactive(data, key, data[key])
    })
  }

  defineReactive(data, key, value) {
    const dep = new Dep()
    
    // 递归观察嵌套对象
    if (typeof value === 'object' && value !== null) {
      new Observer(value)
    }

    Object.defineProperty(data, key, {
      enumerable: true,
      configurable: true,
      get() {
        // 收集依赖
        if (Dep.target) {
          dep.addSub(Dep.target)
        }
        return value
      },
      set(newValue) {
        if (value === newValue) return
        value = newValue
        
        // 新值是对象时继续观察
        if (typeof value === 'object' && value !== null) {
          new Observer(value)
        }
        
        // 通知更新
        dep.notify()
      }
    })
  }
}

关键点解析:

  • 递归观察:通过递归调用,Vue能够深度观察嵌套对象的所有属性
  • 数据劫持 :使用Object.defineProperty拦截对数据的读取和设置操作
  • 依赖收集:在getter中收集依赖该数据的Watcher
  • 变更通知:在setter中通知所有依赖的Watcher进行更新

3. Dep类:依赖管理的枢纽

javascript 复制代码
class Dep {
  constructor() {
    this.subs = [] //存储订阅者
  }

  addSub(sub) {
    this.subs.push(sub)
  }

  notify() {
    this.subs.forEach(sub => sub.update())
  }
}

关键点解析:

  • 订阅者列表subs数组存储所有依赖该数据的Watcher
  • 添加订阅addSub方法用于添加新的Watcher
  • 通知更新notify方法触发所有Watcher的更新

4. Watcher类:数据和视图的桥梁

javascript 复制代码
class Watcher {
  constructor(vm, key, callback) {
    this.vm = vm
    this.key = key
    this.callback = callback
    Dep.target = this

    this.oldValue = vm[key]
    Dep.target = null
  }
  
  update() {
    const newValue = this.vm[this.key]
    if (newValue === this.oldValue) return

    this.callback(newValue)
    this.oldValue = newValue
  }
}

关键点解析:

  • 依赖收集触发:在实例化时通过读取数据值触发getter,从而将自身添加到Dep中
  • 更新优化:通过比较新旧值避免不必要的更新
  • 回调执行:数据变化时执行回调函数更新视图

5. Compile类:模板编译的引擎

javascript 复制代码
class Compile {
  constructor(vm) {
    this.vm = vm;
    this.el = vm.$el;
    this.compile(this.el);
  }

  compile(el) {
    // 遍历所有子节点
    const childNodes = el.childNodes;
    Array.from(childNodes).forEach((node) => {
      if (this.isTextNode(node)) {
        this.compileText(node);
      } else if (this.isElementNode(node)) {
        this.compileElement(node);
      }

      // 递归编译子节点
      if (node.childNodes && node.childNodes.length) {
        this.compile(node);
      }
    });
  }

  compileText(node) {
    const reg = /\{\{(.+?)\}\}/g
    const value = node.textContent

    // 处理插值表达式
    if (!reg.test(value)) return
    reg.lastIndex = 0

    const tokens = []
    let result, index, lastIndex = 0
    
    while ((result = reg.exec(value)) !== null) {
      index = result.index

      // 添加普通文本
      if (index > lastIndex) {
        tokens.push(value.slice(lastIndex, index))
      }

      const key = result[1].trim()
      const initialValue = this.getDataValue(this.vm, key)
      tokens.push(initialValue !== undefined ? initialValue : '')
      
      lastIndex = index + result[0].length
      const pos = tokens.length - 1

      // 为每个插值创建Watcher
      new Watcher(this.vm, key, newValue => {
        tokens[pos] = newValue !== undefined ? newValue : ''
        node.textContent = tokens.join('')
      })
    }

    // 更新节点内容
    if (lastIndex < value.length) {
      tokens.push(value.slice(lastIndex))
    }
    node.textContent = tokens.join('')
  }

  getDataValue(obj, path) {
    const keys = path.split('.')
    let result = obj
    for (let key of keys) {
      if (result === null || result === undefined) break
      result = result[key]
    }
    return result
  }
}

关键点解析:

  • 节点类型判断:区分文本节点和元素节点,分别处理
  • 插值表达式解析 :使用正则表达式/\{\{(.+?)\}\}/g匹配{``{ }}语法
  • 令牌化处理:将文本和插值分开处理,避免整体替换
  • 路径解析 :支持obj.prop这样的嵌套属性访问
  • Watcher创建:为每个插值表达式创建对应的Watcher

响应式系统的工作流程

让我们通过一个具体的例子来理解整个系统是如何协同工作的:

html 复制代码
<div id="app">
  <p>{{ message }}</p>
  <p>{{ user.name }}</p>
</div>

<script>
const app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!',
    user: {
      name: 'John'
    }
  }
})
</script>

初始化阶段:

  1. Vue实例化,代理data属性
  2. Observer遍历data所有属性,用defineProperty进行劫持
  3. Compile解析模板,遇到{``{ message }}{``{ user.name }}
  4. 为每个插值创建Watcher,触发对应属性的getter
  5. getter中将Watcher添加到Dep的订阅列表中

更新阶段:

  1. 当执行app.message = 'Hello World'
  2. 触发message的setter
  3. setter调用dep.notify()
  4. notify遍历所有订阅的Watcher,调用它们的update方法
  5. Watcher更新回调函数执行,更新DOM内容

设计亮点与注意事项

1. 依赖收集的巧妙设计

通过Dep.target静态属性临时存储当前正在计算的Watcher,在getter中将其添加到依赖列表。

2. 性能优化

  • 通过比较新旧值避免不必要的更新
  • 精确的依赖收集,只有真正用到的数据变化才会触发更新

3. 递归观察

支持嵌套对象的深度观察,这是实现复杂数据结构的响应式基础。

4. 局限性

  • 使用Object.defineProperty无法检测到对象属性的添加和删除
  • 数组的变化需要通过重写数组方法来实现(本文未实现)

总结

通过这个迷你Vue的实现,我们深入理解了Vue响应式系统的核心机制:

  1. 数据劫持:通过Object.defineProperty拦截数据的访问和修改
  2. 依赖收集:在getter中收集依赖,建立数据与Watcher的关联
  3. 派发更新:在setter中通知所有依赖的Watcher进行更新
  4. 编译优化:通过精确的依赖关系和差异比较,实现高效的视图更新

这只是一个简化版的实现,真实的Vue.js包含了更多优化和功能,但核心原理是相通的。理解这些基础概念将帮助你更好地使用Vue,并在遇到问题时能够快速定位和解决。

希望这篇博客能帮助你理解Vue响应式原理的核心思想!

相关推荐
梦6502 小时前
React 简介
前端·react.js·前端框架
一只小阿乐2 小时前
react 中的判断显示
前端·javascript·vue.js·react.js·react
光影少年2 小时前
useMemo 和 React.memo区别
前端·react.js·前端框架
小沐°2 小时前
React-页码组件
前端·javascript·react.js
消失的旧时光-19432 小时前
Flutter 与 React/Vue 为什么思想一致?——声明式 UI 体系的深度对比(超清晰版)
vue.js·flutter·react.js
零一科技2 小时前
Vue3学习第三课: ref 与 reactive 选择指南
前端·vue.js
余杭子曰3 小时前
播放状态与播放序列的关系(999篇一线博客第107篇)
前端
e***U8204 小时前
前端路由懒加载实现,React.lazy与Suspense
前端·react.js·前端框架