基于Object.defineProperty实现简易响应式

228 阅读2分钟

响应式 基于Object.defineProperty

实现数据劫持


<html>
    <header>
        <title>数据劫持</title>
    </header>
    <body>
        <div id="app">a</div>
        <script src="./mvvm.js"></script>
        <script>
            let mvvm = new Mvvm({
                el: '#app',
                data: {
                   a: 1,
                   b: {
                       c: 2
                   }
                }
            })
        </script>
    </body>
</html>
class Mvvm {
  constructor(options) {
    this.$options = options
    let data = this._data = this.$options.data
    observe(data)
    // 通过对this进行监听绑定对应的数据,完成数据代理,可以通过mvvm.b.c读取
    for(let key in data) {
      Object.defineProperty(this, key, {
        configurable: true,
        get () {
          return this._data[key]
        },
        set (newVal) {
          this._data[key] = newVal
        }
      })
    }
  }
  // 编译替换
  new Compile(this.$options.el, this)
}
function observe (data) {
  if (!data || typeof data !== 'object') return
  return new Observe(data)
}
function Observe (data) {
  for(let key in data) {
    let val = data[key]
    observe(val)
    Object.defineProperty(data, key, {
      configurable: true,
      get () {
        return val
      },
      set (newVal) {
        val = newVal
        observe(newVal)
      }
    })
  }
}

可以看到对应的data上的数据都已经监听到了

image.png

另外改写对应的属性数据,也可以看到对应的会递归完成绑定,将原有的b对应的对象,set为新对象,会递归的将新对象完成数据监听。

image.png

proxy实现监听

function reactive(obj) {
    if (typeof obj !== 'object' && obj != null){ 
        return obj 
    } // Proxy相当于在对象外层加拦截 
    const observed = new Proxy(obj, {
        get(target, key, receiver) {
            const res = Reflect.get(target, key, receiver)
            console.log(`获取${key}:${res}`)
            return isObject(res) ? reactive(res) : res }, return observed
        },
        set(target, key, value, receiver) {
            const res = Reflect.set(target, key, value, receiver)
            console.log(`设置${key}:${value}`) return res
        }
    }
    return observed

数据编译

MVVM.js 构造器增加
new Compile(this.$options.el, this)

class Compile {
  constructor (el, vm) {
    // 将el挂载到dom下
    this.$vm = vm
    vm.$el = document.querySelector(el)
    // 利用文档碎片存储
    let fragment = document.createDocumentFragment()
    let child = null
    // 利用appendChild存在的元素会被移除的特性进行循环
    while (child = vm.$el.firstChild) {
      fragment.appendChild(child)    // 此时将el中的内容放入内存中
    }
    this.replace(fragment)
    // 利用文档碎片放回到el中
    vm.$el.appendChild(fragment)
  }

  replace (fragment) {
    Array.from(fragment.childNodes).forEach(node => {
      let txt = node.textContent
      let reg = /\{\{(.*?)\}\}/g   // 正则匹配{{}}
      if (node.nodeType === 3 && reg.test(txt)) {
        console.log(RegExp.$1)
        let arr = RegExp.$1.split('.')
        let val = this.$vm
        arr.forEach(key => {
          val = val[key]
        })
        node.textContent = txt.replace(reg, val).trim()
      }
      // 如果还有子节点,继续递归replace
      if (node.childNodes && node.childNodes.length) {
        this.replace(node);
      }
    })
  }
}

数据更新视图

通过发布订阅来进行数据的监听并更新视图

1、改造replace替换时,增加创建监听

class Mvvm {
  constructor(options) {
    this.$options = options
    let data = this._data = this.$options.data
    observe(data)
    for(let key in data) {
      Object.defineProperty(this, key, {
        configurable: true,
        get () {
          return this._data[key]
        },
        set (newVal) {
          this._data[key] = newVal
        }
      })
    }
    new Compile(this.$options.el, this)
  }
}
function observe (data) {
  if (!data || typeof data !== 'object') return
  return new Observe(data)
}
function Observe (data) {
  let dep = new Dep()
  for(let key in data) {
    let val = data[key]
    observe(val)
    Object.defineProperty(data, key, {
      configurable: true,
      get () {
        // 将watcher添加到订阅事件中 [watcher]
  +     Dep.target && dep.addSub(Dep.target) // 新增
        return val
      },
      set (newVal) {
        if (val === newVal) {
          return
        }
        val = newVal
        observe(newVal)
  +     dep.notify();   // 让所有watcher的update方法执行即可
      }
    })
  }
}

class Compile {
  constructor (el, vm) {
    // 将el挂载到dom下
    this.$vm = vm
    vm.$el = document.querySelector(el)
    // 利用文档碎片存储
    let fragment = document.createDocumentFragment()
    let child = null
    // 利用appendChild存在的元素会被移除的特性进行循环
    while (child = vm.$el.firstChild) {
      fragment.appendChild(child)    // 此时将el中的内容放入内存中
    }
    this.replace(fragment)
    // 利用文档碎片放回到el中
    vm.$el.appendChild(fragment)
  }

  replace (fragment) {
    Array.from(fragment.childNodes).forEach(node => {
      let txt = node.textContent
      let reg = /\{\{(.*?)\}\}/g   // 正则匹配{{}}
      if (node.nodeType === 3 && reg.test(txt)) {
        console.log(RegExp.$1)
        // let arr = RegExp.$1.split('.')
        // let val = this.$vm
        // arr.forEach(key => {
        //   val = val[key]
        // })
        // 监听变化
        // 给Watcher再添加两个参数,用来取新的值(newVal)给回调函数传参
  +     new Watcher(this.$vm, RegExp.$1, newVal => {
  +       node.textContent = txt.replace(reg, newVal).trim()
  +     })
  +   }
      // 如果还有子节点,继续递归replace
      if (node.childNodes && node.childNodes.length) {
        this.replace(node);
      }
    })
  }
}
class Dep {
  constructor() {
    this.subs = []
  }
  addSub(sub) {
    this.subs.push(sub)
  }
  notify() {
    this.subs.forEach(sub => sub.update())
  }
}

class Watcher {
  constructor(vm, exp, fn) {
    this.fn = fn
    this.vm = vm
    this.exp = exp
    Dep.target = this
    let arr = exp.split('.')
    let val = vm
    arr.forEach(key => {
      val = val[key] // 通过获取this.a.b的方式,进行监听的绑定
    })
    Dep.target = null
  }
  update() {
    // notify的时候值已经更改了
    // 再通过vm, exp来获取新的值
    let arr = this.exp.split('.')
    let val = this.vm
    arr.forEach(key => {
      val = val[key]   // 通过get获取到新的值
    })
    this.fn(val)   // 将每次拿到的新值去替换{{}}的内容即可
  }

}

双向数据绑定

通过监听dom上对应元素的输入或者change变化,来调用数据的set,来通知到update完成数据的响应视图

    // html结构
    <input v-model="c" type="text">
    
    // 数据部分
    data: {
        a: {
            b: 1
        },
        c: 2
    }
    
    function replace(frag) {
        // 省略...
+       if (node.nodeType === 1) {  // 元素节点
            let nodeAttr = node.attributes; // 获取dom上的所有属性,是个类数组
            Array.from(nodeAttr).forEach(attr => {
                let name = attr.name;   // v-model  type
                let exp = attr.value;   // c        text
                if (name.includes('v-')){
                    node.value = vm[exp];   // this.c 为 2
                }
                // 监听变化
                new Watcher(vm, exp, function(newVal) {
                    node.value = newVal;   // 当watcher触发时会自动将内容放进输入框中
                });
                
                node.addEventListener('input', e => {
                    let newVal = e.target.value;
                    // 相当于给this.c赋了一个新值
                    // 而值的改变会调用set,set中又会调用notify,notify中调用watcher的update方法实现了更新
                    vm[exp] = newVal;   
                });
            });
+       }
        if (node.childNodes && node.childNodes.length) {
            replace(node);
        }
    }

参考文章: juejin.cn/post/684490…
参考视频地址: www.bilibili.com/video/BV1u4…