手写mini-vue之重新理解

introduce

核心模块(core modules)

Reactivity Module
    有响应式对象发生改变的时候会重新调用render函数
Compiler Module 获取HTML模板并编译成渲染函数(render function)
Renderer Module
    -Render Phase(渲染阶段)
        调用render函数生成Vnode
    -Mount Phase
        调用VNode挂载到网页
    -Patch Phase(补丁阶段也就是更新阶段)
        将所有新旧VNode进行比较并更新到网页
  • Renderer Module

渲染函数返回虚拟dom

template或者HTML节点->render()->Vnode->Element->mount->RealElement
import {h} from 'vue'
function h(tag,props,children)
{
    return {
        tag,
        props,
        children
    }
}
function render()
{
    return h(elementTypeName,{
        elementProps
    },elementChild)
    //比如
    return h('div',{
        id:"#app",
        onClick:()=>{this.count++}
    },h('span','hello world'))// <div id="app"><span>hello world</span></div>
}
//如何处理类似于onClick的事件?
//在执行mount函数遍历props属性的时候执行
for(const key in vnode.props)
{
    const value=vnode.props[key];
    if(key.startWith('on'))
    {
        //addEventListener
        el.addEventListener(key.slice(2).toLowerCase(),value);
    }
    else
    {
        el.setAttribute(key,value);
    }
}

Rendering Mechanism(渲染原理)

core code(核心代码)

import { h } from 'vue'
function render()
{
    return h(elementTypeName,{
        elementProps
    },elemntChild)
}
  • 举个例子
import { h } from 'vue'
function render()
{
    return h('div',{
        id:"#app",
        onClick:clicked
    },'hello world')
}

How to Use Render Function

如何使用类似于V-if或者v-for这些指令

import {h} from 'vue'
const App={
    render(){
        //v-if='ok' 使用三元表达式
        return this.ok?
        h('div',"i am the ok content"):
        h('span','i am the ont ok content');
    }
    //如何使用v-for呢?
    render()
    {
        //v-for='item in list'
        return this.list.map(item=>{
            return h('span',{
                key:item.id
            },item.text)
        })
    }
}

如何处理slot

const App={
    render(){
        //拿到默认插槽的虚拟DOM
        const slot:Array<VNode>=this.$slot.default();
        return slot.map(child=>{
            return h('div',child)
        })
    }
}
//比如我做一个嵌套的左侧导航栏
<Wrap>
<div class='margin-4'>
    <div>h1</div>
    <div>h1</div>
    <div>h1</div>
</div>
<Wrap>
<div class='margin-4'>
    <div>h2</div>
    <div>h2</div>
    <div>h2</div>
</div>
</Wrap>
<Wrap>

Compiler And Renderer API

使用了缓存的render可以不用再patch的时候做重新render

//可以缓存一个html元素的所有东西,props或者监听事件等等
//这个缓存类似于react的useMemo useCallback
//不同的是Vue已经帮你做了这个工作
import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"

const _hoisted_1 = /*#__PURE__*/_createVNode("div", {
  id: "app",
  onclick: "test"
}, "Hello World!", -1 /* HOISTED */)

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock("div", null, [
    _hoisted_1
  ]))
}

compiler做的工作

//比如下面的一个DOM
<div>
<section></section>
<div></div>
<span>{{msg}}</span>
</div>
//当然,人一眼就看出来只有span才是有可能变化的东西,但是机器不知道
//如果compiler不告诉机器只有span可能会变化,机器会遍历整个DOM树
return (_openBlock(), _createBlock("div", null, [
    _hoisted_1,
    _createVNode("span", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
]))
//_openBlock这个函数执行之后,会生成一个动态子节点添加到根节点上去
//现在的根节点就只有动态子节点了

Create A Mount Function

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app"></div>
</body>
<script>
function h(tag,props,children)
{
    return {
        tag,
        props,
        children
    }
}
function mount(vnode,container)
{
    const {tag,props,children}=vnode;
    //将真实的DOM元素存储到vnode上去,方便等会patch的时候使用
    const element=vnode.el=document.createElement(tag);
    if(props)
    {
        for(const key in props)
        {
            const value=props[key];
            if(key.startsWith('on'))
            {
                //addEventListener
                element.addEventListener(key.slice(2).toLowerCase(),value);
            }
            else
            {
                element.setAttribute(key,value);
            }
        }
    }
    //children->可以字符串和 数组类型的children
    if(typeof children ==="string")
    {
        const textNode=document.createTextNode(children);
        element.append(textNode);
    }
    //children是一个数组
    else if(Array.isArray(children))
    {
        children.forEach(vnode=>{
            //在当前的element容器中递归调用‘
            //并且挂载到当前的容器上去
            mount(vnode,element);
        })
    }
    container.append(element);
}
const vdom=h('div',{
    class:"red"
},[
    h('span',null,'hello')
])
mount(vdom,document.querySelector('#app'))
</script>
</html>

How To Patch(更新虚拟DOM)

//v1->old vnode  n2->new vnode
const vdom2=h('div',{
    class:"green"
},[
    h('span',null,'changed')
])
function patch(n1,n2)
{
    //同样的给新节点也赋一个这样的属性,以便新节点在变成旧节点的时候使用
    const el=n2.el=n1.el//之前存储的就节点派上用场
    //相同tag
    if(n1.tag===n2.tag)
    {
        //props
        const oldProps=n1.props || {};
        const newProps=n2.props || {};
        for(const key in oldProps)
        {
            const oldValue=oldProps[key];
            const newValue=newProps[key];
            if(oldValue!=newValue)
            {
                el.setAttribute(key,newValue)
            }
        }
        //旧节点有,新节点没有
        for(const key in oldProps)
        {
            if(!(key in newProps))
            {
                el.removeAttribute(key);
            }
        }
        //children
        //1.newChildren-> string  (old:string   old:Array)
        //2.newChildren-> array   (old:string   old:array)
        const oldChildren=n1.children || [];
        const newChildren=n22.children || [];
        if(typeof newChildren=='string')
        {
            if(typeof oldChildren=='string')
            {
                if(newChildren!=oldChildren)
                {
                    el.textContent=newChildren
                }
            }
            else if(Array.isArray(oldChildren))
            {
                el.textContent=newChildren
            }
        }
        else if(Array.isArray(newChildren))
        {
            if(typeof oldChildren=='string')
            {
                el.innerHTML='';
                newChildren.forEach(child=>{
                    mount(child.el);
                })
            }
            else
            {
                //both children are array
                const length=Math.min(oldChildren.length,newChildren.length);
                for(let i=0;i<length;i++)
                {
                    patch(oldChildren[i],newChildren[i]);
                }
                if(newChildren.length>length)
                {
                    //add new element
                    newChildren.slice(length).forEach(addChild=>{
                        mount(addChild,el);
                    })
                }
                if(oldChildren.length>length)
                {
                    //remove old element
                    for(let index=length;index<oldChildren.length;index++)
                    {
                        const oldVnode=oldChildren[index];
                        oldVnode.el.parent.removeChild(oldVnode.el);
                    }

                }
            }
        }
    }
    else
    {
        //replace
        n1.el.replaceWith(document.createElement(n2.tag));
    }
}
patch(vdom,vdom2)//这样就直接使用啦

Reactivity(响应式的实现)

let update,state
const onStateChanged=()=>{
    view=render(state)
}
const setState=newState=>{
    state=newState;
    update();
}

发布订阅的实现

let activeEffect;
//为了在调用depend()的时候把相关函数作为订阅者
//添加到subscribers中去,以后notify的时候就可以用了
class Dep{
    constructor(value)
    {
        this.subscribers=new Set();///所有的订阅者
        this._value=value;
    }
    get value()
    {
        this.depend();
        return this._value
    }
    set value(newVal)
    {
        this._value=newVal;
        this.notify();
    }
    depend()
    {
        if(activeEffect)
        {
            this.subscribers.add(activeEffect)
        }
    }
    notify(){
        //通知订阅者
        this.subscribers.forEach(effect=>{
            effect();
        })
    }
}
function watchEffect(effect)
{
    activeEffect=effect;
    effect();
    activeEffect=null;
}
const dep = new Dep('primaryValue');
//如何将一个dep与一个值相关联
//在Dep的构造函数中加一个value属性,通知的时候使用value就好了
watchEffect(()=>{
    console.log(dep.value)
    //自动调用dep.depned()
})
dep.value='changed'//自动调用dep.notify()

ref的实现

function ref(target)
{
    let value=target;
    let dep=new Dep();
    const obj={
        get value()
        {
            dep.depend();
            return value;
        },
        set value(newVal)
        {   
            value=newVal;
            dep.notify();
        }
    }
    return obj
}

reactive的实现

let activeEffect;

class Dep{
    constructor(value)
    {
        this.subscribers=new Set();///所有的订阅者
        //在实现reactive的时候,Dep不需要维护自己的value
        //value来自reactive中的参数
    }
    depend()
    {
        if(activeEffect)
        {
            this.subscribers.add(activeEffect)
        }
    }
    notify(){
        //通知订阅者
        this.subscribers.forEach(effect=>{
            effect();
        })
    }
}
function watchEffect(effect)
{
    activeEffect=effect;
    effect();
    activeEffect=null;
}
const targetMap=new WeakMap();
//weakmap只能用对象作为键
function getDep(target,key)
{
    let depsMap=targetMap.get(target);
    //depsMap包含了所有与对象相关的dep
    if(!depsMap)
    {
        depsMap=new Map();
        targetMap.set(target,depsMap);
    }
    let dep=depsMap.get(key)
    if(!dep)
    {
        dep=new Dep();
        depsMap.set(key,dep);
    }
    return dep;
}
const reactiveHandler={
    get(target,key)
    {
        //难点在于怎么找到dep
        let dep=getDep(target,key);
        dep.depend();
        return Reflect.get(target,key);
    },
    set(target,key,value,receiver)
    {
        const res=Reflect.set(target,key,value);
        let dep=getDep(target,key);
        dep.notify();
        //必须返回一个布尔值来指示操作是否成功
        return res
    }
}
function reactive(obj)
{
    //使用object.defineProperty的缺点
    //只能对现有的属性进行响应式监听
    //添加的新属性无法做响应式
    //可以监听到数组的方法
    return new Proxy(obj,reactiveHandler)
}
const state=reactive({
    count:0
})
watchEffect(()=>{
    console.log(state.count)
})
state.count++;

测试代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .red{
            color: red;
            font-size: 66px;
            cursor: pointer;
        }
        .green{
            color: green;
        }
    </style>
</head>
<body>
    <div id="app"></div>
</body>
<script>
function h(tag,props,children)
{
    return {
        tag,
        props,
        children
    }
}
function mount(vnode,container)
{
    const {tag,props,children}=vnode;
    const element=vnode.el=document.createElement(tag);
    if(props)
    {
        for(const key in props)
        {
            const value=props[key];
            if(key.startsWith('on'))
            {
                //addEventListener
                element.addEventListener(key.slice(2).toLowerCase(),value);
            }
            else
            {
                element.setAttribute(key,value);
            }
        }
    }
    //children->可以字符串和 数组类型的children
    if(typeof children ==="string")
    {
        const textNode=document.createTextNode(children);
        element.append(textNode);
    }
    //children是一个数组
    else if(Array.isArray(children))
    {
        children.forEach(vnode=>{
            //在当前的element容器中递归调用‘
            //并且挂载到当前的容器上去
            mount(vnode,element);
        })
    }
    container.append(element);
}
function patch(n1,n2)
{
    //同样的给新节点也赋一个这样的属性,以便新节点在变成旧节点的时候使用
    const el=n2.el=n1.el//之前存储的就节点派上用场
    //相同tag
    if(n1.tag===n2.tag)
    {
        //props
        const oldProps=n1.props || {};
        const newProps=n2.props || {};
       
        for(const key in oldProps)
        {
            const oldValue=oldProps[key];
            const newValue=newProps[key];
            if(oldValue!=newValue)
            {
                el.setAttribute(key,newValue)
            }
        }
        
        //旧节点有,新节点没有
        
        for(const key in oldProps)
        {
            if(!(key in newProps))
            {
                el.removeAttribute(key);
            }
        }
        
        //children
        //1.newChildren-> string  (old:string   old:Array)
        //2.newChildren-> array   (old:string   old:array)
        const oldChildren=n1.children || [];
        const newChildren=n2.children || [];
        if(typeof newChildren=='string')
        {
            if(typeof oldChildren=='string')
            {
                if(newChildren!=oldChildren)
                {
                    el.textContent=newChildren
                }
            }
            else if(Array.isArray(oldChildren))
            {
                el.textContent=newChildren
            }
        }
        else if(Array.isArray(newChildren))
        {
            if(typeof oldChildren=='string')
            {
                el.innerHTML='';
                newChildren.forEach(child=>{
                    mount(child,el);
                })
            }
            else
            {
                //both children are array
                const length=Math.min(oldChildren.length,newChildren.length);
                for(let i=0;i<length;i++)
                {
                    patch(oldChildren[i],newChildren[i]);
                }
                if(newChildren.length>length)
                {
                    //add new element
                    newChildren.slice(length).forEach(addChild=>{
                        mount(addChild,el);
                    })
                }
                if(oldChildren.length>length)
                {
                    //remove old element
                    for(let index=length;index<oldChildren.length;index++)
                    {
                        const oldVnode=oldChildren[index];
                        oldVnode.el.parent.removeChild(oldVnode.el);
                    }

                }
            }
        }
    }
    else
    {
        //replace
        n1.el.replaceWith(document.createElement(n2.tag));
    }
}
let activeEffect;

class Dep{
    constructor(value)
    {
        this.subscribers=new Set();///所有的订阅者
        //在实现reactive的时候,Dep不需要维护自己的value
        //value来自reactive中的参数
    }
    depend()
    {
        if(activeEffect)
        {
            this.subscribers.add(activeEffect)
        }
    }
    notify(){
        //通知订阅者
        this.subscribers.forEach(effect=>{
            effect();
        })
    }
}
function watchEffect(effect)
{
    activeEffect=effect;
    effect();
    activeEffect=null;
}
const targetMap=new WeakMap();
//weakmap只能用对象作为键
function getDep(target,key)
{
    let depsMap=targetMap.get(target);
    //depsMap包含了所有与对象相关的dep
    if(!depsMap)
    {
        depsMap=new Map();
        targetMap.set(target,depsMap);
    }
    let dep=depsMap.get(key)
    if(!dep)
    {
        dep=new Dep();
        depsMap.set(key,dep);
    }
    return dep;
}
const reactiveHandler={
    get(target,key)
    {
        //难点在于怎么找到dep
        let dep=getDep(target,key);
        dep.depend();
        return Reflect.get(target,key);
    },
    set(target,key,value,receiver)
    {
        const res=Reflect.set(target,key,value);
        let dep=getDep(target,key);
        dep.notify();
        //必须返回一个布尔值来指示操作是否成功
        return res
    }
}
function reactive(obj)
{
    //使用object.defineProperty的缺点
    //只能对现有的属性进行响应式监听
    //添加的新属性无法做响应式
    //可以监听到数组的方法
    return new Proxy(obj,reactiveHandler)
}
function ref(target)
{
    let value=target;
    // let keyMap=new Map();
    // keyMap.set(value,new Dep())
    // let dep=keyMap.get(value)
    let dep=new Dep();
    const obj={
        get value()
        {
            // let dep=getDep(obj,'value');
            dep.depend();
            return value;
        },
        set value(newVal)
        {   
            // let dep=getDep(obj,'value');
            value=newVal;
            dep.notify();
        }
    }
    return obj
}
const App={

    data:{count:ref(0),
    test:ref(1)},
    render(){
        return h('div',{
            class:'red',
            onClick:()=>{
                this.data.count.value++;
                this.data.test.value++
            }
        },String(`${this.data.count.value}--${this.data.test.value}`))
    }
}
function mountApp(component,container)
{
    let isMounted=false;
    let preVdom;
    watchEffect(()=>{
        if(!isMounted)
         {
             preVdom=component.render();
             mount(preVdom,container);
             isMounted=true;
         }   
         else
         {
             let newVdom=component.render();
             patch(preVdom,newVdom);
             preVdom=newVdom;
         }
    
    })
}
mountApp(App,document.querySelector("#app"))
</script>
</html>

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值