Vue响应式原理与模拟

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方法来更新视图
  • 类图
    Observer类图
  • 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
  • 类图
    Compiler类图
  • 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方法
  • 功能
    • 收集依赖,添加观察者
    • 通知所有观察者
  • 类图
    Dependency类图
  • 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

Watcher类的工作原理

  • 功能
    • 数据变化时触发依赖,Dependency通知所有watcher更新视图
    • watcher自身实例化的时候,添加自己到Dependency实例的subs数组中
  • 类图
    Watcher类图
  • 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
    })
  }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值