实现一个小目标:完成一个简单的双向绑定

一个简易版的双向绑定

话不多说,代码实现如下:

<body>
    <input type="text" id='a'>
    <p id = 'b'></p>
    <script type="text/javascript">
        var obj = {};
        Object.defineProperty(obj,'prop',{
            set:newVal=>{
                document.getElementById('a').value = newVal;
                document.getElementById('b').innerHTML = newVal;
            }
        })
        document.addEventListener('keyup',function(e){
            obj.prop = e.target.value
        })
    </script>
</body>

这段代码已经可以做到对输入框与标签的双向数据绑定,可以随文本框输入文字的变化,span 中会同步显示相同的文字内容;在js或控制台显式的修改 obj.prop 的值,视图会相应更新。

但是我们的目标是像实现如下这样的:

<div id='app'>
    <input type="text" id='a' v-model='text'>
    {{text}}
</div>
var vm = new Vue({
    el:'app',
    data:{
        text:'hello world'
    }
})

所以,我们开始进阶版的双向绑定吧~


进阶版双向绑定


  • 监听器 observer
    • 需要设置一个监听器observer,对数据进行劫持监听,用来监听所有属性。
    • 实现observer的关键知识点是Object.defineProperty
    • 使用Object.defineProperty()进行监听数据的变动,对需要observer的数据遍历,都使用gettersetter,当对象的某个属性赋值,就会触发setter,这样就可以监听到数据的变化了。
function observe(obj,vm){
    Object.keys(obj).forEach(key=>{
        defineReactive(obj,key,obj[key])
    })
}

function defineReactive(obj,key,val){
    Object.defineProperty(obj,key,{
        get:()=>val,
        set:(newVal)=>{
            if(newVal==val) return;
            val = newVal;
            console.log(val)
        }
    })
}

这样已经可以监听数据的变化了。然后我们实现Compile–模板解析器。


  • 模板解析器 Compile
    • compile要将模板中的变量替换成数据。 所以要遍历整个DOM树,调用对应的指令渲染函数进行渲染,并调用对应的指令更新函数进行绑定。
    • 在遍历解析的过程有多次操作dom节点,为提高性能和效率,会先将跟节点el转换成文档碎片fragment进行解析编译操作,解析完成,再将fragment添加回原来的真实dom节点中。

因为我们要遍历解析这个模板

<div id='app'>
    <input type="text" id='a' v-model='text'>
    {{text}}
</div>

所以,首先对 DOM 进行编译:

function nodeToFragment(node,vm) {
    var flag = document.createDocumentFragment();
    var child;
    while(child = node.firstChild){
        Compile(child,vm)//对每个子节点进行扫描解析编译,调用对应的指令渲染函数进行渲染,并调用对应的指令更新函数进行绑定
        flag.appendChild(child);//劫持id为app节点内的所有的子节点
    }
    return flag
}

在实现双向响应前,先实现数据绑定初始化:

function Compile(node,vm){
    var reg = /\{\{(.*)\}\}/;
    //节点类型为元素
    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的值
                node.value =  vm.data[name];//将data的值赋给node
                node.removeAttribute('v-model')
            }
        }
    }
    //节点类型为文本,即获取{{text}}节点
    if(node.nodeType===3){
        if(reg.test(node.nodeValue)){
            var name = RegExp.$1;//获取{{}}内的内容,text
            console.log(name);
            name = name.trim();
            node.nodeValue = vm.data[name]
        }
    }
}

构造Vue构造器:

function Vue(options){
    this.options.data = data;
    var id = options.el;
    observe(this.data,this);//监听实例对象的data对象
    var dom = nodeToFragment(document.getElementById(id),this)//编译id的DOM树
    document.getElementById(id).appendChild(dom)//将元素返回id内
}

新建一个vm实例,来测试一下目前实现的数据绑定效果:

var vm = new Vue({
    el:'app',
    data:{
        text:'hello world'
    }
})

如图,数据初始化绑定成功,input框和span都有对应的值:
Model --> View效果

当我们在输入input框时,text的属性值需要随着变化,我们只需在Compile内给为输入框添加事件监听触发:

function Compile(node,vm){
//......
    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的值
                node.addEventListener('input',(e)=>{//绑定事件,将输入的值赋给传入的实例的对应得属性值
                    vm.data[name]=e.target.value;
                })
                node.value =  vm.data[name];//将data的值赋给node
                node.removeAttribute('v-model')
            }
        }
    }
}

目前为止,实现了:”当在input框内容发生变化时,控制台也相应的打印出变化的值。”
此时text 属性变化了,set 方法触发了,但是文本节点的内容没有变化。如何让同样绑定到 text 的文本节点也同步变化呢?我们就需要订阅发布模式来帮忙了。

  • 订阅发布模式】:该模式定义了一种一对多的关系,让多个观察者同时监听某一个主题对象,这个主题对象的状态发生改变时就会通知所有观察者对象

  • 消息订阅器 Dep

    • 要怎么通知订阅者呢,我们首先建立一个消息订阅器Dep,这个消息订阅器内有一个订阅者数组,用来收集订阅者(即后面的Watcher
    • observer监听到数据变化触发消息订阅器Depnotify方法,进而触发订阅器的update方法。
function defineReactive(obj,key,val){
    var dep = new Dep();
    Object.defineProperty(obj,key,{
        get:...,//省略
        set:(newVal)=>{
            if(newVal==val) return;
            val = newVal;
            console.log(val)
            dep.notify();//通知所有订阅者
        }
    })
}
function Dep (){
    this.subs=[];//订阅者数组
}
Dep.prototype = {
    addSub :function(sub){this.subs.push(sub)},
    notify:function(){
        this.subs.forEach(sub=>sub.update())//触发订阅者的update方法
        }
    }

  • 订阅者Watcher
function Watcher(vm,node,name){
    Dep.target = this;//全局变量
    this.name = name;
    this.node = node;
    this.vm = vm;
    this.update();
    Dep.target = null;//是 watcher 与 dep 关联的唯一桥梁,任何时刻都必须保证 Dep.target 只有一个值。
}

Watcher.prototype = {
    //获取data中的属性值
    get:function(){
    //读取vm的访问器属性,触发了访问器属性的 get 方法,get 方法中将该 watcher 添加到了对应访问器属性的 dep 中;
        this.value= this.vm.data[this.name];
    },
    update:function(){
        this.get();
        this.node.nodeValue = this.value;
    }
}

但是什么时候会有双向绑定? viewModel --> view 可能会发生在所有类型的DOM节点上,而 view --> viewModel 只能发生在 input, select, textarea 等交互控件上。所以将文本节点包装成 Watcher , 添加相关元素的观察者列表中,Watcher 负责更新页面元素

function Compile(node,vm){
    //...
   if(node.nodeType ===3){    //文本节点类型
        if(reg.test(node.nodeValue)){
            var name= RegExp.$1.trim()
            new Watcher = (vm,node,name)    //为该页面元素node生产watcher
        }
    }
}

然后在oberser内,添加订阅者Watcher到主体对象Dep

function defineReactive(obj,key,val){
    var dep = new Dep();
    Object.defineProperty(obj,key,{
        get:()=>{
            if(Dep.target)
                dep.addSub(Dep.target)//Dep.target存在,将目标元素添加到当前data属性的观察者列表中
            return val
            },
        set:....
    })
}

这样就实现了一个双向绑定啦~ 文本内容会随输入框内容同步变化,在控制器中修改 vm.text 的值,会同步反映到文本内容中。

参考:
Vue.js双向绑定的实现原理
剖析Vue实现原理 - 如何实现双向绑定mvvm

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值