怎樣可以做網(wǎng)站培訓(xùn)機(jī)構(gòu)網(wǎng)站制作
前言
前面我們實現(xiàn)了基本的數(shù)據(jù)更新到視圖渲染的邏輯,但是這種方式(innerHTML)是極其低效的, 因此,我們相應(yīng)引入 dom 和 diff 算法, 數(shù)據(jù)到視圖的過程變?yōu)?
state -> vdom -> dom
vNode 層
所謂 vNode, 就是一個表示 dom 結(jié)構(gòu)的輕量對象
{tag, props, children;
}
為了方便創(chuàng)建, 引入創(chuàng)建一個創(chuàng)建節(jié)點的方法h
export function h(tag, props, children) {return {tag,props,children,};
}
我們需要修改 render 函數(shù), 讓其返回一個創(chuàng)建好的 vNode(vTree)
render(context) {return h('div',{id: 'id-1',class: 'class-1'},[h('p', null, String(context.value)), h('p', null, String(context.value))])},
接下來對返回的 vTree 掛載到真實的節(jié)點
let subTree = rootComponent.render(context);
mountElement(subTree, rootContainer);
mountElement 的實現(xiàn)邏輯
- 根據(jù)標(biāo)簽創(chuàng)建元素
- 更新屬性
- 如果子節(jié)點為文本節(jié)點,直接創(chuàng)建, 若為數(shù)組,則遞歸創(chuàng)建
export function mountComponent(vnode, container) {const { tag, props, children } = vnode;// taglet ele = document.createElement(tag);// propsfor (const key in props) {if (Object.hasOwnProperty.call(props, key)) {const value = props[key];ele.setAttribute(key, value);}}/* children1. string2. object*/if (typeof children === "string") {const textNode = document.createTextNode(children);ele.appendChild(textNode);} else if (isArray(children)) {children.forEach((vnode) => {mountComponent(vnode, ele);});}container.appendChild(ele);
}function isArray(ele) {return typeof ele.sort === "function";
}
diff 算法
除了第一次掛載需要生成所有節(jié)點以外, 新的更新是在舊的基礎(chǔ)上"縫縫補(bǔ)補(bǔ)", 這個差量更新的過程交給我們的 diff 算法
我們用一個變量isMounted
來將掛載和更新兩階段分開
export default function createApp(rootComponent) {return {mount(rootContainer) {let context = rootComponent.setup();let isMounted = false;let oldSubTree;effectWatch(() => {if (!isMounted) {isMounted = true;let subTree = (oldSubTree = rootComponent.render(context));mountElement(subTree, rootContainer);} else {let newSubTree = rootComponent.render(context);diff(newSubTree, oldSubTree);oldSubTree = newSubTree;}});},};
}
接下來我們就可以處理diff
的邏輯了, 需要分別對tag
,props
,children
的變更做處理,
因為 diff 的郭恒要對真實的 dom 節(jié)點進(jìn)行操作, 在 mounted 過程中將 dom 渲染完成后,我們需要將其掛載到對應(yīng)的 vNode 上
export function mountElement(vNode, container) {// ...let ele = (vNode.el = document.createElement(tag));// ...
}
- tag 變化的處理 ,這里用到了原生的
replaceWith
操作方法
if (newTree.tag !== oldTree.tag) {oldTree.el.replaceWith(document.createElement(newTree.tag));
}
- props 節(jié)點的處理
newTree.el = oldTree.el;
// props, 對比兩個對象, 各自遍歷一遍,找出各自不同的地方
let { props: newProps } = newTree;
let { props: oldProps } = oldTree;
if (newProps && oldProps) {Object.keys(newProps).forEach((key) => {// 同時存在,意味著需要更新節(jié)點let newVal = newProps[key];if (Object.hasOwnProperty.call(oldProps, key)) {let oldVal = oldProps[key];if (newVal !== oldVal) {newTree.el.setAttribute(key, newVal);}} else {// 舊的不存在, 創(chuàng)建newTree.el.setAttribute(key, newVal);}});
}
// 移除已不存在的舊節(jié)點
if (oldProps) {Object.keys(oldProps).forEach((key) => {if (!Object.hasOwnProperty.call(newProps, key)) {newTree.el.removeAttribute(key);}});
}
當(dāng)然, 為了演示, 這里的處理過程比較簡單,
- children 的處理
chilren 的處理相對比較麻煩,為了簡化, 目前根據(jù) children 的類型區(qū)分
即: newChildren[string, array] * oldChildren[array, string] = 4 種情況
前三種比較簡單
let { children: oldChildren } = oldTree;
let { children: newChildren } = newTree;
if (typeof newChildren === "string") {if (typeof oldChildren === "string") {if (newChildren !== oldChildren) {newTree.el.textContent = newChildren;}} else if (isArray(oldChildren)) {newTree.el.textContent = newChildren;}
} else if (isArray(newChildren)) {if (typeof oldChildren === "string") {newTree.el.textContent = ``;mountElement(newTree, newTree.el);} else if (Array.isArray(oldChildren)) {// ...}
}
下面分析兩者都是數(shù)組的情況, 為了簡化, 只對節(jié)點的長度作處理,不處理相同長度內(nèi)的節(jié)點移位操作
// 暴力解法: 只對節(jié)點的長度作處理,不處理相同長度內(nèi)的節(jié)點移位操作
const length = Math.min(newChildren.length, oldChildren.length);
// 更新相同長度的部分
for (var index = 0; index < length; index++) {let newTree = newChildren[index];let oldTree = oldChildren[index];diff(newTree, oldTree);
}
// 創(chuàng)建
if (newChildren.length > oldChildren.length) {for (let index = length; index < newChildren.length; index++) {const newVNode = newChildren[index];mountElement(newVNode, newTree.el);}
}
// 刪除
if (oldChildren.length > newChildren.length) {for (let index = length; index < oldChildren.length; index++) {const vNode = oldChildren[index];vNode.el.remove(); // 節(jié)點移除自身}
}
本文首發(fā)于個人 Github前端開發(fā)筆記,由于筆者能力有限,文章難免有疏漏之處,歡迎指正