vue双向数据绑定原理与实现

vue双向数据绑定原理与实现

Object.defineProperty

语法:Object.defineProperty(obj,prop,descriptor)
obj:目标对象
prop:需要定义的属性和方法的名称
descriptor:目标属性所拥有的特性

第三个参数对应为对象,可供定义的属性列表
value:属性的值
writable:如果为false,属性的值就不能被重写。
get: 一旦目标属性被访问就会调回此方法,并将此方法的运算结果返回用户。
set:一旦目标属性被赋值,就会调回此方法。
configurable: 如果为false,则任何尝试删除目标属性或修改属性性以下特性(writable, configurable, enumerable)的行为将被无效化。
enumerable: 是否能在for…in循环中遍历出来或在Object.keys中列举出来

function text(){
        const object1 = {};

        Object.defineProperty(object1, 'property1', {
            value: 42,
            writable: false
        });

        object1.property1 = 77;
        // throws an error in strict mode

        console.log(object1.property1);
        // expected output: 42
    }
function text(){
        const object1 = {
            _property1: 42
        };

        Object.defineProperty(object1, 'property1', {
            get:function(){
                return object1._property1
            },
            set:function(newValue){
                this._property1 = newValue
                console.log("set"+newValue)
            }   
        });

        object1.property1 = 77;
        // throws an error in strict mode

        console.log(object1.property1);
        // expected output: 42
    }

注意在你的对象中(第一个参数),不要存在与需要定义的属性和方法的名称(第二个参数)相同的名称,否则会引起栈溢出(当你在对象声明同名属性,已经调用getset,在Object.defineProperty重复声明getset会引起栈溢出)

发布-订阅模式

发布—订阅模式又叫观察者模式,它定义了对象间的一种一对多的关系,让多个观察者对象同时监听某一个主题对象,当一个对象发生改变时,所有依赖于它的对象都将得到通知。

//发布者
    var pub = {
        publish: function(){
            dep.notify();
        }
    }
    //三个订阅者
    var sub1 = { update: function(){console.log(1)} }
    var sub2 = { update: function(){console.log(2)} }
    var sub3 = { update: function(){console.log(3)} }
    //主题对象
    function Dep(){
        this.subs = [sub1, sub2, sub3]
    }
    //主题对象的原型方法  让订阅者响应
    Dep.prototype.notify = function(){
        this.subs.forEach(function (sub) {
            sub.update();
        })
    }
    var dep = new Dep()
    //发布者发布消息 主题对象执行notify  触发订阅者执行update
    pub.publish()

原理

vue 双向数据绑定是通过 数据劫持 结合 发布订阅模式的方式来实现
数据和视图同步,数据发生变化,视图跟着变化,视图变化,数据也随之发生改变

实现简单的双向数据绑定

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>webpack-template</title>
</head>
<body>
    <div id="main-con">
        <input type="text" id="a">
        <span id="b"></span>
    </div>
</body>
<script>
    var obj = {}
    var val = 'hello'
    Object.defineProperty(obj,'val',{
        get: function(){
            return val
        },
        set: function(newValue){
            val = newValue
            document.getElementById('b').innerHTML = val
        }
    });

    document.addEventListener('keyup',function(e){
        obj.val = e.target.value
    })
</script>
</html>

DocuemntFragment(碎片化文档)

当每个节点都插入到文档当中都会引发一次浏览器的回流
浏览器的回流与重绘
回流必将引起重绘,重绘不一定会引起回流
首先
1.浏览器使用流式布局模型 (Flow Based Layout)。
2.浏览器会把HTML解析成DOM,把CSS解析成CSSOM,DOM和CSSOM合并就产生了Render Tree。
有了RenderTree,我们就知道了所有节点的样式,然后计算他们在页面上的大小和位置,最后把节点绘制到页面上。
3.由于浏览器使用流式布局,对Render Tree的计算通常只需要遍历一次就可以完成,但table及其内部元素除外,他们可能需要多次计算,通常要花3倍于同等元素的时间,这也是为什么要避免使用table布局的原因之一。
回流 (Reflow)
当Render Tree中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流。
重绘 (Repaint)
当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。

避免频繁操作DOM,提高浏览器性能
创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中。

内容绑定

function compile(node, vm){
        //正则 检测{{}}表达式  并且可通过()提取参数
        var reg = /\{\{(.*)\}\}/
        //判断元素节点
        if (node.nodeType === 1) {
            //返回该元素所有属性节点的一个实时集合 字符串形式的名/值对
            var attr = node.attributes;
            //遍历节点全部属性,看看那个属性绑定了data 
            for (let i = 0; i < attr.length; i++) {
                //指定属性绑定data 解析为h5
                if(attr[i].nodeName == 'v-model'){
                    //获得指定的变量名称
                    const name = attr[i].nodeValue;
                    //把data值赋给该node
                    node.value = vm.data[name]
                    //删除此属性
                    node.removeAttribute('v-model')   
                }
            }
        }
        //判断文本节点
        if(node.nodeType === 3){
            //判断是否符合{{}}表达式
            if(reg.test(node.nodeValue)){
                //通过RegExp.$1取得变量或者表达式
                var name = RegExp.$1
                //剔除空格
                name = name.trim()
                //把data值赋给该node
                node.nodeValue = vm.data[name]
            }
        }
    }
    
    //通过DocuemntFragment(碎片化文档)减少回流
    function nodeToFragment(node,vm){
        var fragment = document.createDocumentFragment();
        var child ;
        while (child = node.firstChild) {
            compile(child,vm)
            fragment.appendChild(child)
        }
        return fragment
    }

    //模拟vue
    function Vue (options){
        this.data = options.data
        var id = options.el
        var dom = nodeToFragment(document.getElementById(id),this)
        document.getElementById(id).appendChild(dom)
    }

    //vue实例
    var vm = new Vue({
        el: 'app',
        data: {
            text: 'hello'
        }
    })

view => model

//监听函数
    function defineReactive(obj, key, val){
        Object.defineProperty(obj, key, {
            set: function(newVal){
                val = newVal
                console.info('new',val)
            },
            get: function(){
                return val
            }
        })
    }

    //观察者
    function observe(obj,vm){
        //实现对实例的每一个属性都进行监听
        for(let key of Object.keys(obj)){
            defineReactive(vm, key, obj[key]);
        }
    }

    function compile(node, vm){
        //正则 检测{{}}表达式  并且可通过()提取参数
        var reg = /\{\{(.*)\}\}/
        //判断元素节点
        if (node.nodeType === 1) {
            //返回该元素所有属性节点的一个实时集合 字符串形式的名/值对
            var attr = node.attributes;
            //遍历节点全部属性,看看那个属性绑定了data 
            for (let i = 0; i < attr.length; i++) {
                //指定属性绑定data 解析为h5
                if(attr[i].nodeName == 'v-model'){
                    //获得指定的变量名称
                    const name = attr[i].nodeValue;
                    // //把data值赋给该node
                    // node.value = vm.data[name]
                    node.addEventListener('input',function(e){
                        vm[name] = e.target.value
                    }) 
                    node.value = vm[name]
                    //删除此属性
                    node.removeAttribute('v-model') 
                }
            }
        }
        //判断文本节点
        if(node.nodeType === 3){
            //判断是否符合{{}}表达式
            if(reg.test(node.nodeValue)){
                //通过RegExp.$1取得变量或者表达式
                var name = RegExp.$1
                //剔除空格
                name = name.trim()
                //把data值赋给该node
                // node.nodeValue = vm.data[name]
                node.value = vm[name]
            }
        }
    }

    //通过DocuemntFragment(碎片化文档)减少回流
    function nodeToFragment(node,vm){
        var fragment = document.createDocumentFragment();
        var child ;
        while (child = node.firstChild) {
            compile(child,vm)
            fragment.appendChild(child)
        }
        return fragment
    }

    //模拟vue
    function Vue (options){
        this.data = options.data
        var data = this.data
        observe(data, this)

        var id = options.el
        var dom = nodeToFragment(document.getElementById(id),this)
        document.getElementById(id).appendChild(dom)
    }

    //vue实例
    var vm = new Vue({
        el: 'app',
        data: {
            text: 'hello'
        }
    })

model => view

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>webpack-template</title>
</head>
<body>
    <div id="app">
        <input type="text" id="a" v-model="text">
        {{text}}
    </div>
</body>
<script>

    //模拟vue
    function Vue (options){
        this.data = options.data
        var data = this.data
        observe(data, this)

        var id = options.el
        var dom = nodeToFragment(document.getElementById(id),this)
        document.getElementById(id).appendChild(dom)
    }

    //观察者
    function observe(obj,vm){
        //实现对实例的每一个属性都进行监听
        for(let key of Object.keys(obj)){
            defineReactive(vm, key, obj[key]);
        }
    }

   //监听函数
   function defineReactive(obj, key, val){
       //为每一个属性生成一个主题对象
       var  dep = new Dep()
       //对每个data数据进行监听
        Object.defineProperty(obj, key, {
            set: function(newVal){
                val = newVal
                console.info('new',val)
                //当数据变化时 更新通知
                dep.notify()
            },
            get: function(){
                if(Dep.target){
                    dep.addSub(Dep.target)
                }
                return val
            }
        })
    }

    //dep构造函数  主题对象
    function Dep(){
        this.subs = []
    }
    //dep原型方法  
    Dep.prototype = {
        addSub(sub){
            this.subs.push(sub)
        },
        notify(){
            this.subs.forEach(function(sub) {
                sub.update();
            })
        }
    }


    //通过DocuemntFragment(碎片化文档)减少回流
    function nodeToFragment(node,vm){
        var fragment = document.createDocumentFragment();
        var child ;
        while (child = node.firstChild) {
            compile(child,vm)
            fragment.appendChild(child)
        }
        return fragment
    }

    //编译函数  解析为h5
    function compile(node, vm){
        //正则 检测{{}}表达式  并且可通过()提取参数
        var reg = /\{\{(.*)\}\}/
        //判断元素节点
        if (node.nodeType === 1) {
            //返回该元素所有属性节点的一个实时集合 字符串形式的名/值对
            var attr = node.attributes;
            //遍历节点全部属性,看看那个属性绑定了data 
            for (let i = 0; i < attr.length; i++) {
                //指定属性绑定data 解析为h5
                if(attr[i].nodeName == 'v-model'){
                    //获得指定的变量名称
                    const name = attr[i].nodeValue;
                    // //把data值赋给该node
                    // node.value = vm.data[name]
                    node.addEventListener('input',function(e){
                        vm[name] = e.target.value
                    }) 
                    node.value = vm[name]
                    //删除此属性
                    node.removeAttribute('v-model') 
                }
            }
        }
        //判断文本节点
        if(node.nodeType === 3){
            //判断是否符合{{}}表达式
            if(reg.test(node.nodeValue)){
                //通过RegExp.$1取得变量或者表达式
                var name = RegExp.$1
                //剔除空格
                name = name.trim()
                //把data值赋给该node
                // node.nodeValue = vm.data[name]
                // node.value = vm[name]
                //会为每个与数据绑定相关的节点生成一个订阅者 watcher,watcher 会将自己添加到相应属性的 dep 容器中
                new Watcher(vm, node, name)
            }
        }
    }


    //Watcher构造函数  监听者
    function Watcher(vm, node, name){
        //将自己赋给了一个全局变量 Dep.target
        Dep.target = this
        //获得vue实例
        this.vm = vm
        //获得节点
        this.node = node
        //获得属性名
        this.name = name
        //实例化时执行update
        this.update()
        Dep.target = null
    }

    //Watcher原型方法
    Watcher.prototype = {
        update(){
            //通过update 执行get
            this.get()
            //更改节点内容
            this.node.nodeValue = this.value
        },
        //get 的方法读取了 vm 的访问器属性
        //从而触发了访问器属性的 get 方法,get 方法中将该 watcher 添加到了对应访问器属性的 dep 中
        get(){
            this.value = this.vm[this.name]
        }
    } 

    //vue实例
    var vm = new Vue({
        el: 'app',
        data: {
            text: 'hello'
        }
    })
</script>
</html>

这样我们基本实现了vue的双向绑定,光看有点难以理解,建议大家自己敲一遍。

源码借鉴于:https://www.jianshu.com/p/e7ebb1500613

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值