19Vue-源码学习(未完成,大家不要看这个)

290 阅读8分钟

真实DOM渲染

图片.png

虚拟节点vnode 通过 diff算法 对比 要比 element div属性更有效率

以前DOM都是用HTML来编写的,但是我们使用虚拟DOM可以通过JS来操作,会更加的简单高效 图片.png

将VNode转为对应平台中 所需要的 控件,实现 跨平台 允许自己开发渲染器,将VNode渲染成你所需要的 控件等 图片.png

虚拟DOM的渲染过程

图片.png

图片.png

三大核心系统

图片.png

1.compiler:编译模板的

图片.png 将这些代码转换成 render函数

不同的开发情况有不同的转法。 我们这里交给vue-loader,然后loader依赖@vue/compiler-sfc

最后render调用后返回vnode,vnode之间会生成虚拟树,也就是虚拟DOM,最后虚拟DOM挂载后变成真实DOM,最后被渲染出来

compiler-core:存放公共核心源码 compiler-dom:对DOM操作的源码 compiler-sfc:存放@vue/compiler-sfc库的包 compiler-ssr: 服务端使用的代码

2.Runtime:运行时,渲染器模块,真正负责渲染的

图片.png Runtime是真正渲染的一个过程,主要的核心代码是交给renderer() 也就是渲染器,帮我们完成真正的渲染

他来执行render函数,拿到虚拟DOM,将虚拟DOM挂载到真实的DOM上,最终可以再界面上看到内容

runtime-core: runtime-dom: runtime-test:

3.Reactivity模块:无论是data()、setup()、还是ref()中的数据,当他们里面的数据发生改变后,重新执行某一段代码,会生成新的节点,新的节点就会和旧的节点进行对比。这个对比过程是一个diff算法,diff算法我们会在一个patch函数中执行,找到不一样的地方,然后对真实DOM进行修改。就是一个patch过程(打补丁过程)

三大核心如何协同工作?

我们一般会在渲染系统中利用响应式系统对我们的数据监听,当数据改变时,我们新的VNode和旧的VNode进行diff算法,最终进行修改。

图片.png

实现Mini-Vue

实现一个简洁版的Mini-Vue框架,(暂时跳过compiler)该Vue包括三个模块:

1.渲染系统模块(Runtime)
    runtime-->vnode-->真实dom

2.可响应式系统模块
    reactive

3.应用程序入口模块
    实现createApp(rootComponent).mount("#app")
1.渲染系统实现

1.h函数,用于返回一个VNode对象

2.mount函数,用于将VNode挂载到DOM上

3.patch函数,用于对两个VNode进行对比,决定如何处理新的VNode

注:slice(2)是将事件onClick,只取后面的click并且删除驼峰,因为addEvent中的事件是没有on的。普通的btn的是btn.onClick

renderer.js

2.//index调用h函数,我这里就得书写一个h函数,给他调用
//这里我们只处理标签,组件就不处理了
const h = (tag,props,children) => {
    //3.h函数返回的是一个VNode,就是一个js对象,就是一个 {}
    return{
        tag,
        props,
        children,
        el(就是tag传来的element)
    } 
}

4.渲染器:将数据挂载(app)
//虚拟dom和需要挂在的地方
const mount = (vnode,container)=>{
    //vnode-->element 虚拟DOM 转换成element元素
    //实际就是创建element,通过传来的tag(="div")来
    最好给我们的真实DOM也保存一个数据
    到时候上方的return里会动态返回el,我们可以不用写
    
    创建出真实的原生,并且在vnode上保留el
    const el = vnode.el = document.createElement(vnode.tag)
    
//5.处理props
    if(vnode.props){
  //props就是class="title"这种格式
        for(const key in vnode.props){
            const value = vnode.props[key]
            
            //但是props可能会传一个事件,对应一个方法
            //我们要对这种情况进行特殊处理
            if(key.startsWith("on")){
            //将on后面的字母变成小写(避免驼峰)
            //对我们事件监听的判断
                el.addEventListener
                (key.slice(2).toLowerCase(),value)
            }else{
                对el设置属性
                el.setAttribute(key,value)
            }
        }
    }

//6.处理children(只对字符串和数组操作,不考虑插槽等)
    if(vnode.children){
        if(typeof vnode.children === "string"){
            如果是字符串,直接设置到el的textContent中
            相当于原封不动的复制粘贴一下
            el.textContent = vnode.children
        }else{对数组遍历
            vnode.children.forEach(item =>{
            //如果我数组里又有一个h函数,那么我就递归
            //再次挂载到el上面
                mount(item,el);
            })
        }
    }

//7.将el挂载到container中(将div等数据添加页面上)
    container.appendChild(el);
}

10.实现patch函数,执行diff算法
    const patch = (n1,n2) =>{
        if(n1.tag !== n2.tag){
            //先拿到旧的,拿到他的父元素,删除她自己
          const n1ElParent = n1.el.parentElement;
          n1ElParent.removeChild(n1.el)
            //删除后,重新挂载新的VNode
            mount(n2,n1ElParent);
        }else{
        
        //既然n2和n1的el都一样,那当我修改el的值的时候,n1和n2的el要一起被修改
        //01:取出element对象,并且在n2中进行保存
            const el = n2.el = n1.el;
        //el就是tag中的element(div)的

        //因为我只对tag进行了判断,所以后续还需要判断props和children
        //02.0:处理props(默认为空对象)
            const oldProps = n1.props || {}
            const newProps = n2.props || {}
        //02.1获取所有的newProps添加到el
            for(const key in newProps){
                const oldValue = oldProps[key]
                const newValue = newProps[key]
                if(newValue!==oldValue){ 表示 value是不一样的,进行diff
                    if(key.startsWith("on")){//对事件监听进行判断
                        el.addEventListener(key.slice(2).toLowerCase(),newvalue)}
                        else{
                        这样就把新的Vnode的key和value重新修改
                        el.setAttribute(key,newValue)
                    }
                }
            }
        //02.2删除旧的props 
        for(const key in oldProps){
        //如果我旧的props中的key不在newProps中
            if(!(key in newProps)){
            //先删除事件和value
                if(key.startsWith("on")){
                const value = oldProps[key]
                    el.removeEventListener(key.slick(2).toLowerCase().value)
                }else{
                //然后删除key
                    el.removeAttribute(key)
                } 
            }
        }
        
            
        //03:处理children
        const oldChildren = n1.children || []
        const newChildren = n2.children || []
        
        //如果children只是一个字符串,直接输出
        if(typeof newChildren === "string"){
            if(typeof oldChilren === "string"){
                if(newChildren !== oldChildren){
                    el.textContent = newChildren
                }
            } 
            else{
                //如果oldChildren不是一个string
                但是我new是一个string啊,我直接输出new就行
                el.innerHTML = newChildren
                }
        }
        else{
            //如果newChildren不是一个String,是数组
            if(typeof oldChildren === "string"){
                el.innerHTML = "";清空数据
                newChildren.forEach(item=>{
                    遍历数组,拿到children中的VNode将他挂载
                    mount(item,el);
                })
            }
            else{
            
            //001取出old和new的中数组长度小的数组,对少的数组进行遍历,然后互相进行diff算法
            后面剩余多出来的数组直接添加(前面有相同节点的进行patch操作)
                const commonLength = 
                Math.min(oldChildren.length,
                newChildren.length)
                
                for(let i =0;i<commonLength;i++){
                    进行diff算法
                    patch(oldChilren[i],newChildren[i]);
                }
                
             //002new大于old(new比old多余的,直接挂载添加)   
                if(newChildren.length>oldChildren.length){
                    newChildren.slice(oldChildren.length).forEach(item=>{
                    mount(item,el)
                    })
                }
            }
            
            //003new小于old(把多出来的卸载掉)
            if(newChildren.length<oldChildren.length){
                oldChildren.slice(newChildren.length).forEach(item=>{
                el.removeChild(item.el)
                })
            }
        }
        
        }
    }

于是h函数里的children中,又包含h函数,被包含的h函数的children又包含h函数,慢慢的,就形成了一个VNode树结构。

index.html

<div id = "app"></div>
<script src = "./renderer.js"></script>

<script>
    //1.通过h函数来创建一个VNode
    const vnode = h('div',{class:"why"},[
        h("h2",null,"当前计数:100"),
        h("button",null,"+1")
    ])//vdom
    
    //8.通过mount函数,将vnode挂载到div#app上(虚拟DOM-->真实DOM的过程)
    mount(vnode,document.querySelector("#app"))
    
    //9.创建新的vnode(和上面那个旧的vnode进行diff算法)
        const vnode1 = h('div',{class:"lyx"},"哈哈哈");
        
    //11.调用patch()
    setTimeout(()=>{
        patch(vnode,vnode1);
    },2000)
    
</script>
2.1响应式系统入门理解

index.html

<script src= "./reactive.js"></script>

reactive.js

const info = {counter: 100}

function doubleCounter(){
    console.log(info.counter*2)
}

doubleCounter()

info.counter++;
//doubleCounter();

此时还不是响应式的(我这里counter改变了,但是不会再执行一次了,输出的只是200,没有202)

响应式的核心理念:很多地方同时使用这个数据,当这个数据发生改变的时候,其他地方应该再次对它进行执行,然后根据最新的数据,拿到最新的结果

reactive.js

思路:对 所有依赖此数据的函数 进行一个收集,当我有一天数据发生变化的时候,我对收集过来的依赖此数据的函数再次执行一次就可以了。

使用Set因为集合里面不能存储重复的元素,同一个函数我只需要更新一次就行了。

effect方法主要用于处理函数的响应式,可用于计算属性和watchEffect等功能,通过触发函数中响应式变量的proxyget方法实现将自身加入到proxydeps中,实现与proxy关联,也可以将其他依赖收集到自己的deps中

1.使用类,DepDependencies依赖的缩写
class Dep{
    //构造器
    constructor(){
    //每当我new一次的时候,就会产生一个subscriber
    //2.这个集合专门用来存储收集 依赖数据的函数
        this.subscriber = new Set();
    }
    
    //3.对重新需要执行的函数称:副作用
    addEffect(effect){
    //集合里面用add,不用push,开始存储 该函数
        this.subscriber.add(effect);
    }
    
    //4.通知所有的subscriber对他重新执行
    notify(){
        this.subscriber.forEach(effect => {
            effect();
        })
    }
}
const info = {counter: 100}

//5.创建dep对象
const dep = new Dep();

function doubleCounter(){
    console.log(info.counter*2)
}

function powerCounter(){
    console.log(info.counter * info.counter)
}

//6.存储 对counter依赖的函数
dep.addEffect(doubleCounter);
dep.addEffect(powerCounter);

info.counter++
//7.当我counter发生改变的时候,我就notify,通知sub再次执行一次 对数据依赖的 所有函数
dep.notify();

缺点:需要手动addEffect,数据发生变化的时候还要自己手动notify。

2.2响应式系统 升级版本
1.使用类,DepDependencies依赖的缩写
class Dep{
    //构造器
    constructor(){
    //每当我new一次的时候,就会产生一个subscriber
    //2.这个集合专门用来存储收集 依赖数据的函数
        this.subscriber = new Set();
    }
    
    //3.添加depend函数
    depend(){
    //如果active有值,我就添加进来
        if(activeEffect){
            this.subscribers.add(activeEffect)
        }
    }
    
    //7.通知所有的subscriber对他重新执行
    notify(){
        this.subscriber.forEach(effect => {
            effect();//是传进来的watchEffect函数
        })
    }
}

//4.
let activeEffect = null;
//6.实现watchEffect函数,接收 副作用函数
function watchEffect(effect){
    acticeEffect = effect;
    //执行depend函数,直接把effect添加到sub中
    dep.depend();
    
    effect();//会先立即执行一次,对老的数据先输出一次
    //只有先执行一次,才会调用get,才会自动调用依赖
    
    //再置空
    activeEffect = null;
}

const info = {counter: 100}

const dep = new Dep();

//5.改变function的写法,传到watchEffect里面
watchEffect(function(){
    console.log(info.counter*2)
})

watchEffect(function(){
    console.log(info.counter * info.counter)
})

info.counter++;
dep.notify();

自此,不用再手动addWatch

2:05:00(跳过) 但是,如果我有很多对象,存储的不同数据,并不是所有的函数都对相同的数据有所依赖,有的函数压根对某些数据没有依赖,我们希望就算此数据改变时,那跟我这个函数最好不要有关系,也就是说,我需要对他们进行严格的数据结构的区分管理。

Vue3进行数据劫持

function reactive(raw){
    return new Proxy(raw,{
        get(target,key){
            
        },
        set(target,key,newValue){
            
        }
    });
}

const proxy = reactive({name:"123"})
proxy.name = "321";

target拿到的是raw对象

图片.png