Vue双向绑定

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的双向绑定思想

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • 我的github: vue双向绑定原理 MVC模式 以往的MVC模式是单向绑定,即Model绑定到View,当我们...
    KlausXu阅读 44,539评论 7 91
  • 前言 在之前面试中,有被问到这个问题,虽然了解过是劫持Object.defineProperty方法,但是其细节并...
    Aleph_Zheng阅读 1,041评论 0 5
  • 思路 vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()...
    IT小孢子阅读 281评论 0 1
  • 1、双向绑定 vue中一个很大的特点就是可以通过v-model创建双向数据绑定,按照官方文档的说法:v-model...
    七_五阅读 319评论 0 0
  • 春风十里吹皱春水初生 粼粼的水荡漾着春波万顷 温润流淌的碧玉 盈盈的笑意脉脉 仿佛伊人在水一方 寒冰偎依碧潭 羞涩...
    莺溪蝶语阅读 184评论 2 4