双向绑定
核心:对data完成可观察对象的转化+发布订阅模式
- 可观察对象的转化:Object.defineProperty()来设置getter/setter的钩子
- 发布订阅模式: 使用依赖注入的设计思想,通过中间人Dep对象来处理发布-订阅(更新-使用)过程的处理
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <h1 id="name"></h1> <input type="text"> <input type="button" value="改变data内容" onclick="changeInput()"> <script src="observer.js"></script> <script src="watcher.js"></script> <script> function myVue (data, element, key) { this.data = data; observable(data);//将数据变的可观测 element.innerHTML = this.data[key];// 初始化模板数据的值 //watcher是data.key.dep.subs这个订阅名单数组中的元素,作用是把DOM对象和已经完成observablelization的对象里的key联系起来 new Watcher(this, key, function (value) { element.innerHTML = value; }); return this; } var ele = document.querySelector('#name'); var input = document.querySelector('input'); /* @param ( Object ) data对象 @param ( Object ) 绑定的DOM @param ( String ) data当中需要和DOM绑定的字段 */ var myVue = new myVue({ name: 'hello world' }, ele, 'name'); //改变输入框内容 input.oninput = function (e) { myVue.data.name = e.target.value; } //改变data内容 function changeInput(){ myVue.data.name = "Hi, button changed myVue.data.name"; } </script> </body> </html>
observer.js
每一个组件都有一个data对象,使得data对象的每一项(key)都带有3样东西:
1.订阅器Dep,统一管理调度所有的订阅者(watcher/subscriber)
2.Getter钩子 - dep.depend()依赖收集,即把wathcer添加到名单中
3.Setter钩子 - dep.notify()依赖注入,即要告诉_所有的watcher:值发生了改变
function observable (obj) { if (!obj || typeof obj !== 'object') { return; } let keys = Object.keys(obj); keys.forEach((key) =>{ defineReactive(obj,key,obj[key]) }) return obj; } function defineReactive (obj,key,val) { let dep = new Dep(); Object.defineProperty(obj, key, { get(){ dep.depend(); console.log(`${key} was read`); return val; }, set(newVal){ val = newVal; console.log(`${key}was modified`); dep.notify()//数据变化通知所有订阅者 } }) } class Dep { constructor(){ this.subs = [] } //增加订阅者 addSub(sub){ this.subs.push(sub); } //判断是否增加订阅者 depend () { if (Dep.target) { this.addSub(Dep.target) } } //通知订阅者更新 notify(){ this.subs.forEach((sub) =>{ sub.update() }) } } Dep.target = null;
watcher.js
class Watcher { constructor(vm,exp,cb){ this.vm = vm; this.exp = exp; this.cb = cb; this.value = this.get(); // 将自己添加到订阅器的操作 } get(){ Dep.target = this; // 缓存自己 let value = this.vm.data[this.exp] // 初始化赋值且触发observable当中的getter钩子 Dep.target = null; // 释放自己 return value; } update(){ let value = this.vm.data[this.exp]; let oldVal = this.value; if (value !== oldVal) { this.value = value; this.cb.call(this.vm, value, oldVal); } } }
从现象/用户体验的角度来看整个过程
1.document/组件加载,中间完成几件事:
- 完成data对象向可观察对象的转化(observablization):data中每一项(key)都带有3样东西:dependencies,getter,setter
- 初始化(第一次触发getter钩子'name was read')
- new一个wathcer,会自动把自己添加到Dep的subscibor当中(并且第二次触发getter钩子'name was read')
2.DOM操作(点击按钮/在input中输入的时候)进行更新,并在DOM对象h1上显示出来新值
- 把DOM值赋值给data.name(触发setter钩子'name was modified')
- setter钩子当中,会调用dep.notify()通知所有subscribor来执行各自的update()函数
- 现在subscribor当中只有一个watcher,在new的时候第三个参数-回调函数(处理业务逻辑)作为update()的主体。此处因为是数据的双向绑定,所以业务逻辑就是把watcher接受到的新值,渲染到DOM对象h1上(第三次触发getter钩子'name was read')