yizihan / blog

Record
1 stars 0 forks source link

Vue - 数据双向绑定 #18

Open yizihan opened 6 years ago

yizihan commented 6 years ago

双向数据绑定

Vue采用数据劫持结合发布者-订阅者模式 #13 的方式,通过Object.defineProperty()来劫持各个属性的getter/setter,在数据发生变化时发布消息给订阅者,触发相应的监听回调。

  1. 实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者。
  2. 实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数。
  3. 实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性的变动的通知,还能执行绑定的回调函数,从而更新视图。
  4. MVVM入口函数,整合以上三者。

MVVM

var vm = new Vue({
    el: 'app',
    data: {
        text: 'hello world'
    }
});

function Vue (options) {
    // this => new Vue的实例
    // 保证了不同实例之间的数据
    this.data = options.data;       // 每个实例的data=new时传入的data
    // data = {text: 'hello world'}
    var data = this.data;
    // 为data内的"text"添加get()/set()
    observe(data, this);
    var id = options.el;
    // 执行编译
    var dom = new Compile(document.getElementById(id), this);
    document.getElementById(id).appendChild(dom);
}

实现Vue构造函数,并实例化传参。

Observer

function observe (obj, vm) {    // obj => data   vm => this(实例)此时的this也包含data数据
    // 普通遍历obj,得到所有key和value(obj[key])
    Object.keys(obj).forEach(function(key) {    // key => data的每个属性
        defineReactive(vm, key, obj[key]);          // obj[key] => data对应的每个属性值
    })
}

function defineReactive(obj, key, val) {        // obj => this(包含data)
    var dep = new Dep();            // new出的实例包含一个数组和两个方法
    Object.defineProperty(obj, key, {
        get: function () {
            // 添加订阅者watcher到主题对象Dep
            if(Dep.target) {
                // JS的浏览器单线程特性,保证这个全局变量在同一时间内,只会有同一个监听器使用
                dep.addSub(Dep.target);
            }
            return val;
        },
        set: function (newVal) {
            if(newVal === val) return;
            // 从input输入的新值
            val = newVal;
            // 作为发布者发出通知
            dep.notify();
        }
    })
}

监听实例的所有属性,如果发生变化则通过发布者(Dep)通知给订阅者(Watcher)。

Compile

// node => id节点   vm => this实例
function Compile(node, vm) {    
    if (node) {
        this.$frag = this.nodeToFragment(node, vm);
        return this.$frag;
    }
}

Compile.prototype = {
    nodeToFragment: function (node, vm) {
        var self = this;        // 缓存this
        var frag = document.createDocumentFragment();
        var child;
        // 循环 尾递归
        while(child = node.firstChild) {    
            // 当前元素的子节点
            self.compileElement(child, vm);
            // 将所有子节点添加到fragment中
            frag.append(child);         
        }
        return frag;
    },
    compileElement: function (node, vm) {
        // 找到所有的 {{ }}
        var reg = /\{\{(.*)\}\}/;   

        // 节点类型为元素
        if(node.nodeType === 1) {
            // 找到所有绑定在节点上的属性
            var attr = node.attributes; 
            // 解析属性
            for(var i = 0; i < attr.length; i++) {
                if (attr[i].nodeName == 'v-model') {
                    // 获取v-model绑定的属性名
                    var name = attr[i].nodeValue;       
                    // 监听input输入事件
                    node.addEventListener('input', function(e) {
                        // 给相应的data属性赋值,进而触发该属性的set方法 => Dep.notify() => Watcher.update()
                        vm[name] = e.target.value;
                    });
                    // node.value = vm[name];   // 将data的值赋给该node
                    // 完成了数据的初始化及建立Watcher
                    new Watcher(vm, node, name, 'value');
                }
            }
        }
        // 节点类型为text
        if(node.nodeType === 3) {
            if(reg.test(node.nodeValue)) {
                // 获取匹配到的字符串
                var name = RegExp.$1;           
                name = name.trim();
                // node.value = vm[name];       // 将data的值赋给该node
                // 完成了数据的初始化及建立Watcher
                new Watcher(vm, node, name, 'nodeValue');
            }
        }
    }
}

解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者(Wathcer),一旦数据有变动,收到通知,更新视图。

Watcher

  1. 在自身实例化的同时往属性订阅器(Dep)中添加自己;
  2. 自身有一个update()方法
  3. 待属性重新赋值时,执行 set() => Dep.notify() => Watcher.update() 更新视图
    function Watcher (vm, node, name, type) {
    Dep.target = this;
    this.name = name;
    this.node = node; 
    this.vm = vm;
    this.type = type;
    this.update();
    Dep.target = null;  // 消除当前属性
    }
    Watcher.prototype = {
    get: function () {
        // 调用this.value的同时,会调用Ojbect.defineProperty.get(),同时将Dep.target添加到dep
        this.value = this.vm[this.name];
    },
    // 订阅者执行相应操作
    update: function () {
        // 触发属性的getter
        this.get();
        // 更新View
        this.node[this.type] = this.value;  
    }
    }

Dep

function Dep () {
    this.subs = [];
}
Dep.prototype = {
    // 添加订阅者
    addSub: function (sub) {
        this.subs.push(sub);
    },
    // 通知订阅者更新
    notify: function () {
        // 通知对应的Watcher调用update()
        this.subs.forEach(sub => sub.update());
    }
}

代码实现来源自网络


Vue整体流程

1. 解析模板成render函数

2. 响应式开始监听

3. 首次渲染,显示页面,且绑定依赖

4. data属性变化