前言
vue3项目中组件是怎么生成怎么渲染的呢?组件是怎么生成虚拟dom的呢?新虚拟dom与旧虚拟dom是怎么比对的呢?其中涉及的diff算法又是怎么实现的呢?生命周期又是怎么实现的呢?本文章将会用vue3源码,根据渲染顺序为大家解读。(注意:这里采用的是render函数渲染的方式)
了解使用
let {createApp,reactive,h}=VueRuntimeDom
let App={
setup(){
let state=reactive({
age:2
})
return {
state
}
},
render(proxy){//整合所有参数到一个对象返回h函数(创建标签)
this==proxy(组件所有除render以外的参数)
return h('div',{style:{coloe:'red'},onClick:fn},`HELLO${state.age}`)
}
// 便捷写法 所以不提倡vue3写v2
//return ()=>{
// return h('div'{style{coloe:'red'},onClick:fn},//`HELLO${state.age}`)
//}
}
createApp(App).mount("#app")
//createApp(App,{name:"ppa"}).mount("#app")也可以这么些
这一段代码大家再熟悉不过了,其中render函数就是渲染组件的核心,参数proxy对象是setup所有数据的整合,跟他的this指向是同一个,h函数是用来生成虚拟dom,然后更新组件的。setup函数是一个入口函数,有2参数,一个是props组件传递的参数,一个是content是执行上下文。
首先createApp会利用传进来的参数生成一个虚拟dom
虚拟dom就是用节点属性,元素类型和子组件等参数创建的一个js对象
//创建虚拟dom
export const createVnode=(type,props,children=null)=>{
//组件还是元素
let shapeFlog =isString(type)?ShapeFlage.ELEMENT:isObject(type)?ShapeFlage.FUNCTIONAL_COMPONET:0
const vnode={
_v_isVnode:true,
type,
props,
children,
key:props&&props.key,
el:null,
component:{},
shapeFlog
}
//儿子标识 判断是否有儿子组件 有做相应处理 如果儿子也是组件也会生成虚拟dom
normalizeChildren(vnode,children)
return vnode
}
将虚拟dom作为参数生成组件实例
export const createComponentInstance = (InitialVnode) => {
//组件初始化
const instance = {
InitialVnode,
type:InitialVnode.type,//实例的所有内容
props: {}, //组件属性
attrs: {},
setupState: {},//setup返回值
ctx: {}, //处理代理
proxy: {},
render:false,
isMounted: false, //是否挂载
};
instance.ctx = { _: instance };
return instance;
};
将虚拟dom数据(就是一开始createApp传进来的第二个参数)解析到实例上,并把虚拟dom赋值组件的一个属性,来达到保存旧的虚拟dom的要求
//解析数据到组件实例
export const setupComponent = (instance) => {
//代理 访问对象给出对象props属性
instance.proxy=new Proxy(instance.ctx,componentPublicInstance as any)
//设置值
const {props, children}=instance.vnode
//解析数据
instance.props=props
instance.children=children//slot 插槽
//看一下组件有没有setup方法
let isStateFul = instance.vnode.shapeFlag & ShapeFlage.STATEFUL_COMPONENT
if(isStateFul){
//有状态的组件
setUpStateComponent(instance)
}else{
//setu没有
}
};
处理setup参数,将setup参数解析到数组,并将虚拟dom的参数和上下文作为参数放入setup,方便使用者调用时拿到
function setUpStateComponent(instance){
//setup返回值是render()函数
//获取新建的类型拿到组件的setup方法
let Component =instance.type
let {setup}=Component
//处理参数
//setup之前生成全局实例
currentInstance=instance
if(setup){
let setupContext =createConenttext(instance)
let setUpResult=setup(instance.props,setupContext)
//setup执行完毕
currentInstance=null
handlerSetupResult(instance,setUpResult)//如果是对象将值放在对象上 如果是函数就是render
}else{
//调用render
finishComponentSetup(instance)//vnode
}
Component.render(instance)
}
//处理setup的返回结果
function handlerSetupResult(instance,setUpResult){
if(isFunction(setUpResult)){
//render
instance.render=setUpResult
}else if(isObject(setUpResult
)){
instance.setupState=setUpResult
}
finishComponentSetup(instance)
}
//处理render
function finishComponentSetup(instance){
//判断组件中有没有render
let Component=instance.type
if(!instance.render){
//模板编译
if(!Component.render &&Component.template){
}
instance.render=Component.render
}
}
function createConenttext(instance){
return {
attrs:instance.attrs,
slots:instance.slot,
emit:()=>{},
expose:()=>{},
}
}
创建一个effect ,这是render函数在第一次渲染后如果组件更新再次渲染的核心(对于effect,vue3源码——reactive和ref中有详细介绍。),并在其中执行patch方法进行刚刚生成的虚拟dom和原来的虚拟dom对比然后更新。
注意:每个虚拟dom在生成的时候都会将真实节点赋值给虚拟节点的一个属性方便新旧虚拟对比后更新节点。
function setupRenderEffect(instance, container) {
let {bm,m,bu,u}=instance
effect(function componrntEffrct() {
if (!instance.isMounted) {
//组件实例渲染之前-------
if(bm){
invokeArrayFns(bm)
}
let proxy = instance.proxy;
let subTree = (instance.subTree = instance.render.call(proxy.proxy)); //一个虚拟dom
//渲染字树
patch(null, subTree, container);
instance.isMounted = true;
//组件实例渲染之后-------
if(m){
//怎么执行生命周期钩子函数
invokeArrayFns(m)
}
} else {
if(bu){
//怎么执行生命周期钩子函数
invokeArrayFns(bu)
}
//新旧对比
let proxy = instance.proxy;
let preTree = instance.subTree;
let nextTree = (instance.subTree = instance.render.call(proxy.proxy)); //一个虚拟dom
instance.subTree = nextTree;
patch(preTree, nextTree, container);
if(u){
invokeArrayFns(u)
}
}
});
}
patch方法
shapeFlag是一个枚举判断虚拟dom的类型是文本数组还是其他
const patch = (n1, n2, container,ancher=null) => {
//针对不同类型 1。组件2元素
//1.先比对是不是同一个元素 不是直接替换
// 2.是同一个元素
if (n1 && !isSomeVnode(n1, n2)) {
unmount(n1);
n1 = null;
renderOptionDom.insert(n2.el, container);
} else {
//同一个元素 比对属性
patchElement(n1, n2, container);
}
let { shapeFlag, type } = n2;
switch (type) {
case TEXT:
//处理文本
processText(n1, n2, container);
break;
default:
if (shapeFlag & shapeFlag.ELEMENT) {
//处理元素 加载组件一样
processElement(n1, n2, container,ancher);
} else if (shapeFlag & shapeFlag.STATEFUL_COMPONENT) {
//组件
processCompent(n1, n2, container);
}
}
};
//组件创建
const processCompent = (n1, n2, container) => {
if (n1 == null) {
//是第一次吗
mountComponent(n2, container);
} else {
//更新
}
};
const processElement = (n1, n2, container,ancher=null) => {
if (n1 == null) {
mountElement(n2, container,ancher);
} else {
}
};
//处理孩子
function mountChildren(el, children) {
//循环
for (let i = 0; i < children.length; i++) {
let child = CVnode(children[i]);
//
patch(null, child, el);
}
}
//递归渲染再变成 dom操作放到对应页面
const mountElement = (vnode, container,ancher=null) => {
const { props, shapeFlag, type, children } = vnode;
//创建真实元素
let el = (vnode.el = renderOptionDom.createElement(type));
//添加属性
if (props) {
for (let key in props) {
renderOptionDom.patchProps(el, key, null, props[key]);
}
}
//处理儿子
if (children) {
if (shapeFlag & ShapeFlage.TEXT_CHILDREN) {
// 创建文本元素
renderOptionDom.setElementText(el, children);
} else if (shapeFlag & ShapeFlage.ARRAY_CHILDREN) {
//递归 patch
mountChildren(el, children);
}
}
//放到对应位置
renderOptionDom.insert(el, container,ancher);
};
//处理文本
function processText(n1, n2, container) {
if (n1 == null) {
//创建文本 渲染到页面
renderOptionDom.insert(renderOptionDom.createText(n2.children), container);
}
}
patch方法新旧虚拟dom对比的diff算法
1.先比对虚拟dom是否相同元素 不相同直接将旧的虚拟节点删除然后渲染新的虚拟节点 2.如果相同元素就对比元素的属性 3.如果都有子组件就进行子组件比对 1)将2个子组件数组从头开始比 其中一个子组件不同时暂停 2)将2个子组件从后往前对比有不同就展厅 3)中间部分用乱序表比对 来达到子组件复用(用新的虚拟dom生成一个哈希表,遍历旧的子组件数组去哈希表找来达到增删改查的目的) (renderOptionDom里面包含很多节点的真实操作,比如添加删除节点,增加文本属性)
//比对儿子
const patchChildre = (el, n1, n2) => {
const c1 = n1.children;
const c2 = n2.children;
const prevShapleflage = n1.shapeFlag;
const newShapleflage = n2.shapeFlag; //新的标识
//儿子 4中
//1. 旧的有儿子 新的没有
//2. 新的有儿子 旧的没有
//3. 儿子都是文本
//4. 都有儿子 儿子都是数组
if (newShapleflage && ShapeFlage.TEXT_CHILDREN) {
//文本
renderOptionDom.setText(el, c2);
} else {
if (prevShapleflage.ARRAY_CHILDREN && newShapleflage.ARRAY_CHILDREN) {
//新的是数组
//都有儿子
patchKeyChildren(el, c1, c2);
} else {
//旧的是文本
//将旧的文本删除
renderOptionDom.setElementText(el, "");
mountChildren(c1, c2);
}
}
};
const patchKeyChildren = (el, c1, c2) => {
//v2双指针
let i = 0;
let e1 = c1.length - 1;
let e2 = c2.length - 1;
//async from start
while (i <= e1 && i <= e2) {
const n1 = c1[i];
const n2 = c2[i];
if (isSomeVnode(n1, n2)) {
patch(n1, n2, el);
} else {
break;
}
i++;//比对的位置
}
//async from end
while (i <= e1 && i <= e2) {
const n1 = c1[e1];
const n2 = c2[e2];
if (isSomeVnode(n1, n2)) {
patch(n1, n2, el);
} else {
break;
}
e1--;//比对的位置
e2--
}
if(i>e1){
//旧的数据少新的数据多添加数据
const nextPros = e2 +1
//如果是前追加 e2+1 c2
const ancher = nextPros<c2.length?c2[nextPros].el:null
while(i>=e2){
patch(null,c2[i++],el,ancher)
}
}else if(i>e1){
//旧的多新的少
//删除
while(i<=e1){
unmount(c1[i ++])
}
}else{
//1.新的乱序创建引射表
// 2.用旧的乱的数据去新的表中找 要不就删除要不就插入
let s1=i
let s2=i
//解决乱序比对问题 用数组
const toBepathed= e2-s2+1
const newIndexToPatchMap=new Array(toBepathed).fill(0)
//创建引射表 唯一性n
let keyIndedexMap = new Map()
//用新的数据创建表
for(let i=s2;i<=e2;i++){
const childVnode=c2[i]
keyIndedexMap.set(childVnode.key,i)
}
//去老的里面找
for(let i=s1;i<=e1;i++){
const oldVnode = c1[i]
let newIndex= keyIndedexMap.get(oldVnode.key)
if(newIndex===undefined){//新的表中没有就删除
unmount(oldVnode)
}else{//有就对比
patch(oldVnode,c2[newIndex],el)
//旧和新的关系
newIndexToPatchMap[newIndex - s2]=i+1//新的数据在老的索引的1位置+1
}
}
//移动节点 添加新曾的元素 方法倒叙循环
for(let i= toBepathed -1;i>=0;i--){
let currentIndex =i + s2//新增h元素的索引
let child =c2[currentIndex]
let ancher=currentIndex+1 <c2.length?c2[currentIndex+1].el:null
if(newIndexToPatchMap[i]==0){
patch(null,child,el)
}else{
renderOptionDom.insert(child.el,el,ancher)
}
}
}
};
const patch = (n1, n2, container,ancher=null) => {
//针对不同类型 1。组件2元素
//1.先比对是不是同一个元素 不是直接替换
// 2.是同一个元素
if (n1 && !isSomeVnode(n1, n2)) {
unmount(n1);
n1 = null;
renderOptionDom.insert(n2.el, container);
} else {
//同一个元素 比对属性
patchElement(n1, n2, container);
}
let { shapeFlag, type } = n2;
switch (type) {
case TEXT:
//处理文本
processText(n1, n2, container);
break;
default:
if (shapeFlag & shapeFlag.ELEMENT) {
//处理元素 加载组件一样
processElement(n1, n2, container,ancher);
} else if (shapeFlag & shapeFlag.STATEFUL_COMPONENT) {
//组件
processCompent(n1, n2, container);
}
}
};
//组件创建
const processCompent = (n1, n2, container) => {
if (n1 == null) {
//是第一次吗
mountComponent(n2, container);
} else {
//更新
}
};
//组件渲染
const mountComponent = (InitialVnode, container) => {
//1先有一个组件的实例对象render(proxy)
const instance = (InitialVnode.component =
createComponentInstance(InitialVnode));
//2解析数据到实例对象当中
setupComponent(instance);
//3创建一个effect 让render函数执行
setupRenderEffect(instance, container);
};
const processElement = (n1, n2, container,ancher=null) => {
if (n1 == null) {
mountElement(n2, container,ancher);
} else {
}
};
//处理孩子
function mountChildren(el, children) {
//循环
for (let i = 0; i < children.length; i++) {
let child = CVnode(children[i]);
//
patch(null, child, el);
}
}
//递归渲染再变成 dom操作放到对应页面
const mountElement = (vnode, container,ancher=null) => {
const { props, shapeFlag, type, children } = vnode;
//创建真实元素
let el = (vnode.el = renderOptionDom.createElement(type));
//添加属性
if (props) {
for (let key in props) {
renderOptionDom.patchProps(el, key, null, props[key]);
}
}
//处理儿子
if (children) {
if (shapeFlag & ShapeFlage.TEXT_CHILDREN) {
// 创建文本元素
renderOptionDom.setElementText(el, children);
} else if (shapeFlag & ShapeFlage.ARRAY_CHILDREN) {
//递归 patch
mountChildren(el, children);
}
}
//放到对应位置
renderOptionDom.insert(el, container,ancher);
};
//处理文本
function processText(n1, n2, container) {
if (n1 == null) {
//创建文本 渲染到页面
renderOptionDom.insert(renderOptionDom.createText(n2.children), container);
}
}
渲染组件完毕就挂载
export const createApp=(rootComponent,rootProps)=>{
let app =createRender(renderOptionDom).createApp(rootComponent,rootProps)
let {mount} =app
app.mount=function(container){//#app
//挂在组件 清空
container=document.querySelector(container)
container.innerHTML=''
mount(container)
}
return app
}
vue3没有this但是可以在setup用 getCurrentInstance拿到this也就是组件实例。那生命周期和this的获取怎么来的呢?
很简单,在setup调用前将组件实例暴露给全局,调用结束后将全局组件实例变量赋值为undefined(防止子组件也拿到父组件的实例) 这段代码是前面提到将setup所需参数放入。
export let currentInstance
//处理setup
function setUpStateComponent(instance){
//setup返回值是render()函数
//获取新建的类型拿到组件的setup方法
let Component =instance.type
let {setup}=Component
//处理参数
//setup之前生成全局实例
currentInstance=instance
if(setup){
let setupContext =createConenttext(instance)
let setUpResult=setup(instance.props,setupContext)
//setup执行完毕全局实例清楚
currentInstance=null
handlerSetupResult(instance,setUpResult)//如果是对象将值放在对象上 如果是函数就是render
}else{
//调用render
finishComponentSetup(instance)//vnode
}
Component.render(instance)
}
export function getCurrentInstance(){
return currentInstance
}
所以以组件实例只有在setup执行中拿到
vue3生命周期没有beforeCreate和create,可以将setup的执行看作这2个时期。那么onBeforeMount,onMount呢?
上面已经讲完组件的patch方法,用来对比新旧虚拟dom然后更新,更新就意味着组件的更新和渲染。组件挂载前的patch方法前后就属于onBeforeMount,onMount,节点挂载后的patch方法前后就属于onBeforeUpdate,onUpdate。并且,你拿到的组件实例上面个会有bm,m,bu,u这4个数组,里面的方法就代表4个生命周期创作者传入的回调函数。所以在执行生命周期时在调用生命周期的回调函数前将生命周期赋值给组件实例。
const enum lifeCycle{
BEFOREMOUNT="bm",
MOUNTED="m",
BEFOREUPDATE="bu",
UPDATED="u",
}
//写4个生命周期
export const onBeforeMount=createHook(lifeCycle.BEFOREMOUNT)
export const onMount=createHook(lifeCycle.MOUNTED)
export const onBeforeUpdate=createHook(lifeCycle.BEFOREUPDATE)
export const onUpdate=createHook(lifeCycle.UPDATED)
//返回值是函数
function createHook(lifecycle){
//核心是生命周期和当前组件产生关联
//问题:vue组件父子关系
return function(hook,target=currentInstance){
//hook生命周期中的方法 获取当前组件实例
injectHook(lifecycle,hook,target)
}
}
function injectHook(lifeCycle,hook,target){
if(!target){
return
}
const hooks =target[lifeCycle]||(target[lifeCycle]=[])//将生命周赋值给组件实例上的bm等属性
const rap=()=>{
setCurrentTinstance(target)//将组件设置为全局变量
hook()//生命周期回调函数函数的执行
setCurrentTinstance(null)
}
hooks.push(hook)
}
//生命周期执行
export function invokeArrayFns(FnArr){
FnArr.forEach(fn=>{
fn()
})
}
function setupRenderEffect(instance, container) {
let {bm,m,bu,u}=instance
effect(function componrntEffrct() {
if (!instance.isMounted) {
//组件实例渲染之前-------
if(bm){
invokeArrayFns(bm)
}
let proxy = instance.proxy;
let subTree = (instance.subTree = instance.render.call(proxy.proxy)); //一个虚拟dom
//渲染字树
patch(null, subTree, container);
instance.isMounted = true;
//组件实例渲染之后-------
if(m){
//怎么执行生命周期钩子函数
invokeArrayFns(m)
}
} else {
if(bu){
//怎么执行生命周期钩子函数
invokeArrayFns(bu)
}
//新旧对比
let proxy = instance.proxy;
let preTree = instance.subTree;
let nextTree = (instance.subTree = instance.render.call(proxy.proxy)); //一个虚拟dom
instance.subTree = nextTree;
patch(preTree, nextTree, container);
if(u){
invokeArrayFns(u)
}
}
});
}
总结:
graph TD
产生vnode --> 实例化组件将虚拟dom赋值给组件并将虚拟dom的属性赋值给组件实例 --> 将组件暴露全局 --> 处理setup函数传入参数并拿到setup返回的参数赋值给组件实例 --> 调用effect方法执行render函数 --> 分别在组件是否挂载的patch方法调用前后生命周期函数 --> 在生命周期函数调后前将生命周期函数赋值给组件实例(件实例上的生命周期是数组吗,值是创作者传入的回调函数) -->调用组件实例上的回调函数-->组件渲染或更新完毕将组件挂载