从零手写Vue3核心原理

一.虚拟DOM转真实DOM

let { render } = Vue
const state = { count: 0 };
const vnode = {
    tag: 'div',
    props: {
        style: { color: 'red' }
    },
    children: [{
        tag: 'p',
        props: null,
        children: `vue@3- 计数器 ${state.count}`
    }, {
        tag: 'button',
        props: {
            onClick: () => alert(state.count)
        },
        children: '点我啊'
    }]
}
render(vnode, app);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

先创建一个虚拟节点对象,调用render方法将虚拟节点转化为真实节点。

实现DOM操作方法

export const nodeOps = {
    insert: (child, parent, anchor) => {
        if (anchor) {
            parent.insertBefore(child, anchor);
        } else {
            parent.appendChild(child);
        }
    },
    remove: (child) => {
        const parent = child.parentNode;
        if (parent) {
            parent.removeChild(child);
        }
    },
    createElement: (tag) => document.createElement(tag),
    setElementText: (el, text) => el.textContent = text
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

将虚拟节点转化为真实节点

import { nodeOps } from './runtime-dom';
export function render(vnode, container) {
    // 渲染分为两种 一种是初始化 一种是是diff
    patch(null, vnode, container);
}
function patch(n1, n2, container) {
    if (typeof n2.tag == 'string') {
        // 将虚拟节点挂载到对应的容器中
        mountElement(n2, container);
    }
}
function mountElement(vnode, container) {
    const { tag, props, children } = vnode;
    let el = nodeOps.createElement(tag);
    if (typeof children === 'string') {
        nodeOps.setElementText(el, children);
    } else if (Array.isArray(children)) {
        mountChildren(children, el);
    }
    nodeOps.insert(el, container, null);
}
// 循环挂载子元素
function mountChildren(children, container) {
    for (let i = 0; i < children.length; i++) {
        const child = children[i];
        patch(null, child, container);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

处理DOM中的属性

const onRe = /^on[^a-z]/;
export const nodeOps = {
    // ...
    hostPatchProps: (el, key, value) => {
        const isOn = key => onRe.test(key);
        if (isOn(key)) { // 事件添加
            const name = key.slice(2).toLowerCase();
            el.addEventListener(name, value);
        } else {
            if (key === 'style') { // 样式处理
                for (let key in value) {
                    el.style[key] = value[key];
                }
            } else {
                el.setAttribute(key, value);
            }
        }
    }
}
function mountElement(vnode, container) {
    const { tag, props, children } = vnode;
    let el = (vnode.el = nodeOps.createElement(tag));
    if (props) {
        // 循环所有属性添加属性
        for (let key in props) {
            nodeOps.hostPatchProps(el, key, props[key]);
        }
    }
    if (typeof children === 'string') {
        nodeOps.setElementText(el, children);
    } else if (Array.isArray(children)) {
        mountChildren(children, el);
    }
    nodeOps.insert(el, container, null);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

二. Vue3.0组件的实现

const MyComponent = {
    setup() {
        return () => ({
            tag: 'div',
            props: { style: { color: 'blue' } },
            children: '我是一个组件' + state.count
        })
    }
}
1
2
3
4
5
6
7
8
9

Vue3.x中组件拥有setup方法,当组件渲染前会先调用此方法

const vnode = {
    tag: 'div',
    props: {
        style: { color: 'red' }
    },
    children: [{
            tag: 'p',
            props: null,
            children: `vue@3- 计数器 ${state.count}`
        }, {
            tag: 'button',
            props: {
                onClick: () => alert(state.count)
            },
            children: '点我啊'
        },
        { tag: MyComponent, props: null, children: '' },
        { tag: MyComponent, props: null, children: '' }
    ]
}
render(vnode, app);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

将组件同时传入children属性中。内部根据tag类型做不同的初始化操作

function patch(n1, n2, container) {
    if (typeof n2.tag == 'string') {
        // 将虚拟节点挂载到对应的容器中
        mountElement(n2, container);
    }else if (typeof n2.tag === 'object') {
        // 组件的挂载
        mountComponent(n2, container);
    }
}
function mountComponent(vnode, container) {
    const instance = { // 创建元素实例
        vnode,
        tag: vnode.tag,
        render: null, // setup返回的结果
        subTree: null, // 子元素
    }
    const Component = instance.tag;
    instance.render = Component.setup(); // 调用setUp方法
    instance.subTree = instance.render && instance.render();
    patch(null, instance.subTree, container); // 将子树挂载在元素上
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

三.Vue3.0响应式原理

effect(() => {
    const vnode = {
        tag: 'div',
        props: {
            style: { color: 'red' }
        },
        children: [{
                tag: 'p',
                props: null,
                children: `vue@3- 计数器` + state.count
            }, {
                tag: 'button',
                props: {
                    onClick: () => state.count++
                },
                children: '点我啊'
            },
            { tag: MyComponent, props: null, children: '' },
            { tag: MyComponent, props: null, children: '' }
        ]
    }
    render(vnode, app);
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

注入副作用函数,当数据变化时可以自动重新执行。

let activeEffect;
export function effect(fn) {
    activeEffect = fn;
    fn();
}
export function reactive(target) {
    return new Proxy(target, {
        set(target, key, value, receiver) {
            const res = Reflect.set(target, key, value, receiver);
            activeEffect();
            return res;
        },
        get(target, key, receiver) {
            const res = Reflect.get(target, key, receiver)
            return res;
        }
    })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

通过proxy代理数据当数据更新时,重新执行effect函数。

依赖收集原理

let activeEffect;
export function effect(fn) {
    activeEffect = fn;
    fn();
    activeEffect = null; // 当前effect置为空
}
const targetMap = new WeakMap();
function track(target,key){ // 依赖收集
    let depsMap = targetMap.get(target);
    if(!depsMap){ // 属性对应依赖关系
        targetMap.set(target,(depsMap = new Map()));
    }
    let deps = depsMap.get(key);
    if(!deps){ // 设置set 存放effect
        depsMap.set(key,(deps=new Set()));
    }
    if(activeEffect && !deps.has(activeEffect)){
        deps.add(activeEffect);
    }
}
function trigger(target,key,val){
    const depsMap = targetMap.get(target);
    if(!depsMap){
        return;
    }
    const effects = depsMap.get(key);
    effects && effects.forEach(effect =>effect());
}

export function reactive(target) {
    return new Proxy(target, {
        set(target, key, value, receiver) {
            const res = Reflect.set(target, key, value, receiver);
            trigger(target,key,value); // 触发更新
            return res;
        },
        get(target, key, receiver) {
            const res = Reflect.get(target, key, receiver);
            track(target,key); // 收集依赖
            return res;
        }
    })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

四. 组件级更新

function mountComponent(vnode, container) {
    const instance = {
        vnode,
        tag: vnode.tag,
        render: null, // setup返回的结果
        subTree: null, // 子元素
    }
    const Component = instance.tag;
    instance.render = Component.setup(); // 调用setUp方法
    effect(()=>{ 
        // 给组件添加effect,组件中的属性变化时只执行对应方法
        instance.subTree = instance.render && instance.render();
        patch(null, instance.subTree, container); // 将子树挂载在元素上
    })
}

const MyComponent = {
    setup() {
        return () => ({
            tag: 'div',
            props: { style: { color: 'blue' } },
            children: [
                {
                    tag: 'h3',
                    children: '姓名:' + state.name
                },
                {
                    tag: 'button',
                    children: '更新',
                    props: {
                        onClick:()=>state.name = 'jw'
                    }
                }
            ]
        })
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

只更新组件对应区域

五.Vue3.0diff算法