>[success] # 了解 -- 响应式工作原理(二) ~~~ 1.这次从代码运行角度来看这个问题 2.在使用vue 时候,'数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新', 从而形成我们常说的数据操作视图的效果 ~~~ >[info] ## 如何实现这种效果 ~~~ 1.下面的说明案例引用了,vue官方文档中推荐的教学视频里面的内容 ~~~ [在 Vue Mastery 观看视频讲解](https://www.vuemastery.com/courses/advanced-components/build-a-reactivity-system "Vue Reactivity") >[danger] ##### 先看一个vue 的例子 ~~~ 1.在页面中调用的'addPrice' 方法,页面会重新计算相乘的表达式price * quantity,然后更新页面 ~~~ ~~~ <div id="app"> <div>Price: ${{ price * this.quantity }}</div> <div>Taxes: ${{ totalPriceWithTax }}</div> <button @click='addPrice'>点击</button> </div> <script> var vm = new Vue({ el: '#app', data: { price: 5.00, quantity: 2 }, }, methods:{ addPrice(){ this.price++ } } }) </scprit> ~~~ >[danger] ##### 将上面的代码用js 通常编程工作方式表示 ~~~ 1.下面正常的js逻辑代码是否能像vue一样,当price 发生改变期待着total 也可以重新计算,但实际结果却是 打印出来的结果为10 而不是12 2.JavaScript是程序性的,不是反应性的,为了使total反应性,我们必须使用JavaScript使事物表现不同 ~~~ ~~~ let price = 5 let quantity = 2 let total = price * quantity price++ console.log(`total is ${total}`) // 10 ~~~ >[danger] ##### 如何做到响应式 ~~~ 1.如果可以让上面的代码total 可以在每次price 发生变化后接着调用,就可以实现响应式的效果列如: let price = 5 let quantity = 2 let total = price * quantity price++ total = price * quantity console.log(`total is ${total}`) // 12 2.虽然实现了我们想要的但是过于简陋,现在进行优化,我们的思路是需要有一个可以记录行为的地方 也就是' price * quantity '这个操作,在数据发生变化的时候需要一个方法重新调用我们记录的这个行为 let price = 5 let quantity = 2 let total = 0 let target = null let storage = [] // 来记录需要执行的行为动作,暂时保存起来 function record() { storage.push(target) } // 当数据变化的时候执行在record记录的要执行的相应动作 function replay() { storage.forEach(run => run()) } target = () => { total = price * quantity } // 负责记录price * quantity 这个行为 record() target() console.log(`total is ${total}`) // 10 price++ // 数据改变调用记录的行为 replay() console.log(`total is ${total}`) // 12 ~~~ >[danger] ##### 通过观察者模式 来有优化上面的代码 ~~~ 1.发布者 -- 发布者要收集所有观察者才能 给每个观察者发送要接受的内容 2.观察者 -- 等待发布者发送指令 3.也就是说发布者收集所有需要被观察的内容,当发布者到达某个触发条件时候,会依次触发记录下来的 观察者,发布者更像一个开关负责收集所有需要被在特定时间要出触发的观察者 ~~~ * 最初的构想代码 ~~~ 1.下面的问题 就是纯靠控制 dep 做到发布 和 订阅,其实是可以的没问题的解决思路,如果你看过这个章节 的第一篇你就会知道利用'Object.defineProperty' 给属性的'get' 和 'set' 属性进行一次注册绑定,绑定后 我们想通过外部因素影响内部,先记住这个想法 ~~~ ~~~ // 观察者 和 发布订阅 共同性质 改变通知 可以一对多 class Dep { constructor() { this.sub = []; } // 记录 行为 listen(fn) { if (typeof fn === 'function') this.sub.push(fn); } // 调用行为 trigger(...args) { return this.sub.length > 0 && this.sub.forEach((fn) => fn(...args)); } } const watcher = { total: 0, price: 1, num: 1, }; const dep = new Dep(); dep.listen(() => { watcher.total = watcher.price * watcher.num; }); dep.trigger(); console.log(watcher.total); watcher.price++; dep.trigger(); console.log(watcher.total); ~~~ * 改进一下抽离观察者 ~~~ // 发布者 -- 主要是用来收集,和统一执行被收集来的订阅者 class Dep { constructor() { this.subscribers = [] // 收集订阅者 } depend() { // 收集订阅者方法 if (target && !this.subscribers.includes(target)) { this.subscribers.push(target) } } notify() { // 负责统一执行这些订阅者 this.subscribers.forEach(sub => sub()) } } // 订阅 function watcher(myFunc) { target = myFunc // 在接受到调用者指令时候执行的回调函数 dep.depend() target() // 上来先调用一次 target = null } const dep = new Dep() let price = 5 let quantity = 2 let total = 0 watcher(() => { total = price * quantity }) console.log(`total is ${total}`) // 10 price++ dep.notify() console.log(`total is ${total}`) // 12 ~~~ >[info] ## 新的问题 合适触发发布者 ~~~ 1.刚才都是通过代码逻辑进行逻辑步骤调用,想让数据更加灵活不用每次都手动调用,vue2.0采用了 'Object.defineProperty'这是一个函数,'它允许我们为属性定义getter和setter函数' ~~~ >[danger] ##### Object.definePropert [关于 Object.definePropert 可以看我另外一篇文章 ](https://www.kancloud.cn/cyyspring/more/1246692) ~~~ 1.通过下面的小例子可以发现,现在已经可以对数据变化进行监控了,这样就可以做到对应触发'发布者', ~~~ ~~~ let person = { name: 'w', } Object.defineProperty(person, 'name', { get() { console.log('调用时候执行') }, set(val) { console.log('赋值时候执行') } }) person.name // 执行get person.name = 'y' // 这里执行set ~~~ >[danger] ##### 将上面的案例进行结合 ~~~ 1.这里做个说明Object.defineProperty 对属性进行get 和 set 是时候,在没有调用情况仅仅只是一个绑定。 2.仅仅是绑定因此'watcher' 先触发有了统一的target 3.这个案例就说明了抽离观察这和订阅者的好处 可以通过外部去控制 ~~~ ~~~html <!DOCTYPE html> <html lang="en" style="height: 100%;"> <body style="height: 100%;"> <div id="app">hello</div> <div id="app1">hello</div> <button id="changeViwe">触发视图更新 </button> <button id="changeViwe1">触发视图更新1 </button> </body> <script> /** * 描述 响应代理 * @param {Object} vm 代理后的对象 * @param {Object} data 被代理的对象 * @returns {any} */ function proxy(vm,data){ Object.keys(data).forEach(key => { // 注册观察者 const dep = new Dep() Object.defineProperty(vm,key,{ enumerable:true, configurable:true, get(){ console.log(1234); // 在get 时候记录这个订阅 // Object.defineProperty 在第一次注册的时候是不会执行get的 // 因此其实第一遍给每个属性绑定上get 和set时候只是注册了 dep 观察者对象,并没有执行内部get 和set 方法 // 所以也就并没有往里面填入实际的观察方法 dep.depend() return data[key]; }, set(val){ console.log(1234); if(val===data[key]) return; data[key] = val; // 原来简易版本的执行是写死的只能固定某个位置渲染 // document.getElementById('app').textContent = Object.values(data).join() // 现在利用观察者模型在dep.depend() 去注册了 外部希望执行的回调函数代码,代码灵活 dep.notify() // 在赋值时候触发订阅动作 }, }) }) } /** * 描述 发布者 -- 主要是用来收集,和统一执行被收集来的订阅者 */ class Dep{ constructor(){ this.subs = [] } // 这里有点区别之前的做的观察者模型,那时候是有个函数参数 // 这个函数参数通过调用depend 注册 // 但是现在触发他的方法被在Object.defineProperty get 提前声明 // 你在最初的时候不知道数据会根据什么数据动态变化 // 因此需要更为动态的形式去传递 方法 depend(){ if(target && !this.subs.includes(target)){ this.subs.push(target) } } // 去调用 notify(msg){ const { length } = this.subs length && this.subs.forEach(fun=>fun()) } } /** * 描述 订阅者 * @param {Function} fun 执行的订阅回调函数 */ function watcher(fun){ target = fun // 最开始的进阶案例我们在 wather 这个函数调用了 Dep 中depend 来进行观察的注册 // 现在 因为通过Object.defineProperty 做了响应模型,这样get 时候就能够自动 // 帮助我们注册了 Dep 中depend 来进行观察 // 因此只用当fun 这个回调方法中使用了Object.defineProperty 包裹的属性才会触发 // 其实就是为解决在get 方法中自动添加监听回调触发 fun() target = null } const data = {msg:"123",age:123} const vm = {} proxy(vm,data) /** * 描述 id为app 的dom 节点插入内容 */ function appendAppRoot(){ document.getElementById('app').textContent = vm.msg + vm.age } /** * 描述 id为app1 的dom 节点插入内容 */ function appendApp1Root(){ document.getElementById('app1').textContent = vm.msg + vm.age } watcher( // 更加动态的在外部执行订阅的方法 // watcher 内部会自调用 回调函数,回调函数中 vm.msg + vm.age 这两段就会触发get // 形成订阅 appendAppRoot ) document.getElementById('changeViwe').onclick = function(){ // 当赋值的时候触发set ,set 会触发在调用watcher 时候注册进入观者某型中subs里面的回调 vm.msg = "测试" } watcher( // 更加动态的在外部执行订阅的方法 appendApp1Root ) document.getElementById('changeViwe1').onclick = function(){ vm.msg = "测试666" } </script> </html> ~~~