美文网首页
Vue双向绑定

Vue双向绑定

作者: silly鸿 | 来源:发表于2018-02-27 20:40 被阅读0次

Vue双向绑定

Obejct.definePropertysetter/getter和发布订阅

Vue双向绑定原理

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

Vue双向绑定实现

1.实现Observer

  • 利用Obeject.defineProperty()来监听属性变动
  • 需要将observer的数据进行递归遍历,包括子属性对象的属性,都加上settergetter
  • 给某个对象赋值,就会触发setter,那么就能监听到数据变化,通过notify()发布出去
    function observer(data, vm){
        if(typeof data !== 'object') return

        Object.keys(data).forEach(function(key){
            defineReactive(vm, key, data[key])
        })
    }

    function defineReactive(obj, key ,val){
        var dep = new Dep()
        Object.defineProperty(obj, key, {
            get: function(){
                // alert('属性监听 get '+Dep.target)
                // // Watcher的实例调用了getter 添加订阅者watcher
                if(Dep.target) dep.addSub(Dep.target)
                    return val
            },
            set: function(newVal){
                // alert('属性监听 set'+newVal)
                if(newVal === val){
                    return
                }else{
                    val = newVal
                    //作为发布者发出通知
                    dep.notify()
                }
            }
        })
    }

这样我们已经可以监听每个数据的变化了,那么监听到变化之后就是怎么通知订阅者了,所以接下来我们需要实现一个消息订阅器,很简单,就是一个数组用来收集订阅者,数据变动触发notify,再调用订阅者的update方法

    function Dep(){
        //定义subs数组存储watcher
        this.subs = []
    }

    Dep.prototype.addSub = function(sub){
        this.subs.push(sub)
    }

    Dep.prototype.notify = function(){
        this.subs.forEach(function(sub){
            sub.update()
        })
    }

2.实现Compile

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


    function compile(node, vm){
        //指令 v- 模板引擎 {{}}
        var reg = /\{\{(.*)\}\}/;
        // 判断节点类型 nodeType  元素1  文本3
        if(node.nodeType === 1){
            var attr = node.attributes
            for(var i = 0; i < attr.length; i++){
                if(attr[i].nodeName == 'v-model'){
                    var name = attr[i].nodeValue//获取v-model绑定的属性名
                    // console.log(name)//text
                    node.addEventListener('input', function(e){
                        console.log(vm)
                        vm[name] = e.target.value
                    })
                    // 给相应的data属性赋值,触发该属性的set方法
                    node.value = vm.data[name]  //将data值赋值给node
                    node.removeAttribute('v-model')

                }
            }
        }

        //节点类型是文本
        if(node.nodeType === 3){
            if(reg.test(node.nodeValue)){
                var name = RegExp.$1 //获取过来{{text}} =>text
                name = name.trim()//trim
                // node.nodeValue = vm.data[name] //将data.text => {{text}}
                new Watcher(vm, node, name); //这里改成订阅者形式,
            }
        }

    }

3.实现Watcher

Watcher订阅者为ObserverCompile之间通信的桥梁,主要做的事

  • Compile中实例化时
  • 往属性订阅器(dep)里添加在自己
  • 自身必须有一个update()方法
  • 当数据变动是接到dep属性订阅器的notify发布通知时,能够调用自身的update()方法,从而触发get方法去更新数据
    //定义wactch
    function Watcher(vm, node, name){
        // console.log(this)
        this.vm = vm
        this.node = node
        this.name = name
        this.update()
    }

    Watcher.prototype = {
        update: function () {
            this.get();
            this.node.nodeValue = this.value;
        },
        // 获取data中的属性值 加入到dep中
        get: function () {
            Dep.target = this
            this.value = this.vm[this.name]; 
            Dep.target = null

        }
    }

Vue双向绑定效果

vue双向绑定

完整代码

HTML

    <div id='app'>
        <input v-model='text'>
        {{ text }}
        <span></span>
    </div>

<script src="vue.0.0.1.js"></script>
<script >
var app = new Vue({
    el: 'app',
    data: {
        text: "this is test text",
        message: {
            name: "weiong"
        }
    }
})

JS

'use strict';
(function(global, factory){
    global.Vue = factory();
})(this, function(){
    var Vue = function(options){
        //挂载的节点
        var id = options.el||"body";
        // console.log(id)

        //模板数据
        this.data = options.data || {}
        var data = this.data

        //监听模板数据
        observer(data, this)

        //节点劫持到一个DOM容器
        var dom  = nodeTodocumentfragment(document.getElementById(id), this)
        //最后挂载
        document.getElementById(id).appendChild(dom)
    }


    function Dep(){
        //定义subs数组存储watcher
        this.subs = []
    }

    Dep.prototype.addSub = function(sub){
        this.subs.push(sub)
    }

    Dep.prototype.notify = function(){
        this.subs.forEach(function(sub){
            sub.update()
        })
    }

    //定义wactch
    function Watcher(vm, node, name){
        // console.log(this)
        this.vm = vm
        this.node = node
        this.name = name
        this.update()
    }

    Watcher.prototype = {
        update: function () {
            // alert('Watcher update')
            this.get();
            this.node.nodeValue = this.value;
        },
        // 获取data中的属性值 加入到dep中
        get: function () {
            // alert('Watcher get')
            Dep.target = this

            this.value = this.vm[this.name]; // 触发相应属性的getter,从而添加订阅者
            Dep.target = null

        }
    }

    //数据监听 obj是data对象
    function observer(data, vm){
        if(typeof data !== 'object') return

        Object.keys(data).forEach(function(key){
            defineReactive(vm, key, data[key])
        })
    }

    function defineReactive(obj, key ,val){
        var dep = new Dep()
        Object.defineProperty(obj, key, {
            get: function(){
                // alert('属性监听 get '+Dep.target)
                // // Watcher的实例调用了getter 添加订阅者watcher
                if(Dep.target) dep.addSub(Dep.target)
                    return val
            },
            set: function(newVal){
                // alert('属性监听 set'+newVal)
                if(newVal === val){
                    return
                }else{
                    val = newVal
                    //作为发布者发出通知
                    dep.notify()
                }
            }
        })
    }

    //节点劫持
    //documentfragment DOM 容器
    function nodeTodocumentfragment(obj, vm){
        var flag = document.createDocumentFragment()
        var child
        // appendChild 成功后,会把节点从原来的节点位置移除;
        // 中转站
        while(child = obj.firstChild){
            // console.log(child)
            // 扫描 节点劫持  model数据模板编译
            compile(child, vm)
            flag.appendChild(child)
        }
        // console.log(flag)
        return flag
    }


    // compile扫描每一个子节点
    function compile(node, vm){
        //指令 v- 模板引擎 {{}}
        var reg = /\{\{(.*)\}\}/;
        // 判断节点类型 nodeType  元素1  文本3
        if(node.nodeType === 1){
            var attr = node.attributes
            for(var i = 0; i < attr.length; i++){
                if(attr[i].nodeName == 'v-model'){
                    var name = attr[i].nodeValue//获取v-model绑定的属性名
                    // console.log(name)//text
                    node.addEventListener('input', function(e){
                        console.log(vm)
                        vm[name] = e.target.value
                    })
                    // 给相应的data属性赋值,触发该属性的set方法
                    node.value = vm.data[name]  //将data值赋值给node
                    node.removeAttribute('v-model')
                    // alert('节点赋值')

                }
            }
        }


        //节点类型是文本
        if(node.nodeType === 3){
            if(reg.test(node.nodeValue)){
                var name = RegExp.$1 //获取过来{{text}} =>text
                name = name.trim()//trim
                // node.nodeValue = vm.data[name] //将data.text => {{text}}
                // alert('文本赋值 new Watcher')
                new Watcher(vm, node, name); //这里改成订阅者形式,
            }
        }

    }


    return Vue
})


参考连接
剖析Vue原理&实现双向绑定MVVM
vue的双向绑定思想

相关文章

网友评论

      本文标题:Vue双向绑定

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