模拟 Vue 响应式原理简单实现

289 阅读5分钟

目前的MVVM框架都解决了数据与视图直接维护的关系,在Vue中的一个特性就是数据驱动,Vue的学习过程中,经常会看到三个词:数据响应式、双向绑定、数据驱动

数据响应式

  • 数据(即数据模型)是普通的 JS 对象,当修改数据时,视图会进行更新,避免了繁琐的 DOM 操作,提高开发效率

双向绑定

  • 数据改变,视图改变;视图改变,数据也随之改变
  • 我们可以使用 v-model 在表单元素上创建双向数据绑定

数据驱动是 Vue 最独特的特性之一

  • 开发过程中仅需要关注数据本身,不需要关心数据是如何渲染到视图

Vue2.x版本的数据响应式原理

采用Object.defindProperty来进行数据劫持

当把一个普通的JS对象在data中传入给Vue实例时,会遍历这个对象所有的property,并使用Object.defindProperty设置它的getter/setter,在访问对象属性的getter和setter时就可以进行额外操作

// 多个属性时 就用Object.keys()得到所有自身的key来遍历
Object.defineProperty(obj, 'count', {
  enumerable: true,
  configurable: true,
  get() {
    // 这里可以做其他处理
    return obj[key]
  },
  set() {
    if (obj[key] === newVal) {
      return
    }
    obj[key] = newVal
    // 简单地在这里进行dom操作
    document.querySelector('#count-div').innerHTML = newVal
  }
})

Vue3.x版本 使用ES6的Proxy 代理对象

Proxy创建的代理对象,是可以处理对象的所有属性,不需要每个属性处理

为原对象创建一个代理对象,在getter、setter中进行额外处理

代理对象是不会影响到原来的对象,需要使用代理对象才有额外处理的效果

// obj是没有变化 要使用vm
let vm = new Proxy(obj, {
  get(target, key) {
    return Reflect.get(target, key)
  },
  set(target, key, newVal) {
    if (newVal === Reflect.get(target, key)) {
      return
    }
    Reflect.set(target, key, newVal)
    // 简单地在这里进行dom操作
    document.querySelector('#app').innerHTML = newVal
  }
})

发布订阅模式

  • 订阅者:向信号中心订阅一个信息号,当信号被触发后,执行自己的事件
  • 发布者:在某个时机向信息中心发布一个信号,发布信号后会订阅者才执行它自己的事件
  • 信号中心:存储订阅者的订阅,接收发布者的通知

信号中心隔绝了订阅者和发布者关系,双方不知道对方存在(订阅者不知道谁发布的,发布者不知道谁订阅了)

例如 Vue 中的eventBus

// vm是信号中心
// vm.$on 订阅者
vm.$on('dataChange' , () => {
  console.log('订阅者工作')
})
// vm.$emit 发布者
vm.$emit('dataChange')

观察者模式

观察者(订阅者) -- Watcher

  • update() 当事件发生时 要做的事情

目标(发布者) -- Dep

  • subs数组 存放所有的观察者
  • addSub() 添加观察者
  • notify() 当事件发生 调用所有观察者update

没有事件中心

发布者需要知道观察者存在

// 发布者-目标
class Dep {
  constructor () {
    // 记录所有的订阅者
    this.subs = []
  }
  // 添加订阅者
  addSub (sub) {
    if (sub && sub.update) {
      this.subs.push(sub)
    }
  }
  // 发布通知
  notify () {
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}
// 订阅者-观察者
class Watcher {
  update () {
    console.log('update')
  }
}

// 测试
let dep = new Dep()
let watcher = new Watcher()

dep.addSub(watcher)

dep.notify()

模拟Vue响应式原理

模拟Vue响应式原理需要实现这5个内容

  • Vue: 把 data 中的成员注入到 Vue 实例,并且把 data 中的成员转成 getter/setter
  • Observer: 能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知 Dep
  • Compiler: 解析每个元素中的指令/插值表达式,并替换成相应的数据
  • Dep: 添加观察者(watcher),当数据变化通知所有观察者
  • Watcher: 数据变化更新视图

Vue

  • constructor (options)构造器:  保存options、data、el

  • options.data的数据挂载到vue实例上

  • 调用proxyData() 把data的数据挂载到vue实例上的

  • 创建Observer对象,把data和data的子成员变成数据响应式

  • 创建Compiler对象编译template

    constructor (options) { // 保存接收的参数 // 保存data // 保存el this.options=optionsthis.options = options || {} this.data = options.data || {} // todo el 没有判空 this.el=typeofoptions.el===string?document.querySelector(options.el):options.el//options.data的数据挂载到vue实例上this.proxyData(this.el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el // 把options.data的数据挂载到vue实例上 this.proxyData(this.data) new Observer(this.$data) new Compiler(this) }

    // 遍历data的属性 挂载到this(Vue实例) proxyData (data) { Object.keys(data).forEach(key => { Object.defineProperty(this, key, { configurable: true, enumerable: true, get () { return data[key] }, set (newValue) { if (newValue === data[key]) { return } data[key] = newValue } }) }) }

Observer

  • constructor() 调用walk

  • walk 会先判断传入的参数是不是对象,不是就返回,是对象就遍历key调用defineReative

  • defineReative 这里进行响应式处理,用Object.defindProperty(),创建这个key的Dep对象,再调用walk(如果这个key的值是对象,就会在这里递归遍历全部子节点)

  • getter里面进行Dep.target判断,加入到subs,返回value

  • setter里面调用walk(传入newVal,如果传入是对象就再进行递归遍历),赋值,dep.notify发布更新

    constructor (data) { this.walk(data) } walk (data) { if (!data || typeof data !== 'object') { return } // 这里是将 this.data的数据进行响应式处理 Object.keys(data).forEach(key => { this.defineReative(data, key, data[key]) }) } // 这里的data 就是 this.data // 这个value是作为get的返回值 避免死递归 defineReative (data, key, value) { let that = this let dep = new Dep() this.walk(value) Object.defineProperty(data, key, { configurable: true, enumerable: true, // 这个get方法是在访问 data.变量时就会触发 // 如果返回 data[key] 就会再次触发 data.key的get函数 // 导致死递归 // 所以用了一个value作为闭包 返回值 get () { // Dep.target只在创建Watcher时存放 // 存放后会立刻触发一次变量get到这里来 if (Dep.target) { dep.addSub(Dep.target) // 添加后要把这个target 置为null // 避免重复添加 Dep.target = null } return value }, set (newValue) { if (value === newValue) { return } value = newValue // 调用walk判断 如果新值是对象就把它的成员变成响应式 that.walk(newValue) // 发布更新信息 让订阅者更新 dep.notify() } }) }

Compiler

  • constructor() 保存el、实例vm、调用compiler编译模板

  • compiler 获取el的子节点,遍历子节点,区分是文本还是元素节点,对应调用两种处理方法,再遍历时判断当前子节点,是否还有子节点,递归遍历

  • compilerText编译文本节点 判断是不是插值表达式,是的话就在vm里面拿对应的值替到textContent,并且在这里添加一个Watcher,当这里插值表达式对应vm里面的值变化后,再次更新DOM

  • compilerElement 获取节点全部属性,遍历这些属性,找有没有是v-开头的指令,找到指令就用指令名拼接方式,调用对应的处理函数

  • v-text 指令处理类似插值表达式,替换文本,添加Watcher用于更新

  • v-model指令 找到key对应的value,替换value属性(用于表单元素),添加Watcher用于更新

  • v-html指令 拿到key对应的value,用innerHTML方式,插入HTML语法的字符串,添加Watcher用于更新

    const onRE = /^on:/ class Compiler { constructor (vm) { this.el = vm.el this.vm = vm this.compiler(this.el) } // 编译模板 处理文本 元素 compiler (el) { let childNodes = el.childNodes Array.from(childNodes).forEach(node => { if (this.isTextNode(node)) { // 文本节点 this.compilerText(node) } else if (this.isElementNode(node)) { this.compilerElement(node) } if (node.childNodes && node.childNodes.length > 0) { this.compiler(node) } }) } compilerElement (node) { Array.from(node.attributes).forEach(attr => { let attrName = attr.name if (this.isDirective(attrName)) { attrName = attrName.substr(2) let key = attr.value this.update(node, key, attrName) } }) } update (node, key, attrName) { if (onRE.test(attrName)) { // v-on const evnet = attrName.replace(onRE, '') this.onUpdate(node, this.vm[key], evnet) return } let updateFun = this[attrName + 'Update'] updateFun && updateFun.call(this, node, this.vm[key], key) } // 先考虑一个事件 onUpdate (node, value, key) { // value 里面放的是处理事件的函数 // key 里面放的是事件名 node.addEventListener(key, value, false) } htmlUpdate (node, value, key) { node.innerHTML = value new Watcher(this.vm, key, (newValue) => { node.innerHTML = newValue }) } textUpdate (node, value, key) { node.textContent = value new Watcher(this.vm, key, (newValue) => { node.textContent = newValue }) } modelUpdate (node, value, key) { node.value = value new Watcher(this.vm, key, (newValue) => { node.value = newValue }) node.addEventListener('input', () => { this.vm[key] = node.value }) } compilerText (node) { let reg = /\{\{(.+?)\}\}/ let value = node.textContent if (reg.test(value)) { let key = RegExp.1.trim() node.textContent = value.replace(reg, this.vm[key]) new Watcher(this.vm, key, (newValue) => { node.textContent = newValue }) } } isDirective (attrName) { return attrName.startsWith('v-') } isTextNode (node) { return node.nodeType === 3 } isElementNode (node) { return node.nodeType === 1 } }

Dep

  • constructor() 初始化一个 subs 用来存放Watcher

  • addSub 当触发对应属性(一个属性就new 一个Dep)getter时,如果有Dep.target有值(Watcher),把这个观察者放入subs

  • notify 当触发对应属性setter时,遍历subs里面观察者的update()

    class Dep { constructor () { this.subs = [] } addSub (sub) { if (sub && sub.update) { this.subs.push(sub) } } notify () { this.subs.forEach(sub => { sub.update() }) } }

Watcher

  • constructor () 存放实例的vm、属性的key、真正更新DOM的cb,给Dep.target赋值自己本身this,给oldValue赋值vm[key](这里触发了一次key的getter,会在那里添加到dep.subs,实现了订阅效果) 

  • update 判断一下newVal是不是变化了,变化就调用cb来更新DOM

    class Watcher { constructor (vm, key, cb) { this.vm = vm this.key = key this.cb = cb Dep.target = this this.oldValue = vm[key] } update () { let newValue = this.vm[this.key] if (this.oldValue === newValue) { return } this.cb(newValue) } }