>[success] # 手写虚拟dom 渲染
![](https://img.kancloud.cn/e1/a2/e1a2ee593c3c220475057f2c3a2249b4_1687x1261.jpg)
* **项目结构**
~~~
├─public
│ └─ └─index.html # 打包的html 模板
├─src # 项目源代码
│ ├─vdom # 整虚拟dom 文件目录
│ │ ├─create-element.js # 虚拟dom对象定义
│ │ ├─h.js # 将函数表达式的形式渲染成 虚拟dom
│ │ ├─index.js # 我们手写的入口
│ └─ └─patch.js #
├─index.js # 打包的入口
└─webpack.config.js # webpack 配置
~~~
>[info] ## h 函数
1. 将函数表达式的形式渲染成 **虚拟dom**,我们这里处理不像`snabbdom`,对参数做了重载处理,我们这里采用最简单的仅仅判断是**对象**还是**文本**如果是**文本就手动转成虚拟dom**
>[danger] ##### 代码
~~~
import {vnode} from './create-element'
export default function h(tag,props,...children){
let key = props.key;
delete props.key; // 属性中不包括key属性
children = children.map(child=>{
if(typeof child === 'object'){
return child
}else{
return vnode(undefined,undefined,undefined,undefined,child)
}
})
return vnode(tag,props,key,children);
}
~~~
>[danger] ##### 使用
第一个参数`div` 对应函数 `tag `形参,`{id:'container',key:1,class: 'abc'}`,对应形参`props`是用来配置**tag标签属性**,剩下的**h 函数** 和 `zf` 对应的都是`...children`参数,如果是对象说明已经是h 函数转换完毕的Vnode 对象,如果不是说明是文本节点,需要拼接为文本节点的Vnode
~~~
let oldVnode = h('div', {
id: 'container',
key: 1,
class: 'abc'
},
h('span', {
style: {
color: 'red',
background: 'yellow'
}
}, 'hello'),
'zf'
);
~~~
>[info] ## create-element -- Vnode
将从`h`函数收集的参数进行转换为`Vnode`对象格式
>[danger] ##### 代码
1. 要注意了如果是文本的虚拟dom对象就不会`children`,有`children`的就不会有文本,因此下面的`text `和`children `参数是不能同时存在的
~~~
export function vnode(tag, props, key, children, text) {
return {
tag, // 表示的是当前的标签名
props, // 表示的是当前标签上的属性
key, // 唯一表示用户可能传递
children,
text
}
}
~~~
>[info] ## 虚拟dom 转换真实dom工具方法
1. `render `函数,他做的很简单将虚拟dom生成的dom节点插入到他对应的父节点中
2. `createElm `创建dom,将虚拟dom转换成真实dom
3. `updateProperties`,将虚拟dom创建时候定义的props 属性也就`dom`节点的属性要赋值到dom上
~~~
/*
将虚拟dom 渲染到页面上成为真实dom
@params container 是一个真实dom, 用来指定虚拟dom 转成
真实dom 要插入的位置,理解成是父容器的位置
@params vnode 虚拟dom 对象
*/
export function render(vnode, container) {
let el = createElm(vnode)
container.appendChild(el) // 将虚拟dom对象转换成的真实dom 插入对应的父节点中
}
// 将虚拟dom转换成真实dom
function createElm(vnode) {
let {
tag,
children,
key,
props,
text
} = vnode
// 判断虚拟dom 是文本还是普通标签
if (typeof tag === 'string') {
// 存在tag 说明是一个dom 节点,要注意文本节点
// 我们在创建的时候tag 是undefind 所以可以简单认为有tag就是一个dom
// 通过虚拟dom也就是h函数返回的createEle对象,里面的tag参数来生成一个dom标签
vnode.el = document.createElement(tag)
// 创建完dom 也要对dom上的属性进行处理例如class style 这些
updateProperties(vnode);
// 如果有children 这是后需要递归层级创建每一个h函数返回createEle对象对应的dom标签
children.forEach(child => { // child 是虚拟节点
render(child, vnode.el) // 让这些虚拟节点形成嵌套传入他们的父节点dom对象形成递归
})
} else {
vnode.el = document.createTextNode(text) // 创建文本标签
}
return vnode.el
}
// 更新的时候是新老的比较,第一次时候默认老的是空
function updateProperties(vnode, oldProps = {}) {
// 获取当前vnode虚拟节点上该节点的所有属性
let newProps = vnode.props
// 获取要加这些属性的 dom 节点对象
let el = vnode.el
// -------------------------------------------
// 后续会存在新老的虚拟dom节点比较,因此我们也需要比较看看
// 新老变化后那些dom 属性删除了,这样只需要操作针对变化属性即可
// 同样这里要考虑的是style 和其他属性,因为style比较特殊
let newStyle = newProps.style || {};
let oldStyle = oldProps.style || {};
// 循环老的属性不在新的属性中存在说明被删除了
for (let key in oldStyle) {
if (!newStyle[key]) {
el.style[key] = ''
}
}
// 如果下次更新时 我应该用新的属性 来更新老的节点
// 如果老的中有属性 新的中没有
for (let key in oldProps) {
if (!newProps[key]) {
delete el[key]; // 如果新的中没有这个属性了 那就直接删除掉dom上的这个属性
}
}
// -------------------------------------------
// 循环这个在虚拟dom定义的对象并且依次赋值到dom节点上
for (let key in newProps) {
if (key === 'style') { // 如果是style的话 需要再次遍历添加
for (let styleName in newProps.style) { // {color:red}
// el.style.color = 'red'
el.style[styleName] = newProps.style[styleName]
}
} else if (key === 'class') {
el.className = newProps.class
} else { // 给这个元素添加属性 值就是对应的值
el[key] = newProps[key]
}
}
}
~~~
>[info] ## patch
1. 当 patch 函数内部触发`sameVnode(oldVnode, vnode)` 说明新老Vnode 相同的接下来需要进入`patchVnode`用来比较相同节点内容中子节点逻辑处理,当双方都有子节点进入`updateChildren ` 函数开始**diff发生区域**
![](https://img.kancloud.cn/82/39/8239a9ad4a2cd197c09b94f99552007b_889x299.png)
>[danger] ##### 代码
~~~
export function patch(oldVnode,newVnode){
// 1) 先比对 标签一样不一样
if(oldVnode.tag !== newVnode.tag){ // 以前是div 现在是p标签
// 必须拿到当前元素的父亲 才能替换掉自己
oldVnode.el.parentNode.replaceChild(createElm(newVnode),oldVnode.el)
}
// 2) 比较文本了 标签一样 可能都是undefined
if(!oldVnode.tag){
if(oldVnode.text !== newVnode.text){ // 如果内容不一致直接根据当前新的元素中的内容来替换到文本节点
oldVnode.el.textContent = newVnode.text;
}
}
// 标签一样 可能属性不一样了
let el = newVnode.el = oldVnode.el; // 标签一样复用即可
updateProperties(newVnode,oldVnode.props); // 做属性的比对
// 必须要有一个根节点
// 比较孩子
let oldChildren = oldVnode.children || [];
let newChildren = newVnode.children || [];
// 老的有孩子 新的有孩子 updateChildren
if(oldChildren.length > 0 && newChildren.length > 0){
updateChildren(el,oldChildren,newChildren); // 不停的递归比较
}else if(oldChildren.length > 0){ // 老的有孩子 新的没孩子
el.innerHTML = ''
}else if(newChildren.length > 0){ // 老的没孩子 新的有孩子
for(let i = 0; i < newChildren.length ;i++){
let child = newChildren[i];
el.appendChild(createElm(child)); // 将当前新的儿子 丢到老的节点中即可
}
}
return el;
}
~~~
>[info] ## updateChildren
1. diff 算法发生位置 主要是为将相同 `老dom` 复用到`新dom`上
>[danger] ##### 代码
~~~
function isSameVnode(oldVnode, newVnode) {
// 如果两个人的标签和key 一样我认为是同一个节点 虚拟节点一样我就可以复用真实节点了
return (oldVnode.tag === newVnode.tag) && (oldVnode.key === newVnode.key)
}
function updateChildren(parent, oldChildren, newChildren) {
// ------------------老children数组--------------------
let oldStartIndex = 0 // 老节点的初始位置
let oldStartVnode = oldChildren[0] // 老节点数组开始节点的值
let oldEndIndex = oldChildren.length - 1 // 老节点末尾的位置
let oldEndVnode = oldChildren[oldEndIndex] // 老节点数组末尾节点的值
// ------------------新的children数组--------------------
let newStartIndex = 0 // 新节点初始位置
let newStartVnode = newChildren[0] // 新节点开始节点的值
let newEndIndex = newChildren.length - 1 // 新节点末尾的值
let newEndVnode = newChildren[newEndIndex] // 新节点数组末尾的值
// ------------------------------------------------------------
function makeIndexByKey(children) { // 为乱序的情况准备,看老节点有没有能复用的节点
let map = {};
children.forEach((item, index) => {
map[item.key] = index
});
return map; // {a:0,b:1...}
}
let map = makeIndexByKey(oldChildren);
// // 采用指针的方式 单层循环 替代for 双层循环
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
// 依次按顺序比较每个节点是否相同 依次比较为了向后插入
if (isSameVnode(oldStartVnode, newStartVnode)) {
patch(oldStartVnode, newStartVnode); // 比较新老属性 节点中的值递归比较里面的孩子
oldStartVnode = oldChildren[++oldStartIndex];
newStartVnode = newChildren[++newStartIndex];
} else if (isSameVnode(oldEndVnode, newEndVnode)) {
// 向前插入的情况做判断 从后往前判断
patch(oldEndVnode, newEndVnode)
oldEndVnode = oldChildren[--oldEndIndex]
newEndVnode = newChildren[--newEndIndex]
} else if (isSameVnode(oldStartVnode, newEndVnode)) { // 倒叙
patch(oldStartVnode, newEndVnode);
parent.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling);
oldStartVnode = oldChildren[++oldStartIndex];
newEndVnode = newChildren[--newEndIndex]
} else if (isSameVnode(oldEndVnode, newStartVnode)) { // 将尾部插入头部
patch(oldEndVnode, newStartVnode);
parent.insertBefore(oldEndVnode.el, oldStartVnode.el);
oldEndVnode = oldChildren[--oldEndIndex];
newStartVnode = newChildren[++newStartIndex]
} else { // 乱序
// 会先拿新节点的第一项 去老节点中匹配,如果匹配不到直接将这个节点插入到老节点开头的前面,如果能查找到则直接移动老节点
let moveIndex = map[newStartVnode.key];
if (moveIndex == undefined) {
parent.insertBefore(createElm(newStartVnode), oldStartVnode.el);
} else {
// 我要移动这个元素
let moveVnode = oldChildren[moveIndex];
oldChildren[moveIndex] = undefined;
parent.insertBefore(moveVnode.el, oldStartVnode.el);
patch(moveVnode, newStartVnode);
}
// 要将新节点的指针向后移动
newStartVnode = newChildren[++newStartIndex]
}
}
if (newStartIndex <= newEndIndex) { // 如果到最后还剩余 需要将剩余的插入,针对前插和后插
for (let i = newStartIndex; i <= newEndIndex; i++) {
// 要插入的元素
let ele = newChildren[newEndIndex + 1] == null ? null : newChildren[newEndIndex + 1].el;
parent.insertBefore(createElm(newChildren[i]), ele);
}
}
if (oldStartIndex <= oldEndIndex) { // 可能老节点中还有剩余 则直接删除老节点中剩余的属性针对乱序
for (let i = oldStartIndex; i <= oldEndIndex; i++) {
let child = oldChildren[i];
if (child != undefined) {
parent.removeChild(child.el)
}
}
}
}
~~~
- 工程化 -- Node
- vscode -- 插件
- vscode -- 代码片段
- 前端学会调试
- 谷歌浏览器调试技巧
- 权限验证
- 包管理工具 -- npm
- 常见的 npm ci 指令
- npm -- npm install安装包
- npm -- package.json
- npm -- 查看包版本信息
- npm - package-lock.json
- npm -- node_modules 层级
- npm -- 依赖包规则
- npm -- install 安装流程
- npx
- npm -- 发布自己的包
- 包管理工具 -- pnpm
- 模拟数据 -- Mock
- 页面渲染
- 渲染分析
- core.js && babel
- core.js -- 到底是什么
- 编译器那些术语
- 词法解析 -- tokenize
- 语法解析 -- ast
- 遍历节点 -- traverser
- 转换阶段、生成阶段略
- babel
- babel -- 初步上手之了解
- babel -- 初步上手之各种配置(preset-env)
- babel -- 初步上手之各种配置@babel/helpers
- babel -- 初步上手之各种配置@babel/runtime
- babel -- 初步上手之各种配置@babel/plugin-transform-runtime
- babel -- 初步上手之各种配置(babel-polyfills )(未来)
- babel -- 初步上手之各种配置 polyfill-service
- babel -- 初步上手之各种配置(@babel/polyfill )(过去式)
- babel -- 总结
- 各种工具
- 前端 -- 工程化
- 了解 -- Yeoman
- 使用 -- Yeoman
- 了解 -- Plop
- node cli -- 开发自己的脚手架工具
- 自动化构建工具
- Gulp
- 模块化打包工具为什么出现
- 模块化打包工具(新) -- webpack
- 简单使用 -- webpack
- 了解配置 -- webpack.config.js
- webpack -- loader 浅解
- loader -- 配置css模块解析
- loader -- 图片和字体(4.x)
- loader -- 图片和字体(5.x)
- loader -- 图片优化loader
- loader -- 配置解析js/ts
- webpack -- plugins 浅解
- eslit
- plugins -- CleanWebpackPlugin(4.x)
- plugins -- CleanWebpackPlugin(5.x)
- plugin -- HtmlWebpackPlugin
- plugin -- DefinePlugin 注入全局成员
- webapck -- 模块解析配置
- webpack -- 文件指纹了解
- webpack -- 开发环境运行构建
- webpack -- 项目环境划分
- 模块化打包工具 -- webpack
- webpack -- 打包文件是个啥
- webpack -- 基础配置项用法
- webpack4.x系列学习
- webpack -- 常见loader加载器
- webpack -- 移动端px转rem处理
- 开发一个自己loader
- webpack -- plugin插件
- webpack -- 文件指纹
- webpack -- 压缩css和html构建
- webpack -- 清里构建包
- webpack -- 复制静态文件
- webpack -- 自定义插件
- wepack -- 关于静态资源内联
- webpack -- source map 对照包
- webpack -- 环境划分构建
- webpack -- 项目构建控制台输出
- webpack -- 项目分析
- webpack -- 编译提速优护体积
- 提速 -- 编译阶段
- webpack -- 项目优化
- webpack -- DefinePlugin 注入全局成员
- webpack -- 代码分割
- webpack -- 页面资源提取
- webpack -- import按需引入
- webpack -- 摇树
- webpack -- 多页面打包
- webpack -- eslint
- webpack -- srr打包后续看
- webpack -- 构建一个自己的配置后续看
- webpack -- 打包组件和基础库
- webpack -- 源码
- webpack -- 启动都做了什么
- webpack -- cli做了什么
- webpack - 5
- 模块化打包工具 -- Rollup
- 工程化搭建代码规范
- 规范化标准--Eslint
- eslint -- 扩展配置
- eslint -- 指令
- eslint -- vscode
- eslint -- 原理
- Prettier -- 格式化代码工具
- EditorConfig -- 编辑器编码风格
- 检查提交代码是否符合检查配置
- 整体流程总结
- 微前端
- single-spa
- 简单上手 -- single-spa
- 快速理解systemjs
- single-sap 不使用systemjs
- monorepo -- 工程
- Vue -- 响应式了解
- Vue2.x -- 源码分析
- 发布订阅和观察者模式
- 简单 -- 了解响应式模型(一)
- 简单 -- 了解响应式模型(二)
- 简单 --了解虚拟DOM(一)
- 简单 --了解虚拟DOM(二)
- 简单 --了解diff算法
- 简单 --了解nextick
- Snabbdom -- 理解虚拟dom和diff算法
- Snabbdom -- h函数
- Snabbdom - Vnode 函数
- Snabbdom -- init 函数
- Snabbdom -- patch 函数
- 手写 -- 虚拟dom渲染
- Vue -- minVue
- vue3.x -- 源码分析
- 分析 -- reactivity
- 好文
- grpc -- 浏览器使用gRPC
- grcp-web -- 案例
- 待续