日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 前端技术 > vue >内容正文

vue

自己动手写一个 SimpleVue

發(fā)布時間:2024/4/13 vue 41 豆豆
生活随笔 收集整理的這篇文章主要介紹了 自己动手写一个 SimpleVue 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

最近看到一句話很有感觸 —— 有人問 35 歲之后你還會在寫代碼嗎?各種中年程序員的言論充斥的耳朵,好像中年就不該寫代碼了,但是我想說,若干年以后,有人問你閑來無事你會干什么,我想我會說,寫代碼,我想這個答案就夠了,年齡不是你不愛的理由。

理論基礎

雙向綁定是 MVVM 框架最核心之處,那么雙向綁定的核心是什么呢?核心就是 Object.defineProperty 這個 API,關于這個 API 的具體內(nèi)容,請移步 MDN - Object.defineProperty ,里面有更詳細的說明。

接下來我們來看一下 Vue 是怎么設計的:

圖中有幾個重要的模塊:

  • 監(jiān)聽者(Observer): 這個模塊的主要功能是給 data 中的數(shù)據(jù)增加 getter 和 setter,以及往觀察者列表中增加觀察者,當數(shù)據(jù)變動時去通知觀察者列表。
  • 觀察者列表(Dep): 這個模塊的主要作用是維護一個屬性的觀察者列表,當這個屬性觸發(fā) getter 時將觀察者添加到列表中,當屬性觸發(fā) setter 造成數(shù)據(jù)變化時通知所有觀察者。
  • 觀察者(Watcher): 這個模塊的主要功能是對數(shù)據(jù)進行觀察,一旦收到數(shù)據(jù)變化的通知就去改變視圖。

我們簡化一下 Vue 里的各種代碼,只關注我們剛剛說的那些東西,實現(xiàn)一個簡單版的 Vue。

Coding Time

我們就拿 Vue 的一個例子來檢驗成果。

<body><div id="app"><p>{{ message }}</p><button v-on:click="reverseMessage">逆轉消息</button></div> </body> <script src="vue/index.js"></script> <script src="vue/observer.js"></script> <script src="vue/compile.js"></script> <script src="vue/watcher.js"></script> <script src="vue/dep.js"></script> <script> const vm = new Vue({el: '#app',data: {message: 'Hello Vue.js!'},methods: {reverseMessage: function () {this.message = this.message.split('').reverse().join('')}},mounted: function() {setTimeout(() => {this.message = 'I am changed after mounte';}, 2000);}, }); </script> 復制代碼

new Vue()

首先,看 Vue 的源碼我們就能知道,在 Vue 的構造函數(shù)中我們完成了一系列的初始化工作,以及生命周期鉤子函數(shù)的設置。那我們的簡易版 Vue 該怎么寫呢?我們在使用 Vue 的時候是通過一個構造函數(shù)來開始使用,所以我們的簡易代碼也從構造函數(shù)開始。

class Vue {constructor(options) {this.data = options.data;this.methods = options.methods;this.mounted = options.mounted;this.el = options.el;this.init();}init() {// 代理 dataObject.keys(this.data).forEach(key => {this.proxy(key);});// 監(jiān)聽 dataobserve(this.data, this);// 編譯模板const compile = new Compile(this.el, this);// 生命周期其實就是在完成一些操作后調(diào)用的函數(shù),// 所以有些屬性或者實例在一些 hook 里其實還沒有初始化,// 也就拿不到相應的值this.callHook('mounted');}proxy(key) {Object.defineProperty(this, key, {enumerable: false,configurable: true,get: function() {return this.data[key]},set: function(newVal) {this.data[key] = newVal;}});}callHook(lifecycle) {this[lifecycle]();} } 復制代碼

可以看到我們在構造函數(shù)中實例化了 Vue,并且對 data 進行代理,為什么我們要進行代理呢?原因是通過代理我們就能夠直接通過 this.message 操作 message,而不需要 this.data.message,代理的關鍵也是我們上面所說的 Object.defineProperty。而生命周期其實在代碼中也是在特定時間點調(diào)用的函數(shù),所以我們做一些操作的時候也要去想想,它初始化完成沒有,新手經(jīng)常犯的錯誤就是在沒有完成初始化的時候去進行操作,所以對生命周期的理解是非常重要的。

好了,完成了初始化,下面我們就要開始寫如何監(jiān)聽這些數(shù)據(jù)的變化了。

Observer

通過上面的認識,我們知道,Observer 主要是給 data 的每個屬性都加上 getter 和 setter,以及在觸發(fā)相應的 get、set 的時候執(zhí)行的功能。

class Observer {constructor(data) {this.data = data;this.init();}init() {this.walk();}walk() {Object.keys(this.data).forEach(key => {this.defineReactive(key, this.data[key]);});}defineReactive(key, val) {const dep = new Dep();const observeChild = observe(val);Object.defineProperty(this.data, key, {enumerable: true,configurable: true,get() {if(Dep.target) {dep.addSub(Dep.target);}return val;},set(newVal) {if(newVal === val) {return;}val = newVal;dep.notify();observe(newVal);}});} }function observe(value, vm) {if(!value || typeof value !== 'object') {return;}return new Observer(value); } 復制代碼

在上面,我們完成了對 data 的監(jiān)聽,通過遞歸調(diào)用實現(xiàn)了對每個屬性值的監(jiān)聽,給每個數(shù)據(jù)都添加了 setter 和 getter,在我們對數(shù)據(jù)進行取值或者是賦值操作的時候都會觸發(fā)這兩個方法,基于這兩個方法,我們就能夠做更多的事了。

現(xiàn)在我們知道了怎么監(jiān)聽數(shù)據(jù),那么我們?nèi)绾稳ゾS護觀察者列表呢?我相信有些朋友和我一樣,看到 get 中的 Dep.target 有點懵逼,這到底是個啥,怎么用的,帶著這個疑問,我們來看看觀察者列表是如何實現(xiàn)的。

Dep

class Dep {constructor() {this.subs = [];}addSub(sub) {this.subs.push(sub);}notify() {this.subs.forEach(sub => sub.update());} }Dep.target = null; 復制代碼

在 Dep 中我們維護一個觀察者列表(subs),有兩個基礎的方法,一個是往列表中添加觀察者,一個是通知列表中所有的觀察者。可以看到我們最后一行的 Dep.target = null;,可能大家會好奇,這東西是干什么用的,其實很好理解,我們定義了一個全局的變量 Dep.target,又因為 JavaScript 是單線程的,同一時間只可能有一個地方對其進行操作,那么我們就能夠在觀察者觸發(fā) getter 的時候,將自己賦值給 Dep.target,然后添加到對應的觀察者列表中,這也就是上面的 Observer 的 getter 中有個對 Dep.target 的判斷的原因,然后當 Watcher 被添加到列表中,這個全局變量又會被設置成 null。當然了這里面有些東西還需要在 Watcher 中實現(xiàn),我們接下來就來看看 Watcher 如何實現(xiàn)。

Watcher

在寫代碼之前我們先分析一下,Watcher 需要一些什么基礎功能,Watcher 需要訂閱 Dep,同時需要更新 View,那么在代碼中我們實現(xiàn)兩個函數(shù),一個訂閱,一個更新。那么我們?nèi)绾巫龅接嗛喣?#xff1f;看了上面的代碼我們應該有個初步的認識,我們需要在 getter 中去將 Watcher 添加到 Dep 中,也就是依靠我們上面說的 Dep.target,而更新我們使用回調(diào)就能做到,我們看代碼。

class Watcher {constructor(vm, exp, cb) {this.vm = vm;this.exp = exp;this.cb = cb;this.value = this.get();}get() {Dep.target = this;const value = this.vm.data[this.exp.trim()];Dep.target = null;return value;}update() {const newVal = this.vm.data[this.exp.trim()];if(this.value !== newVal) {this.value = newVal;this.cb.call(this.vm, newVal);}} } 復制代碼

那么,我們有了 Watcher 之后要在什么地方去調(diào)用它呢?想這個問題之前,我們要思考一下,我們?nèi)绾文玫侥阍?template 中寫的各種 {{message}}、v-text等等指令以及變量。對,我們還有一個模版編譯的過程,那么我們是不是可以在編譯的時候去觸發(fā) getter,然后我們就完成了對這個變量的觀察者的添加,好了說了那么多,我們來看下下面的模塊如何去做。

Compile

Compile 主要要完成的工作就是把 template 中的模板編譯成 HTML,在編譯的時候拿到變量的過程也就觸發(fā)了這個數(shù)據(jù)的 getter,這時候就會把觀察者添加到觀察者列表中,同時也會在數(shù)據(jù)變動的時候,觸發(fā)回調(diào)去更新視圖。我們下面就來看看關于 Compile 這個模塊該怎么去完成。

// 判斷節(jié)點類型 const nodeType = {isElement(node) {return node.nodeType === 1;},isText(node) {return node.nodeType === 3;}, };// 更新視圖 const updater = {text(node, val) {node.textContent = val;},// 還有 model 啥的,但實際都差不多 };class Compile {constructor(el, vm) {this.vm = vm;this.el = document.querySelector(el);this.fragment = null;this.init();}init() {if(this.el) {this.fragment = this.nodeToFragment(this.el);this.compileElement(this.fragment);this.el.appendChild(this.fragment);}}nodeToFragment(el) {// 使用 document.createDocumentFragment 的目的就是減少 Dom 操作const fragment = document.createDocumentFragment();let child = el.firstChild;// 將原生節(jié)點轉移到 fragmentwhile(child) {fragment.appendChild(child);child = el.firstChild;}return fragment;}// 根據(jù)節(jié)點類型不同進行不同的編譯compileElement(el) {const childNodes = el.childNodes;[].slice.call(childNodes).forEach((node) => {const reg = /\{\{(.*)\}\}/;const text = node.textContent;// 根據(jù)不同的 node 類型,進行編譯,分別編譯指令以及文本節(jié)點if(nodeType.isElement(node)) {this.compileEl(node);} else if(nodeType.isText(node) && reg.test(text)) {this.compileText(node, reg.exec(text)[1]);}// 遞歸的對元素節(jié)點進行深層編譯if(node.childNodes && node.childNodes.length) {this.compileElement(node);}});}// 在這里我們就完成了對 Watcher 的添加compileText(node, exp) {const value = this.vm[exp.trim()];updater.text(node, value);new Watcher(this.vm, exp, (val) => {updater.text(node, val);});}compileEl(node) {const attrs = node.attributes;Object.values(attrs).forEach(attr => {var name = attr.name;if(name.indexOf('v-') >= 0) {const exp = attr.value;// 只做事件綁定const eventDir = name.substring(2);if(eventDir.indexOf('on') >= 0) {this.compileEvent(node, eventDir, exp);}}});}compileEvent(node, dir, exp) {const eventType = dir.split(':')[1];const cb = this.vm.methods[exp];if(eventType && cb) {node.addEventListener(eventType, cb.bind(this.vm));}} } 復制代碼

這就是 Compile 完成的部分工作,當然了這個模塊不會這么簡單,這里只是簡單的實現(xiàn)了一點功能,如今 Vue 2.0 引入了 Virtual DOM,對元素的操作也不像這么簡單了。

最后實現(xiàn)的功能由于我比較懶,大家可以自己寫一寫或者在我的 GitHub 倉庫里可以看到。

總結

上面的代碼也借鑒了前人的想法,但由于時間比較久了,所以我也沒找到,感謝大佬提供思路。

Vue 的設計很有意思,在學習之中也能有很多不一樣的感受,同時,在讀源碼的過程中,不要過多的追求讀懂每一個變量,每一個句子。第一遍代碼,先讀懂程序是怎么跑起來的,大概是怎么走的,通讀一遍,第二遍再去深究,扣一扣當時不清楚的東西,這是我看源碼的一些心得,可能每個人的方法不一樣,希望你能有所收獲。

最后,因為 Vue 2.0 已經(jīng)出來一段時間了,源碼也有很多的變動,生命周期的變化、Virtual DOM 等等,還有比較感興趣的 diff 算法,這些后續(xù)會繼續(xù)研究的,謝謝。


關注微信公眾號:創(chuàng)宇前端(KnownsecFED),碼上獲取更多優(yōu)質(zhì)干貨!

總結

以上是生活随笔為你收集整理的自己动手写一个 SimpleVue的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。