美文网首页
vue双向绑定原理及实现

vue双向绑定原理及实现

作者: moofyu | 来源:发表于2020-03-12 14:12 被阅读0次

vue是采用数据劫持配合发布者-订阅模式的方式,通过Object.definePropery()来劫持各个属性的settergetter,在数据变动时发布消息给消息订阅器-Dep,通知订阅者-Watcher,触发相应回调函数,去更新视图。

vue创建实例的时候,MVVM作为绑定的入口,整合Observer, CompileWatcher三种,通过Observer来监听model数据变化,通过Compile解析编译模板指令,最终利用Watcher搭起了Observer, Compile之前的通信桥梁,达到数据变化=>视图更新;视图交互变化=>数据model变更的双向绑定效果。

数据双向绑定流程图

实现 Observer

  • Observer是一个数据监听器,用来劫持监听所有属性,如果有变动,就通知订阅者
  • 核心方法是用Object.defineProperty(),递归遍历所有属性,给每个属性加上settergetter,当给对象的某个属性赋值,就会触发 setter, 那么就能监听到了数据变化。

怎么通知订阅者?消息订阅器(Dep)-调度中心

  • 需要实现一个消息订阅器-Dep,来收集所有订阅者-Watcher
  • Observer中植入消息订阅器-Dep
  • 数据变动触发Dep的notify,再调用订阅者的update方法
  • 订阅器Depwatcher的方法放在Observer的getter里面(原因看watcher实现)

如下代码,实现了一个Observer

//实现一个`Observer`对象
class Observer{
    constructor(data){
        this.observe(data);
    }
    // data是一个对象,可能嵌套其它对象,需要采用递归遍历的方式进行观察者绑定
    observe(data){
        if(data && typeof data === 'object'){
            Object.keys(data).forEach(key =>{
                this.defineReactive(data, key, data[key]);
            })
        }
    }
    // 通过 object.defineProperty方法对对象属性进行劫持
    defineReactive(obj, key, value){
        // 递归观察
        this.observe(value);
        const dep = new Dep();
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: false,
            get(){
                //初始化
                if (Dep.target) {// 判断是否需要添加订阅者
                    dep.addWatcher(Dep.target); // 在这里添加一个订阅者
                }
                return value;
            },
            // 采用箭头函数在定义时绑定this的定义域
            set: (newVal)=>{
                if(newVal !== value){
                    console.log('哈哈哈,监听到值变化了 ', val, ' --> ', newVal);
                    this.observe(newVal);
                    value = newVal;
                    dep.notify(); // 如果数据变化,通知所有订阅者
                }
            }
        })
    }
}

// Dep类存储watcher对象,并在数据变化时通知订阅者
class Dep{
    constructor(){
        this.watcherCollector = [];
    }
    // 添加watcher
    addWatcher(watcher){
        console.log('观察者', this.watcherCollector);
        this.watcherCollector.push(watcher);
    }
    // 数据变化时通知watcher更新
    notify(){
        this.watcherCollector.forEach(w=>w.update());
    }
}

实现Watcher

订阅者Watcher在初始化的时候需要将自己添加进订阅器Dep中,那该如何添加呢?

  1. 在监听器Observergetter函数中执行添加订阅者Watcher的操作
  2. 只要在订阅者Watcher初始化的时候触发对应的getter函数去执行添加订阅者操作即可(只要获取对应的属性值就可以触发了)
  3. 只有在订阅者Watcher初始化的时候才需要添加订阅者,所以需要做一个判断操作,因此可以在订阅器上做一下手脚:在Dep.target上缓存下订阅者,添加成功后再将其去掉就可以了

订阅者Watcher的实现如下:

class Watcher{
    // 通过回调函数实现更新的数据通知到视图
    constructor(expr, vm, cb){
        this.expr = expr;
        this.vm = vm;
        this.cb = cb;
        this.oldVal = this.getOldVal();
    }
    // 获取旧数据
    getOldVal(){
        // 在利用getValue获取数据调用getter()方法时先把当前观察者挂载
        Dep.target = this;// 缓存自己
        const oldVal = compileUtil.getValue(this.expr, this.vm);
        // 挂载完毕需要注销,防止重复挂载 (数据一更新就会挂载)
        Dep.target = null;
        return oldVal;
    }
    // 通过回调函数更新数据
    update(){
        const newVal = compileUtil.getValue(this.expr, this.vm);
        if(newVal !== this.oldVal){
            this.cb(newVal);
        }
    }
}

至此,监听器-Observer、消息订阅器-Dep、订阅者-Watcher的实现,已经具备了监听数据和数据变化通知订阅者的功能。那么接下来就是实现Compile了。

实现Compile

  • 指令解析器:解析模板指令,并替换模板数据,初始化视图
  • 在编译工具中绑定Watcher:将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图,如图所示:
//Complier编译类设计

const compileUtil = {
    getValue(expr, vm){
        // 处理 person.name 这种对象类型,取出真正的value
        return expr.split('.').reduce((data,currentVal)=>{
            return data[currentVal];
        }, vm.$data)
    },
    setVal(expr, vm, inputValue){
        expr.split('.').reduce((data,currentVal)=>{
            data[currentVal] = inputValue;
        }, vm.$data)
    },
    text(node, expr, vm) {
        let value;
        if(expr.indexOf('{{')!==-1){
            value = expr.replace(/\{\{(.+?)\}\}/g, (...args)=>{
                 // text的 Watcher应在此绑定,因为是对插值{{}}进行双向绑定
                // Watcher的构造函数的 getOldVal()方法需要接受数据或者对象,而{{person.name}}不能接收
                new Watcher(args[1], vm, ()=>{
                    this.updater.textUpdater(node, this.getContent(expr, vm));
                });
                return this.getValue(args[1], vm);
            });
        }else{
            value = this.getValue(expr, vm);
        }
        this.updater.textUpdater(node, value);  
    },
    html(node, expr, vm) {
        const value = this.getValue(expr, vm);
         // html对应的 Watcher
        new Watcher(expr, vm, (newVal)=>{
            this.updater.htmlUpdater(node, newVal);
        })
        this.updater.htmlUpdater(node, value);
    },
    model(node, expr, vm) {
        const value = this.getValue(expr, vm);
        // v-model绑定对应的 Watcher, 数据驱动视图:数据=>视图
        new Watcher(expr, vm, (newVal)=>{
            this.updater.modelUpdater(node, newVal);
        })
         // 视图 => 数据 => 视图
        node.addEventListener('input', (e)=>{
            this.setVal(expr, vm, e.target.value);
        })
        this.updater.modelUpdater(node, value);
    },
    on(node, expr, vm, detailStr) {
        let fn = vm.$options.methods && vm.$options.methods[expr];
        node.addEventListener(detailStr,fn.bind(vm), false);
    },
    bind(node, expr, vm, detailStr){
        // v-on:href='...' => href='...'
        node.setAttribute(detailStr, expr);
    },
    // 视图更新函数
    updater: {
        textUpdater(node, value) {
            node.textContent = value;
        },
        htmlUpdater(node, value){
            node.innerHTML = value;
        },
        modelUpdater(node, value){
            node.value = value;
        }
    }

}

// 编译HTML模版对象
class Compiler {
    constructor(el, vm) {
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        this.vm = vm;
        // 1. 将预编译的元素节点放入文档碎片对象中,避免DOM频繁的回流与重绘,提高渲染性能
        const fragments = this.node2fragments(this.el);
        // 2. 编译模版
        this.compile(fragments);
        // 3. 追加子元素到根元素
        this.el.appendChild(fragments);
    }
    compile(fragments) {
        // 1.获取子节点
        const childNodes = fragments.childNodes;
        // 2.递归循环编译
        [...childNodes].forEach(child => {
            // 如果是元素节点
            if (this.isElementNode(child)) {
                this.compileElement(child);
            } else {
                // 文本节点
                this.compileText(child);
            }
            //递归遍历
            if(child.childNodes && child.childNodes.length){
                this.compile(child);
            }
        })
    }
    compileElement(node) {
        let attributes = node.attributes;
        // 对于每个属性进行遍历编译
        // attributes是类数组,因此需要先转数组
        [...attributes].forEach(attr => {
            let {name,value} = attr; // v-text="msg"  v-html=htmlStr  type="text"  v-model="msg"
            if (this.isDirector(name)) { // v-text  v-html  v-mode  v-bind  v-on:click v-bind:href=''
                let [, directive] = name.split('-');
                let [compileKey, detailStr] = directive.split(':');
                // 更新数据,数据驱动视图
                compileUtil[compileKey](node, value, this.vm, detailStr);
                // 删除有指令的标签属性 v-text v-html等,普通的value等原生html标签不必删除
                node.removeAttribute('v-' + directive);
            }else if(this.isEventName(name)){
                // 如果是事件处理 @click='handleClick'
                let [, detailStr] = name.split('@');
                compileUtil['on'](node, value, this.vm, detailStr);
                node.removeAttribute('@' + detailStr);
            }

        })

    }
    compileText(node) {
        // 编译文本中的{{person.name}}--{{person.age}}
        const content = node.textContent;
        if(/\{\{(.+?)\}\}/.test(content)){
            compileUtil['text'](node, content, this.vm);
        }
    }
    isEventName(attrName){
        // 判断是否@开头
        return attrName.startsWith('@');
    }
    isDirector(attrName) {
        // 判断是否为Vue特性标签
        return attrName.startsWith('v-');
    }
    node2fragments(el) {
        // 创建文档碎片对象
        const f = document.createDocumentFragment();
        let firstChild;
        while (firstChild = el.firstChild) {
            f.appendChild(firstChild);
        }
        return f;
    }
    isElementNode(node) {
        // 元素节点的nodeType属性为 1
        return node.nodeType === 1;
    }
}

MVue入口类设计

Mvue类接收一个参数对象作为初始输入,然后利用Compiler类对模版进行编译及渲染、创建观察者,观察数据。


class MVue {
    constructor(options) {
        // 初始元素与数据通过options对象绑定
        this.$el = options.el;
        this.$data = options.data;
        this.$options = options;
        // 通过Compiler对象对模版进行编译,例如{{}}插值、v-text、v-html、v-model等Vue语法
        if (this.$el) {
            // 1. 创建观察者,利用Observer对象对数据进行劫持
            new Observer(this.$data);
            // 2. 编译模版
            new Compiler(this.$el, this);
            // 3. 通过数据代理实现 this.person.name = '子非鱼-cool'功能,而不是this.$data.person.name = '子非鱼-cool'
            this.proxyData(this.$data);
        }
    }
     //用vm代理vm.$data
     proxyData(data){
        for(let key in data){
            Object.defineProperty(this,key,{
                get(){
                    return data[key];
                },
                set(newVal){
                    data[key] = newVal;
                }
            })
        }
    }
}

相关文章

网友评论

      本文标题:vue双向绑定原理及实现

      本文链接:https://www.haomeiwen.com/subject/egwotctx.html