Vue知识点整理

202 阅读3分钟

一、 vue双向绑定原理

实现mvvm主要包含两个方面,数据变化更新视图,视图变化更新数据。
视图更新数据很容易实现,可以通过事件监听即可,比如input标签监听 'input' 事件就可以实现了。 所以我们着重来分析vue的响应原理,即当数据改变,如何更新视图?

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

以下只放部分代码,完整代码请戳 -> github.com/liaoyali/si…

实现过程:

1.实现一个Observer

1)Observer是一个数据监听器,实现它的核心方法就是利用Object.defineProperty( )。一般我们通过递归方法遍历所有属性值,从而对所有属性都进行监听,然后利用Object.defineProperty()对每个属性进行处理。

2)由于有很多个订阅者,所以我们还需要创建一个可以容纳订阅者的消息订阅器Dep,订阅器Dep主要负责收集订阅者

3)当属性变化的时候,执行set里的dep.notify()去通知相应的订阅者让他们执行对应的更新函数update()。

observer.js:

function Observer(data) {
    this.data = data;
    this.walk(data);
}

Observer.prototype = {
    walk: function(data) {
        Object.keys(data).forEach(function(key) {
            this.defineReactive(data, key, data[key]);
        }.bind(this));
    },
    defineReactive: function(data, key, val) {
        observe(val); // 递归遍历所有子属性
        let dep = new Dep();
        Object.defineProperty(data, key, {  // 1) 数据劫持
            enumerable: true,
            configurable: true,
            get: function() {
                if (Dep.target) {
                    dep.addSub(Dep.target); // 2.2) 在这里添加一个订阅者
                }
                return val;
            },
            set: function(newVal) {
                if (val === newVal) {
                    return;
                }
                val = newVal;
                console.log('属性' + key + '已经被监听了,现在值为:“' + newVal.toString() + '”');
                dep.notify(); // 3.1) 如果数据变化,通知所有订阅者
            }
        });
    }
}

function observe(data) {
    if (!data || typeof data !== 'object') {
        return;
    }
    return new Observer(data);
};

function Dep() {  // 2.1) 订阅器Dep
    this.subs = [];
}

Dep.prototype = {
    addSub: function(sub) {
        this.subs.push(sub);
    },
    notify: function() {  // 3.2) 通知订阅者去更新
        this.subs.forEach(function(sub) {
            sub.update();  
        })
    }
}

Dep.target = null;

2. 实现watcher

1)订阅者watcher在初始化的时候需要把自己添加进订阅器Dep中
之前在实现Observer的时候,在get函数里已具有添加订阅者watcher的操作,所以我们只要在订阅者watcher初始化的时候去触发对应的get函数从而执行添加订阅者即可(只要获取对应的属性值就可以被触发)。

2)自身还需要实现 update()方法,当observer.js监听到属性发生改变被通知更新的时候就执行watcher.js的update()从而更新

watcher.js:

function Watcher(vm, exp, cb) {
    this.cb = cb;
    this.vm = vm;
    this.exp = exp;
    this.value = this.get(); // 将自己添加到订阅器的操作
}

Watcher.prototype = {
    get: function() {
        Dep.target = this; //缓存自己
        let value = this.vm.data[this.exp]; //强制执行监听器里的get函数      只要获取value就可以触发observer里的get
        Dep.target = null; //释放自己
        return value;
    },
    update: function() {
        this.run();
    },
    run: function() {
        let value = this.vm.data[this.exp]; //获取的新值
        let oldVal = this.value; //旧值
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value);
        }
    },
}

3. 实现compile

observer和watcher已设置完成,需要再实现一个解析器compiler去做解析和绑定工作,
1.解析模板指令(比如{{name}}),并替换模板数据,初始化视图;

2.将模板指令对应的节点绑定对应的更新函数,初始化(new Watcher)相应的订阅器watcher。

compile.js部分代码:

function Compile(el, vm) {
    this.vm = vm;
    this.el = document.querySelector(el);
    this.fragment = null;
    this.init();
}

Compile.prototype = {
    init: function() {
        if (this.el) {
            this.fragment = this.nodeToFragment(this.el);
            this.compileElement(this.fragment);
            this.el.appendChild(this.fragment);
        } else {
            console.log('Dom元素不存在');
        }
    },
    nodeToFragment: function(el) {
        let fragment = document.createDocumentFragment();
        let child = el.firstChild;
        while (child) {
            // 将所有dom元素移入片段fragment
            fragment.appendChild(child);
            child = el.firstChild
        }
        return fragment;
    },
    <!--匹配模板指令-->
    compileElement: function(el) {
        let childNodes = el.childNodes;
        let that = this;
        [].slice.call(childNodes).forEach(function(node) {
            let reg = /\{\{\s*(.*?)\s*\}\}/;
            let text = node.textContent;

            if (that.isElementNode(node)) {
                that.compile(node);
            } else if (that.isTextNode(node) && reg.test(text)) {
                // 判断是否是符合资助形式{{}}的指令
                that.compileText(node, reg.exec(text)[1]);
            }
            if (node.childNodes && node.childNodes.length) {
                that.compileElement(node); // 继续递归遍历子节点
            }
        })
    },
    compile: function(node) {
        let nodeAttrs = node.attributes;
        Array.prototype.forEach.call(nodeAttrs, function(attr) {
            let attrName = attr.name;
            if (this.isDirective(attrName)) {
                let exp = attr.value;
                let dir = attrName.substring(2);
                if (this.isEventDirective(dir)) { // 事件指令
                    this.compileEvent(node, this.vm, exp, dir);
                } else { // v-model 指令
                    this.compileModel(node, this.vm, exp, dir);
                }
                node.removeAttribute(attrName);
            }
        }.bind(this));
    },
    <!--监听input输入-->
        node.addEventListener('input', function(e) {  
            var newValue = e.target.value;
            if (val === newValue) {
                return;
            }
            self.vm[exp] = newValue;
            val = newValue;
        });
    },
    <!--生成订阅器watcher-->
    compileText: function(node, exp) {
        let initText = this.vm[exp];
        this.updateText(node, initText); // 将初始化的数据初始化到视图中
        new Watcher(this.vm, exp, function(value) {
            // 生成订阅器并绑定更新函数
            this.updateText(node, value);
        }.bind(this));
    },
    ...

参考自canfoo
附上vue源码介绍电子书