摘要:要實現最小化刷新,我們要將模板中的每個綁定都收集起來。思考題在最后的實現下,我們把模板改為下面這樣雖然很少會有人這樣寫,就會出現重復的實例,該如何解決這個問題,參考早期源碼學習系列之四如何實現動態數據綁定
上一篇文章我們了解了怎樣實現一個簡單模板引擎。但這個模板引擎只適合靜態模板,因為它是將模板整體編譯成字符串進行全量替換。如果每次數據改變都進行一次替換,會有兩個最主要的問題:
性能差。DOM 操作本身就非常大的開銷,更別說每一次都替換這么大的量。
破壞事件綁定。這個是最麻煩的,如果我們沒有給解綁移除 DOM 綁定的事件,還會造成內存泄露。而且每一次替換都要重新綁定事件。
因此,沒有人會將這種模板引擎用來編譯動態模板。那我們如何編譯動態模板呢?
回答這個問題之前,我們先要了解前端的世界何時出現了動態模板:它是由 MVVM 框架帶來的,動態模板是 MVVM 框架的視圖層(view)。我們知道的 MVVM 框架有 knockout.js、angular.js、avalon 和 vue。
對于這些框架,大部分人最熟悉的應該就是 vue,所以我下面也是以 vue 1.0 作為參考,來實現一個功能更簡單的動態模板引擎。它是框架自帶的一個功能,讓框架能夠響應數據的改變。從而刷新頁面。
MVVM 動態模板的特點是能最小化刷新:哪個變量改變了,與之相關的節點才會更新。這樣我們就能避免上面提到的靜態模板的兩大問題。
要實現最小化刷新,我們要將模板中的每個綁定都收集起來。這個收集工作是框架在完成第一次渲染前就已經完成了,每個綁定都會生成一個 Directive 實例:
class Directive { constructor(vm, el, exp, update) { this.vm = vm this.el = el this.exp = exp this.update = update this.watchers = [] this.get = getEvaluationFn(exp).bind(this, vm.$data) this.bind() } } function getEvaluationFn(exp) { return new Function("data", "with(data) { return " + exp + "}") }
我們知道,每個綁定都由指令和指令值(指令值可能是表達式,可能是語句,也可能就是一個變量,還可能是框架自定義的語法)構成,每種指令都有對應的刷新函數(update)。如節點值的綁定的刷新函數是:
function updateTextNode() { const value = this.get() this.el.nodeValue = value console.log(this.exp + " updated: " + value) }
有了刷新函數,那如何做到在數據改變時調用刷新函數更新節點的值呢?我們就還要將每個指令里的相關變量都跟這個 Directive 實例關聯起來。我們用一個 $binding 對象來記錄,它的鍵是變量,值是 Binding 實例:
class Binding { constructor() { this.subs = [] } addChild(key) { return this[key] || new Binding() } addSub(watcher) { this.subs.push(watcher) } }
那上面的 subs 里添加的為什么不是 Directive 實例呢,而是 watcher 呢?它其實是 Watcher 的實例,這是為了以后能夠實現 $watch 方法提前引入的概念,Watcher 實例的 cb 既可以是指令的刷新函數,也可以是 $watch 方法的回調函數:
class Watcher { constructor(vm, path, cb, ctx) { this.id = ++uid this.vm = vm this.path = path this.cb = cb this.ctx = ctx || vm this.addDep() } }
class Directive { bind() { this.watchers.push(new Watcher(this.vm, this.exp, this.update, this)) } }
我們先考慮最簡單的情況,指令值就是一個變量,根據上面的思路,我們就可以寫出最簡單的實現了,代碼就不貼了,有興趣的直接看源碼。
MVVM
My name is {{name.first}}-{{name.last }},{{age}} years old
上面實現的動態模板是在我們假定了指令值是最簡單的變量的情況下實現的。那要是把上面的模板改為下面這樣呢?
MVVM
My name is {{name.first}}-{{name.last }},{{"age: " + age}} years old
salary: {{ salary.toLocaleString() }}
那我們上面的實現有一些數據就不能動態刷新了,原因很簡單,就是我們是直接將 "age: " + age 和 Directive 實例關聯,而我們修改的只是 age,自然就找不到對應的實例了。那我們如何解決呢?
首先想到的肯定是按照現有的實現來擴展,讓它支持模板插值是表達式的情況。已有的實現是直接解析得到變量,那我們就繼續想辦法直接解析表達式得到變量。像 "age: " + age 這種表達式直接解析出 age 其實不難。但 salary.toLocaleString() 這種就不好做了,要是 salary.toLocaleString().slice(1) 這種可以說是沒辦法解析了。
既然這條路行不通,其實我們是有更簡單的方法。既然我們都已經將 data 進行了代理,那我們就可以在 get 獲取變量值時進行依賴收集。因為我們本來就會運行 Directive 實例的求值函數進行初始值的替換,這就會觸發變量的 get 。具體的代碼怎么寫就不說了,詳細的修改和支持表達式的源碼。
當然現在只實現動態模板最簡單的插值指令。還有一些更復雜的指令如:if 和 for 的實現方式,下次有機會再分享。
思考題在最后的實現下,我們把模板改為下面這樣(雖然很少會有人這樣寫),就會出現重復的 Watcher 實例,該如何解決這個問題?
參考MVVM
hello,My name is {{name.first + "-" + name.last }}
vue早期源碼學習系列之四:如何實現動態數據綁定
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/107754.html
摘要:看這篇之前,如果沒有看過之前的文章,移步拉到文章末尾查看之前的文章。而該組件實例的父實例卻并不固定,所以我們將這些在使用時才能確定的參數在組件實例化的時候傳入。系列文章地址優化優化總結 看這篇之前,如果沒有看過之前的文章,移步拉到文章末尾查看之前的文章。 前言 在上一步,我們實現 extend 方法,用于擴展 Vue 類,而我們知道子組件需要通過 extend 方法來實現,我們從測試例...
摘要:面向對象是自己組裝電腦,硬件已生產完畢。面向過程吃狗屎面向對象狗吃屎確切的講是一種軟件設計規范,早在年的理念就已經誕生。后期的維護成本會減少很多。減輕了開發人員的負擔,也減少了操作邏輯導致業務邏輯混亂的可能性。 什么是MVC,什么是MVVM? 面向過程 --> 面向對象 --> MVC --> MV* 面向過程: 開發人員按照需求邏輯順序開發代碼邏輯,主要思維模式在于如何實現。先細節,...
摘要:接下來要看看這個訂閱者的具體實現了實現訂閱者作為和之間通信的橋梁,主要做的事情是在自身實例化時往屬性訂閱器里面添加自己自身必須有一個方法待屬性變動通知時,能調用自身的方法,并觸發中綁定的回調,則功成身退。 本文能幫你做什么?1、了解vue的雙向數據綁定原理以及核心代碼模塊2、緩解好奇心的同時了解如何實現雙向綁定為了便于說明原理與實現,本文相關代碼主要摘自vue源碼, 并進行了簡化改造,...
閱讀 3070·2023-04-25 16:50
閱讀 903·2021-11-25 09:43
閱讀 3512·2021-09-26 10:11
閱讀 2518·2019-08-26 13:28
閱讀 2531·2019-08-26 13:23
閱讀 2419·2019-08-26 11:53
閱讀 3566·2019-08-23 18:19
閱讀 2987·2019-08-23 16:27