一、響應式原理揭祕
(1)頁面部分html
<body> <div id="app"> <p>{{name}}</p> <p k-text="name"></p> <p>{{age}}</p> <p> {{doubleAge}} </p> <input type="text" k-model="name"> <button @click="changeName">呵呵</button> <div k-html="html"></div> </div> <script src='./watcher.js'></script> <script src='./compile.js'></script> <script src='./kvue.js'></script> <script src='./dep.js'></script> <script> let kaikeba = new KVue({ el:'#app', data: { name: "I am test.", age:12, html:'<button>這是一個按鈕</button>' }, created(){ console.log('開始啦') setTimeout(()=>{ this.name='我是蝸牛' }, 15000) }, methods:{ changeName(){ this.name = '哈嘍,開課吧' this.age=1 this.id = 'xx' console.log(1,this) } } }) </script> </body>
(2)響應式實現代碼:
響應式原理口訣:
Vue定義每一個響應式數據時,都建立一個Dep依賴收集容器。
Compile編譯每一個綁定數據的節點時,都建立一個Watcher監聽數據變化。
數據-->視圖 綁定依靠Watcher。
視圖-->數據 綁定依靠addEventListener('input/change...')。vue
/** * 一、將數據丁意思成響應式 * 二、用this.key 代理 this.$data.key的訪問 */ class KVue { constructor(options) { this.$data = options.data this.$options = options this.observer(this.$data) if(options.created){ options.created.call(this) } this.$compile = new Compile(options.el, this) } observer(value) { if (!value || (typeof value !== 'object')) { return } Object.keys(value).forEach((key) => { this.proxyData(key) // 用this.key 代理 this.$data.key的訪問 this.defineReactive(value, key, value[key]) // 將數據丁意思成響應式 }) } defineReactive(obj, key, val) { debugger const dep = new Dep() Object.defineProperty(obj, key, { enumerable: true, configurable: true, get() { // 將Dep.target(即當前的Watcher對象存入Dep的deps中) Dep.target && dep.addDep(Dep.target) console.log(dep); return val }, set(newVal) { debugger if (newVal === val) return val = newVal // 在set的時候觸發dep的notify來通知全部的Watcher對象更新視圖 dep.notify() } }) } proxyData(key) { Object.defineProperty(this, key, { configurable: false, enumerable: true, get() { return this.$data[key] }, set(newVal) { this.$data[key] = newVal } }) } } /** * 每一個數據定義成響應式時,都建立一個依賴收集容器 * 在Compiler編譯器編譯文檔時,讀取數據(觸發數據get方法)時 */ class Dep { constructor() { // 存數全部的依賴 this.deps = [] } // 在deps中添加一個監聽器對象 addDep(dep) { this.deps.push(dep) } depend() { // 貌似該方法沒用過 Dep.target.addDep(this) } // 通知全部監聽器去更新視圖 notify() { this.deps.forEach((dep) => { dep.update() }) } } /** * 編譯文檔,深度優先遍歷全部HTML節點,完成vm命名空間下 「節點初始賦值」 和 「節點數據雙向監聽綁定」 * 數據 --> 視圖節點綁定: 爲監聽數據的節點建立Watcher(vm, key, cb),cb(node, value)更新視圖 * 視圖節點 --> 數據綁定:爲輸入控件例如<input> 添加 node.addEventListener('input', cb), cb(vm, key)更新數據 */ class Compile { constructor(el,vm) { this.$vm = vm this.$el = document.querySelector(el) if (this.$el) { this.$fragment = this.node2Fragment(this.$el) this.compileElement(this.$fragment) this.$el.appendChild(this.$fragment) } } node2Fragment(el) { // 建立文檔片斷 DocumentFragment let fragment = document.createDocumentFragment() let child // 將原生節點拷貝到文檔片斷 while (child = el.firstChild) { // appendChild() 方法向節點添加最後一個子節點 // 當子節點來源於原生節點時,appendChild() 方法從一個元素向另外一個元素中移動元素 fragment.appendChild(child) } return fragment } compileElement(el) { let childNodes = el.childNodes Array.from(childNodes).forEach((node) => { let text = node.textContent // 表達式文本 // 就是識別{{}}中的數據 let reg = /\{\{(.*)\}\}/ // 按元素節點方式編譯 if (this.isElementNode(node)) { this.compile(node) } else if (this.isTextNode(node) && reg.test(text)) { // 文本 而且有{{}} this.compileText(node, RegExp.$1) } // 遍歷編譯子節點 if (node.childNodes && node.childNodes.length) { this.compileElement(node) } }) } compile(node) { let nodeAttrs = node.attributes Array.from(nodeAttrs).forEach( (attr)=>{ // 規定:指令以 v-xxx 命名 // 如 <span v-text="content"></span> 中指令爲 v-text let attrName = attr.name // v-text let exp = attr.value // content if (this.isDirective(attrName)) { let dir = attrName.substring(2) // text // 普通指令 this[dir] && this[dir](node, this.$vm, exp) } if(this.isEventDirective(attrName)){ let dir = attrName.substring(1) // text this.eventHandler(node, this.$vm, exp, dir) } }) } compileText(node, exp) { this.text(node, this.$vm, exp) } isDirective(attr) { return attr.indexOf('k-') == 0 } isEventDirective(dir) { return dir.indexOf('@') === 0 } isElementNode(node) { return node.nodeType == 1 } isTextNode(node) { return node.nodeType == 3 } text(node, vm, exp) { this.update(node, vm, exp, 'text') } html(node, vm, exp) { this.update(node, vm, exp, 'html') } model(node, vm, exp) { this.update(node, vm, exp, 'model') node.addEventListener('input', (e)=>{ let newValue = e.target.value vm[exp] = newValue }) } // 編譯發現節點有數據綁定時,執行對應updater(node, value)賦值 // 並在vm命名空間下建立Watcher(vm, key, cb)監聽vm.key改動,再次執行更新函數 update(node, vm, exp, dir) { let updaterFn = this[dir + 'Updater'] updaterFn && updaterFn(node, vm[exp]) new Watcher(vm, exp, function(value) { updaterFn && updaterFn(node, value) }) } // 編譯發現節點有事件綁定時,在命名空間vm中找對應方法,綁定到節點事件上 eventHandler(node, vm, exp, dir) { let fn = vm.$options.methods && vm.$options.methods[exp] if (dir && fn) { node.addEventListener(dir, fn.bind(vm), false) } } textUpdater(node, value) { node.textContent = value } htmlUpdater(node, value) { node.innerHTML = value } modelUpdater(node, value) { node.value = value } } /** * 編譯文檔時,每一個綁定數據的節點,都建立一個Watcher監聽數據變化 * Watcher初始化時訪問所監聽響應式數據,並把自身賦值給Dep.target,這時響應式數據執行get方法,給本身的Dep收集到Watcher後,清空Dep.target. */ class Watcher { /** * * @param {Vue} vm Vue實例,做用空間 * @param {String} key 監聽屬性 * @param {Function} cb 更新視圖的方法 */ constructor(vm, key, cb) { // 在new一個監聽器對象時將該對象賦值給Dep.target,在get中會用到 // 將 Dep.target 指向本身 // 而後觸發屬性的 getter 添加監聽 // 最後將 Dep.target 置空 this.cb = cb this.vm = vm this.key = key this.value = this.get() } get() { Dep.target = this let value = this.vm[this.key] // 監聽屬性的值 return value } // 更新視圖的方法 update() { this.value = this.get() // 有嚴重缺陷,反覆調用響應式數據的get方法會重複建立Watcher並添加到數據的Dep之中,執行幾回後瀏覽器便會卡死。 this.cb.call(this.vm, this.value) } }
二、diff&patch過程及算法node
三、keep-alive原理算法
巴拉巴拉
應用場景:npm
四、CSS的scoped私有做用域和深度選擇器原理瀏覽器
編譯前: <div class="example">測試文本</div> <style scoped> .example { color: red; } 編譯後: <div data-v-4fb7e322 class="example">測試文本</div> <style scoped> .example[data-v-4fb7e322] { color: red; }
原理:給Vue組件的全部標籤添加一個相同的屬性,私有樣式選擇器都加上該屬性。可是第三方組件(如UI庫)內部的標籤並無編譯爲附帶[data-v-xxx]這個屬性,因此局部樣式對第三方組件不生效。
注意:儘可能別用標籤選擇器,特別是與特性選擇器組合使用時會慢不少。取而代之能夠給標籤起個class名或者id。
若是但願scoped樣式中的一個選擇器可以做用的更深,例如影響子組件,可使用 >>> 操做符。而對於less或者sass等預編譯,是不支持 >>> 操做符的,可使用 /deep/ 來替換。緩存
五、nextTick原理:
使用Vue.js的global API的$nextTick方法,便可在回調中獲取已經更新好的DOM實例了。sass
nextTick的實現比較簡單,執行的目的是在microtask或者task中推入一個function,在當前棧執行完畢(也許還會有一些排在前面的須要執行的任務)之後執行nextTick傳入的function,看一下源碼:網絡
/** * Defer a task to execute it asynchronously. */ /* 延遲一個任務使其異步執行,在下一個tick時執行,一個當即執行函數,返回一個function 這個函數的做用是在task或者microtask中推入一個timerFunc,在當前調用棧執行完之後以此執行直到執行到timerFunc 目的是延遲到當前調用棧執行完之後執行 */ export const nextTick = (function () { /*存放異步執行的回調*/ const callbacks = [] /*一個標記位,若是已經有timerFunc被推送到任務隊列中去則不須要重複推送*/ let pending = false /*一個函數指針,指向函數將被推送到任務隊列中,等到主線程任務執行完時,任務隊列中的timerFunc被調用*/ let timerFunc /*下一個tick時的回調*/ function nextTickHandler () { /*一個標記位,標記等待狀態(即函數已經被推入任務隊列或者主線程,已經在等待當前棧執行完畢去執行),這樣就不須要在push多個回調到callbacks時將timerFunc屢次推入任務隊列或者主線程*/ pending = false /*執行全部callback*/ const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } } ...... }
看了源碼發現timerFunc會檢測當前環境而不一樣實現,其實就是按照Promise,MutationObserver,setTimeout優先級,哪一個存在使用哪一個,最不濟的環境下使用setTimeout。
這裏解釋一下,一共有Promise、MutationObserver以及setTimeout三種嘗試獲得timerFunc的方法。
優先使用Promise,在Promise不存在的狀況下使用MutationObserver,這兩個方法的回調函數都會在microtask中執行,它們會比setTimeout更早執行,因此優先使用。
(1)首先是Promise,Promise.resolve().then()能夠在microtask中加入它的回調,
(2)MutationObserver新建一個textNode的DOM對象,用MutationObserver綁定該DOM並指定回調函數,在DOM變化的時候則會觸發回調,該回調會進入microtask,即textNode.data = String(counter)時便會加入該回調。
(3)setTimeout是最後的一種備選方案,它會將回調函數加入task中,等到執行。
綜上,nextTick的目的就是產生一個回調函數加入task或者microtask中,當前棧執行完之後(可能中間還有別的排在前面的函數)調用該回調函數,起到了異步觸發(即下一個tick時觸發)的目的。app
如今有這樣的一種狀況,mounted的時候test的值會被++循環執行1000次。每次++時,都會根據響應式觸發setter->Dep->Watcher->update->patch。
因此Vue.js實現了一個queue隊列,在下一個tick的時候會統一執行queue中Watcher的run。同時,擁有相同id的Watcher不會被重複加入到該queue中去,因此不會執行1000次Watcher的run。最終更新視圖只會直接將test對應的DOM的0變成1000。保證更新視圖操做DOM的動做是在當前棧執行完之後下一個tick的時候調用,大大優化了性能。
100、Hiper:一款使人愉悅的性能分析工具
安裝: npm install hiper -g 使用: #請求頁面,報告DNS查詢耗時、TCP鏈接耗時、TTFB... hiper baidu.com?a=1&b=2 //省略協議頭時,默認添加 https:// #加載指定頁面100次 hiper -n 100 "baidu.com?a=1&b=2" --no-cache #使用指定useragent加載網頁100次 hiper -n 100 "baidu.com?a=1&b=2" -u "Mozilla/5.0(Macintosh;Intel Mac OS X 10_13_14) AppleWebkit/537.36(KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36"
平時咱們查看性能的方式,是在performance和network中查看數據,記錄下幾個關鍵的性能指標,而後刷新幾回再看這些性能指標。有時候咱們發現,因爲採樣太少,受當前【網絡】、【CPU】、【內存】的繁忙成都的影響很重,有時優化後的項目反而比優化前更慢。 若是有一個工具,一次性的請求N次網頁,而後把各個性能指標取出來求平均值,咱們就能很是準確的知道這個優化是【正優化】仍是【負優化】。 hiper就是解決這個痛點的。