zoukankan      html  css  js  c++  java
  • vue2.0数据双向绑定原理分析及代码实现

    vue采用数据劫持结合发布者订阅者模式的方式,通过es5中Object.defineproperty()来劫持各个属性的setter、getter,在数据变动时发布消息给依赖收集器,去通知观察者,触发响应回调,去更新视图。

    将以上的描述用以下的图来展示:

             

    实现分析,具体步骤:

    第一步:需要Observer的数据对象进行递归遍历,包括子属性对象的属性,都加上setter和getter,这样的话,给这个对象的某个值赋值,就会出发setter,那么就能监听到了数据变化

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

    第三步:watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是:

      1.在自身实例化时往属性订阅器(Dep)里面添加自己

      2.自身必须有一个update()方法

      3.待属性变动Dep.notice()通知时,能调用自身的Update()方法,并触发compile中绑定的回调

    第四步:MVVM作为数据绑定的入口,整合Observer、Compile、Watcher三者,通过Observer来监听自己的model数据变化,通过Complie来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的桥梁通信,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果

     
    Complie 和 Watcher
      在哪里绑定关联两者: 在编译v-html等时创建Watcher,添加Watcher
    Observer 和 Dep
      在哪里关联两者:在Observer获取数据的时候将Dep和Observer关联;
      在Observer数据更改时,告诉Dep通知变化

    1.Compile

    class Compile{
        constructor(el,vm){
            this.el = this.isElementNode(el) ? el : document.querySelector(el);
            this.vm = vm;
            // 1. 获取文档碎片对象,放入内存中会减少页面的回流和重绘
            const fragment = this.node2Fragment(this.el)
            // 2. 编译模板
            this.compile(fragment)
            // 3. 追加子元素到根元素
            this.el.appendChild(fragment)
        }
        // 判断是不是元素节点,元素节点nodeType为1
        isElementNode(node){
            return node.nodeType === 1;
        }
        // 获取文档碎片对象
        node2Fragment(el){
            // 创建文档碎片对象
            const f = document.createDocumentFragment()
            let firstChild;
            while(firstChild = el.firstChild){
                f.appendChild(firstChild);
            }
            return f
        }
        // 编译模板:渲染页面元素
        compile(fragment){
            const childNodes = fragment.childNodes;
            [...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){
            const attributes = node.attributes;
            [...attributes].forEach(attr => {
                const {name, value} = attr;
                if(this.isDirective(name)){
                    const [,directive] = name.split('-');
                    const [dirName, eventName] = directive.split(':');
                    // 更新视图
                    compileUtil[dirName](node, value, this.vm, eventName)
                    // 删除有指令的标签上的属性
                    node.removeAttribute('v-'+ directive);
                    if(directive === 'model'){
                        node.setAttribute('value',compileUtil.getVal(value, this.vm));
                    }
                }else if(this.isEventName(name)){
                    // 处理@click事件
                    let [,eventName] = name.split('@')
                    compileUtil['on'](node, value, this.vm, eventName)
                    // 删除有指令的标签上的@事件
                    node.removeAttribute('@'+ eventName);
                }else if(this.isAttr(name)){
                    // 处理:属性
                    let [,attrName] = name.split(':')
                    compileUtil['bind'](node, value, this.vm, attrName)
                    // 删除有指令的标签上的:属性
                    node.removeAttribute(':'+ attrName);
                }
            });
    
        }
        // 编译文本节点
        compileText(node){
            const content = node.textContent;
            if(/{{(.+?)}}/.test(content)){
                compileUtil['text'](node,content,this.vm)
            }
        }
        // 判断是不是v-开头指令
        isDirective(attrName){
            return attrName.startsWith('v-');
        }
        // 判断是不是事件绑定简写@
        isEventName(attrName){
            return attrName.startsWith('@');
        }
        // 判断是不是属性绑定:
        isAttr(attrName){
            return attrName.startsWith(':');
        }
    }

    2.Compile工具集和Watcher

    // 编译指令的方法集
    const compileUtil = {
        // 获取value的值
        getVal(exp,vm){
            return exp.split('.').reduce((data,currentVal) => {
                return data[currentVal]
            },vm.$data)
        },
        getContentVal(exp,vm){
            return  exp.replace(/{{(.+?)}}/g, (...args) => {
                return this.getVal(args[1],vm)
            })
        },
        setVal(exp,vm,inputVal){
            return exp.split('.').reduce((data,currentVal) => {
                data[currentVal] = inputVal
            },vm.$data)
        },
        // 处理v-text 和插值表达式{{}}
        text(node,exp,vm){
            let value;
            // 对v-text和插值表达式做区分处理
            if(exp.indexOf('{{') !== -1){
                // ????????????
                value = exp.replace(/{{(.+?)}}/g, (...args) => {
                    // 绑定观察者,将来数据发生变化 触发这里的回调 进行更新
                    new Watcher(vm,args[1], () =>{
                        this.updater.textUpdate(node,this.getContentVal(exp,vm))
                    })
                    return this.getVal(args[1],vm)
                })
            }else{
                value = this.getVal(exp, vm)
            }
            this.updater.textUpdate(node,value)
        },
        // 处理v-html
        html(node,exp,vm){
            const value = this.getVal(exp, vm)
            // 添加数据观察者更新数据
            //1. Complie和updater关联,添加数据观察者
            new Watcher(vm,exp,(newVal) =>{
                this.updater.htmlUpdate(node,newVal)
            })
            // 初始话更新视图
            this.updater.htmlUpdate(node,value)
        },
        // 处理v-model 文本框
        model(node,exp,vm){
            const value = this.getVal(exp, vm)
            // 绑定更新函数,数据=》视图
            new Watcher(vm,exp,(newVal) =>{
                this.updater.modelUpdate(node,newVal)
            })
            // 视图 =》数据=》视图
            node.addEventListener('input', (e) =>{
                // 设置值
                this.setVal(exp,vm,e.target.value)
            })
            this.updater.modelUpdate(node,value)
        },
        // 处理事件绑定:v-bind和简写@
        on(node,exp,vm,eventName){
            let fn = vm.$options.methods && vm.$options.methods[exp]
            node.addEventListener(eventName,fn.bind(vm),false)
        },
        // 处理事件绑定: v-bind和简写:
        bind(node,exp,vm,attrName){
            const value = this.getVal(exp, vm)
            this.updater.attrUpdate(node,attrName,value)
        },
        // 更新视图(没有抽离出去)
        updater:{
            // text视图更新,设置text
            textUpdate(node,value){
                node.textContent = value
            },
            // html视图更新,设置innerHTML
            htmlUpdate(node,value){
                node.innerHTML = value
            },
            // model视图更新,设置文本框的value值
            modelUpdate(node,value){
                node.value = value
            },
            // 属性值视图更新,设置元素的属性值
            attrUpdate(node,attrName,value){
                node.setAttribute(attrName,value)
            }
        }
    }

    3.Observer

    class Observer{
        constructor(data){
            this.observer(data)
        }
        observer(data){
            if(data && typeof data === 'object'){
                Object.keys(data).forEach( key => {
                    this.defineReactive(data,key,data[key])
                })
            }
        }
        defineReactive(obj,key,value){
            // 递归遍历
            this.observer(value);
            // 实例化Dep
            const dep = new Dep()
            // 劫持并监听所有的属性
            Object.defineProperty(obj,key,{
                enumerable:true,
                configurable:false,
                // 初始化时(编译之前)
                get(){
                    // 订阅数据变化时,往dep中添加观察者
                    // 获取数据的时候,将Dep 和 Observer关联
                    Dep.target && dep.addSub(Dep.target);
                    return value
                },
                set:(newVal) => {
                    this.observer(newVal)
                    if(newVal !== value){
                        value = newVal
                    }
                    // 告诉Dep通知变化
                    dep.notify()
                }
            })
        }
    }

    4.Dep

    class Dep{
        constructor(){
            this.subs = []
        }
        // 添加观察者
        addSub(watcher){
            this.subs.push(watcher)
        }
        // 通知观察者去更新视图
        notify(){
            console.log('通知观察者',this.subs)
            this.subs.forEach( w => {
                // console.log(w.update)
                w.update()
            })
        }
    }

    5.Watcher

    class Watcher{
        constructor(vm,exp,cb){
            this.vm = vm;
            this.exp = exp;
            this.cb = cb;
            // 先把旧值保存起来
            this.oldVal = this.getOldVal()
        }
        getOldVal(){
            // 在拿到旧数据之前将Watcher和Dep关联起来
            Dep.target = this
            const oldVal = compileUtil.getVal(this.exp,this.vm)
            // 在拿到旧数据之后将观察者和dep关联取消,避免生成无数的watcher
            Dep.target = null
            return oldVal
        }
        update(){
            const newVal = compileUtil.getVal(this.exp,this.vm)
            if(newVal !== this.oldVal){
                this.cb(newVal)
            }
        }
    }

    6.Mvue

    class MVue{
        constructor(options){
            this.$el = options.el;
            this.$data = options.data;
            this.$options = options;
            if(this.$el){
                // 实现数据观察者
                new Observer(this.$data)
                // 实现指令解析器
                new Compile(this.$el, this)
                // 代理
                this.proxyData(this.$data)
            }
        }
        proxyData(data){
            for(const key in data){
                Object.defineProperty(this, key,{
                    get(){
                        return data[key]
                    },
                    set(){
                        data[key] = newVal
                    }
                })
            }
        }
    }

    7.index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
        <script src="./Mvue.js"></script>
        <script src="./Observer.js"></script>
    </head>
    <body>
        <div id="app">
            <h2>{{person.name}}</h2>
            <h2>{{person.name}} -- {{person.age}}</h2>
            <ul>
                <li>{{msg}}</li>
                <li v-text='person.name'></li>
                <li v-text='msg'></li>
                <li v-html='htmlStr'>2</li>
                <li v-html='person.htmlStr'></li>
            </ul>
            <img v-bind:src="img" alt=" ">
            <img :src="img" alt=" ">
            <div>
                <a :href="aLink">百度</a>
                <a v-bind:href="aLink">百度</a>
            </div>
            <input v-model='msg' @click='clickFun' />
            <button v-on:click='clickFun'>点击事件</button>
        </div>
        <script>
            let vm new MVue({
                el: '#app',
                data:{
                        person:{
                            name: 'hzz',
                            age: 18,
                            fav: 'code',
                            htmlStr: '<h2>这是个html片段person</h2>',
                        },
                        msg: '这是消息',
                        htmlStr: '<h2>这是个html片段</h2>',
                        img:'https://dss3.baidu.com/-rVXeDTa2gU2pMbgoY3K/it/u=3139850293,1719705775&fm=202&src=608&crossm&mola=new&crop=v1',
                        aLink:'https://www.baidu.com'
                },
                methods:{
                    clickFun(){
                        console.log(this)
                    }
                }
            })
        </script>
        
    </body>
    </html>

    页面应用展示组成

      1.Mvue.js文件包含(编译指令方法集、complie、Mvue)

      2.observer.js文件中包含 (Watcher、Dep)

      3.index.html

  • 相关阅读:
    ie6不支持label
    IE6下li会继承ul属性的bug、产生条件、解决办法
    玉树地震与汶川地震
    IE6给png图片添加透明级别
    使用Float布局容器高度出错的决办法
    CSS冒泡窗口,有机会改成js的
    沁园春《房》
    乱接电话的笑话~
    禁止使用英文及其缩写?
    jQuery
  • 原文地址:https://www.cnblogs.com/wcx-20151115-hzz/p/14986273.html
Copyright © 2011-2022 走看看