美文网首页
vue运行机制笔记

vue运行机制笔记

作者: 风间澈618 | 来源:发表于2020-09-22 10:18 被阅读0次
    组件精讲

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

    相关文章

      网友评论

          本文标题:vue运行机制笔记

          本文链接:https://www.haomeiwen.com/subject/aptbyktx.html