Vue3渲染

209 阅读6分钟

Vue3的Dom渲染

runtime-dom

  • Vue的dom操作模块放在runtime-dom中
体验下
  • 核心api是h与createRenderer
  • h创建虚拟dom对象(h不再runtime-dom里,在runtime-core中)
  • createRenderer接收一些固定参数,返回的结果有一个render方法,这个方法会执行dom插入
    • render方法接收的是一个虚拟dom
<script src="../../../node_modules/@vue/runtime-dom/dist/runtime-dom.global.js"></script>
const app = document.querySelector('#app');

let {
    createRenderer,
    h
} = VueRuntimeDOM;
let renderer = createRenderer({
    //创建dom节点
    createElement(element) {
        return document.createElement(element)
    },
    //设置文字
    setElementText(el, text) {
        el.innerHTML = text;
    },
    //插入节点
    insert(el, container) {
        container.appendChild(el)
    },
    //设置dom元素(样式、类名、事件...)
    patchProp(el, key, prevValue, nextValue) {}
});
renderer.render(h('h1', {
    style: {
        color: 'red'
    }
}, 'hello word'), app)

实现

index.ts

import { nodeOps } from "./nodeOps";
import { patchProp } from "./patchProp";
//合并
const renderOptions = Object.assign(nodeOps, { patchProp });

console.log(renderOptions);

nodeOps.ts

  • 将dom操作放置在这里
export const nodeOps = {
    // dom的增删改查操作
    /**
     * 插入
     * @param child 当前要插入节点
     * @param parent 父节点
     * @param anchor 插入到哪个位置
     */
    insert(child, parent, anchor = null) {
        //如果anchore为null,那么等价于appendChild
        parent.insertBefore(child, anchor);
    },
    /**
     * 删除节点
     * @param child 
     */
    remove(child) {
        let parentNode = child.parentNode;
        if (parentNode) {
            parentNode.removeChild(child);
        }
    },
    /**
     * 设置节点内容
     * @param el 
     * @param text 
     */
    setElementText(el, text) {
        el.textContent = text;
    },
    /**
     * 文本节点最好使用nodeValue来设置
     * @param node 文本节点 document.createTextNode('')
     * @param text 
     */
    setText(node, text) {
        node.nodeValue = text;
    },
    /**
     * 查找dom
     * @param selector 
     * @returns 
     */
    querySelector(selector) {
        return document.querySelector(selector);
    },
    /**
     * 查找父节点
     * @param node 
     * @returns 
     */
    parentNode(node) {
        return node.parentNode;
    },
    /**
     * 查找兄弟节点
     * @param node 
     * @returns 
     */
    nextSibling(node) {
        return node.nextSibling;
    },
    /**
     * 创建dom节点
     * @param tagName 
     * @returns 
     */
    createElement(tagName) {
        return document.createElement(tagName);
    },
    /**
     * 创建文本节点
     * @param text 
     * @returns 
     */
    createText(text) {
        return document.createTextNode(text);
    }
}

pathProp.ts

import { patchAttr } from "./modules/attr";
import { patchClass } from "./modules/class";
import { patchEvent } from "./modules/event";
import { patchStyle } from "./modules/style";

/**
 * 对dom进行类名、样式、行间属性、事件等操作
 * @param el 
 * @param key 
 * @param prevValue 
 * @param nextValue 
 */
export function patchProp(el, key, prevValue, nextValue) {
    if (key === 'class') {
        patchClass(el, nextValue);
    } else if (key === 'style') {
        patchStyle(el, prevValue, nextValue);
    } else if (/^on[^a-z]/.test(key)) {
        patchEvent(el, key, nextValue);
    } else {
        patchAttr(el, key, nextValue);
    }
}

modules/

  • 将pathProp.ts使用到的公共方法抽离出来,每个方法一个文件,保证代码的可读性
attr.ts
export function patchAttr(el, key, nextValue) {
    if (nextValue) {
        el.setAttribute(key, nextValue)
    } else {
        el.removeAttribute(key);
    }
}
class.ts
/**
 * 如果为空,就清除class,否则直接赋值
 * @param el 
 * @param nextValue 
 */
export function patchClass(el, nextValue) {
    if (nextValue == null) {
        el.removeAttribute('class');
    } else {
        el.className = nextValue;
    }
}
event.ts
/**
 * 创建一个对象,在对象内部调用函数,将事件源传递给内部函数
 * 这里有一个好处,我们不需要频繁的卸载和新增事件,在事件变更的时候只需要更新value即可
 * @param preValue 
 * @returns 
 */
function createInvoker(preValue) {
    const invoker = (e) => { invoker.value(e) };
    invoker.value = preValue;
    return invoker;
}
/**
 * 事件需要考虑解绑和绑定
 * @param el 
 * @param key 
 * @param nextValue 
 */
export function patchEvent(el, eventName, nextValue) {
    const invokers = el._vei || (el._vel = {});
    const exitingInvoker = invokers[eventName];//是否存在旧的invoker
    if (exitingInvoker && nextValue) {
        //如果有旧的存在,且nextValue存在,那么只更新value即可
        exitingInvoker.value = nextValue;
    } else {
        const eName = eventName.slice(2).toLowerCase();//去除on,且将后面变为小写

        if (nextValue) {
            //如果nextValue存在,代表是第一次,那么新增即可
            const invoker = createInvoker(nextValue);
            invokers[eventName] = invoker;
            el.addEventListener(eName, invoker)

        } else if (exitingInvoker) {
            //如果exitingInvoker存在且nextValue不存在,代表需要清除事件
            el.removeEventListene(eName, exitingInvoker);
            invokers[eventName] = null;
        }
    }
}
style.ts
/**
 * 样式需要比对前后差异,
 *  如果next存在,那么直接赋值
 *  如果prev存在,但next不存在,需要删掉
 * @param el 
 * @param prevValue
 * @param nextValue 
 */
export function patchStyle(el, prevValue, nextValue) {
    prevValue = prevValue || {};
    nextValue = nextValue || {};
    for (let key in nextValue) {
        el.style[key] = nextValue[key];
    }
    for (let key in prevValue) {
        if (nextValue[key] == null) {
            el.style[key] = null;
        }
    }
}

runtime-core

  • 核心api是h,作用为创建一个虚拟dom对象
  • h函数内部依赖于createVNode

h

  • 传入固定的值,返回虚拟dom对象
  • 子节点只要不是文本,会转成数组
  • 使用方式:
    • h(元素,{属性},文本)
    • h(元素,{属性},[儿子,儿子])
    • h(元素,null,[儿子,儿子])

实现h

import { isArray, isObject } from "@vue/shared";
import { isVNode, createVNode } from "./createVNode";

/**
 * 创建虚拟dom
 *  注意:如果儿子不是文本,需要用数组包起来
 * h(type,文本)
 * h(type,[{虚拟dom}])
 * h(type,{属性})
 * h(type,{属性},文本)
 * h(type,{属性},[文本,虚拟dom])
 * h(type,{属性},文本,文本)
 * @param type 
 * @param propsOrChildren 
 * @param children 
 * @returns 返回虚拟dom对象
 */
export function h(type, propsOrChildren, children = null) {
    // debugger
    let l = arguments.length;
    if (l === 2) {
        if (isObject(children) && !isArray(children)) {
            //如果是对象且是不是数组
            if (isVNode(propsOrChildren)) {
                //如果是vnode,那么代表是儿子,所以属性传null
                return createVNode(type, null, [propsOrChildren]);
            }
            //不是vnode代表是属性,儿子为空
            return createVNode(type, propsOrChildren);
        } else {
            //不是对象,或是数组,也可能是文本,那都是儿子
            return createVNode(type, null, propsOrChildren);
        }
    } else {
        if (l > 3) {
            children = Array.from(arguments).slice(2);//大于三个的情况下,所有第二位后面的统统是儿子

        }
        else if (l === 3 && isVNode(children)) {
            //防止儿子不是文本
            children = [children];
        }
        return createVNode(type, propsOrChildren, children);

    }
}

createVNode

import { isArray, isString } from "@vue/shared";

/**
 * 使用位运算的好处
 * 概念补充:
 *      1 << 1 相当于向前进一位    如2进制的1进一位就是10 那么转为10进制就是2
 *      2 | 1  将符号两端转为2进制,再比较,取于每个进位上不为0的值,如果都为0,那么取0 ,当前为 10 | 01 那么就是 11 转换完成就是3
 *      3 & 1  将符号两端转为2进制,再比较,进位相同则累加,如  11 & 01 结果为01,就是1
 * 那么这样我们就可以做一些包含的操作,比如说是权限校验,存储的时候,将权限|,拿到总和权限,比对的时候进行&,可以得到拆分的权限,(我们需要保证每个权限的占位不重复),比如查看权限为2,编辑权限为1,总权限就可以存3,比对的时候直接3&权限即可
 */
import { isArray, isString } from "@vue/shared";
/**
 * 文本节点标记
 */
export const Text = Symbol('Text');
/**
 * 判断两个虚拟节点是否是同一个节点
 *  key相等
 *  标签名要相等
 * @param n1 
 * @param n2 
 */
export function isSameVnode(n1, n2) {
    return (n1.type === n2.type) && (n1.key === n2.key)
}
/**
 * 使用位运算的好处
 * 概念补充:
 *      1 << 1 相当于向前进一位    如2进制的1进一位就是10 那么转为10进制就是2
 *      2 | 1  将符号两端转为2进制,再比较,取于每个进位上不为0的值,如果都为0,那么取0 ,当前为 10 | 01 那么就是 11 转换完成就是3
 *      3 & 1  将符号两端转为2进制,再比较,进位相同则累加,如  11 & 01 结果为01,就是1
 * 那么这样我们就可以做一些包含的操作,比如说是权限校验,存储的时候,将权限|,拿到总和权限,比对的时候进行&,可以得到拆分的权限,(我们需要保证每个权限的占位不重复),比如查看权限为2,编辑权限为1,总权限就可以存3,比对的时候直接3&权限即可
 */
export const enum ShapeFlags { // vue3提供的形状标识
    ELEMENT = 1,
    FUNCTIONAL_COMPONENT = 1 << 1,
    STATEFUL_COMPONENT = 1 << 2,
    TEXT_CHILDREN = 1 << 3,
    ARRAY_CHILDREN = 1 << 4,
    SLOTS_CHILDREN = 1 << 5,
    TELEPORT = 1 << 6,
    SUSPENSE = 1 << 7,
    COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
    COMPONENT_KEPT_ALIVE = 1 << 9,
    COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}
export function isVNode(value: any) {
    return value ? value.__v_isVNode === true : false
}
export function createVNode(type, props = null, children = null) {
    // debugger;

    let shapeFlag = isString(type) ? ShapeFlags.ELEMENT : 0;
    const vnode = {
        __v_isVNode: true,
        type,
        props,
        children,
        key: props && props.key,
        el: null,
        shapeFlag
    }
    if (children) {
        let temp = 0;
        if (isArray(children)) {
            temp = ShapeFlags.ARRAY_CHILDREN;
        } else {
            //可能是数字也可能是字符串,那么我们直接转成字符串
            children = String(children);
            temp = ShapeFlags.TEXT_CHILDREN;
        }
        //位运算 包含关系
        //例如01 | 10  = 11
        vnode.shapeFlag |= temp;
    }
    return vnode;
}

runderer.ts

  • 进行diff
import { isString } from "@vue/shared";
import { ShapeFlags, Text, createVNode, isSameVnode } from "./createVNode";

export function createRenderer(renderOptions) {
    const {
        insert: hostInsert,
        remove: hostRemove,
        patchProp: hostPatchProp,
        createElement: hostCreateElement,
        createText: hostCreateText,
        setText: hostSetText,
        setElementText: hostSetElementText,
        parentNode: hostParentNode,
        nextSibling: hostNextSibling,
    } = renderOptions;
    /**
     * 处理节点,如果是文本节点,处理下
     * @param child 
     * @returns 
     */
    const normlize = (children, i) => {
        if (isString(children[i])) {
            let vnode = createVNode(Text, null, children[i]);
            children[i] = vnode;
        }
        return children[i];
    }
    /**
     * 遍历渲染
     * @param children 
     * @param container 
     */
    const mountChildren = (children, container) => {
        for (let i = 0; i < children.length; i++) {
            let child = normlize(children, i);
            patch(null, child, container);
        }
    }
    const unmount = (vnode) => {
        hostRemove(vnode.el)
    }
    /**
     * 文本节点插入
     * @param n1 
     * @param n2 
     * @param container 
     */
    const processText = (n1, n2, container) => {
        if (n1 == null) {
            hostInsert(n2.el = hostCreateText(n2.children), container)
        } else {
            const el = n2.el = n1.el;
            hostSetText(el, n2.children)
        }
    }
    const mountElement = (vnode, container, anchor = null) => {
        let { type, props, shapeFlag, children } = vnode;
        let el = vnode.el = hostCreateElement(type);//将真实dom挂载到虚拟dom上,后续可能会复用        
        hostInsert(el, container, anchor);
        if (props) {
            for (let key in props) {
                //派发属性
                hostPatchProp(el, key, null, props[key]);
            }
        }
        if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
            //文本节点
            hostSetElementText(el, children);
        } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
            //数组
            mountChildren(children, el);
        }
    }
    /**
     * 新旧props比较
     * @param oldProps 
     * @param newProps 
     * @param el 
     */
    const patchProps = (oldProps, newProps, el) => {
        for (let key in newProps) {
            //新旧比较,新的为主
            hostPatchProp(el, key, oldProps[key], newProps[key]);
        }
        for (let key in oldProps) {
            if (newProps[key] == null) {
                //如果新对象不存在,代表要删除
                hostPatchProp(el, key, oldProps[key], null);
            }
        }
    }
    /**
     * 清除子节点
     * @param children 
     */
    const unmountChildren = (children) => {
        for (let i = 0; i < children.length; i++) {
            unmount(children[i])
        }
    }
    /**
     * 数组vs数组,diff算法核心部分
     * @param c1 
     * @param c2 
     * @param el 
     */
    const patchKeyChildren = (c1, c2, el) => {
        let i = 0;
        let e1 = c1.length - 1;
        let e2 = c2.length - 1;
        //先从起始位置开始比较相同的,遇到不相同的退出比较
        /**
         *  特殊情况1:
         *  [a,b,c] vs [a,b,d,e]
         */
        while (i <= e1 && i <= e2) {
            const n1 = c1[i];
            const n2 = c2[i];
            if (isSameVnode(n1, n2)) {
                //如果是同一个节点(key与标签相同)
                patch(n1, n2, el);//比对属性和子节点
            } else {
                break;
            }
            i++;
        }
        /**
         * 特殊情况2:
         * [c,a,b] vs [p,m,a,b]
         * 尾部开始比较
         */
        while (i <= e1 && i <= e2) {
            const n1 = c1[e1];
            const n2 = c2[e2];
            if (isSameVnode(n1, n2)) {
                //如果是同一个节点(key与标签相同)
                patch(n1, n2, el);//比对属性和子节点
            } else {
                break;
            }
            e1--;
            e2--;
        }
        //i>e1说明要新增
        //1与e2的之间就是新增的部分
        if (i > e1) {
            if (i <= e2) {
                while (i <= e2) {
                    const nextPos = e2 + 1;
                    //计算参照物,决定插入到哪个位置
                    //后边有人,就是往前插,没人就是往后插
                    const anchor = nextPos < c2.length ? c2[nextPos].el : null;
                    //创建新节点
                    patch(null, c2[i], el, anchor);
                    i++;
                }
            }
        } else if (i > e2) {
            //i>e2代表有要卸载的
            if (i <= e1) {
                while (i <= e1) {
                    unmount(c1[i]);
                    i++;
                }
            }
        }
        //优化完成,开始乱序比对  [a,b,c,d,e,f,g] vs [a,b,e,c,d,h]
        let s1 = i;
        let s2 = i;
        //建立映射表
        let keyToNewIndexMap = new Map();
        for (let i = s2; i <= e2; i++) {
            keyToNewIndexMap.set(c2[i].key, i);
        }
        const toBePatched = e2 - s2 + 1;//新的总数 (需要修改的个数)
        //创建一个与toBePatched等长的数组,且填充为0 
        //作用为记录是否比对过
        const newIndexToOldIndex = new Array(toBePatched).fill(0);
        //循环老的元素,看下新的有没有,有的话进行比对,没有就删掉
        for (let i = s1; i < e1; i++) {
            //获取老的
            const oldChild = c1[i];
            //拿到老的key,去刚使用新的创建的map集合中找index
            let newIndex = keyToNewIndexMap.get(oldChild.key);
            if (!newIndex) {
                //不存在就删掉
                unmount(oldChild);
            } else {
                newIndexToOldIndex[newIndex - s2] = i + 1;//给一个不为0的值,i+1保证不为0,代表比对过了
                patch(oldChild, c2[newIndex], el);
            }
        }
        //需要移动的位置,以最后一个为基准进行插入换位
        for (let i = toBePatched - 1; i >= 0; i--) {
            let index = i + s2;
            let current = c2[index];
            let anchor = (index + 1) < c2.length ? c2[index + 1].el : null;
            if (newIndexToOldIndex[i] === 0) {
                //为0代表需要创建
                patch(null, current, el, anchor);
            } else {
                //不为0代表已经比对过了,但还没排序
                hostInsert(current.el, el, anchor);//复用节点
            }
        }
    }
    /**
     * 新旧儿子比较,关键diff部分
     *  子节点有几种:
     *          数组、文本、空
     *  
     * 
     * @param n1 
     * @param n2 
     * @param container 
     */
    const patchChildren = (n1, n2, el) => {
        /*
        *  考虑情况:
        *  新儿子    旧儿子      措施
        *  文本      数组       删除旧儿子,设置文本内容
        *  文本      文本       更新文本即可
        *  文本      空         直接添加文本
        *  数组      数组        diff算法
        *  数组      文本        删除文本节点,渲染数组
        *  数组      空         直接渲染
        *  空        数组       清除
        *  空        文本       清除
        *  空        空         无需处理
        * 
        * */
        const c1 = n1.children;
        const c2 = n2.children;
        const preShafeFlag = n1.shapeFlag;
        const shafeFlag = n2.shapeFlag;
        if (shafeFlag & ShapeFlags.TEXT_CHILDREN) {
            //新的是文本
            if (preShafeFlag & ShapeFlags.ARRAY_CHILDREN) {
                //老的是数组
                unmountChildren(c1);
            }
            if (c1 !== c2) {
                //如果新旧文本不相等
                hostSetElementText(el, c2)
            }
        } else {
            if (preShafeFlag & ShapeFlags.ARRAY_CHILDREN) {
                //如果旧的是数组
                if (shafeFlag & ShapeFlags.ARRAY_CHILDREN) {
                    //新的也是数组  diff
                    patchKeyChildren(c1, c2, el)
                } else {
                    //现在不是数组,为空,因为文本在一开始就处理了
                    unmountChildren(c1);//删掉旧的
                }
            } else {
                //走到这代表之前肯定不是数组
                if (preShafeFlag & ShapeFlags.TEXT_CHILDREN) {
                    //如果以前是文本,那么清除文本
                    hostSetElementText(el, '');//文本设置为空
                }
                if (shafeFlag & ShapeFlags.ARRAY_CHILDREN) {
                    //现在是数组 以前可能为空或文本
                    //空不需要处理,文本在上面已经清除
                    mountChildren(c2, el);//遍历渲染
                }
            }
        }
    }
    /**
     * diff
     * @param n1 
     * @param n2 
     * @param container 
     */
    const patchElement = (n1, n2) => {
        let el = n2.el = n1.el;
        let oldProps = n1.props || {};
        let newProps = n2.props || {};
        //新旧props比较
        patchProps(oldProps, newProps, el);
        //新旧儿子比较
        patchChildren(n1, n2, el);
    }
    const processElement = (n1, n2, container, anchor = null) => {
        if (n1 === null) {
            mountElement(n2, container, anchor);
        } else {
            // 更新
            patchElement(n1, n2)
        }
    }
    const patch = (n1, n2, container, anchor = null) => {
        //如果新旧值相同,不用重新渲染
        if (n1 === n2) return;
        //如果两个虚拟节点指向的不是一个dom,代表第一次的dom要被卸载
        if (n1 && !isSameVnode(n1, n2)) {
            unmount(n1);
            n1 = null;
        }
        const { type, shapeFlag } = n2;
        switch (type) {
            case Text:
                processText(n1, n2, container);
                break;
            default:
                if (shapeFlag & ShapeFlags.ELEMENT) {
                    processElement(n1, n2, container, anchor);
                }
        }
    }
    const render = (vnode, container) => {
        if (vnode == null) {
            if (container._vnode) {
                unmount(container._vnode);
            }
            //卸载逻辑
        } else {
            //更新逻辑
            patch(container._vnode || null, vnode, container);
        }
        container._vnode = vnode;
    }
    return { render };
}

index.ts

  • 出口
export { createVNode,Text } from './createVNode';
export { createRenderer } from './renderer'
export * from '@vue/reactivity'
export { h } from './h';