Vue
1. Vue原理
Vue是采用數據劫持配合發(fā)布者-訂閱者模式,通過
Object.defineProperty()來劫持各個屬性的getter和setter。在數據發(fā)生變化的時候,發(fā)布消息給依賴收集器,去通知觀察者,做出對應的回調函數去更新視圖。
具體執(zhí)行流程:
1.MVVM作為綁定入口,整合Observe,Compil和Watcher三者,通過Observe來監(jiān)聽model的變化。
2.通過Compil來解析編譯模版指令,最終利用Watcher搭起Observe和Compil之前的通信橋梁。
3.從而達到數據變化 => 更新視圖,視圖交互變化(input) => 數據model變更的雙向綁定效果。
2. Vue的生命周期
- 單個組件的生命周期
1.beforeCreated
2.created
3.beforeMounted
4.mounted
5.activated
6.beforeUpdated
7.updated
8.deactivated
9.beforeDestory
10.destoryed
- 父子組件的生命周期順序
1.父組件先執(zhí)行(beforeCreated -> created -> beforeMounted)函數
2.父組件掛載前,子組件再執(zhí)行(beforeCreated -> created -> beforeMounted -> mounted )。
3.子組件掛載完成之后,最后執(zhí)行父組件掛載函數mounted。
4.接著是下面的三種情況:
(1)更新
只更新父或子: 局部更新,父或子beforeUpdate -> updated
同時更新父和子: 父beforeUpdate -> 子beforeUpdate -> 子updated -> 父updated
(2)銷毀父組件
父beforeDestroy -> 子beforeDestroy -> 子destroyed -> 父destroyed
(3)激活父組件
子activated -> 父activated -> 停止 -> 子deactivated -> 父deactivated
3. Vue響應式原理
使用
defineReactive函數將深度遍歷一個對象(或循環(huán)遍歷數組),將對象構建成響應式式對象。明顯的標志就是ob屬性 實質是通過Object.defineProperty對屬性(深度遍歷)進行setter和getter攔截。get中主要做依賴收集dep.depend()【子屬性也收集該依賴】set中主要做派發(fā)更新 (新的值才更新)dep.notify()調用dep數組中每個渲染watcher的update方法更新DOM響應式對象使用應該注意哪些點
1.對象的新增屬性,數組的新增元素,因為不是響應式的,所以不會觸發(fā)視圖渲染。此時應該使用 $set
2.改變某一下標的元素,因為Vue未實現(xiàn)監(jiān)聽,所以不會觸發(fā)視圖渲染。此時應該使用 $set
3.刪除對象的屬性,數組下標的某一元素,確保刪除屬性能觸發(fā)視圖渲染。此時應該使用 $delete
4. data為什么必須是函數而不是對象?
- 首先舉個栗子
var option = {
data: {
a: 1
}
}
class component {
constructor(opt) {
this.data = opt.data;
Object.defineProperty(this.data, `a`, {
get: function () {
console.log('get val');
return this._a;
},
set: function (newVal) {
console.log('set val:' + newVal);
this._a = newVal;
}
});
}
}
var c1 = new component(option);
var c2 = new component(option);
c1.data.a = 3;
c2.data.a = 5;
console.log(`c1 : ` + c1.data.a);//c1 : 5
console.log(`c2 : ` + c2.data.a);//c2 : 5
示例代碼中 Object.defineProperty 把傳進來組件中的data 的 a 屬性轉化為 getter 和 setter,可以實現(xiàn) data.a的數據監(jiān)控。這個轉化是Vue.js 響應式的基石。
這樣就不難理解data為什么不能是對象了,如果傳進來是對象,new出來的兩個實例同時引用一個對象,那么當你修改其中一個屬性的時候,另外一個實例也會跟著改。
總結:
1.對象是對于內存地址的引用。直接定義個對象的話,組件之間都會使用這個對象,這樣會造成組件之間數據相互影響。
2.組件就是為了抽離開來提高復用的, 如果組件之間數據默認存在關系,就違背了組件的意義。
3.函數 return 一個新的對象,其實還是為 data 定義了一個對象, 只不過這個對象是內存地址的單獨引用了,這樣組件之間就不會存在數據干擾的問題。
5. v-model基本原理
- 首先在編譯階段,
v-model被當成普通指令解析到el.directives,然后在解析v-model的時候,會根據節(jié)點的不同請求去執(zhí)行不同的邏輯。
1.如果節(jié)點是select、checkbox, radio,則監(jiān)聽的是change事件
2.如果節(jié)點是input,textarea,則監(jiān)聽一般是input事件,在.lazy下的情況下是change事件。
3.如果節(jié)點是組件,則是使用自定義的回調函數
- 在運行的時候,通過相應事件的監(jiān)聽函數去更改數據
v-model實質是一種語法糖,換成模板寫法如下:
<input :value="sth" @input="sth = $event.target.value" />
- 組件中使用
v-model
// 自定義屬性名和事件名需要一致
export default {
model: {
prop: 'num', // 自定義屬性名
event: 'addNum' // 自定義事件名
},
props: {
num: Number,
},
methods: {
add() {
this.$emit('addNum', this.num + 1)
}
}
}
6. vue2.0響應式的缺陷
Object.defineProperty無法監(jiān)控到數組下標的變化,導致通過數組下標添加元素,不能實時響應Object.defineProperty本身是可以監(jiān)控到數組下標的變化的,但是在Vue中,從性能/體驗的性價比考慮,棄用了這個特性。Object.defineProperty只能劫持對象的屬性,從而需要對每個對象,每個屬性進行遍歷,如果,屬性值是對象,還需要深度遍歷
7. Vue3.0為什么使用Proxy實現(xiàn)響應式
Proxy可以劫持整個對象,并返回一個新的對象。Proxy不僅可以代理對象,還可以代理數組。還可以代理動態(tài)增加的屬性。
8. Vue的通信方式
props和$emit$parent和$childrenvueBus: 中央事務總線,一個發(fā)布訂閱中心vuex:狀態(tài)樹管理(單一的)ref和refs$attr和$listener: v-bind="$attrs" v-on="$listeners"provide和inject: 實質就是遞歸父組件幫你尋找對應的providerprovide和inject綁定并不是可響應的。這是刻意為之的。然而,如果你傳入了一個可監(jiān)聽的對象,那么其對象的屬性還是可響應的。
9. Vue.nextTick的原理
Vue.nextTick是在執(zhí)行render渲染后運行的,即在render渲染后的下一次tick(event loop最開始的時候執(zhí)行)Vue.nextTikc的降級順序(優(yōu)先使用)Promise.then(microtask) , MutationObserver(microtask) , setImmediate(task) , setTimeout(fn, 0)(task)Vue在修改數據后,視圖不會立刻更新,而是等同一事件循環(huán)中的所有數據變化完成之后,再統(tǒng)一進行視圖更新。應用場景
1.在Vue生命周期的created()鉤子函數進行DOM操作一定要放到Vue.nextTick()的回調函數中。
2.在數據變化后要執(zhí)行的某個操作,而這個操作需要使用隨數據改變而改變的DOM結構的時候,這個操作都應該放進Vue.nextTick()的回調函數中。
-
10. new Vue會做什么操作
Vue.prototype._init = function (options) {
const vm = this
// ...忽略,從第45行看起
if (process.env.NODE_ENV !== 'production') {
initProxy(vm) // 作用域代理,攔截組件內訪問其它組件的數據
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm) // 建立父子組件關系,在當前實例上添加一些屬性和生命周期標識。
initEvents(vm) // 用來存放除 @hook:生命周期鉤子名稱="綁定的函數"事件的對象。如:$on、 $emit等
initRender(vm) // 用于初始化 $slots、 $attrs、 $listeners
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props // 初始化 inject,一般用于更深層次的組件通信,相當于加強版子組件的 props。用于組件庫開發(fā)較多
initState(vm) // 是很多選項初始化的匯總,包括:props、methods、data、computed和watch 等。
initProvide(vm) // resolve provide after data/props // 初始化 provide
callHook(vm, 'created')
// ...忽略
if (vm.$options.el) {
vm.$mount(vm.$options.el) // 掛載實例
}
}
11. Vue的diff原理
- 主要執(zhí)行的是patch函數。主要流程如下:
function patch (oldVnode, vnode, hydrating, removeOnly)
1.如果oldVnode不存在,即是新添加的節(jié)點,則創(chuàng)建vnode的DOM
2.如果不是真實的節(jié)點且是相同類型的節(jié)點,則進入結點diff,即patchVnode函數。否則會用新的節(jié)點替換老的。這里的相同類型指的是具有相同的key值和一些其他條件,例如標簽相同等等。
3.如果oldVnode === vnode,則認為沒有變化, 如果oldVnode的isAsyncPlaceholder屬性為true時,跳過檢查異步組件,return;
4.如果oldVnode跟vnode都是靜態(tài)節(jié)點(實例不會發(fā)生變化),且具有相同的key,并且當vnode是克隆節(jié)點或是v-once指令控制的節(jié)點時,則把oldVnode.elm和oldVnode.child都復制到vnode上;
5.如果vnode不是文本節(jié)點或注釋節(jié)點
(1)如果vnode和oldVnode都有子節(jié)點并且兩者的子節(jié)點不一致時,就調用updateChildren更新子節(jié)點
(2)如果只有vnode有子節(jié)點,則調用addVnodes創(chuàng)建子節(jié)點
(3)如果只有oldVnode有子節(jié)點,則調用removeVnodes把這些子節(jié)點都刪除
(4)如果vnode文本為undefined,則清空vnode.elm文本;
6.如果vnode是文本節(jié)點但是vnode.text != oldVnode.text時只需要更新vnode.elm的文本內容就可以。
7.在updateChildren主要是子節(jié)點數組對比,思路是通過首尾兩端對比,如果是相同類型的節(jié)點也會使用patchVnode函數。
- 在做對比中
key的作用 主要是
1.決定節(jié)點是否可以復用
2.建立key-index的索引,主要是替代遍歷,提升性能
12. computed 和 watcher
computed是計算屬性,依賴其他屬性計算,并且computed的值有緩存,只有當計算值發(fā)生變化才會返回內容。所以,對于任何復雜邏輯,你都應當使用計算屬性。watch主要用于監(jiān)控vue實例的變化,它監(jiān)控的變量當然必須在data里面聲明才可以,它可以監(jiān)控一個變量,也可以是一個對象。比較適合的場景是一個數據影響多個數據。watch支持異步。watcher的分類
1.內部-watcher vue組件上的每一條數據綁定指令(例如{{myData}})和computed屬性,通過compile最后都會生成一個對應的 watcher 對象。
2.user--watcher 在watch屬性中,由用戶自己定義的,都屬于這種類型,即只要監(jiān)聽的屬性改變了,都會觸發(fā)定義好的回調函數
3.render-watcher每一個組件都會有一個 render-watcher, function () {vm._update(vm._render(), hydrating);}, 當 data / computed中的屬性改變的時候,會調用該 render-watcher 來更新組件的視圖
watcher 也有固定的執(zhí)行順序,分別是:內部-watcher -> user-watcher -> render-watcher
13. Vue指令
// 全局
Vue.directive('my-click', config)
// 局部
new Vue({
directives:{
focus: config // v-focus
}
}})
- 配置參數
1.一個指令定義對象可以提供如下幾個鉤子函數 (均為可選):
(1)bind:只調用一次,指令第一次綁定到元素時調用。在這里可以進行一次性的初始化設置。
(2)inserted:被綁定元素插入父節(jié)點時調用 (僅保證父節(jié)點存在,但不一定已被插入文檔中)。
(3)update:所在組件的 VNode 更新時調用,但是可能發(fā)生在其子 VNode 更新之前。指令的值可能發(fā)生了改變,也可能沒有。但是你可以通過比較更新前后的值來忽略不必要的模板更新。
(4)componentUpdated:指令所在組件的 VNode 及其子 VNode 全部更新后調用。
(5)unbind:只調用一次,指令與元素解綁時調用。
每個鉤子函數都有四個參數el、binding、vnode 和 oldVnode
14. 混入 (mixin)
混入 (
mixin) 提供了一種非常靈活的方式,來分發(fā) Vue 組件中的可復用功能。全局和局部
mixin
var mixin = {
data: function () {
return {
message: 'hello',
foo: 'abc'
}
}
}
<!-- 全局mixin -->
Vue.mixin(mixin)
<!-- 局部mixin -->
new Vue({
mixins: [mixin],
})
- 合并策略
1.鉤子函數將合并成數組,且混入的函數先執(zhí)行
2.其他的值為對象的將被合并為同一個對象。兩個對象鍵名沖突時,取組件對象的鍵值對。
3.默認的合并策略可以使用下面的方面更改
Vue.config.optionMergeStrategies.myOption = function (toVal, fromVal) {
// 返回合并后的值
}
15. vue-router
-
Vue Router是Vue.js官方的路由管理器。它和Vue.js的核心深度集成,讓構建單頁面應用變得易如反掌
<router-link>和<router-view>和<keep-alive>
- 路由模式
1.HashHistory模式:實質是監(jiān)聽onhashchange事件 (window.location API - location.hash)
2.HTML5History模式:實質是使用h5的 window.history API, 監(jiān)聽popstate事件(pushState, replaceState)。使用該模式,服務器和前端需要做好頁面404的處理
3.AbstractHistory模式:在不支持上面兩種方式的環(huán)境下使用,如node環(huán)境,實際是使用數組模擬路由歷史棧
- 導航守衛(wèi)
// 全局守衛(wèi)
// 在項目中,一般在beforeEach這個鉤子函數中進行路由跳轉的一些信息判斷。
判斷是否登錄,是否拿到對應的路由權限等等。
router.beforeEach((to, from, next) => {
to: Route: // 即將要進入的目標 路由對象
from: Route: // 當前導航正要離開的路由
next: Function: // 一定要調用該方法來 resolve 這個鉤子。
})
router.afterEach((to, from) => {})
router.beforeResolve((to, from) => {})
// 與afterEach類似, 區(qū)別是在導航被確認之前,同時在所有組件內守衛(wèi)和異步路由組件被解析之后,解析守衛(wèi)就被調用
// 路由獨享守衛(wèi)
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {},
...
}
]
})
// 組件內守衛(wèi)
const Foo = {
template: `...`,
beforeRouteEnter (to, from, next) {
// 在渲染該組件的對應路由被 confirm 前調用
// 不!能!獲取組件實例 `this`
// 因為當守衛(wèi)執(zhí)行前,組件實例還沒被創(chuàng)建
},
beforeRouteUpdate (to, from, next) {
// 在當前路由改變,但是該組件被復用時調用
// 舉例來說,對于一個帶有動態(tài)參數的路徑 /foo/:id,在 /foo/1 和 /foo/2 之間跳轉的時候,
// 由于會渲染同樣的 Foo 組件,因此組件實例會被復用。而這個鉤子就會在這個情況下被調用。
// 可以訪問組件實例 `this`
},
beforeRouteLeave (to, from, next) {
// 導航離開該組件的對應路由時調用
// 可以訪問組件實例 `this`
}
16. VueRouter
基于
vue的插件機制,全局混入beforeCreated和destroyed的生命鉤子查找根實例上的
route,注入到每個組件上,監(jiān)聽current變化
Vue.util.defineReactive(this, '_route', this._router.history.current)
-
vue原型上添加兩個屬性$router和$route, 攔截get操作,限制set操作
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
- 注冊全局組件RouterView 和 RouterLink
17. Vuex
Vue.js是一個專為Vue.js應用程序開發(fā)的狀態(tài)管理模式。它采用集中式存儲管理應用的所有組件的狀態(tài),并以相應的規(guī)則保證狀態(tài)以一種可預測的方式發(fā)生變化。核心概念
1.state:使用單一狀態(tài)樹,用一個對象就包含了全部的應用層級狀態(tài)。
2.getter:可看成數據的computed計算屬性
3.mutation:唯一更改數據的方法 通過 store.commit 使用相應的 mutation方法
4.Action:支持異步的提交mutation 通過 store.dispatch 使用相應的Action方法
5.module:數據分模塊。如moduleA.xx
- 如何注入
在使用 Vue.use(vuex)的時候會執(zhí)行install 方法在(vue插件機制)。這個方法會混入一個minxin
Vue.mixin({
beforeCreate() {
const options = this.$options
// store injection
// 非根組件指向其父組件的$store,使得所有組件的實例,都指向根的store對象
if (options.store) {
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store
}
}
})
- 如何實現(xiàn)響應式
通過添加到data中實現(xiàn)響應式
store._vm = new Vue({
data: {
$$state: state
},
computed // 這里是store的getter
})
18. 首屏加載慢的優(yōu)化方案
-
webpack來打包Vue項目,vendor.js過大問題解決
1.造成過大的原因是因為在main.js導入第三庫太多時,webpack合并js時生成了vendor.js(我們習慣把第三方庫放在vendor里面)造成的,js文件過多,拖慢加載速度,所以:首先在index.html中,使用CDN的資源
<body>
<script src="https://cdn.bootcss.com/vue/2.5.2/vue.min.js"></script>
<script src="https://cdn.bootcss.com/vue-router/3.0.1/vue-router.min.js"></script>
<script src="https://cdn.bootcss.com/vuex/3.0.1/vuex.min.js"></script>
<script src="https://unpkg.com/vue-awesome-swiper@2.6.7/dist/vue-awesome-swiper.js"></script>
</body>
2.在bulid/webpack.base.conf.js文件中,添加externals,導入index.html下所需的資源模塊:
module.exports = {
context: path.resolve(__dirname, '../'),
entry: {
app: ['babel-polyfill', 'lib-flexible', './src/main.js']
},
externals: { // <-添加
vue: 'Vue',
vuex: 'Vuex',
'vue-router': 'VueRouter',
VueAwesomeSwiper: 'VueAwesomeSwiper'
},
3.在main.js里將以下 import 注釋 替換 require 引入模塊
// import Vue from 'vue'
// import VueAwesomeSwiper from 'vue-awesome-swiper'
const Vue = require('vue')
const VueAwesomeSwiper = require('VueAwesomeSwiper')
Vue.use(VueAwesomeSwiper)
4.當然可以在生產環(huán)境當中刪除掉不必要的console.log,打開build/webpack.prod.conf.js 在plugins里添加以下代碼
plugins: [
new webpack.optimize.UglifyJsPlugin({ //添加-刪除console.log
compress: {
warnings: false,
drop_debugger: true,
drop_console: true
},
sourceMap: true
}),
5.執(zhí)行npm run build之后,會發(fā)現(xiàn)文件的體積明顯小了很多,如果把一些Ui庫也替換成CDN的方式,可能體積會更小,渲染解析更快。
-
Vue-cli開啟打包壓縮 和后臺配合gzip訪問開啟打包壓縮 和后臺配合gzip訪問
1.首先打開 config/index.js,找到 build 對象中的productionGzip ,改成 true
2.打開 productionGzip: true 之前,先要安裝依賴 compression-webpack-plugin ,官方推薦的命令是:
npm install --save-dev compression-webpack-plugin
//(此處有坑) 如果打包報錯,應該是版本問題 ,先卸載之前安裝的此插件 ,然后安裝低版本
npm install --save-dev compression-webpack-plugin@1.1.11
3.等安裝好了,重新打包 npm run build,此時打包的文件會 新增 .gz文件。是不是比原來的js文件小很多呢,之后項目訪問的文件就是這個.gz文件
4.后臺nginx開啟gzip模式訪問,瀏覽器訪問項目,自動會找到 .gz的文件。加載速度明顯提高。
http { //在 http中配置如下代碼,
gzip on;
gzip_disable "msie6";
gzip_vary on;
gzip_proxied any;
gzip_comp_level 8; #壓縮級別
gzip_buffers 16 8k;
#gzip_http_version 1.1;
gzip_min_length 100; #不壓縮臨界值
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
}
19. Vue核心之虛擬DOM
- 真實DOM和其解析流程,瀏覽器渲染引擎工作流程都差不多,大致分為5步,創(chuàng)建
DOM樹-->創(chuàng)建StyleRules-->創(chuàng)建Render樹-->布局Layout-->繪制Painting。
1.用HTML分析器,創(chuàng)建DOM樹。
2.用CSS分析器,生成樣式規(guī)則表。
3.關聯(lián)DOM樹和規(guī)則表,生成渲染樹。
4.通過渲染樹計算節(jié)點屬性。
5.通過計算好的節(jié)點屬性,渲染頁面
DOM樹的構建是文檔加載完成開始的?構建DOM數是一個漸進過程,為達到更好用戶體驗,渲染引擎會盡快將內容顯示在屏幕上。它不必等到整個HTML文檔解析完畢之后才開始構建render數和布局。
Render樹是DOM樹和CSSOM樹構建完畢才開始構建的嗎?這三個過程在實際進行的時候又不是完全獨立,而是會有交叉。會造成一邊加載,一遍解析,一遍渲染的工作現(xiàn)象。
CSS的解析是從右往左逆向解析的(從DOM樹的下-上解析比上-下解析效率高),嵌套標簽越多,解析越慢。
-
JS操作真實DOM的代價
用我們傳統(tǒng)的開發(fā)模式,原生JS或JQ操作DOM時,瀏覽器會從構建DOM樹開始從頭到尾執(zhí)行一遍流程。在一次操作中,我需要更新10個DOM節(jié)點,瀏覽器收到第一個DOM請求后并不知道還有9次更新操作,因此會馬上執(zhí)行流程,最終執(zhí)行10次。例如,第一次計算完,緊接著下一個DOM更新請求,這個節(jié)點的坐標值就變了,前一次計算為無用功。計算DOM節(jié)點坐標值等都是白白浪費的性能。即使計算機硬件一直在迭代更新,操作DOM的代價仍舊是昂貴的,頻繁操作還是會出現(xiàn)頁面卡頓,影響用戶體驗。
- 虛擬
DOM有什么好處?虛擬DOM,其實是一個大對象
1.Web界面由DOM樹(樹的意思是數據結構)來構建,當其中一部分發(fā)生變化時,其實就是對應某個DOM節(jié)點發(fā)生了變化。
2.虛擬DOM就是為了解決瀏覽器性能問題而被設計出來的。如前,若一次操作中有10次更新DOM的動作,虛擬DOM不會立即操作DOM,而是將這10次更新的diff內容保存到本地一個JS對象中,最終將這個JS對象一次性attch到DOM樹上,再進行后續(xù)操作,避免大量無謂的計算量。所以,用JS對象模擬DOM節(jié)點的好處是,頁面的更新可以先全部反映在JS對象(虛擬DOM)上,操作內存中的JS對象的速度顯然要更快,等更新完成后,再將最終的JS對象映射成真實的DOM,交由瀏覽器去繪制。