這篇文章帶給大家的內容是關於帶你詳細實現vue雙向綁定,有一定的參考價值,有需要的朋友可以參考一下,希望對你有所幫助。
當今前端天下以 Angular、React、vue 三足鼎立的局面,你不選擇一個陣營基本上無法立足於前端,甚至是兩個或者三個陣營都要選擇,大勢所趨。
所以我們要時時保持好奇心,擁抱變化,只有在不斷的變化中你才能利於不敗之地,保守只能等死。
最近在學習Vue,一直以來對它的雙向綁定只能算了解並不深入,最近幾天打算深入學習下,透過幾天的學習查閱資料,算是對它的原理有所認識,所以自己動手寫了一個雙向綁定的例子,下面我們一步步看如何實現的。
看完這篇文章後我相信你會對 Vue 的雙向綁定原理有一個清楚的認識。也能幫助我們更好的認識 Vue。
先看效果圖
//代码: <div> <input> <h1>{{name}}</h1> </div> <script></script> <script></script> <script></script> <script></script> <script> const vm = new Mvue({ el: "#app", data: { name: "我是摩登" } }); </script>
在正式開始之前我們先說資料綁定的事情,資料綁定我的理解就是讓資料M(model)展示到視圖V(view)上。我們常見的架構模式有 MVC、MVP、MVVM模式,目前前端框架基本上都是採用 MVVM 模式實現雙向綁定,Vue 自然也不例外。但是各個框架實現雙向綁定的方法略有不同,目前大概有三種實作方式。
發布訂閱模式
Angular 的髒查機制
資料劫持
而Vue 則採用的是資料劫持與發布訂閱結合的方式實現雙向綁定,而資料劫持主要透過Object.defineProperty
來實現。
這篇文章我們不詳細討論 Object.defineProperty 的用法,我們主要看看它的儲存屬性 get 與 set。讓我們來看看透過它設定的物件屬性之後有何變化。
var people = { name: "Modeng", age: 18 } people.age; //18 people.age = 20;
上述程式碼就是普通的取得/設定物件的屬性,看不到什麼奇怪的變化。
var modeng = {} var age; Object.defineProperty(modeng, 'age', { get: function () { console.log("获取年龄"); return age; }, set: function (newVal) { console.log("设置年龄"); age = newVal; } }); modeng.age = 18; console.log(modeng.age);
你會發現透過上述操作之後,我們存取age 屬性時會自動執行get 函數,設定age 屬性時,會自動執行set 函數,這就給我們的雙向綁定提供了非常大的方便。
我們知道 MVVM 模式在於資料與視圖的保持同步,意思是說資料改變時會自動更新視圖,視圖發生變化時會更新資料。
所以我們需要做的就是如何偵測資料的變化然後通知我們去更新視圖,如何偵測到視圖的變化然後去更新資料。偵測視圖這個比較簡單,無非就是我們利用事件的監聽即可。
那麼如何知道資料屬性改變呢?這就是利用我們上面說到的 Object.defineProperty 當我們的屬性改變時,它會自動觸發 set 函數以便能夠通知我們去更新視圖。
透過上面的描述與分析我們知道Vue 是透過資料劫持結合發布訂閱模式來實現雙向綁定的。我們也知道資料劫持是透過 Object.defineProperty 方法,當我們知道這些之後,我們就需要一個監聽器 Observer 來監聽屬性的變化。得知屬性改變之後我們需要一個 Watcher 訂閱者來更新視圖,我們還需要一個 compile 指令解析器,用於解析我們的節點元素的指令與初始化視圖。所以我們需要如下:
Observer 監聽器:用來監聽屬性的變化通知訂閱者
Watcher 訂閱者:收到屬性的變化,然後更新視圖
Compile 解析器:解析指令,初始化模版,綁定訂閱者
##順著這條思路我們一步一步去實現。 監聽器Observer監聽器的作用就是去監聽資料的每一個屬性,我們上面也說了使用
Object.defineProperty 方法,當我們監聽到屬性改變之後我們需要通知Watcher 訂閱者執行更新函數去更新視圖,在這個過程中我們可能會有很多個訂閱者Watcher 所以我們要建立一個容器Dep 去做一個統一的管理。
function defineReactive(data, key, value) { //递归调用,监听所有属性 observer(value); var dep = new Dep(); Object.defineProperty(data, key, { get: function () { if (Dep.target) { dep.addSub(Dep.target); } return value; }, set: function (newVal) { if (value !== newVal) { value = newVal; dep.notify(); //通知订阅器 } } }); } function observer(data) { if (!data || typeof data !== "object") { return; } Object.keys(data).forEach(key => { defineReactive(data, key, data[key]); }); } function Dep() { this.subs = []; } Dep.prototype.addSub = function (sub) { this.subs.push(sub); } Dep.prototype.notify = function () { console.log('属性变化通知 Watcher 执行更新视图函数'); this.subs.forEach(sub => { sub.update(); }) } Dep.target = null;
var modeng = { age: 18 } observer(modeng); modeng.age = 20;
我们可以看到浏览器控制台打印出 “属性变化通知 Watcher 执行更新视图函数” 说明我们实现的监听器没毛病,既然监听器有了,我们就可以通知属性变化了,那肯定是需要 Watcher 的时候了。
Watcher 主要是接受属性变化的通知,然后去执行更新函数去更新视图,所以我们做的主要是有两步:
把 Watcher 添加到 Dep 容器中,这里我们用到了 监听器的 get 函数
接收到通知,执行更新函数。
function Watcher(vm, prop, callback) { this.vm = vm; this.prop = prop; this.callback = callback; this.value = this.get(); } Watcher.prototype = { update: function () { const value = this.vm.$data[this.prop]; const oldVal = this.value; if (value !== oldVal) { this.value = value; this.callback(value); } }, get: function () { Dep.target = this; //储存订阅器 const value = this.vm.$data[this.prop]; //因为属性被监听,这一步会执行监听器里的 get方法 Dep.target = null; return value; } }
这一步我们把 Watcher 也给弄了出来,到这一步我们已经实现了一个简单的双向绑定了,我们可以尝试把两者结合起来看下效果。
function Mvue(options, prop) { this.$options = options; this.$data = options.data; this.$prop = prop; this.$el = document.querySelector(options.el); this.init(); } Mvue.prototype.init = function () { observer(this.$data); this.$el.textContent = this.$data[this.$prop]; new Watcher(this, this.$prop, value => { this.$el.textContent = value; }); }
这里我们尝试利用一个实例来把数据与需要监听的属性传递进来,通过监听器监听数据,然后添加属性订阅,绑定更新函数。
<p>{{name}}</p> const vm = new Mvue({ el: "#app", data: { name: "我是摩登" } }, "name");
我们可以看到数据已经正常的显示在页面上,那么我们在通过控制台去修改数据,发生变化后视图也会跟着修改。
到这一步我们我们基本上已经实现了一个简单的双向绑定,但是不难发现我们这里的属性都是写死的,也没有指令模板的解析,所以下一步我们来实现一个模板解析器。
Compile 的主要作用一个是用来解析指令初始化模板,一个是用来添加添加订阅者,绑定更新函数。
因为在解析 DOM 节点的过程中我们会频繁的操作 DOM, 所以我们利用文档片段(DocumentFragment)来帮助我们去解析 DOM 优化性能。
function Compile(vm) { this.vm = vm; this.el = vm.$el; this.fragment = null; this.init(); } Compile.prototype = { init: function () { this.fragment = this.nodeFragment(this.el); }, nodeFragment: function (el) { const fragment = document.createDocumentFragment(); let child = el.firstChild; //将子节点,全部移动文档片段里 while (child) { fragment.appendChild(child); child = el.firstChild; } return fragment; } }
然后我们就需要对整个节点和指令进行处理编译,根据不同的节点去调用不同的渲染函数,绑定更新函数,编译完成之后,再把 DOM 片段添加到页面中。
Compile.prototype = { compileNode: function (fragment) { let childNodes = fragment.childNodes; [...childNodes].forEach(node => { let reg = /\{\{(.*)\}\}/; let text = node.textContent; if (this.isElementNode(node)) { this.compile(node); //渲染指令模板 } else if (this.isTextNode(node) && reg.test(text)) { let prop = RegExp.$1; this.compileText(node, prop); //渲染{{}} 模板 } //递归编译子节点 if (node.childNodes && node.childNodes.length) { this.compileNode(node); } }); }, compile: function (node) { let nodeAttrs = node.attributes; [...nodeAttrs].forEach(attr => { let name = attr.name; if (this.isDirective(name)) { let value = attr.value; if (name === "v-model") { this.compileModel(node, value); } node.removeAttribute(name); } }); }, //省略。。。 }
因为代码比较长如果全部贴出来会影响阅读,我们主要是讲整个过程实现的思路,文章结束我会把源码发出来,有兴趣的可以去查看全部代码。
到这里我们的整个的模板编译也已经完成,不过这里我们并没有实现过多的指令,我们只是简单的实现了 v-model
指令,本意是通过这篇文章让大家熟悉与认识 Vue 的双向绑定原理,并不是去创造一个新的 MVVM 实例。所以并没有考虑很多细节与设计。
现在我们实现了 Observer、Watcher、Compile,接下来就是把三者给组织起来,成为一个完整的 MVVM。
这里我们创建一个 Mvue 的类(构造函数)用来承载 Observer、Watcher、Compile 三者。
function Mvue(options) { this.$options = options; this.$data = options.data; this.$el = document.querySelector(options.el); this.init(); } Mvue.prototype.init = function () { observer(this.$data); new Compile(this); }
然后我们就去测试一下结果,看看我们实现的 Mvue 是不是真的可以运行。
<p> </p><h1>{{name}}</h1> <script></script> <script></script> <script></script> <script></script> <script> const vm = new Mvue({ el: "#app", data: { name: "完全没问题,看起来是不是很酷!" } }); </script>
我们尝试去修改数据,也完全没问题,但是有个问题就是我们修改数据时时通过 vm.$data.name 去修改数据,而不是想 Vue 中直接用 vm.name 就可以去修改,那这个是怎么做到的呢?其实很简单,Vue 做了一步数据代理操作。
我们来改造下 Mvue 添加数据代理功能,我们也是利用 Object.defineProperty
方法进行一步中间的转换操作,间接的去访问。
function Mvue(options) { this.$options = options; this.$data = options.data; this.$el = document.querySelector(options.el); //数据代理 Object.keys(this.$data).forEach(key => { this.proxyData(key); }); this.init(); } Mvue.prototype.init = function () { observer(this.$data); new Compile(this); } Mvue.prototype.proxyData = function (key) { Object.defineProperty(this, key, { get: function () { return this.$data[key] }, set: function (value) { this.$data[key] = value; } }); }
到这里我们就可以像 Vue 一样去修改我们的属性了,非常完美。完全自己动手实现,你也来试试把,体验下自己动手写代码的乐趣。
本文主要是对 Vue 双向绑定原理的学习与实现。
主要是对整个思路的学习,并没有考虑到太多的实现与设计的细节,所以还存在很多问题,并不完美。
源码地址,整个过程的全部代码,希望对你有所帮助。
以上是帶你詳細實作vue雙向綁定的詳細內容。更多資訊請關注PHP中文網其他相關文章!