>[success] # 手寫虛擬dom 渲染

* **項目結構**
~~~
├─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發生區域**

>[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 -- 案例
- 待續