vdom 是实现 Vue 和 React 的重要基石
01 创建虚拟DOM
// element.js// 虚拟DOM元素的类,构建实例对象,用来描述DOMclass Element {constructor(type, props, children) {this.type = type;this.props = props;this.children = children;}}// 创建虚拟DOM,返回虚拟节点(object)function createElement(type, props, children) {return new Element(type, props, children);}export {Element,createElement}
02 调用createElement方法
在主入口文件里,我们主要做的操作就是来创建一个DOM对象,渲染DOM以及通过diff后去打补丁更新DOM,不啰嗦了,直接看代码:
// 调用createElement方法
createElement方法也是vue和react用来创建虚拟DOM的方法,
我们也叫这个名字,方便记忆。接收三个参数,分别是type,props和children
**
- 参数分析
- type: 指定元素的标签类型,如’li’, ‘div’, ‘a’等
- props: 表示指定元素身上的属性,如class, style, 自定义属性等
- children: 表示指定元素是否有子节点,参数以数组的形式传入
虚拟dom自带有type props children信息的树状结构
到目前为止,已经轻而易举的实现了创建虚拟DOM。那么,接下来进行下一步,将其渲染为真实的DOM,别犹豫,继续回到element.js文件中
03 渲染虚拟DOM
// element.jsclass Element {// 省略}function createElement() {// 省略}// render方法可以将虚拟DOM转化成真实DOMfunction render(domObj) {// 根据type类型来创建对应的元素let el = document.createElement(domObj.type);// 再去遍历props属性对象,然后给创建的元素el设置属性for (let key in domObj.props) {// 设置属性的方法setAttr(el, key, domObj.props[key]);}// 遍历子节点// 如果是虚拟DOM,就继续递归渲染// 不是就代表是文本节点,直接创建domObj.children.forEach(child => {child = (child instanceof Element) ? render(child) : document.createTextNode(child);// 添加到对应元素内el.appendChild(child);});return el;}// 设置属性function setAttr(node, key, value) {switch(key) {case 'value':// node是一个input或者textarea就直接设置其value即可if (node.tagName.toLowerCase() === 'input' ||node.tagName.toLowerCase() === 'textarea') {node.value = value;} else {node.setAttribute(key, value);}break;case 'style':// 直接赋值行内样式node.style.cssText = value;break;default:node.setAttribute(key, value);break;}}// 将元素插入到页面内function renderDom(el, target) {target.appendChild(el);}export {Element,createElement,render,setAttr,renderDom};
调用render方法就是将虚拟dom转化为实际dom树,并调用renderDom方法直接将DOM添加到了页面内
04 DOM-diff闪亮登场
作用: 根据两个虚拟对象创建出补丁,描述改变的内容,将这个补丁用来更新实际DOM
// diff.jsfunction diff(oldTree, newTree) {// 声明变量patches用来存放补丁的对象let patches = {};// 第一次比较应该是树的第0个索引let index = 0;// 递归树 比较后的结果放到补丁里walk(oldTree, newTree, index, patches);return patches;}function walk(oldNode, newNode, index, patches) {// 每个元素都有一个补丁let current = [];if (!newNode) { // rule1current.push({ type: 'REMOVE', index });} else if (isString(oldNode) && isString(newNode)) {// 判断文本是否一致if (oldNode !== newNode) {current.push({ type: 'TEXT', text: newNode });}} else if (oldNode.type === newNode.type) {// 比较属性是否有更改let attr = diffAttr(oldNode.props, newNode.props);if (Object.keys(attr).length > 0) {current.push({ type: 'ATTR', attr });}// 如果有子节点,遍历子节点diffChildren(oldNode.children, newNode.children, patches);} else { // 说明节点被替换了current.push({ type: 'REPLACE', newNode});}// 当前元素确实有补丁存在if (current.length) {// 将元素和补丁对应起来,放到大补丁包中patches[index] = current;}}function isString(obj) {return typeof obj === 'string';}function diffAttr(oldAttrs, newAttrs) {let patch = {};// 判断老的属性中和新的属性的关系for (let key in oldAttrs) {if (oldAttrs[key] !== newAttrs[key]) {patch[key] = newAttrs[key]; // 有可能还是undefined}}for (let key in newAttrs) {// 老节点没有新节点的属性if (!oldAttrs.hasOwnProperty(key)) {patch[key] = newAttrs[key];}}return patch;}// 所有都基于一个序号来实现let num = 0;function diffChildren(oldChildren, newChildren, patches) {// 比较老的第一个和新的第一个oldChildren.forEach((child, index) => {walk(child, newChildren[index], ++num, patches);});}// 默认导出export default diff;
比较规则
- 新的DOM节点不存在{type: ‘REMOVE’, index}
- 文本的变化{type: ‘TEXT’, text: 1}
- 当节点类型相同时,去看一下属性是否相同,产生一个属性的补丁包{type: ‘ATTR’, attr: {class: ‘list-group’}}
- 节点类型不相同,直接采用替换模式{type: ‘REPLACE’, newNode}
05 patch补丁更新
import { Element, render, setAttr } from './element';let allPatches;let index = 0; // 默认哪个需要打补丁function patch(node, patches) {allPatches = patches;// 给某个元素打补丁walk(node);}function walk(node) {let current = allPatches[index++];let childNodes = node.childNodes;// 先序深度,继续遍历递归子节点childNodes.forEach(child => walk(child));if (current) {doPatch(node, current); // 打上补丁}}function doPatch(node, patches) {// 遍历所有打过的补丁patches.forEach(patch => {switch (patch.type) {case 'ATTR':for (let key in patch.attr) {let value = patch.attr[key];if (value) {setAttr(node, key, value);} else {node.removeAttribute(key);}}break;case 'TEXT':node.textContent = patch.text;break;case 'REPLACE':let newNode = patch.newNode;newNode = (newNode instanceof Element) ? render(newNode) : document.createTextNode(newNode);node.parentNode.replaceChild(newNode, node);break;case 'REMOVE':node.parentNode.removeChild(node);break;default:break;}});}export default patch;
patch做了什么?
- 用一个变量来得到传递过来的所有补丁allPatches
- patch方法接收两个参数(node, patches)
- 在方法内部调用walk方法,给某个元素打上补丁
- walk方法里获取所有的子节点
- 给子节点也进行先序深度优先遍历,递归walk
- 如果当前的补丁是存在的,那么就对其打补丁(doPatch)
- doPatch打补丁方法会根据传递的patches进行遍历
- 判断补丁的类型来进行不同的操作
- 属性ATTR for in去遍历attrs对象,当前的key值如果存在,就直接设置属性setAttr; 如果不存在对应的key值那就直接删除这个key键的属性
- 文字TEXT 直接将补丁的text赋值给node节点的textContent即可
- 替换REPLACE 新节点替换老节点,需要先判断新节点是不是Element的实例,是的话调用render方法渲染新节点;
不是的话就表明新节点是个文本节点,直接创建一个文本节点就OK了。
之后再通过调用父级parentNode的replaceChild方法替换为新的节点 - 删除REMOVE 直接调用父级的removeChild方法删除该节点
- 属性ATTR for in去遍历attrs对象,当前的key值如果存在,就直接设置属性setAttr; 如果不存在对应的key值那就直接删除这个key键的属性
- 判断补丁的类型来进行不同的操作
- 将patch方法默认导出方便调用
用JS对象模拟DOM(虚拟DOM)
把此虚拟DOM转成真实DOM并插入页面中(render)
如果有事件发生修改了虚拟DOM,比较两棵虚拟DOM树的差异,得到差异对象(diff)
把差异对象应用到真正的DOM树上(patch)
**
