Vue.js 响应式原理

Vue.js 响应式原理

准备工作

数据驱动

在学习 Vue.js 的过程中,我们经常看到三个概念:

  • 数据驱动
  • 数据响应式
  • 双向数据绑定
响应式的核心原理
核心原理分析
  • Vue 2.x 版本与 Vue 3.x 版本的响应式实现有所不同,我们将分别讲解。
    • Vue 2.x 响应式基于 ES5 的 Object.defineProperty 实现。
<script>
    var obj = {
      name: 'rae',
      age: '18'
    }
    // Object.defineProperty()操作演示与回顾
    Object.defineProperty(obj, 'gender', {
      // 设置值
      value: '女',
      // 是否可写
      writeable: true,
      // 是否可以遍历
      enumerable: true,
      // 重新配置
      configurable: true
    })

    Object.defineProperty(obj, 'gender', {
      enumerable: false
    })

    for (var k in obj) {
      console.log(k, obj[k])
    }

  </script>
    var genderValue = '女'
    Object.defineProperty(obj, 'gender', {
      get () {
        console.log('任意获取时需要的自定义操作')
        return genderValue
      },
      set (newValue) {
        console.log('任意设置时需要的自定义操作')
        genderValue = newValue
      }
    })
  • Vue 3.x 响应式基于 ES6 的 Proxy 实现
Vue 2 响应式原理
  • Vue 2.x 的数据响应式通过 Object.defineProperty() 实现的
    • 设置 data 后,遍历所有属性,转换为 Getter、Setter,从而在数据变化时进行视图更新等操作。
  • 下面我们来通过一段代码实现数据绑定的基础效果
  • 数据变化,自动更新到视图
<div id="app">原始内容</div>
  <script>
    // 声明数据对象,模拟vue实例的data属性
    let data = {
      msg: 'hello'
    }
    // 模拟vue实例的对象
    let vm = {}
    // 通过数据劫持的方式,给data的属性设置为getter与setter
    Object.defineProperty(vm, 'msg', {
      // 可遍历
      enumerable: true,
      // 可配置
      configurable: true,
      get () {
        console.log('访问了属性')
        return data.msg
      },
      set (newValue) {
        // 更新数据
        data.msg = newValue
        // 数据更改,更新视图中的dom元素的内容
        document.querySelector('#app').textContent = data.msg
      }
    })
  </script>
改进
  • 上述版本只是雏形,问题如下:
    • 操作中只监听了一个属性,多个属性无法处理
    • 无法监听数组变化(Vue 中同样存在)
    • 无法处理属性也为对象的情况
  • 下面我们来进行改进
<div id="app">原始内容</div>
  <script>
    // 声明数据对象,模拟vue实例的data属性
    let data = {
      msg1: 'hello',
      msg2: 'world',
      arr: [1, 2, 3],
      obj: {
        name: 'jack',
        age: 18
      }
    }
    // 模拟vue实例的对象
    let vm = {}
    // 封装为函数,用于对数据进行响应式处理
    const createReactive = (function () {
      // 添加数组方法支持
      const arrMethodName = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']

      // 用于存储处理结果的对象,准备替换掉数组实例的原型指针_proto_
      const customProto = {}

      // 为了避免数组实例无法再使用其他的数组方法
      customProto.__proto__ = Array.prototype

      arrMethodName.forEach(method => {
        customProto[method] = function () {

          // 确保原始功能可以使用(this为数组实例对象)
          const result = Array.prototype[method].apply(this, arguments)
          // 进行其他自定义功能设置,更新视图
          document.querySelector('#app').textContent = this
          return result
        }
      })
      // 需要进行数据劫持的主题功能,也是递归时需要的功能
      return function (data, vm) {
        // 遍历被劫持对象的所有属性
        Object.keys(data).forEach(key => {
          // 检测是否为数组
          if (Array.isArray(data[key])) {
            // 将当前数组实例的_proto_更换为customproto即可
            data[key].__proto__ = customProto
          } else if (typeof data[key] === 'object' && data[key] !== null) {
            // 检测是否为对象,如果是对象,需要进行递归处理
            vm[key] = {}
            createReactive(data[key], vm[key])
            return 
          }
          // 通过数据劫持的方式,给data的属性设置为getter与setter
          Object.defineProperty(vm, key, {
            // 可遍历
            enumerable: true,
            // 可配置
            configurable: true,
            get () {
              console.log('访问了属性')
              return data[key]
            },
            set (newValue) {
              // 更新数据
              data[key] = newValue
              // 数据更改,更新视图中的dom元素的内容
              document.querySelector('#app').textContent = data[key]
            }
          })
        })
      } 
    })()

    createReactive(data, vm)
Proxy 回顾
<script>
    const data = {
      msg1: '内容',
      arr: [1, 2, 3],
      obj: {
        name: 'rae',
        age: 18
      }
    }
    const p = new Proxy(data, {
      get (target, property, receiver) {
        console.log(target, property, receiver)
        return target[property]
      },
      set (target, property, value, receiver) {
        console.log(target, property, value, receiver)
        target[property] = value
      }
    })
  </script>
Vue 3 响应式原理

-Vue 3.x 与 Vue 2.x 的区别为数据响应式是通过 Proxy 实现的,其他相同,下面我们来进行原理示。

相关设计模式

设计模式(design pattern)是针对软件设计中普遍存在的各种问题所提出的解决方案。

观察者模式
  • 观察者模式(Observer pattern)指的是在对象间定义一个一对多(被观察者与多个观察者)的关联,当一个对象改变了状态,所有其他相关的对象会被通知并且自动刷新。
  • 核心概念:
  • 观察者 Observer
  • 被观察者(观察目标)Subject
<script>
    // 被观察者(观察目标)
    // 1.添加观察者
    // 2.通知所有观察者
    class Subject {
      constructor () {
        // 存储所有的观察者
        this.observers = []
      }
      // 添加观察者功能
      addObserver (observer) {
        // 检测传入的参数是否为观察者实例
        if (observer && observer.update) {
          this.observers.push(observer)
        }
      }
      // 当特殊事件发生时,通知观察者
      notify () {
        // 调用观察者列表中每个观察者的更新方法
        this.observers.forEach(observer => {
          observer.update()
        })
      }
    }

    // 观察者
    // 1 当观察目标发生状态变化时,进行更新
    class Observer {
      update () {
        console.log('事件发生了,进行相应的处理...')
      }
    }


    // 功能测试
    const subject = new Subject()
    const ob1 = new Observer()
    const ob2 = new Observer()

    // 将观察者添加给要观察的观察目标
    subject.addObserver(ob1)
    subject.addObserver(ob2)

    // 通知观察者进行操作(某些具体的场景项)
    subject.notify()
  </script>
发布-订阅模式
  • 发布-订阅模式(Publish-subscribepattern)可认为是为观察者模式解耦的进阶版本,特点如下:
  • 在发布者与订阅者之间添加消息中心,所有的消息均通过消息中心管理,
    而发布者与订阅者不会直接联系,实现了两者的解耦。
  • 核心概念:
  • 消息中心 Dep
  • 订阅者 Subscriber
  • 发布者 Publishe*
<script>
    // 创建vue实例(消息中心)
    const eventBus = new Vue()

    // 注册事件(设置订阅者)
    eventBus.$on('dataChange', () => {
      console.log('事件处理功能1')
    })

    eventBus.$on('dataChange', () => {
      console.log('事件处理功能2')
    })

    // 触发事件(设置发布者)
    eventBus.$emit('dataChange')
  </script>
设计模式小结
  • 观察者模式是由观察者与观察目标组成的,适合组件内操作。
    • 特性:特殊事件发生后,观察目标统一通知所有观察者。
  • 发布/订阅模式是由发布者与订阅者以及消息中心组成,更加适合消息类型复杂的情况。
    • 特性:特殊事件发生,消息中心接到发布指令后,会根据事件类型给对应的订阅者发送信息。

Vue 响应式原理模拟

整体分析
<div id="app">
    <p>{{ msg1 }}</p>
    <p v-text="msg2"></p>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
  <script>
    const vm = new Vue({
      el: '#app',
      data: {
        msg1: '内容1',
        msg2: '内容2'
      }
    })
  </script>
  • 要模拟 Vue 实现响应式数据,首先我们观察一下 Vue 实例的结构,分析要实现哪些属性与功能。
  • Vue
    • 目标:将 data 数据注入到 Vue 实例,便于方法内操作。
  • Observer(发布者)
    • 目标:数据劫持,监听数据变化,并在变化时通知 Dep
  • Dep(消息中心)
    • 目标:存储订阅者以及管理消息的发送
  • Watcher(订阅者)
    • 目标:订阅数据变化,进行视图更新
  • Compiler
    • 目标:解析模板中的指令与插值表达式,并替换成相应的数据
Vue 类
  • 功能:
    • 接收配置信息
    • 将 data 的属性转换成 Getter、Setter,并注入到 Vue 实例中。
    • *监听 data 中所有属性的变化,设置成响应式数据
    • *调用解析功能(解析模板内的插值表达式、指令等)
class Vue {
  constructor (options) {
    // 1 存储属性
    this.$options = options || {}
    this.$data = options.data || {}
    // 判断el值的类型,并进行相应处理
    const { el } = options
    this.$el = typeof el === 'string' ? document.querySelector(el) : el

    // 2 将data属性注入到Vue实例中(该功能为内部使用)
    _proxyData(this, this.$data)

    // * 3 创建Observer 实例监视data的属性变化
    new Observer(this.$data)

    // * 4 调用Compiler
    new Compiler(this)
  }
}

// 将data的属性注入到Vue实例
function _proxyData (target, data) {
  Object.keys(data).forEach(key => {
    Object.defineProperty(target, key, {
      enumerable: true,
      configurable: true,
      get () {
        return data[key]
      },
      set (newValue) {
        data[key] = newValue
      }
    })
  })
}
Observer 类
  • 功能:
    • 通过数据劫持方式监视 data 中的属性变化,变化时通知消息中心 Dep。
    • 需要考虑 data 的属性也可能为对象,也要转换成响应式数
class Observer {
  // 接收传入的对象,将这个对象的属性转换为getter或setter
  constructor (data) {
    this.data = data
    // 遍历数据
    this.walk(data)
  }
  // 封装用于数据遍历的方法
  walk (data) {
    // 将遍历后的数据转换为getter、setter
    Object.keys(data).forEach(key => this.convert(key, data[key]))
  }
  // 封装用于将对象转换为响应式数据的方法
  convert (key, value) {
    defineReactive(this.data, key, value)
  }
}

// 用于为对象定义一个响应式的属性
function defineReactive (data, key, value) {
  // 创建消息中心
  const dep = new Dep()

  //检测是否为对象,如果是创建一个新的Oberver实例进行管理
  observer(value)
  // 进行数据劫持
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get () {
      console.log('获取了属性')
      //  *在触发getter时,添加订阅者
      Dep.target && dep.addSub(Dep.target)
      return value
    },
    set (newValue) {
      console.log('设置了属性')
      if (newValue === value) return
      value = newValue
      observer(value)

      //  *数据变化是,通知消息中心
      dep.notify()
    }
  })
}

function observer (value) {
  if (typeof value === 'object' && value !== null) {
    return new Observer(value)
  } 
}

Dep 类
  • Dep 是 Dependency 的简写,含义为“依赖”,指的是 Dep 用于收集与管理订阅者与发布者之间的依赖关系。
  • 功能:
    • *为每个数据收集对应的依赖,存储依赖。
    • 添加并存储订阅者。
    • 数据变化时,通知所有观察者
class Dep {
  constructor () {
    // 存储订阅者
    this.subs = []
  }
  // 添加订阅者
  addSub (sub) {
    if (sub && sub.update) {
      this.subs.push(sub)
    }
  }
  // 通知订阅者的方法
  notify () {
    // 遍历订阅者并执行更新功能即可
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}
Watcher 类
  • 功能:
    • 实例化 Watch 时,往dep对象中添加自己
    • 当数据变化触发 dep, dep 通知所有对应的 Watcher 实例更新视图
class Watcher {
  constructor (vm, key, cb) {
    // 当前value实例
    this.vm = vm
    // 订阅的属性名
    this.key = key
    // 数据变化后,要执行的回调
    this.cb = cb

    // 触发getter前,将当前订阅者实例存储给Dep类
    Dep.target = this
    // 记录属性更改之前的值,用于进行更新状态检测(导致了属性Getter的触发)
    this.oldValue = vm[key]
    // 操作完毕后,清除target,用来存储下一个watcher实例
    Dep.target = null
  }
  // 封装数据变化时更新视图的功能
  update () {
    const newValue = this.vm[this.key]
    // 如果数据不变无需更新
    if (newValue === this.oldValue) return
    // 数据改变,调用更新后的回调
    this.cb(newValue)
  }
}
Compiler 类
  • 功能:
    • 进行编译模板,并解析内部指令与插值表达式。
    • 进行页面的首次渲染
    • 数据变化后,重新渲染视图
class Compiler {
  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 (isTextNode(node)) {
        // 编译文本节点内容
        this.compileText(node)
      } else if (isElementNode(node)) {
        // 编译元素节点内容
        this.compileElement(node)
      }
      // 检测当前节点是否存在子节点
      if (node.childNodes && node.childNodes.length) {
        this.compile(node)
      } 
    })
  }
  // 封装文本节点的编译方法
  compileText (node) {
    const reg = /\{\{(.+?)\}\}/g
    // 去除内容中不必要的空格与换行
    const value = node.textContent.replace(/\s/g, '')
    // 声明数据存储多段文本
    const tokens = []
    // 记录已经操作过的位置的索引
    let lastIndex = 0
    // 记录当前正在提取内容的初始索引
    let index

    let result
    while (result = reg.exec(value)) {
      // 本次提取内容的初始索引
      index = result.index
      // 判断
      if (index > lastIndex) {
        // 将中间部分内容存储到tokens中
        tokens.push(value.slice(lastIndex, index))
      }
      // 处理插值表达式内容(去除空格的操作可以省略)
      const key = result[1].trim()
      // 根据key过去对应属性值,存储到tokens
      tokens.push(this.vm[key])

      // 更新lastIndex
      lastIndex = index + result[0].length

      // 创建订阅者 Watcher实时订阅数据变化
      const pos = tokens.length - 1
      new Watcher(this.vm, key, newValue => {
        // 数据变化,修改token中的对应数据
        tokens[pos] = newValue
        node.textContent = tokens.join('')
      })
    }
    // 初始页面,初始渲染
    node.textContent = tokens.join('')
  }
  // 封装元素节点的处理方法
  compileElement (node) {
    // 获取属性节点
    Array.from(node.attributes).forEach(attr => {
      // 保存属性名称,并检测属性的功能
      let attrName = attr.name
      if (!isDirective(attrName)) return
      // 获取指令的具体名称
      attrName = attrName.slice(2)
      // 获取指令的值,代表响应式数据的名称
      let key = attr.value
      // 封装update方法,用于进行不同指令的功能分配
      this.update(node, key, attrName)
    })
  }
  // 用于指令分配的方法
  update (node, key, attrName) {
    //  名称处理
    let updateFn = this[attrName + 'Updater']
    // 检测并调用
    updateFn && updateFn.call(this, node, key, this.vm[key])
  }
  // v-text 处理
  textUpdater (node, key, value){
    // 给元素设置内容
    node.textContent = value
    // 订阅数据变化
    new Watcher(this.vm, key, newValue => {
      node.textContent = newValue
    })
  }
  // v-model处理
  modelUpdater (node, key, value) {
    // 给元素设置内容
    node.value = value
    // 订阅数据变化
    new Watcher(this.vm, key, newValue => {
      node.value = newValue
    })
    // 监听input事件,实现双向绑定
    node.addEventListener('input', () => {
      this.vm[key] = node.value
    })
  }
}

// 判断节点是否为元素节点
function isElementNode (node) {
  return node.nodeType === 1
}

// 判断节点是否为文本节点
function isTextNode (node) {
  return node.nodeType === 3
}

// 判断属性名是否为指令
function isDirective (attrName) {
  return attrName.startsWith('v-')
}
功能回顾与总结
  • Vue 类
    • 把 data 的属性注入到 Vue 实例
    • 调用 Observer 实现数据响应式处理
    • 调用 Compiler 编译模板
  • Observer
    • 将 data 的属性转换成 Getter/Setter
    • 为 Dep 添加订阅者 Watcher
    • 数据变化发送时通知 De
  • Dep
    • 收集依赖,添加订阅者(watcher)
    • 通知订阅者
  • Watcher
    • 编译模板时创建订阅者,订阅数据变化
    • 接到 Dep 通知时,调用 Compiler 中的模板功能更新视图
  • Compiler
    • 编译模板,解析指令与插值表达式
    • 负责页面首次渲染与数据变化后重新渲染
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值