Vue响应式原理

Vue.js的响应式原理是其核心特性,通过数据驱动和双向绑定实现视图与数据的同步。当数据发生变化,利用Object.defineProperty进行数据劫持,创建响应式对象,更新视图。同时,Vue使用发布订阅模式和观察者模式,确保数据变化时能够通知并更新相关视图。文中还介绍了如何模拟实现Vue的响应式系统,包括数据转换、模板编译和视图渲染的过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

相关概念

  1. 数据驱动
    1. 数据响应式
      数据模型仅仅是普通js对象,当我们修改数据时,更图会自动更新,避免了繁琐的DOM操作,提高开发效率。
    2. 双向绑定
      数据改变,视图随之发生改变;视图改变,数据也会发生改变。使用v-model在表单上创建双向绑定。
    3. 数据驱动
      Vue最独特的特性之一,开发过程中只需要关注数据,不需要关心数据时如何渲染到视图。
  2. 响应式核心原理
    1. Vue2.x响应式核心原理
      当把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setterObject.definePropertyES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。
      Object.defineProperty(obj, prop, descriptor) 会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

      1. obj:要定义属性的对象
      2. prop:要定义或修改的属性的名称或 Symbol
      3. descriptor:要定义或修改的属性描述符。
        1. 属性描述符有两种主要形式:数据描述符和存取描述符。一个描述符只能是这两者其中之一;不能同时是两者。
        2. 两种描述符都是对象。它们共享以下可选键值:
          1. configurable:键值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为false
          2. enumerable:键值为 true 时,该属性才会出现在对象的枚举属性中。默认为false
        3. 数据描述符还具有以下可选键值:
          1. value:该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为undefined
          2. writable:键值为 true 时,属性的值,也就是上面的 value,才能被赋值运算符改变。默认为false
        4. 存取描述符还具有以下可选键值:
          1. get:属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。该函数的返回值会被用作属性的值。默认为 undefined
          2. set:属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值)。默认为 undefined
        var data = {
          msg: 'Hello World'
        };
        
        var vm = {}
        Object.defineProperty(vm, 'msg', {
          configurable: true, // 是否可配置
          enumerable: true, // 是否可枚举
          set(val) {
            console.log('set', val);
            if (val === data.msg) return;
            data.msg = val;
            document.querySelector('#app').textContent = data.msg;
          },
          get() {
            console.log('get', data.msg);
            return data.msg;
          }
        })
        
    2. Vue3.x响应式核心原理
      使用ES6中新增的Proxy,能直接监听对象,而不是属性,性能由浏览器优化,比Object.defineProperty更好,IE不支持。
      Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

      const p = new Proxy(target, handler)
      
      1. target
        要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
      2. handler
        一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。是一个容纳一批特定属性的占位符对象。它包含有 Proxy 的各个捕获器(trap)。所有的捕捉器是可选的。如果没有定义某个捕捉器,那么就会保留源对象的默认行为。
        1. handler.defineProperty()Object.defineProperty 方法的捕捉器。
        2. handler.has()in 操作符的捕捉器。
        3. handler.get():属性读取操作的捕捉器。
        4. handler.set():属性设置操作的捕捉器。
        5. handler.deleteProperty()delete 操作符的捕捉器。
        6. handler.ownKeys()Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器。
        7. handler.apply():函数调用操作的捕捉器。
        8. handler.construct()new 操作符的捕捉器。
        var data = {
         msg: 'Hello World'
        };
        
        const p = new Proxy(data, {
          get(target, key) {
            console.log('set', target, key);
            return target[key]
          },
          set(target, key, val) {
            console.log('set', target, key, val);
            if (target[key] === val) reutrn;
            target[key] = val;
            document.querySelector('#app').textContent = val
          }
        })
        
  3. 发布订阅模式和观察者模式
    1. 发布订阅模式
      假定存在一个信号中心,某个任务执行完成时,就像信号中心发布一个信号,其他任务可以向信号中心订阅这个信号,从而知道自己什么时候开始执行。
      Vue中可以简单的认为$.on注册的事件是订阅者,而$emit注册的事件是发布者
      模拟实现发布订阅模式:

      1. 定义一个类,类中包含一个$on$emit函数,一个存储$on定义的事件和回调函数的映射对象subs,一个事件可能存在多个回调函数。
      2. $on订阅事件,接收事件名以及一个事件处理函数,然后将事件名和函数存储到subs中。
        // 订阅
         $on(eventType, handler) { // 将注册的事件和回调函数放到subs对象中
           this.subs[eventType] = this.subs[eventType] || []; // 确保事件对应的值是一个数组。
           this.subs[eventType].push(handler);
         }
        
      3. $emit发布事件,接受一个事件名,然后挨个执行对象中对应的所有的处理函数。
        // 发布,执行事件函数
        $emit(eventType) {
           if (this.subs[eventType]) {
             this.subs[eventType].forEach(handler => {
               handler();
             });
           }
         }
        
    2. 观察者模式
      观察者模式和发布订阅模式类似,只是没有事件中心,只有发布者和订阅者,并且发布者需要知道订阅者的存在。

      1. 在观察者模式中,订阅者,又叫观察者(Watcher)。观察者自身具有update方法,负责当数据改变时更新视图。当事件发生时,会调用所有观察者的update方法。
        // 观察者
        class Watcher{
          update() {
            console.log('watcher');
          }
        }
        
      2. 在观察者模式中,发布者,又叫观察目标(Dep)。在观察目标内会记录所有观察者,当事件发生时,会调用所有观察者的update()方法。
        // 观察目标
        class Dep{
          constructor() {
            this.subs = [] // 保存观察者
          }
          addWatcher(sub) {
            if (sub.update) {
              this.subs.push(sub)
            }
          }
          notify() {
            this.subs.forEach(sub => {
              sub.update()
            })
          }
        }
        
      3. 观察者模式怎么工作的呢?首先需要调用观察目标的添加观察者的方法,将观察者记录到观察目标中,然后当事件发生时,观察目标会通知所有记录中的观察者。
        const watcher = new Watcher()
        const dep = new Dep()
        dep.addWatcher(watcher)
        dep.notify()
        

模拟实现Vue

  1. Vue类的实现:
    1. Vue类的功能:

      1. 负责接收初始化的选项参数
      2. 负责把data中的属性注入到Vue实例,转换成getter/setter
      3. 负责调用observer监听data中所有属性的变化
      4. 负责调用compiler解析指令/差值表达式
    2. Vue类的结构:
      在这里插入图片描述
      $options:是选项参数
      $el:实例的根DOM元素
      $datadata副本
      _proxyData():将data中的属性转换成getter/setter注入到Vue实例中。

    3. 编码实现

      1. 实现构造函数,保存变量到实例,data属性转换并注入Vue实例
        constructor(options) {
          // 保存选项参数
          this.$options = options || {} 
          // 保存data
          this.$data = options.data || {}
          // 保存 el,如果el是字符串,表示传入另一个选择器,否则人为传入了DOM元素
          this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
          // 将data中的属性转换为getter/setter,并注入到实例中
          this._proxyData(this.$data)
        }
        
      2. 实现data属性转换并注入Vue实例 的方法
        _proxyData(data) {
          Object.keys(data).forEach(key => {
            Object.defineProperty(this, key, {
              // 是否可配置
              configurable: true,
              // 是否可枚举
              enumerable: true,
              // 拦截赋值
              set(newVal) {
                if (newVal === data[key]) return
                data[key] = newVal
              },
              // 拦截获取值
              get() {
                return data[key]
              }
            })
          })
        }
        
  2. Observer
    1. 负责把vue.$data对象中的属性转换成响应式数据(getter/setter)
    2. vue.$data中的某个属性是对象时,也需要把该属性转换成响应式数据
    3. 数据变化发送通知
    4. 类结构
      在这里插入图片描述
      walk:遍历data
      defineReactive:将data中的属性转换成getter/setter
      1. value === data[key]。这里为什么要传递value呢,因为经过Object.defineProperty之后,每次obj[key]都会去调用get方法,如果get里面return obj[key],就会出现get的循环调用。
      2. data中的属性为一个对象时,该对象也要是响应式的。
      3. 如果给data中的属性重新赋值为一个对象,该对象也要是响应式的。
    5. 代码实现
      /**
       * 将vm.$data转换为响应式数据
       */
      class Observer {
        constructor(data) {
          this.walk(data)
        }
        /**
         * 遍历data
         * @param {} data 
         */
        walk(data) {
          // 判断data是否为空和是否为对象
          if(!data && typeof data !== 'object') return
          Object.keys(data).forEach(key => {
            this.defineReactive(data, key, data[key])
          })
        }
        /**
         * 将对象的属性转化为getter/setter
         * @param {对象} obj    
         * @param {要转化的属性} key 
         * @param {对应的旧值} val 
         */
        defineReactive(obj, key, val) { // val === obej[key] 这里为什么要传递val,因为经过Object.defineProperty之后,每次obj[key]都会去调用get方法,如果get里面return obj[key],就会出现get的循环调用。
          const _this = this
          // 如果val是一个对象,需要把val也转换成响应式的对象
          this.walk(val)
          Object.defineProperty(obj, key, {
            configurable: true,
            enumerable: true,
            set(newVal) {
              if (newVal === val) return
              val = newVal
              // 如果newVal是一个对象,需要把newVal也转换成一个响应式的对象
              _this.walk(newVal)
              // 发送通知
            },
            get() {
              return val
            }
          })
        }
      }
      
  3. compiler
    1. 功能

      1. 负责编译模板,解析指令/差值表达式
      2. 负责页面的首次渲染
      3. 当数据变化时重新渲染视图
    2. 结构
      在这里插入图片描述

    3. 代码实现

      1. 构造函数constructor:保存vmel,调用compile编译el
        constructor(vm) {
          this.vm = vm
          this.el = vm.$el
          this.compile(this.el)
          this.initDirectives()
        }
        // 初始化指令处理函数,为每个处理函数绑定当前作用域
        initDirectives() {
          Object.keys(directives).forEach(d => {
            directives[d] = directives[d].bind(this)
          });
        }
        
      2. compile:获取子节点,遍历子节点,判断子节点类型,并使用对应的编译函数编译子节点。
        compile(el) {
          // 获取根节点下所有子节点
          const childNodes = el.childNodes || []
          if (!childNodes.length) return
          // 遍历子元素,判断子节点类型,并根据类型解析子节点。childNodes是个类数组,需要通过Array.from转化为数组
          Array.from(childNodes).forEach(node => {
            if (this.isTextNode(node)) {// 处理文本节点
              this.compileText(node)
            } else if(this.isElementNode(node)) { // 处理元素节点
              this.compileElement(node)
            }
        
            // 调用一下compile编译node节点的子节点
            this.compile(node)
          })
        }
        
      3. compileElement:编译元素节点。编译指令,先获取所有属性,然后判断是否是指令,如果是指令,则调用响应的指令处理函数处理。这两把指令处理函数放到一个指令与处理函数对应的键值对象中,这样在判断属性是指令之后可以直接调用指令对应的处理函数,不过执行构造函数时需要将处理函数的作用域改为当前compiler对象。
        compileElement(node) {
          // 获取元素属性列表,
          const attrs = node.attributes
          console.log(attrs);
          Array.from(attrs).forEach(attr => {
            const attrName = attr.name // 属性名
            if (this.isDirective(attrName)) { // 判断是否是指令
              const attrValue = attr.value // 属性值
              directives[attrName](node, this.vm[attrValue])
            }
          })
        }
        
      4. compileText:编译文本节点,处理差值表达式。定义差值表达式正则,并通过正则捕获来获取到差值表达式内的属性名,然后将属性名对应的值替换差值表达式的位置放入到显示文本中。
        compileText(node) {
          const reg = /\{\{(.+?)\}\}/
          const text = node.textContent
          if (reg.test(text)) { // 如果是差值表达式,则通过RegExp.$1来获取匹配到的key
            const key = RegExp.$1.trim()
            // 将值替换掉差值表达式显示在视图上
            node.textContent = text.replace(reg, this.vm[key])
          }
        }
        
      5. isDirective:判断某个属性是否是指令。
        isDirective(attr) {
         return attr.startsWith('v-')
        }
        
      6. isTextNode:判断某个节点是否是文本节点,文本节点的nodeType3
        isTextNode(node) {
         return node.nodeType === 3
        }
        
      7. isElementNode:判断某个节点是否是元素节点,元素节点的nodeType1
        isElementNode(node) {
          return node.nodeType === 1
        }
        
  4. Dep类观察者模式中的观察目标
    1. 功能
      1. 记录所有的观察者
      2. 数据更新时通知所有观察者
    2. 使用
      1. 首先Vue中的观察目标是响应式对象的属性,所以在ObserverdefineReactive将对象的属性转换为getter/setter时,需要为此属性创建观察者对象dep
      2. 然后在defineReactive转化的getter方法中,将观察者添加到观察目标的观察者数组中。
      3. 在数据更新时,也就是setter方法中需要调用dep.notify方法来通知观察者。
    3. 代码实现,及使用代码
      /**
       * 观察者,
       * 1、数据更新时更新视图
       * 2、自身实例化时向Dep添加自己
       */
      class Watcher {
        constructor(vm, key, cb) {
          this.vm = vm
          this.key = key
          this.cb = cb
          // 向Dep添加自己
          Dep.target = this // 将Dep的target设置为自己
          // 需要触发添加,vm[key]时会触发observer中的get
          this.oldVal = vm[key]
          Dep.target = null // 防止重复添加
        }
        update() {
          const newValue = this.vm[this.key]
          if(this.oldVal === newValue) return
          this.cb(newValue)
        }
      }
      
      更新后的defineReactive
      defineReactive(obj, key, val) { // val === obej[key] 这里为什么要传递val,因为经过Object.defineProperty之后,每次obj[key]都会去调用get方法,如果get里面return obj[key],就会出现get的循环调用。
          const _this = this
          // 如果val是一个对象,需要把val也转换成响应式的对象
          this.walk(val)
          // 每个双向绑定数据都是观察目标
          const dep = new Dep()
          Object.defineProperty(obj, key, {
            configurable: true,
            enumerable: true,
            set(newVal) {
              if (newVal === val) return
              val = newVal
              // 如果newVal是一个对象,需要把newVal也转换成一个响应式的对象
              _this.walk(newVal)
              // 发送通知
              dep.notify()
            },
            get() {
              // 添加观察者
              Dep.target && dep.addSub(Dep.target)
              return val
            }
          })
        }
      
  5. Watcher类 观察者模式的观察者
    1. 功能

      1. 数据更新时更新视图
      2. 自身实例化时向Dep添加自己
    2. 使用

      1. 观察者的功能主要是当数据更新时更新视图, 所以Watcher应该在处理响应式视图时创建,及Compiler类中。
      2. Compiler类在处理文本节点时,将差值表达式替换为对应属值时,若数据更新,需要将属性的最新值替换到差值表达式的位置。
      3. Compiler类在处理元素节点时,数据的渲染在于指令的处理方法中,所以需要在指令的处理方法中添加数据更新时的处理,即创建一个Watcher对象。
    3. 代码实现

      /**
       * 观察者,
       * 1、数据更新时更新视图
       * 2、自身实例化时向Dep添加自己
       */
      class Watcher {
        constructor(vm, key, cb) {
          this.vm = vm
          this.key = key
          this.cb = cb
          // 向Dep添加自己
          Dep.target = this // 将Dep的target设置为自己
          // 需要触发添加,vm[key]时会触发observer中的get
          this.oldVal = vm[key]
          Dep.target = null // 防止重复添加
        }
        update() {
          const newValue = this.vm[this.key]
          if(this.oldVal === newValue) return
          this.cb(newValue)
        }
      }
      

      Compiler类中文本节点的处理

      // 编译文本节点
      compileText(node) {
        const reg = /\{\{(.+?)\}\}/
        const text = node.textContent
        if (reg.test(text)) { // 如果是差值表达式,则通过RegExp.$1来获取匹配到的key
          const key = RegExp.$1.trim()
          // 将值替换掉差值表达式显示在视图上
          node.textContent = text.replace(reg, this.vm[key])
          new Watcher(this.vm, key, newValue => {
            node.textContent = text.replace(reg, newValue)
          })
        }
      }
      

      指令处理函数中创建Watcher对象。

      const directives = {
        'v-text' : function(node, key, value) { // 更新node的nodeContent属性
          node.textContent = value
          console.log(this);
          new Watcher(this.vm, key, value => {
            node.textContent = value
          })
        },
        'v-model': function(node, key, value) { // 更新node的value属性
          node.value = value
          new Watcher(this.vm, key, value => {
            node.value = value
          })
        }
      }
      
  6. 数据的双向绑定
    数据的双向绑定包含两部分:
    1. 数据更新,视图随之更新。上面的代码已经实现这一部分。
    2. 视图更新,数据随之更新。视图更新主要通过v-model绑定的表单元素来实现,只要在v-model的处理函数中,为node节点添加一个input事件的监听即可,在事件的回调函数中,将node节点的值赋值给属性即可。
    'v-model': function(node, key, value) { // 更新node的value属性
      node.value = value
      new Watcher(this.vm, key, value => {
        node.value = value
      })
      node.addEventListener('input', () => {
        this.vm[key] = node.value
      })
    }
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值