vue運(yùn)行機(jī)制筆記

組件精講

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
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容