vue运行机制笔记

2020-09-22  本文已影响0人  风间澈618
组件精讲

vue组件的三个API:prop,event,slot

<template>
    <button :class="'i-button-size'+size" :disabled='disabled' @click="handleClick">
        <slot></slot>    
        <!-- 具名插槽 -->
        <slot name="icon"></slot>  
    </button>
</template>
<script>
判断参数是否是其中之一
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 是整个项目第一个被渲染的组件,而且只会渲染一次(即使切换路由,app.vue 也不会被再次渲染),
利用这个特性,很适合做一次性全局的状态数据管理,例如,我们将用户的登录信息保存起来:

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

如果你的项目足够复杂,或需要多人协同开发时,在 app.vue 里会写非常多的代码,多到结构复杂难以维护。这时可以使用 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))
            }
        }
    },
    
}

//得理解思路才行,具体代码可以不用太认真,有一个总体的概念
//动态渲染.vue文件的组件 ---display

运行机制
new vue =>init=>$mount=>render function =>vnode
init 对数据进行响应式化
render function 会被转化成 VNode 节点
    obj: 目标对象
    prop: 需要操作的目标对象的属性名
    descriptor: 描述符
Object.defineProperty(obj,prop,descrptor)

Object.defineProperty 用来把对象变成可观察的

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)
        }
    })
}

当然这是不够的,我们需要在上面再封装一层 observer 。这个函数传入一个 value(需要「响应式」化的对象),通过遍历所有属性的方式对该对象的每一个属性都通过 defineReactive 处理。
(注:实际上 observer 会进行递归调用,为了便于理解去掉了递归的过程)

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观察者对象

class Dep{
    constructor(){
        //用来存放watcher对象的数组
        this.subs=[]
    }
    //在subs中添加一个watch对象
    addSub(sub){
        this.subs.push(sub)
    }
    //通知所有watcher对象视图更新
    notify(){
        this.subs.forEach(sub=>{
            sub.update()
        })
    }
}
完成两件事

1 用addSub方法可以在目前的Dep对象中增加一个watcher的订阅操作
2 用notify方法通知目前对象dep对象的subs中的所有watcher对象触发更新操作

观察者watcher

class watcher{
    constructor(){
        /*在new一个watch对象时将该对象赋值给Dep.target,在get中用到*/
        Dep.target=this;
    }
    update(){
        console.log('视图更新啦)
    }
}

修改一下 defineReactive 以及 Vue 的构造函数,来完成依赖收集。

function defineReactive(obj,key,val){
    //一个Dep类对象
    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)
        /*新建一个watch观察者对象,这时候Dep.target会指向这个watcher对象*/
        new watcher()
        //模拟render过程,触发test属性的get函数
        console.log('render~',this._data.test)

    }
}

数据状态更新时的差异diff及patch机制

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算法,是通过同层的树节点进行比较,而非对树进行逐层遍历搜索,时间复杂度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
}
######对比相同的节点有哪些变化
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 实现了一个 nextTick 函数,传入一个 cb ,这个 cb 会被存储到一个队列中,在下一个 tick 时触发队列中的所有 cb 事件。

因为目前浏览器平台并没有实现 nextTick 方法,所以 Vue.js 源码中分别用 Promise、setTimeout、setImmediate 等方式在 microtask(或是task)中创建一个事件,目的是在当前调用栈执行完毕以后(不一定立即)才会去执行这个事件。

笔者用 setTimeout 来模拟这个方法,当然,真实的源码中会更加复杂,笔者在小册中只讲原理,有兴趣了解源码中 nextTick 的具体实现的同学可以参考next-tick。

首先定义一个 callbacks 数组用来存储 nextTick,在下一个 tick 处理这些回调函数之前,所有的 cb 都会被存在这个 callbacks 数组中。pending 是一个标记位,代表一个等待的状态。

setTimeout 会在 task 中创建一个事件 flushCallbacks ,flushCallbacks 则会在执行时将 callbacks 中的所有 cb 依次执行。

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

我们使用一个叫做 has 的 map,里面存放 id -> true ( false ) 的形式,用来判断是否已经存在相同的 Watcher 对象 (这样比每次都去遍历 queue 效率上会高很多)。

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 本身结合,如何利用 Vue 的响应式机制来实现核心 Store 的「响应式化」。

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
    }
}
上一篇下一篇

猜你喜欢

热点阅读