莲开十月人思量 2019-06-27
关于MVVM前端框架大家都有了解,或多或少的使用过,比如Angular,React,VUE等等。那么你是否也想自己手写一个MVVM的前端框架呢,我们从Virtual DOM入手,手把手教你写基于Virtual DOM的前端框架,在整个编写的过程中,希望大家学习更多,理解更多。
Github代码: https://github.com/chalecao/v...
真实的DOM是网页上的文档对象模型,由一个个HTML元素节点构成的树形结构。
如图中所示,我们用JS创建出来的节点就是虚拟节点,Virtual node,当然由这些虚拟节点vd构成的树形结构就称为虚拟DOM,Virtual DOM。我们本节课介绍的就是要如何创建这样的虚拟DOM。
首先我们需要分析一个node节点的构成,比如他的节点类型type,节点属性的集合props,子元素的集合。这样我们就可以抽象一个数据模型来表示这个节点。虚拟DOM是由许多虚拟节点按照层级结构组合起来的,那么我们实现虚拟节点的数据模型抽象之后,就可以构建虚拟DOM的数据模型抽象。
手工实现DOM模型构建不太合理,我们可以借助JSX的工具来完成这个转换。本节我们以rollup打包工具结合babel转换插件实现数据的抽象。具体代码配置参考:github中package.json配置和rollup.config.js
const vdom = ( <div id="_Q5" style="border: 1px solid red;"> <div style="text-align: center; margin: 36px auto 18px; width: 160px; line-height: 0;"> <img src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_160x56dp.png" height="56" style="border: none; margin: 8px 0px;"></img> hello </div> </div>)
上面我们定义的vdom片段采用JSX处理器处理后如下面代码:
/* fed123.com */ 'use strict'; var vdom = vnode( "div", { id: "_Q5", style: "border: 1px solid red;" }, vnode( "div", { style: "text-align: center; margin: 36px auto 18px; width: 160px; line-height: 0;" }, vnode("img", { src: "https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_160x56dp.png", height: "56", style: "border: none; margin: 8px 0px;" }), "google" ) );
是不是很好理解,JSX编译后会自动根据定义好的语法格式提取出元素的类型和属性和子元素,并填入vnode方法中,我们只需要实现vnode方法就可以。我们可以编写vnode方法用于构建虚拟节点的模型,编写createElement方法用于根据vnode模型创建元素。并且把vnode的子元素追加到父元素上,形成树形层级结构。
function vnode(type, props, ...children) { return { type, props, children }; } function createElement(node) { if (typeof node === 'string') { return document.createTextNode(node); } const $el = document.createElement(node.type); node.children .map(createElement) .forEach($el.appendChild.bind($el)); return $el; } document.body.appendChild(createElement(vdom));
这样我们就完成了虚拟节点vnode和虚拟vDOM的构建。
如图展示了最简单的一层DOM的结构变化,无非也就这么几种:增加元素节点、修改节点,删除节点。我们可以基于DOM API来实现这些基本的操作,代码如下:
function updateElement($parent, newnode, oldnode) { var index = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0; if (!newnode) { $parent.removeChild($parent.childNodes[index]); } else if (!oldnode) { $parent.appendChild(createElement(newnode)); } else if (isChange(newnode, oldnode)) { $parent.replaceChild(createElement(newnode), $parent.childNodes[index]); } else if (newnode.type) { var newL = newnode.children.length; var oldL = oldnode.children.length; for (var i = 0; i < newL || i < oldL; i++) { updateElement($parent.childNodes[index], newnode.children[i], oldnode.children[i], i); } } }
上面的代码中我们实际上是把diff VirtualDOM 和update vdom放在一起处理了,采用了深度优先遍历的算法,从根节点优先查到子节点,判断子节点是否变化,有变化就进行变更处理,然后再回到上级节点。
{ type: “div”, props: {“style”: ”…”}, children: [ {type: “img”, props: {“src”: ”…”} ]}
上面我们抽取的vnode的模型中已经把props拿出来了,我们这里需要把这些样式设置到对应元素上就好了。我们先看下元素的属性变化有哪几种情况:
如上,元素属性可以增加可以减少,我们通过DOM API实现属性的更新操作,代码如下:
//handle props function setProp($el, name, value) { if (typeof value == "boolean") { handleBoolProp($el, name, value); } else { $el.setAttribute(name, value); } } function handleBoolProp($el, name, value) { if (!!value) { $el.setAttribute(name, value); $el[name] = !!value; } else { $el[name] = !!value; } } function removeProp($el, name, value) { if (typeof value == "boolean") { $el[name] = false; } $el.removeAttribute(name, value); } function updateProp($el, name, newvalue, oldValue) { if (!newvalue) { removeProp($el, name, oldValue); } else if (!oldValue || newvalue != oldValue) { setProp($el, name, newvalue); } } function updateProps($el) { var newprops = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var oldProps = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; var _props = Object.assign({}, newprops, oldProps); Object.keys(_props).forEach(function (key) { updateProp($el, key, newprops[key], oldProps[key]); }); }
<ListBox Name="sideMenu" SelectedIndex="{Binding MenuSelectedIndex}" ItemsSource="{Binding MenuList}