实现minVue,包含reactive,diff算法最长上升子序列的应用,renderer渲染器的实现,createApp的实现,组件生命周期钩子(一)

262 阅读8分钟

为什么写这篇文章呢,因为直接看vue源码很痛苦,写一个篇文章让大家对vue的大致更新流程有一定概念,再去读源码可能会轻松些。

正文

vue的UI的效果就是响应式数据变化界面也跟着刷新。 所以我们可以简单表示一下这个过程

响应式数据变化 ---> 组件更新

如果组件的更新可以调用一个函数就可以那么过程就可以这样表示

响应式数据变化 ---> 调用组件的更新函数

所以我们要实现的响应式数据就要能做到收集函数的功能

在vue中使用proxy去做响应式,如果一个对象被proxy包裹,那么它的读取和设置就会先经过proxy这个对象

const obj = new Proxy({
    num: 0
  }, {
    get(target, key) {
      console.log('get', key)
      return target[key];
    },
    set(target, key, value) {
      console.log('set', key, value)
      target[key] = value;
      return true;
    }
  });
  
  function refresh() {
    document.body.innerHTML = obj.num
  }
  refresh()

现在我们要做的就是将这个refresh函数捕捉到,因为js是同步运行的,所以我们只需要使用一个全局变量记录下来当前运行的函数是哪个就可以了,然后在触发get的时候收集下来该函数。 在set的时候重新运行函数即可。

//不影响垃圾回收机会,不会因为使用到了一个对象当key导致该对象不被回收
const map = new WeakMap()

const obj = new Proxy({
    num: 0
  }, {
    get(target, key) {
        if (activeEffect) {
            !map[target] && (map[target] = new Map())
            !map[target].get(key) && map[target].set(key, new Set())
            const deps = map[target].get(key)
            deps.add(activeEffect)
        }
        return target[key];
    },
    set(target, key, value) {
        target[key] = value;    
        const deps = map[target].get(key)
        deps.forEach(fn => fn())
        return true;
    }
});
  
function refresh() {
    document.body.innerHTML = obj.num
}

let activeEffect = null;
function effect(fn) {
    function runner() {
        activeEffect = fn
        fn()
        activeEffect = null
    }
    runner()
}

effect(refresh)

window.addEventListener('click', () => {
  obj.num++
  console.log('obj.num', obj.num)
})

由此可以看出被effect包裹的函数一开始就会运行一次,进行依赖收集。

这样响应式逻辑就实现的差不多了,我们把他封装成一个hooks

let activeEffect = null;

function reactive(obj) {
    //这里就直接用闭包存一下每个对象属性的依赖了
    const map = new Map()

    return new Proxy(obj, {
        get(target, key) {
            if (activeEffect) {
                !map.get(key) && map.set(key, new Set())
                const deps = map.get(key)
                deps.add(activeEffect)
            }
            return target[key];
        },
        set(target, key, value) {
            target[key] = value;    
            const deps = map.get(key)
            deps.forEach(fn => fn())
            return true;
        }
    });
}
  
function effect(fn) {
    function runner() {
        activeEffect = fn
        fn()
        activeEffect = null
    }
    runner()
}


const obj = reactive({
    num: 0
})

function refresh() {
    document.body.innerHTML = obj.num
}

effect(refresh)

window.addEventListener('click', () => {
  obj.num++
  console.log('obj.num', obj.num)
})

我们再来具体实现以下渲染逻辑。 前文的渲染逻辑就是重新对html赋值,真实的逻辑肯定不是这么简单。

渲染逻辑就是 ---> 将Vnode渲染成真实dom 所以入参肯定就是Vnode

我们先来简单定义一个渲染器,不带组件版本的

function createRenderer() {

    function mountElement(vnode, container) {
        const el = document.createElement(vnode.type)
        //将dom挂到vnode上
        vnode.el = el
        container.appendChild(el)

        for (const key in vnode.props) {
            el.setAttribute(key, vnode.props[key])

            //以on开头的事件
            if (key.startsWith('on')) {
                el.addEventListener(key.slice(2).toLowerCase(), vnode.props[key])
            }
        }

        if  (vnode.children) {
            if (typeof vnode.children === 'string') {
                el.textContent = vnode.children
            } else {
                vnode.children.forEach(child => {
                    mountElement(child, el)
                })
            }
        }

    }

    return {
        mountElement
    }
}

const { mountElement } = createRenderer()

mountElement({ type: 'div', props: { id: 'app', onClick: () => {
    console.log('click')
} }, children: [
    { type: 'h1', props: { id: 'title' }, children: 'Hello World' },
    { type: 'button', props: { id: 'btn' }, children: 'Click me' }
] }, document.body)

这个就是挂载逻辑就是一个dfs过程

我们在实现一下更新逻辑

更新逻辑肯定就是需要对比newVnode和oldVnode,去进行更新

function createRenderer() {

    //通用方法将一个vnode挂载到container上
    function mountElement(vnode, container) {
        const el = document.createElement(vnode.type)
        //将dom挂到vnode上
        vnode.el = el
        container.appendChild(el)

        for (const key in vnode.props) {
            el.setAttribute(key, vnode.props[key])

            //以on开头的事件
            if (key.startsWith('on')) {
                el.addEventListener(key.slice(2).toLowerCase(), vnode.props[key])
            }
        }

        if  (vnode.children) {
            if (typeof vnode.children === 'string') {
                el.textContent = vnode.children
            } else {
                vnode.children.forEach(child => {
                    mountElement(child, el)
                })
            }
        }

    }

    //更新,挂载统一在patch中处理,因为n1为空也就是oldVnode为空,其实就是挂载
    //n1为旧的vnode,n2为新的vnode
    function patch(n1, n2, container) {
        if (!n1) {
            mountElement(n2, container)
        } else if (n1.type !== n2.type) {
            //对于类型不一致情况,不做复用,直接重新挂载
            const el = document.createElement(n2.type)
            n1.el.replaceWith(el)
            mountElement(n2, el.parentNode)
        } else {
            //对于类型一致情况,进行复用dom节点
            const el = (n2.el = n1.el)
   
            const oldProps = n1.props || {};
            const newProps = n2.props || {};
      
            //更新props
            for (const key in newProps) {
              const oldVal = oldProps[key];
              const newVal = newProps[key];
              if (oldVal !== newVal) {
                if (key.startsWith('on')) {
                  el.addEventListener(key.slice(2).toLowerCase(), newVal);
                } else {
                  el.setAttribute(key, newVal);
                }
              }
            }
            //删除旧的props
            for (const key in oldProps) {
              if (!(key in newProps)) {
                el.removeAttribute(key);
              }
            }

            //更新children

            const oldChildren = n1.children || [];
            const newChildren = n2.children || [];


            if (typeof newChildren === 'string') {
                //如果newChildren是字符串,直接更新textContent,并且移除掉oldChildren
                if (typeof oldChildren === 'string') {
                    if (oldChildren !== newChildren) {
                        el.textContent = newChildren
                    }
                } else {
                    el.innerHTML = ''
                    el.textContent = newChildren
                }
                return
            } 

            const oldKeyToIdx = new Map();
            oldChildren.forEach((child, idx) => {
                if (child.key != null) oldKeyToIdx.set(child.key, idx);
            });
            const newKeyToIdx = new Map();
            newChildren.forEach((child, idx) => {
                if (child.key != null) newKeyToIdx.set(child.key, idx);
            });

            newChildren.forEach(child => {
                //找到key相同的节点,进行复用更新,找不到就是null,直接挂载
                patch(oldChildren[oldKeyToIdx.get(child.key)], child, el)
            })

            //删除旧的children
            oldChildren.forEach(child => {
                if (!newKeyToIdx.has(child.key)) {
                    el.removeChild(child.el)
                }
            })
        }
    }

    return {
        patch
    }
}

const { patch } = createRenderer()

const curVnode = {
    type: 'div',
    props: {
        id: 'app',
        onClick: () => {
            console.log('click')
        },
        key: 'app'
    },
    children: [
        { type: 'h1', key: 'title', props: { id: 'title' }, children: 'Hello World', key: '1' },
        { type: 'button', key: 'btn', props: { id: 'btn' }, children: 'Click me', key: '2' }
    ]
}



patch(null, curVnode, document.body)


window.addEventListener('click', () => {
    patch(curVnode, {
        type: 'div',
        props: {
            id: 'app',
            onClick: () => {
                console.log('click')
            },
            key: 'app'
        },
        children: [
            { type: 'h1', key: 'title', props: { id: 'title' }, children: '更新', key: '1' },
        ]
    }, document.body)
})

注意这里的更新,并没有考虑newVnode和oldVnode顺序不一致的情况,为了方便大家理解,这里就先不书写这部分内容,准备留到一下编讲解

上面已经可以正常处理普通dom节点的情况了,那如何处理组件的情况呢。 我们先来看一下一个vue组件编译之后长什么样子

//编译前
<script setup lang="ts">
import { ref } from 'vue'
let count = ref(0)
</script>
<template>
    <div>
        <h1>Hello World</h1>
    </div>
</template>
<style scoped lang='scss'>

</style>
//编译后
import { createHotContext as __vite__createHotContext } from "/@vite/client";import.meta.hot = __vite__createHotContext("/src/App.vue");import { defineComponent as _defineComponent } from "/node_modules/.vite/deps/vue.js?v=992b6564";
import { ref } from "/node_modules/.vite/deps/vue.js?v=992b6564";
//
const _sfc_main = /* @__PURE__ */ _defineComponent({
  __name: "App",
  setup(__props, { expose: __expose }) {
    __expose();
    let count = ref(0);
    const __returned__ = { get count() {
      return count;
    }, set count(v) {
      count = v;
    } };
    Object.defineProperty(__returned__, "__isScriptSetup", { enumerable: false, value: true });
    return __returned__;
  }
});
import { createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "/node_modules/.vite/deps/vue.js?v=992b6564";
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  return _openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
    _createElementVNode(
      "h1",
      null,
      "Hello World",
      -1
      /* HOISTED */
    )
  ]));
}
export default /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render], ["__file", "D:/\u5FAE\u524D\u7AEF\u9879\u76EE/\u4F4E\u4EE3\u7801\u6D77\u62A5\u9879\u76EE/low-code/src/App.vue"]]);

编译之后的代码很多很复杂,但是其实只需要关注最后一行export default那块就可以,给他简化一下

export default {
    render() {
          return 虚拟dom
    },
    setup() {
        return {
          count
        }
    }
}

可以看出,编译之后,有两个函数,render用来返回Vnode,一个用来将内部定义的响应式变量返回。

我们写一个h函数方便我们创建Vnode

function h(type, props = {}, children = null) {
  return { type, props, children, el: null, key: props.key };
}

然后我们再来看一下如何创建带有组件的Vnode

//组件
const MyComponent = {
  setup() {
    const state = reactive({ count: 0 });

    onMounted(() => {
      console.log('Component mounted!');
    });

    return {
      state
    };
  },
  render(ctx) {
    return h('div', {}, [
      h('p', { key: 'text' }, 'Count: ' + ctx.state.count),
      h('button', {
        key: 'btn',
        onClick: () => {
          ctx.state.count++;
          console.log('state.count', ctx.state.count);
        }
      }, 'Increment')
    ]);
  }
};
//这个APP也是一个组件,只是没有setup函数
const App = {
  render() {
    return h('div', {}, [h(MyComponent)]);
  }
};

所以对如组件type其实是组件导出的对象 所以我们只需要在挂载的时候单独判断一下,如果type不是字符串的化,我们要挂在的就是组件

来实现一下挂载组件的方法


//设置一下当前正在操作哪个组件,这样生命周期钩子函数onMount(fn)的fn的就知道要放到哪个组件实例上了
    let currentInstance = null;
    function setCurrentInstance(instance) {
      currentInstance = instance;
    }

  function mountComponent(vnode, container) {
  //组件实例
    const instance = {
    //组件的虚拟dom
      vnode,
    //组件是否挂载
      isMounted: false,
   //setup函数返回的响应式变量
      setupState: null,
   //组件的render函数,用于返回vnode的
      render: null,
   //组件的子树
      subTree: null,
   //组件的mounted钩子函数
      mounted: []
    };

    const component = vnode.type;

    setCurrentInstance(instance);
    const setupResult = component.setup?.(vnode.props);
    setCurrentInstance(null);

    instance.setupState = setupResult || {};

    instance.render = () => component.render(instance.setupState);
    
    function update() {
      //没有挂载
      if (!instance.isMounted) {
        const subTree = instance.render();
        patch(null, subTree, container);
        instance.subTree = subTree;
        instance.isMounted = true;
        instance.mounted.forEach(fn => fn());
      } else {
      //挂载了就是更新,重新运行一遍render函数产生newSubTree
        const newSubTree = instance.render();
        patch(instance.subTree, newSubTree, container);
        instance.subTree = newSubTree;
      }
    }
    
    update();
 }

这样挂载就实现了,那么如果如和实现响应式变量变化,组件也跟着更新呢,还记得上文得effect函数,将一个函数放入到effect函数中,这个函数就能被响应式变量收集到,进而在更改响应式变量值的时候重新运行。

effect(() => {
    function update() {
      //没有挂载
      if (!instance.isMounted) {
      //注意:-----》 是在这个render中读取的响应式变量得值
        const subTree = instance.render();
        patch(null, subTree, container);
        instance.subTree = subTree;
        instance.isMounted = true;
        instance.mounted.forEach(fn => fn());
      } else {
      //挂载了就是更新,重新运行一遍render函数产生newSubTree
        const newSubTree = instance.render();
        patch(instance.subTree, newSubTree, container);
        instance.subTree = newSubTree;
      }
    }
    
    update();
})

这样以后在更改变量得值,就会重新update函数进而刷新组件状态。

到这里就大致已经可以运行了

成品

//vue.js
let activeEffect = null;

function reactive(obj) {
    const map = new Map();

    return new Proxy(obj, {
        get(target, key) {
            if (activeEffect) {
                if (!map.get(key)) {
                    map.set(key, new Set());
                }
                map.get(key).add(activeEffect);
            }
            return target[key];
        },
        set(target, key, value) {
            target[key] = value;
            const deps = map.get(key);
            if (deps) {
                deps.forEach(fn => fn());
            }
            return true;
        }
    });
}

function effect(fn) {
    function runner() {
        activeEffect = fn;
        fn();
        activeEffect = null;
    }
    runner();
}

function h(type, props = {}, children = null) {
    return { type, props, children, el: null, key: props.key };
}

let currentInstance = null;
function setCurrentInstance(instance) {
    currentInstance = instance;
}

function createRenderer() {
    function mountElement(vnode, container) {
        const el = document.createElement(vnode.type);
        vnode.el = el;
        container.appendChild(el);

        for (const key in vnode.props) {
            if (key.startsWith('on')) {
                el.addEventListener(key.slice(2).toLowerCase(), vnode.props[key]);
            } else {
                el.setAttribute(key, vnode.props[key]);
            }
        }

        if (vnode.children) {
            if (typeof vnode.children === 'string') {
                el.textContent = vnode.children;
            } else {
                vnode.children.forEach(child => {
                    mountElement(child, el);
                });
            }
        }
    }

    function mountComponent(vnode, container) {
        const instance = {
            vnode,
            isMounted: false,
            setupState: null,
            render: null,
            subTree: null,
            mounted: [],
            container
        };

        const component = vnode.type;

        setCurrentInstance(instance);
        const setupResult = component.setup?.(vnode.props);
        setCurrentInstance(null);

        instance.setupState = setupResult || {};
        instance.render = () => component.render(instance.setupState);

        effect(() => {
            function update() {
                if (!instance.isMounted) {
                    const subTree = instance.render();
                    patch(null, subTree, container);
                    instance.subTree = subTree;
                    instance.isMounted = true;
                } else {
                    const newSubTree = instance.render();
                    patch(instance.subTree, newSubTree, container);
                    instance.subTree = newSubTree;
                }
            }
            update();
        });
    }

    function patch(n1, n2, container) {
        if (!n1) {
            if (typeof n2.type === 'string') {
                mountElement(n2, container);
            } else {
                mountComponent(n2, container);
            }
        } else if (n1.type !== n2.type) {
            const el = document.createElement(n2.type);
            n1.el.replaceWith(el);
            mountElement(n2, el.parentNode);
        } else {
            const el = (n2.el = n1.el);
            const oldProps = n1.props || {};
            const newProps = n2.props || {};

            for (const key in newProps) {
                const oldVal = oldProps[key];
                const newVal = newProps[key];
                if (oldVal !== newVal) {
                    if (key.startsWith('on')) {
                        el.removeEventListener(key.slice(2).toLowerCase(), oldVal);
                        el.addEventListener(key.slice(2).toLowerCase(), newVal);
                    } else {
                        el.setAttribute(key, newVal);
                    }
                }
            }

            for (const key in oldProps) {
                if (!(key in newProps)) {
                    el.removeAttribute(key);
                }
            }

            const oldChildren = n1.children || [];
            const newChildren = n2.children || [];

            if (typeof newChildren === 'string') {
                if (typeof oldChildren === 'string') {
                    if (oldChildren !== newChildren) {
                        el.textContent = newChildren;
                    }
                } else {
                    el.innerHTML = '';
                    el.textContent = newChildren;
                }
                return;
            }

            const oldKeyToIdx = new Map();
            oldChildren.forEach((child, idx) => {
                if (child.key != null) oldKeyToIdx.set(child.key, idx);
            });

            const newKeyToIdx = new Map();
            newChildren.forEach((child, idx) => {
                if (child.key != null) newKeyToIdx.set(child.key, idx);
            });

            newChildren.forEach(child => {
                patch(oldChildren[oldKeyToIdx.get(child.key)], child, el);
            });

            oldChildren.forEach(child => {
                if (!newKeyToIdx.has(child.key)) {
                    el.removeChild(child.el);
                }
            });
        }
    }

    return {
        patch
    };
}

const { patch } = createRenderer();

const App = {
    setup() {
        return reactive({
            count: 0
        });
    },
    render(ctx) {
        return h('div', {
            onClick: () => ctx.count++
        }, ctx.count + '');
    }
};

patch(null, h(App), document.body);

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    //js文件
    <script src="./vue.js" defer></script>
</head>
<body>
    <div id="app">
    </div>
</body>

<script>    

</script>
</html>

下一章更新

  • 集成最长上升子序列(以动图得方法讲解最长上升子序列得diff算法)
  • 实现createApp函数
  • 后面应该还会把vue3.5的链表响应式更新一下