組件精講
vue組件的三個(gè)API:prop,event,slot
<template>
<button :class="'i-button-size'+size" :disabled='disabled' @click="handleClick">
<slot></slot>
<!-- 具名插槽 -->
<slot name="icon"></slot>
</button>
</template>
<script>
判斷參數(shù)是否是其中之一
function oneOf(value,sizeList){
for(let i=0;i<sizeList.length;i++){
if(value===sizeList[i]){
return true
}
}
return false
}
export default {
props:{
size:{
validator(value){
return oneOf(value,['small','large','default']);
},
default:'default'
},
disabled:{
type:Boolean,
default:false
}
},
methods:{
handleClick(event){
this.$emit('on-click',event)
}
}
}
</script>
<i-button size="large" disabled @on-click="handleClick"></i-button>
<i-button @click.native="handleClick"></i-button>
組件通信
無依賴的組件通信方法provide/inject
//A.vue
export default{
provide:{
name:'Aresn'
}
}
//B.vue
export default{
inject:['name'],
mounted(){
console.log(this.name) //aresn
}
}
app.vue 是整個(gè)項(xiàng)目第一個(gè)被渲染的組件,而且只會(huì)渲染一次(即使切換路由,app.vue 也不會(huì)被再次渲染),
利用這個(gè)特性,很適合做一次性全局的狀態(tài)數(shù)據(jù)管理,例如,我們將用戶的登錄信息保存起來:
<script>
export default {
provide(){
return{
app:this
}
},
data(){
return {
userInfo:null
}
},
methods:{
getUserInfo(){
//偽代碼
$.ajax('/user/info',(data)=>{
this.userInfo=data
})
}
},
mounted(){
this.getUserInfo()
}
}
</script>
<template>
<div>{{app.userinfo}}</div>
</template>
<script>
export default {
inject:['app'],
methods:{
changeUserInfo(){
//偽代碼
$.ajax('/user/update',()=>{
this.app.getUserInfo()
})
}
}
}
</script>
如果你的項(xiàng)目足夠復(fù)雜,或需要多人協(xié)同開發(fā)時(shí),在 app.vue 里會(huì)寫非常多的代碼,多到結(jié)構(gòu)復(fù)雜難以維護(hù)。這時(shí)可以使用 Vue.js 的混合 mixins,將不同的邏輯分開到不同的 js 文件里。
比如上面的用戶信息,就可以放到混合里:
<script>
import mixins_user from '../mixins/user.js'
export default {
mixins: [mixins_user]
}
</script>
function broadcast(compoentName,event,params){
this.$children.forEach(child=>{
const name=child.$options.name
if(name===componentName){
child.$emit.apply(child,[eventName].concat(params))
}else{
broadcast.apply(child,[componentName,eventName].concat(params))
}
})
}
export default{
methods:{
dispatch(compnentName,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))
}
}
},
}
//得理解思路才行,具體代碼可以不用太認(rèn)真,有一個(gè)總體的概念
//動(dòng)態(tài)渲染.vue文件的組件 ---display
運(yùn)行機(jī)制
new vue =>init=>$mount=>render function =>vnode
init 對(duì)數(shù)據(jù)進(jìn)行響應(yīng)式化
render function 會(huì)被轉(zhuǎn)化成 VNode 節(jié)點(diǎn)
obj: 目標(biāo)對(duì)象
prop: 需要操作的目標(biāo)對(duì)象的屬性名
descriptor: 描述符
Object.defineProperty(obj,prop,descrptor)
Object.defineProperty 用來把對(duì)象變成可觀察的
function defineReactive(obj,key,val){
Object.defineProperty(obj,key,{
enumerable:true, //屬性可枚舉
configurable:true, //屬性可被修改或刪除
get:function reactiveGetter(){
return val;
},
set:function reactiveSetter(newVal){
if(newVal===val) return ;
cb(newVal)
}
})
}
當(dāng)然這是不夠的,我們需要在上面再封裝一層 observer 。這個(gè)函數(shù)傳入一個(gè) value(需要「響應(yīng)式」化的對(duì)象),通過遍歷所有屬性的方式對(duì)該對(duì)象的每一個(gè)屬性都通過 defineReactive 處理。
(注:實(shí)際上 observer 會(huì)進(jìn)行遞歸調(diào)用,為了便于理解去掉了遞歸的過程)
function observer(obj){
if(!obj||(typeof obj!=='object')){
return
}
Object.keys(obj).forEach(key=>{
defineReactive(obj,key,obj[key])
})
}
class Vue{
constructor(options){
this._data=options.data;
observer(this._data)
}
}
訂閱者dep (Dependency)
主要作用是存放watcher觀察者對(duì)象
class Dep{
constructor(){
//用來存放watcher對(duì)象的數(shù)組
this.subs=[]
}
//在subs中添加一個(gè)watch對(duì)象
addSub(sub){
this.subs.push(sub)
}
//通知所有watcher對(duì)象視圖更新
notify(){
this.subs.forEach(sub=>{
sub.update()
})
}
}
完成兩件事
1 用addSub方法可以在目前的Dep對(duì)象中增加一個(gè)watcher的訂閱操作
2 用notify方法通知目前對(duì)象dep對(duì)象的subs中的所有watcher對(duì)象觸發(fā)更新操作
觀察者watcher
class watcher{
constructor(){
/*在new一個(gè)watch對(duì)象時(shí)將該對(duì)象賦值給Dep.target,在get中用到*/
Dep.target=this;
}
update(){
console.log('視圖更新啦)
}
}
修改一下 defineReactive 以及 Vue 的構(gòu)造函數(shù),來完成依賴收集。
function defineReactive(obj,key,val){
//一個(gè)Dep類對(duì)象
const dep=new Dep()
Object.defineProperty(obj,key,{
enumerable:true,
configurable:true,
get:function reactiveGetter(){
dep.addSub(Dep.target)
return val;
},
set:function reactiveSetter(newVal){
if(newVal===val) return;
dep.notify()
}
})
}
class Vue{
constructor(options){
this._data=options.data;
observer(this._data)
/*新建一個(gè)watch觀察者對(duì)象,這時(shí)候Dep.target會(huì)指向這個(gè)watcher對(duì)象*/
new watcher()
//模擬render過程,觸發(fā)test屬性的get函數(shù)
console.log('render~',this._data.test)
}
}
數(shù)據(jù)狀態(tài)更新時(shí)的差異diff及patch機(jī)制
const nodeOps={
setTextContent(text){
if(platform==='weex'){
node.parentNode.setAttr('value',text)
}else if(platform==='web'){
node.setTextContent=text
}
},
parentNode(){
},
removeChild(){
},
nextSibling(){
},
insertBefore(){
}
}
patch的diff算法,是通過同層的樹節(jié)點(diǎn)進(jìn)行比較,而非對(duì)樹進(jìn)行逐層遍歷搜索,時(shí)間復(fù)雜度O(n)
patch的簡單過程代碼
function patch(oldVnode,vnode,parentElm){
if(!oldVnode){
addVnodes(parentElm,null,vnode,0,vnode.length-1)
}else if(!vnode){
removeVnodes(parentElm,oldVnode,0,oldVnode.length-1)
}else{
if(sameVnode(oldVnode,vnode)){
patchVnode(oldVNode,vnode)
}else{
removeVnodes(parentElm,oldVNode,0,oldVNode.length-1)
addVnodes(parentElm,null,vnode,0,vnode.length-1)
}
}
}
sameVnode的判斷
function sameVnode(){
return (
a.key===b.key&&
a.tag===b.tag&&
a.isComment===b.isComment&&
(!!a.data)===(!!b.data) &&
sameInputType(a,b)
)
}
function sameInputType(a,b){
if(a.tag!=='input') return true
let i
const typeA=(i=a.data)&&(i=i.attrs)&&i.type
const typeB=(i=b.data)&&(i=i.attrs)&&i.type
return typeA===typeB
}
######對(duì)比相同的節(jié)點(diǎn)有哪些變化
function patchVnode(oldVnode,vnode){
if(oldVNode===vnode){
return
}
if(vnode.isStatic&&oldVnode.isStatic&&vnode.key===oldnode.key){
vnode.elm=oldVnode.elm;
vnode.componentInstance=oldVnode.componentInstance
return
}
const elm=vnode.elm=oldVNode.elm
const oldCh=oldVnode.children;
const ch=vnode.children
if(vnode.text){
nodeOps.setTextContent(elm,vnode.text)
}else
if(oldCh&&ch&&(oldCh!==ch)){
updateChildren(elm,oldCh,ch)
}else if(ch){
if(oldVnode.text) nodeOps.setTextContent(elm,'')
addVnodes(elm,null,ch,0,ch.length-1)
}else if(oldCh){
removeVnodes(elm,oldCh,0,oldCh.length-1)
}else if(oldVNode.text){
nodeOps.setTextContent(elm,'')
}
}
//updateChildren
function updateChildren(parentElm,oldCh,newCh){
let oldStartIdx=0;
let newStartIdx=0;
let oldEndIdx=oldCh.length-1
let oldStartVnode=oldCh[0]
let oldEndVnode=oldCh[oldEndIdx]
let newEndIdx=newCh.length-1
let newStartVnode=newCh[0]
let newEndVnode=newCh[newEndIdx]
let oldKeyToIdx,idxInOld,elmToMove,refElm;
while(oldStartIdx<=oldEndIdx&&newStartIdx<=newEndIdx){
if(!oldStartVnode){
oldStartVnode=oldCh[++oldStartIdx]
}else if(!oldEndVnode){
oldEndVnode=oldCh[--oldEndIdx]
}else if ( sameVnode(oldStartVnode,newStartVnode) ){
patchVnode(oldStartVnode,newStartVnode)
oldStartVnode=oldCh[++oldStartIdx]
newStartVnode=newCh[++newStartIdx]
}else if(sameVnode(oldEndVnode,newEndVnode)){
patchVnode(oldEndVnode,newEndVnode)
oldEndVnode=oldCh[--oldEndIdx]
newEndVnode=newCh[--newEndIdx]
}else if(sameVnode(oldStartVnode,newEndVnode)){
patchVnode(oldStartVnode,newEndVnode)
nodeOps.insertBefore(parentElm,oldStartVnode.elm,nodeOps.nextSibling(oldEndVnode.oldStartVnode=oldCh[++oldStartIdx]))
newEndVnode=newCh[--newEndIdx]
}else if(sameVnode(oldEndVnode,newStartVnode)){
patchVnode(oldEndVnode,newStartVnode)
nodeOps.insertBefore(parentElm,oldEndVnode.elm,oldStartVnode.elm)
oldEndVnode=oldCh[--oldEndIdx]
newStartVnode=newCh[++newStartIdx]
}else{
let elmToMove=oldCh[idxInOld]
if(!oldKeyToIdx) oldKeyToIdx=createKeyToOldIdx(oldCh,oldStartIdx,oldEndIdx)
idxInOld=newStartVnode.key?oldKeyToIdx[newStartVnode.key]:null
if(!idexInOld){
createElm(newStartVnode,parentElm);
newStartVnode=newCh[++newStartIdx]
}else{
elmToMove=oldCh[idxInOld]
if(sameVnode(elmToMove,newStartVnode)){
patchVnode(elmToMove,newStartVnode)
oldCh[idxInOld]=undefined
nodeOps.insertBefore(parentElm,newStartVnode.elm,oldStartVnode.elm)
newStartVnode=newCh[++newStartIdx]
}else{
createElm(newStartVnode,parentElm)
newStartVnode=newCh[++newStartIdx]
}
}
}
}
if(oldStartIdx>oldEndIdx){
refElm=(newCh[newEndIdx+1])?newCh[newEndIdx+1].elm:null;
addVnodes(parentElm,refElm,newCh,newStartIdx,newEndIdx)
}else if(newStartIdx>newEndIdx){
removeVnodes(parentElm,oldCh,oldStartIdx,oldEndIdx)
}
}
nexttick原理
Vue.js 實(shí)現(xiàn)了一個(gè) nextTick 函數(shù),傳入一個(gè) cb ,這個(gè) cb 會(huì)被存儲(chǔ)到一個(gè)隊(duì)列中,在下一個(gè) tick 時(shí)觸發(fā)隊(duì)列中的所有 cb 事件。
因?yàn)槟壳盀g覽器平臺(tái)并沒有實(shí)現(xiàn) nextTick 方法,所以 Vue.js 源碼中分別用 Promise、setTimeout、setImmediate 等方式在 microtask(或是task)中創(chuàng)建一個(gè)事件,目的是在當(dāng)前調(diào)用棧執(zhí)行完畢以后(不一定立即)才會(huì)去執(zhí)行這個(gè)事件。
筆者用 setTimeout 來模擬這個(gè)方法,當(dāng)然,真實(shí)的源碼中會(huì)更加復(fù)雜,筆者在小冊(cè)中只講原理,有興趣了解源碼中 nextTick 的具體實(shí)現(xiàn)的同學(xué)可以參考next-tick。
首先定義一個(gè) callbacks 數(shù)組用來存儲(chǔ) nextTick,在下一個(gè) tick 處理這些回調(diào)函數(shù)之前,所有的 cb 都會(huì)被存在這個(gè) callbacks 數(shù)組中。pending 是一個(gè)標(biāo)記位,代表一個(gè)等待的狀態(tài)。
setTimeout 會(huì)在 task 中創(chuàng)建一個(gè)事件 flushCallbacks ,flushCallbacks 則會(huì)在執(zhí)行時(shí)將 callbacks 中的所有 cb 依次執(zhí)行。
let callbacks=[]
let pending=false;
function nexttick(cb){
callbacks.push(cb)
if(!pending){
pending=true;
setTimeout(flushCallbacks,0)
}
}
function flushCallbacks(){
pending=false;
const copies=callbacks.slice(0)
callbacks.length=0;
for(let i=0;i<copies.length;i++){
copies[i]()
}
}
//update 方法,
let uid=0;
class watcher{
constructor(){
this.id=++uid;
}
update(){
console.log('watch'+this.id+'update')
queueWatcher(this)
}
run(){
console.log('watch'+this.id+'視圖更新啦')
}
}
queueWatcher
我們使用一個(gè)叫做 has 的 map,里面存放 id -> true ( false ) 的形式,用來判斷是否已經(jīng)存在相同的 Watcher 對(duì)象 (這樣比每次都去遍歷 queue 效率上會(huì)高很多)。
let has={}
let queue=[]
let waiting=false
function queueWatcher(watcher){
const id=watcher.id;
if(has[id]==null){
has[id]=true;
queue.push(watcher)
if(!waiting){
waiting=true;
nextTick(flushSchedulerQueue)
}
}
}
//flushSchedulerQueue
function flushSchedulerQueue(){
let watcher ,id;
for(index=0;index<queue.length;index++){
watcher=queue[index]
id=watcher.id
has[id]=null
watcher.run();
}
waiting=false
}
store
理解 Vuex 的核心在于理解其如何與 Vue 本身結(jié)合,如何利用 Vue 的響應(yīng)式機(jī)制來實(shí)現(xiàn)核心 Store 的「響應(yīng)式化」。
let Vue;
export default install(_Vue){
Vue.mixin({beforeCreate:vuexInit})
Vue=_Vue
}
function vuexInit(){
const options=this.$options;
if(options.store){
this.$store=options.store
}else{
this.$store=options.parent.$store
}
}