vue数据双向绑定原理及简单实现

数据双向绑定原理简单概括的话就是:

View层影响Model层是通过对 ‘keyup’ 等事件的监听。

Model层影响View层是通过 Object.defineProperty( ) 方法劫持数据并结合发布订阅者模式的方式来实现数据的双向绑定。
(vue 3.0版本里用 Proxy 替代 Object.defineProperty)

当然不能只掌握到这个层面,下面介绍如何进行数据劫持以及发布订阅者模式:

1、Object.defineProperty( ) 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象

Object.defineProperty(obj, prop, descriptor)

  • obj:要定义属性的对象。
  • prop:要定义或修改的属性的名称。
  • descriptor:要定义或修改的属性描述符。

这里我们主要是通过重定义属性描述符里的 get 和 set 方法进行数据劫持

get:属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。该函数的返回值会被用作属性的值。

set:属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。

总结:当我们修改数据层(Model)某个属性值(数据)时,就会触发重定义过的 set 方法去同步修改视图层(View)的数据

2、发布订阅者模式的应用

订阅者(Watcher)把自己想订阅的数据或事件添加到订阅者收集器(Dep),当某事件(set或get等)触发时,发布者(Observer)发布该事件到订阅者收集器,由订阅者收集器统一通知有订阅该事件的订阅者(Watcher)去执行相应的更新函数从而更新视图。(参考下图流程)
图片转自参考文章一
(图片转自参考文章一)

到此应该有个大概的思路了,下面根据原理图来介绍整个流程:

1、首先使用Object.defineProperty()中的 getter/setter 作为一个Observer(劫持器)去劫持data对象中的所有属性,在属性 set 的时候通知Dep(订阅者收集器)去通知相关订阅者。

2、实现一个 Watcher(订阅者),Watcher 就是收到 Dep 数据变化的通知后,会去执行相对应的更新函数来更新视图,同一个数据可能在多处被使用,所以订阅者不止一个;这也是 Dep 存在的意义,对 Watcher 集中起来统一管理。

3、Dep(订阅者收集器),里面存放每个数据对应的所有 Watcher,当Observer 的 set 方法被触发时,就调用 Dep 里面的的notify(通知)方法,逐条去通知所有的 Watcher 。

4、Complier是一个编译器,作用是解析模板指令,扫描和解析 vue 代码中每一个节点,先将节点转换为碎片化文档 DocumentFragment(性能优化,减少重排),再一次性 append 所有节点至目标 element 内,完成视图的初始化;同时编译器还担当着初始化 Watcher 的任务,即给 Watcher 绑定相关的更新函数 ,最终使 Watcher 添加到 Dep 中去。

简单实现(不包括Dep、Complier的实现):

<body>
   <div id="app">
       <input type="text" id="txt">
       <span id="show-txt"></span>
   </div>
   <script>
       var obj = {}
       Object.defineProperty(obj, 'val', {
           get: function () {
               return val
           },
           set: function (newValue) {
               document.getElementById('txt').value = newValue
               document.getElementById('show-txt').innerHTML = newValue
           }
       })
       document.addEventListener('keyup', function (e) {
           obj.val = e.target.value
       })
   </script>
</body>

掌握以上内容已经算是上道了(面试时解释完上面内容也差不多了),下面是较为完整的实现(加深理解):

 1.首先实现一个劫持器,对数据进行劫持
 
function defineReactive(obj, key, val) {

    var dep = new Dep();  //创建dep订阅者收集器

    Object.defineProperty(obj, key, {

        get: function() {
            if(Dep.target) {
                dep.addSub(Dep.target)
            }
            return val
        },

        set: function(newVal) {
            if(newVal === val) {
                return
            }
            val = newVal;
            dep.notify();  //执行dep的通知函数去通知相关订阅者
        }
    })
}

2、实现一个观察者,对于一个实例的每一个属性值都进行观察

function Observer(obj, vm) {
    for(let key of Object.keys(obj)) {
        defineReactive(vm, key, obj[key]);
    }
}

3、实现dep的构造函数

function Dep() {
    this.subs = [] //用来收集订阅者
}
Dep.prototype = {
    addSub(sub) {  //添加订阅者的方法
        this.subs.push(sub)  
    },
    notify() {  //通知相关的所有订阅者执行更新函数
        this.subs.forEach(function(sub) {  
            sub.update();
        })
    }
}

4、实现Watcher订阅者

function Watcher(vm, node, name) {

    Dep.target = this; //辨识订阅者者要添加到哪个dep收集器里

    this.vm = vm;
    this.node = node;
    this.name = name;
    this.update();

    Dep.target = null;

}

Watcher.prototype = {

    update() {
        this.get();
        this.node.nodeValue = this.value //更新函数
    },
    get() {
        this.value = this.vm[this.name] //触发相应的get
    }

}

5、实现编译器Complier

function compile(node, vm) {

    var reg = /\{\{(.*)\}\}/; // 用正则来匹配{{messeage}}

    if(node.nodeType === 1) { //如果是元素节点

        var attr = node.attributes;

        //解析元素节点的所有属性

        for(let i = 0; i < attr.length; i++) {

            if(attr[i].nodeName == 'v-model') {

                var name = attr[i].nodeValue //看看是与哪一个数据相关

                node.addEventListener('input', function(e) { //将与其相关的数据改为最新值
                    vm[name] = e.target.value
                })

                node.value = vm.data[name]; //将data中的值赋予给该node

                node.removeAttribute('v-model')

            }

        }

    }
    //如果是文本节点,即{{messeage}}情况

    if(node.nodeType === 3) {

        if(reg.test(node.nodeValue)) {

            var name = RegExp.$1; //获取到匹配的字符串

            name = name.trim();
            
            node.nodeValue = vm[name]; //将data中的值赋予给该node

            new Watcher(vm, node, name) //绑定一个订阅者
        }

    }

}

//在向碎片化文档中添加节点时,每个节点都处理一下

function nodeToFragment(node, vm) {

    var fragment = document.createDocumentFragment();

    var child;

    while(child = node.firstChild) {

        compile(child, vm);

        fragment.appendChild(child);

    }

    return fragment

}

 6、 Vue构造函数     

function Vue(options) {

    this.data = options.data;

    observe(this.data, this)   //给data中的所有属性值增添了observe

    var id = options.el;

    var dom = nodeToFragment(document.getElementById(id), this)

    //处理完所有节点后,重新把内容添加回去,减少重排
    document.getElementById(id).appendChild(dom)

}

(有兴趣更进一步研究的话,去看源码吧!)

参考文章:
VUE双向数据绑定原理及简单实现
Vue数据双向绑定原理及简单实现
理解VUE双向数据绑定原理和实现—赵佳乐

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值