说明: 直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。javascript
备注: 应当直接在 Object
构造器对象上调用此方法,而不是在任意一个 Object
类型的实例上调用。html
get
和 set
方法const data = {} let value = '' Object.defineProperty(this.data, "msg", { get(){ // 当对象的 key 被访问的时候会执行这个方法 // 这里添加咱们本身的方法就会优先执行 return value }, set(newVal){ // 与 get 方法类似,当给当前属性赋值的时候会自调用 set 方法 // 本身的方法 if (value === newVal) return value = newVal } })
set
get
方法时须要一个真实的中间变量,而咱们又不想将这个变量暴露在外面,所以咱们将其封装// 咱们封装这样一个函数,这样 value 能够充当中间变量 // 这里会触发闭包,value 这个值一直保存在内存中 defineReactive(obj, key, value) { Object.defineProperty(obj, key, { get() { console.log(value); return value }, set: (newVal) => { if (value === newVal) return console.log(value, newVal); value = newVal } }) }
这两种设计模式一直傻傻分不清楚,知道有一天我逛 知乎 我发现其中的奥妙,没啥区别 - -!vue
其核心思想就是经过感知变化从而作出反应java
// 定义一个被观察者 Subject 或者叫 Observable class Subject { constructor() { this.observers = [] // 维护一个观察者(Observer)的集合 - 观察列表 this.data = {} this.defineReactive(this.data, "msg", '') } // 将 this.data 进行数据劫持,当给这个属性赋值时向订阅者推送消息 defineReactive(obj, key, value) { Object.defineProperty(obj, key, { set: (newVal) => { if (value === newVal) return this.publicMsg(newVal) value = newVal } }) } publicMsg(msg) { this.observers.forEach(observer => [ observer.receive(msg) ]) } addObserver(observer) { this.observers.push(observer) } } // 定义观察者,须要接收一个参数一个被观察者,将本身添加到其观察列表 class Observer { constructor(name, subject) { this.name = name subject.addObserver(this) } receive(msg) { console.log(`${this.name} 收到了消息 ${msg}`); } } const sub = new Subject const obs1 = new Observer('limy1', sub) const obs2 = new Observer('limy2', sub) sub.data.msg = '观察者模式'
vue
咱们只看实现原理,一些特殊状况不作考虑,以最理想的最精简的方式展示 MVVMclass Vue
拿到外部传入的数据class Vue { constructor(options) { this.$el = options.el this.$data = options.data } }
{{obj.name}}
和 v-text
这种相似于槽的地方填上咱们传入的数据呢// 咱们建立一个编译的类 class Compile { constructor(el, vm) { this.el = this.isElementNode(el) ? el : document.querySelector(el) this.vm = vm const fragment = this.node2Fragment(this.el) // 将 dom 转化为文档碎片 this.compile(fragment) // 在这里完成 dom 上的槽与 data 上的数据结合 this.el.appendChild(fragment) // 合并好的数据添加到页面 } node2Fragment(node) { const fragment = document.createDocumentFragment() let firstChild while (firstChild = node.firstChild) { fragment.appendChild(firstChild) } return fragment } // 文本节点和元素节点不一样,因此咱们分别处理,考虑到 dom 节点会出现嵌套,所以使用递归完成深度遍历 compile(node) { const childNodes = node.childNodes; [...childNodes].forEach(childNode => { this.isElementNode(childNode) ? this.compileElement(childNode) : this.compileText(childNode); (childNode.childNodes && childNode.childNodes.length) && this.compile(childNode) }) } compileText(node) { const text = node.textContent if (/\{\{(.+?)\}\}/g.test(text)) { compileUtil.text(node, text, this.vm) } } compileElement(node) { const [...attrs] = node.attributes attrs.forEach(attr => { const { name, value } = attr if (name.startsWith('v-')) { // 找到以 v-开头的属性 const [_, directive] = name.split('-') // ["v", "text"] compileUtil[directive](node, value, this.vm) } }) } isElementNode(node) { return node.nodeType === 1 } } // 解耦 将处理不一样格式的数据封装 const compileUtil = { getVal(expr, vm) { // 将传入的表达式在 data 中取值 return expr.split('.').reduce((data, currentVal) => data[currentVal], vm.$data) }, text(node, expr, vm) { // 这里是精简版不考虑 {{obj.name}} -- {{obj.name}} 这种状况 let val if (expr.indexOf('{{') !== -1) { // expr {{obj.name}} val = expr.replace(/\{\{(.+?)\}\}/g, (...args) => { expr = args[1] return this.getVal(args[1], vm) }) } else { // expr v-text val = this.getVal(expr, vm) } // new Watcher(vm, expr, newVal => this.updater(node, newVal)) this.updater(node, val) }, updater(node, val) { node.textContent = val } }
完成这些 咱们就能在网页上看到合并后的结果,控制台也没有出现错误node
$data
上的全部属性class Observer { constructor(data) { this.observe(data) } observe(obj) { if (obj && typeof obj === 'object') { Object.keys(obj).forEach(key => { this.defineReactive(obj, key, obj[key]) }) } } defineReactive(obj, key, value) { this.observe(value) // 考虑到数据嵌套,咱们对其递归处理 Object.defineProperty(obj, key, { get() { return value }, set(newVal) { console.log('newVal', newVal); if (value !== newVal) { value = newVal } } }) } }
能够看到当咱们对 vue
实例上 $data
的 msg
属性进行赋值时,会打印出 newVal newMsg
,说明咱们已经完成了对 $data
数据的劫持监听web
class Dep { constructor() { this.subs = [] } addSub(watcher) { this.subs.push(watcher); } // 通知变化 notify() { this.subs.forEach(w => w.update()); } }
class Watcher { constructor(vm, expr, cb) { // 观察新值和旧值的变化,若是有变化 更新视图 this.vm = vm; this.expr = expr; this.cb = cb; // 先把旧值存起来 this.oldVal = this.getOldVal(); } getOldVal() { Dep.target = this; // 在这里咱们将 watcher 实例挂在到 Dep.target 上 // 在执行时会访问 $data 上的属性,这样就会触发劫持的 get() 方法 // 在 get 方法中 咱们经过 Dep.target 就可以获取到当前实例 将其添加到 subs 中,这样就完成了对应 let oldVal = compileUtil.getVal(this.expr, this.vm); Dep.target = null; // 防止同时添加多个 watcher 咱们将 Dep.target 置空 return oldVal; } update() { // 更新操做 数据变化后 Dep会发生通知 告诉观察者更新视图 let newVal = compileUtil.getVal(this.expr, this.vm); if (newVal !== this.oldVal) { this.cb(newVal); } } }
// 对数据代理,使之能够经过实例访问属性 vm.$data.msg => vm.msg proxyData() { Object.keys(this.$data).forEach(key => { Object.defineProperty(this, key, { get() { return this.$data[key] }, set(newVal) { this.$data[key] = newVal } }) }) }
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>vue</title> </head> <body> <div id="app"> <h2>{{obj.name}}</h2> <h2>{{obj.age}}</h2> <h3 v-text='obj.name' id="h3"></h3> <h4 v-text='msg'></h4> <ul> <li>1</li> <li>2</li> <li>3</li> </ul> <h3>{{msg}}</h3> </div> <script> class Vue { constructor(options) { this.$el = options.el this.$data = options.data new Observer(this.$data) this.proxyData() new Compile(this.$el, this) } } class Compile { constructor(el, vm) { this.el = this.isElementNode(el) ? el : document.querySelector(el) this.vm = vm const fragment = this.node2Fragment(this.el) // 将 dom 转化为文档碎片 this.compile(fragment) // 在这里完成 dom 上的槽与 data 上的数据结合 this.el.appendChild(fragment) // 合并好的数据添加到页面 } node2Fragment(node) { const fragment = document.createDocumentFragment() let firstChild while (firstChild = node.firstChild) { fragment.appendChild(firstChild) } return fragment } // 文本节点和元素节点不一样,因此咱们分别处理,考虑到 dom 节点会出现嵌套,所以使用递归完成深度遍历 compile(node) { const childNodes = node.childNodes; [...childNodes].forEach(childNode => { this.isElementNode(childNode) ? this.compileElement(childNode) : this.compileText(childNode); (childNode.childNodes && childNode.childNodes.length) && this.compile(childNode) }) } compileText(node) { const text = node.textContent if (/\{\{(.+?)\}\}/g.test(text)) { compileUtil.text(node, text, this.vm) } } compileElement(node) { const [...attrs] = node.attributes attrs.forEach(attr => { const { name, value } = attr if (name.startsWith('v-')) { // 找到以 v-开头的属性 const [_, directive] = name.split('-') // ["v", "text"] compileUtil[directive](node, value, this.vm) } }) } isElementNode(node) { return node.nodeType === 1 } } const compileUtil = { getVal(expr, vm) { // 将传入的表达式在 data 中取值 return expr.split('.').reduce((data, currentVal) => data[currentVal], vm.$data) }, text(node, expr, vm) { // 这里是精简版不考虑 {{obj.name}} -- {{obj.name}} 这种状况 let val if (expr.indexOf('{{') !== -1) { // expr {{obj.name}} val = expr.replace(/\{\{(.+?)\}\}/g, (...args) => { expr = args[1] return this.getVal(args[1], vm) }) } else { // expr v-text val = this.getVal(expr, vm) } new Watcher(vm, expr, newVal => this.updater(node, newVal)) this.updater(node, val) }, updater(node, val) { node.textContent = val } } class Observer { constructor(data) { this.observe(data) } observe(obj) { if (obj && typeof obj === 'object') { Object.keys(obj).forEach(key => { this.defineReactive(obj, key, obj[key]) }) } } defineReactive(obj, key, value) { this.observe(value) // 考虑到数据嵌套,咱们对其递归处理 const dep = new Dep Object.defineProperty(obj, key, { get() { Dep.target && dep.addSub(Dep.target) return value }, set(newVal) { console.log('newVal', newVal); if (value !== newVal) { value = newVal } dep.notify() } }) } } class Watcher { constructor(vm, expr, cb) { // 观察新值和旧值的变化,若是有变化 更新视图 this.vm = vm; this.expr = expr; this.cb = cb; // 先把旧值存起来 this.oldVal = this.getOldVal(); } getOldVal() { Dep.target = this; let oldVal = compileUtil.getVal(this.expr, this.vm); Dep.target = null; return oldVal; } update() { // 更新操做 数据变化后 Dep会发生通知 告诉观察者更新视图 let newVal = compileUtil.getVal(this.expr, this.vm); if (newVal !== this.oldVal) { this.cb(newVal); } } } class Dep { constructor() { this.subs = [] } // 添加订阅者 addSub(watcher) { this.subs.push(watcher); } // 通知变化 notify() { // 观察者中有个update方法 来更新视图 this.subs.forEach(w => w.update()); } } const vm = new Vue({ el: '#app', data: { obj: { name: 'limy', age: 24, }, msg: 'vue 简易版', } }) </script> </body> </html>