Spiga

前端提升7:手撸Vue3组件渲染

2025-04-12 23:21:56

一、实现DOM操作API

1. 构建环境

创建runtime-dom目录,存放dom操作api

创建runtime-core目录,存放允许时虚拟dom操作核心代码

在两个目录中分别构建环境

/packages/runtime-dom/package.json

{
  "name": "@vue/runtime-dom",
  "module": "dist/runtime-dom.esm-bundler.js",
  "unpkg": "dist/runtime-dom.global.js",
  "buildOptions": {
    "name": "VueRuntimeDOM",
     "formats": [
      "esm-bundler",
      "cjs",
      "global"
     ]
  }
}

/packages/runtime-core/package.json

{
  "name": "@vue/runtime-core",
  "module": "dist/runtime-core.esm-bundler.js",
  "unpkg": "dist/runtime-core.global.js",
  "buildOptions": {
    "name": "VueRuntimeCore",
     "formats": [
      "esm-bundler",
      "cjs",
      "global"
     ]
  }
}

2. 定义操作API

/packages/runtime-dom/src/nodeOps.ts

// dom操作 vue虚拟dom, 通过数据对象在内存中对比差异, 找到最小的改动点,使用dom操作完成更新。

// 原生dom操作api
export const nodeOps = {
    // 插入, 追加
    insert: (child, parent, anchor = null) => {
        parent.insertBefore(child, anchor);  // parent.appendChild(child)
    },
    // 删除子节点
    remove: child => {
        const parent = child.parentNode;
        if (parent) {
            parent.removeChild(child);
        }
    },
    // 创建元素
    createElement: tag => document.createElement(tag),
    // 创建文本标签
    createText: text => document.createTextNode(text),
    // 更新元素内容
    setElementText: (el, text) => el.textContent = text,
    // 更新文本
    setText: (node, text) => node.nodeValue = text,
    // 查询父节点
    parentNode: node => node.parentNode,
    // 查询后面的节点  div span  jq写页面都是操作dom
    nextSibling: node => node.nextSibling, 
    // dom查询
    querySelector: selector => document.querySelector(selector)
}

3. 为dom操作实现补丁函数

/packages/runtime-dom/src/patchProp.ts

// // new
// <button mystyle={color:'red'; fontSize: '13px'} type="text" size="small" class="myclass" :style={color:'red'; fontSize: '13px'} onclick="aaa"></button>

// // old
// <button flag=2 mystyle={color:'red'; fontSize: '13px'} type="text" class="aclass" :style={color:"green";background:'white'} onclick="ccc"></button>

// // diff算法,1. 对比是根元素,2. 如果相同,对比根元素的属性 3. 对比子类,如果都有子类则需要diff

// 对比属性差异
export const 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)) {
        // onclick
        // 1. 新绑定事件,直接绑定  2. 无绑定事件,删除绑定 removeEventListener 3. 换绑定
        patchEvent(el, key, nextValue);
    } else {
        // 其他属性,自定义属性, setAttribute
        patchAttr(el, key, nextValue);
    }
}

// 对比属性,相同则替换,新的插入,旧的删除
function patchClass(el, value) {
    if (value == null) {
        el.removeAttribute('class');  // 删除
    } else {
        el.className = value;  // 直接替换
    }
}
// 样式对比 next--新元素  prev--旧元素
function patchStyle(el, prev, next) {
    const oldStyle = el.style;   // 获取样式

    // 将新元素的所有样式直接拿过来
    for (let key in next) {
        // 直接覆盖旧样式
        oldStyle[key] = next[key];
    }
    // 如果有老的样式,需要删除掉
    if (prev) {
        for (let key in prev) {
            // 将旧元素的key去新元素中查找,如果没有则是需要删除的属性
            if (next[key] == undefined) {
                oldStyle[key] = undefined;
            }
        }
    }
}

function createInvoker(value) {
    // 调用代理函数
    const invoker = (e)=>{
        // 真实的调用函数
        invoker.value(e);
    }
    invoker.value = value;  // 存储这个变量,如果想换绑,可以直接更新value
    return invoker;
}

// 事件处理 key=onclick
function patchEvent(el, key, nextValue) {
    // 事件函数代理对象 vue event invoker vei
    // 在元素上绑定一个自定义属性,用于记录绑定的事件 {click: fn, touch: fn, mousemove: fn}
    const invokers = el._vei || (el._vei = {})
    // 有没有已绑定过的事件函数
    let existingInvoker = invokers[key];
    // 之前有绑定且新绑定也有函数 换绑定
    if (existingInvoker && nextValue) {
        existingInvoker.value = nextValue;
    } else {
        const eventName = key.slice(2).toLowerCase(); // 事件名
        if (nextValue) {
            // 有新的绑定函数
            const invoker = invokers[key] = createInvoker(nextValue);
            el.addEventListener(eventName, invoker);
        } else if (existingInvoker) {
            // 如果新的元素无绑定事件,需要将旧的元素解绑定
            el.removeEventListener(eventName, existingInvoker);
            invokers[key] = undefined;   // 清除缓存
        }
    }
}

// patch自定义属性 不确定自定义属性的格式,直接整个属性替换。
function patchAttr(el, key, value) {
    if (value == null) {
        // 新的没有该属性,老的有则删除
        el.removeAttribute(key);
    } else {
        el.setAttribute(key, value); // 直接用新的属性覆盖
    }
}

4. 导出createApp函数

  1. 在runtime-core中创建一个空渲染器实现

/packages/runtime-core/src/rendener.ts

// 渲染器

export function createRendener(renderOptions){
    // TODO
}

/packages/runtime-core/src/index.ts

// 创建渲染器

export * from '@Vue/reactivity'

export {createRendener} from './rendener'
  1. 导出createApp函数

/packages/runtime-dom/src/index.ts

// dom操作的api, 属性操作的api, 这些api需要将导入到runtime-core中

// 将dom操作api与属性操作api融合

// dom操作api
import {nodeOps} from './nodeOps'
// 属性操作api
import {patchProp} from './patchProp'
// 从core中导入
import {createRendener} from '@vue/runtime-core'

// 整合api
const renderOptions = Object.assign(nodeOps, {patchProp});  // 包含所需要的所有api

// 渲染函数
export const createApp = (component, rootProps = null) => {
    // 需要创建一个渲染器
    const { createApp } = createRendener(renderOptions); // 从runtime-core中调用
    let app = createApp(component, rootProps);  //vue3实例对象
    let { mount } = app;   // 从app实例上解构mount
    app.mount = function (container) {  // 重写mount  包一层
        container = nodeOps.querySelector(container);  // 将 #app 转为真实dom对象
        container.innerHTML = '';  // 清空原生dom内容, 再重新加载新内容
        mount(container);// 将真实dom节点传入
    }
    return app;
}

// createApp -> createRendener -> createApp -> app -> mount

export * from '@vue/runtime-core';  // 导出core模块中所有代码

5. 调试测试

/packages/runtime-dom/dist/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>vue事件动态处理</title>
</head>
<body>
    <button>Event点击事件</button>
    <script>
        const btn = document.querySelector('button');

        // 调用函数
        const invoker = ()=>{
            invoker.value();  // 执行真正的事件函数
        }
        invoker.value = ()=>{
            alert("点击事件1")
        }
        // 绑定事件
        btn.addEventListener('click', invoker);

        // 改变按钮的处理事件函数
        setTimeout(()=>{
            invoker.value = ()=>{
                alert('点击事件2');
            }
        }, 2000)
    </script>
</body>
</html>

二、初次渲染流程

1. 创建渲染对象的形状标识枚举

/packages/shared/src/index.ts

// 其他函数

// 渲染对象的形状标识 位运算方式实现
export const enum ShapeFlags {
    ELEMENT = 1,                                    // 元素类型
    FUNCTIONAL_COMPONENT = 1 << 1,                  // 函数式组件2
    STATEFUL_COMPONENT = 1 << 2,                    // 普通组件4
    TEXT_CHILDREN = 1 << 3,                         // 孩子是文本8
    ARRAY_CHILDREN = 1 << 4,                        // 孩子是数组16
    SLOTS_CHILDREN = 1 << 5,                        // 组件是插槽
    TELEPORT = 1 << 6,                              // teleport组件
    SUSPENSE = 1 << 7,                              // suspense组件
    COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}

// 位 byte,  存储单位: byte  1b,  1kb,  1mb,  1gb
// 位运算:
// 1. 十进制转二进制  二进制转十进制
// 2. 运算法制:与 两位都为1结果为1,否则为0;  或运算, 相当于加法,只要有一个为1则为1
// 非运算:取反 原是0变1 是1变0   异或运算:两位不相同则为1,相同则为0
// 左移运算:将数值向左移动若干位,

2. createVNode函数

/packages/runtime-core/src/createVNode.ts

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

// type-类型  props-属性 子集
export function createVNode(type, props, children = null) {

    // 虚拟节点:用一个对象来描述dom信息
    
    // 文本,元素,对象 不同的标识来区分它们

    // let flag = 1;  // 文本   flag = 2; //元素  位运算很简单的区分不同的类型

    // 位运算确定元素类别
    const shapeFlag = isObject(type) ? 
          ShapeFlags.COMPONENT : 
             isString(type) ?
                 ShapeFlags.ELEMENT : 0

    // vnode
    const vnode = {
        __v_isVNode: true,                           // 虚拟dom标识
        type,
        shapeFlag,
        props,
        children,
        key: props && props.key,                     // key for diff
        component: null,                             // 如果是组件的虚拟节点要保存组件的实例
        el: null,                                    // 虚拟节点对应的真实节点
    }
    if(children) {
        // 告诉节点,是什么形状 shape flag
        // 渲染虚拟节点的时候,可以判断子是数组还是其它,如果是数组就要循环渲染
        vnode.shapeFlag = vnode.shapeFlag | (isString(children) ? ShapeFlags.TEXT_CHILDREN : ShapeFlags.ARRAY_CHILDREN);
    }
    // vnode就是描述对象
    return vnode;
}

// 判断是否是虚拟dom
export function isVNode(vnode) {
    return !!vnode.__v_isVNode;
}

// 将文本转为虚拟节点
export const Text = Symbol();   // 定义字符串类型
export function normalizeVNode(vnode) {
    if (isObject(vnode)) { // 对象忽略
        return vnode;
    }
    return createVNode(Text, null, String(vnode));
}

// 判断两个元素是不是同一个类型
export function isSameVNodeType(n1, n2) {
    // 比较类型是否一致, 比较key是否一致
    return n1.type === n2.type && n1.key === n2.key;
}

3. createAppAPI函数

/packages/runtime-core/src/apiCreateApp.ts

import { createVNode } from "./createVNode";

export function createAppAPI(render) {
    return (rootComponent, rootProps)=>{
        // 判断当前是否已经被加载
        let isMounted = false;
        const app = {
            // 加载组件
            mount(container) {
                // 创建组件虚拟节点
                let vnode = createVNode(rootComponent, rootProps); // h函数
                // 挂载的核心就是根据传入的组件对象,创造一个组件的虚拟节点,再将这个虚拟节点渲染到容器中
                render(vnode, container);
                // 判断是否已经挂载
                if (!isMounted) {
                    isMounted = true;
                }
            }
        }

        return app;
    }
}

4. 初步实现渲染器

/packages/runtime-core/src/rendener.ts

// 渲染器

import { ShapeFlags } from "@vue/shared";
import { createAppAPI } from "./apiCreateApp"

export function createRendener(renderOptions){
    // 组件的加载
    const mountComponent = (initialVNode, container) => {
        // TODO 根据组件的虚拟节点,创造一个真实的节点,渲染到容器中。
        console.log(initialVNode, container);
    }
    // 处理元素
    const processElement = (n1, n2, container) => {
        if (n1 == null) {
            // 首次加载渲染 组件初始化
            mountComponent(n2, container);
        } else {
            // 组件更新
        }
    }

    // n1 -- old n2 -- newVnode
    const patch = (n1, n2, container) => {
        if (n1 == n2) return;   // 相同的元素直接退出

        const { shapeFlag } = n2;   // 拿到新节点的形状标识
        // 1. 文本 与 组件 结果是0  2. 文本 与 函数组伯 结果是0 3. 组件 与 组件 有值
        if (shapeFlag & ShapeFlags.COMPONENT) {
            // 处理元素
            processElement(n1, n2, container);
        }
    }

    const render = (vnode, container) => {
        //将虚拟节点转化成真实节点渲染到容器中

        // patch 包含初次渲染, 如果有更新也会走patch
        patch(null, vnode, container);
    }

    return {
        createApp: createAppAPI(render),
        render
    }
}

三、组件实现

1. 实现组件模型

/packages/runtime-core/src/component.ts

// 组件需要实现响应式,因此要引用响应式模块
import { reactive } from "@vue/reactivity";
import { hasOwn, isFunction, isObject } from "@vue/shared";

export function createComponentInstance(vnode) {
    const type = vnode.type;
    const instance = {
        vnode,                       // 实例对应的虚拟节点
        type,                        // 组件对象
        subTree: null,               // 组件渲染的内容 组件的子集
        ctx: {},                     // 组件上下文
        props: {},                   // 组件属性,from parent
        attrs: {},                   // 除了props之外的属性
        slots: {},                   // 组件的插槽
        setupState: {},              // setup返回的结果
        propsOptions: type.props,    // 属性选项
        proxy: null,                 // 实例的代理对象
        render: null,                // 组件的渲染函数
        emit: null,                  // 事件触发
        exposed: {},                 // 暴露的方法
        isMounted: false,            // 是否挂载完成
    }
    instance.ctx = {_: instance};    // 上下文
    return instance;
}

// 组件props初始化处理
export function initProps(instance, rawProps) {
    const props = {}
    const attrs = {}
    const options = Object.keys(instance.propsOptions);   // 用户注册过的属性
    if (rawProps) {
        for (let key in rawProps) {
            const value = rawProps[key];
            if (options.includes(key)) {
                props[key] = value;
            } else {
                attrs[key] = value;
            }
        }
    }
    // 响应式
    instance.props = reactive(props);
    instance.attrs = attrs;
}

// 上下文,将内部的方法或属性暴露
function createSetupContext(instance) {
    return {
        attrs: instance.attrs,
        slots: instance.slots,
        emit: instance.emit,
        expose: (exposed) => instance.exposed || {}
    }
}

// 响应式get set拦截
const PublicInstanceProxyHandlers = {
    get({_:instance}, key){
        const {setupState, props} = instance;
        // 判断setup结果中是否拥有属性key
        if (hasOwn(setupState, key)) {
            return setupState[key]
        } else if (hasOwn(props, key)) {
            return props[key];
        } else {
            // ....
        }
    },
    set({_:instance}, key, value){
        const {setupState, props} = instance;   // 属性不能修改
        if (hasOwn(setupState, key)) {
            setupState[key] = value;
        } else if (hasOwn(props, key)) {
            // props 不能修改
            console.log("************PROPS IS READONLY************")
            return false;
        } else {
            // ...
        }
        return true;
    }
}

// 调用组件的setup方法
export function setupStatefulComponent(instance) {
    const Component = instance.type;
    const { setup } = Component;
    // 组件代理
    instance.proxy = new Proxy(instance.ctx,  PublicInstanceProxyHandlers);
    if (setup) {
        // setup 上下文 context
        const setupContext = createSetupContext(instance);
        let setupResult = setup(instance.props, setupContext);  // 获取setup的返回值
        // 返回结果可能是函数,也可能是对象
        if (isFunction(setupResult)) {
            instance.render = setupResult;  // 如果setup返回的是函数,那就是render函数
        } else if (isObject(setupResult)) {
            instance.setupState = setupResult;  // 暂存setup返回结果
        }
    }
    // 组件中setup外添加render
    if (!instance.render) {
        // 如果没有render, 而写的是template, 可能要模板编译, 再调用render
        // 如果setup没有返回render, 那就采用组件本身的render函数。
        instance.render = Component.render;
    }
    console.log("instance:", instance)
    console.log("proxy:", instance.proxy.count)
    // instance.proxy.title = 111
}

// 组件的实例赋值
export function setupComponent(instance) {
    const {props, children} = instance.vnode;
    // 组件的props初始化, atts也要初始化
    initProps(instance, props);

    // setup调用
    setupStatefulComponent(instance);   // 调用setup函数,拿到返回值
}

// 父子组件, 子接收父的参数 props,   子里面还有其它属性  放attrs
// 同名, props: {name}   attrs:{name}

2. 实现渲染虚拟函数

/packages/runtime-core/src/h.ts

// 渲染虚拟函数

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

// type-节点类别  propsOrChildren-节点属性  children--节点的子集
export function h(type, propsOrChildren, children) {
    // 1.   h('div',  {title: xxxx})    // <div title="xxxxx">
    // 2.   h('div', h('span'))          // <div><span></span></div>   children
    // 3.   h('div', 'hello')            // <div>hello</div> 
    // 4.   h('div', ['hello', 'hello'])
    // 5.   h('div',{}, "child")  ===> h('div', {}, ["child"])
    // 6.   h('div', {}, 'child','child')  ==> h('div', {}, ['child','child'])

    // 参数的长度
    let len = arguments.length;
    if (len === 2) {
        //
        if (isObject(propsOrChildren)  &&  !Array.isArray(propsOrChildren)) {
            if (isVNode(propsOrChildren)) {
                // 如果是虚拟dom, 需要处理children的shapeflag
                return createVNode(type, null, [propsOrChildren]);   // h('div', h('span'))
            }
            // 子是文本
            return createVNode(type, propsOrChildren);  //h('div',  {title: xxxx})
        } else {
            // 不是对象 或  数组
            return createVNode(type, null, propsOrChildren);  // h('div', 'hello')  h('div', ['hello', 'hello'])
        }
    } else {
        if (len > 3) {
            // 从第二个参数开始截取
            children = Array.prototype.slice.call(arguments, 2);
        } else if (len === 3 && isVNode(children)) {
            children = [children]
        }
        return createVNode(type, propsOrChildren, children);
    }
}

3. 组件实例挂载

/packages/runtime-core/src/rendener.ts

import { createComponentInstance, setupComponent } from "./component";

export function createRendener(renderOptions){
    // 组件的加载
    const mountComponent = (initialVNode, container) => {
        // TODO 根据组件的虚拟节点,创造一个真实的节点,渲染到容器中。
        console.log(initialVNode, container);

        // 根据组件的虚拟节点,创造一个真实节点,渲染到容器中
        // 1.要给组件创造一个组件的实例
        debugger
        const instance = initialVNode.component = createComponentInstance(initialVNode);
        // 2. 需要给组件的实例进行赋值操作
        setupComponent(instance);
    }
    // 处理元素
    const processElement = (n1, n2, container) => {
        if (n1 == null) {
            // 首次加载渲染 组件初始化
            mountComponent(n2, container);
        } else {
            // 组件更新
        }
    }
    
    //其他代码
}

4. 导出函数

/packages/runtime-core/src/index.ts

export { h } from './h'

export * from '@vue/reactivity'

export {createRendener} from './rendener'

调试测试

/packages/runtime-dom/dist/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app"></div>
    <script src="./runtime-dom.global.js"></script>
    <script>
        let {createApp, h, ref} = VueRuntimeDOM;

        console.log(ref,h)
        function useCounter(){
            const count = ref(0);
            const add = ()=>{
                count.value++;
            }
            return {count, add}
        }

        // 单向传递  父---->子 如果要改props,应该子---edmit----父---udpate
        let App = {
            props: {
                title: {}
            },
            setup(props, ctx) {
                let {count, add} = useCounter();

                return {
                    add,
                    count
                }
                
                // return ()=>{
                //     return h('h1', {onClick: add}, 'rendener- ' + count.value);
                // }
            },
            // 每次更新重新调用render方法
            render(proxy) {
                return h('h1', { onClick: this.add, title: proxy.title}, 'hello ' + this.count);
            }
        }

        let app = createApp(App, {title: "vue3-rendener", v: "test_v", cc: "cc"});
        app.mount("#app");
    </script>
</body>
</html>

5. 组件渲染

/packages/runtime-core/src/rendener.ts

// 渲染器

import { ShapeFlags } from "@vue/shared";
import { createAppAPI } from "./apiCreateApp"
import { createComponentInstance, setupComponent } from "./component";
import { ReactiveEffect } from "packages/reactivity/src/effect";
import { Text, isSameVNodeType, isVNode, normalizeVNode } from "./createVNode";

export function createRendener(renderOptions){

    // dom操作
    const {
        insert: hostInsert,
        remove: hostRemove,
        patchProp: hostPatchProp,
        createElement: hostCreateElement,
        createText: hostCreateText,
        setElementText: hostSetElementText,
        setText: hostSetText,
        parentNode: hostParentNode,
        nextSibling: hostNextSibling,

    } = renderOptions;

    const setupRenderEffect = (initialVNode, instance, container)=>{
        let { proxy } = instance;
        // 创建渲染effect, 数据变化,重新调用render
        const componentUpdateFn = ()=>{
            debugger
            // 判断是否已经加载
            if (!instance.isMounted) {
                // 未加载  直接渲染 不用对比
                // 调用render方法,拿到渲染的结果 第一个proxy是作用域, 第二个是参数
                const subTree = instance.subTree = instance.render.call(proxy, proxy);

                // 渲染子级元素,递归调用
                patch(null, subTree, container);

                instance.isMounted = true;  // 已加载,已渲染到页面上 应该有对应dom
                initialVNode.el = subTree.el
            } else {
                // 已加载
                console.log("组件更新")
                
                // 拿到新旧节点
                const prevTree = instance.subTree;
                const nextTree = instance.render.call(proxy, proxy);
                patch(prevTree, nextTree, container);
            }
        }

        const effect = new ReactiveEffect(componentUpdateFn);
        // 默认调用update方法, 即执行componentUpdateFn
        const update = effect.run.bind(effect);
        update();
    }

    // 组件的加载
    const mountComponent = (initialVNode, container) => {
        // TODO 根据组件的虚拟节点,创造一个真实的节点,渲染到容器中。
        console.log(initialVNode, container);

        // 根据组件的虚拟节点,创造一个真实节点,渲染到容器中
        // 1.要给组件创造一个组件的实例
        const instance = initialVNode.component = createComponentInstance(initialVNode);
        // 2. 需要给组件的实例进行赋值操作
        setupComponent(instance);

        // 3. 调用render方法实现组件的渲染逻辑, 如果依赖的状态发生变化,组件要重新渲染
        // 数据和视图的双向绑定,如果数据变化视图要更新, 响应式原理
        setupRenderEffect(initialVNode, instance, container);  // 渲染effect
    }

    // 处理组件
    const processComponent = (n1, n2, container) => {
        if (n1 == null) {
            // 组件初始化
            mountComponent(n2, container);
        } else {
            // 组件更新
        }
    }

    const mountChildren = (children, container) => {
        // ['文本', '文本2'] 多个文本, 需要创建多个文本的节点 加入父元素中
        // <div><span>文本1</span><span>文本2</span></div>

        for (let i=0; i<children.length; i++) {

            // 创建虚拟节点
            const child = (children[i] = normalizeVNode(children[i]));
            patch(null, child, container);
        }
    }

    // 加载元素
    const mountElement = (vnode, container, anchor) => {

        // vnode中的children  可能是字符串 数组 对象数组 字符串数组
        // 虚拟dom的类型, 属性, 儿子的形状
        let { type, props, shapeFlag, children } = vnode;

        // 根据type创建元素
        let el = vnode.el = hostCreateElement(type);

        // 根据元素的形状处理  字符串  数组
        if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {// 字符串的子节点  直接替换内容 el.textContent=xxx
            hostSetElementText(el, children);
        } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
            mountChildren(children, el); // 数组
        }

        // 处理样式
        if (props) {
            for (const key in props) {
                // 给元素添加属性
                hostPatchProp(el, key, null, props[key]);
            }
        }

        // 插入
        hostInsert(el, container, anchor);
    }
    
    const patchProps = (oldProps, newProps, el) => {
        if (oldProps == newProps) return;

        //新的属性要添加或更新到节点上
        for (let key in newProps) {
            const prev = oldProps[key];  //旧的属性
            const next = newProps[key];  //新的属性
            if (prev !== next) {
                hostPatchProp(el, key, prev, next);
            }
        }
        //旧的属性要从节点上删除
        for (let key in oldProps) {
            if (!(key in newProps)) {
                hostPatchProp(el, key, oldProps[key], null);
            }
        }
    }

    // 处理元素
    const processElement = (n1, n2, container, anchor) => {
        if (n1 == null) {
            // 首次加载渲染 组件初始化
            mountElement(n2, container, anchor);
        } else {
            // 组件更新
        }
    }

    // 处理文本
    const processText = (n1, n2, container) => {
        if (n1 == null) {
            // 文本初始化
            let textNode = hostCreateText(n2.children);
            hostInsert(textNode, container);
            // 要让虚拟节点和真实节点挂载上
            n2.el = textNode;
        }
    }

    const unmount = (vnode) =>{
        hostRemove(vnode.el);  // 删除真实节点
    }

    // n1 -- old n2 -- newVnode
    const patch = (n1, n2, container, anchor = null) => {
        // 新旧元素不相同则卸载元素
        if (n1 && isSameVNodeType(n1, n2)) {
            // 卸载旧元素
            unmount(n1);
            n1 = null;
        }

        if (n1 == n2) return;   // 相同的元素直接退出

        const { shapeFlag, type } = n2;   // 拿到新节点的形状标识
        switch(type) {
            case Text:
                processText(n1, n2, container);
                break;
            
            default:
                if (shapeFlag & ShapeFlags.COMPONENT) {
                    // 处理元素
                    processComponent(n1, n2, container);
                } else if (shapeFlag & ShapeFlags.ELEMENT) {
                    processElement(n1, n2, container, anchor)
                }
        }


        // 1. 文本 与 组件 结果是0  2. 文本 与 函数组伯 结果是0 3. 组件 与 组件 有值
        
    }

    const render = (vnode, container) => {
        //将虚拟节点转化成真实节点渲染到容器中

        // patch 包含初次渲染, 如果有更新也会走patch
        patch(null, vnode, container);
    }

    return {
        createApp: createAppAPI(render),
        render
    }
}

调试测试

/packages/runtime-dom/dist/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app"></div>
    <script src="./runtime-dom.global.js"></script>
    <script>
        let {createApp, h, ref} = VueRuntimeDOM;

        console.log(ref,h)
        function useCounter(){
            const count = ref(0);
            let flag = ref(true);
            const add = ()=>{
                count.value++;
            }
            return {count, add, flag}
        }

        // 单向传递  父---->子 如果要改props,应该子---edmit----父---udpate
        let App = {
            props: {
                title: {}
            },
            setup(props, ctx) {
                let {count, add, flag} = useCounter();

                setTimeout(()=>{
                    flag.value = !flag.value;
                    console.log("flag:", flag.value)
                }, 3000)

                return {
                    add,
                    count,
                    flag
                }
                
                // return ()=>{
                //     return h('h1', {onClick: add}, 'rendener- ' + count.value);
                // }
            },
            // 每次更新重新调用render方法
            // render(proxy) {
            //     return this.flag.value ? 
            //     h('h1', { onClick: this.add, title: proxy.title, style:{color: 'red'}}, h('span', ['aaa',h('a', 'bbbb')]), this.count.value):
            //     h('h1', { onClick: this.add, title: proxy.title, style:{color: 'green'}}, h('span', ['aaa',h('a', 'cccc')]), this.count.value);
            // }
            render(proxy) {
                return this.flag.value ?
                h('div', {}, [
                    h('li', {key: 'a'}, 'a'),
                    h('li', {key: 'b'}, 'b'),
                    h('li', {key: 'c'}, 'c'),
                    h('li', {key: 'd'}, 'd'),
                ]):
                h('div', {}, [
                    h('li', {key: 'a'}, 'a'),
                    h('li', {key: 'b'}, 'b'),
                    h('li', {key: 'c'}, 'c'),
                ])
            }
        }

        let app = createApp(App, {title: "vue3-rendener", v: "test_v", cc: "cc"});
        app.mount("#app");
    </script>
</body>
</html>

四、diff算法

1. vue3 与 vue2 diff算法对比

  • Vue2的Diff算法可以比作"全量比较"的方式

    • 双端比较算法:从新旧节点的两端开始比较(头头、尾尾、头尾、尾头),像在数组中查找差异的双指针方法。
    • 就地更新策略:尽量复用相同类型的节点,通过移动节点位置而不是创建新节点。
    • Key的重要性:有key时可以准确识别节点身份。没有key时会暴力比对,效率低。
    • 问题点
      • 总是进行全树比较,即使某些部分从未变化
      • 移动DOM节点的操作有时不够高效
      • 对静态内容也会进行不必要的比较
  • Vue3的改进可以比作"智能增量更新"

    • 编译时优化:静态节点提升:像.NET中的常量,编译时标记,运行时跳过。静态树提升:整个静态子树只创建一次,后续复用。
    • 块树(Block Tree)概念:模板被编译为"块",每个块知道自己的动态节点。像.NET中的partial class,只关注变化部分。
    • 基于最长递增子序列的算法:最小化DOM操作次数,处理节点移动时更高效。
    • 事件缓存:避免每次更新都创建新的事件处理函数,像.NET中缓存委托实例。

2. 举例

<!-- 之前 -->
<ul>
  <li key="a">A</li>
  <li key="b">B</li>
  <li key="c">C</li>
</ul>

<!-- 之后 -->
<ul>
  <li key="c">C</li>
  <li key="a">A</li>
  <li key="b">B</li>
</ul>

Vue2处理方式:
比较头头(a-c) → 不匹配
比较尾尾(c-b) → 不匹配
比较头尾(a-b) → 不匹配
比较尾头(c-a) → 不匹配
暴力遍历查找,然后移动节点

Vue3处理方式:
识别出key的顺序变化
使用最长递增子序列找出最优移动方案
只需移动C到最前面

3. 实现diff算法

export function createRendener(renderOptions){
  
    // 其他代码
	
	/**
     * diff算法
     * @param c1 老的儿子
     * @param c2 新的儿子
     * @param container 
     */
    const patchKeyedChildren = (c1, c2, container) => {
        let e1 = c1.length - 1;    // 老的数组长度  e==end
        let e2 = c2.length - 1;    // 新的数组长度
        let i = 0;                 // 开始比较计数指针  i左指针

        // 1. sync from start 从头开始比较,遇到不同的节点就停止比较
        while(i <= e1 && i <= e2) {
            const n1 = c1[i];      // 取出一个老节点
            const n2 = c2[i];      // 取出一个新节点

            // 如果两个节点是相同节点,则需要递归比较孩子和自身的属性
            if (isSameVNodeType(n1, n2)) {
                patch(n1, n2, container);  // 对比属性和子集
            } else {
                break;              // 新老节点不相同,停止比较
            }
            i++;
        }

        // 2. sync from end 从后往前遍历
        while(i <= e1 && i <= e2) { // 如果i与新的数组或老数组长度重合,说明比较结束了
            const n1 = c1[e1];  // 取右边的老节点
            const n2 = c2[e2];  // 取右边的新节点

            if (isSameVNodeType(n1, n2)) {
                patch(n1, n2, container);
            } else {
                break;
            }
            e1--;
            e2--;               // 从右向左移动指针
        }

        // 3. common sequence + mount
        // 对比索引大小,处理新增或删除节点
        if (i > e1) {
            if (i <= e2) {
                // insert---需要增加的元素
                const nextPos = e2 + 1;  // 下一个元素索引位置
                // 取e2的下一个元素,如果没有,则长度和当前c2长度相同, 说明是追加
                // 如果下一个元素有,说明要插入在下一个元素的前面,将下一个元素作为参照物
                const anchor = nextPos < c2.length ? c2[nextPos].el : null;

                // 参照物作用是判断是向前插入还是向后插入
                while(i <= e2) {
                    patch(null, c2[i], container, anchor);
                    i++;
                }
            }
        } else if (i > e2) {// 4 common sequence + unmount
            // 存在被删除节点
            // 1. 左侧被删除, 2. 中间被删除, 3. 右铡被删除
            while(i <= e1) {
                // i 与 e1 之间的就是要删除的
                unmount(c1[i]);
                i++;
            }
        }

        // 5. unknown sequence  未知序列
        const s1 = i;   // 老节点的开始位置
        const s2 = i;   // 新节点的结束位置

        // 根据新的节点,创造一个映射表,用老的列表去里面找有没有,如果有则复用,没有就删除。
        const keyToNewIndexMap = new Map();  //可以用老的节点来查看有没有新的
        for (let i = s2; i<=e2; i++) {
            const child = c2[i];  //取出新的节点
            keyToNewIndexMap.set(child.key, i);  //将新元素的位置记录下来
        }

        // 需要比较的新元素数量
        const toBepatched = e2 - s2 + 1;
        // 创建一个要比较的数组, 长度为toBepatched,默认值为0
        const newIndexToOldMapIndex = new Array(toBepatched).fill(0);

        // 遍历老节点,找到一样的需要patch属性和孩子
        for (let i = s1; i <= e1; i++) {
            const prevChild = c1[i];  // 老节点
            // 在新节点的映射表中查找是否存在
            let newIndex = keyToNewIndexMap.get(prevChild.key);
            if (newIndex == undefined) {
                // 老节点在新的映射表中不存在,要删除
                unmount(prevChild);
            } else {
                // 更新映射表
                newIndexToOldMapIndex[newIndex - s2] = i + 1; // 保证不为0,0表示要增加元素
                // 对比属性和孩子
                patch(prevChild, c2[newIndex], container);
            }
        }

        // 新节点比老节点要多,则需要遍历新节点 从右到左
        for (let i = toBepatched - 1; i >= 0; i--) {
            let lastIndex = s2 + i;  //末尾元素索引
            let lastChild = c2[lastIndex];  //末尾元素
            // 参照节点
            let anchor = lastIndex + 1 < c2.length ? c2[lastIndex + 1].el : null;

            if (newIndexToOldMapIndex[i] == 0) {
                // 为0表示还没有真实节点,需要创建真实节点并插入
                patch(null, lastChild, container, anchor);
            } else {
                // 不为0表示已存在相应老节点,要移动元素
                hostInsert(lastChild.el, container, anchor);
            }
        }
    }

    // 将所有儿子卸载
    const unmountChildren = (children) => {
        for (let i = 0; i<children.length; i++) {
            unmount(children[i]);
        }
    }

    // 比较儿子 n1--旧节点, n2--新节点
    const patchChildren = (n1, n2, el) => {
        const c1 = n1 && n1.children; // 老的儿子
        const c2 = n2 && n2.children; // 新的儿子
        const prevShapeFlag = n1.shapeFlag;   // 老的形状标识
        const shapeFlag = n2.shapeFlag;       // 新的形状标识

        // c1和c2类型
        // 1. 老的是数组,新的是文本, 删除老的,添加新的
        // 2. 老的是数组,新的是数组,比较它们的儿子---diff
        // 3. 老的是文本,新的是空,直接删除老的
        // 4. 老的是文本,新的是文本,直接更新文本
        // 5. 老的是文本,新的数组,删除文本,新增数组
        // 6. 老的是空, 新的是文本,直接添加

        // 位运算判断
        if (shapeFlag & ShapeFlags.TEXT_CHILDREN ) {// 新的是文本
            if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {// 老的是数组
                unmountChildren(c1);  // 1
            }
            if (c1 !== c2) {// 老的是文本  46
                hostSetElementText(el, c2);
            }

        } else {
            if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {// 老的是数组
                if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {// 新的是数组
                    // diff
                    patchKeyedChildren(c1, c2, el); //2
                } else {
                    // 新的不是数组
                    unmountChildren(c1);// 1
                }
            } else {
                if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {// 老的是文本
                    hostSetElementText(el, c2); // 4
                }
                if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {// 新的是数组,老的是文本
                    mountChildren(c2, el); //5
                }
            }
        }

    }

    const patchElement = (n1, n2) => {
        debugger
        let el = n2.el = n1.el;   // 先比较元素 元素是一致
        const oldProps = n1.props || {};  // 旧元素的属性
        const newProps = n2.props || {};  // 新元素的属性
        // 比较元素的属性
        patchProps(oldProps, newProps, el);

        // 儿子比较
        patchChildren(n1, n2, el);
    }

    // 处理元素
    const processElement = (n1, n2, container, anchor) => {
        if (n1 == null) {
            // 首次加载渲染 组件初始化
            mountElement(n2, container, anchor);
        } else {
            // 组件更新  比较2个元素之间的差异
            patchElement(n1, n2);
        }
    }
    
    // 其他代码
}

4. diff未知序列

export function createRendener(renderOptions){
  
    // 其他代码
	
	/**
     * diff算法
     * @param c1 老的儿子
     * @param c2 新的儿子
     * @param container 
     */
    const patchKeyedChildren = (c1, c2, container) => {
        let e1 = c1.length - 1;    // 老的数组长度  e==end
        let e2 = c2.length - 1;    // 新的数组长度
        let i = 0;                 // 开始比较计数指针  i左指针
		
        // 其他代码

        // 5. unknown sequence  未知序列
        const s1 = i;   // 老节点的开始位置
        const s2 = i;   // 新节点的结束位置

        // 根据新的节点,创造一个映射表,用老的列表去里面找有没有,如果有则复用,没有就删除。
        const keyToNewIndexMap = new Map();  //可以用老的节点来查看有没有新的
        for (let i = s2; i<=e2; i++) {
            const child = c2[i];  //取出新的节点
            keyToNewIndexMap.set(child.key, i);  //将新元素的位置记录下来
        }

        // 需要比较的新元素数量
        const toBepatched = e2 - s2 + 1;
        // 创建一个要比较的数组, 长度为toBepatched,默认值为0
        const newIndexToOldMapIndex = new Array(toBepatched).fill(0);

        // 遍历老节点,找到一样的需要patch属性和孩子
        for (let i = s1; i <= e1; i++) {
            const prevChild = c1[i];  // 老节点
            // 在新节点的映射表中查找是否存在
            let newIndex = keyToNewIndexMap.get(prevChild.key);
            if (newIndex == undefined) {
                // 老节点在新的映射表中不存在,要删除
                unmount(prevChild);
            } else {
                // 更新映射表
                newIndexToOldMapIndex[newIndex - s2] = i + 1; // 保证不为0,0表示要增加元素
                // 对比属性和孩子
                patch(prevChild, c2[newIndex], container);
            }
        }

        // 新节点比老节点要多,则需要遍历新节点 从右到左
        for (let i = toBepatched - 1; i >= 0; i--) {
            let lastIndex = s2 + i;  //末尾元素索引
            let lastChild = c2[lastIndex];  //末尾元素
            // 参照节点
            let anchor = lastIndex + 1 < c2.length ? c2[lastIndex + 1].el : null;

            if (newIndexToOldMapIndex[i] == 0) {
                // 为0表示还没有真实节点,需要创建真实节点并插入
                patch(null, lastChild, container, anchor);
            } else {
                // 不为0表示已存在相应老节点,要移动元素
                hostInsert(lastChild.el, container, anchor);
            }
        }
    }
    
    // 其他代码
}

5. 计算最长递增子序列

js算法

var seq = function (arr) {
    let result = [arr[0]];  // 先存入第1个值
    for (let i=1; i<arr.length; ++i){
        // 如果当前数值大于已选结果的最后一位,则直接往后新增
        // 若当前数组更小,则直接替换前面第一个大于它的数值
        if (arr[i] > result[result.length - 1]) {
            result[result.length] = arr[i];
        } else {
            // 二分查找: 找到第一个大于当前数值的结果进行替换
            let left = 0, right = result.length - 1;
            while (left < right) {
                let middle = ((left + right) / 2) | 0;
                if (result[middle] < arr[i]) {
                    left = middle + 1;
                } else {
                    right = middle;
                }
            }
            // 替换当前下标
            result[left] = arr[i];
        }
    }
    return result;
}


arr = [10,9,2,5,3,7,101,18]
console.log(seq(arr))

diff算法扩展最长递增子序列

// 其他代码

// 计算最长递增子序列  索引值
function getSequence(arr) {
    let len = arr.length;
    const result = [0];   // 索引值
    let p = arr.slice(0);  // 用来记录前驱节点的索引,回溯正确的顺序
    let lastIndex;
    let start;
    let end;
    let middle;
    
    for (let i=0; i<len; i++) {
        const arrI = arr[i]; // 存每一项的值
        if (arrI !== 0) {
            lastIndex = result[result.length - 1]; // 获取结果中最后一个
            if (arr[lastIndex] < arrI) {
                // 当前结果集中的最后一个  和这一顶比较
                p[i] = lastIndex;
                result.push(i);
                continue;
            }
            // 二分查找 替换元素
            start = 0;
            end = result.length - 1;
            while(start < end) {
                middle = ((start + end) / 2) | 0; // 中间索引值
                // 找到序列中间的索引值,通过索引找到对应的值
                if (arr[result[middle]] < arrI) {
                    start = middle + 1;
                } else {
                    end = middle;
                }
            }
            if (arrI < arr[result[start]]) {// 要替换成3的索引
                // 替换前,应该让当前元素的索引 标识到p上
                p[i] = result[start - 1];
                result[start] = i;  // 贪心算法
            }
        }
    }

    let i = result.length; // 拿到最后一个,开始向前追溯, 都是基于索引值
    let last = result[i - 1]; // 取最后一个

    while(i-- > 0) {
        // 通过前驱节点找到正确的调用顺序
        result[i] = last; // 最后一项肯定是正确
        // 从p中往前找
        last = p[last];
    }
    return result;
}

export function createRendener(renderOptions){
  
    // 其他代码
	
	/**
     * diff算法
     * @param c1 老的儿子
     * @param c2 新的儿子
     * @param container 
     */
    const patchKeyedChildren = (c1, c2, container) => {
        let e1 = c1.length - 1;    // 老的数组长度  e==end
        let e2 = c2.length - 1;    // 新的数组长度
        let i = 0;                 // 开始比较计数指针  i左指针
		
        // 其他代码
        
        // 需要移动的元素
        let queue = getSequence(newIndexToOldMapIndex);

        let j = queue.length - 1;  // 拿到最长递增子序列的末尾索引

        // 新节点比老节点要多,则需要遍历新节点 从右到左
        for (let i = toBepatched - 1; i >= 0; i--) {
            let lastIndex = s2 + i;  //末尾元素索引
            let lastChild = c2[lastIndex];  //末尾元素
            // 参照节点
            let anchor = lastIndex + 1 < c2.length ? c2[lastIndex + 1].el : null;

            if (newIndexToOldMapIndex[i] == 0) {
                // 为0表示还没有真实节点,需要创建真实节点并插入
                patch(null, lastChild, container, anchor);
            } else {
                // 不为0表示已存在相应老节点,要移动元素
                // 性能优化,最长递增子序列 可减少dom的操作
                if (i !== queue[j]) {
                    // 移动元素
                    hostInsert(lastChild.el, container, anchor);
                } else {
                    j--; // 不需要移动元素
                }
            }
        }
    }
    
    // 其他代码
}

完整代码

/packages/runtime-core/src/rendener.ts

// 渲染器

import { ShapeFlags } from "@vue/shared";
import { createAppAPI } from "./apiCreateApp"
import { createComponentInstance, setupComponent } from "./component";
import { ReactiveEffect } from "packages/reactivity/src/effect";
import { Text, isSameVNodeType, isVNode, normalizeVNode } from "./createVNode";

// 计算最长递增子序列  索引值
function getSequence(arr) {
    let len = arr.length;
    const result = [0];   // 索引值
    let p = arr.slice(0);  // 用来记录前驱节点的索引,回溯正确的顺序
    let lastIndex;
    let start;
    let end;
    let middle;
    
    for (let i=0; i<len; i++) {
        const arrI = arr[i]; // 存每一项的值
        if (arrI !== 0) {
            lastIndex = result[result.length - 1]; // 获取结果中最后一个
            if (arr[lastIndex] < arrI) {
                // 当前结果集中的最后一个  和这一顶比较
                p[i] = lastIndex;
                result.push(i);
                continue;
            }
            // 二分查找 替换元素
            start = 0;
            end = result.length - 1;
            while(start < end) {
                middle = ((start + end) / 2) | 0; // 中间索引值
                // 找到序列中间的索引值,通过索引找到对应的值
                if (arr[result[middle]] < arrI) {
                    start = middle + 1;
                } else {
                    end = middle;
                }
            }
            if (arrI < arr[result[start]]) {// 要替换成3的索引
                // 替换前,应该让当前元素的索引 标识到p上
                p[i] = result[start - 1];
                result[start] = i;  // 贪心算法
            }
        }
    }

    let i = result.length; // 拿到最后一个,开始向前追溯, 都是基于索引值
    let last = result[i - 1]; // 取最后一个

    while(i-- > 0) {
        // 通过前驱节点找到正确的调用顺序
        result[i] = last; // 最后一项肯定是正确
        // 从p中往前找
        last = p[last];
    }
    return result;
}

export function createRendener(renderOptions){

    // dom操作
    const {
        insert: hostInsert,
        remove: hostRemove,
        patchProp: hostPatchProp,
        createElement: hostCreateElement,
        createText: hostCreateText,
        setElementText: hostSetElementText,
        setText: hostSetText,
        parentNode: hostParentNode,
        nextSibling: hostNextSibling,

    } = renderOptions;

    const setupRenderEffect = (initialVNode, instance, container)=>{
        let { proxy } = instance;
        // 创建渲染effect, 数据变化,重新调用render
        const componentUpdateFn = ()=>{
            // 判断是否已经加载
            if (!instance.isMounted) {
                // 未加载  直接渲染 不用对比
                // 调用render方法,拿到渲染的结果 第一个proxy是作用域, 第二个是参数
                const subTree = instance.subTree = instance.render.call(proxy, proxy);

                // 渲染子级元素,递归调用
                patch(null, subTree, container);

                instance.isMounted = true;  // 已加载,已渲染到页面上 应该有对应dom
                initialVNode.el = subTree.el
            } else {
                // 已加载
                console.log("组件更新")
                
                // 拿到新旧节点
                const prevTree = instance.subTree;  // 老虚拟dom
                const nextTree = instance.render.call(proxy, proxy);  //新虚拟dom
                patch(prevTree, nextTree, container);
            }
        }

        const effect = new ReactiveEffect(componentUpdateFn);
        // 默认调用update方法, 即执行componentUpdateFn
        const update = effect.run.bind(effect);
        update();
    }

    // 组件的加载
    const mountComponent = (initialVNode, container) => {
        // TODO 根据组件的虚拟节点,创造一个真实的节点,渲染到容器中。
        console.log(initialVNode, container);

        // 根据组件的虚拟节点,创造一个真实节点,渲染到容器中
        // 1.要给组件创造一个组件的实例
        const instance = initialVNode.component = createComponentInstance(initialVNode);
        // 2. 需要给组件的实例进行赋值操作
        setupComponent(instance);

        // 3. 调用render方法实现组件的渲染逻辑, 如果依赖的状态发生变化,组件要重新渲染
        // 数据和视图的双向绑定,如果数据变化视图要更新, 响应式原理
        setupRenderEffect(initialVNode, instance, container);  // 渲染effect
    }

    // 处理组件
    const processComponent = (n1, n2, container) => {
        if (n1 == null) {
            // 组件初始化
            mountComponent(n2, container);
        } else {
            // 组件更新
        }
    }

    const mountChildren = (children, container) => {
        // ['文本', '文本2'] 多个文本, 需要创建多个文本的节点 加入父元素中
        // <div><span>文本1</span><span>文本2</span></div>

        for (let i=0; i<children.length; i++) {

            // 创建虚拟节点
            const child = (children[i] = normalizeVNode(children[i]));
            patch(null, child, container);
        }
    }

    // 加载元素
    const mountElement = (vnode, container, anchor) => {

        // vnode中的children  可能是字符串 数组 对象数组 字符串数组
        // 虚拟dom的类型, 属性, 儿子的形状
        let { type, props, shapeFlag, children } = vnode;

        // 根据type创建元素
        let el = vnode.el = hostCreateElement(type);

        // 根据元素的形状处理  字符串  数组
        if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {// 字符串的子节点  直接替换内容 el.textContent=xxx
            hostSetElementText(el, children);
        } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
            mountChildren(children, el); // 数组
        }

        // 处理样式
        if (props) {
            for (const key in props) {
                // 给元素添加属性
                hostPatchProp(el, key, null, props[key]);
            }
        }

        // 插入
        hostInsert(el, container, anchor);
    }

    const patchProps = (oldProps, newProps, el) => {
        if (oldProps == newProps) return;

        //新的属性要添加或更新到节点上
        for (let key in newProps) {
            const prev = oldProps[key];  //旧的属性
            const next = newProps[key];  //新的属性
            if (prev !== next) {
                hostPatchProp(el, key, prev, next);
            }
        }
        //旧的属性要从节点上删除
        for (let key in oldProps) {
            if (!(key in newProps)) {
                hostPatchProp(el, key, oldProps[key], null);
            }
        }
    }

    /**
     * diff算法
     * @param c1 老的儿子
     * @param c2 新的儿子
     * @param container 
     */
    const patchKeyedChildren = (c1, c2, container) => {
        let e1 = c1.length - 1;    // 老的数组长度  e==end
        let e2 = c2.length - 1;    // 新的数组长度
        let i = 0;                 // 开始比较计数指针  i左指针

        // 1. sync from start 从头开始比较,遇到不同的节点就停止比较
        while(i <= e1 && i <= e2) {
            const n1 = c1[i];      // 取出一个老节点
            const n2 = c2[i];      // 取出一个新节点

            // 如果两个节点是相同节点,则需要递归比较孩子和自身的属性
            if (isSameVNodeType(n1, n2)) {
                patch(n1, n2, container);  // 对比属性和子集
            } else {
                break;              // 新老节点不相同,停止比较
            }
            i++;
        }

        // 2. sync from end 从后往前遍历
        while(i <= e1 && i <= e2) { // 如果i与新的数组或老数组长度重合,说明比较结束了
            const n1 = c1[e1];  // 取右边的老节点
            const n2 = c2[e2];  // 取右边的新节点

            if (isSameVNodeType(n1, n2)) {
                patch(n1, n2, container);
            } else {
                break;
            }
            e1--;
            e2--;               // 从右向左移动指针
        }

        // 3. common sequence + mount
        // 对比索引大小,处理新增或删除节点
        if (i > e1) {
            if (i <= e2) {
                // insert---需要增加的元素
                const nextPos = e2 + 1;  // 下一个元素索引位置
                // 取e2的下一个元素,如果没有,则长度和当前c2长度相同, 说明是追加
                // 如果下一个元素有,说明要插入在下一个元素的前面,将下一个元素作为参照物
                const anchor = nextPos < c2.length ? c2[nextPos].el : null;

                // 参照物作用是判断是向前插入还是向后插入
                while(i <= e2) {
                    patch(null, c2[i], container, anchor);
                    i++;
                }
            }
        } else if (i > e2) {// 4 common sequence + unmount
            // 存在被删除节点
            // 1. 左侧被删除, 2. 中间被删除, 3. 右铡被删除
            while(i <= e1) {
                // i 与 e1 之间的就是要删除的
                unmount(c1[i]);
                i++;
            }
        }

        // 5. unknown sequence  未知序列
        const s1 = i;   // 老节点的开始位置
        const s2 = i;   // 新节点的结束位置

        // 根据新的节点,创造一个映射表,用老的列表去里面找有没有,如果有则复用,没有就删除。
        const keyToNewIndexMap = new Map();  //可以用老的节点来查看有没有新的
        for (let i = s2; i<=e2; i++) {
            const child = c2[i];  //取出新的节点
            keyToNewIndexMap.set(child.key, i);  //将新元素的位置记录下来
        }

        // 需要比较的新元素数量
        const toBepatched = e2 - s2 + 1;
        // 创建一个要比较的数组, 长度为toBepatched,默认值为0
        const newIndexToOldMapIndex = new Array(toBepatched).fill(0);

        // 遍历老节点,找到一样的需要patch属性和孩子
        for (let i = s1; i <= e1; i++) {
            const prevChild = c1[i];  // 老节点
            // 在新节点的映射表中查找是否存在
            let newIndex = keyToNewIndexMap.get(prevChild.key);
            if (newIndex == undefined) {
                // 老节点在新的映射表中不存在,要删除
                unmount(prevChild);
            } else {
                // 更新映射表
                newIndexToOldMapIndex[newIndex - s2] = i + 1; // 保证不为0,0表示要增加元素
                // 对比属性和孩子
                patch(prevChild, c2[newIndex], container);
            }
        }

        debugger
        // 需要移动的元素
        let queue = getSequence(newIndexToOldMapIndex);

        let j = queue.length - 1;  // 拿到最长递增子序列的末尾索引

        // 新节点比老节点要多,则需要遍历新节点 从右到左
        for (let i = toBepatched - 1; i >= 0; i--) {
            let lastIndex = s2 + i;  //末尾元素索引
            let lastChild = c2[lastIndex];  //末尾元素
            // 参照节点
            let anchor = lastIndex + 1 < c2.length ? c2[lastIndex + 1].el : null;

            if (newIndexToOldMapIndex[i] == 0) {
                // 为0表示还没有真实节点,需要创建真实节点并插入
                patch(null, lastChild, container, anchor);
            } else {
                // 不为0表示已存在相应老节点,要移动元素
                // 性能优化,最长递增子序列 可减少dom的操作
                if (i !== queue[j]) {
                    // 移动元素
                    hostInsert(lastChild.el, container, anchor);
                } else {
                    j--; // 不需要移动元素
                }
            }
        }
    }

    // 将所有儿子卸载
    const unmountChildren = (children) => {
        for (let i = 0; i<children.length; i++) {
            unmount(children[i]);
        }
    }

    // 比较儿子 n1--旧节点, n2--新节点
    const patchChildren = (n1, n2, el) => {
        const c1 = n1 && n1.children; // 老的儿子
        const c2 = n2 && n2.children; // 新的儿子
        const prevShapeFlag = n1.shapeFlag;   // 老的形状标识
        const shapeFlag = n2.shapeFlag;       // 新的形状标识

        // c1和c2类型
        // 1. 老的是数组,新的是文本, 删除老的,添加新的
        // 2. 老的是数组,新的是数组,比较它们的儿子---diff
        // 3. 老的是文本,新的是空,直接删除老的
        // 4. 老的是文本,新的是文本,直接更新文本
        // 5. 老的是文本,新的数组,删除文本,新增数组
        // 6. 老的是空, 新的是文本,直接添加

        // 位运算判断
        if (shapeFlag & ShapeFlags.TEXT_CHILDREN ) {// 新的是文本
            if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {// 老的是数组
                unmountChildren(c1);  // 1
            }
            if (c1 !== c2) {// 老的是文本  46
                hostSetElementText(el, c2);
            }

        } else {
            if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {// 老的是数组
                if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {// 新的是数组
                    // diff
                    patchKeyedChildren(c1, c2, el); //2
                } else {
                    // 新的不是数组
                    unmountChildren(c1);// 1
                }
            } else {
                if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {// 老的是文本
                    hostSetElementText(el, c2); // 4
                }
                if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {// 新的是数组,老的是文本
                    mountChildren(c2, el); //5
                }
            }
        }

    }

    const patchElement = (n1, n2) => {
        let el = n2.el = n1.el;   // 先比较元素 元素是一致
        const oldProps = n1.props || {};  // 旧元素的属性
        const newProps = n2.props || {};  // 新元素的属性
        // 比较元素的属性
        patchProps(oldProps, newProps, el);

        // 儿子比较
        patchChildren(n1, n2, el);
    }

    // 处理元素
    const processElement = (n1, n2, container, anchor) => {
        if (n1 == null) {
            // 首次加载渲染 组件初始化
            mountElement(n2, container, anchor);
        } else {
            // 组件更新  比较2个元素之间的差异
            patchElement(n1, n2);
        }
    }

    // 处理文本
    const processText = (n1, n2, container) => {
        if (n1 == null) {
            // 文本初始化
            let textNode = hostCreateText(n2.children);
            hostInsert(textNode, container);
            // 要让虚拟节点和真实节点挂载上
            n2.el = textNode;
        }
    }

    const unmount = (vnode) =>{
        hostRemove(vnode.el);  // 删除真实节点
    }

    // n1 -- old n2 -- newVnode
    const patch = (n1, n2, container, anchor = null) => {
        // 新旧元素不相同则卸载元素
        if (n1 && !isSameVNodeType(n1, n2)) {
            // 卸载旧元素
            unmount(n1);
            n1 = null;
        }

        if (n1 == n2) return;   // 相同的元素直接退出

        const { shapeFlag, type } = n2;   // 拿到新节点的形状标识
        switch(type) {
            case Text:
                processText(n1, n2, container);
                break;
            
            default:
                if (shapeFlag & ShapeFlags.COMPONENT) {
                    // 处理元素
                    processComponent(n1, n2, container);
                } else if (shapeFlag & ShapeFlags.ELEMENT) {
                    processElement(n1, n2, container, anchor)
                }
        }


        // 1. 文本 与 组件 结果是0  2. 文本 与 函数组伯 结果是0 3. 组件 与 组件 有值
        
    }

    const render = (vnode, container) => {
        //将虚拟节点转化成真实节点渲染到容器中

        // patch 包含初次渲染, 如果有更新也会走patch
        patch(null, vnode, container);
    }

    return {
        createApp: createAppAPI(render),
        render
    }
}

6. 调试测试

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app"></div>
    <script src="./runtime-dom.global.js"></script>
    <script>
        let {createApp, h, ref} = VueRuntimeDOM;

        console.log(ref,h)
        function useCounter(){
            const count = ref(0);
            let flag = ref(true);
            const add = ()=>{
                count.value++;
            }
            return {count, add, flag}
        }

        // 单向传递  父---->子 如果要改props,应该子---edmit----父---udpate
        let App = {
            props: {
                title: {}
            },
            setup(props, ctx) {
                let {count, add, flag} = useCounter();

                setTimeout(()=>{
                    flag.value = !flag.value;
                    console.log("flag:", flag.value)
                }, 3000)

                return {
                    add,
                    count,
                    flag
                }
                
                // return ()=>{
                //     return h('h1', {onClick: add}, 'rendener- ' + count.value);
                // }
            },
            // 每次更新重新调用render方法
            // render(proxy) {
            //     return this.flag.value ? 
            //     h('h1', { onClick: this.add, title: proxy.title, style:{color: 'red'}}, h('span', ['aaa',h('a', 'bbbb')]), this.count.value):
            //     h('h1', { onClick: this.add, title: proxy.title, style:{color: 'green'}}, h('span', ['aaa',h('a', 'cccc')]), this.count.value);
            // }
            render(proxy) {
                return this.flag.value ?
                h('div', {}, [
                    h('li', {key: 'a'}, 'a'),
                    h('li', {key: 'b'}, 'b'),
                    h('li', {key: 'c'}, 'c'),
                    h('li', {key: 'd'}, 'd'),
                    h('li', {key: 'e'}, 'e'),
                    h('li', {key: 'f'}, 'f'),
                    h('li', {key: 'g'}, 'g'),
                ]):
                h('div', {}, [
                    h('li', {key: 'a'}, 'a'),
                    h('li', {key: 'b'}, 'b'),
                    h('li', {key: 'e'}, 'e'),
                    h('li', {key: 'c'}, 'c'),
                    h('li', {key: 'd'}, 'd'),
                    h('li', {key: 'h'}, 'h'),
                    h('li', {key: 'f'}, 'f'),
                    h('li', {key: 'g'}, 'g'),
                ])
            }
        }

        let app = createApp(App, {title: "vue3-rendener", v: "test_v", cc: "cc"});
        app.mount("#app");
    </script>
</body>
</html>