前端提升7:手撸Vue3组件渲染
2025-04-12 23:21:56一、实现DOM操作API
1. 构建环境
创建runtime-dom目录,存放dom操作api
创建runtime-core目录,存放允许时虚拟dom操作核心代码
在两个目录中分别构建环境
/packages/runtime-dom/package.json
{
"name": "@vue/runtime-dom",
"module": "dist/runtime-dom.esm-bundler.js",
"unpkg": "dist/runtime-dom.global.js",
"buildOptions": {
"name": "VueRuntimeDOM",
"formats": [
"esm-bundler",
"cjs",
"global"
]
}
}
/packages/runtime-core/package.json
{
"name": "@vue/runtime-core",
"module": "dist/runtime-core.esm-bundler.js",
"unpkg": "dist/runtime-core.global.js",
"buildOptions": {
"name": "VueRuntimeCore",
"formats": [
"esm-bundler",
"cjs",
"global"
]
}
}
2. 定义操作API
/packages/runtime-dom/src/nodeOps.ts
// dom操作 vue虚拟dom, 通过数据对象在内存中对比差异, 找到最小的改动点,使用dom操作完成更新。
// 原生dom操作api
export const nodeOps = {
// 插入, 追加
insert: (child, parent, anchor = null) => {
parent.insertBefore(child, anchor); // parent.appendChild(child)
},
// 删除子节点
remove: child => {
const parent = child.parentNode;
if (parent) {
parent.removeChild(child);
}
},
// 创建元素
createElement: tag => document.createElement(tag),
// 创建文本标签
createText: text => document.createTextNode(text),
// 更新元素内容
setElementText: (el, text) => el.textContent = text,
// 更新文本
setText: (node, text) => node.nodeValue = text,
// 查询父节点
parentNode: node => node.parentNode,
// 查询后面的节点 div span jq写页面都是操作dom
nextSibling: node => node.nextSibling,
// dom查询
querySelector: selector => document.querySelector(selector)
}
3. 为dom操作实现补丁函数
/packages/runtime-dom/src/patchProp.ts
// // new
// <button mystyle={color:'red'; fontSize: '13px'} type="text" size="small" class="myclass" :style={color:'red'; fontSize: '13px'} onclick="aaa"></button>
// // old
// <button flag=2 mystyle={color:'red'; fontSize: '13px'} type="text" class="aclass" :style={color:"green";background:'white'} onclick="ccc"></button>
// // diff算法,1. 对比是根元素,2. 如果相同,对比根元素的属性 3. 对比子类,如果都有子类则需要diff
// 对比属性差异
export const patchProp = (el, key, prevValue, nextValue) => {
if (key === 'class') {
// 类名对比
patchClass(el, nextValue);
} else if (key === 'style') {
// 样式对比
patchStyle(el, prevValue, nextValue);
} else if (/^on[^a-z]/.test(key)) {
// onclick
// 1. 新绑定事件,直接绑定 2. 无绑定事件,删除绑定 removeEventListener 3. 换绑定
patchEvent(el, key, nextValue);
} else {
// 其他属性,自定义属性, setAttribute
patchAttr(el, key, nextValue);
}
}
// 对比属性,相同则替换,新的插入,旧的删除
function patchClass(el, value) {
if (value == null) {
el.removeAttribute('class'); // 删除
} else {
el.className = value; // 直接替换
}
}
// 样式对比 next--新元素 prev--旧元素
function patchStyle(el, prev, next) {
const oldStyle = el.style; // 获取样式
// 将新元素的所有样式直接拿过来
for (let key in next) {
// 直接覆盖旧样式
oldStyle[key] = next[key];
}
// 如果有老的样式,需要删除掉
if (prev) {
for (let key in prev) {
// 将旧元素的key去新元素中查找,如果没有则是需要删除的属性
if (next[key] == undefined) {
oldStyle[key] = undefined;
}
}
}
}
function createInvoker(value) {
// 调用代理函数
const invoker = (e)=>{
// 真实的调用函数
invoker.value(e);
}
invoker.value = value; // 存储这个变量,如果想换绑,可以直接更新value
return invoker;
}
// 事件处理 key=onclick
function patchEvent(el, key, nextValue) {
// 事件函数代理对象 vue event invoker vei
// 在元素上绑定一个自定义属性,用于记录绑定的事件 {click: fn, touch: fn, mousemove: fn}
const invokers = el._vei || (el._vei = {})
// 有没有已绑定过的事件函数
let existingInvoker = invokers[key];
// 之前有绑定且新绑定也有函数 换绑定
if (existingInvoker && nextValue) {
existingInvoker.value = nextValue;
} else {
const eventName = key.slice(2).toLowerCase(); // 事件名
if (nextValue) {
// 有新的绑定函数
const invoker = invokers[key] = createInvoker(nextValue);
el.addEventListener(eventName, invoker);
} else if (existingInvoker) {
// 如果新的元素无绑定事件,需要将旧的元素解绑定
el.removeEventListener(eventName, existingInvoker);
invokers[key] = undefined; // 清除缓存
}
}
}
// patch自定义属性 不确定自定义属性的格式,直接整个属性替换。
function patchAttr(el, key, value) {
if (value == null) {
// 新的没有该属性,老的有则删除
el.removeAttribute(key);
} else {
el.setAttribute(key, value); // 直接用新的属性覆盖
}
}
4. 导出createApp函数
- 在runtime-core中创建一个空渲染器实现
/packages/runtime-core/src/rendener.ts
// 渲染器
export function createRendener(renderOptions){
// TODO
}
/packages/runtime-core/src/index.ts
// 创建渲染器
export * from '@Vue/reactivity'
export {createRendener} from './rendener'
- 导出createApp函数
/packages/runtime-dom/src/index.ts
// dom操作的api, 属性操作的api, 这些api需要将导入到runtime-core中
// 将dom操作api与属性操作api融合
// dom操作api
import {nodeOps} from './nodeOps'
// 属性操作api
import {patchProp} from './patchProp'
// 从core中导入
import {createRendener} from '@vue/runtime-core'
// 整合api
const renderOptions = Object.assign(nodeOps, {patchProp}); // 包含所需要的所有api
// 渲染函数
export const createApp = (component, rootProps = null) => {
// 需要创建一个渲染器
const { createApp } = createRendener(renderOptions); // 从runtime-core中调用
let app = createApp(component, rootProps); //vue3实例对象
let { mount } = app; // 从app实例上解构mount
app.mount = function (container) { // 重写mount 包一层
container = nodeOps.querySelector(container); // 将 #app 转为真实dom对象
container.innerHTML = ''; // 清空原生dom内容, 再重新加载新内容
mount(container);// 将真实dom节点传入
}
return app;
}
// createApp -> createRendener -> createApp -> app -> mount
export * from '@vue/runtime-core'; // 导出core模块中所有代码
5. 调试测试
/packages/runtime-dom/dist/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>vue事件动态处理</title>
</head>
<body>
<button>Event点击事件</button>
<script>
const btn = document.querySelector('button');
// 调用函数
const invoker = ()=>{
invoker.value(); // 执行真正的事件函数
}
invoker.value = ()=>{
alert("点击事件1")
}
// 绑定事件
btn.addEventListener('click', invoker);
// 改变按钮的处理事件函数
setTimeout(()=>{
invoker.value = ()=>{
alert('点击事件2');
}
}, 2000)
</script>
</body>
</html>
二、初次渲染流程
1. 创建渲染对象的形状标识枚举
/packages/shared/src/index.ts
// 其他函数
// 渲染对象的形状标识 位运算方式实现
export const enum ShapeFlags {
ELEMENT = 1, // 元素类型
FUNCTIONAL_COMPONENT = 1 << 1, // 函数式组件2
STATEFUL_COMPONENT = 1 << 2, // 普通组件4
TEXT_CHILDREN = 1 << 3, // 孩子是文本8
ARRAY_CHILDREN = 1 << 4, // 孩子是数组16
SLOTS_CHILDREN = 1 << 5, // 组件是插槽
TELEPORT = 1 << 6, // teleport组件
SUSPENSE = 1 << 7, // suspense组件
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}
// 位 byte, 存储单位: byte 1b, 1kb, 1mb, 1gb
// 位运算:
// 1. 十进制转二进制 二进制转十进制
// 2. 运算法制:与 两位都为1结果为1,否则为0; 或运算, 相当于加法,只要有一个为1则为1
// 非运算:取反 原是0变1 是1变0 异或运算:两位不相同则为1,相同则为0
// 左移运算:将数值向左移动若干位,
2. createVNode函数
/packages/runtime-core/src/createVNode.ts
import { ShapeFlags, isObject, isString } from "@vue/shared";
// type-类型 props-属性 子集
export function createVNode(type, props, children = null) {
// 虚拟节点:用一个对象来描述dom信息
// 文本,元素,对象 不同的标识来区分它们
// let flag = 1; // 文本 flag = 2; //元素 位运算很简单的区分不同的类型
// 位运算确定元素类别
const shapeFlag = isObject(type) ?
ShapeFlags.COMPONENT :
isString(type) ?
ShapeFlags.ELEMENT : 0
// vnode
const vnode = {
__v_isVNode: true, // 虚拟dom标识
type,
shapeFlag,
props,
children,
key: props && props.key, // key for diff
component: null, // 如果是组件的虚拟节点要保存组件的实例
el: null, // 虚拟节点对应的真实节点
}
if(children) {
// 告诉节点,是什么形状 shape flag
// 渲染虚拟节点的时候,可以判断子是数组还是其它,如果是数组就要循环渲染
vnode.shapeFlag = vnode.shapeFlag | (isString(children) ? ShapeFlags.TEXT_CHILDREN : ShapeFlags.ARRAY_CHILDREN);
}
// vnode就是描述对象
return vnode;
}
// 判断是否是虚拟dom
export function isVNode(vnode) {
return !!vnode.__v_isVNode;
}
// 将文本转为虚拟节点
export const Text = Symbol(); // 定义字符串类型
export function normalizeVNode(vnode) {
if (isObject(vnode)) { // 对象忽略
return vnode;
}
return createVNode(Text, null, String(vnode));
}
// 判断两个元素是不是同一个类型
export function isSameVNodeType(n1, n2) {
// 比较类型是否一致, 比较key是否一致
return n1.type === n2.type && n1.key === n2.key;
}
3. createAppAPI函数
/packages/runtime-core/src/apiCreateApp.ts
import { createVNode } from "./createVNode";
export function createAppAPI(render) {
return (rootComponent, rootProps)=>{
// 判断当前是否已经被加载
let isMounted = false;
const app = {
// 加载组件
mount(container) {
// 创建组件虚拟节点
let vnode = createVNode(rootComponent, rootProps); // h函数
// 挂载的核心就是根据传入的组件对象,创造一个组件的虚拟节点,再将这个虚拟节点渲染到容器中
render(vnode, container);
// 判断是否已经挂载
if (!isMounted) {
isMounted = true;
}
}
}
return app;
}
}
4. 初步实现渲染器
/packages/runtime-core/src/rendener.ts
// 渲染器
import { ShapeFlags } from "@vue/shared";
import { createAppAPI } from "./apiCreateApp"
export function createRendener(renderOptions){
// 组件的加载
const mountComponent = (initialVNode, container) => {
// TODO 根据组件的虚拟节点,创造一个真实的节点,渲染到容器中。
console.log(initialVNode, container);
}
// 处理元素
const processElement = (n1, n2, container) => {
if (n1 == null) {
// 首次加载渲染 组件初始化
mountComponent(n2, container);
} else {
// 组件更新
}
}
// n1 -- old n2 -- newVnode
const patch = (n1, n2, container) => {
if (n1 == n2) return; // 相同的元素直接退出
const { shapeFlag } = n2; // 拿到新节点的形状标识
// 1. 文本 与 组件 结果是0 2. 文本 与 函数组伯 结果是0 3. 组件 与 组件 有值
if (shapeFlag & ShapeFlags.COMPONENT) {
// 处理元素
processElement(n1, n2, container);
}
}
const render = (vnode, container) => {
//将虚拟节点转化成真实节点渲染到容器中
// patch 包含初次渲染, 如果有更新也会走patch
patch(null, vnode, container);
}
return {
createApp: createAppAPI(render),
render
}
}
三、组件实现
1. 实现组件模型
/packages/runtime-core/src/component.ts
// 组件需要实现响应式,因此要引用响应式模块
import { reactive } from "@vue/reactivity";
import { hasOwn, isFunction, isObject } from "@vue/shared";
export function createComponentInstance(vnode) {
const type = vnode.type;
const instance = {
vnode, // 实例对应的虚拟节点
type, // 组件对象
subTree: null, // 组件渲染的内容 组件的子集
ctx: {}, // 组件上下文
props: {}, // 组件属性,from parent
attrs: {}, // 除了props之外的属性
slots: {}, // 组件的插槽
setupState: {}, // setup返回的结果
propsOptions: type.props, // 属性选项
proxy: null, // 实例的代理对象
render: null, // 组件的渲染函数
emit: null, // 事件触发
exposed: {}, // 暴露的方法
isMounted: false, // 是否挂载完成
}
instance.ctx = {_: instance}; // 上下文
return instance;
}
// 组件props初始化处理
export function initProps(instance, rawProps) {
const props = {}
const attrs = {}
const options = Object.keys(instance.propsOptions); // 用户注册过的属性
if (rawProps) {
for (let key in rawProps) {
const value = rawProps[key];
if (options.includes(key)) {
props[key] = value;
} else {
attrs[key] = value;
}
}
}
// 响应式
instance.props = reactive(props);
instance.attrs = attrs;
}
// 上下文,将内部的方法或属性暴露
function createSetupContext(instance) {
return {
attrs: instance.attrs,
slots: instance.slots,
emit: instance.emit,
expose: (exposed) => instance.exposed || {}
}
}
// 响应式get set拦截
const PublicInstanceProxyHandlers = {
get({_:instance}, key){
const {setupState, props} = instance;
// 判断setup结果中是否拥有属性key
if (hasOwn(setupState, key)) {
return setupState[key]
} else if (hasOwn(props, key)) {
return props[key];
} else {
// ....
}
},
set({_:instance}, key, value){
const {setupState, props} = instance; // 属性不能修改
if (hasOwn(setupState, key)) {
setupState[key] = value;
} else if (hasOwn(props, key)) {
// props 不能修改
console.log("************PROPS IS READONLY************")
return false;
} else {
// ...
}
return true;
}
}
// 调用组件的setup方法
export function setupStatefulComponent(instance) {
const Component = instance.type;
const { setup } = Component;
// 组件代理
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers);
if (setup) {
// setup 上下文 context
const setupContext = createSetupContext(instance);
let setupResult = setup(instance.props, setupContext); // 获取setup的返回值
// 返回结果可能是函数,也可能是对象
if (isFunction(setupResult)) {
instance.render = setupResult; // 如果setup返回的是函数,那就是render函数
} else if (isObject(setupResult)) {
instance.setupState = setupResult; // 暂存setup返回结果
}
}
// 组件中setup外添加render
if (!instance.render) {
// 如果没有render, 而写的是template, 可能要模板编译, 再调用render
// 如果setup没有返回render, 那就采用组件本身的render函数。
instance.render = Component.render;
}
console.log("instance:", instance)
console.log("proxy:", instance.proxy.count)
// instance.proxy.title = 111
}
// 组件的实例赋值
export function setupComponent(instance) {
const {props, children} = instance.vnode;
// 组件的props初始化, atts也要初始化
initProps(instance, props);
// setup调用
setupStatefulComponent(instance); // 调用setup函数,拿到返回值
}
// 父子组件, 子接收父的参数 props, 子里面还有其它属性 放attrs
// 同名, props: {name} attrs:{name}
2. 实现渲染虚拟函数
/packages/runtime-core/src/h.ts
// 渲染虚拟函数
import { isObject } from "@vue/shared";
import { createVNode, isVNode } from "./createVNode";
// type-节点类别 propsOrChildren-节点属性 children--节点的子集
export function h(type, propsOrChildren, children) {
// 1. h('div', {title: xxxx}) // <div title="xxxxx">
// 2. h('div', h('span')) // <div><span></span></div> children
// 3. h('div', 'hello') // <div>hello</div>
// 4. h('div', ['hello', 'hello'])
// 5. h('div',{}, "child") ===> h('div', {}, ["child"])
// 6. h('div', {}, 'child','child') ==> h('div', {}, ['child','child'])
// 参数的长度
let len = arguments.length;
if (len === 2) {
//
if (isObject(propsOrChildren) && !Array.isArray(propsOrChildren)) {
if (isVNode(propsOrChildren)) {
// 如果是虚拟dom, 需要处理children的shapeflag
return createVNode(type, null, [propsOrChildren]); // h('div', h('span'))
}
// 子是文本
return createVNode(type, propsOrChildren); //h('div', {title: xxxx})
} else {
// 不是对象 或 数组
return createVNode(type, null, propsOrChildren); // h('div', 'hello') h('div', ['hello', 'hello'])
}
} else {
if (len > 3) {
// 从第二个参数开始截取
children = Array.prototype.slice.call(arguments, 2);
} else if (len === 3 && isVNode(children)) {
children = [children]
}
return createVNode(type, propsOrChildren, children);
}
}
3. 组件实例挂载
/packages/runtime-core/src/rendener.ts
import { createComponentInstance, setupComponent } from "./component";
export function createRendener(renderOptions){
// 组件的加载
const mountComponent = (initialVNode, container) => {
// TODO 根据组件的虚拟节点,创造一个真实的节点,渲染到容器中。
console.log(initialVNode, container);
// 根据组件的虚拟节点,创造一个真实节点,渲染到容器中
// 1.要给组件创造一个组件的实例
debugger
const instance = initialVNode.component = createComponentInstance(initialVNode);
// 2. 需要给组件的实例进行赋值操作
setupComponent(instance);
}
// 处理元素
const processElement = (n1, n2, container) => {
if (n1 == null) {
// 首次加载渲染 组件初始化
mountComponent(n2, container);
} else {
// 组件更新
}
}
//其他代码
}
4. 导出函数
/packages/runtime-core/src/index.ts
export { h } from './h'
export * from '@vue/reactivity'
export {createRendener} from './rendener'
调试测试
/packages/runtime-dom/dist/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script src="./runtime-dom.global.js"></script>
<script>
let {createApp, h, ref} = VueRuntimeDOM;
console.log(ref,h)
function useCounter(){
const count = ref(0);
const add = ()=>{
count.value++;
}
return {count, add}
}
// 单向传递 父---->子 如果要改props,应该子---edmit----父---udpate
let App = {
props: {
title: {}
},
setup(props, ctx) {
let {count, add} = useCounter();
return {
add,
count
}
// return ()=>{
// return h('h1', {onClick: add}, 'rendener- ' + count.value);
// }
},
// 每次更新重新调用render方法
render(proxy) {
return h('h1', { onClick: this.add, title: proxy.title}, 'hello ' + this.count);
}
}
let app = createApp(App, {title: "vue3-rendener", v: "test_v", cc: "cc"});
app.mount("#app");
</script>
</body>
</html>
5. 组件渲染
/packages/runtime-core/src/rendener.ts
// 渲染器
import { ShapeFlags } from "@vue/shared";
import { createAppAPI } from "./apiCreateApp"
import { createComponentInstance, setupComponent } from "./component";
import { ReactiveEffect } from "packages/reactivity/src/effect";
import { Text, isSameVNodeType, isVNode, normalizeVNode } from "./createVNode";
export function createRendener(renderOptions){
// dom操作
const {
insert: hostInsert,
remove: hostRemove,
patchProp: hostPatchProp,
createElement: hostCreateElement,
createText: hostCreateText,
setElementText: hostSetElementText,
setText: hostSetText,
parentNode: hostParentNode,
nextSibling: hostNextSibling,
} = renderOptions;
const setupRenderEffect = (initialVNode, instance, container)=>{
let { proxy } = instance;
// 创建渲染effect, 数据变化,重新调用render
const componentUpdateFn = ()=>{
debugger
// 判断是否已经加载
if (!instance.isMounted) {
// 未加载 直接渲染 不用对比
// 调用render方法,拿到渲染的结果 第一个proxy是作用域, 第二个是参数
const subTree = instance.subTree = instance.render.call(proxy, proxy);
// 渲染子级元素,递归调用
patch(null, subTree, container);
instance.isMounted = true; // 已加载,已渲染到页面上 应该有对应dom
initialVNode.el = subTree.el
} else {
// 已加载
console.log("组件更新")
// 拿到新旧节点
const prevTree = instance.subTree;
const nextTree = instance.render.call(proxy, proxy);
patch(prevTree, nextTree, container);
}
}
const effect = new ReactiveEffect(componentUpdateFn);
// 默认调用update方法, 即执行componentUpdateFn
const update = effect.run.bind(effect);
update();
}
// 组件的加载
const mountComponent = (initialVNode, container) => {
// TODO 根据组件的虚拟节点,创造一个真实的节点,渲染到容器中。
console.log(initialVNode, container);
// 根据组件的虚拟节点,创造一个真实节点,渲染到容器中
// 1.要给组件创造一个组件的实例
const instance = initialVNode.component = createComponentInstance(initialVNode);
// 2. 需要给组件的实例进行赋值操作
setupComponent(instance);
// 3. 调用render方法实现组件的渲染逻辑, 如果依赖的状态发生变化,组件要重新渲染
// 数据和视图的双向绑定,如果数据变化视图要更新, 响应式原理
setupRenderEffect(initialVNode, instance, container); // 渲染effect
}
// 处理组件
const processComponent = (n1, n2, container) => {
if (n1 == null) {
// 组件初始化
mountComponent(n2, container);
} else {
// 组件更新
}
}
const mountChildren = (children, container) => {
// ['文本', '文本2'] 多个文本, 需要创建多个文本的节点 加入父元素中
// <div><span>文本1</span><span>文本2</span></div>
for (let i=0; i<children.length; i++) {
// 创建虚拟节点
const child = (children[i] = normalizeVNode(children[i]));
patch(null, child, container);
}
}
// 加载元素
const mountElement = (vnode, container, anchor) => {
// vnode中的children 可能是字符串 数组 对象数组 字符串数组
// 虚拟dom的类型, 属性, 儿子的形状
let { type, props, shapeFlag, children } = vnode;
// 根据type创建元素
let el = vnode.el = hostCreateElement(type);
// 根据元素的形状处理 字符串 数组
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {// 字符串的子节点 直接替换内容 el.textContent=xxx
hostSetElementText(el, children);
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(children, el); // 数组
}
// 处理样式
if (props) {
for (const key in props) {
// 给元素添加属性
hostPatchProp(el, key, null, props[key]);
}
}
// 插入
hostInsert(el, container, anchor);
}
const patchProps = (oldProps, newProps, el) => {
if (oldProps == newProps) return;
//新的属性要添加或更新到节点上
for (let key in newProps) {
const prev = oldProps[key]; //旧的属性
const next = newProps[key]; //新的属性
if (prev !== next) {
hostPatchProp(el, key, prev, next);
}
}
//旧的属性要从节点上删除
for (let key in oldProps) {
if (!(key in newProps)) {
hostPatchProp(el, key, oldProps[key], null);
}
}
}
// 处理元素
const processElement = (n1, n2, container, anchor) => {
if (n1 == null) {
// 首次加载渲染 组件初始化
mountElement(n2, container, anchor);
} else {
// 组件更新
}
}
// 处理文本
const processText = (n1, n2, container) => {
if (n1 == null) {
// 文本初始化
let textNode = hostCreateText(n2.children);
hostInsert(textNode, container);
// 要让虚拟节点和真实节点挂载上
n2.el = textNode;
}
}
const unmount = (vnode) =>{
hostRemove(vnode.el); // 删除真实节点
}
// n1 -- old n2 -- newVnode
const patch = (n1, n2, container, anchor = null) => {
// 新旧元素不相同则卸载元素
if (n1 && isSameVNodeType(n1, n2)) {
// 卸载旧元素
unmount(n1);
n1 = null;
}
if (n1 == n2) return; // 相同的元素直接退出
const { shapeFlag, type } = n2; // 拿到新节点的形状标识
switch(type) {
case Text:
processText(n1, n2, container);
break;
default:
if (shapeFlag & ShapeFlags.COMPONENT) {
// 处理元素
processComponent(n1, n2, container);
} else if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(n1, n2, container, anchor)
}
}
// 1. 文本 与 组件 结果是0 2. 文本 与 函数组伯 结果是0 3. 组件 与 组件 有值
}
const render = (vnode, container) => {
//将虚拟节点转化成真实节点渲染到容器中
// patch 包含初次渲染, 如果有更新也会走patch
patch(null, vnode, container);
}
return {
createApp: createAppAPI(render),
render
}
}
调试测试
/packages/runtime-dom/dist/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script src="./runtime-dom.global.js"></script>
<script>
let {createApp, h, ref} = VueRuntimeDOM;
console.log(ref,h)
function useCounter(){
const count = ref(0);
let flag = ref(true);
const add = ()=>{
count.value++;
}
return {count, add, flag}
}
// 单向传递 父---->子 如果要改props,应该子---edmit----父---udpate
let App = {
props: {
title: {}
},
setup(props, ctx) {
let {count, add, flag} = useCounter();
setTimeout(()=>{
flag.value = !flag.value;
console.log("flag:", flag.value)
}, 3000)
return {
add,
count,
flag
}
// return ()=>{
// return h('h1', {onClick: add}, 'rendener- ' + count.value);
// }
},
// 每次更新重新调用render方法
// render(proxy) {
// return this.flag.value ?
// h('h1', { onClick: this.add, title: proxy.title, style:{color: 'red'}}, h('span', ['aaa',h('a', 'bbbb')]), this.count.value):
// h('h1', { onClick: this.add, title: proxy.title, style:{color: 'green'}}, h('span', ['aaa',h('a', 'cccc')]), this.count.value);
// }
render(proxy) {
return this.flag.value ?
h('div', {}, [
h('li', {key: 'a'}, 'a'),
h('li', {key: 'b'}, 'b'),
h('li', {key: 'c'}, 'c'),
h('li', {key: 'd'}, 'd'),
]):
h('div', {}, [
h('li', {key: 'a'}, 'a'),
h('li', {key: 'b'}, 'b'),
h('li', {key: 'c'}, 'c'),
])
}
}
let app = createApp(App, {title: "vue3-rendener", v: "test_v", cc: "cc"});
app.mount("#app");
</script>
</body>
</html>
四、diff算法
1. vue3 与 vue2 diff算法对比
Vue2的Diff算法可以比作"全量比较"的方式
- 双端比较算法:从新旧节点的两端开始比较(头头、尾尾、头尾、尾头),像在数组中查找差异的双指针方法。
- 就地更新策略:尽量复用相同类型的节点,通过移动节点位置而不是创建新节点。
- Key的重要性:有key时可以准确识别节点身份。没有key时会暴力比对,效率低。
- 问题点
- 总是进行全树比较,即使某些部分从未变化
- 移动DOM节点的操作有时不够高效
- 对静态内容也会进行不必要的比较
Vue3的改进可以比作"智能增量更新"
- 编译时优化:静态节点提升:像.NET中的常量,编译时标记,运行时跳过。静态树提升:整个静态子树只创建一次,后续复用。
- 块树(Block Tree)概念:模板被编译为"块",每个块知道自己的动态节点。像.NET中的partial class,只关注变化部分。
- 基于最长递增子序列的算法:最小化DOM操作次数,处理节点移动时更高效。
- 事件缓存:避免每次更新都创建新的事件处理函数,像.NET中缓存委托实例。
2. 举例
<!-- 之前 -->
<ul>
<li key="a">A</li>
<li key="b">B</li>
<li key="c">C</li>
</ul>
<!-- 之后 -->
<ul>
<li key="c">C</li>
<li key="a">A</li>
<li key="b">B</li>
</ul>
Vue2处理方式:
比较头头(a-c) → 不匹配
比较尾尾(c-b) → 不匹配
比较头尾(a-b) → 不匹配
比较尾头(c-a) → 不匹配
暴力遍历查找,然后移动节点
Vue3处理方式:
识别出key的顺序变化
使用最长递增子序列找出最优移动方案
只需移动C到最前面
3. 实现diff算法
export function createRendener(renderOptions){
// 其他代码
/**
* diff算法
* @param c1 老的儿子
* @param c2 新的儿子
* @param container
*/
const patchKeyedChildren = (c1, c2, container) => {
let e1 = c1.length - 1; // 老的数组长度 e==end
let e2 = c2.length - 1; // 新的数组长度
let i = 0; // 开始比较计数指针 i左指针
// 1. sync from start 从头开始比较,遇到不同的节点就停止比较
while(i <= e1 && i <= e2) {
const n1 = c1[i]; // 取出一个老节点
const n2 = c2[i]; // 取出一个新节点
// 如果两个节点是相同节点,则需要递归比较孩子和自身的属性
if (isSameVNodeType(n1, n2)) {
patch(n1, n2, container); // 对比属性和子集
} else {
break; // 新老节点不相同,停止比较
}
i++;
}
// 2. sync from end 从后往前遍历
while(i <= e1 && i <= e2) { // 如果i与新的数组或老数组长度重合,说明比较结束了
const n1 = c1[e1]; // 取右边的老节点
const n2 = c2[e2]; // 取右边的新节点
if (isSameVNodeType(n1, n2)) {
patch(n1, n2, container);
} else {
break;
}
e1--;
e2--; // 从右向左移动指针
}
// 3. common sequence + mount
// 对比索引大小,处理新增或删除节点
if (i > e1) {
if (i <= e2) {
// insert---需要增加的元素
const nextPos = e2 + 1; // 下一个元素索引位置
// 取e2的下一个元素,如果没有,则长度和当前c2长度相同, 说明是追加
// 如果下一个元素有,说明要插入在下一个元素的前面,将下一个元素作为参照物
const anchor = nextPos < c2.length ? c2[nextPos].el : null;
// 参照物作用是判断是向前插入还是向后插入
while(i <= e2) {
patch(null, c2[i], container, anchor);
i++;
}
}
} else if (i > e2) {// 4 common sequence + unmount
// 存在被删除节点
// 1. 左侧被删除, 2. 中间被删除, 3. 右铡被删除
while(i <= e1) {
// i 与 e1 之间的就是要删除的
unmount(c1[i]);
i++;
}
}
// 5. unknown sequence 未知序列
const s1 = i; // 老节点的开始位置
const s2 = i; // 新节点的结束位置
// 根据新的节点,创造一个映射表,用老的列表去里面找有没有,如果有则复用,没有就删除。
const keyToNewIndexMap = new Map(); //可以用老的节点来查看有没有新的
for (let i = s2; i<=e2; i++) {
const child = c2[i]; //取出新的节点
keyToNewIndexMap.set(child.key, i); //将新元素的位置记录下来
}
// 需要比较的新元素数量
const toBepatched = e2 - s2 + 1;
// 创建一个要比较的数组, 长度为toBepatched,默认值为0
const newIndexToOldMapIndex = new Array(toBepatched).fill(0);
// 遍历老节点,找到一样的需要patch属性和孩子
for (let i = s1; i <= e1; i++) {
const prevChild = c1[i]; // 老节点
// 在新节点的映射表中查找是否存在
let newIndex = keyToNewIndexMap.get(prevChild.key);
if (newIndex == undefined) {
// 老节点在新的映射表中不存在,要删除
unmount(prevChild);
} else {
// 更新映射表
newIndexToOldMapIndex[newIndex - s2] = i + 1; // 保证不为0,0表示要增加元素
// 对比属性和孩子
patch(prevChild, c2[newIndex], container);
}
}
// 新节点比老节点要多,则需要遍历新节点 从右到左
for (let i = toBepatched - 1; i >= 0; i--) {
let lastIndex = s2 + i; //末尾元素索引
let lastChild = c2[lastIndex]; //末尾元素
// 参照节点
let anchor = lastIndex + 1 < c2.length ? c2[lastIndex + 1].el : null;
if (newIndexToOldMapIndex[i] == 0) {
// 为0表示还没有真实节点,需要创建真实节点并插入
patch(null, lastChild, container, anchor);
} else {
// 不为0表示已存在相应老节点,要移动元素
hostInsert(lastChild.el, container, anchor);
}
}
}
// 将所有儿子卸载
const unmountChildren = (children) => {
for (let i = 0; i<children.length; i++) {
unmount(children[i]);
}
}
// 比较儿子 n1--旧节点, n2--新节点
const patchChildren = (n1, n2, el) => {
const c1 = n1 && n1.children; // 老的儿子
const c2 = n2 && n2.children; // 新的儿子
const prevShapeFlag = n1.shapeFlag; // 老的形状标识
const shapeFlag = n2.shapeFlag; // 新的形状标识
// c1和c2类型
// 1. 老的是数组,新的是文本, 删除老的,添加新的
// 2. 老的是数组,新的是数组,比较它们的儿子---diff
// 3. 老的是文本,新的是空,直接删除老的
// 4. 老的是文本,新的是文本,直接更新文本
// 5. 老的是文本,新的数组,删除文本,新增数组
// 6. 老的是空, 新的是文本,直接添加
// 位运算判断
if (shapeFlag & ShapeFlags.TEXT_CHILDREN ) {// 新的是文本
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {// 老的是数组
unmountChildren(c1); // 1
}
if (c1 !== c2) {// 老的是文本 46
hostSetElementText(el, c2);
}
} else {
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {// 老的是数组
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {// 新的是数组
// diff
patchKeyedChildren(c1, c2, el); //2
} else {
// 新的不是数组
unmountChildren(c1);// 1
}
} else {
if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {// 老的是文本
hostSetElementText(el, c2); // 4
}
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {// 新的是数组,老的是文本
mountChildren(c2, el); //5
}
}
}
}
const patchElement = (n1, n2) => {
debugger
let el = n2.el = n1.el; // 先比较元素 元素是一致
const oldProps = n1.props || {}; // 旧元素的属性
const newProps = n2.props || {}; // 新元素的属性
// 比较元素的属性
patchProps(oldProps, newProps, el);
// 儿子比较
patchChildren(n1, n2, el);
}
// 处理元素
const processElement = (n1, n2, container, anchor) => {
if (n1 == null) {
// 首次加载渲染 组件初始化
mountElement(n2, container, anchor);
} else {
// 组件更新 比较2个元素之间的差异
patchElement(n1, n2);
}
}
// 其他代码
}
4. diff未知序列
export function createRendener(renderOptions){
// 其他代码
/**
* diff算法
* @param c1 老的儿子
* @param c2 新的儿子
* @param container
*/
const patchKeyedChildren = (c1, c2, container) => {
let e1 = c1.length - 1; // 老的数组长度 e==end
let e2 = c2.length - 1; // 新的数组长度
let i = 0; // 开始比较计数指针 i左指针
// 其他代码
// 5. unknown sequence 未知序列
const s1 = i; // 老节点的开始位置
const s2 = i; // 新节点的结束位置
// 根据新的节点,创造一个映射表,用老的列表去里面找有没有,如果有则复用,没有就删除。
const keyToNewIndexMap = new Map(); //可以用老的节点来查看有没有新的
for (let i = s2; i<=e2; i++) {
const child = c2[i]; //取出新的节点
keyToNewIndexMap.set(child.key, i); //将新元素的位置记录下来
}
// 需要比较的新元素数量
const toBepatched = e2 - s2 + 1;
// 创建一个要比较的数组, 长度为toBepatched,默认值为0
const newIndexToOldMapIndex = new Array(toBepatched).fill(0);
// 遍历老节点,找到一样的需要patch属性和孩子
for (let i = s1; i <= e1; i++) {
const prevChild = c1[i]; // 老节点
// 在新节点的映射表中查找是否存在
let newIndex = keyToNewIndexMap.get(prevChild.key);
if (newIndex == undefined) {
// 老节点在新的映射表中不存在,要删除
unmount(prevChild);
} else {
// 更新映射表
newIndexToOldMapIndex[newIndex - s2] = i + 1; // 保证不为0,0表示要增加元素
// 对比属性和孩子
patch(prevChild, c2[newIndex], container);
}
}
// 新节点比老节点要多,则需要遍历新节点 从右到左
for (let i = toBepatched - 1; i >= 0; i--) {
let lastIndex = s2 + i; //末尾元素索引
let lastChild = c2[lastIndex]; //末尾元素
// 参照节点
let anchor = lastIndex + 1 < c2.length ? c2[lastIndex + 1].el : null;
if (newIndexToOldMapIndex[i] == 0) {
// 为0表示还没有真实节点,需要创建真实节点并插入
patch(null, lastChild, container, anchor);
} else {
// 不为0表示已存在相应老节点,要移动元素
hostInsert(lastChild.el, container, anchor);
}
}
}
// 其他代码
}
5. 计算最长递增子序列
js算法
var seq = function (arr) {
let result = [arr[0]]; // 先存入第1个值
for (let i=1; i<arr.length; ++i){
// 如果当前数值大于已选结果的最后一位,则直接往后新增
// 若当前数组更小,则直接替换前面第一个大于它的数值
if (arr[i] > result[result.length - 1]) {
result[result.length] = arr[i];
} else {
// 二分查找: 找到第一个大于当前数值的结果进行替换
let left = 0, right = result.length - 1;
while (left < right) {
let middle = ((left + right) / 2) | 0;
if (result[middle] < arr[i]) {
left = middle + 1;
} else {
right = middle;
}
}
// 替换当前下标
result[left] = arr[i];
}
}
return result;
}
arr = [10,9,2,5,3,7,101,18]
console.log(seq(arr))
diff算法扩展最长递增子序列
// 其他代码
// 计算最长递增子序列 索引值
function getSequence(arr) {
let len = arr.length;
const result = [0]; // 索引值
let p = arr.slice(0); // 用来记录前驱节点的索引,回溯正确的顺序
let lastIndex;
let start;
let end;
let middle;
for (let i=0; i<len; i++) {
const arrI = arr[i]; // 存每一项的值
if (arrI !== 0) {
lastIndex = result[result.length - 1]; // 获取结果中最后一个
if (arr[lastIndex] < arrI) {
// 当前结果集中的最后一个 和这一顶比较
p[i] = lastIndex;
result.push(i);
continue;
}
// 二分查找 替换元素
start = 0;
end = result.length - 1;
while(start < end) {
middle = ((start + end) / 2) | 0; // 中间索引值
// 找到序列中间的索引值,通过索引找到对应的值
if (arr[result[middle]] < arrI) {
start = middle + 1;
} else {
end = middle;
}
}
if (arrI < arr[result[start]]) {// 要替换成3的索引
// 替换前,应该让当前元素的索引 标识到p上
p[i] = result[start - 1];
result[start] = i; // 贪心算法
}
}
}
let i = result.length; // 拿到最后一个,开始向前追溯, 都是基于索引值
let last = result[i - 1]; // 取最后一个
while(i-- > 0) {
// 通过前驱节点找到正确的调用顺序
result[i] = last; // 最后一项肯定是正确
// 从p中往前找
last = p[last];
}
return result;
}
export function createRendener(renderOptions){
// 其他代码
/**
* diff算法
* @param c1 老的儿子
* @param c2 新的儿子
* @param container
*/
const patchKeyedChildren = (c1, c2, container) => {
let e1 = c1.length - 1; // 老的数组长度 e==end
let e2 = c2.length - 1; // 新的数组长度
let i = 0; // 开始比较计数指针 i左指针
// 其他代码
// 需要移动的元素
let queue = getSequence(newIndexToOldMapIndex);
let j = queue.length - 1; // 拿到最长递增子序列的末尾索引
// 新节点比老节点要多,则需要遍历新节点 从右到左
for (let i = toBepatched - 1; i >= 0; i--) {
let lastIndex = s2 + i; //末尾元素索引
let lastChild = c2[lastIndex]; //末尾元素
// 参照节点
let anchor = lastIndex + 1 < c2.length ? c2[lastIndex + 1].el : null;
if (newIndexToOldMapIndex[i] == 0) {
// 为0表示还没有真实节点,需要创建真实节点并插入
patch(null, lastChild, container, anchor);
} else {
// 不为0表示已存在相应老节点,要移动元素
// 性能优化,最长递增子序列 可减少dom的操作
if (i !== queue[j]) {
// 移动元素
hostInsert(lastChild.el, container, anchor);
} else {
j--; // 不需要移动元素
}
}
}
}
// 其他代码
}
完整代码
/packages/runtime-core/src/rendener.ts
// 渲染器
import { ShapeFlags } from "@vue/shared";
import { createAppAPI } from "./apiCreateApp"
import { createComponentInstance, setupComponent } from "./component";
import { ReactiveEffect } from "packages/reactivity/src/effect";
import { Text, isSameVNodeType, isVNode, normalizeVNode } from "./createVNode";
// 计算最长递增子序列 索引值
function getSequence(arr) {
let len = arr.length;
const result = [0]; // 索引值
let p = arr.slice(0); // 用来记录前驱节点的索引,回溯正确的顺序
let lastIndex;
let start;
let end;
let middle;
for (let i=0; i<len; i++) {
const arrI = arr[i]; // 存每一项的值
if (arrI !== 0) {
lastIndex = result[result.length - 1]; // 获取结果中最后一个
if (arr[lastIndex] < arrI) {
// 当前结果集中的最后一个 和这一顶比较
p[i] = lastIndex;
result.push(i);
continue;
}
// 二分查找 替换元素
start = 0;
end = result.length - 1;
while(start < end) {
middle = ((start + end) / 2) | 0; // 中间索引值
// 找到序列中间的索引值,通过索引找到对应的值
if (arr[result[middle]] < arrI) {
start = middle + 1;
} else {
end = middle;
}
}
if (arrI < arr[result[start]]) {// 要替换成3的索引
// 替换前,应该让当前元素的索引 标识到p上
p[i] = result[start - 1];
result[start] = i; // 贪心算法
}
}
}
let i = result.length; // 拿到最后一个,开始向前追溯, 都是基于索引值
let last = result[i - 1]; // 取最后一个
while(i-- > 0) {
// 通过前驱节点找到正确的调用顺序
result[i] = last; // 最后一项肯定是正确
// 从p中往前找
last = p[last];
}
return result;
}
export function createRendener(renderOptions){
// dom操作
const {
insert: hostInsert,
remove: hostRemove,
patchProp: hostPatchProp,
createElement: hostCreateElement,
createText: hostCreateText,
setElementText: hostSetElementText,
setText: hostSetText,
parentNode: hostParentNode,
nextSibling: hostNextSibling,
} = renderOptions;
const setupRenderEffect = (initialVNode, instance, container)=>{
let { proxy } = instance;
// 创建渲染effect, 数据变化,重新调用render
const componentUpdateFn = ()=>{
// 判断是否已经加载
if (!instance.isMounted) {
// 未加载 直接渲染 不用对比
// 调用render方法,拿到渲染的结果 第一个proxy是作用域, 第二个是参数
const subTree = instance.subTree = instance.render.call(proxy, proxy);
// 渲染子级元素,递归调用
patch(null, subTree, container);
instance.isMounted = true; // 已加载,已渲染到页面上 应该有对应dom
initialVNode.el = subTree.el
} else {
// 已加载
console.log("组件更新")
// 拿到新旧节点
const prevTree = instance.subTree; // 老虚拟dom
const nextTree = instance.render.call(proxy, proxy); //新虚拟dom
patch(prevTree, nextTree, container);
}
}
const effect = new ReactiveEffect(componentUpdateFn);
// 默认调用update方法, 即执行componentUpdateFn
const update = effect.run.bind(effect);
update();
}
// 组件的加载
const mountComponent = (initialVNode, container) => {
// TODO 根据组件的虚拟节点,创造一个真实的节点,渲染到容器中。
console.log(initialVNode, container);
// 根据组件的虚拟节点,创造一个真实节点,渲染到容器中
// 1.要给组件创造一个组件的实例
const instance = initialVNode.component = createComponentInstance(initialVNode);
// 2. 需要给组件的实例进行赋值操作
setupComponent(instance);
// 3. 调用render方法实现组件的渲染逻辑, 如果依赖的状态发生变化,组件要重新渲染
// 数据和视图的双向绑定,如果数据变化视图要更新, 响应式原理
setupRenderEffect(initialVNode, instance, container); // 渲染effect
}
// 处理组件
const processComponent = (n1, n2, container) => {
if (n1 == null) {
// 组件初始化
mountComponent(n2, container);
} else {
// 组件更新
}
}
const mountChildren = (children, container) => {
// ['文本', '文本2'] 多个文本, 需要创建多个文本的节点 加入父元素中
// <div><span>文本1</span><span>文本2</span></div>
for (let i=0; i<children.length; i++) {
// 创建虚拟节点
const child = (children[i] = normalizeVNode(children[i]));
patch(null, child, container);
}
}
// 加载元素
const mountElement = (vnode, container, anchor) => {
// vnode中的children 可能是字符串 数组 对象数组 字符串数组
// 虚拟dom的类型, 属性, 儿子的形状
let { type, props, shapeFlag, children } = vnode;
// 根据type创建元素
let el = vnode.el = hostCreateElement(type);
// 根据元素的形状处理 字符串 数组
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {// 字符串的子节点 直接替换内容 el.textContent=xxx
hostSetElementText(el, children);
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(children, el); // 数组
}
// 处理样式
if (props) {
for (const key in props) {
// 给元素添加属性
hostPatchProp(el, key, null, props[key]);
}
}
// 插入
hostInsert(el, container, anchor);
}
const patchProps = (oldProps, newProps, el) => {
if (oldProps == newProps) return;
//新的属性要添加或更新到节点上
for (let key in newProps) {
const prev = oldProps[key]; //旧的属性
const next = newProps[key]; //新的属性
if (prev !== next) {
hostPatchProp(el, key, prev, next);
}
}
//旧的属性要从节点上删除
for (let key in oldProps) {
if (!(key in newProps)) {
hostPatchProp(el, key, oldProps[key], null);
}
}
}
/**
* diff算法
* @param c1 老的儿子
* @param c2 新的儿子
* @param container
*/
const patchKeyedChildren = (c1, c2, container) => {
let e1 = c1.length - 1; // 老的数组长度 e==end
let e2 = c2.length - 1; // 新的数组长度
let i = 0; // 开始比较计数指针 i左指针
// 1. sync from start 从头开始比较,遇到不同的节点就停止比较
while(i <= e1 && i <= e2) {
const n1 = c1[i]; // 取出一个老节点
const n2 = c2[i]; // 取出一个新节点
// 如果两个节点是相同节点,则需要递归比较孩子和自身的属性
if (isSameVNodeType(n1, n2)) {
patch(n1, n2, container); // 对比属性和子集
} else {
break; // 新老节点不相同,停止比较
}
i++;
}
// 2. sync from end 从后往前遍历
while(i <= e1 && i <= e2) { // 如果i与新的数组或老数组长度重合,说明比较结束了
const n1 = c1[e1]; // 取右边的老节点
const n2 = c2[e2]; // 取右边的新节点
if (isSameVNodeType(n1, n2)) {
patch(n1, n2, container);
} else {
break;
}
e1--;
e2--; // 从右向左移动指针
}
// 3. common sequence + mount
// 对比索引大小,处理新增或删除节点
if (i > e1) {
if (i <= e2) {
// insert---需要增加的元素
const nextPos = e2 + 1; // 下一个元素索引位置
// 取e2的下一个元素,如果没有,则长度和当前c2长度相同, 说明是追加
// 如果下一个元素有,说明要插入在下一个元素的前面,将下一个元素作为参照物
const anchor = nextPos < c2.length ? c2[nextPos].el : null;
// 参照物作用是判断是向前插入还是向后插入
while(i <= e2) {
patch(null, c2[i], container, anchor);
i++;
}
}
} else if (i > e2) {// 4 common sequence + unmount
// 存在被删除节点
// 1. 左侧被删除, 2. 中间被删除, 3. 右铡被删除
while(i <= e1) {
// i 与 e1 之间的就是要删除的
unmount(c1[i]);
i++;
}
}
// 5. unknown sequence 未知序列
const s1 = i; // 老节点的开始位置
const s2 = i; // 新节点的结束位置
// 根据新的节点,创造一个映射表,用老的列表去里面找有没有,如果有则复用,没有就删除。
const keyToNewIndexMap = new Map(); //可以用老的节点来查看有没有新的
for (let i = s2; i<=e2; i++) {
const child = c2[i]; //取出新的节点
keyToNewIndexMap.set(child.key, i); //将新元素的位置记录下来
}
// 需要比较的新元素数量
const toBepatched = e2 - s2 + 1;
// 创建一个要比较的数组, 长度为toBepatched,默认值为0
const newIndexToOldMapIndex = new Array(toBepatched).fill(0);
// 遍历老节点,找到一样的需要patch属性和孩子
for (let i = s1; i <= e1; i++) {
const prevChild = c1[i]; // 老节点
// 在新节点的映射表中查找是否存在
let newIndex = keyToNewIndexMap.get(prevChild.key);
if (newIndex == undefined) {
// 老节点在新的映射表中不存在,要删除
unmount(prevChild);
} else {
// 更新映射表
newIndexToOldMapIndex[newIndex - s2] = i + 1; // 保证不为0,0表示要增加元素
// 对比属性和孩子
patch(prevChild, c2[newIndex], container);
}
}
debugger
// 需要移动的元素
let queue = getSequence(newIndexToOldMapIndex);
let j = queue.length - 1; // 拿到最长递增子序列的末尾索引
// 新节点比老节点要多,则需要遍历新节点 从右到左
for (let i = toBepatched - 1; i >= 0; i--) {
let lastIndex = s2 + i; //末尾元素索引
let lastChild = c2[lastIndex]; //末尾元素
// 参照节点
let anchor = lastIndex + 1 < c2.length ? c2[lastIndex + 1].el : null;
if (newIndexToOldMapIndex[i] == 0) {
// 为0表示还没有真实节点,需要创建真实节点并插入
patch(null, lastChild, container, anchor);
} else {
// 不为0表示已存在相应老节点,要移动元素
// 性能优化,最长递增子序列 可减少dom的操作
if (i !== queue[j]) {
// 移动元素
hostInsert(lastChild.el, container, anchor);
} else {
j--; // 不需要移动元素
}
}
}
}
// 将所有儿子卸载
const unmountChildren = (children) => {
for (let i = 0; i<children.length; i++) {
unmount(children[i]);
}
}
// 比较儿子 n1--旧节点, n2--新节点
const patchChildren = (n1, n2, el) => {
const c1 = n1 && n1.children; // 老的儿子
const c2 = n2 && n2.children; // 新的儿子
const prevShapeFlag = n1.shapeFlag; // 老的形状标识
const shapeFlag = n2.shapeFlag; // 新的形状标识
// c1和c2类型
// 1. 老的是数组,新的是文本, 删除老的,添加新的
// 2. 老的是数组,新的是数组,比较它们的儿子---diff
// 3. 老的是文本,新的是空,直接删除老的
// 4. 老的是文本,新的是文本,直接更新文本
// 5. 老的是文本,新的数组,删除文本,新增数组
// 6. 老的是空, 新的是文本,直接添加
// 位运算判断
if (shapeFlag & ShapeFlags.TEXT_CHILDREN ) {// 新的是文本
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {// 老的是数组
unmountChildren(c1); // 1
}
if (c1 !== c2) {// 老的是文本 46
hostSetElementText(el, c2);
}
} else {
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {// 老的是数组
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {// 新的是数组
// diff
patchKeyedChildren(c1, c2, el); //2
} else {
// 新的不是数组
unmountChildren(c1);// 1
}
} else {
if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {// 老的是文本
hostSetElementText(el, c2); // 4
}
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {// 新的是数组,老的是文本
mountChildren(c2, el); //5
}
}
}
}
const patchElement = (n1, n2) => {
let el = n2.el = n1.el; // 先比较元素 元素是一致
const oldProps = n1.props || {}; // 旧元素的属性
const newProps = n2.props || {}; // 新元素的属性
// 比较元素的属性
patchProps(oldProps, newProps, el);
// 儿子比较
patchChildren(n1, n2, el);
}
// 处理元素
const processElement = (n1, n2, container, anchor) => {
if (n1 == null) {
// 首次加载渲染 组件初始化
mountElement(n2, container, anchor);
} else {
// 组件更新 比较2个元素之间的差异
patchElement(n1, n2);
}
}
// 处理文本
const processText = (n1, n2, container) => {
if (n1 == null) {
// 文本初始化
let textNode = hostCreateText(n2.children);
hostInsert(textNode, container);
// 要让虚拟节点和真实节点挂载上
n2.el = textNode;
}
}
const unmount = (vnode) =>{
hostRemove(vnode.el); // 删除真实节点
}
// n1 -- old n2 -- newVnode
const patch = (n1, n2, container, anchor = null) => {
// 新旧元素不相同则卸载元素
if (n1 && !isSameVNodeType(n1, n2)) {
// 卸载旧元素
unmount(n1);
n1 = null;
}
if (n1 == n2) return; // 相同的元素直接退出
const { shapeFlag, type } = n2; // 拿到新节点的形状标识
switch(type) {
case Text:
processText(n1, n2, container);
break;
default:
if (shapeFlag & ShapeFlags.COMPONENT) {
// 处理元素
processComponent(n1, n2, container);
} else if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(n1, n2, container, anchor)
}
}
// 1. 文本 与 组件 结果是0 2. 文本 与 函数组伯 结果是0 3. 组件 与 组件 有值
}
const render = (vnode, container) => {
//将虚拟节点转化成真实节点渲染到容器中
// patch 包含初次渲染, 如果有更新也会走patch
patch(null, vnode, container);
}
return {
createApp: createAppAPI(render),
render
}
}
6. 调试测试
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script src="./runtime-dom.global.js"></script>
<script>
let {createApp, h, ref} = VueRuntimeDOM;
console.log(ref,h)
function useCounter(){
const count = ref(0);
let flag = ref(true);
const add = ()=>{
count.value++;
}
return {count, add, flag}
}
// 单向传递 父---->子 如果要改props,应该子---edmit----父---udpate
let App = {
props: {
title: {}
},
setup(props, ctx) {
let {count, add, flag} = useCounter();
setTimeout(()=>{
flag.value = !flag.value;
console.log("flag:", flag.value)
}, 3000)
return {
add,
count,
flag
}
// return ()=>{
// return h('h1', {onClick: add}, 'rendener- ' + count.value);
// }
},
// 每次更新重新调用render方法
// render(proxy) {
// return this.flag.value ?
// h('h1', { onClick: this.add, title: proxy.title, style:{color: 'red'}}, h('span', ['aaa',h('a', 'bbbb')]), this.count.value):
// h('h1', { onClick: this.add, title: proxy.title, style:{color: 'green'}}, h('span', ['aaa',h('a', 'cccc')]), this.count.value);
// }
render(proxy) {
return this.flag.value ?
h('div', {}, [
h('li', {key: 'a'}, 'a'),
h('li', {key: 'b'}, 'b'),
h('li', {key: 'c'}, 'c'),
h('li', {key: 'd'}, 'd'),
h('li', {key: 'e'}, 'e'),
h('li', {key: 'f'}, 'f'),
h('li', {key: 'g'}, 'g'),
]):
h('div', {}, [
h('li', {key: 'a'}, 'a'),
h('li', {key: 'b'}, 'b'),
h('li', {key: 'e'}, 'e'),
h('li', {key: 'c'}, 'c'),
h('li', {key: 'd'}, 'd'),
h('li', {key: 'h'}, 'h'),
h('li', {key: 'f'}, 'f'),
h('li', {key: 'g'}, 'g'),
])
}
}
let app = createApp(App, {title: "vue3-rendener", v: "test_v", cc: "cc"});
app.mount("#app");
</script>
</body>
</html>