模拟vue双向绑定

数据劫持

Vue 内部使用了 Obeject.defineProperty() 来实现双向绑定,通过这个函数可以监听到 set 和 get 的事件。

var data = { name: 'sonya' }
observe(data)
let name = data.name // -> get value
data.name = 'aha' // -> change value

function observe(obj) {
  // 判断类型
  if (!obj || typeof obj !== 'object') {
    return
  }
  Object.keys(data).forEach(key => {
    defineReactive(data, key, data[key])
  })
}

function defineReactive(obj, key, val) {
  // 递归子属性
  observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      console.log('get value')
      return val
    },
    set: function reactiveSetter(newVal) {
      console.log('change value')
      val = newVal
    }
  })
}

以上代码简单的实现了如何监听数据的 set 和 get 的事件,但是仅仅如此是不够的,还需要在适当的时候给属性添加发布订阅

<div>
    
</div>

在解析如上模板代码时,遇到 就会给属性 name 添加发布订阅。

// 通过 Dep 解耦
class Dep {
  constructor() {
    this.subs = []
  }
  addSub(sub) {
    // sub 是 Watcher 实例
    this.subs.push(sub)
  }
  notify() {
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}
// 全局属性,通过该属性配置 Watcher
Dep.target = null

function update(value) {
  document.querySelector('div').innerText = value
}

class Watcher {
  constructor(obj, key, cb) {
    // 将 Dep.target 指向自己
    // 然后触发属性的 getter 添加监听
    // 最后将 Dep.target 置空
    Dep.target = this
    this.cb = cb
    this.obj = obj
    this.key = key
    this.value = obj[key]
    Dep.target = null
  }
  update() {
    // 获得新值
    this.value = this.obj[this.key]
    // 调用 update 方法更新 Dom
    this.cb(this.value)
  }
}
var data = { name: 'yck' }
observe(data)
// 模拟解析到 `` 触发的操作
new Watcher(data, 'name', update)
// update Dom innerText
data.name = 'yyy' 

接下来,对 defineReactive 函数进行改造

function defineReactive(obj, key, val) {
  // 递归子属性
  observe(val)
  let dp = new Dep()
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      console.log('get value')
      // 将 Watcher 添加到订阅
      if (Dep.target) {
        dp.addSub(Dep.target)
      }
      return val
    },
    set: function reactiveSetter(newVal) {
      console.log('change value')
      val = newVal
      // 执行 watcher 的 update 方法
      dp.notify()
    }
  })
}

以上实现了一个简易的双向绑定,核心思路就是手动触发一次属性的 getter 来实现发布订阅的添加。

模拟vue响应式

function  cb(val){
  /* 渲染视图 */
  console.log("视图更新了~~");
}

function defineReactive(obj,key,val){
  observer(val);  //递归子属性
  /* 一个 Dep 类对象 */
  const dep = new Dep();

  Object.defineProperty(obj,key,{
    enumerable:true,
    configurable:true,
    get:function reactiveGetter(){
      console.log("in getter:",val);
      /* 将 Dep.target(即当前的 Watcher 对象存入 dep 的 subs 中) */
      dep.addSub(Dep.target);
      return val;
    },
    set:function reactSetter(newVal){
      if(val === newVal) return;
      console.log("in setter:",val,newVal);
      /*cb(newVal);*/
      /* 在 set 的时候触发 dep 的 notify 来通知所有的 Watcher 对象更新视图 */
      dep.notify();
    }
  });
}

function observer(value){
  if(!value || (typeof value !== "object")){
    return;
  }

  Object.keys(value).forEach((key)=>{
    defineReactive(value,key,value[key]);
  });
}

/*实现一个Vue*/
/*class Vue{
  constructor(options){
    this._data = options.data;
    observer(this._data);
  }
}

let o = new Vue({
    data: {
        test: "I am test."
    }
});
o._data.test = "hello,world.";  /* 视图更新啦~ */


/*订阅者Dep*/
class Dep{
  constructor(){
    /*用来存放watcher对象的数组*/
    this.subs = [];
  }

  /* 在 subs 中添加一个 Watcher 对象 */
  addSub(sub){
    this.subs.push(sub);
  }

  /* 通知所有 Watcher 对象更新视图 */
  notify(){
    this.subs.forEach((sub)=>{
      sub.update();
    });
  }
}

/*观察者Watcher*/
class Watcher{
  constructor(){
    /* 在new一个Watcher对象时将该对象赋值给Dep.target,在 get 中会用到 */
    Dep.target = this;
  }

  /* 更新视图的方法 */
  update(){
    console.log("update:视图更新啦~~~~");
  }
}
Dep.target = null;


class Vue {
    constructor(options) {
        this._data = options.data;
        observer(this._data);
        /* 新建一个 Watcher 观察者对象,这时候 Dep.target 会指向这个 Watcher 对象 */
        new Watcher();
        /* 在这里模拟 render 的过程,为了触发 test 属性的 get 函数 */
        console.log('render~', this._data.test);
    }
}

let o = new Vue({
    data: {
        test: "I am test."
    }
});
o._data.test = "hello,world.";  /* 视图更新啦~ */
Table of Contents