vue3初始化渲染流程
1.初始化渲染流程流程图
2.理解runtime-dom和runtime-core核心模块的区别
理解vue3的初始化渲染过程首先要弄清楚vue3非常重要的两个核心模块runtime-dom和runtime-core。 runtime-dom名字中有dom, 明显可以看出runtime-dom是和平台相关的操作,所以和平台操作相关的东西全部封装在runtime-dom模块中,而runtime-core是底层渲染逻辑,不涉及专属某个平台的操作,各平台相关的操作方法定义好提供给runtime-core模块使用来进行渲染
3.总体描述初始化渲染流程
-
3.1 应用程序初始化并执行mount挂载, 当用户调用createApp创建app时,进行函数的一系列初始化定义,并返回app中是方法集合包括mount方法
-
3.2 用户调用mount方法挂载,mount内部调用 createVNode创建vnode,并调用baseCreateRenderer中定义的render函数进行渲染
-
3.3 render函数调用patch传入新老虚拟dom和容器container
-
3.4 patch内部进行节点类型区分处理逻辑 如果节点是元素节点调用processElement处理, 如果是组件节点调用processComponent处理, 初始挂载应该是组件节点
-
3.5 processComponent内判别旧的虚拟dom即n1是否存在,不存在执行mountComponent挂载组件,存在即为组件 updateComponent更新组件逻辑
-
3.6 mountComponent内逻辑
- 1.执行createComponentIstance生成组件实例instance
- 2.执行setUpComponent设置props emits等 并 执行setup函数,并存储setup函数的结果
-
- 执行setUpComponentEffect触发effect副作用函数渲染界面
-
3.7 setUpComponentEffect函数内根据组件是否挂载区分逻辑
-
- 组件未挂载过,执行instance的render函数获取渲染树,patch第一参数传入null标识初始挂载
-
- 组件已挂载过,获取到新老dom渲染子树,用patch对新老dom对比
-
-
3.8 processElement函数调用
-
- 不存在老的虚拟dom即n1==null 执行mountElement创建元素和其子元素,并将元素挂载,挂载完界面显示内容
-
- n1!==null,执行updateElement更新元素,并进行页面内容渲染
-
4.代码实现
1.用户调用的createApp及生成渲染器函数
export function createApp (rootComponent) {
return ensureRenderer(rootComponent);
}
// 基础dom操作,创建元素,插入元素,移除元素等
export const nodeOps = {
// 创建元素
createElement (tag) {
return document.createElement(tag);
},
// 插入元素
insert (child, parent, ancher = null) {
parent.insertBefore(child, ancher);
},
// 移除元素
remove (child) {
const parent = child.parentNode;
if (parent) {
parent.removeChild(child)
}
},
// 设置文本内容
setElementText (el, text) {
el.textContent = text;
}
}
// 处理元素的class操作
function patchClass (el, val) {
if (val == null) {
val = "";
}
el.className = val;
}
// 处理元素的央视操作
function patchStyle (el, oldVal, newVal) {
for (let key in newVal) {
el.style[key] = newVal[key];
}
for (let key in oldVal) {
if (!(key in newVal)) {
el.style[key] = "";
}
}
}
// 处理元素的属性操作
function patchAttr (el, key, val) {
if (!val) {
el.removeAttribute(key);
} else {
el.setAttribute(key, val);
}
}
// 关于元素属性的操作,将属性、class和style区分处理
export function patchProps (el, key, oldVal = null, newVal = null) {
switch (key) {
case "class":
patchClass(el, newVal)
break;
case "style":
patchStyle(el, oldVal, newVal);
break;
default:
patchAttr(el, key, newVal);
break;
}
}
// 和dom操作相关的方法集合
let nodeOptions = { ...nodeOps, patchProps };
// 生成一个渲染器
function ensureRenderer (rootComponent) {
// createRenderer关于渲染的具体操作在runtime-core核心代码中
let app = createRenderer(nodeOptions).createApp(rootComponent);
const mount = app.mount;
// 重写mount 方法
app.mount = function (root) {
// 这是与浏览器平台相关的dom操作, 所以不适合在runtime-core
// 中操作, 所以在这里重新写
// root 请传选择器
const container = document.querySelector(root);
// 清空跟组件的原有内容
container.innerHTML = "";
// 利用runtime-core中的mount进行挂在操作, 传入跟组件和最外层容器
mount(container);
}
// 返回app
return app;
}
- 1.createApp函数传入组件配置,并调用ensureRenderer函数生成渲染器
- 2.ensureRenderer调用runtime-core中提供的createRenderer函数,createRenderer内部调用baseCreateRenderer(后续涉及说明其返回值及内部的各种方法)生成渲染器,并在此传入dom相关操作集合nodeOptions, 在渲染的过程中,会用提供的集合nodeOptions中的方法,创建、删除和更新界面相关内容(例如:创建dom插入元素,更新界面),执行调用createRenderer(nodeOptions)返回值的createApp返回app(一个对象包含mount方法)
- 3.缓存并重写mount方法,因为调用mount挂载时,我们需要操作和web平台相关的dom如获取根元素,挂载逻辑mount中不能涉及某个平台相关的操作,所以这里重写mount方法处理dom操作,同vue2中的mount重写思路差不多,vue2中重写mount方法是为了进行运行时没有render函数编译组件模板。
- 4.执行缓存的最初始mount方法进行挂载,渲染页面
2.调用baseCreateRenderer生成基础渲染器的过程
function baseCreateRenderer (options) {
// 对在runtime-dom中定义的dom操作方法引入并重命名
const {
createElement: hostCreateElement,
insert: hostInsert,
remove: hostRemove,
setElementText: hostSetElementText,
patchProps: hostPatchProps
} = options;
// 渲染函数
const render = (vnode, container) => {}
// path挂载
const patch = (n1, n2, container, ancher = null) => {}
// 处理元素节点
const processElement = (n1, n2, container, ancher) => {}
// 处理组件节点
const processComponent = (n1, n2, container) => {}
// 创建挂载元素
const mountElement = (n2, container, ancher) => {}
// 挂载子元素
const mountChildren = (children, container) => {}
// 进行dom对比, 更新元素
const updateElement = (n1, n2, container) => {}
// 对比子元素
const patchChildren = (n1, n2, container) => {}
// dom diff流程
const patchKeyChildren = (c1, c2, container) => {}
// 对比元素属性
const patchProps = (n1, n2, el) => {}
const isSameVnode = (n1, n2) => {
return n1.type === n2.type && n1.key === n2.key;}
const mountComponent = (vnode, container) => {}
const setUpComponentEffect = (instance, container) => {}
return {
render,
createApp: createAppApi(render)
}
}
- 1.baseCreateRenderer函数中包含了挂在过程中很多逻辑方法处理,包括渲染函数render、patch进行虚拟dom对比挂载、processElement处理元素节点、processComponent处理组件节点、mountElement创建挂载元素节点、patchKeyChildren进行dom比对的过程、执行setUpComponentEffect执行组件的渲染函数,渲染界面等
-
- 返回一些方法集合,其中包括给用户使用的createApp
3.调用createAppApi生成用户使用的createApp方法
export function createAppApi (render) {
const createApp = (rootComponnet) => {
const app = {
mount (root) {
const vnode = createVnode(rootComponnet);
render(vnode, root)
}
}
return app;
}
return createApp;
}
- 1.createAppApi函数中的createApp就是最终提供给用户使用创建app的,执行createApp的用户获得一个方法集合其中包括mount 方法, 用户执行了其中的mount方法就开始了初始化渲染逻辑的执行
- 2.mount通过createVnode创建虚拟dom 通过render函数渲染vnode
4.执行createVnode创建虚拟vnode
// 元素类型集合标识
export const shapFlags = {
ELEMENT: 1,
FUNCTIONAL_COMPONENT: 1 << 1,
STATEFUL_COMPONENT: 1 << 2,
ARRAY_CHILD: 1 << 3,
TEXT_CHILD: 1 << 4
}
// 创建虚拟dom
export function createVnode (type, props = {}, children = null) {
// 素类型是字符串shapFlag设置成1, 组件类型是对象shapFlag设置成2
let shapFlag = isString(type) ? shapFlags.ELEMENT : isObject(type) ? shapFlags.STATEFUL_COMPONENT : 0;
const vnode = {
type, // 类型 元素类型是字符串, 组件类型是对象
props, //属性
children, //子元素
component: null, //组件实例
key: props.key, //key
shapFlag
}
/*
因为shapFlags中的字段数据都是二进制1向前位移的值,
所以在二进制相同的位上不会存在都是1的情况
然后在这里用异或 操作两个二进制, 相当于数据相加
相加后的值即能代表父元素的类型也能知道子元素的类型
相当于 现在 元素节点是1, 子节点是数组为16
当 1 | 16 等于17
当下次判断时
17 & 1 是1 代表当前元素是元素节点, 当16 & 17 输出是16代表当前类型是数组
&相当于二进制当前位数相乘的结果
*/
// 如果子元素是数组
if (Array.isArray(children)) {
vnode.shapFlag = shapFlag | shapFlags.ARRAY_CHILD;
} else {
// 如果子元素是字符串或null
vnode.shapFlag = shapFlag | shapFlags.TEXT_CHILD;
}
// 返回虚拟节点
return vnode;
}
- 1.首先需要定义shapFlags对象标识各种类型节点,元素节点ELEMENT用1表示,函数式组件FUNCTIONAL_COMPONENT1向左移动1位即用2表示,有状态的组件STATEFUL_COMPONENT 1向左移动2位即用4表示,子节点是数组ARRAY_CHILD向左移动3位用8表示、子节点是文本TEXT_CHILD向左移动4位用16表示
- 2.根据当前传入的节点类型type得到标识shapFlag,type有两种类型:字符串和对象,当为字符串时就是dom节点, 当为对象时就是组件节点。例如初始挂载时createVnode传入的是用户调用createApp函数传入的根组件(假定根组件是有状态的组件),那么当前的shapFlag值就等于4。
- 3.定义vnode对象,其中包括标识节点类型的type属性, 节点属性props、子节点children、key值和shapFlag
- 4.处理shapFlag,用来标识当前节点和子节点的类型,简化类型判断时的处理
- 5.返回vnode对象
5.执行render函数进行渲染逻辑
// 渲染函数
const render = (vnode, container) => {
// 用patch挂载组件或元素, 因为是第一次挂载所以旧的虚拟dom是null
patch(null, vnode, container);
}
const patch = (n1, n2, container, ancher = null) => {
// 这里要用shapFlags区分是处理元素节点还是处理组件
let { shapFlag } = n2;
if (shapFlag & shapFlags.ELEMENT) {
// 处理元素节点
processElement(n1, n2, container, ancher);
} else if (shapFlag & shapFlags.STATEFUL_COMPONENT) {
// 处理组件节点
processComponent(n1, n2, container);
}
}
-
- 调用patch方法,因为是初始挂载,所以旧的vnode不存在,第一个参数直接传入null,第二个参数为当前的vnode,第三个参数container为用户传入的根元素
-
- patch内部对元素类型分类处理(这里只对元素节点和组件节点进行处理,源码中处理的类型很多,请自行查看), 取出shapFlag标识,如果是元素节点就用processElement方法处理, 组件节点用processComponent处理
这里的类型判断以有状态组件并且子元素为数组为例,所以shapFlag就是12 二进制位0000 1100,而shapFlags.STATEFUL_COMPONENT二进制位0000 0100,所以两者相与值为0000 0100也就是4,就会执行processComponent。如果当shapFlag为1时,二进制为0000 0001与shapFlags.STATEFUL_COMPONENT相与值为0 所以就不会进入processComponent的处理逻辑
6.执行processComponent函数处理组件节点
const processComponent = (n1, n2, container) => {
// 根据n1是否为null 判断是挂载组件,还是更新组件
if (n1 == null) {
mountComponent(n2, container)
} else {
updateComponent(n1, n2, container);
}
}
// 挂载组件
const mountComponent = (vnode, container) => {
// 第一步生成组件实例
const instance = vnode.instance = createComponentIstance(vnode);
// 第二部,设置props emits等 并 执行setup函数
setUpComponent(instance);
// 组件的effect函数, 形成响应式依赖, 数据改变,组件重新渲染
setUpComponentEffect(instance, container);
}
// 生成组件实例
export function createComponentIstance (vnode) {
const { setUp } = vnode.type;
return {
vnode,
setUp,
isMounted: false, // 标识组件是否挂载
setUpState: null, // setup函数的返回结果对象
render: null, // 组件的渲染函数
}
}
export function setUpComponent (instance) {
// 设置props
// 设置emits
// instance
// 执行setup函数
const { setUp } = instance;
if (setUp) {
const setUpResult = setUp();
if (isObject(setUpResult)) {
instance.setUpState = setUpResult;
} else {
instance.render = setUpResult;
}
}
// 完成setUp后的操作, 需要查看vnode是否有渲染函数, 和最后如果无渲染函数
// 需要执行模板编译过程
finishSetUpComponent(instance);
// 这里应该还有vue2 和vue3版本的兼容
}
// 存在render函数就存入instance实例中,不存在就进行模板编译
function finishSetUpComponent (instance) {
// 如果在vnode里写了专门的render函数, 此函数优先级高于setup中的
// 渲染函数优先级
const { render } = instance.vnode;
if (render) {
instance.render = render;
} else if (!instance.render) {
// 如果还是不存在render 函数,就需要进行模板编译过程
// compiler()
}
}
// 执行组件的effect副作用函数渲染组件
const setUpComponentEffect = (instance, container) => {
instance.update = effect(function componentEffect () {
if (!instance.isMounted) {
// 如果组件没有挂载, subTree 用于下次组件更新比对
const subTree = instance.subTree = instance.render();
// 标识组件已挂载
patch(null, subTree, container)
instance.isMounted = true;
} else {
const prev = instance.subTree;
const next = instance.render();
instance.subTree = next;
patch(prev, next, container)
}
}, { scheduler: queueJob }) // 更新时用queueJob将界面渲染添加为异步任务
}
-
- 当旧的虚拟dom n1不存在时执行挂载组件逻辑,n1存在执行更新组件逻辑
- 2.mountComponent方法中先执行createComponentIstance生成组件的实例,实例中包含vnode属性、setUp函数、是否已挂载标识isMounted、setUp函数返回的结果setUpState、render函数等。
- 3.执行setUpComponent设置组件实例props和emit等,获取用户的setUp函数区别处理。setUp的执行返回的结果是函数,那么函数就是渲染函数 存入instance.render中,如果不是函数用户返回的就是对象结果,存入instance.setUpState中,最后finishSetUpComponent函数处理render函数的优先级和模板编译流程
-
-
执行setUpComponentEffect进行组件渲染
- 1.引入响应式effect函数,传入处理函数为参数,并将副作用函数存入instance.update。
- 2.根据instance.isMounted判断组件是否已经挂载过,如果为false就是第一次挂载,执行render函数获取组件渲染树subTree,调用patch渲染,instance.isMounted = true 标识组件已挂载
- 3.如果instance.isMounted == true 表示是更新组件,先获取老的渲染树prev,再执行render函数获取新的渲染树next,调用patch进行组件更新
-
7.执行processElement函数处理元素节点
处理完组件节点调用patch最后会走到处理元素的逻辑
```javascript
const processElement = (n1, n2, container, ancher) => {
// 根据n1是否为null 判断是挂载元素节点,还是更新元素节点
if (n1 == null) {
mountElement(n2, container, ancher);
} else {
updateElement(n1, n2, container);
}
}
// 创建挂载元素
const mountElement = (n2, container, ancher) => {
const { type, children, props } = n2;
// 创建元素, 并将元素节点映射到虚拟dom中
const el = n2.el = hostCreateElement(type);
// 循环处理props
for (let key in props) {
hostPatchProps(el, key, null, props[key])
}
// 将元素插入父元素中
hostInsert(el, container, ancher);
// 处理子元素
if (!Array.isArray(children)) {
// 如果children不是数组, 那就是文本, 直接设置
hostSetElementText(el, children)
} else {
// children是数组, 需要循环处理
mountChildren(children, el)
}
}
// 挂载子元素
const mountChildren = (children, container) => {
for (let i = 0; i < children.length; i++) {
mountElement(children[i], container);
}
}
// 进行dom对比, 更新元素
const updateElement = (n1, n2, container) => {
// 1. n1,n2 是不是同一个节点
// 删除n1节点重置为null
const el = n2.el = n1.el;
if (n1 && !isSameVnode(n1, n2)) {
hostRemove(el);
n1 = null;
}
if (n1 == null) {
patch(null, n2, container)
} else {
// 元素一样, 先更新属性
patchProps(n1, n2, el);
// 然后对比子元素
patchChildren(n1, n2, el);
}
}
// 判断是否是同一节点
const isSameVnode = (n1, n2) => {
return n1.type === n2.type && n1.key === n2.key;
}
```
- 1.当旧的节点n1不存在时执行mountElement挂载元素
- 1.获取新dom的type, children, props属性
- 2.调用hostCreateElement创建节点
-
- 循环属性props集合调用hostPatchProps方法更新节点属性
-
- 调用hostInsert将节点插入父节点下
-
- 判断children类型,如果children不是数组,说明子元素是文本字符串,直接通过hostSetElementText创建文本即可,当children是数组调用mountChildren,对数组中的每一个元素执行mountElement进行创建挂载
- 2.当旧的节点n1存在时执行updateElement更新元素
-
- 获取当前元素节点el
- 2.根据标签名和key判断当前元素是不是同一个,如果不是同一个节点元素,就需要将老的节点从父节点下删除掉,并将你 设置为null
-
- 当n1为null的时, 说明新老元素不一样并且老元素已经从dom中删除掉了, 只要将新的节点创建并插入进父节点即可
-
- 当n1 存在时,复用元素节点,用patchProps更新节点的属性,调用patchChildren对比其子元素
-
8.patchChildren对比节点子元素
// 对比子元素
const patchChildren = (n1, n2, container) => {
const c1 = n1.children;
const c2 = n2.children;
const prveShapFlag = n1.shapFlag;
const nextShapFlag = n2.shapFlag;
// 子元素的类型分为数组和字符串类型, 所以, 这有四种情况
// 1.c2 为字符串, c1为字符串
// 2.c2为字符串, c1 为数组
// 3. c2 为数组, c1为字符串
// 4. c2为数组, c1为数组
if (nextShapFlag & shapFlags.TEXT_CHILD) {
// 如果c2是字符串, 不管c1是字符串还是数组, 直接用 textContent设置新值即可
// 所以不用区分情况, 只是需要判别c1和c2都为字符串时 相等就不用更改
if (c1 !== c2) {
hostSetElementText(container, c2);
}
} else if (nextShapFlag & shapFlags.ARRAY_CHILD) {
if (prveShapFlag & shapFlags.ARRAY_CHILD) {
// c2 是数组 c1是数组, 最复杂的dom对比
patchKeyChildren(c1, c2, container)
}
if (prveShapFlag & shapFlags.TEXT_CHILD) {
// c1 是字符串, 先将字符串删除, 再循环挂在新元素
hostSetElementText(container, "");
for (let i = 0; i < c2.length; i++) {
// 将每个新子元素挂载
patch(null, c2[i], container);
}
}
}
}
- 1.获取到新老节点shapFlag标识和子元素集合
- 2.分为两种情况处理子元素,一种是子元素children为字符串,另一种是子元素是一个数组集合
- 1.当前新节点元素的children为字符串类型,即表示子节点是文本节点, 当c1 !== c2前后文本节点不相同,直接更新文本即可
- 2.当前新节点元素的children为数组集合,需再判断老节点的子元素集合类型进行分类处理
- 1.老节点的children集合也是数组, 前后children都是数组就要进行最为复杂的diff操作
-
- 老节点的children为字符串类型说明子元素是文本,而新元素children是数组,只需用hostSetElementText将元素中的文本清空,循环新节点children,执行patch将每一个子元素挂载即可
git地址
代码链接 miniVue3文件夹为vue3的简单实现代码