欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Vue响应式原理与模拟

程序员文章站 2024-02-02 11:16:58
...

Vue

数据响应式原理:

数据劫持(拦截)、观察者模式
Vue响应式原理与模拟

Vue2: getter和setter

Vue3: Proxy

通信模式

发布订阅模式

  • 事件中心,提供发布事件(emit方法)和订阅事件(on)的接口,是发布订阅模式的核心,作为事件和事件处理函数的仓库。
  • 订阅者:使用事件中心的订阅接口来订阅某种类型的事件,待事件发布时,会调用事件处理函数
  • 发布者:使用事件中心的发布接口来发布某种类型的事件,触发事件处理函数的调用

观察者模式:Vue的响应式机制

  • 观察者(订阅者):watcher
    • 拥有一个update方法,当事件发生时调用update
  • 目标(发布者):dependency作为了观察者模式的核心,收集依赖,通知依赖
    • subs数组:存储所有观察者
    • addSub():添加观察者
    • notify():当事件发生时,通知所有观察者(即调用所有观察者的update方法)

实现Vue

任务

  • 负责接收初始化参数(选项对象)
  • 负责把data中的属性注入到当前的实例,转换为getter/setter,这样方便直接操作实例属性。
  • 负责调用boserver监听data中的所有属性的变化
  • 负责调用compiler解析模板中的指令和插值表达式

类图

Vue响应式原理与模拟
vue实例属性以$开头

  • $options:存储选项对象
  • $el:将选项对象的el属性转换为DOM节点并存储。通常options.el是一个CSS选择器或DOM节点,作为Vue实例的挂载点
  • $data:存储选项对象的data属性
  • _proxyData():私有成员,将data中的属性转换为getter/setter注入到Vue实例中
class Vue {
  constructor (options) {
    // 1. 通过属性保存options和options.data、options.el
    this.$options = options || {}
    this.$el = typeof options.el === 'string'? document.querySelector(options.el): options.el
    this.$data = options.data || {}
    // 2. 把options.data的成员转为getter/setter,注入到实例中
    this._proxyData(this.$data)
    // 3. 调用observer来监听数据变化
    // 4. 调用compiler对象来编译模板,解析指令和插值表达式
  }
  // 遍历options.data的所有属性注入到实例中,转换为getter/setter
  _proxyData (data) {
    Object.keys(data).forEach(key => {
      Object.defineProperty(this, key, {
        enumerable: true,
        configurable: true,
        get () {
          // vue中不存在值,值存储在data中
          return data[key]
        },
        set (newValue) {
          if (newValue === data[key]) {
            return
          }
          data[key] = newValue
        }
      })
    })
  }

}

Observer

  • 功能
    • 负责把data选项中的属性(深度遍历,即如果属性也是一个对象,则要递归地将该对象的属性也转为getter/setter)转换为getter/setter响应式数据
    • 监听数据,数据变化时发送通知给watcher,由watcher的update方法来更新视图
  • 类图
    Vue响应式原理与模拟
  • walk(data):遍历data对象,walk方法会在遍历的过程中,调用defineReactive方法。
  • defineReactive(data, key, value):将data对象的属性转为getter/setter
class Observer {
  constructor (data) {
    this.walk(data)
  }
  walk (data) {
    // 1. 判断data是否是空对象,或者是否是对象
    if (!data || typeof data !== 'object') {
      return
    }
    // 2. 遍历data的所有属性
    Object.keys(data).forEach(key => {
      this.defineReactive(data, key, data[key])
    })
  }
  // 把对象的属性转为getter/setter,value是属性值,
  // 因为如果不传入value,使用obj[key]来获取属性值,会导致getter函数的无限递归
  defineReactive (obj, key, value) {
    // 如果value是对象,其属性也会转为响应式数据
    this.walk(value)

    let self = this
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get () {
        // 这里的value不能替换为obj[key]
        return value
      },
      // 这里的value值形成了一个闭包
      set (newValue) {
        if (newValue === value) {
          return
        }
        value = newValue
        self.walk(newValue)
        // 发送通知
      }
    })
  }
}

Compiler

  • 功能
    • 负责编译模板,解析指令和插值表达式
    • 负责页面的首次渲染
    • 当数据变化后重新渲染视图
    • 这里没有使用虚拟DOM,而是直接操作DOM
  • 类图
    Vue响应式原理与模拟
  • el:保存Vue实例的$el属性,即挂载点DOM
  • vm:保存当前Vue实例
  • compile(el):编译el内部的模板
  • compilerElement(node):编译元素节点
  • compileText(node):编译文本节点
  • isDirective(attrName):判断属性是否是指令
  • isTextNode(node):判断节点是否是文本节点
  • isElementNode(node):判断节点是否是元素节点
class Compiler {
  constructor (vm) {
    this.vm = vm
    this.el = vm.$el
    this.compile(this.el)
  }
  // 编译模板,处理文本节点和元素节点
  compile (el) {
    let childNodes = el.childNodes
    Array.from(childNodes).forEach(node => {
      if (this.isTextNode(node)) {
        // 处理文本节点
        this.compileText(node)
      } else if (this.isElementNode(node)) {
        // 处理元素节点
        this.compileElement(node)
      }
      // 判断node是否有子节点,有子节点,则递归调用compile
      if (node.childNodes && node.childNodes.length) {
        this.compile(node)
      }
    })
  }
  // 编译元素节点,处理指令
  compileElement (node) {
    // console.log(node.attributes)
    // 遍历所有属性节点
    Array.from(node.attributes).forEach(attr => {
      let attrName = attr.nodeName
      // 判断是否是指令
      if (this.isDirective(attrName)) {
        // v-text --> text
        attrName = attrName.substr(2)
        let key = attr.nodeValue
        this.update(node, key, attrName)
      }
    })
  }
  update (node, key, attrName) {
    let updateFn = this[attrName + 'Updater']
    updateFn && updateFn.call(this, node, this.vm[key], key)
  }
  // 处理v-text指令,将节点的文本内容替换为v-text属性值
  textUpdater (node, value, key) {
    node.textContent = value
    new Watcher(this.vm, key, (newValue) => {
      node.textContent = newValue
    })
  }
  // 处理v-model指令,将表单元素的value值替换为v-model属性值
  modelUpdater (node, value, key) {
    node.value = value
    new Watcher(this.vm, key, (newValue) => {
      node.value = newValue
    })
  }
  // 编译文本节点,处理插值表达式
  compileText (node) {
    // 将node以对象的形式输出
    // console.dir(node)
    // 注意这里的正则表达式中.+?使用了非贪婪模式,意味着匹配{{ msg }}而不是{{ msg }} msg }}
    let reg = /\{\{(.+?)\}\}/
    let value = node.textContent
    if (reg.test(value)) {
      // 使用正则表达式构造函数的静态属性来获取捕获组
      let key = RegExp.$1.trim()
      // 将文本节点的插值表达式替换为对应的属性值
      node.textContent = value.replace(reg, this.vm[key])
    }
  }
  // 判断属性是否是指令
  isDirective (attrName) {
    return attrName.startsWith('v-')
  }
  // 判断是否是文本节点
  isTextNode (node) {
    return node.nodeType === 3
  }
  // 判断是否是元素节点
  isElementNode (node) {
    return node.nodeType === 1
  }
}

Dependency:收集依赖,通知依赖

  • 收集依赖:每一个响应式的数据属性都会创建一个Watcher,在getter中收集这些依赖于数据属性的watcher,实际就是将watcher添加到Dependency中。
    • 依赖的收集开始于模板编译,因为只有在模板编译时才会去访问数据属性值,创建watcher,才能触发getter,在getter中收集依赖。
  • 通知依赖:当数据属性发生变化时,在setter中调用notify方法通知并调用watcher的update方法
  • 功能
    • 收集依赖,添加观察者
    • 通知所有观察者
  • 类图
    Vue响应式原理与模拟
  • subs:依赖(watcher)数组
  • addSub(sub):添加依赖(watcher)
  • notify():通知依赖
class Dependency {
  constructor () {
    // 初始化所有观察者数组
    this.subs = []
  }
  // 添加观察者
  addSub (sub) {
    // 判断是否存在并且是观察者(拥有update方法)
    if (sub && sub.update) {
      this.subs.push(sub)
    }
  }
  // 通知观察者
  notify () {
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}

Watcher

Vue响应式原理与模拟

  • 功能
    • 数据变化时触发依赖,Dependency通知所有watcher更新视图
    • watcher自身实例化的时候,添加自己到Dependency实例的subs数组中
  • 类图
    Vue响应式原理与模拟
  • vm:Vue实例
  • key:属性名,可以通过vm[key]得到数据属性值,用来更新视图
  • cb:每个watcher对象所对应的回调函数,用来更新对应的视图,因为每个watcher所对应的视图不同,更新方式不同。
  • oldValue:视图之前的数据
  • update():更新视图

在做DOM操作时创建watcher对象,因为watcher对象就是用来更新视图的,watcher对象的cb回调函数接收新的数据属性值来更新DOM。

class Watcher {
  constructor (vm, key, cb) {
    this.vm = vm
    // key即data对象的属性名
    this.key = key
    // 回调函数用来更新视图
    this.cb = cb

    // 当前watcher对象记录到Dependency的静态属性target
    // 触发get方法,在get方法中调用addSub
    Dependency.target = this
    // 当访问该属性时,已经触发了get方法
    this.oldValue = this.vm[this.key]
    // 将target置空
    Dependency.target = null
  }
  // 当数据发生变化时,更新视图
  update () {
    // 在调用update时,数据已经得到更新,所以可以通过属性名拿到新属性值
    let newValue = this.vm[this.key]
    if (this.oldValue === newValue) {
      return
    }
    this.cb(newValue)
  }
}

双向绑定

对于表单元素而言,双向绑定使得

  • 视图更新触发数据更新
  • 数据更新触发视图更新
// compiler.js
// 处理v-model指令,将表单元素的value值替换为v-model属性值
  modelUpdater (node, value, key) {
    node.value = value
    new Watcher(this.vm, key, (newValue) => {
      node.value = newValue
    })
    // 双向绑定,就是给表单元素添加一个数据更新要触发的事件,如input,change等,然后将元素的value值更新到data的数据属性
    node.addEventListener('input', () => {
      this.vm[key] = node.value
    })
  }
相关标签: vue 响应式原理

上一篇: 【MVVM】WPF

下一篇: