<script src="https://cdn.jsdelivr.net/npm/vue@2.5.21/dist/vue.js"></script> <div id="app"> <form @submit="validate"> <input v-model="text"> <br> <input v-model="email"> <ul v-if="!$v.valid" style="color:red"> <li v-for="error in $v.errors"> {{ error }} </li> </ul> <input type="submit" :disabled="!$v.valid"> </form> </div> <script> const emailRE = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ const validationPlugin = { install(Vue) { // 全局注入的方法 Vue.mixin({ computed: { $v() { const rules = this.$options.validations let valid = true let errors = [] Object.keys(rules || {}).forEach(key => { const rule = rules[key] const value = this[key] const result = rule.validate(value) if(!result) { valid = false errors.push( rule.message(key, value) ) } }) return { valid, errors } } } }) } } Vue.use(validationPlugin) new Vue({ el: '#app', data: { text: 'foo', email: '' }, validations: { text: { validate: value => value.length >= 5, message: (key, value) => `${key} should have a min length of 5, but got ${value.length}` }, email: { validate: value => emailRE.test(value), message: key => `${key} must be a valid email` } }, methods: { validate (e) { if (!this.$v.valid) { e.preventDefault() alert('not valid!') } } } }) </script>
<script src="../node_modules/vue/dist/vue.js"></script> <div id="app"> <h1>{{ $t('welcome-message') }}</h1> <button @click="changeLang('en')">English</button> <button @click="changeLang('zh')">中文</button> <button @click="changeLang('nl')">Dutch</button> </div> <script> const i18nPlugin = { install(Vue,locales){ Vue.prototype.$t=function(id){ return locales[this.$root.lang][id] } } } Vue.use(i18nPlugin, /* option */ { en: { 'welcome-message': 'hello' }, zh: { 'welcome-message': '你好' }, nl: { 'welcome-message': 'Hallo' } }) new Vue({ el: '#app', data: { lang: 'en' }, methods: { changeLang (lang) { this.lang = lang } } }) </script>
Vue組件=Vue實例=new Vue(options)html
屬性vue
事件node
插槽react
<template> <div> <lazy-component /> </div> </template> <script> const lazyComponent = () => import('Component.vue') export default { components: { lazyComponent } } </script>
基於路由拆分git
const routes = [ { path: /foo', component: () => import('./RouteComponent.vue') } ]
無狀態 、實例、this上下文、生命週期github
<TempVar :var1="`hello ${name}`" :var2="destroyClock ? 'hello vue' : 'hello world'" > <template v-slot="{ var1, var2 }"> {{ var1 }} {{ var2 }} </template> </TempVar> <script> export default { functional:true, render:(h,ctx)=>{ return ctx.scopedSlots.default && ctx.scopedSlots.default(ctx.props||{}) } } </script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.21/dist/vue.js"></script> <div id="app"> <function-component :tags="['h1','h2','h3']"></function-component> </div> <script> // 函數組件的渲染函數還會接收一個額外的 context 參數,爲沒有實例的函數組件提供上下文信息。 // 這裏咱們用對象解構context const FunctionComponent = { functional: true, // 標記組件爲 functional render (h, {props:{tags}}){ return h('div', {attrs: { class:'function' }}, tags.map((tag, i) => h(tag, i)) ) } } Vue.component('function-component', FunctionComponent) new Vue({el: '#app'}) </script>
function createApp ({ el, model, view, actions }) { const wrappedActions={} Object.keys(actions).forEach(key=>{ const originalAction=actions[key] wrappedActions[key]=()=>{ const nextModel=originalAction(model) vm.model=nextModel } }) const vm=new Vue({ el, data:{model}, render(h){ return view(h,this.model,wrappedActions) } }) } createApp({ el: '#app', model: { count: 0 }, actions: { inc: ({ count }) => ({ count: count + 1 }), dec: ({ count }) => ({ count: count - 1 }) }, view: (h, model, actions) => h('div', { attrs: { id: 'app' }}, [ model.count, ' ', h('button', { on: { click: actions.inc }}, '+'), h('button', { on: { click: actions.dec }}, '-') ]) })
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.21/dist/vue.js"></script> <div id="app"> <smart-avatar username="vuejs" id="hello"> <div slot="foo"> 這是一個具名插槽 </div> </smart-avatar> </div> <script> // mock API function fetchURL(username, cb) { setTimeout(() => { cb('https://avatars3.githubusercontent.com/u/6128107?v=4&s=200') }, 500) } const Avatar = { props: ['src'], template: `<img :src="src" />` } // 高階組件withAvatarUrl function withAvatarUrl(innerComponent, fetchURL) { return { props: ['username'], data() { return { url: `http://via.placeholder.com/200*200` } }, created() { fetchURL(this.username, url => { this.url = url; }) }, render(h) { // console.log(this.$slots.default); // console.log(this.$slots.foo); return h(innerComponent, { props: { src: this.url, attrs: this.$attrs } }, this.$slots.default) } } } const SmartAvatar = withAvatarUrl(Avatar, fetchURL); new Vue({ el: '#app', components: {SmartAvatar} }) </script>
const AsyncComp = () => ({ // 須要加載的組件。應當是一個 Promise component: import('./MyComp.vue'), // 加載中應當渲染的組件 loading: LoadingComp, // 出錯時渲染的組件 error: ErrorComp, // 渲染加載中組件前的等待時間。默認:200ms。 delay: 200, // 最長等待時間。超出此時間則渲染錯誤組件。默認:Infinity timeout: 3000 }) Vue.component('async-example', AsyncComp)
<!-- 子組件 --> <template> <ul> <li v-for="item in dataList">{{item}}</li> </ul> </template> <script> export default { props : { dataList : [] } } </script> <!-- 父組件 --> <template> <component-child v-bind:data-list="dataList"> </component-child> <input v-model="dataInput" v-on:keyup.13="addDataItem()" ></input> </template> <script> import ComponentChild from './child.vue' export default { data () { return { dataInput: "", dataList : [ 'hello world!','welcome to use vue.js' ] } }, components : { ComponentChild }, methods : { addDataItem () { let self = this if( !(self.dataInput && self.dataInput.length > 0) ) { return } self.dataList.push( self.dataInput ) self.dataInput = "" } } } </script>
在組件中,可使用 $emit, $on, $off 分別來分發、監聽、取消監聽事件面試
// NewTodoInput --------------------- // ... methods: { addTodo: function () { eventHub.$emit('add-todo', { text: this.newTodoText }) this.newTodoText = '' } } // DeleteTodoButton --------------------- // ... methods: { deleteTodo: function (id) { eventHub.$emit('delete-todo', id) } } // Todos --------------------- // ... created: function () { eventHub.$on('add-todo', this.addTodo) eventHub.$on('delete-todo', this.deleteTodo) }, // 最好在組件銷燬前 // 清除事件監聽 beforeDestroy: function () { eventHub.$off('add-todo', this.addTodo) eventHub.$off('delete-todo', this.deleteTodo) }, methods: { addTodo: function (newTodo) { this.todos.push(newTodo) }, deleteTodo: function (todoId) { this.todos = this.todos.filter(function (todo) { return todo.id !== todoId }) } }
$ref ref="xxx"
$parent / $children:訪問父 / 子實例正則表達式
<input :value="value" v-bind="$attrs" v-on="listeners" > <script> computed: { listeners() { return { ...this.$listeners, input: event => this.$emit('input', event.target.value) } } } </script>
// 父級組件提供 'foo' var Provider = { provide: { foo: 'bar' }, // ... } // 子組件注入 'foo' var Child = { inject: ['foo'], created () { console.log(this.foo) // => "bar" } // ... }
// 父級組件提供 'state' var Provider = { provide: { state = Vue.observable({ count: 0 }) }, // ... } // 子組件注入 'foo' var Child = { inject: ['state'], created () { console.log(this.state) // => { count: 0 } } // ... }
const state={count:0} const Counter = { data(){return state}, render:h=>h('div',state.count) } new Vue({ el: '#app', components:{Counter}, methods:{ inc(){ state.count++ } } })
//中央事件總線 var bus = new Vue(); var app = new Vue({ el: "#app", template: ` <div> <brother1></brother1> <brother2></brother2> </div> ` }); // 在組件 brother1 的 methods 方法中觸發事件 bus.$emit("say-hello", "world"); // 在組件 brother2 的 created 鉤子函數中監聽事件 bus.$on("say-hello", function(arg) { console.log("hello " + arg); // hello world });
**$dispatch 和 $broadcast 已經被棄用,使用
Vuex代替**vuex
如下自實現參考 iview/emitter.js at 2.0 · iview/iview -githubnpm
function broadcast(componentName, eventName, params) { this.$children.forEach(child => { const name = child.$options.name; if (name === componentName) { child.$emit.apply(child, [eventName].concat(params)); } else { // todo 若是 params 是空數組,接收到的會是 undefined broadcast.apply(child, [componentName, eventName].concat([params])); } }); } export default { methods: { dispatch(componentName, eventName, params) { let parent = this.$parent || this.$root; let name = parent.$options.name; while (parent && (!name || name !== componentName)) { parent = parent.$parent; if (parent) { name = parent.$options.name; } } if (parent) { parent.$emit.apply(parent, [eventName].concat(params)); } }, broadcast(componentName, eventName, params) { broadcast.call(this, componentName, eventName, params); } } };
單向數據流,雙向綁定語法糖
<PersonalInfo v-model="phoneInfo" :zip-code.sync="zipCode" /> <!-- 等同於 --> <PersonalInfo :phone-info="phoneInfo" @change="val=>(phoneInfo=val)" :zip-code="zipCode" @update:zipCode="val=>(zipCode=val)" />
數據劫持+觀察訂閱模式:
經過Object.defineProperty(obj,key,desc)對data進行數據劫持,即建立get/set函數
// 全局的依賴收集器Dep window.Dep = class Dep { constructor() {this.subscribers = new Set()} depend() {activeUpdate&&this.subscribers.add(activeUpdate)} notify() {this.subscribers.forEach(sub =>sub())} } let activeUpdate function autorun(update) { function wrapperUpdate() { activeUpdate = wrapperUpdate update() activeUpdate = null } wrapperUpdate() } function observer(obj) { Object.keys(obj).forEach(key => { var dep = new Dep() let internalValue = obj[key] Object.defineProperty(obj, key, { get() { // console.log(`getting key "${key}": ${internalValue}`) // 將當前正在運行的更新函數追加進訂閱者列表 activeUpdate&&dep.depend() //收集依賴 return internalValue }, set(newVal) { //console.log(`setting key "${key}" to: ${internalValue}`) // 加個if判斷,數據發生變化再觸發更新 if(internalValue !== newVal) { internalValue = newVal dep.notify() // 觸發依賴的更新 } } }) }) } let state = {count:0} observer(state); autorun(() => { console.log('state.count發生變化了', state.count) }) state.count = state.count + 5; // state.count發生變化了 0 // state.count發生變化了 5
Model-View-ViewModel,其核心是提供對View 和 ViewModel 的雙向數據綁定,這使得ViewModel 的狀態改變能夠自動傳遞給 View
observe(data, this) 給 data 對象添加 Observer作監聽。
建立一個 Observer 對象
建立了一個 Dep 對象實例(觀察者模式)
getter 和 setter 方法調用時會分別調用 dep.depend 方法和 dep.notify
全局註冊的選項,被引用到你的每一個組件中
一、Vue.component 註冊的 【全局組件】
二、Vue.filter 註冊的 【全局過濾器】
三、Vue.directive 註冊的 【全局指令】
四、Vue.mixin 註冊的 【全局mixin】
合併權重
一、組件選項
二、組件 - mixin
三、組件 - mixin - mixin
四、.....
x、全局 選項
函數合併疊加(data,provide)
數組疊加(created,watch)
原型疊加(components,filters,directives)
兩個對象合併的時候,不會相互覆蓋,而是 權重小的 被放到 權重大 的 的原型上
覆蓋疊加(props,methods,computed,inject)
兩個對象合併,若是有重複key,權重大的覆蓋權重小的
直接替換(el,template,propData 等)
something | myFilter
被解析成_f('myFilter')( something )
Vue.js 在默認狀況下,每次觸發某個數據的 setter 方法後,對應的 Watcher 對象其實會被 push 進一個隊列 queue 中,在下一個 tick 的時候將這個隊列 queue 所有拿出來 run( Watcher 對象的一個方法,用來觸發 patch 操做) 一遍。
真實DOM操做昂貴,虛擬DOM就是js對象,操做代價小
平時開發寫vue文件都是用模板template的方法寫html,模板會被編譯成render函數,流程以下:
初始化的時候
- 模板會被編譯成render函數 - render函數返回虛擬DOM - 生成真正的DOM
數據更新的時候
- render函數返回新的virtual Dom - 新的virtual Dom和舊的virtual Dom作diff - 將差別運用到真實DOM
render API
//template, jsx, render本質都是同樣的, 都是一種dom和數據狀態之間關係的表示 render(h) { h(tag, data, children) } // tag能夠是原生的html標籤 render(h) { return h('div', { attrs: {}}, []) } // 也能夠是一個vue component import vueComponent from '...' render(h) { h(vueComponent, { props: {} }) }
compile 編譯能夠分紅 parse、 optimize 與 generate 三個階段,最終須要獲得 render function。
parse:會用正則等方式解析 template 模板中的指令、class、style 等數據,造成 AST。
optimize:標記 static 靜態節點,而減小了比較的過程 等優化
optimize(ast, options)
generate:是將 AST 轉化成 render function 字符串
const code = generate(ast, options)
vue模板編譯先後:
`<ul :class="bindCls" class="list" v-if="isShow"> <li v-for="(item,index) in data" @click="clickItem(index)">{{item}}:{{index}}</li> </ul>` with(this){ return (isShow) ? _c('ul', { staticClass: "list", class: bindCls }, _l((data), function(item, index) { return _c('li', { on: { "click": function($event) { clickItem(index) } } }, [_v(_s(item) + ":" + _s(index))]) }) ) : _e() }
對比react的jsx編譯先後
`<div id="1" class="li-1"> Hello World <MyComp></MyComp> </div>` h('div',{ id: '1', 'class': 'li-1' },'Hello World', h(MyComp, null) )
vnode
{ el: div //對真實的節點的引用,本例中就是document.querySelector('#id.classA') tagName: 'DIV', //節點的標籤 sel: 'div#v.classA' //節點的選擇器 data: null, // 一個存儲節點屬性的對象,對應節點的el[prop]屬性,例如onclick , style children: [], //存儲子節點的數組,每一個子節點也是vnode結構 text: null, //若是是文本節點,對應文本節點的textContent,不然爲null }
核心 patch (oldVnode, vnode)
patchVnode (oldVnode, vnode)節點比較5種狀況
key的做用:
Vue 很「 囂張 」,它宣稱能夠更快地計算出Virtual DOM的差別,這是因爲它在渲染過程當中,因爲vue會跟蹤每個組件的依賴收集,經過setter / getter 以及一些函數的劫持,可以精確地知道變化,並在編譯過程標記了static靜態節點,在接下來新的Virtual DOM 而且和原來舊的 Virtual DOM進行比較時候,跳過static靜態節點。因此不須要從新渲染整個組件樹。React默認是經過比較引用的方式進行,當某個組件的狀態發生變化時,它會以該組件爲根,從新渲染整個組件子樹。若是想避免沒必要要的子組件從新渲染,你須要在全部可能的地方使用PureComponent,或者手動實現shouldComponentUpdate方法。可是Vue中,你能夠認定它是默認的優化。
類vue vdom
類react vdom
vue插件,經過hash /history 2中方式實現可配路由
Hash
$router.push()
顯式調用方法HashHistory.push()
根據hash模式調用,設置hash並添加到瀏覽器歷史記錄(window.location.hash= XXX)History.transitionTo()
開始更新History.updateRoute()
更新路由app._route= route
vm.render()
更新視圖但流程2變爲替換當前hash (window.location.replace= XXX)
3.監聽地址欄變化:在setupListeners中監聽hash變化(window.onhashchange)並調用replace
History
實戰
const Foo = { props: ['id'], template: `<div>foo with id: {{id}} </div>` } const Bar = { template: `<div>bar</div>` } const NotFound = { template: `<div>not found</div>` } const routeTable = { '/foo/:id': Foo, '/bar': Bar, } const compiledRoutes = []; Object.keys(routeTable).forEach(path => { const dynamicSegments = [] const regex = pathToRegexp(path, dynamicSegments) const component = routeTable[path] compiledRoutes.push({ component, regex, dynamicSegments }) }) window.addEventListener('hashchange', () => { app.url = window.location.hash.slice(1); }) const app = new Vue({ el: '#app', data() { return { url: window.location.hash.slice(1) } }, render(h) { const url = '/' + this.url let componentToRender let props = {} compiledRoutes.some(route => { const match = route.regex.exec(url) if (match) { componentToRender = route.component route.dynamicSegments.forEach((segment,index) => { props[segment.name] = match[index+1] }) } }) return h('div', [ h('a', { attrs: { href: '#foo/123' } }, 'foo123'), '|', h('a', { attrs: { href: '#foo/234' } }, 'foo234'), '|', h('a', { attrs: { href: '#bar' } }, 'bar'), h(componentToRender || NotFound, { props }) ]) } })
父組件怎麼傳值給子組件的 props
父組件的模板 會被解析成一個 模板渲染函數,執行時會綁定 父組件爲做用域
(function() { with(this){ return _c('div',{staticClass:"a"},[ _c('testb',{attrs:{"child-name":parentName}}) ],1) } })
組件怎麼讀取 props
父組件數據變化,子組件props如何更新
父組件數據變化,觸發set,從而通知依賴收集器的watcher從新渲染
vuex 僅僅是做爲 vue 的一個插件而存在,不像 Redux,MobX 等庫能夠應用於全部框架,vuex 只能使用在 vue 上,很大的程度是由於其高度依賴於 vue 的 computed 依賴檢測系統以及其插件系統,
vuex 總體思想誕生於 flux,可其的實現方式完徹底全的使用了 vue 自身的響應式設計,依賴監聽、依賴收集都屬於 vue 對對象 Property set get 方法的代理劫持。vuex 中的 store 本質就是沒有 template 的隱藏着的 vue 組件;
state
getter
雖然在組件內也能夠作計算屬性,可是 getters 能夠在多給件之間複用
mutaion
action
module
簡單版Vuex實現
//min-vuex import Vue from 'vue' const Store = function Store (options = {}) { const {state = {}, mutations={}} = options this._vm = new Vue({ data: { $$state: state }, }) this._mutations = mutations } Store.prototype.commit = function(type, payload){ if(this._mutations[type]) { this._mutations[type](this.state, payload) } } Object.defineProperties(Store.prototype, { state: { get: function(){ return this._vm._data.$$state } } }); export default {Store}
使用 Vuex 只需執行 Vue.use(Vuex),並在 Vue 的配置中傳入一個 store 對象的示例,store 是如何實現注入的?
Vue.use(Vuex) 方法執行的是 install 方法,它實現了 Vue 實例對象的 init 方法封裝和注入,使傳入的 store 對象被設置到 Vue 上下文環境的store中。所以在VueComponent任意地方都可以經過this.store 訪問到該 store。
state 內部支持模塊配置和模塊嵌套,如何實現的?
在 store 構造方法中有 makeLocalContext 方法,全部 module 都會有一個 local context,根據配置時的 path 進行匹配。因此執行如 dispatch('submitOrder', payload)這類 action 時,默認的拿到都是 module 的 local state,若是要訪問最外層或者是其餘 module 的 state,只能從 rootState 按照 path 路徑逐步進行訪問。
在執行 dispatch 觸發 action(commit 同理)的時候,只需傳入(type, payload),action 執行函數中第一個參數 store 從哪裏獲取的?
store 初始化時,全部配置的 action 和 mutation 以及 getters 均被封裝過。在執行如 dispatch('submitOrder', payload)的時候,actions 中 type 爲 submitOrder 的全部處理方法都是被封裝後的,其第一個參數爲當前的 store 對象,因此可以獲取到 { dispatch, commit, state, rootState } 等數據。
Vuex 如何區分 state 是外部直接修改,仍是經過 mutation 方法修改的?
Vuex 中修改 state 的惟一渠道就是執行 commit('xx', payload) 方法,其底層經過執行 this._withCommit(fn) 設置_committing 標誌變量爲 true,而後才能修改 state,修改完畢還須要還原_committing 變量。外部修改雖然可以直接修改 state,可是並無修改_committing 標誌位,因此只要 watch 一下 state,state change 時判斷是否_committing 值爲 true,便可判斷修改的合法性。
調試時的"時空穿梭"功能是如何實現的?
devtoolPlugin 中提供了此功能。由於 dev 模式下全部的 state change 都會被記錄下來,'時空穿梭' 功能其實就是將當前的 state 替換爲記錄中某個時刻的 state 狀態,利用 store.replaceState(targetState) 方法將執行 this._vm.state = state 實現。
Vue.js 是構建客戶端應用程序的框架。默認狀況下,能夠在瀏覽器中輸出 Vue 組件,進行生成 DOM 和操做 DOM。
然而,也能夠將同一個組件渲染爲服務器端的 HTML 字符串,將它們直接發送到瀏覽器,最後將靜態標記"混合"爲客戶端上徹底交互的應用程序。
服務器渲染的 Vue.js 應用程序也能夠被認爲是"同構"或"通用",由於應用程序的大部分代碼均可以在服務器和客戶端上運行。
服務端渲染的核心就在於:經過vue-server-renderer插件的renderToString()方法,將Vue實例轉換爲字符串插入到html文件