vue核心原理全解

官網文檔介紹《Vue.js》html

MVVM

MVVM 和 MVC 是兩種不一樣的軟件設計模式vue

Vue 和 React 使用的是 MVVM 的設計模式,與傳統的 MVC 不一樣,它經過數據驅動視圖。MVVM 模式是組件化的基礎。node

MVVM

MVVM: Model-View-ViewModel,數據驅動視圖react

  • 各部分之間的通訊,都是雙向的
  • View 與 Model 不發生聯繫,經過 viewModel 傳遞

MVC

MVC: Model-View-Controllerlinux

  • View 傳送指令到 Controller
  • Controller 完成業務邏輯後,要求 Model 改變狀態
  • Model 將新的數據發送到 View,用戶獲得反饋

在 MVC 下,全部通訊都是單向的webpack

響應式原理

在不一樣的vue版本,實現響應式的方法不一樣:git

  • vue2.0:Object.defineProperty
  • vue3.0:Proxy

Object.defineProperty

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) // 監聽數組

優點

  • 兼容性好,支持 IE9

不足

  • 沒法監聽數組的變化
  • 必須遍歷對象的每一個屬性
  • 必須深層遍歷嵌套的對象
  • 沒法監聽新增屬性、刪除屬性
  • 須要在開始時一次性遞歸全部屬性

Proxy

Proxy 是 es6 新增的內置對象,它用於定義基本操做的自定義行爲。可用於運算符重載、對象模擬,對象變化事件、雙向綁定等。github

Proxy實現響應式

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)

優點

  • Proxy 能夠直接監聽對象而非屬性,能夠監聽新增/刪除屬性;
  • Proxy 能夠直接監聽數組的變化;
  • Proxy 有多達 13 種攔截方法,不限於 apply、ownKeys、deleteProperty、has 等等是 Object.defineProperty 不具有的;
  • Proxy 返回的是一個新對象,咱們能夠只操做新的對象達到目的,而 Object.defineProperty 只能遍歷對象屬性直接修改;

不足

  • 兼容性問題,並且沒法使用 polyfill 抹平(es5 中沒有能夠模擬Proxy的函數/方法)

虛擬Dom

虛擬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

snabbdom是一個簡潔強大的 vdom 庫,易學易用。vue 是參考它實現的 vdom 和 diff 算法。能夠經過 snabbdom 學習 vdom。

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算法

diff 算法是 vdom 中最核心、最關鍵的部分。vue 的 diff 算法處理位於 patch.js 文件中。

diff 即對比,是一個普遍的概念,不是 vue、react 特有的。如 linux diff 命令,git diff 等。

二叉樹diff算法

原樹 diff 算法須要經歷每一個節點遍歷對比,最後排序的過程。若是有1000個節點,須要計算1000^3=10億次,時間複雜度爲O(n^3)。

很明顯,直接使用原 diff 算法是不可行的。

vue中的diff算法

vue 將 diff 的時間複雜度下降爲O(n),主要作了如下的優化:

  • 只比較同一層級,不跨級比較
  • tag 不相同,則直接刪掉重建,再也不深度比較
  • tag 和 key 二者都相同,則認爲是相同節點,再也不深度比較



模板編譯

模板編譯是指對 vue 文件內容的編譯轉換。Vue 的模板實際上被編譯成了 render 函數,執行 render 函數返回 vnode。

with語句

在瞭解模板編譯以前,須要先了解下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

vue-template-compiler api:

  • compile(): 編譯 template 標籤內容,並返回一個對象
  • parseComponent(): 將單文件組件或*.vue文件解析成flow declarations
  • compileToFunctions(): 相似 compiler.compile,但直接返回實例化函數
  • ssrCompile(): 相似 compiler.compile ,將部分模板優化成字符串鏈接來生成特定於SSR的呈現函數代碼
  • ssrCompileToFunctions(): 相似 compileToFunction , 將部分模板優化成字符串鏈接來生成特定於SSR的呈現函數代碼
  • generateCodeFrame(): 將 template 標籤內容高亮顯示

舉個栗子

template.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 會針對模板中的各類標籤、指令、事件進行提取拆分,分別處理。

組件渲染與更新

初次渲染

  1. 解析模板爲 render 函數(或在開發環境已完成,vue-loader)
  2. 觸發響應式,監聽 data 屬性 getter setter
  3. 執行 render 函數,生成 vnode
  4. path(elem, vnode)

更新過程

  1. 修改 data,觸發 setter(此前在 getter 中已被監聽)
  2. 從新執行 render 函數,生成 newVnode
  3. path(vnode, newVnode)

異步更新

Vue 在更新 DOM 時是異步執行的。只要偵聽到數據變化,Vue 將開啓一個隊列,並緩衝在同一事件循環中發生的全部數據變動。

簡單來講,事件循環會先執行完全部的宏任務(macro-task),再執行微任務(micro-task)。vue 將全部的更新都插入一個隊列,當這個隊列執行清空後再調用微任務。而 MutationObserver 、promise.then等都屬於微任務(setTimeout屬於宏任務)。

nextTick()

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)
        }
    }
}
相關文章
相關標籤/搜索