双向绑定

核心:对data完成可观察对象的转化+发布订阅模式

  1. 可观察对象的转化:Object.defineProperty()来设置getter/setter的钩子
  2. 发布订阅模式: 使用依赖注入的设计思想,通过中间人Dep对象来处理发布-订阅(更新-使用)过程的处理

bind

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')