>[success] # 响应式模型 1. 从代码运行角度来看这个问题,如果代码正常执行的顺序来说,JavaScript是程序性的,不是反应性的 2. vue2 利用 `Object.defineProperty` 进行收集「**依赖收集**」的**响应劫持**使得程序成为了具备反应性功能 >[info] ## 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] ##### 让程序具备响应性 total 可以在每次price 发生变化后**接着调用**,就可以实现**响应式的效果** ~~~ let price = 5 let quantity = 2 let total = price * quantity price++ total = price * quantity console.log(`total is ${total}`) // 12 ~~~ 让程序具备响应式,就是在指定位置去触发效果的改变,先收集行为,在指定触发即可 ~~~ /** * 设计思路,记录操作步骤=》在某些触发节点触发记录步骤=》数据改变 */ let price = 5 let quantity = 2 let total = 0 // 收集步骤的空间 const storage = [] // 收集步骤方法 function record(step) { storage.push(step) } // 触发步骤方法 function replay() { storage.forEach((run) => run()) } // 定义一些需要进行步骤方法例如 价格*数量 - 固定金额 为了方便演示拆两个方法 const all = () => { total = price * quantity } const subtraction = () => { total = total - 8 } // 第一次收集需要执行步骤形式 record(all) record(subtraction) // 收集完开始触发 replay() console.log(total) // 2 // 再次触发需要调用改变数据行为的方法 price++ // 改变价格 replay() console.log(total) // 4 ~~~ * 发布订阅来优化代码 ~~~ // 发布者 class Dep { // 初始化构造收集 constructor() { this.sub = [] } // 收集 addSub(target) { this.sub.push(target) } // 触发 notify() { this.sub.forEach((fun) => { fun() }) } } // 订阅(观察者) function watcher(myFunc) { target = myFunc // 在接受到调用者指令时候执行的回调函数 dep.addSub(target) 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] ## 利用 defineProperty 解决 手动触发 上面问题是需要每次自己找到触发点去手动触发,随着逻辑越来越多这种穿插的行为会越来越多,需要找到一个方法可以帮助我们自动去触发我们注册想执行的逻辑,,vue2.0采用了 `Object.defineProperty`这是一个函数,它允许我们为属性**定义getter和setter函数** ~~~ let person = { name: 'w', } Object.defineProperty(person, 'name', { get() { console.log('调用时候执行') }, set(val) { console.log('赋值时候执行') } }) person.name // 执行get person.name = 'y' // 这里执行set ~~~ 现在可以利用对**数据变化进行监控**,这样就可以做到对应触发**发布者** ~~~ type VueOpt = { data: Record<any, any> } function cb(v: any) { console.log('更新') } class Vue { _data: Record<any, any> constructor(opt: VueOpt) { this._data = opt.data this.observer(this._data) } // observer 观察者 observer(data: Record<any, any>) { if (!data || typeof data !== 'object') return Object.keys(data).forEach((key) => { this.defineReactive(data, key, data[key]) }) } // 转换赋值Object.defineProperty defineReactive(data: Record<any, any>, key: string, value: any) { Object.defineProperty(data, key, { enumerable: true, configurable: true, get() { return value }, set(v) { if (v === data[key]) return value = v // 简单相应拦截 cb(v) }, }) } } const vue = new Vue({ data: { test: '测试', }, }) vue._data.test = '123' console.log(vue._data.test) ~~~ >[danger] ##### 简单的渲染案例 1. 这里做个说明Object.defineProperty 对属性进行get 和 set 是时候,在没有调用情况仅仅只是一个绑定。 2. 仅仅是绑定因此'**watcher**' 先触发有了统一的**target ** 3. 这个案例就说明了抽离观察这和订阅者的好处 可以通过外部去控制 ~~~ <!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> ~~~ >[danger] ##### 参考 [在 Vue Mastery 观看视频讲解](https://www.vuemastery.com/courses/advanced-components/build-a-reactivity-system "Vue Reactivity") [关于 Object.definePropert 可以看我另外一篇文章](https://www.kancloud.cn/cyyspring/more/1246692)