官網文檔介紹《Vue.js》html
MVVM 和 MVC 是兩種不一樣的軟件設計模式。vue
Vue 和 React 使用的是 MVVM 的設計模式,與傳統的 MVC 不一樣,它經過數據驅動視圖。MVVM 模式是組件化的基礎。node
MVVM: Model-View-ViewModel,數據驅動視圖react
MVC: Model-View-Controllerlinux
在 MVC 下,全部通訊都是單向的webpack
在不一樣的vue版本,實現響應式的方法不一樣:git
Object.defineProperty
Proxy
Vue 會遍歷 data 全部的 property,並使用 Object.defineProperty 把這些 property 所有轉爲 getter/setter
,每一個組件實例都對應一個 watcher 實例,它會在組件渲染的過程當中把「接觸」過的數據 property 記錄爲依賴。以後當依賴項的 setter 觸發時,會通知 watcher,從而使它關聯的組件從新渲染。es6
function defineReactive(target, key, value) { // 深度監聽(對象) Observer(value) // 核心API - 響應 Object.defineProperty(target, key, { get: function() { return value }, set: function(newVal) { if (value !== newVal) { // 深度監聽(對象) Observer(newVal) value = newVal updateView() } } }) } function updateView() { console.log('視圖更新') } // 從新定義數組原型 const oldArrayProperty = Array.prototype; // 建立新對象,原型指向 oldArrayProperty,再拓展新方法不會影響新原型 const arrProto = Object.create(oldArrayProperty) const methods = ['push', 'pop', 'shift', 'unshift', 'splice'] methods.forEach( methodName => { arrProto[methodName] = function() { updateView(); // 視圖更新 oldArrayProperty[methodName].call(this, ...arguments) // 調用數組原型方法進行更新 } }); function Observer(target) { if (typeof target !== 'object' || target === null) { return target } // 深度監聽(數組) if (Array.isArray(target)) { target.__proto__ = arrProto } for (key in target) { defineReactive(target, key, target[key]) } } const data = { name: 'jack', age: 18, info: { address: '北京' }, nums: [1, 2, 3] } // data 實現了雙向綁定,深度監聽 Observer(data) // data.info.address = '上海' // 深度監聽 // data.nums.push(4) // 監聽數組
Proxy 是 es6 新增的內置對象,它用於定義基本操做的自定義行爲。可用於運算符重載、對象模擬,對象變化事件、雙向綁定等。github
function reactive(target = {}) { if (typeof target !== 'object' || target === null) { // 非對象或數組,返回 return target } // 代理配置 const proxyConf = { get(target, key, receiver) { // 指處理自己(非原型的)屬性 const ownKeys = Reflect.ownKeys(target) if (ownKeys.includes(key)) { // 監聽 } const result = Reflect.get(target, key, receiver) // 在進行get的時候,再遞歸深度監聽 - 性能提高 return reactive(result) }, set(target, key, value, receiver) { // 重複數據, 不處理 if (value === target[key]) { return true } // 指處理自己(非原型的)屬性 const ownKeys = Reflect.ownKeys(target) if (ownKeys.includes(key)) { console.log('已有的key', key) } else { console.log('新增的key', key) } const result = Reflect.set(target, key, value, receiver) return result }, deleteProperty(target, key) { const result = Reflect.deleteProperty(target, key) return result } } // 生成代理對象 const observed = new Proxy(target, proxyConf) return observed } const data = { name: 'jack', age: 18, info : { city: 'beijing' } } const proxyData = reactive(data)
虛擬Dom 也就是 visual dom
,常叫爲 vdom
。vdom 是實現 vue 和 react 的重要基石。web
在瞭解 vdom 以前,瞭解一下瀏覽器的工做原理是很重要的。瀏覽器在渲染網頁時,會有幾個步驟,其中一個就是解析HTML,生成 DOM 樹。如下面 HTML 爲例:
<div> <h1>My title</h1> Some text content <!-- TODO: Add tagline --> </div>
當瀏覽器讀到這些代碼時,會解析爲對應的 DOM 節點樹
每個元素、文字、註釋都是一個節點,衆所周知,若是直接操做 dom 去更新,是很是耗費性能的,由於每一次的操做都會觸發瀏覽器的從新渲染。Js 的執行相對來講是很是快的,因而,便出現了 vdom。
snabbdom是一個簡潔強大的 vdom 庫,易學易用。vue 是參考它實現的 vdom 和 diff 算法。能夠經過 snabbdom 學習 vdom。
Vue 經過創建一個虛擬 DOM 來追蹤本身要如何改變真實 DOM,核心方法是createElement
函數。createElement
函數會生成一個虛擬節點,也就是 vNode
,它會告訴瀏覽器應該渲染什麼節點。vdom 是對由 Vue 組件樹創建起來的整個 vnode 樹的稱呼。
使用render
方式建立組件能更直觀看到 createElement 如何建立一個vnode(《render函數的約束》)
createElement(標籤名, 屬性對象, 文本/子節點數組)
Vue.component('my-component', { props: { title: { type: String, default: '標題' } }, data() { return { docUrl: 'https://cn.vuejs.org/v2/guide/render-function.html#%E5%9F%BA%E7%A1%80' } }, render(createElement) { return createElement( 'div', { 'class': 'page-container' }, [ createElement( 'h1', { attrs: { id: 'title' } }, this.title ), createElement( 'a', { attrs: { href: this.docUrl } }, 'vue文檔' ) ] ) } })
上面方法,會生成一個 vnode 樹(即AST 樹)
將關鍵屬性抽離出來後,能夠看到一個相似於瀏覽器解析 Html 的節點樹。這個結構會被渲染成真正的 Dom,並顯示在瀏覽器上。
{ "tag": "div", "data": { "class": "page-container" }, "children": [ { "tag": "h1", "data": { "attrs": { "id": "title" } } }, { "tag": "a", "data": { "attrs": { "href": "https://cn.vuejs.org/v2/guide/render-function.html#%E5%9F%BA%E7%A1%80" } } } ] }
初次渲染的時候,這個 AST 樹會被存儲起來,當監聽到數據有改變時,將被用來跟新的 vdom 作對比。這個對比的過程使用的是
diff
算法。
diff
算法是 vdom
中最核心、最關鍵的部分。vue 的 diff 算法處理位於 patch.js 文件中。
diff 即對比,是一個普遍的概念,不是 vue、react 特有的。如 linux diff 命令,git diff 等。
原樹 diff 算法須要經歷每一個節點遍歷對比,最後排序的過程。若是有1000個節點,須要計算1000^3=10億次,時間複雜度爲O(n^3)。
很明顯,直接使用原 diff 算法是不可行的。
vue 將 diff 的時間複雜度下降爲O(n),主要作了如下的優化:
模板編譯是指對 vue 文件內容的編譯轉換。Vue 的模板實際上被編譯成了 render 函數,執行 render 函數返回 vnode。
在瞭解模板編譯以前,須要先了解下with 語句。
with語句能夠擴展一個語句的做用域鏈。將某個對象添加到做用域鏈的頂部,默認查找該對象的屬性。
var obj = {a: 100}; // {} 內的自由變量,當作 obj 的屬性來查找 with(obj) { console.log(a); // 100 console.log(b); // ReferenceError: b is not defined }
不被推薦使用,在 ECMAScript 5 嚴格模式中該標籤已被禁止。
當使用 template 模板的時候,vue 會將模板解析爲 AST樹
(abstract syntax tree,抽象語法樹),語法樹再經過 generate 函數把 AST樹 轉化爲 render
函數,最後生成 vnode
對象。
vue-template-compiler
api:
*.vue
文件解析成flow declarationstemplate.js
const compiler = require('vue-template-compiler'); const template = '<p>{{message}}</p>' console.log(compiler.compile(template))
執行
# 編譯 node template.js
輸出,返回一個這樣的對象
{ ast: { type: 1, tag: 'p', attrsList: [], attrsMap: {}, rawAttrsMap: {}, parent: undefined, children: [ [Object] ], plain: true, static: false, staticRoot: false }, render: 'with(this){return _c(\'p\',[_v(_s(message))])}', staticRenderFns: [], errors: [], tips: [] }
使用 webpack 打包,在開發環境 vue-loader 實現了編譯
render 中 _c
表明 createElement
,其餘的縮寫函數說明:
function installRenderHelpers (target) { target._o = markOnce; target._n = toNumber; target._s = toString; target._l = renderList; target._t = renderSlot; target._q = looseEqual; target._i = looseIndexOf; target._m = renderStatic; target._f = resolveFilter; target._k = checkKeyCodes; target._b = bindObjectProps; target._v = createTextVNode; target._e = createEmptyVNode; target._u = resolveScopedSlots; target._g = bindObjectListeners; target._d = bindDynamicKeys; target._p = prependModifier; }
vue-template-compiler 會針對模板中的各類標籤、指令、事件進行提取拆分,分別處理。
Vue 在更新 DOM 時是異步執行的。只要偵聽到數據變化,Vue 將開啓一個隊列,並緩衝在同一事件循環中發生的全部數據變動。
簡單來講,事件循環會先執行完全部的宏任務(macro-task),再執行微任務(micro-task)。vue 將全部的更新都插入一個隊列,當這個隊列執行清空後再調用微任務。而 MutationObserver 、promise.then等都屬於微任務(setTimeout屬於宏任務)。
nextTick()
是更新後的回調函數,在 nextTick() 能夠拿到最新 dom 元素。
驗證
<template> <div class="hello"> <ul ref="list"> <li v-for="(item, index) in list" :key="index"> {{item}} </li> </ul> <button @click="handleClick">點擊</button> </div> </template> <script> export default { data() { return { list: [] } }, watch: { list: { handler: function(val) { console.log('watch', val.length) // 3 - 僅觸發一次 }, deep: true } }, methods: { handleClick() { // 修改 3 次 this.list.push(1) this.list.push(2) this.list.push(3) console.log('before>>', this.$refs.list.children.length) // 0 - 未更新 this.$nextTick(() => { console.log('after>>', this.$refs.list.children.length) // 3 - 已更新 }) } } } </script>
定義:nextTick (文件路徑:vue/src/core/util/next-tick.js)
var callbacks = []; // 全部須要執行的回調函數 var pending = false; // 狀態,是否有正在執行的回調函數 function flushCallbacks () { // 執行callbacks全部的回調 pending = false; var copies = callbacks.slice(0); callbacks.length = 0; for (var i = 0; i < copies.length; i++) { copies[i](); } } var timerFunc; // 保存正在被執行的函數 /** * 延遲調用函數支持的判斷 * 1. Promise.then * 2. then、MutationObserver * 3. setImmediate * 4. setTimeout(fn, 0) * */ if (typeof Promise !== 'undefined' && isNative(Promise)) { var p = Promise.resolve(); timerFunc = function () { p.then(flushCallbacks); if (isIOS) { setTimeout(noop); } }; } else if (!isIE && typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || MutationObserver.toString() === '[objectMutationObserverConstructor]')) { var counter = 1; var observer = new MutationObserver(flushCallbacks); var textNode = document.createTextNode(String(counter)); observer.observe(textNode, { characterData: true }); timerFunc = function () { counter = (counter + 1) % 2; textNode.data = String(counter); }; } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { timerFunc = function () { setImmediate(flushCallbacks); }; } else { timerFunc = function () { setTimeout(flushCallbacks, 0); }; } function nextTick (cb, ctx) { var _resolve; callbacks.push(function () { if (cb) { try { cb.call(ctx); } catch (e) { handleError(e, ctx, 'nextTick'); } } else if (_resolve) { _resolve(ctx); } }); if (!pending) { pending = true; timerFunc(); } // $flow-disable-line if (!cb && typeof Promise !== 'undefined') { return new Promise(function (resolve) { _resolve = resolve; }) } }
監聽變化:update (文件路徑:vue/src/core/observer/watcher.js)
// update 默認是異步的 update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true } else if (this.sync) { /*同步則執行run直接渲染視圖*/ this.run() } else { /*異步推送到觀察者隊列中,下一個tick時調用。*/ queueWatcher(this) } }
隊列監聽:queueWatcher (文件路徑:vue/src/core/observer/scheduler.js)
let waiting = false // 是否刷新 let flushing = false // 隊列更新狀態 // 重置 function resetSchedulerState () { index = queue.length = activatedChildren.length = 0 has = {} if (process.env.NODE_ENV !== 'production') { circular = {} } waiting = flushing = false } export function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) { has[id] = true if (!flushing) { // 未更新,則加入 queue.push(watcher) } else { // 已更新過,把這個watcher再放到當前執行的下一位, 當前的watcher處理完成後, 當即會處理這個最新的 let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } queue.splice(i + 1, 0, watcher) } // waiting 爲false, 等待下一個tick時, 會執行刷新隊列 if (!waiting) { waiting = true if (process.env.NODE_ENV !== 'production' && !config.async) { flushSchedulerQueue() return } // 執行視圖更新 nextTick(flushSchedulerQueue) } } }