【vue3】自己动手实现一个mini-vue(上)

1,168 阅读4分钟

响应式(reactivity)的实现 -> vue3的ref,reactive和watchEffect

  • 从简单版开始(监控一个简单变量的变化->ref) 预期目标:通过watchEffect注册变量值改变后的回调函数,当变量值改变时触发。举个简单例子,下面的代码输出10 20
let a = ref(10);
watchEffect(() => {
    console.log(a.value);
})
a.value = 20;//输出10 20

具体实现:

  1. 创建类Dep,一个变量对应一个Dep。监控的值保存在其value属性上,回调函数保存在其effects属性上,然后使用getter,setter进行拦截;
  2. 调用watchEffet时将依赖保存在一个全局变量上(以便在类中调用)
  3. getter时收集依赖,setter时触发依赖
let currentEffect = null;
class Dep {
    constructor(val) {
        this._val = val;//_val用value也可
        this.effects = new Set();//存放所有的依赖
    }

    //监视的值放到value变量上,然后监视value的值的变化
    get value() {
        this.depend();
        return this._val;
    }

    set value(newVal) {
        this._val = newVal;
        this.notify();
    }

    depend() {
        //收集依赖
        if (currentEffect) {
            this.effects.add(currentEffect);
        }
    }

    notify() {
        //触发依赖
        this.effects.forEach((effect) => {
            effect();
        })
    }
}

function watchEffect(effect) {
    currentEffect = effect;
    effect();
    currentEffect = null;
}

let dep = new Dep(10);
watchEffect(() => {
    console.log(dep.value);
})
dep.value = 20;//输出10 20

后面的事就简单了:

function ref(value) {
    let dep = new Dep(value);
    return dep;
}
let a = ref(10);
watchEffect(() => {
    console.log(a.value);
})
a.value = 20;//输出10 20
  • 进阶版,如果需要监视一个对象内的属性值的改变呢?(->reactive) 预期目标:输出xiaohong xiaohei
const user = {
    name: "xiaohong",
    age: 18,
};
const userState = reactive(user);

watchEffect(() => {
    console.log(userState.name);
});

userState.name = "xiaohei";

实际上,简单想法就是:对象中的每一个属性对应一个dep(先不考虑深层监控),同时为每一个属性设置getter和setter进行拦截,vue2中Object.defineProperty实现,vue3中采用Proxy。

let targetsMap = new Map;//targetsMap = {target1:{key1:dep1,key2:dep2,...},target2:{},...}

vue2

function reactive(target) {
    for (let key in target) {
        let oldVal = target[key];//暂存,防止爆栈
        Object.defineProperty(target, key, {
            get() {
                const dep = getDep(target, key);
                dep.depend();
                return oldVal;
            },
            set(newVal) {
                const dep = getDep(target, key);
                oldVal = newVal;//为什么改变oldVal,就能改变target[key]???
                dep.notify();
            }
        })
    }
    return target;
}

vue3

function reactive(target) {
    return new Proxy(target, {
        get(target, key) {
            const dep = getDep(target, key);
            dep.depend();
            return Reflect.get(target, key);
        },
        set(target, key, newVal) {
            const dep = getDep(target, key);
            const result = Reflect.set(target, key, newVal);
            dep.notify();
            return result;
        }
    })
}

getDep实际上就是从targetsMap对应target的对应key的dep:

function getDep(target, key) {
    let depsMap = targetsMap.get(target);
    // 不能取出来,是因为我们之前都没有存过
    if (!depsMap) {
        // 存一下呗
        depsMap = new Map();
        targetsMap.set(target, depsMap);
    }
    // dep
    //只存一次,之后从targetsMap中取!!!
    let dep = depsMap.get(key);
    if (!dep) {
        dep = new Dep();
        depsMap.set(key, dep);
    }

    return dep;
}

最后,删除Dep类中的_val和gettter,setter

整体流程

(简单版-没有实现虚拟dom)

入口文件index.js

//import { createApp } from "vue";
//改为从自己写的文件中导入createApp
import { createApp } from "./my/index.js";;
import App from "./App.js";

createApp(App).mount("#app");

主要做的就是通过,然后挂(mount)到根节点上

./App.js:

import { reactive } from "./my/reactivity/index.js";
export default {
    //渲染
    render(context) {
    	const div = document.createElement("div");
        div.innerText = context.state.count;
        return div;
    },
    //数据
    setup() {
        const state = reactive({
            count: 1,
        });
        window.state = state;
        // count -> change
        return {
            state,
        };
    },
};

./my/reactivity/index.js:

import { watchEffect } from "./reactivity/index.js";

export function createApp(rootComponent) {
    return {
        mount(rootSelector) {
            let rootContainer = document.querySelector(rootSelector);
            const setupResult = rootComponent.setup();

            watchEffect(() => {
                let element = rootComponent.render(setupResult);
                rootContainer.innerHTML = ``;
                rootContainer.append(element);
            })
    	},
    };
}

虚拟DOM

(进阶版-实现虚拟dom)

首先,简单介绍虚拟DOM的概念,虚拟DOM实际上就是描述节点的对象:

export function h(type, props, children) {
  return {
    type,
    props,
    children,
  };
}

比如说有如下DOM结构:

<div class="test" id="test"></div>

转换为虚拟DOM就是:

{
    type:"div",
    props:{class:"test",id="test"},
    //children为数组或字符串类型
    children:[]
}

将虚拟DOM转为真实DOM

function createElement(type) {
    return document.createElement(type);
}

function patchProps(el, key, preValue, nextValue) {
    el.setAttribute(key, nextValue);
}

function insertElement(parent, child) {
    parent.append(child);
}

export function mountElement(vnode, container) {
    const { type, props, children } = vnode;

     const el = (vnode.el = createElement(type));//vnode.el是在下面的diff算法中会用到

    if (props) {
        for (const key in props) {
            const val = props[key];
            patchProps(el, key, null, val);
        }
    }

    if (typeof children === 'string') {
        const text = document.createTextNode(children);
        insertElement(el, text);
    } else if (Array.isArray(children)) {
        children.forEach((child) => mountElement(child, el));
    }

    insertElement(container, el);
}

diff算法

(vue3版原理)这里只实现了一个简易版的,完整版以及原理可参考 www.jb51.net/article/189…

export function diff(prev, cur) {
    if (prev.type !== cur.type) {
        prev.el.replaceWith(createElement(cur.type));
    } else {
        const el = (cur.el = prev.el)

        //处理props
        const oldProps = prev.props || {};
        const newProps = cur.props || {};

        //新的替换
        Object.keys(newProps).forEach((key) => {
            if (newProps[key] !== oldProps[key]) {
                patchProps(el, key, oldProps[key], newProps[key]);
            }
        })

        //不存在了的删除
        Object.keys(oldProps).forEach((key) => {
            if (newProps[key] !== oldProps[key]) {
                patchProps(el, key, oldProps[key], null);
            }
        })


        //处理children
        const oldChildren = prev.children || [];
        const newChildren = cur.children || [];
        if (typeof newChildren === 'string') {
            if (newChildren !== oldChildren) {
                el.textContent = newChildren;
            }
        } else {
            if (!Array.isArray(oldChildren)) {
                el.innerHTML = ``;
                newChildren.forEach((child) => {
                    mountElement(el, child);
                })
            } else {
                //两个都是数组
                //简单暴力法,源码中做了很多优化!!!
                let oldLen = oldChildren.length;
                let newLen = newChildren.length;
                let len = Math.min(oldLen, newLen);

                for (let i = 0; i < len; i++) {
                    let _1 = oldChildren[i];
                    let _2 = newChildren[i];
                    diff(_1, _2);
                }

                //老的多需要删除
                if (oldLen > len) {
                    for (let i = len + 1; i < oldLen; i++) {
                        el.removeChild(oldChildren[i].el);
                    }
                }
                //新的多需要增加
                if (newLen > len) {
                    for (let i = len + 1; i < newLen; i++) {
                        mountElement(newChildren[i].el, el);
                    }
                }
            }
        }
    }
}