vue3更新机制的理解

2,616 阅读21分钟

基于理解vue3更新机制的基础上自己模拟实现一个简单的更新机制

收集依赖和触发依赖

  • 这里通过@vue/reactivity第三方库来了解vue3的更新机制,学会收集依赖和触发依赖的思想
  • 体会下方的代码中通过收集依赖和触发依赖的思想来更新数据的手法:
//这里的代码是在nodejs环境下执行的所以采用require的引入方式
const { ref, reactive, effect } = require("@vue/reactivity");

// 1.创建一个响应式数据 :10
const a = ref(10);

// 注意点:
//下方effect里的函数中因为a的值变更导致b的值也跟着发生更新
let b;

//2.在effect中的传递的函数会在一开始便执行一次
//该函数执行时会检索到有一个响应式数据 a

//3.于是该函数就被 响应式数据a 收集成了a的依赖
//当a的值变更时(setter)就会触发a的依赖
//就是触发该函数 :() => {b = a.value + 10;console.log(b);});
effect(() => {
  b = a.value + 10;
  console.log(b);
});

//4.当a的值变更时(setter)就会触发a的依赖
//在这里也就是触发该函数 :() => {b = a.value + 10;console.log(b);});
a.value = 20;
  • 在上方的逻辑代码中因为effect里的函数被判定为响应式数据a的依赖,在a的值发生变更时会触发有关a的依赖,直接导致了b的数据也跟着发生了更新,如果此时b代表着视图,那么是否同样可以做到因为一个响应式数据值的变更触发该值相应的依赖后引起视图的重新渲染更新呢?
  • 接着来验证我们的猜想,代码如下:
//这里的代码是执行是在浏览器的环境下执行的
/*
  <body>
    <div id="app"></div>
    <script src="./index.js" type="module"></script>
  </body>
*/
import {
  ref,
  reactive,
  effect,
} from "./node_modules/@vue/reactivity/dist/reactivity.esm-browser.js";



/*注意这里的写法
第一步 App={render(context){},setup(){return {}}}
第二步 App.render(App.setup());
必不可少
*/
const App = {
  render(context) {
    //构建 view
    effect(() => {
      document.querySelector("#app").innerHTML = ``;
      const div = document.createElement("div");
      //count是响应式数据  于是将该回调函数收集为依赖
      //等待着count的值变更会再次触发依赖(即再次更新视图)
      div.innerText = context.count.value;
      document.querySelector("#app").append(div);
    });
  },

  setup() {
    //方便调试
    window.count = ref(0);
    //4.count数据更改后触发视图更新
    return {
      count,
    };
  },
};

App.render(App.setup());
  • 可以从效果图看出count值的变更会引起视图的重新更新
  • 通过@vue/reactivity第三方库了解了vue3的更新机制,学会收集依赖和触发依赖的思想后,总结了如下的流程图:
  • 接着我们自己动手来完成上方的效果,实现并理清收集依赖和收集依赖的时机和过程

实现对普通值的更新机制

  • 提问一:如何判定在什么情况下去收集该值的依赖?
  • 提问二:如何判定在什么情况下去触发该值的依赖
  • 提问三:该值的依赖收集后要存储在什么地方?

解决:通过一个实例化类Dep来管理该值的更新机制,在该值被调用的时刻去收集该值的依赖,在该值变更的时刻去触发该值的所有依赖

  • 流程图如下:
  • 下面是实现该实例化类Dep来管理该值更新机制的逻辑代码:
// 创建一个全局的变量 
// 用来让该值的依赖暴露给一个全局变量
// 并通过这全局变量被收集为该值的依赖
let currentEffect = null;

class Dep{
    constructor(val) {
        // 1.把实例化时传递的这个参数存起来
        // 这个传递进来被保存的参数好比是响应式数据
        this._val = val;
        // 2.得有一个容器进行依赖的收集
        // 并且保证不能重复添加同一个依赖
        this.effects=new Set()
    }
    depend(){
      // 3.职责 收集依赖
      // 行为 要收集下方watchEffect中的effect  
      // 即this.effects.add(依赖)  
      // 依赖被暴露给了全局变量
      // 全局变量可能是null 在做一层判断
      if(currentEffect){
          //全局变量有值 则收集
          //有值证明该值的依赖被赋值给了全局变量currentEffect
          this.effects.add(currentEffect)
      }
    }

    notice(){
       //4.触发在依赖容器中的每一个依赖
        this.effects.forEach(effect=>{
            effect()
        })
    }
	
    //这个普通值名为value
    //该值getter的时候触发
    //5.即在dep=new Dep(10)后通过dep.value调用时被触发执行
    get value(){
        return this._val
    }
    
    //这个普通值名为value
    //该值setter的时候触发
    //6.即在dep=new Dep(10)后通过dep.value=20调用时被触发执行
    set value(newVal){
        this._val=newVal
    }
}
  • 写到这里,我们已经解决了提问三的该值的依赖收集后要存储在什么地方的问题。还需要解决的是提问一和提问二的问题,答案在下方的代码中:
// 传进来的effect就是该值的依赖
// 这里传进来的effect是函数会在一开始就被执行一次
function watchEffect(effect) {
        // 7.为了方便收集这个effect即该值的依赖
        // 把该依赖赋值给一个全局变量 暴露出去
        // 然后通过执行dep.depend来收集依赖
        currentEffect=effect

        //8.一上来就立马调用一次这个传递过来的函数
        //执行完成这个函数之后 证明dep.depend()收集依赖也执行完成了
        //得重新把全局变量给赋值null
        effect()

        //9.重新赋值回null
        currentEffect=null
}

//10.创建一个实例并传递一个普通值
//可以给一个值 现在给的是普通值 这个值好比是响应式数据
const dep=new Dep(10)

// 自己实现的函数watchEffect的功能是依照
// @vue/reactivity第三方库的effect函数的功能实现的
// 这个函数是要被dep.value依赖的函数
watchEffect(()=>{
    // 当我们对该值做get的时候要去触发收集依赖的时机
    // 当我们对该值做set的时候要去触发该值所有依赖的时机
    console.log(dep.value,'我出现了快收集该值的依赖');
    //11.我要收集依赖 这里是手动调用了收集依赖
    dep.depend()
})


// 该函数中有一个响应式对象  
// 假设这里dep.value是响应式对象
// 假设成立时上方watchEffect中的函数参数被做为了dep.value的依赖
dep.value=20;
//12.这里先手动调用 触发依赖
dep.notice()
  • 写到这里,我们自己封装实现了watchEffect函数,该函数的功能实现是类比@vue/reactivity第三方库的effec函数的功能实现的,传递进入watchEffect中的函数参数(这里传的参是一个函数),会在一开始就被执行一次,并根据该函数参数中的数据(这里是dep.value)对该数据进行收集依赖的操作(即会把这个函数做为该值的依赖),并在该值变更的时候,触发该值的所有依赖(即再次调用这个函数)。
  • 我们判定在该值(类比响应式数据)出现在传递给watchEffect中的函数参数时(即该值出现在传递的函数中表现为:watchEffect(()=>{console.log(dep.value,'我出现了快收集该值的依赖');dep.depend()})),便去收集该值的依赖
  • 我们判定在该值变更时,即dep.value=20,便去触发该值的所有依赖,即再次调用()=>{console.log(dep.value,'我出现了快收集该值的依赖');dep.depend()},注意此时虽然会再一次调用该依赖中的dep.depend()但是由于此时的全局变量currentEffect被恢复为null,所以不会重新的去收集依赖。
  • 我们不应该去手动的调用收集依赖和触发依赖,可以在dep.value的时候在get value()的逻辑中去收集依赖,在dep.value=20的时候在set value(newVal)的逻辑中去触发依赖,代码改写如下:
let currentEffect = null;

class Dep{
    constructor(val) {
        //存储该值
        this._val = val;
        //保证该值的依赖是不重复的
        this.effects=new Set()
    }
    depend(){
     //在全局变量(即该值的依赖)存在的时候才会执行收集依赖的操作
      if(currentEffect){
          this.effects.add(currentEffect)
      }
    }

    notice(){
    	//触发该值的所有依赖
        this.effects.forEach(effect=>{
            effect()
        })
    }
	

    //const dep=new Dep(10)
    //在dep.value的时候收集依赖并且返回该值
    get value(){
        //在dep.value的时刻去收集依赖
        //省去我们手动的去触发依赖
        this.depend()
        return this._val
    }
    
	//const dep=new Dep(10)
    //在dep.value=20的时候去触发该值的所有依赖
    set value(newVal){
        this._val=newVal
        //注意一定是先赋值为新值后再去通知触发依赖
        this.notice()
    }
}

//我们自己实现的watchEffect的功能
//是模仿@vue/reactivity第三方库的effec函数的功能实现的
function watchEffect(effect) {
        currentEffect=effect
        effect()
        currentEffect=null
}


const dep=new Dep(10)
watchEffect(()=>{
    //在该值getter的时候调用dep.depend()
    console.log(dep.value,'depvalue');
})


// 在该值setter的时候调用dep.notice()
dep.value=20;
  • 写到这里,我们已经实现了对该普通值的更新机制,运行代码打印的结果为10 'depvalue' 20 'depvalue',证明该逻辑是正确的,传递给watchEffect中的函数参数在一开始便被执行了一次才打印出10 'depvalue',并且在该函数中的dep.value也收集了该函数做为依赖,在dep.value=20的时刻去触发了该值的依赖即该函数,再一次的打印出了该值更新后的结果即20 'depvalue'

实现对对象的更新机制

  • 提问一:对象可能存在多个值,如何做到在多个值的情况下单独的对每一个值进行依赖收集?
  • 提问二:对象可能存在多个值,如何做到对每一个值进行相应的依赖收集,做到互不干扰的同时,可以方便的取出每一个值的依赖并触发想要的值的所有依赖?
  • 提问三:对象可能存在多个值,如果判定在访问对象的值的时候去执行收集依赖,要采用怎么样的数据结构来表达这一逻辑?
  • 提问四:对象可能存在多个值,如果判定在修改对象的值的时候去执行触发相对应的依赖,要采用怎么样的数据结构来表达这一逻辑?

解决:通过一个实例化类Dep来管理对象的更新机制,在对象的key被调用的时刻去收集该key的依赖,在修改对象key的value的时刻去触发该key的所有依赖

  • 流程图如下:
  • 下面是实现该实例化类Dep来管理对象更新机制的逻辑代码:
//创建一个全局的变量 依赖被暴露给了全局变量
let currentEffect = null;
class Dep{
    constructor() {
        // 得有一个容器进行依赖的收集 保证不能重复添加同一个依赖
        this.effects=new Set()
    }
    depend(){
            if(currentEffect){
                //全局变量有值 则收集
                this.effects.add(currentEffect)
            }
    }
    notice(){
        this.effects.forEach(effect=>{
            effect()
        })
    }
}
  • 我们需要做到将对象的多个key与通过实例化Dep后的dep给一一对应起来,通过Map的结构来建立关系,各自的dep中管理着对象各自key的依赖
  • 假设该对象为{name:'Ming',age:'22'},则我们的目标是通过Map的结构来建立关系,name对应着专属于name的dep,在watchEffect中的函数参数中使用到name这个key时去收集依赖,并且在改变name对应的value值时,去触发name的依赖
  • 明确目的后,来看代码实现:
let currentEffect = null;

//下方传递给watchEffect的参数是一个函数
function watchEffect(effect) {
    //将依赖即该函数 暴露给全局变量
    currentEffect=effect
    //该函数在一开始的时候就会被调用一次
    effect()
    //effect()该函数执行完成之后 即收集依赖执行完成之后
    //需要把该全局变量重新赋值为null
    //防止不必要的重新执行收集依赖
    currentEffect=null
}


const user={
    name:'Ming',
    age:'22'
}

//reactive函数封装了具体的实现逻辑
//reactive函数实现的代码这里先省略
const userState=reactive(user);

watchEffect(()=>{
    //具体的执行收集依赖逻辑被封装在reactive函数中
    console.log(userState.name);
})

//具体的执行触发依赖逻辑被封装在reactive函数中
userState.name='hhhh'
  • 从上方的代码中,可以看出大体的流程是与实现对普通值的更新机制是一样的,难点在于如何在userState.name的时刻找到name对应的dep去执行收集依赖,如何在userState.name='hhhh'的时刻找到name对应的dep去执行触发依赖,我们将这些逻辑封装在reactive函数中
  • 下方的代码为你揭晓这一实现的逻辑:
function reactive(raw){
    //这里的raw 是传递进来的user({name:'Ming', age:'22'})
    //返回一个通过Proxy方法代理的对象
    return new Proxy(raw,{
    	//get() 方法用于拦截对象的读取属性操作。
        //通过new Proxy让我们在userState.name的时候可以找到name对应的dep
        //通过对应的专属于name的dep来去完成收集依赖和触发依赖
        get(target,key){
    
            //getDep抽离为函数
            //getDep的职责是找到对应key的dep
            const dep=getDep(target,key)

            //获取到key对用的dep后
            //可以收集依赖了
            dep.depend()

            //废弃的写法:return target[key]
            return Reflect.get(target,key)
        },
        set(target,key,value){
            //找到key 对应的dep 最后用notice来触发依赖
            const dep=getDep(target,key)
            // 改变旧值为新的值
            // 废弃的写法:target[key]=value
            const result=Reflect.set(target,key,value)
            //notice一定要在修改值之后调用
            dep.notice()
            //set操作是得有返回值的
            return result;
        }

    })
}




//存储所有的对象
const targetMap=new Map()

//知道key是谁 就要去取key对应的那个dep
//找到key对应的dep之后才能在去收集和触发依赖
function getDep(target,key){
    let depsMap=targetMap.get(target)
    if(!depsMap){
    	// 第一次获取的时候肯定是无值的所以需要一个初始化的构建步骤
        // 保证了depsMap一定是有值的
        // targetMap.set(target,'值是什么')
        depsMap=new Map()
        targetMap.set(target,depsMap)
    }
    let dep=depsMap.get(key)
    if(!dep){
        // 一开始获取的时候dep也是没有值的
        //对象的每一个属性做为map的key,属性对应的dep做为map的value
        dep=new Dep();
        depsMap.set(key,dep)
    }
    //返回的是一个dep实例
    //返回的是专属于这个key的dep实例
    return dep
}
  • 写到这里,我们已经实现了对对象的更新机制,运行代码打印的结果为Ming hhhh,证明该逻辑是正确的,传递给watchEffect中的函数参数在一开始便被执行了一次才打印出Ming,在该函数参数中使用了reactive函数加工返回的对象userState并调用了该对象的key值即userState.name
  • 在reactive函数中通过Proxy方法对传递进入的对象进行了get和set的拦截,使得在访问userState.name不仅可以返回值还会对该key值进行依赖的收集,在修改userState.name的值时不仅能成功修改值还会触发key的依赖。

自己实现对页面的更新机制

  • 借助@vue/reactivity第三方库的方法可以实现对页面的更新机制,现在我们自己完成了对普通值和对对象的更新机制,已经可以借助自己封装的这两套机制去自己实现对页面的更新机制了。
  • 先来回顾一下借助@vue/reactivity第三方库实现对页面的更新机制的代码实现:
//@vue/reactivityy第三方库的reactive方法我们自己实现了叫做reactive
//@vue/reactivityy第三方库的effect方法我们自己实现了叫做watchEffect
import {
  ref,
  reactive,
  effect,
} from "./node_modules/@vue/reactivity/dist/reactivity.esm-browser.js";

//即将要自己来构造一个App对象 单独抽离出去为一个文件
const App = {
  render(context) {
    //即将要用自己实现的watchEffect方法替换下方第三方库的effect方法
    effect(() => {
      document.querySelector("#app").innerHTML = ``;
      const div = document.createElement("div");
      //count是响应式数据  于是将该回调函数收集为依赖
      //等待着count的值变更会再次触发依赖(即再次更新视图)
      div.innerText = context.count.value;
      document.querySelector("#app").append(div);
    });
  },

  setup() {
    //方便调试
    //即将要用自己实现reactive替换下方第三方库的ref方法
    window.count = ref(0);
    //4.count数据更改后触发视图更新
    return {
      count,
    };
  },
};


//这一步必不可少
App.render(App.setup());
  • 现在@vue/reactivity第三方库的reactive、ref和effect方法都可以被我们自己实现的reactive和watchEffect所替代,我们先将自己实现的这两个方法导出,代码如下:
//这里是写在"./core/reactivity/index.js"文件下
//所以下方导入的写法是
//import { reactive, watchEffect } from "./core/reactivity/index.js";


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


export function reactive(raw) {
  return new Proxy(raw, {
    get(target, key) {
      const dep = getDep(target, key);
      dep.depend();
      return Reflect.get(target, key);
    },
    set(target, key, value) {
      const dep = getDep(target, key);
      const result = Reflect.set(target, key, value);
      dep.notice();
      return result;
    },
  });
}

const targetsMap = new Map();

function getDep(target, key) {
  let depsMap = targetsMap.get(target);
  if (!depsMap) {
    depsMap = new Map();
    targetsMap.set(target, depsMap);
  }
  let dep = depsMap.get(key);
  if (!dep) {
    dep = new Dep();
    depsMap.set(key, dep);
  }

  return dep;
}


let currentEffect = null;

class Dep {
  constructor() {
    this.effects = new Set();
  }
  depend() {
    if (currentEffect) {
      this.effects.add(currentEffect);
    }
  }
  notice() {
    this.effects.forEach((effect) => {
      effect();
    });
  }
}
  • 基于自己实现的这两个方法实现对页面的更新机制,代码如下:
/*
这里是写在"./index.js"文件下
可以通过html引入该文件在浏览器下查看效果即
  <body>
    <div id="app"></div>
    <script src="./index.js" type="module"></script>
  </body>
*/

//导入自己实现的两个方法
import { reactive, watchEffect } from "./core/reactivity/index.js";


//为了醒目这里的App对象可以单独抽离出去做为一个文件
const App = {
    render(context) {
      //原@vue/reactivity第三方库的effect换成watchEffect
        watchEffect(() => {
            document.querySelector("#app").innerHTML = ``;
            const div = document.createElement("div");
            div.innerText = context.state.count;
            document.querySelector("#app").append(div);
        });
    },

    setup() {
       //原@vue/reactivity第三方库的ref换成reactive
        const state = reactive({
            count: 1,
        });
        return {
            state,
        };
    },
};


//这一步是必须的
//为了醒目这里的调用也可以单独抽离出去做为一个文件
App.render(App.setup());
  • 写到这里,我们可以直观的在浏览器上看到运行的效果是成功的,如下:
  • 紧接着我们在这套逻辑的实现基础上来模拟实现vue3的入口构建方式,目录结构和主入口文件的代码如下:
  • 下方是主入口文件的代码:
//主入口文件index.js引入所有依赖

//导入的是createApp(rootComponent)方法
import { createApp } from "./core/createApp.js";

//导入的是一个{render(){},setup(){}}的对象
import App from "./App.js";


//以这样的形式封装实现了类比vue3的更新机制的写法
createApp(App).mount(document.querySelector("#app"));
  • 下方是"./core/createApp.js"文件的代码:
//导入自己实现的 watchEffect 方法
import { watchEffect } from "./reactivity/index.js";

/*
类比@vue/reactivity库最后必须实现的App.render(App.setup());
1.const setupResult = rootComponent.setup();
2.const element=rootComponent.render(setupResult)
*/
export function createApp(rootComponent){
  return {
    mount(rootContainer) {
      //这里的rootComponent是{render(){},setup(){}}对象
      //1.类比@vue/reactivity库App.setup()的步骤
      //执行完成之后这里返回的是 state 这个响应式数据
      const setupResult = rootComponent.setup();
		
      //传进的函数参数会在一开始就执行一次并被收集为值的依赖
      watchEffect(() => {
        //这里的rootComponent是{render(){},setup(){}}对象
        //2.类比@vue/reactivity库App.render(App.setup())的步骤
        //执行完成之后这里返回的是 div 元素节点
        const element=rootComponent.render(setupResult)
        //呈现在浏览器上
        rootContainer.append(element)
      })
    }
  }
}
  • 下方是"./App.js"文件的代码:
//导入自己实现的 reactive 方法
import { reactive } from "./core/reactivity/index.js";


//类比之前在@vue/reactivity库定义的App对象
export default {
  render(context) {
    document.querySelector("#app").innerHTML = ``;
    const div = document.createElement("div");
    // 响应式数据   count的变化会引起视图更新
    div.innerText = context.state.count;
    // 返回视图容器
    return div;
  },
  setup() {
    const state = reactive({
      //count现在被自己封装的方法给变成可以触发页面更新的数据了
      count: 1,
    });
    window.state = state;
    // count改变会引起视图更新
    return {
      state,
    };
  },
};
  • 下面是'index.html'文件的代码:
<body>
  <div id="app"></div>
  <script src="./index.js" type="module"></script>
</body>
  • 下面是"./reactivity/index.js"文件的代码:
//自己实现的reactive和watchEffect两个方法

export function watchEffect(effect) {
  currentEffect = effect;
  effect();
  currentEffect = null;
}
const targetsMap = new Map();



export function reactive(raw) {
  return new Proxy(raw, {
    get(target, key) {
      const dep = getDep(target, key);
      dep.depend();
      return Reflect.get(target, key);
    },
    set(target, key, value) {
      const dep = getDep(target, key);
      const result = Reflect.set(target, key, value);
      dep.notice();
      return result;
    },
  });
}

function getDep(target, key) {
  let depsMap = targetsMap.get(target);
  if (!depsMap) {
    depsMap = new Map();
    targetsMap.set(target, depsMap);
  }
  let dep = depsMap.get(key);
  if (!dep) {
    dep = new Dep();
    depsMap.set(key, dep);
  }

  return dep;
}


let currentEffect = null;
class Dep {
  constructor() {
    this.effects = new Set();
  }

  depend() {
    if (currentEffect) {
      this.effects.add(currentEffect);
    }
  }

  notice() {
    this.effects.forEach((effect) => {
      effect();
    });
  }
}

虚拟节点的模拟实现

  • 在上方自己实现对页面的更新机制中,我们可以观察到"./App.js"文件的代码,是将创建dom的操作给写死了,我们可以引入虚拟节点,来将创建dom的权利交给用户,先来回顾一下"./App.js"文件的代码:
import { reactive } from "./core/reactivity/index.js";


//类比之前在@vue/reactivity库定义的App对象
export default {
  render(context) {
   //将创建dom的操作给写死了
   //在引入一层虚拟节点
    document.querySelector("#app").innerHTML = ``;
    const div = document.createElement("div");
    div.innerText = context.state.count;
    return div;
  },
  setup() {
    const state = reactive({
      count: 1,
    });
    window.state = state;
    return {
      state,
    };
  },
};
  • 实现的流程图如下:
  • 引入用js对象表示的虚拟节点,虚拟节点的构造如图:
  • 这里用h()函数来构造该虚拟节点并导出以供使用,代码如下:
//这里是写在'core/h.js'文件下

//创建虚拟节点
export function h(type,props,children){
    return {
        type,
        props,
        children
    }
}
  • 接着我们不再在"./App.js"文件中把创建dom元素给写死,而是通过引入'core/h.js'文件下的h()函数方法来先创建虚拟节点,代码如下:
import { reactive } from "./core/reactivity/index.js";

import { h } from './core/h.js';

export default {
  render(context) {

    //在这里加入了中间层  先返回虚拟节点 在转换为真实的dom元素
    //第三个参数 支持string的写法 和 支持数组的写法
    //return h('div',null,[h('p',null,'test for children')])
    return h('div',{id:'test'},'test app')
  },
  setup() {
    const state = reactive({
      count: 1,
    });
    window.state = state;
    return {
      state,
    };
  },
};
  • 'core/createApp.js'文件中去接收到这个虚拟节点,并做将虚拟节点转换为真实的dom元素的操作,代码如下:
import { watchEffect } from "./reactivity/index.js";

import { mountElement } from '../core/mountElement.js';


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

      watchEffect(() => {
        //替换!! const element=rootComponent.render(setupResult)
        //替换!! rootContainer.append(element)

        //现在得到的是虚拟节点树
        const subTree=rootComponent.render(setupResult)
        console.log(subTree,'subTree');
        //接着我们要将虚拟的节点转换为真实的dom才能够呈现在浏览器上


        //在这里又加入了一层中间层  在这里接收到虚拟节点
        // 在mountElement方法中真正的转为真实的dom元素呈现在视图
        //封装一个接收虚拟节点和根容器的方法  mountElement(subTree,rootContainer)
        mountElement(subTree,rootContainer)

      })
    }
  }
}
  • 将虚拟的节点转换为真实的dom封装在mountElement函数中,代码如下:
//这里是'../core/mountElement.js'文件的代码

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

function patchProp(el,key,prevValue,nextValue){
    el.setAttribute(key,nextValue)
}

//这个方法可以将虚拟转换为真实的dom并且呈现在浏览器上
export function mountElement(vnode,container) {
    //创建标签type
    //设置props
    //添加children

    const {type,props,children} =vnode;
    const el=createElement(type)

    //当props存在的话设置属性
    if(props){
        for(const key in props){
            const val =props[key]
            patchProp(el,key,null,val)
        }
    }

    //设置children
    if(typeof children === 'string'){
        const text =document.createTextNode(children)
        //孩子节点先单独添加一下
        el.append(text)
    }else if(Array.isArray(children)){
        //return h('div',null,[h('p',null,'children')])
        children.forEach((v)=>{
            // children 是返回 {type,props,children}
            // children 的 container是 el 即当前的容器
            console.log(v,'vnode');
            //递归
            mountElement(v,el)
        })
    }

    //在这里在抽离为函数 insert
    container.append(el)
};
  • 效果图:

diff算法的需求体现

  • 在以上的方法实现中,我们会发现每一次的更新视图都会导致视图中的节点被全部干掉在重新的载入,节点每次都被重新删除在创建载入是十分浪费性能的,因为那些不参与更新的视图也会被强制的删除在更新
  • 可以效果图来感受一下这个场景:
  • diff算法就是应该去算出哪些是不需要更新的视图,最大化的节省性能消耗,算出最小的更新节点
  • 思路:可以通过对比更新前的虚拟节点和更新后的虚拟节点来对比算出如何最小化的进行节点的更新,对比图如下:
//这里是写在"./core/createApp"文件下
//即将要引入diff算法


import { watchEffect } from "./reactivity/index.js";
/*
类比@vue/reactivity库最后必须实现的App.render(App.setup());
1.const setupResult = rootComponent.setup();
2.const element=rootComponent.render(setupResult)
*/

//将diff算法的实现逻辑抽离出去在另一个文件中实现
import { mountElement,diff } from '../core/mountElement.js';
export function createApp(rootComponent){
  return {
    mount(rootContainer) {
      //这里的rootComponent是{render(){},setup(){}}对象
      //执行完成之后这里返回的是 state 这个响应式数据
      const setupResult = rootComponent.setup();

      //需要区分是初始化过程还是要重新更新的过程
      //通过isMounted变量来记录此时的操作是初始化还是要进行更新
      let isMounted=false;
      //通过prevSubTree变量来记录每一次更新之前的虚拟节点的结构
      let prevSubTree=null;
      
      watchEffect(() => {
        //是初始化的过程
        if(!isMounted){
          //设置为true意味着下次再更新的时候走的是另一个逻辑
          //不需要再走初始化的逻辑了
          isMounted=true;
          const subTree=rootComponent.render(setupResult)
          rootContainer.innerHTML=``
          mountElement(subTree,rootContainer);

          //存一下之前的subTree 虚拟节点
          prevSubTree=subTree;
          
        }else{
          //更新的逻辑 加入diff算法
          //我们需要两个虚拟节点  
          //更新之前的虚拟节点和更新后的虚拟节点
          //通过diff算法来实现最小化的节点的更新
          let subTree=rootComponent.render(setupResult)
          //console.log(JSON.stringify(prevSubTree,null,4));
          //console.log(JSON.stringify(subTree,null,4));
          // console.log('currentTree现在的虚拟节点',subTree);
          // console.log('prevTree之前的虚拟节点', prevSubTree);

          //引入diff算法并更新节点
          //todo...  2.01.43


          //更新完成之后再把现在的虚拟节点变为之前的虚拟节点
          //等待下一次的更新
          prevSubTree=subTree
        }
      })
    }
  }
}