vue那些事

53 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第13天,点击查看活动详情

diff算法,虚拟dom

虚拟节点与DOM Diff算法源码实现:→视频学习

为什么需要虚拟DOM?

虚拟dom的优势:对数据进行了封装 ,还有数据发生变化有劫持 ,然后优秀的diff算法找到哪里发生了变化,那么只需重新渲染局部的内容就可,性能大大提升↑↑↑
一句话理解:替换更新后的节点,减少频繁对dom的操作

什么是diff算法?

image.png diff优化算法:1、只比较同一层级,不跨级对比;
2、标签名不同直接删除,不深度比较;
3、标签名相同,key也相同,就认为是相同节点,不深度比较

patch函数

snabbdom文档:一个虚拟DOM库
(下面的都不是完整的代码↓)

/**
 * 数据变化 --> 虚拟dom(计算变更) ---> 操作真实dom ---> 更新视图
 * 
 */
// patch是通过init函数创建出来的
const patch = init([
    // Init patch function with chosen modules
    classModule, // makes it easy to toggle classes
    propsModule, // for setting properties on DOM elements
    styleModule, // handles styling on elements with support for animations
    eventListenersModule, // attaches event listeners
]);

const container = document.getElementById("container");

const vnode = h("div#container.two.classes", {
    on: {
        click: someFn
    }
}, [
    h("span", {
        style: {
            fontWeight: "bold"
        }
    }, "This is bold"),
    " and this is just normal text",
    h("a", {
        props: {
            href: "/foo"
        }
    }, "I'll take you places!"),
]);
// Patch into empty DOM element – this modifies the DOM as a side effect
patch(container, vnode);  // 首次渲染执行一次,目的:把vnode渲染都一个空容器里

const newVnode = h(
    "div#container.two.classes", {
        on: {
            click: anotherEventHandler
        }
    },
    [
        h(
            "span", {
                style: {
                    fontWeight: "normal",
                    fontStyle: "italic"
                }
            },
            "This is now italic type"
        ),
        " and this is still just normal text",
        h("a", {
            props: {
                href: "/bar"
            }
        }, "I'll take you places!"),
    ]
);
// Second `patch` invocation
patch(vnode, newVnode); // 页面更新时候,patch会把新vnode替换旧vnode

init.ts文档

......

   //   patch函数
    return function patch(
        oldVnode: VNode | Element | DocumentFragment,
        vnode: VNode
    ): VNode {
        let i: number, elm: Node, parent: Node;
        const insertedVnodeQueue: VNodeQueue = [];
        for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();

        /**
         * 首次渲染,传入patch函数的第一个参数是个dom元素,
         * 命中这个逻辑:通过emptyNodeAt方法创建一个空的vnode,并关联dom元素
         */
        if (isElement(api, oldVnode)) {
            oldVnode = emptyNodeAt(oldVnode);
        } else if (isDocumentFragment(api, oldVnode)) {
            oldVnode = emptyDocumentFragmentAt(oldVnode);
        }
        // 之后更新渲染调用
        // 根据传进来的两个参数是否为同一vnode进行判断
        if (sameVnode(oldVnode, vnode)) {
            // key,tag相同,说明同一个vnode,直接patch做更新
            patchVnode(oldVnode, vnode, insertedVnodeQueue);
        } else {
            // key,tag不同,说明不同的vnode
            elm = oldVnode.elm!;
            parent = api.parentNode(elm) as Node;
            // 创建新的dom元素
            createElm(vnode, insertedVnodeQueue);

            if (parent !== null) {
                // 插入新的dom元素
                api.insertBefore(parent, vnode.elm!, api.nextSibling(elm));
                // 移除旧元素
                removeVnodes(parent, [oldVnode], 0, 0);
            }
        }

        for (i = 0; i < insertedVnodeQueue.length; ++i) {
            insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i]);
        }
        for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
        return vnode;
    };
    
    // emptyNodeAt函数  
    // sameVnode函数     
    // patchVnode函数
    // createElm函数
    // removeVnodes函数
    

image.png

patchVnode函数

function patchVnode(
        oldVnode: VNode,
        vnode: VNode,
        insertedVnodeQueue: VNodeQueue
    ) {
        // 执行hook
        const hook = vnode.data?.hook;
        hook?.prepatch?.(oldVnode, vnode);
        // 设置新vnode关联的dom元素
        const elm = (vnode.elm = oldVnode.elm)!;
        // 新老vnode一样,不操作
        if (oldVnode === vnode) return;
        // 与hook相关操作
        if (
            vnode.data !== undefined ||
            (isDef(vnode.text) && vnode.text !== oldVnode.text)
        ) {
            vnode.data ??= {};
            oldVnode.data ??= {};
            for (let i = 0; i < cbs.update.length; ++i)
                cbs.update[i](oldVnode, vnode);
            vnode.data?.hook?.update?.(oldVnode, vnode);
        }
        // 老children
        const oldCh = oldVnode.children as VNode[];
        // 新children
        const ch = vnode.children as VNode[];
        // 新vnode:无text有children
        if (isUndef(vnode.text)) {
            // 1.1 新vnode:有children,旧vnode--有children
            if (isDef(oldCh) && isDef(ch)) {
                if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
                //1.2 新vnode:有children,旧vnode 无children有text
            } else if (isDef(ch)) {
                // 清空text
                if (isDef(oldVnode.text)) api.setTextContent(elm, "");
                // 添加children
                addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
                //1.3 新vnode 无children,旧vnode 有children
            } else if (isDef(oldCh)) {
                // 移除children
                removeVnodes(elm, oldCh, 0, oldCh.length - 1);
                //1.4 旧vnode 有text
            } else if (isDef(oldVnode.text)) {
                // 清空text
                api.setTextContent(elm, "");
            }
            //2、 新vnode:有text无children
            //2、 旧vnod text 不等于 新vnode text
        } else if (oldVnode.text !== vnode.text) {
            // 旧vnode 有children
            if (isDef(oldCh)) {
                // 移除旧vnode children
                removeVnodes(elm, oldCh, 0, oldCh.length - 1);
            }
            // 设置新vnode text
            api.setTextContent(elm, vnode.text!);
        }
        hook?.postpatch?.(oldVnode, vnode);
    }

updateChildren函数

新老children的增删改(排序)

image.png

    function updateChildren(
        parentElm: Node,
        oldCh: VNode[],
        newCh: VNode[],
        insertedVnodeQueue: VNodeQueue
    ) {
        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: KeyToIndexMap | undefined;
        let idxInOld: number;
        let elmToMove: VNode;
        let before: any;

        while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
            if (oldStartVnode == null) {
                oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
            } else if (oldEndVnode == null) {
                oldEndVnode = oldCh[--oldEndIdx];
            } else if (newStartVnode == null) {
                newStartVnode = newCh[++newStartIdx];
            } else if (newEndVnode == null) {
                newEndVnode = newCh[--newEndIdx];
                // 1、老开始和新开始对比
            } else if (sameVnode(oldStartVnode, newStartVnode)) {
                patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
                oldStartVnode = oldCh[++oldStartIdx];
                newStartVnode = newCh[++newStartIdx];
                // 2、老结束和新结束对比
            } else if (sameVnode(oldEndVnode, newEndVnode)) {
                patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
                oldEndVnode = oldCh[--oldEndIdx];
                newEndVnode = newCh[--newEndIdx];
                // 3、老开始和新结束对比
            } else if (sameVnode(oldStartVnode, newEndVnode)) {
                // Vnode moved right
                patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
                api.insertBefore(
                    parentElm,
                    oldStartVnode.elm!,
                    api.nextSibling(oldEndVnode.elm!)
                );
                oldStartVnode = oldCh[++oldStartIdx];
                newEndVnode = newCh[--newEndIdx];
                // 4、老结束和新开始对比
            } else if (sameVnode(oldEndVnode, newStartVnode)) {
                // Vnode moved left
                patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
                api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);
                oldEndVnode = oldCh[--oldEndIdx];
                newStartVnode = newCh[++newStartIdx];

                // 以上四种情况都没有命中
            } else {
                if (oldKeyToIdx === undefined) {
                    oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
                }
                // 拿到新开始的key,在老children里找有没有某个节点是对应的这个key
                idxInOld = oldKeyToIdx[newStartVnode.key as string];
                // 没有在老children里面找到:创建新元素
                if (isUndef(idxInOld)) {
                    // New element
                    api.insertBefore(
                        parentElm,
                        createElm(newStartVnode, insertedVnodeQueue),
                        oldStartVnode.elm!
                    );
                // 在老children找到了
                } else {
                    // 找到这个元素
                    elmToMove = oldCh[idxInOld];
                    // 判断tag(标签)是否相等
                    if (elmToMove.sel !== newStartVnode.sel) {  // tag不相等
                        api.insertBefore(
                            parentElm,
                            createElm(newStartVnode, insertedVnodeQueue),
                            oldStartVnode.elm!
                        );
                    } else {  // tag相等
                        patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
                        oldCh[idxInOld] = undefined as any;
                        api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
                    }
                }
                newStartVnode = newCh[++newStartIdx];
            }
        }

        if (newStartIdx <= newEndIdx) {
            before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
            addVnodes(
                parentElm,
                before,
                newCh,
                newStartIdx,
                newEndIdx,
                insertedVnodeQueue
            );
        }
        if (oldStartIdx <= oldEndIdx) {
            removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
        }
    }

vue2响应式

let data = {
    name: 'lily',
    age: 22,
    friend: {
        friendName: 'grace'
    },
    colors: ['red', 'yellow', 'blue']

}

// 改写数组方法
const oldArrayProto = Array.prototype
// 以旧数组原型作为原型创建新数组对象
const newArrayProto = Object.create(oldArrayProto)
// 往newArrayProto空对象新添方法
['push', 'pop', 'shift', 'unshift'].forEach(element => {
    newArrayProto[element] = function () {
        console.log('改写数组方法')
        oldArrayProto[element].call(this, ...arguments)
    }
});


// 把data变成响应式数据
observer(data)

function observer(target) {
    // 判断target的数据类型
    if (typeof target !== 'object' || target === null) {
        return target
    }

    if (Array.isArray(target)) {
        target.__proto__ = newArrayProto
    }
    
    for (let key in target) {
        defineReactive(target, key, target[key])
    }
}

function defineReactive(target, key, value) {
    // 对属性值深度监听
    observer(value)
    // 响应式原理
    Object.defineProperty(target, key, {
        get: function () {
            return value
        },
        set: function (newValue) {
            // 如果我们改变的值为object类型,再当改变该值也需要深度监听
            observer(newValue)
            if (newValue !== value) {
                value = newValue
                console.log('视图更新')
            }
        }
    })
}

// console.log(data.name)
// data.age = 66
// console.log(data.age)

// 深度监听对象类型
// data.friend.friendName = 'gessi'
// data.name = {str:'ben'}
// data.name.str = 'jackson'

// 监听数组方法
data.colors[0] = 'orange'
data.colors.push('green')

// 不会监听到属性删除,新增
// delete data.age     // Vue.delete
// data.test = 'something'  // Vue.set

Object.defineProperty问题:

  • 1、对数据深度监听才能数据改变时候视图更新,但一旦数据层级比较多比较复杂的话,一直深度监听到为普通值,那么页面一开始渲染会卡死; (这个问题在vue3的proxy可以解决:在使用到这个数据时候才对它进行深度监听的过程)
  • 2、如果对象新增,删除属性也不会监听到,需要借助其他api

nextTick

nextTick应用:
例子:点击按钮往列表新添加数据,后台打印列表length,还是旧数组长度,但是页面已经是新数据内容?
(当我们在更新数据后立马去获取DOM中的内容时会发现获取的依然还是旧的内容)

因为在vue里面,dom渲染是异步的,当我们往数组添加了数据,但dom不是马上更新

那么,我们怎么同步拿到最新的数据并渲染到页面?
nextTick(vue实例上的一个实例方法)
this.nextTick(()=>{ // 回调函数里面能够拿到最新的数据信息 })

<template>
    <div>
        <h2>nextTick获取列表长度</h2>
        <ul ref="list">
            <li v-for="(item, index) in list" :key="index">
                {{ item }}</li>
        </ul>
        <button @click="additem">click</button>
    </div>
</template>

<script>
export default {
    components: {},
    props: {},
    data() {
        return {
            list: ['列表1', '列表2', '列表3']
        };
    },
    methods: {
        additem() {
            this.list.push(Date.now())
            this.list.push(Date.now())
            this.list.push(Date.now())  
            // const listref = this.$refs.list
            // console.log(listref.chilNodes.length)

            // vue中的DOM更新是批量处理的:上面执行了三次操作,但nextTick只执行一次
            this.nextTick(() => {
                const listref = this.$refs.list
                console.log(listref.chilNodes.length)
            })
        }
    }
};
</script>

什么时候触发nextTick?

nextTick的目的就是产生一个回调函数加入task或者microtask中,当前栈执行完以后(可能中间还有别的排在前面的函数)调用该回调函数,起到了异步触发(即下一个tick时触发)的目的

学习文章

mixin

抽离组件公共逻辑,本质是一个对象

// mixin
export default{
    data(){
        return {
        xxxx
        }
    },
    methods:{......},
    mounted(){......},
    .... // 内容跟vue组件一样
}

// 组件里引用
import mixin from 'xxxxxx'

mixins:[mixin]

注意:
1、组件和 mixin 的 data 和 事件重复时,就用 组件的 替换 mixin 的,没有重复就用 mixin的;
2、当生命周期钩子重复时,两者是并存的,先执行 mixin ,再执行组件的\

现在好像mixin比较少用了!!!

生命周期

Vue实例从创建到销毁的过程就是生命周期,既指从创建、初始化数据、编译模板、挂载DOM到渲染、更新渲染、销毁等一系列过程,他主要分为8个阶段,创建前后、载入前后、更新前后、销毁前、销毁后、以及一些特殊场景的生命周期。

生命周期描述
beforeCreate组件实例还没创建,通常用于插件开发中初始任务
created组件初始化完毕,数据可用,常用于异步数据获取
beforeMount未执行渲染,更新,dom还没创建
mounted初始化结束,dom创建完毕,可获取访问数据和dom元素
beforeUpdate更新前,可获取更新前的各种状态
updated更新后,所有状态是最新的
beforeDestroy销毁前,可用于一些定时器或订阅器的取消
destroyed组件已销毁

created 和 mounted 区别?

created:在组件实例一旦创建完成时立刻调用,dom节点还没生成;
mounted:在页面dom节点渲染完后立刻执行;
相同点:都能拿到实例对象的属性和方法;但是放在mounted的请求,有可能导致页面闪动,所以一般数据请求放在页面加载前完成最好。