先上效果图

image-20200911162228319

Vue的双向绑定采用数据劫持配合发布者——订阅者模式,通过Object.defineproperty()劫持各个属性getter和setter,数据变动时,发布消息给依赖收集器Dep,然后通知观察者,做出对应的回调,更新视图

在案例中MyVue作为入口,整合Observer,Compile,Watcher,通过Observer来监听数据变化通过Compile来编译模板,利用Watcher来连接Compile和Observer,来实现双向绑定

MyVue作为入口,经过Compile解析之后渲染到页面,同时添加观察者Observer来监听数据变化,当数据变化时通知依赖收集器Dep,将变化分发给watcher然后再渲染视图

image-20200911162534506

数据劫持

示例代码


const obj = {};

function property(obj, key, value) {
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: false,
    get() {
      return '姓名:' + value;
    },
    set(newVal) {
      value = newVal;
      console.log('修改了数据');
    }
  })
}
property(obj, 'name', '张三');
console.log(obj.name);

运行图

image-20200911162410965

当数据读取时会加上“姓名:”,数据修改时会提示修改数据,此时对象obj除了name之外,还有name的getter和setter两个方法

代码

入口

class MyVue {
  constructor(options) {
    this.$el = options.el;
    this.$data = options.data;
    this.$options = options;
    if (this.$el) {
      // 观察者
      new Observer(this.$data);
      // 指令解析器
      new Compile(this.$el, this)
  }
}

Compile

指令解析器,解析网页元素中的双大括号、v-model、v-on……等元素。对指令节点进行判断,然后遍历其所有属性,看是否有匹配的指令的属性。首先要获得dom元素,然后对含有dom元素上含有指令的节点进行处理,建一个fragment片段,将需要解析的dom元素存到fragment片段中在做处理,接下来需要遍历所有节点,对含有指令的节点进行特殊的处理

class Compile {
  constructor(el, vm) {
    // 赋值/获取节点
    this.el = this.isElementNode(el) ? el : document.querySelector(el);
    this.vm = vm;
    // 获取子元素文档碎片
    const frag = this.fragment(this.el);
    // console.log(frag);
    // 编译模板
    this.compile(frag);

    // 追加子元素到根元素
    this.el.appendChild(frag);

  }
  compile(fragment) {
    const children = fragment.childNodes;
    [...children].forEach(child => {
      if (this.isElementNode(child)) {
        // 元素节点
        this.compileElement(child);
      } else {
        // 文本节点
        this.complieText(child);
      }

      if (child.childNodes && child.childNodes.length) {
        this.compile(child)
      }
    });
  }
  complieText(text) {
    const content = text.textContent;
    if(/\{\{(.+?)\}\}/.test(content)) {
      compileUtil['text'](text, content, this.vm);
    }
  }
  compileElement(node) {
    const attributes = node.attributes;
    [...attributes].forEach(attribute => {
      const {
        name,
        value
      } = attribute;
      if (this.isDire(name)) {
        const [, dirctive] = name.split('-');
        const [dirName, eventName] = dirctive.split(':');
        // 更新视图
        compileUtil[dirName](node, value, this.vm, eventName);
        // 删除指令
        node.removeAttribute('v-' + dirctive);
      } else if(this.isEventName(name)) {// 判断@符号
        let [, eventName] = name.split('@');
        compileUtil['on'](node, value, this.vm, eventName);
      } else if(this.isBindName(name)) {
        let [, attr] = name.split(':');
        compileUtil['bind'](node, value, this.vm, attr);
      }
    })
  }
  isBindName(name) {
    return name.startsWith(':')
  }
  isEventName(name) {
    return name.startsWith('@')
  }
  isDire(name) {
    return name.startsWith('v-')
  }
  fragment(el) {
    // 创建文档碎片
    const nodeFrag = document.createDocumentFragment();
    while (el.firstChild) {
      nodeFrag.appendChild(el.firstChild);
    }
    return nodeFrag;
  }
  isElementNode(node) {
    return node.nodeType === 1;
  }
}

Observer

遍历循环对所有属性值进行监听,并对其进行数据劫持。通过observe()方法进行遍历向下找到所有的属性,并通过defineReactive()方法进行数据劫持监听。知道监听器Observer是在get函数中执行了添加订阅者的操作的,所以我们只需要在订阅者Watcher在初始化时触发相对应的get函数来执行添加订阅者的操作,借助依赖收集器


class Observer {
  constructor(data) {
      this.observer(data);
  }
  observer(data) {
    if(data && typeof data === 'object') {
      Object.keys(data).forEach(key => {
        this.defineReactive(data, key, data[key]);
      })
    }
  }
  defineReactive(data, key, value) {
    this.observer(value);
    const depend = new Dep();
    // 劫持
    Object.defineProperty(data, key, {
      enumerable: true,
      configurable: false,
      get() {
        // 数据初始化的时候添加订阅
        Dep.target && depend.addSub(Dep.target)
        return value;
      },
      set: (newValue) => {
        this.observer(newValue);
        if(newValue !== value) {
          value = newValue;
        }
        // 通知Dep变化
        depend.notify();
      }
    })
  }
}

Dep

依赖收集器,主要负责添加订阅以及发布通知

class Dep {
  // 收集watcher,通知watcher
  constructor() {
      this.subs = [];
  }
  // 收集watcher
  addSub(watcher) {
    this.subs.push(watcher);
  }
  // 通知watcher更新
  notify() {
    console.log("观察者", this.subs);
    this.subs.forEach(watcher => {
      watcher.update()
    })
  }
}

Watcher

将Compile和Observer关联起来


class Watcher {
  constructor(vm, express, callback) {
    this.vm = vm;
    this.express = express;
    this.callback = callback;
    // 保存旧值
      this.oldValue = this.getOldValue();
  }
  getOldValue() {
    Dep.target = this;
    let old = compileUtil.getValue(this.express, this.vm);
    Dep.target = null;
    return old;
  }
  update() {
    const newValue = compileUtil.getValue(this.express, this.vm);
    if(newValue !== this.oldValue) {
      this.callback(newValue);
    }
  }
}

上面图中的updater放在了compileUtil中


const compileUtil = {
  // 获取data中的对象值
  getValue(express, vm) {
    return express.split('.').reduce((data, currentVal) => {
      // console.log(currentVal);
      return data[currentVal]
    }, vm.$data)
  },
  setValue(express, vm, inputValue) {
    return express.split('.').reduce((data, currentVal) => {
      data[currentVal] = inputValue;
    }, vm.$data)
  },
  // 区分开{{}}--{{}}
  getContentValue(express, vm) {
    return express.replace(/\{\{(.+?)\}\}/g, (...args) => {
      return this.getValue(args[1], vm);
    })
  },
  // v-text
  text(node, express, vm) {
    let value;
    if(express.indexOf('{{') !== -1) {
      // {{msg}}
      value = express.replace(/\{\{(.+?)\}\}/g, (...args) => {
        // 绑定观察者
        new Watcher(vm, args[1], (newValue) => {
          this.updater.textUpdater(node, this.getContentValue(express, vm));
        })
        return this.getValue(args[1], vm);
      })
    } else {
      value = this.getValue(express, vm);
    }
    this.updater.textUpdater(node, value);
  },
  // v-html
  html(node, express, vm) {
    let value = this.getValue(express, vm);
    new Watcher(vm, express, (newValue) => {
      this.updater.htmlUpdater(node, newValue);
    });
    this.updater.htmlUpdater(node, value);
  },
  // v-model
  model(node, express, vm) {
    // 数据-视图
    const value = this.getValue(express, vm);
    new Watcher(vm, express, (newValue) => {
      this.updater.modelUpdater(node, newValue);
    });
    // 视图-数据-视图
    node.addEventListener('input', e => {
      this.setValue(express, vm, e.target.value);
    })
    this.updater.modelUpdater(node, value);
  },
  // v-on
  on(node, express, vm, event) {
    const f = vm.$options.methods && vm.$options.methods[express];
    node.addEventListener(event, f.bind(vm), false);
  },
  bind(node, express, vm, attribute) {
    const value = this.getValue(express, vm);
    this.updater.bindUpdater(node, attribute, value);
  },
  updater: {
    textUpdater(node, value) {
      node.textContent = value;
    },
    htmlUpdater(node, value) {
      node.innerHTML = value;
    },
    modelUpdater(node, value) {
      node.value = value;
    },
    bindUpdater(node, attribute, value) {
      node.setAttribute(attribute, value);
    }
  }
}

前端小白