◆
Vue3 源码:render 渲染与 diff 算法解析
- 下面 createApp 最终返回 app 这个对象,其中有一个 mount 方法,mount 方法接收一个参数,这个参数 就是 index.html 中的 id 为 #app 的节点,
- 里面会执行一个 render 方法,接收一个虚拟节点 vnode,和 #app 的节点,最后就是要在 render 中将 vnode 虚拟节点渲染在 rootContainer 上面
- 其中 createApp 中的 rootComponent 就是我们写的 vue 中的组件页面,createVNode(rootComponent) 就是要讲我们写的 vue 页面转换为虚拟节点
export function createAppAPI(render) {
return function createApp(rootComponent) {
const app = {
_component: rootComponent,
mount(rootContainer) {
console.log('基于根组件创建 vnode');
const vnode = createVNode(rootComponent);
console.log('调用 render,基于 vnode 进行开箱');
render(vnode, rootContainer);
},
};
return app;
};
}
createVNode 简单来讲就是将 vue-compoentns 中一些关键的属性存储在一个 js 对象中,从而后续操作 dom 都使用这个虚拟节点,而不去操作真实 dom
export const createVNode = function (
type: any,
props?: any,
children?: string | Array<any>,
) {
const vnode = {
type,
props: props || {},
children,
component: null,
key: props && props.key,
el: null,
shapeFlag: getShapeFlag(type),
};
// 基于 children 再次设置 shapeFlag
if (Array.isArray(children)) {
vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN;
} else if (typeof children === 'string') {
vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN;
}
normalizeChildren(vnode, children);
return vnode;
};
// 用 symbol 作为唯一标识
export const Text = Symbol('Text');
export const Fragment = Symbol('Fragment');
export function createTextVNode(text: string = ' ') {
return createVNode(Text, {}, text);
}
export function normalizeChildren(vnode, children) {
if (typeof children === 'object') {
// 暂时主要是为了标识出 slots_children 这个类型来
// 暂时我们只有 element 类型和 component 类型的组件
// 所以我们这里除了 element ,那么只要是 component 的话,那么children 肯定就是 slots 了
if (vnode.shapeFlag & ShapeFlags.ELEMENT) {
// 如果是 element 类型的话,那么 children 肯定不是 slots
} else {
// 这里就必然是 component 了,
vnode.shapeFlag |= ShapeFlags.SLOTS_CHILDREN;
}
}
}
// 基于 type 来判断是什么类型的组件
function getShapeFlag(type: any) {
return typeof type === 'string'
? ShapeFlags.ELEMENT
: ShapeFlags.STATEFUL_COMPONENT;
}
这里 createRenderer 是为了自定义创建、插入移除等节点方式的 api,以设配跨端情况
export function createRenderer(options) {
const {
createElement: hostCreateElement,
patchProp: hostPatchProp,
insert: hostInsert,
remove: hostRemove,
setElementText: hostSetElementText,
setText: hostSetText,
createText: hostCreateText,
} = options;
}
- patch 中需要判断节点类型 Text Fragment ELEMENT STATEFUL_COMPONENT
- Text -> el = document.createTextNode(text) 更新 对比新旧 vnode el 通过 node.nodeValue = text;
function render(vnode, container) {
// 首次传入的 container 就是 index.html 中定义的 root 节点
patch(null, vnode, container, null, null);
}
// 基于 vnode 类型进行不同的处理
function patch(
n1,
n2,
container = null,
anchor = null,
parentComponent = null,
) {
/**
* 原始的 App.js 是这样的,所以一开始的 vnode.type 就是下面这个对象
* {
* setup() {},
* render() {
* return h("div", { tId: 1 }, [h("p", {}, "主页"), h(HelloWorld)]);
* },
* }
* 所以一开始 处理的是组件类型
* 等处理完 App.js 的内容后,紧接着就会进入到 render 函数执行
* 也就是执行 h("div", { tId: 1 }, [h("p", {}, "主页"), h(HelloWorld)])
* 这时候 type 就是 string
*/
const { children, type, shapeFlag } = n2;
switch (type) {
case Text:
processText(n1, n2, container);
break;
// 其中还有几个类型比如: static fragment comment
case Fragment:
processFragment(n1, n2, container);
break;
default:
// if (typeof vnode.type === 'string') {
if (shapeFlag & ShapeFlags.ELEMENT) {
console.log('处理 element');
// 处理 element 类型
processElement(n1, n2, container, anchor, parentComponent);
}
// 虚拟节点的类型为对象时,代码节点是一个组件
// else if (isObject(vnode.type)) {
else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
// 处理组件
processComponent(n1, n2, container, parentComponent);
}
}
}
- ELEMENT mountElement updateElement
type div 创建 div 节点作为 el,判断 children,数组再次执行 patch,处理 props, on 开头则为事件,使用 listen 监听,属性使用 setAttribute 设置
// h("div", { tId: 1 }, [h("p", {}, "主页"), h(HelloWorld)]
function mountElement(vnode, container, anchor) {
// 创建这个类型 type 的元素节点容器
// vnode.el = document.createElement(vnode.type) 存储当前的 el 节点信息
const el = (vnode.el = hostCreateElement(vnode.type));
const { children, props, shapeFlag } = vnode;
// h("div", { class: 'test' }, '普通字符串')
// if (typeof children === 'string') {
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
el.textContent = children;
}
// h("div", { class: 'test' }, ['普通字符串', h("p", {}, "主页")])
// else if (isArray(children)) {
else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(vnode.children, el);
}
const isOn = key => /^on[A-Z]/.test(key);
// 给 el 设置 props 中的属性
for (let key in props) {
const val = props[key];
// if (isOn(key)) {
// const eventName = key.slice(2).toLowerCase();
// el.addEventListener(eventName, val);
// } else {
// el.setAttribute(key, val);
// }
hostPatchProp(el, key, null, val);
}
// 将生成的这个元素添加到 container 上
// container.append(el);
hostInsert(el, container, anchor);
}
function mountChildren(children, container) {
children.forEach(v => patch(null, v, container));
}
props 的更新,old new 都有但是 val 变了,old 又 new 没有
function patchProp(el, key, preValue, nextValue) {
// preValue 之前的值
// 为了之后 update 做准备的值
// nextValue 当前的值
console.log(`PatchProp 设置属性:${key} 值:${nextValue}`);
console.log(`key: ${key} 之前的值是:${preValue}`);
if (isOn(key)) {
// 添加事件处理函数的时候需要注意一下
// 1. 添加的和删除的必须是一个函数,不然的话 删除不掉
// 那么就需要把之前 add 的函数给存起来,后面删除的时候需要用到
// 2. nextValue 有可能是匿名函数,当对比发现不一样的时候也可以通过缓存的机制来避免注册多次
// 存储所有的事件函数
const invokers = el._vei || (el._vei = {});
const existingInvoker = invokers[key];
if (nextValue && existingInvoker) {
// patch
// 直接修改函数的值即可
existingInvoker.value = nextValue;
} else {
const eventName = key.slice(2).toLowerCase();
if (nextValue) {
const invoker = (invokers[key] = nextValue);
el.addEventListener(eventName, invoker);
} else {
el.removeEventListener(eventName, existingInvoker);
invokers[key] = undefined;
}
}
} else {
if (nextValue === null || nextValue === '') {
el.removeAttribute(key);
} else {
el.setAttribute(key, nextValue);
}
}
}
children 对比,text 节点不同直接替换即可 array
function patchChildren(n1, n2, container, anchor, parentComponent) {
const { shapeFlag: prevShapeFlag, children: c1 } = n1;
const { shapeFlag, children: c2 } = n2;
// 如果 n2 的 children 是 text 类型的话
// 就看看和之前的 n1 的 children 是不是一样的
// 如果不一样的话直接重新设置一下 text 即可
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
if (c2 !== c1) {
console.log('类型为 text_children, 当前需要更新');
hostSetElementText(container, c2 as string);
}
} else {
// 如果之前是 array_children
// 现在还是 array_children 的话
// 那么我们就需要对比两个 children 啦
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
patchKeyedChildren(c1, c2, container, anchor, parentComponent);
}
}
}
}
function patchKeyedChildren(
c1: any[],
c2: any[],
container,
parentAnchor,
parentComponent,
) {
let i = 0;
const l2 = c2.length;
let e1 = c1.length - 1;
let e2 = l2 - 1;
const isSameVNodeType = (n1, n2) => {
return n1.type === n2.type && n1.key === n2.key;
};
while (i <= e1 && i <= e2) {
const prevChild = c1[i];
const nextChild = c2[i];
if (!isSameVNodeType(prevChild, nextChild)) {
console.log('两个 child 不相等(从左往右比对)');
console.log(`prevChild:${prevChild}`);
console.log(`nextChild:${nextChild}`);
break;
}
console.log('两个 child 相等,接下来对比这两个 child 节点(从左往右比对)');
patch(prevChild, nextChild, container, parentAnchor, parentComponent);
i++;
}
while (i <= e1 && i <= e2) {
// 从右向左取值
const prevChild = c1[e1];
const nextChild = c2[e2];
if (!isSameVNodeType(prevChild, nextChild)) {
console.log('两个 child 不相等(从右往左比对)');
console.log(`prevChild:${prevChild}`);
console.log(`nextChild:${nextChild}`);
break;
}
console.log('两个 child 相等,接下来对比这两个 child 节点(从右往左比对)');
patch(prevChild, nextChild, container, parentAnchor, parentComponent);
e1--;
e2--;
}
if (i > e1 && i <= e2) {
// 如果是这种情况的话就说明 e2 也就是新节点的数量大于旧节点的数量
// 也就是说新增了 vnode
// 应该循环 c2
// 锚点的计算:新的节点有可能需要添加到尾部,也可能添加到头部,所以需要指定添加的问题
// 要添加的位置是当前的位置(e2 开始)+1
// 因为对于往左侧添加的话,应该获取到 c2 的第一个元素
// 所以我们需要从 e2 + 1 取到锚点的位置
const nextPos = e2 + 1;
const anchor = nextPos < l2 ? c2[nextPos].el : parentAnchor;
while (i <= e2) {
console.log(`需要新创建一个 vnode: ${c2[i].key}`);
patch(null, c2[i], container, anchor, parentComponent);
i++;
}
} else if (i > e2 && i <= e1) {
// 这种情况的话说明新节点的数量是小于旧节点的数量的
// 那么我们就需要把多余的
while (i <= e1) {
console.log(`需要删除当前的 vnode: ${c1[i].key}`);
hostRemove(c1[i].el);
i++;
}
} else {
// 左右两边都比对完了,然后剩下的就是中间部位顺序变动的
// 例如下面的情况
// a,b,[c,d,e],f,g
// a,b,[e,c,d],f,g
let s1 = i;
let s2 = i;
const keyToNewIndexMap = new Map();
let moved = false;
let maxNewIndexSoFar = 0;
// 先把 key 和 newIndex 绑定好,方便后续基于 key 找到 newIndex
// 时间复杂度是 O(1)
for (let i = s2; i <= e2; i++) {
const nextChild = c2[i];
keyToNewIndexMap.set(nextChild.key, i);
}
// 需要处理新节点的数量
const toBePatched = e2 - s2 + 1;
let patched = 0;
// 初始化 从新的index映射为老的index
// 创建数组的时候给定数组的长度,这个是性能最快的写法
const newIndexToOldIndexMap = new Array(toBePatched);
// 初始化为 0 , 后面处理的时候 如果发现是 0 的话,那么就说明新值在老的里面不存在
for (let i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0;
// 遍历老节点
// 1. 需要找出老节点有,而新节点没有的 -> 需要把这个节点删除掉
// 2. 新老节点都有的,—> 需要 patch
for (i = s1; i <= e1; i++) {
const prevChild = c1[i];
// 优化点
// 如果老的节点大于新节点的数量的话,那么这里在处理老节点的时候就直接删除即可
if (patched >= toBePatched) {
hostRemove(prevChild.el);
continue;
}
let newIndex;
if (prevChild.key != null) {
// 这里就可以通过key快速的查找了, 看看在新的里面这个节点存在不存在
// 时间复杂度O(1)
newIndex = keyToNewIndexMap.get(prevChild.key);
} else {
// 如果没key 的话,那么只能是遍历所有的新节点来确定当前节点存在不存在了
// 时间复杂度O(n)
for (let j = s2; j <= e2; j++) {
if (isSameVNodeType(prevChild, c2[j])) {
newIndex = j;
break;
}
}
}
// 因为有可能 nextIndex 的值为0(0也是正常值)
// 所以需要通过值是不是 undefined 或者 null 来判断
if (newIndex === undefined) {
// 当前节点的key 不存在于 newChildren 中,需要把当前节点给删除掉
hostRemove(prevChild.el);
} else {
// 新老节点都存在
console.log('新老节点都存在');
// 把新节点的索引和老的节点的索引建立映射关系
// i + 1 是因为 i 有可能是0 (0 的话会被认为新节点在老的节点中不存在)
newIndexToOldIndexMap[newIndex - s2] = i + 1;
// 来确定中间的节点是不是需要移动
// 新的 newIndex 如果一直是升序的话,那么就说明没有移动
// 所以我们可以记录最后一个节点在新的里面的索引,然后看看是不是升序
// 不是升序的话,我们就可以确定节点移动过了
if (newIndex >= maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex;
} else {
moved = true;
}
patch(prevChild, c2[newIndex], container, null, parentComponent);
patched++;
}
}
// 利用最长递增子序列来优化移动逻辑
// 因为元素是升序的话,那么这些元素就是不需要移动的
// 而我们就可以通过最长递增子序列来获取到升序的列表
// 在移动的时候我们去对比这个列表,如果对比上的话,就说明当前元素不需要移动
// 通过 moved 来进行优化,如果没有移动过的话 那么就不需要执行算法
// getSequence 返回的是 newIndexToOldIndexMap 的索引值
// 所以后面我们可以直接遍历索引值来处理,也就是直接使用 toBePatched 即可
const increasingNewIndexSequence = moved
? getSequence(newIndexToOldIndexMap)
: [];
let j = increasingNewIndexSequence.length - 1;
// 遍历新节点
// 1. 需要找出老节点没有,而新节点有的 -> 需要把这个节点创建
// 2. 最后需要移动一下位置,比如 [c,d,e] -> [e,c,d]
// 这里倒循环是因为在 insert 的时候,需要保证锚点是处理完的节点(也就是已经确定位置了)
// 因为 insert 逻辑是使用的 insertBefore()
for (let i = toBePatched - 1; i >= 0; i--) {
// 确定当前要处理的节点索引
const nextIndex = s2 + i;
const nextChild = c2[nextIndex];
// 锚点等于当前节点索引+1
// 也就是当前节点的后面一个节点(又因为是倒遍历,所以锚点是位置确定的节点)
const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : parentAnchor;
if (newIndexToOldIndexMap[i] === 0) {
// 说明新节点在老的里面不存在
// 需要创建
patch(null, nextChild, container, anchor, parentComponent);
} else if (moved) {
// 需要移动
// 1. j 已经没有了 说明剩下的都需要移动了
// 2. 最长子序列里面的值和当前的值匹配不上, 说明当前元素需要移动
if (j < 0 || increasingNewIndexSequence[j] !== i) {
// 移动的话使用 insert 即可
hostInsert(nextChild.el, container, anchor);
} else {
// 这里就是命中了 index 和 最长递增子序列的值
// 所以可以移动指针了
j--;
}
}
}
}
}
组件挂载
// 组件初始化
function mountComponent(vnode, container, parentComponent) {
// 先创建一个 component instance
const instance = (vnode.component = createComponentInstance(
vnode,
parentComponent,
));
console.log('当前组件实例:', instance);
// 这里会将 instance 中注入 setup 的结果 setupState 以及 App.js 的 render 函数
setupComponent(instance);
setupRenderEffect(instance, vnode, container, parentComponent);
}

还没有留言,来发表第一条吧~