数据驱动
开发过程中只须要关注数据,而不须要关系数据如何渲染到视图
- 数据响应式:数据批改时,视图会随之更新,防止 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/setter
。Object.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 });