美文网首页
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