vue-初步理解VUE双向绑定原理

本文详细介绍了Vue双向绑定的实现原理,包括数据劫持、Observer监听器、Dep存储依赖与派发更新、Watcher订阅者以及Compile指令解析器的工作流程。通过创建Observer、Dep、Watcher和Compile,实现了数据变化到视图更新的完整过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

vue双向绑定原理

vue的双向绑定是由数据劫持结合发布者-订阅者模式实现。

什么是数据劫持?: vue通过Object.defineProperty()来劫持对象属性的setter和getter操作,在数据变动时做你想要做的事情。

var Book = {};

Object.defineProperty(Book,'name',{
    set:function(value) {
        name = value;
        console.log('你取了名叫:'+value);
    },
    get:function() {
        console.log('get方法被监听到');
        return '<'+name+'>';
    }
});
Book.name = '张三';  //你取了名叫: 张三
console.log(Book.name); //<张三>

实现过程

我们已经知道如何实现数据的双向绑定了,那么首先要对数据进行劫持监听,所以我们首先要设置一个监听器Observer,用来监听所有的属性,当属性变化时,就需要通知订阅者Watcher,看是否需要更新。因为属性可能是多个,所以会有多个订阅者,故我们需要一个消息订阅器Dep来专门收集这些订阅者,并在监听器Observer和订阅者Watcher之间进行统一的管理。因为在节点元素上可能存在一些指令,所以我们还需要有一个指令解析器Compile,对每个节点元素进行扫描和解析,将相关指令初始化成一个订阅者Watcher,并替换模板数据并绑定相应的函数,这时候当订阅者Watcher接受到相应属性的变化,就会执行相对应的更新函数,从而更新视图。

整理上面的思路,我们需要实现几个步骤,来完成双向绑定:

  • Observer 监听器,用来劫持并监听所有属性,如果有变动的,就通知订阅者。
  • Dep 存储依赖和派发更新,监听器和订阅者的桥梁。
  • Watcher 订阅者,可以收到属性的变化通知并执行相应的函数,从而更新视图。
  • Compile 解析器,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器
    在这里插入图片描述

1、实现一个监听器

  • 数据监听器的核心方法就是Object.defineProperty( ),通过遍历循环对所有属性值进行监听劫持监听
  • 在属性的 get里面判断是否需要需要添加订阅者,这是为了让Watcher在初始化时触发
  • 在属性的 set方法中,如果函数变化,就会通知对应的所有订阅者,订阅者们将会执行相对应的更新函数

function Observer(data) {
    this.data = data;
    this.walk(data);
}

Observer.prototype = {
    walk: function(data) {
        var self = this;
        Object.keys(data).forEach(function(key) {
            self.defineReactive(data, key, data[key]);
        });
    },
    defineReactive: function(data, key, val) {
        var dep = new Dep();
        var childObj = observe(val);
        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get: function getter () {
                //Dep类在下一个文件详细说
                //Watcher初始化触发、判断是否需要添加订阅者明
                if (Dep.target) {
                    dep.addSub(Dep.target); //添加订阅者
                }
                return val;
            },
            set: function setter (newVal) {
                if (newVal === val) {
                    return;
                }
                val = newVal;
                dep.notify();
            }
        });
    }
};

function observe(value, vm) {
    if (!value || typeof value !== 'object') {
        return;
    }
    return new Observer(value);
}

2、实现一个Dep

  • addSub方法 用于收集订阅者
  • notify方法 执行相对应的更新函数
  • Dep.target 的作用在Watch类中有详细说明

function Dep() {
    this.subs = [];
}

//prototype 属性使您有能力向对象添加属性和方法
//prototype这个属性只有函数对象才有,具体的说就是构造函数具有.只要你声明定义了一个函数对象,这个prototype就会存在
//对象实例是没有这个属性
Dep.prototype = {
    addSub:function(sub) {
        this.subs.push(sub);
    },
    notify:function() {
        this.subs.forEach(function(sub) {
            sub.update();        //通知每个订阅者检查更新
        })
    }
}

Dep.target = null;

3、实现一个 Watcher

  • 缓存Watcher中this 到 Dep.target
  • 强制执行监听器里的get函数
  • update用于执行更新方法
function Watcher(vm,exp,cb) {
    this.vm = vm;    //指向SelfVue的作用域
    this.exp = exp;  //绑定属性的key值
    this.cb = cb;    //闭包
    this.value = this.get();
}

Watcher.prototype = {
    update:function() {
        this.run();
    },
    run:function() {
        var value = this.vm.data[this.exp];
        var oldVal = this.value;
        if(value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm,value,oldVal);
        }
    },
    get:function() {
        Dep.target = this;                   // 缓存自己
        var value = this.vm.data[this.exp];  // 强制执行监听器里的get函数
        Dep.target = null;                   // 释放自己
        return value;
    }
}

现在实现observer和watcher的关联

(1)定义个SelfVue类

//将Observer和Watcher关联起来
function SelfVue(data,el,exp) {
    this.data = data;
    observe(data);
    el.innerHTML = this.data[exp];
    new Watcher(this,exp,function(value) {
        el.innerHTML = value;
    });
    return this;
}

(2)新建个Index.html

<body>
    <h1 id="name">{{name}}</h1>
</body>
 
<script src="../js/observer.js"></script>
<script src="../js/Watcher.js"></script>
<script src="../js/SelfVue.js"></script>
 
<script>
     var ele = document.querySelector('#name');
     var selfVue = new SelfVue({
         name:'hello world'
     },ele,'name');
 
     window.setTimeout(function() {
         console.log('name值改变了');
         selfVue.name = 'byebye world';
     },2000);
</script>

至此,vue的双向绑定就实现了。

4、实现指令解析器Compile

再上面的双向绑定demo中,我们发现整个过程都没有解析dom节点,而是固定某个节点进行替换数据,所以接下来我们要实现一个解析器Compile来解析和绑定工作,分析解析器的作用,实现步骤如下:

作用:

  • 解析模板指令,并替换模板数据,初始化视图
  • 将模板指令对应的节点绑定对应的更新函数,初始化相应的订阅器

实现:

function Compile(el, vm) {
    this.vm = vm;
    this.el = document.querySelector(el);
    this.fragment = null;
    this.init();
}

Compile.prototype = {

   init: function () {
        if (this.el) {
            this.fragment = this.nodeToFragment(this.el);
            this.compileElement(this.fragment);
            this.el.appendChild(this.fragment);
        } else {
            console.log('Dom元素不存在');
        }
    },

	//为了解析模板,首先要获得dom元素,然后对含有dom元素上含有指令的节点进行处理
	//这个过程对dom元素的操作比较繁琐,所以我们可以先建一个fragment片段
	//将需要解析的dom元素存到fragment片段中在做处理:
	nodeToFragment:function(el) {
	    var fragment = document.createDocumentFragment();   //createdocumentfragment()方法创建了一虚拟的节点对象,节点对象包含所有属性和方法。
	    var child = el.firstChild;
	    while(child) {
	        // 将Dom元素移入fragment中
	        fragment.appendChild(child);
	        child = el.firstChild;
	    }
	    return fragment;
	},
	
	//遍历各个节点,对含有相关指定的节点进行特殊处理
	compileElement:function(el) {
	     var childNodes = el.childNodes;   //childNodes属性返回节点的子节点集合,以 NodeList 对象。
	     var self = this;
	     //slice() 方法可从已有的数组中返回选定的元素。
	     [].slice.call(childNodes).forEach(function(node) {
	         var reg = /\{\{(.*)\}\}/;
	         var text = node.textContent;  //textContent 属性设置或返回指定节点的文本内容
	         if(self.isTextNode(node) && reg.test(text)) {      //判断是否符合{{}}的指令
	             //exec() 方法用于检索字符串中的正则表达式的匹配。
	             //返回一个数组,其中存放匹配的结果。如果未找到匹配,则返回值为 null。
	             self.compileText(node,reg.exec(text)[1]);
	         }
	         if(node.childNodes && node.childNodes.length) {
	             self.compileElement(node);    //继续递归遍历子节点
	         }
	     });
	 },
	 
	 compileText:function(node,exp) {
	     var self = this;
	     var initText = this.vm[exp];
	     this.updateText(node,initText);    // 将初始化的数据初始化到视图中
	     new Watcher(this.vm,exp,function(value) {
	         self.updateText(node,value);
	     });
	
	 },
	 
	 updateText:function(node,value) {
	     node.textContent = typeof value == 'undefined' ? '': value;
	 },
	 
     isTextNode: function(node) {
        return node.nodeType == 3;
     }
}

修改下Seftvue和index.html,更改后,我们就不要像之前通过传入固定的元素值进行双向绑定了,可以随便命名各种变量进行双向绑定了

function SelfVue (options) {
    var self = this;
    this.data = options.data;
    this.methods = options.methods;

    Object.keys(this.data).forEach(function(key) {
        self.proxyKeys(key);
    });

    observe(this.data);
    new Compile(options.el, this);
}

SelfVue.prototype = {
    proxyKeys: function (key) {
        var self = this;
        Object.defineProperty(this, key, {
            enumerable: false,
            configurable: true,
            get: function getter () {
                return self.data[key];
            },
            set: function setter (newVal) {
                self.data[key] = newVal;
            }
        });
    }
}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div id="app">
    <h1>{{title}}</h1>
    <h2>{{name}}</h2>
    <h3>{{content}}</h3>
</div>
</body>
<script src="./js/Observer.js"></script>//监听器
<script src="./js/Watcher.js"></script>//订阅器
<script src="./js/Compile.js"></script>//解析器
<script src="./js/Selfvue.js"></script>

<script>
    var selfVue = new SelfVue({
        el:'#app',
        data:{
            title:'aaa',
            name:'bbb',
            content:'ccc'
        }
    });
    window.setTimeout(function() {
        selfVue.title = 'ddd';
        selfVue.name = 'eee';
        selfVue.content = 'fff'
    },2000);
</script>
</html>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值