目录
Props
Setting Props:设置Props
Diffing Props:Props变化比较
Events
Re-Adding Events:重新设置了事件响应
首页 web前端 html教程 从零开始一步一步写一个简单的 Virtual DOM 实现 2 :Props&Event 处理_html/css_WEB-ITnose

从零开始一步一步写一个简单的 Virtual DOM 实现 2 :Props&Event 处理_html/css_WEB-ITnose

Jun 24, 2016 am 11:14 AM

Props

首先我们要回顾下前文讲的一个有些偏差的小点,假设我们在JSX中只写一个最简单的Div:

<div></div>
登录后复制

Babel会自动将该JSX转化为如下的DOM表达式:

{ type: ‘’, props: null, children: [] }
登录后复制

注意,这里的props默认是null,我们在之前的文章中并没有关注到这个属性,而本部分则是要讲解Virtual DOM中Props的用法。一般来说,无论在哪种编程环境下都要尽量避免Null的出现,因此我们首先来改造下 h 函数,使得其能够默认返回一个空的Object,而不是Null:

function h(type, props, …children) { return { type, props: props || {}, children };}
登录后复制

Setting Props:设置Props

接触过React的同学对于Props肯定不会陌生,而设置Props也就跟使用普通的HTML标签属性很类似:

<ul className=”list” style=”list-style: none;”></ul>
登录后复制

而最终会转化为如下的表达式:

{  type: ‘ul’,  props: { className: ‘list’, style: ’list-style: none;’ }  children: []}
登录后复制

props对象中的每个键即为属性名,而值为属性值,一般来说我们只需要简单的调用一个 setAttribute 方法来讲这个Props中的键值对设置到DOM元素上即可:

function setProp($target, name, value) { $target.setAttribute(name, value);}
登录后复制

这个函数用于将单个的Prop值设置到DOM元素上,而对于props对象,我们要做的就是依次遍历:

function setProps($target, props) { Object.keys(props).forEach(name => {   setProp($target, name, props[name]); });}
登录后复制

你应该还记得那个用于创建元素的 createElement 方法吧,我们需要将 setProps 方法放置到元素成功创建之后:

function createElement(node) { if (typeof node === ‘string’) {   return document.createTextNode(node); } const $el = document.createElement(node.type); setProps($el, node.props); node.children   .map(createElement)   .forEach($el.appendChild.bind($el)); return $el;}
登录后复制

不要急,这还远远不够。React的初学教程中一直强调className与class的区别,在我们的setProps中也需要对于这些JS的保留字做一个替换,譬如:

<nav className=”navbar light”> <ul></ul></nav>
登录后复制

另外,还有比较常见的就是对于DOM的布尔属性,譬如checked、disabled等等的处理:

<input type=”checkbox” checked={false} />
登录后复制

在真实的DOM节点上,如果是出现了false的情况,我们并不希望checked属性会出现,那么我们的Props函数就要能智能地进行判断:

function setBooleanProp($target, name, value) { if (value) {   $target.setAttribute(name, value);   $target[name] = true; } else {   $target[name] = false; }}
登录后复制

最后呢,要做的就是对于自定义的,即非标准的HTML属性进行一个过滤,这些属性只应该出现在JS对象上,而不应该出现在真实的DOM对象上:

function isCustomProp(name) { return false;}
登录后复制
function setProp($target, name, value) { if (isCustomProp(name)) {   return; } else if (name === ‘className’) {   $target.setAttribute(‘class’, value); } else if (typeof value === ‘boolean’) {   setBooleanProp($target, name, value); } else {   $target.setAttribute(name, value); }}
登录后复制

总结一下,本部分完整的JSX代码为:

/** @jsx h */function h(type, props, ...children) {  return { type, props: props || {}, children };}function setBooleanProp($target, name, value) {  if (value) {    $target.setAttribute(name, value);    $target[name] = true;  } else {    $target[name] = false;  }}function isCustomProp(name) {  return false;}function setProp($target, name, value) {  if (isCustomProp(name)) {    return;  } else if (name === 'className') {    $target.setAttribute('class', value);  } else if (typeof value === 'boolean') {    setBooleanProp($target, name, value);  } else {    $target.setAttribute(name, value);  }}function setProps($target, props) {  Object.keys(props).forEach(name => {    setProp($target, name, props[name]);  });}function createElement(node) {  if (typeof node === 'string') {    return document.createTextNode(node);  }  const $el = document.createElement(node.type);  setProps($el, node.props);  node.children    .map(createElement)    .forEach($el.appendChild.bind($el));  return $el;}//--------------------------------------------------const f = (  <ul style="list-style: none;">    <li className="item">item 1</li>    <li className="item">      <input type="checkbox" checked={true} />      <input type="text" disabled={false} />    </li>  </ul>);const $root = document.getElementById('root');$root.appendChild(createElement(f));
登录后复制

Diffing Props:Props变化比较

现在我们已经创建了带有Props属性的元素,下一个需要考虑的就是应该如何应用到我们上文提到的Diff算法中。首先我们要来看下如何从真实的DOM中移除某些Props:

function removeBooleanProp($target, name) { $target.removeAttribute(name); $target[name] = false;}function removeProp($target, name, value) { if (isCustomProp(name)) {   return; } else if (name === ‘className’) {   $target.removeAttribute(‘class’); } else if (typeof value === ‘boolean’) {   removeBooleanProp($target, name); } else {   $target.removeAttribute(name); }}
登录后复制

然后我们需要写一个updateProp函数,来根据新旧节点的Props的变化进行恰当的真实DOM节点的修改,共有以下几种情况:

  • 新节点移除了某个旧节点的Prop

  • 新节点添加了某个旧节点没有的Prop

  • 新旧节点的某个Prop的值发生了变化

根据以上规则,我们可知更新Prop的函数为:

function updateProp($target, name, newVal, oldVal) { if (!newVal) {   removeProp($target, name, oldVal); } else if (!oldVal || newVal !== oldVal) {   setProp($target, name, newVal); }}
登录后复制

可以看出,更新单个Prop的函数还是非常简单的,就是将移除与设置结合起来使用,那么我们扩展到Props,就得到如下的函数:

function updateProps($target, newProps, oldProps = {}) {  const props = Object.assign({}, newProps, oldProps);  Object.keys(props).forEach(name => {    updateProp($target, name, newProps[name], oldProps[name]);  });}
登录后复制

同样地,我们需要将该更新函数添加到 updateElement 函数中:

function updateElement($parent, newNode, oldNode, index = 0) {  ...  } else if (newNode.type) {    updateProps(      $parent.childNodes[index],      newNode.props,      oldNode.props    );     ...  }}
登录后复制

Events

用户交互是任何一个应用不可或缺的部分,而在这里我们讨论下如何为Virtual DOM添加事件处理的能力,React大概会这么做:

<button onClick={() => alert(‘hi!’)}></button>
登录后复制

可以看出,设置一个事件处理器就是添加一个Prop,只不过名称会以 on 开始,那么我们可以用如下函数来判断某个Prop是否与事件相关:

function isEventProp(name) { return /^on/.test(name);}
登录后复制

判断是事件类型之后,我们可以提取出事件名:

function extractEventName(name) { return name.slice(2).toLowerCase();}
登录后复制

看到这里,估计你会考虑直接将事件处理也放到setProps与updateProps函数中,不过这边就会存在一个问题,在diffProps的时候,你很难去比较两个function:

因此我们将所有的事件类型的Props认为是自定义的Props,这样我们上面提到的isCustomProp就起作用了:

function isCustomProp(name) { return isEventProp(name);}
登录后复制

而把事件响应函数绑定到真实的DOM节点也很简单:

function addEventListeners($target, props) { Object.keys(props).forEach(name => {   if (isEventProp(name)) {     $target.addEventListener(       extractEventName(name),       props[name]     );   } });}
登录后复制

同样的需要将该函数添加到createElement中:

function createElement(node) { if (typeof node === ‘string’) {   return document.createTextNode(node); } const $el = document.createElement(node.type); setProps($el, node.props); addEventListeners($el, node.props); node.children   .map(createElement)   .forEach($el.appendChild.bind($el)); return $el;}
登录后复制

Re-Adding Events:重新设置了事件响应

在这里我们暂时不考虑地很复杂,即不深入地比较那些事件类型的Prop发生变化的情况,作为替代的,我们引入一个forceUpdate属性,即强制整个DOM进行更新:

function changed(node1, node2) { return typeof node1 !== typeof node2 ||        typeof node1 === ‘string’ && node1 !== node2 ||        node1.type !== node2.type ||        node.props.forceUpdate;}
登录后复制
function isCustomProp(name) { return isEventProp(name) || name === ‘forceUpdate’;}
登录后复制

最后,本文完整的JSX为:

/** @jsx h */function h(type, props, ...children) {  return { type, props: props || {}, children };}function setBooleanProp($target, name, value) {  if (value) {    $target.setAttribute(name, value);    $target[name] = true;  } else {    $target[name] = false;  }}function removeBooleanProp($target, name) {  $target.removeAttribute(name);  $target[name] = false;}function isEventProp(name) {  return /^on/.test(name);}function extractEventName(name) {  return name.slice(2).toLowerCase();}function isCustomProp(name) {  return isEventProp(name) || name === 'forceUpdate';}function setProp($target, name, value) {  if (isCustomProp(name)) {    return;  } else if (name === 'className') {    $target.setAttribute('class', value);  } else if (typeof value === 'boolean') {    setBooleanProp($target, name, value);  } else {    $target.setAttribute(name, value);  }}function removeProp($target, name, value) {  if (isCustomProp(name)) {    return;  } else if (name === 'className') {    $target.removeAttribute('class');  } else if (typeof value === 'boolean') {    removeBooleanProp($target, name);  } else {    $target.removeAttribute(name);  }}function setProps($target, props) {  Object.keys(props).forEach(name => {    setProp($target, name, props[name]);  });}function updateProp($target, name, newVal, oldVal) {  if (!newVal) {    removeProp($target, name, oldVal);  } else if (!oldVal || newVal !== oldVal) {    setProp($target, name, newVal);  }}function updateProps($target, newProps, oldProps = {}) {  const props = Object.assign({}, newProps, oldProps);  Object.keys(props).forEach(name => {    updateProp($target, name, newProps[name], oldProps[name]);  });}function addEventListeners($target, props) {  Object.keys(props).forEach(name => {    if (isEventProp(name)) {      $target.addEventListener(        extractEventName(name),        props[name]      );    }  });}function createElement(node) {  if (typeof node === 'string') {    return document.createTextNode(node);  }  const $el = document.createElement(node.type);  setProps($el, node.props);  addEventListeners($el, node.props);  node.children    .map(createElement)    .forEach($el.appendChild.bind($el));  return $el;}function changed(node1, node2) {  return typeof node1 !== typeof node2 ||         typeof node1 === 'string' && node1 !== node2 ||         node1.type !== node2.type ||         node1.props && node1.props.forceUpdate;}function updateElement($parent, newNode, oldNode, index = 0) {  if (!oldNode) {    $parent.appendChild(      createElement(newNode)    );  } else if (!newNode) {    $parent.removeChild(      $parent.childNodes[index]    );  } else if (changed(newNode, oldNode)) {    $parent.replaceChild(      createElement(newNode),      $parent.childNodes[index]    );  } else if (newNode.type) {    updateProps(      $parent.childNodes[index],      newNode.props,      oldNode.props    );    const newLength = newNode.children.length;    const oldLength = oldNode.children.length;    for (let i = 0; i < newLength || i < oldLength; i++) {      updateElement(        $parent.childNodes[index],        newNode.children[i],        oldNode.children[i],        i      );    }  }}//---------------------------------------------------------function log(e) {  console.log(e.target.value);}const f = (  
  • alert('hi!')}>item 1
  • {/* this node will always be updated */}
  • text
);const g = (
  • alert('hi!')}>item 1
  • {/* this node will always be updated */}
  • text
);const $root = document.getElementById('root');const $reload = document.getElementById('reload');updateElement($root, f);$reload.addEventListener('click', () => { updateElement($root, g, f);});
登录后复制

到这里我们就完成了一个最简单的Virtual DOM算法,不过其与真正能够投入实战的Virtual DOM算法还是有很大距离,进一步阅读推荐:

  • 深度剖析:如何实现一个 Virtual DOM 算法

  • 我的前端故事----React算法又是个什么鬼?!

  • A Virtual DOM and diffing algorithm :一个比较复杂的Virtual DOM算法的实现

  • simple-virtual-dom :一个简单的Virtual DOM的实现

  • how-to-write-your-own-virtual-dom

  • Virtual DOM Benchmark

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热门文章

R.E.P.O.能量晶体解释及其做什么(黄色晶体)
1 个月前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳图形设置
1 个月前 By 尊渡假赌尊渡假赌尊渡假赌
威尔R.E.P.O.有交叉游戏吗?
1 个月前 By 尊渡假赌尊渡假赌尊渡假赌

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

&gt; gt;的目的是什么 元素? &gt; gt;的目的是什么 元素? Mar 21, 2025 pm 12:34 PM

本文讨论了HTML&lt; Progress&gt;元素,其目的,样式和与&lt; meter&gt;元素。主要重点是使用&lt; progress&gt;为了完成任务和LT;仪表&gt;对于stati

&lt; datalist&gt;的目的是什么。 元素? &lt; datalist&gt;的目的是什么。 元素? Mar 21, 2025 pm 12:33 PM

本文讨论了html&lt; datalist&gt;元素,通过提供自动完整建议,改善用户体验并减少错误来增强表格。Character计数:159

&lt; meter&gt;的目的是什么。 元素? &lt; meter&gt;的目的是什么。 元素? Mar 21, 2025 pm 12:35 PM

本文讨论了HTML&lt; meter&gt;元素,用于在一个范围内显示标量或分数值及其在Web开发中的常见应用。它区分了&lt; meter&gt;从&lt; progress&gt;和前

视口元标签是什么?为什么对响应式设计很重要? 视口元标签是什么?为什么对响应式设计很重要? Mar 20, 2025 pm 05:56 PM

本文讨论了视口元标签,这对于移动设备上的响应式Web设计至关重要。它解释了如何正确使用确保最佳的内容缩放和用户交互,而滥用可能会导致设计和可访问性问题。

&lt; iframe&gt;的目的是什么。 标签?使用时的安全考虑是什么? &lt; iframe&gt;的目的是什么。 标签?使用时的安全考虑是什么? Mar 20, 2025 pm 06:05 PM

本文讨论了&lt; iframe&gt;将外部内容嵌入网页,其常见用途,安全风险以及诸如对象标签和API等替代方案的目的。

HTML容易为初学者学习吗? HTML容易为初学者学习吗? Apr 07, 2025 am 12:11 AM

HTML适合初学者学习,因为它简单易学且能快速看到成果。1)HTML的学习曲线平缓,易于上手。2)只需掌握基本标签即可开始创建网页。3)灵活性高,可与CSS和JavaScript结合使用。4)丰富的学习资源和现代工具支持学习过程。

HTML,CSS和JavaScript的角色:核心职责 HTML,CSS和JavaScript的角色:核心职责 Apr 08, 2025 pm 07:05 PM

HTML定义网页结构,CSS负责样式和布局,JavaScript赋予动态交互。三者在网页开发中各司其职,共同构建丰富多彩的网站。

HTML中起始标签的示例是什么? HTML中起始标签的示例是什么? Apr 06, 2025 am 12:04 AM

AnexampleOfAstartingTaginHtmlis,beginSaparagraph.startingTagSareEssentialInhtmlastheyInitiateEllements,defiteTheeTheErtypes,andarecrucialforsstructuringwebpages wepages webpages andConstructingthedom。

See all articles