真实DOM渲染
虚拟节点vnode 通过 diff算法 对比 要比 element div属性更有效率
以前DOM都是用HTML来编写的,但是我们使用虚拟DOM可以通过JS来操作,会更加的简单高效
将VNode转为对应平台中 所需要的 控件,实现 跨平台
允许自己开发渲染器,将VNode渲染成你所需要的 控件等
虚拟DOM的渲染过程
三大核心系统
1.compiler:编译模板的
将这些代码转换成 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:运行时,渲染器模块,真正负责渲染的
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算法,最终进行修改。
实现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等功能,通过触发函数中响应式变量的proxy的get方法实现将自身加入到proxy的deps中,实现与proxy关联,也可以将其他依赖收集到自己的deps中
1.使用类,Dep是Dependencies依赖的缩写
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.使用类,Dep是Dependencies依赖的缩写
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对象