数据驱动

开发过程中只须要关注数据,而不须要关系数据如何渲染到视图
  • 数据响应式:数据批改时,视图会随之更新,防止 DOM 操作
  • 双向绑定:数据扭转,视图扭转;视图扭转,数据扭转

公布订阅模式和观察者模式

定义了对象间一种一对多的依赖关系,当指标对象的状态产生扭转时,所有依赖它的对象都会失去告诉。

观察者模式

  • 发布者:具备 notify 办法,当发生变化时调用观察者对象的 update 办法
  • 观察者:具备 update 办法
  class Subscriber {    constructor () {      this.subs = []    }    add(sub) {      this.subs.push(sub)    }    notify() {      this.subs.forEach(handler => { handler.update() })    }  }  class Observer {    constructor (name) {      this.name = name;    }    update() {      console.log('接到告诉', this.name);    }  }  const subscriber = new Subscriber();  const jack = new Observer('jack');  const tom = new Observer('tom');  subscriber.add(jack)  subscriber.add(tom)  subscriber.notify()

公布订阅模式

公布订阅模式与观察者模式的不同,在发布者和观察者之间引入了事件核心。使得指标对象并不间接告诉观察者,而是通过事件核心来派发告诉。
  class EventController {    constructor() {      this.subs = {}    }    subscribe(key, fn) {      this.subs[key] = this.subs[key] || []      this.subs[key].push(fn)    }    publish(key, ...args) {      if (this.subs[key]) {        this.subs[key].forEach(handler => { handler(...args) });      }    }  }  const event = new EventController()  event.subscribe('onWork', time => { console.log('下班了', time) });  event.subscribe('offWork', time => { console.log('上班了', time); });  event.subscribe('onLaunch', time => { console.log('吃饭了', time); });  event.publish('offWork', '18:00:00');  event.publish('onLaunch', '20:00:00');

总结

  • 观察者模式是由具体指标调度,事件触发时发布者会被动调用观察者的办法,所以发布者和观察者之间存在依赖
  • 公布订阅模式是由事件核心对立调度,因为发布者和订阅者二者之间没有强依赖关系

数据响应式外围原理

Vue2

当你把一个一般的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并应用 Object.defineProperty 把这些 property 全副转为 getter/setterObject.defineProperty 是 ES5 中一个无奈 shim 的个性,这也就是 Vue 不反对 IE8 以及更低版本浏览器的起因。
  function proxyData(data) {    // 遍历 data 对象的所有属性    Object.keys(data).forEach(key => {      // 把 data 中的属性,转换成 vm 的 setter/setter      Object.defineProperty(vm, key, {        enumerable: true,        configurable: true,        get () {          console.log('get: ', key, data[key])          return data[key]        },        set (newValue) {          console.log('set: ', key, newValue)          if (newValue === data[key]) {            return          }          data[key] = newValue          // 数据更改,更新 DOM 的值          document.querySelector('#app').textContent = data[key]        }      })    })  }

因为 JavaScript 的限度,Vue 不能检测以下数组的变动:

  • 当利用索引间接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
  • 当批改数组的长度时,例如:vm.items.length = newLength

Object.defineProperty 在数组中的体现和在对象中的体现是统一的,数组的索引就可以看做是对象中的 key 。

  • 通过索引拜访或设置对应元素的值时,能够触发 getter 和 setter 办法
  • 通过 push 或 unshift 会减少索引,对于新减少的属性,须要再手动初始化能力被 observe 。
  • 通过 pop 或 shift 删除元素,会删除并更新索引,也会触发 setter 和 getter 办法。

官网文档中对于这两点都是简要的概括为“因为JavaScript的限度”无奈实现,其实起因并不是因为 Object.defineProperty 存在破绽,而是出于性能问题的思考。

Vue3

  • Proxy 是 ES 6 中新增的语法,IE 不反对,性能由浏览器优化。
  • Proxy 间接监听对象,而非属性,defineProperty 监听的是对象中的某一个属性。
  let vm = new Proxy(data, {    // 执行代理行为的函数    // 当拜访 vm 的成员会执行    get (target, key) {      console.log('get, key: ', key, target[key])      return target[key]    },    // 当设置 vm 的成员会执行    set (target, key, newValue) {      console.log('set, key: ', key, newValue)      if (target[key] === newValue) {        return      }      target[key] = newValue      document.querySelector('#app').textContent = target[key]    }  })

实现过程

Vue

  • 接管初始化的参数,注入到 Vue 实列 $options 属性中
  • 把 data 中的属性注入到 Vue 实列 $data 属性中,并生成 getter/setter
  • 调用 observer 监听 data 中属性的变动
  • 调用 compiler 解析指令/插值表达式
class Vue {  constructor (options) {    // 1. 通过属性保留选项的数据    this.$options = options || {};    this.$data = options.data || {};    this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el;    // 2. 把data中的成员转换成getter和setter,注入到vue实例中    this._proxyData(this.$data);    // 3. 调用observer对象,监听数据的变动    new Observer(this.$data);    // 4. 调用compiler对象,解析指令和差值表达式    new Compiler(this);  }  _proxyData(data) {    Object.keys(data).forEach(key => {      // 把data的属性注入到vue实例中      Object.defineProperty(this, key, {        configurable: true,        enumerable: true,        get () {          return data[key];        },        set (newValue) {          if (newValue === data[key]) {            return;          }          data[key] = newValue;        }      })    })  }}

Observer

  • 把 data 中的属性转换为响应式数据
  • 数据变动发送告诉
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])    })  }  defineReactive(data, key, val) {    // 如果val是对象,把val外部的属性转换成响应式数据    this.walk(val)    const that = this;    // 收集依赖,并发送告诉    const dep = new Dep();    Object.defineProperty(data, key, {      configurable: true,      enumerable: true,      get() {        // 收集依赖        Dep.target && dep.addSubs(Dep.target);        return val;      },      set(newVal) {        if (newVal === val) { return; }        val = newVal;        // 如果newValue是对象,把newValue外部的属性转换成响应式数据        that.walk(newVal);        // 发送告诉        dep.notify();      }    })  }}

Compiler

  • 编译模板,解析指令
  • 页面渲染,当数据变动后从新渲染视图
class Compiler {  constructor(vm) {    this.el = vm.$el;    this.vm = vm;    this.compile(this.el);  }  // 编译模板,解决文本节点和元祖节点  compile (el) {    Array.from(el.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)      }    })  }  // 编译文本节点,解决差值表达式  compileText (node) {    const reg = /\{\{(.+?)\}\}/    if (reg.test(node.textContent)) {      const key = RegExp.$1.trim();      node.textContent = node.textContent.replace(reg, this.vm[key]);      // 创立watcher对象,当数据扭转更新视图      new Watcher(this.vm, key, (newValue) => { node.textContent = newValue });    }  }  // 编译元素节点,解决指令  compileElement (node) {    Array.from(node.attributes).forEach(attr => {      if (this.isDirective(attr.name)) {}        const attrName = attr.name.substr(2);        const key = attr.value;        const updateFn = this[attrName + 'Updater']        updateFn && updateFn.call(this, node, this.vm[key], key)    })  }  // v-text  textUpdater (node, value, key) {    node.textContent = value;    // 创立watcher对象,当数据扭转更新视图    new Watcher(this.vm, key, (newValue) => { node.textContent = newValue });  }  // v-model  modelUpdater (node, value, key) {    node.value = value;    // 创立watcher对象,当数据扭转更新视图    new Watcher(this.vm, key, (newValue) => { node.value = newValue });    // 双向绑定    node.addEventListener('input', () => { this.vm[key] = node.value });  }  // 判断元素属性是否是指令  isDirective (attrName) {    return attrName.startsWith('v-');  }  // 判断节点是否是文本节点  isTextNode (node) {    return node.nodeType === 3;  }  // 判断节点是否是元素节点  isElementNode (node) {    return node.nodeType === 1;  }}

Dep

  • 发布者
  • 在 getter 中增加观察者,在 setter 中发送告诉
class Dep {  constructor() {    // 存储所有的观察者    this.subs = [];  }  // 增加观察者  addSubs(sub) {    if (sub && sub.update) {      this.subs.push(sub);    }  }  // 发送告诉  notify() {    this.subs.forEach(sub => { sub.update() });  }}

Watcher

  • 实例化时增加到 dep
  • 数据发生变化时,dep 告诉所有 watcher 实列更新视图
class Watcher {  constructor(vm, key, cb) {    // vue 实列    this.vm = vm;    // data中的属性名称    this.key = key;    // 回调函数负责更新视图    this.cb = cb;    // 把watcher对象记录到Dep类的动态属性target    Dep.target = this;    // 触发get办法,在get办法中会调用addSub    this.oldValue = vm[key];    // 置空,避免影响其余属性    Dep.target = null;  }  // 当数据发生变化的时候更新视图  update() {    const newValue = this.vm[this.key]    if (this.oldValue === newValue) {      return    }    this.cb(newValue);  }}

双向绑定

  • 监听文本框 input 事件,更新节点 value
  // 双向绑定  node.addEventListener('input', () => { this.vm[key] = node.value });