关于diff:为什么-React-的-Diff-算法不采用-Vue-的双端对比算法

前言都说“双端比照算法”,那么双端比照算法,到底是怎么样的呢?跟 React 中的 Diff 算法又有什么不同呢?要理解这些,咱们先理解 React 中的 Diff 算法,而后再理解 Vue3 中的 Diff 算法,最初讲一下 Vue2 中的 Diff 算法,能力去比拟一下他们的区别。最初讲一下为什么 Vue 中不须要应用 Fiber 架构。React 官网的解析其实为什么 React 不采纳 Vue 的双端比照算法,React 官网曾经在源码的正文里曾经阐明了,咱们来看一下 React 官网是怎么说的。function reconcileChildrenArray(returnFiber: Fiber, currentFirstChild: Fiber | null, newChildren: Array<*>, expirationTime: ExpirationTime,): Fiber | null { // This algorithm can't optimize by searching from boths ends since we// don't have backpointers on fibers. I'm trying to see how far we can get// with that model. If it ends up not being worth the tradeoffs, we can// add it later.// Even with a two ended optimization, we'd want to optimize for the case// where there are few changes and brute force the comparison instead of// going for the Map. It'd like to explore hitting that path first in// forward-only mode and only go for the Map once we notice that we need// lots of look ahead. This doesn't handle reversal as well as two ended// search but that's unusual. Besides, for the two ended optimization to// work on Iterables, we'd need to copy the whole set.// In this first iteration, we'll just live with hitting the bad case// (adding everything to a Map) in for every insert/move.// If you change this code, also update reconcileChildrenIterator() which// uses the same algorithm.} ...

July 6, 2022 · 3 min · jiezi

关于diff:diff

vue3 Diff算法patch函数的外围就是Diff算法。 function patch (oldVnode, vnode, parentElm) { if (!oldVnode) { addVnodes(parentElm, null, vnode, 0, vnode.length - 1); } else if (!vnode) { removeVnodes(parentElm, oldVnode, 0, oldVnode.length - 1); } else { if (sameVnode(oldVNode, vnode)) { patchVnode(oldVNode, vnode); } else { removeVnodes(parentElm, oldVnode, 0, oldVnode.length - 1); addVnodes(parentElm, null, vnode, 0, vnode.length - 1); } }}patch函数的性能次要是新旧VNode节点,将差别更新到视图上,所以传入参数有新旧2个VNode以及父节点element,先捋清函数的主逻辑: 首先当旧节点oldVnode不存在时,相当于新节点vnode代替没有的节点,所以间接用addVnodes将这些节点批量增加到parentElm下面。 if (!oldVnode) { addVnodes(parentElm, null, vnode, 0, vnode.length - 1);}同理,当新VNode节点vnode不存在时,相当于把旧的节点删除,所以间接应用removeVnodes函数进行批量的节点删除。 else if (!vnode) { removeVnodes(parentElm, oldVnode, 0, oldVnode.length - 1);}最初一种状况,就是当旧节点oldVnode和新节点vnode同时存在的状况下,须要判断它们是否属于sameVnode(雷同的节点)。如果是则执行patchVnode函数进行差别比照更新,否则删除旧节点,减少新节点。 ...

March 15, 2022 · 1 min · jiezi

React-中-Virtual-DOM-与-Diffing-算法的关系

前言这篇文章是基于 React 官方文档对于 Virtual DOM 的理念和 Diffing 算法的策略的整合。 Virtual DOM 是一种编程理念Virtual DOM 是一种编程理念。UI 信息被特定语言描述并保存到内存中,再通过特定的库,例如 ReactDOM 与真实的 DOM 同步信息。这一过程成为 协调 (Reconciliation)。 与之对应的数据结构Virtual DOM 反映到实际的数据结构上,就是每一个 React 的 fiber node // UI 组件描述const Span = (props) => <span></span>// 实际的 Fiber node structure{ stateNode: new HTMLSpanElement, type: "span", alternate: null, key: null, updateQueue: null, memoizedState: null, pendingProps: {}, memoizedProps: {}, tag: 1, effectTag: 0, nextEffect: null}这一抽离结构有点像 React 版本的 AST 抽象语法树。 ...

August 27, 2019 · 2 min · jiezi

从一个小Demo看React的diff算法

前言React的虚拟Dom和其diff算法,是React渲染效率远远高于传统dom操作渲染效率的主要原因。一方面,虚拟Dom的存在,使得在操作Dom时,不再直接操作页面Dom,而是对虚拟Dom进行相关操作运算。再通过运算结果,结合diff算法,得出变更过的部分Dom,进行局部更新。另一方面,当存在十分频繁的操作时,会进行操作的合并。直接在运算出最终状态之后才进行Dom的更新。从而大大提高Dom的渲染效率。\对于React如何通过diff算法来对比出做出变动的Dom,React内部有着复杂的运算过程,此文不做具体代码层级的讨论。仅仅通过一个小小Demo来宏观上的探讨下diff的运算思路。diff的对比思路React的diff对比是采用深度遍历的规则进行遍历比对的。以下图的Dom结构为例:&lt;img src=“https://github.com/ISummerRai...; style=“width: 620px” />\对比过程为:对比组件1(没有变化)-> 对比组件2(没有变化)-> 对比组件4(没有变化)-> 对比组件5(组件5被移除,记录一个移除操作)-> 对比组件3(没有变化)->对比组件3子组件(新增了一个组件5,记录一个新增操作)。对比结束,此时变动数据记录了两个节点的变动,在渲染时,便会执行一次组件5的移除,和一次组件5的新增。其它节点不做变更,从而实现页面Dom的更新操作。Demo初探接下来,我们设计一个简单的demo,来分析页面变化时的整个过程。\首先我们创建几个相同的Demo组件: import React, { Component } from ‘react’; export default class Demo1 extends Component { componentWillMount() { console.log(‘加载组件1’); } componentWillUnmount() { console.log(‘销毁组件1’) } render () { return <div>{this.props.children}</div> } }组件除了将其内部的Dom直接渲染之外,还在组件加载前和卸载前分别在控制台中打印出日志。\接下来通过代码组合出上图中的组件结构,并通过事件触发组件结构的变化。 // 变化前 <Demo1>1 <Demo2>2 <Demo4>4</Demo4> <Demo5>5</Demo5> </Demo2> <Demo3>3</Demo3> </Demo1> // 变化后 <Demo1>1 <Demo2>2 <Demo4>4</Demo4> </Demo2> <Demo3>3 <Demo5>5</Demo5> </Demo3> </Demo1>执行变更操作之后,控制台会打印出日志 加载组件5 销毁组件5结果通分析中一样,分别执行了一次组件5的加载操作和一次组件5的卸载操作。\接下来来分析一些复杂的情况。\首先看下面这种Dom的删除&lt;img src=“https://github.com/ISummerRai...; style=“width: 620px” />\按照前面的分析,比对过程为:对比组件1(没有变化)-> 对比组件2(没有变化)-> 对比组件4(组件4被移除,记录一个移除操作)-> 对比组件5(没有变化)-> 对比组件6(没有变化)-> 对比组件3(没有变化)。对比结束。按照这个分析,用代码进行测试后,控制台日志应该会输出: 销毁组件4这一条日志。然而,在实际测试后,会发现输出日志为: 加载组件5 加载组件6 销毁组件4 销毁组件5 销毁组件6可以发现,除了“销毁组件4”这一个操作之外,还进行了组件5和组件6的销毁和加载操作。难道是我们之前的分析是错误的?别急,我们再来进行另外一个实验:<img src=“https://github.com/ISummerRai...; style=“width: 620px” />\同样只删除了一个组件,只是删除的组件位置不同,按照上次的实验结果,控制台输出日志应该为: 加载组件4 加载组件5 销毁组件4 销毁组件5 销毁组件6然而,实际的实验结果又出乎我们的预料。实际输出结果仅为: 销毁组件6这个现象十分有趣。仅仅是删除了不同位置的组件,diff分析的过程却完全不一样。其实,如果你继续实验删除组件5,你会发现,所得的结果跟前两次也是完全不同。\其实diff算法在进行虚拟Dom的变更比对时,并不能精确的进行一对一的比对(当然react提供了解决方案,后面讨论)。当一个父节点发生变更时,会销毁掉其下所有的子节点。而其兄弟节点,则会按照节点顺序进行一对一的顺序比对。那么在上面第一个例子的比对顺序其实是这样的:对比组件1(没有变化)-> 对比组件2(没有变化)-> 对比组件4(组件4变更为组件5,记录一次组件4的移除操作和一次组件5的新增操作)->对比组件5(组件5变更为组件6,记录一次组件5的移除操作和一次组件6的新增操作)->对比组件6(组件6被移除,记录一次组件6的移除操作)。对比结束。按照这个分析思路,控制台的输出结果就不难理解了。\同样当我们在第二个例子中移除组件6时。组件4和组件5的顺序并没有变化,所以对比时,仍然是跟自身组件的虚拟Dom进行比对,没有变化,所以也就只有一次组件6的移除操作。\我们可以进一步通过新增及修改操作来进一步验证猜想。\通过在组件4前新增一个组件和在组件6后新增一个组件的对比。可以发现结果与我们的猜想结果完全一致。具体实验推演过程,此处不在赘述。\对于修改,由于修改并未改变该组件及其兄弟组件的个数及顺序,所以仅仅会执行替换组件及其子组件的新增操作和被替换组件的移除操作。\同级的组件分析完了,那么如果是跨层级的组件操作呢?比如下面这种dom变更:&lt;img src=“https://github.com/ISummerRai...; style=“width: 620px” />\这种变更,由于组件2,组件4,组件5三个组件的结构均未有任何变化,那么会不会复用其整个结构,只进行相对位置的变更呢?实验发现,控制台日志输出为: 加载组件3 加载组件2 加载组件4 加载组件5 销毁组件2 销毁组件4 销毁组件5 销毁组件3可见组件2及其子组件发生变化时,组件2以及其下的所有子组件均会被重新渲染。那么为什么组件3也会重新渲染呢?其实原因并不是其增加了子节点,而是因为其兄弟节点2被移除,影响了其相对位置而造成的。其完整的对比流程为:对比组件1(没有变化)-> 对比组件2(组件二变更为组件3,记录一次组件2的移除操作以及其子组件:组件4和组件5的移除操作,同时记录组件3的新增操作,以及其子组件:组件2,组件4和组件5的移除操作)-> 对比组件3(组件3被移除,记录一次组件3的移除操作 \分析可见:当一个节点变化时,其下的所有子节点会全部被重新渲染。比如在上个例子中,不进行结构的变更,只是将组件2替换为组件6,组件4和组件5保持不变,但由于组件4和组件5是组件2的子组件,组件2的变更依然会导致组件4和组件4被重新渲染。\此外,分析输出的结果,可以看到,react在进行局部Dom的更新时,会先执行新组件的加载,再执行组件的移除操作。被忽略的key在我们以前的开发工作中,肯定遇到过列表的渲染。此时React会强制我们为列表的每一条数据设置一个唯一的key值(否则控制台会报警告),并且官方禁止使用列表数据的下标来作为key值。在React 16及以后版本中,新增的以数组的形式来渲染多个同级的兄弟节点的写法中,同样要求我们为每一项添加唯一key值。你可能很疑惑这个必须加的key,似乎并没有什么实质的作用,为何却是一个必加项。渲染效率的提升其实,在React进行diff运算时,key值是十分关键的,因为每一个key就是该虚拟Dom节点的身份证,在我们之前的实验中,由于没有定义key值,diff运算在进行虚拟Dom的比对时,并不知道这个虚拟Dom跟之前的哪个虚拟Dom是一样的,所以只能采用顺序比对的方案,进行一对一比对。所以才有了之前分析中的由于位置的不同,导致了完全不同的输出结果。而当我们为每一个组件添加key值之后,由于有了唯一标示,在进行diff运算时,便能进行精确的比对,不再受到位置变动的影响。\回到最初的删除实验,为每一个组件添加上唯一的key:&lt;img src=“https://github.com/ISummerRai...; style=“width: 620px” /> // 变化前 <Demo1 key={1}>1 <Demo2 key={2}>2 <Demo4 key={4}>4</Demo4> <Demo5 key={5}>5</Demo5> <Demo6 key={6}>6</Demo6> </Demo2> <Demo3 key={3}>3</Demo3> </Demo1> // 变化后 <Demo1 key={1}>1 <Demo2 key={2}>2 <Demo4 key={4}>4</Demo4> <Demo5 key={5}>5</Demo5> <Demo6 key={6}>6</Demo6> </Demo2> <Demo3 key={3}>3</Demo3> </Demo1>运行发现,其输出日志正是我们最初设想的那样: 销毁组件4相对于没有key值的操作,避免了组件5和组件6的重新渲染。大大提高了渲染的效率。此时,为什么列表类数据必须加一个唯一的key值,就显而易见了。试想一下在一个无限滚动的移动端列表页面,加载了1000条数据。此时将第一条删除,那么,在没有key值的情况下,要重新渲染这个列表,需要将第一条之后的999条数据全部重新渲染。而有了key值,仅仅只需要对第一条数据进行一次移除操作就可以完成。可见,key值对渲染效率的提升,绝对是巨大的。\key不可设置为数据下标那么,为什么不能将key值设置为数据的下标呢?其实很简单,因为下标都是从0开始的,还是这个移动端的列表,删除了第一条数据,如果将key值设置为了数据下标。那么原来的key值为1的数据,在重新渲染后,key值会重新被设置为0,那么在进行比对时,会把这条数据跟变更前的key为0的数据进行比对,很明显,这两条数据并不是同一条,所以依然会因为数据不同,而导致整个列表的重新渲染。\key值必须唯一?除此之外,还有一个开发中的共识,就是key值必须唯一。但key值真的不能相同吗?\按照之前的实验以及分析,可以看出:当在进行兄弟节点的比对时,key值能够作为唯一的标示进行精确的比对。但是对于非兄弟组件,由于diff运算采用的是深度遍历,且父组件的变动会完全更新子组件,所以理论上key值对于非兄弟组件的作用,就显得微乎其微。那么对于非兄弟组件,key值相同应该是可行的。那么用实验验证一下我们的猜想。 // 变更前 <Demo1 key={1}>1 <Demo2 key={1}>2 <Demo4 key={4}>4</Demo4> <Demo5 key={5}>5</Demo5> <Demo6 key={6}>6</Demo6> </Demo2> <Demo3 key={2}>3 <Demo4 key={4}>4</Demo4> <Demo5 key={5}>5</Demo5> <Demo6 key={6}>6</Demo6> </Demo3> </Demo1> // 变更后 <Demo1 key={1}>1 <Demo2 key={1}>2 <Demo5 key={5}>5</Demo5> <Demo6 key={6}>6</Demo6> </Demo2> <Demo3 key={2}>3 <Demo4 key={4}>4</Demo4> <Demo6 key={6}>6</Demo6> </Demo3> </Demo1>在这个实验中,组件1和组件2有着相同的key值,且组件2和组件3的子组件也有着相同的key值,然而运行该代码,却并没有关于key值相同的警告。执行Dom变更后,日志输出也同之前的猜想没有出入。可见我们的猜想是正确的,key值并非需要绝对唯一,只是需要保证在同一个父节点下的兄弟节点中唯一便可以了。\key的更多用法除了上面提到的这些之外,在了解了key的作用机制之后,还可以利用key值来实现一些其它的效果。比如可以利用key值来更新一个拥有自状态的组件,通过修改该组件的key值,便可以达到使该组件重新渲染到初始状态的效果。此外,key值除了在列表中使用之外,在任何会操作dom,比如新增,删除这种影响兄弟节点顺序的情况,都可以通过添加key值的方法来提高渲染的效率。 ...

April 18, 2019 · 1 min · jiezi

浅析vue2.0的diff算法

一、前言如果不了解virtual dom,要理解diff的过程是比较困难的。虚拟dom对应的是真实dom, 使用document.CreateElement 和 document.CreateTextNode创建的就是真实节点。vue2.0才开始使用了virtual dom,有向react靠拢的意思。文章首发地址:https://www.mwcxs.top/page/56…二、虚拟dom首先,我们先看一下真实的dom,打印出一个空元素的第一层属性,可以看到标准让元素实现的东西太多了。如果每次都重新生成新的元素,对性能是巨大的浪费。var mydiv = document.createElement(‘div’);for(var item in mydiv){ console.log(item );}到底什么是virtual dom呢?通俗易懂的来说就是用一个简单的对象去代替复杂的dom对象。举个简单的例子,我们在body里插入一个class为a的div。var mydiv = document.createElement(‘div’);mydiv.className = ‘a’;document.body.appendChild(mydiv);对于这个div我们可以用一个简单的对象mydivVirtual代表它,它存储了对应dom的一些重要参数,在改变dom之前,会先比较相应虚拟dom的数据,如果需要改变,才会将改变应用到真实dom上。//伪代码var mydivVirtual = { tagName: ‘DIV’, className: ‘a’};var newmydivVirtual = { tagName: ‘DIV’, className: ‘b’}if(mydivVirtual.tagName !== newmydivVirtual.tagName || mydivVirtual.className !== newmydivVirtual.className){ change(mydiv)}// 会执行相应的修改 mydiv.className = ‘b’;//最后 <div class=‘b’></div>为什么不直接修改dom而需要加一层virtual dom呢?很多时候手工优化dom确实会比virtual dom效率高,对于比较简单的dom结构用手工优化没有问题,但当页面结构很庞大,结构很复杂时,手工优化会花去大量时间,而且可维护性也不高,不能保证每个人都有手工优化的能力。至此,virtual dom的解决方案应运而生。virtual dom是“解决过多的操作dom影响性能”的一种解决方案。virtual dom很多时候都不是最优的操作,但它具有普适性,在效率、可维护性之间达平衡。virutal dom的意义:1、提供一种简单对象去代替复杂的dom对象,从而优化dom操作2、提供一个中间层,js去写ui,ios安卓之类的负责渲染,就像reactNative一样。三、diff算法vue的diff位于patch.js文件中,该算法来源于snabbdom,复杂度为O(n)。了解diff过程可以让我们更高效的使用框架。一篇相当经典的文章React’s diff algorithm中的图,react的diff其实和vue的diff大同小异。所以这张图能很好的解释过程。特点:1、比较只会在同层级进行, 不会跨层级比较。举个形象的例子<!– 之前 –><div> <!– 层级1 –> <p> <!– 层级2 –> <b> aoy </b> <!– 层级3 –> <span>diff</Span> </P> </div><!– 之后 –><div> <!– 层级1 –> <p> <!– 层级2 –> <b> aoy </b> <!– 层级3 –> </p> <span>diff</Span></div>我们可能期望将<span>直接移动到<p>的后边,这是最优的操作。但是实际的diff操作是:1、移除<p>里的<span>;2、在创建一个新的<span>插到<p>的后边。因为新加的<span>在层级2,旧的在层级3,属于不同层级的比较。四、源码分析vue的diff位于patch.js文件中,diff的过程就是调用patch函数,就像打补丁一样修改真实dom。4.1patch方法function patch (oldVnode, vnode) { if (sameVnode(oldVnode, vnode)) { patchVnode(oldVnode, vnode) } else { const oEl = oldVnode.el let parentEle = api.parentNode(oEl) createEle(vnode) if (parentEle !== null) { api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) api.removeChild(parentEle, oldVnode.el) oldVnode = null } } return vnode}patch函数有两个参数,vnode和oldVnode,也就是新旧两个虚拟节点。在这之前,我们先了解完整的vnode都有什么属性,举个一个简单的例子:// body下的 <div id=“v” class=“classA”><div> 对应的 oldVnode 就是{ el: div //对真实的节点的引用,本例中就是document.querySelector(’#id.classA’) tagName: ‘DIV’, //节点的标签 sel: ‘div#v.classA’ //节点的选择器 data: null, // 一个存储节点属性的对象,对应节点的el[prop]属性,例如onclick , style children: [], //存储子节点的数组,每个子节点也是vnode结构 text: null, //如果是文本节点,对应文本节点的textContent,否则为null}el属性引用的是此 virtual dom对应的真实dom,patch的vnode参数的el最初是null,因为patch之前它还没有对应的真实dom。patch的第一部分if (sameVnode(oldVnode, vnode)) { patchVnode(oldVnode, vnode)}sameVnode函数就是看这两个节点是否值得比较,代码相当简单:function sameVnode(oldVnode, vnode){ return vnode.key === oldVnode.key && vnode.sel === oldVnode.sel}两个vnode的key和sel相同才去比较它们,比如p和span,div.classA和div.classB都被认为是不同结构而不去比较它们。如果值得比较会执行patchVnode(oldVnode, vnode),稍后会详细讲patchVnode函数。当节点不值得比较,进入else中else { const oEl = oldVnode.el let parentEle = api.parentNode(oEl) createEle(vnode) if (parentEle !== null) { api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) api.removeChild(parentEle, oldVnode.el) oldVnode = null } }过程如下:取得oldvnode.el的父节点,parentEle是真实domcreateEle(vnode)会为vnode创建它的真实dom,令vnode.el =真实domparentEle将新的dom插入,移除旧的dom当不值得比较时,新节点直接把老节点整个替换了最后return vnodepatch最后会返回vnode,vnode和进入patch之前的不同在哪?没错,就是vnode.el,唯一的改变就是之前vnode.el = null, 而现在它引用的是对应的真实dom。var oldVnode = patch (oldVnode, vnode)至此完成一个patch过程。4.2patchNode方法两个节点值得比较时,会调用patchVnode函数patchVnode (oldVnode, vnode) { const el = vnode.el = oldVnode.el let i, oldCh = oldVnode.children, ch = vnode.children if (oldVnode === vnode) return if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) { api.setTextContent(el, vnode.text) }else { updateEle(el, vnode, oldVnode) if (oldCh && ch && oldCh !== ch) { updateChildren(el, oldCh, ch) }else if (ch){ createEle(vnode) //create el’s children dom }else if (oldCh){ api.removeChildren(el) } }}const el = vnode.el = oldVnode.el ,让vnode.el引用到现在的真实dom,当el修改时,vnode.el会同步变化。节点的比较有5种情况:1、if (oldVnode === vnode),他们的引用一致,可以认为没有变化。2、if(oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text),文本节点的比较,需要修改,则会调用Node.textContent = vnode.text。3、if( oldCh && ch && oldCh !== ch ), 两个节点都有子节点,而且它们不一样,这样我们会调用updateChildren函数比较子节点,这是diff的核心,后边会讲到。4、else if (ch),只有新的节点有子节点,调用createEle(vnode),vnode.el已经引用了老的dom节点,createEle函数会在老dom节点上添加子节点。5、else if (oldCh),新节点没有子节点,老节点有子节点,直接删除老节点。4.3updateChildren方法updateChildren (parentElm, oldCh, newCh) { let oldStartIdx = 0, newStartIdx = 0 let oldEndIdx = oldCh.length - 1 let oldStartVnode = oldCh[0] let oldEndVnode = oldCh[oldEndIdx] let newEndIdx = newCh.length - 1 let newStartVnode = newCh[0] let newEndVnode = newCh[newEndIdx] let oldKeyToIdx let idxInOld let elmToMove let before while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (oldStartVnode == null) { //对于vnode.key的比较,会把oldVnode = null oldStartVnode = oldCh[++oldStartIdx] }else if (oldEndVnode == null) { oldEndVnode = oldCh[–oldEndIdx] }else if (newStartVnode == null) { newStartVnode = newCh[++newStartIdx] }else if (newEndVnode == null) { newEndVnode = newCh[–newEndIdx] }else if (sameVnode(oldStartVnode, newStartVnode)) { patchVnode(oldStartVnode, newStartVnode) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] }else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode) oldEndVnode = oldCh[–oldEndIdx] newEndVnode = newCh[–newEndIdx] }else if (sameVnode(oldStartVnode, newEndVnode)) { patchVnode(oldStartVnode, newEndVnode) api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el)) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[–newEndIdx] }else if (sameVnode(oldEndVnode, newStartVnode)) { patchVnode(oldEndVnode, newStartVnode) api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el) oldEndVnode = oldCh[–oldEndIdx] newStartVnode = newCh[++newStartIdx] }else { // 使用key时的比较 if (oldKeyToIdx === undefined) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 有key生成index表 } idxInOld = oldKeyToIdx[newStartVnode.key] if (!idxInOld) { api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el) newStartVnode = newCh[++newStartIdx] } else { elmToMove = oldCh[idxInOld] if (elmToMove.sel !== newStartVnode.sel) { api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el) }else { patchVnode(elmToMove, newStartVnode) oldCh[idxInOld] = null api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el) } newStartVnode = newCh[++newStartIdx] } } } if (oldStartIdx > oldEndIdx) { before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx) }else if (newStartIdx > newEndIdx) { removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) }}直接看源码可能比较难以滤清其中的关系,我们通过图来看一下首先,在新老两个VNode节点的左右头尾两侧都有一个变量标记,在遍历过程中这几个变量都会向中间靠拢。当oldStartIdx <= oldEndIdx或者newStartIdx <= newEndIdx时结束循环。索引与VNode节点的对应关系:oldStartIdx => oldStartVnodeoldEndIdx => oldEndVnodenewStartIdx => newStartVnodenewEndIdx => newEndVnode在遍历中,如果存在key,并且满足sameVnode,会将该DOM节点进行复用,否则则会创建一个新的DOM节点。首先,oldStartVnode、oldEndVnode与newStartVnode、newEndVnode两两比较一共有2*2=4种比较方法。当新老VNode节点的start或者end满足sameVnode时,也就是sameVnode(oldStartVnode, newStartVnode)或者sameVnode(oldEndVnode, newEndVnode),直接将该VNode节点进行patchVnode即可。如果oldStartVnode与newEndVnode满足sameVnode,即sameVnode(oldStartVnode, newEndVnode)。这时候说明oldStartVnode已经跑到了oldEndVnode后面去了,进行patchVnode的同时还需要将真实DOM节点移动到oldEndVnode的后面。如果oldEndVnode与newStartVnode满足sameVnode,即sameVnode(oldEndVnode, newStartVnode)。这说明oldEndVnode跑到了oldStartVnode的前面,进行patchVnode的同时真实的DOM节点移动到了oldStartVnode的前面。如果以上情况均不符合,则通过createKeyToOldIdx会得到一个oldKeyToIdx,里面存放了一个key为旧的VNode,value为对应index序列的哈希表。从这个哈希表中可以找到是否有与newStartVnode一致key的旧的VNode节点,如果同时满足sameVnode,patchVnode的同时会将这个真实DOM(elmToMove)移动到oldStartVnode对应的真实DOM的前面。当然也有可能newStartVnode在旧的VNode节点找不到一致的key,或者是即便key相同却不是sameVnode,这个时候会调用createElm创建一个新的DOM节点。到这里循环已经结束了,那么剩下我们还需要处理多余或者不够的真实DOM节点。 ...

February 28, 2019 · 3 min · jiezi

使用 ale.js 制作一个小而美的表格编辑器(4)

今天来教大家如何使用 ale.js 制作一个小而美的表格编辑器,首先先上 gif:是不是还是有一点非常 cool 的感觉的?那么我们现在开始吧!这是我们这篇文章结束后完成的效果(如果想继续完成请访问第五篇文章):ok,那继续开始吧(本篇文章是表格编辑器系列的第四篇文章,如果您还没有看过第一篇,请访问 第一篇文章(开源中国)):首先我们需要先添加一个 Sreach 按钮(在 handleTemplateRender 函数里)://把 定义DOM基本结构 的 returnValvar returnVal = “<table><thead><tr>”//改为var returnVal = “<table><thead><button class=‘a-btn’ onclick=‘this.methods.trigSearch()’>Search</button><tr>“之后我们需要在 methods 里面添加一个 trigSearch 函数:trigSearch(){ if (this.data.isOpenSearch) { this.data.data = this.staticData.preData; this.data.isOpenSearch = false; } else { this.data.isOpenSearch = true; }}接下来,我们需要在 data 里添加一个 isOpenSearch 变量,默认为 false:isOpenSearch: false还要在 staticData 里添加一个 preData,用来存储 bookData 数据:preData: [ [“The Lord of the Rings”, " J. R. R. Tolkien”, “English”, “1954-1955”, “150 million”], [“The Little Prince”, “Antoine de Saint-Exupéry”, “French”, “1943”, “140 million”], [“Dream of the Red Chamber”, “Cao Xueqin”, “Chinese”, “1791”, “100 million”]]之后我们要在 handleTemplateRender 函数中增加一个判断,判断是否 openSearch 开启了://把//循环遍历bookHeader数据并输出this.data.bookHeader.forEach(function(val, i, arr) { returnVal += “<th onclick=‘this.methods.handleTheadOnclick(event)’>” + val + (sortBy === i ? getSortSign() : ‘’) + “</th>”;})returnVal += “</thead></tr><tbody>”;//改为//循环遍历bookHeader数据并输出this.data.bookHeader.forEach(function(val, i, arr) { returnVal += “<th onclick=‘this.methods.handleTheadOnclick(event)’>” + val + (sortBy === i ? getSortSign() : ‘’) + “</th>”;})var cellId = -1;if (this.data.isOpenSearch) { //这里增加判断 returnVal += “</tr><tr>”; for (var i = 0; i < this.data.bookHeader.length; i++) { cellId++; returnVal += “<th><input data-cell=’” + cellId + “’ type=‘text’ oninput=‘this.methods.handleSearch(this, event)’ placeHolder=‘Search…’></th>”; }}returnVal += “</thead></tr><tbody>";然后我们要继续在 methods 里面添加一个名叫 handleSearch 的函数:handleSearch(el, e) { var newData = [], elVal = el.value; this.staticData.preData.forEach(function(val, i, arr) { //判断是否拥有输入的字段 if (val[e.target.dataset.cell].indexOf(elVal) !== -1) { //添加到返回对列中 newData.push(val); } }); this.data.bookData = newData;}现在我们就已经实现了搜索功能,恭喜!这是我们目前全部的 js 代码:Ale(“excel”, { template() { return this.methods.handleTemplateRender(); }, methods: { handleTemplateRender() { //定义DOM基本结构 var returnVal = “<table><thead><button class=‘a-btn’ onclick=‘this.methods.trigSearch()’>Search</button><tr>”, getSortSign = this.methods.getSortSign, sortBy = this.staticData.sortBy, rowId = -1, edit = this.data.edit; //循环遍历bookHeader数据并输出 this.data.bookHeader.forEach(function(val, i, arr) { returnVal += “<th onclick=‘this.methods.handleTheadOnclick(event)’>” + val + (sortBy === i ? getSortSign() : ‘’) + “</th>”; }) var cellId = -1; if (this.data.isOpenSearch) { returnVal += “</tr><tr>”; for (var i = 0; i < this.data.bookHeader.length; i++) { cellId++; returnVal += “<th><input data-cell=’” + cellId + “’ type=‘text’ oninput=‘this.methods.handleSearch(this, event)’ placeHolder=‘Search…’></th>”; } } returnVal += “</thead></tr><tbody>”; //循环遍历bookData数据并输出 this.data.bookData.forEach(function(thisBook, i, arr) { var cellId = -1; rowId++; //输出一行 returnVal += “<tr>”; thisBook.forEach(function(val, i, arr) { cellId++; if (rowId === edit.row && cellId === edit.cell) { returnVal += “<td><form data-cell=’” + cellId + “’ data-row=’” + rowId + “’ onsubmit=‘this.methods.save(event)’><input type=‘text’ value=’” + val + “’></form></td>”; } else { returnVal += “<td data-cell=’” + cellId + “’ data-row=’” + rowId + “’ ondblclick=‘this.methods.handleBlockOndblclick(event)’>” + val + “</td>”; } }) returnVal += “</tr>”; }) returnVal += “</tbody></table>”; //返回DOM结构 return returnVal; }, handleTheadOnclick(e) { this.methods.changeSortType(e); this.methods.sortList(e); }, changeSortType(e) { this.staticData.sortBy = e.target.cellIndex; if (this.staticData.sortType === “up”) { this.staticData.sortType = “down”; } else { this.staticData.sortType = “up”; } }, sortList(e) { var index = e.target.cellIndex; if (this.staticData.sortType === “up”) { this.data.bookData.sort(function(a, b) { return a[index].charCodeAt(0) > b[index].charCodeAt(0) ? 1 : -1; }) } else { this.data.bookData.sort(function(a, b) { return a[index].charCodeAt(0) < b[index].charCodeAt(0) ? 1 : -1; }) } this.data.bookData = this.data.bookData; }, getSortSign() { if (this.staticData.sortType === “up”) { return ‘\u2191’; } else { return ‘\u2193’; } }, handleBlockOndblclick(e) { if (!this.staticData.isOpenEdit) { this.staticData.isOpenEdit = true; this.data.edit = { row: parseInt(e.target.dataset.row), cell: parseInt(e.target.dataset.cell) } } }, save(e) { e.preventDefault(); var input = e.target.firstChild; this.staticData.isOpenEdit = false; this.data.edit = { row: -1, cell: -1 } this.data.bookData[e.target.dataset.row][e.target.dataset.cell] = input.value; this.data.bookData = this.data.bookData; }, trigSearch() { if (this.data.isOpenSearch) { this.data.bookData = this.staticData.preData; this.data.isOpenSearch = false; } else { this.data.isOpenSearch = true; } }, handleSearch(el, e) { var newData = [], elVal = el.value; this.staticData.preData.forEach(function(val, i, arr) { if (val[e.target.dataset.cell].indexOf(elVal) !== -1) { newData.push(val); } }); this.data.bookData = newData; } }, data: { bookHeader: [ “Book”, “Author”, “Language”, “Published”, “Sales” ], bookData: [ [“The Lord of the Rings”, " J. R. R. Tolkien”, “English”, “1954-1955”, “150 million”], [“The Little Prince”, “Antoine de Saint-Exupéry”, “French”, “1943”, “140 million”], [“Dream of the Red Chamber”, “Cao Xueqin”, “Chinese”, “1791”, “100 million”] ], edit: { row: -1, cell: -1 }, isOpenSearch: false }, staticData: { sortBy: -1, sortType: ‘down’, isOpenEdit: false, preData: [ [“The Lord of the Rings”, " J. R. R. Tolkien”, “English”, “1954-1955”, “150 million”], [“The Little Prince”, “Antoine de Saint-Exupéry”, “French”, “1943”, “140 million”], [“Dream of the Red Chamber”, “Cao Xueqin”, “Chinese”, “1791”, “100 million”] ] } }) Ale.render(“excel”, { el: “#app” })如果想了解更多,欢迎关注我在明天推出的第五篇教程,同时也关注一下 alejs 哦,感谢各位!(非常重要:如果有能力的话不妨去 Github 或 码云 上 star 一下我们吧!不过如果您特别喜欢 alejs 的话也可以 watch 或 fork 一下哦!十分感谢!) ...

January 21, 2019 · 4 min · jiezi

使用 ale.js 制作一个小而美的表格编辑器(3)

今天来教大家如何使用 ale.js 制作一个小而美的表格编辑器,首先先上 gif:是不是还是有一点非常 cool 的感觉的?那么我们现在开始吧!这是我们这篇文章结束后完成的效果(如果想继续完成请访问第四篇文章):ok,那继续开始吧(本篇文章是表格编辑器系列的第三篇文章,如果您还没有看过第一篇,请访问 第一篇文章(开源中国)):首先让我们把每一个列表项都添加一个他们的行数和列数作为 dataset 数据吧!先创建一个 rowId 变量://在 handleTemplateRender 函数里,我们把:var returnVal = “<table><thead><tr>”, getSortSign = this.methods.getSortSign, sortBy = this.staticData.sortBy;//改为var returnVal = “<table><thead><tr>”, getSortSign = this.methods.getSortSign, sortBy = this.staticData.sortBy, rowId = -1;然后再在 “循环遍历bookData数据并输出” 这行注释所对应的forEach函数的里面创建一个名叫 cellId 的变量:(就是输出td标签的forEach)//原来的代码this.data.bookData.forEach(function(thisBook, i, arr) { //输出一行 returnVal += “<tr>”; thisBook.forEach(function(val, i, arr) { //输出一列 returnVal += “<td>” + val + “</td>”; }) returnVal += “</tr>”;})//改为this.data.bookData.forEach(function(thisBook, i, arr) { var cellId = -1; //这里增加了一行代码 //输出一行 returnVal += “<tr>”; thisBook.forEach(function(val, i, arr) { //输出一列 returnVal += “<td>” + val + “</td>”; }) returnVal += “</tr>”;})当然这样还没完,我们还需要改为这样:this.data.bookData.forEach(function(thisBook, i, arr) { var cellId = -1; //这里让rowId++ rowId++; returnVal += “<tr>”; thisBook.forEach(function(val, i, arr) { //这里让cellId++ cellId++; //注意这里写了 dataset returnVal += “<td data-row=’” + rowId + “’ data-cell=’” + cellId + “’>” + val + “</td>”; }) returnVal += “</tr>”;})这样你就可以看到在控制台上已经输出了它们的 dataset:接下来,让我们往 data 里面添加一个名叫 edit 的对象,用来指定我们点击的到底是哪个表格:edit: { row: -1, //默认为-1,因为没有选中表格 cell: -1}然后,我们把下面这行代码,给他添加一个 ondblclick:returnVal += “<td data-row=’” + rowId + “’ data-cell=’” + cellId + “’>” + val + “</td>”;//改为newVal += “<td data-cell=’” + cellId + “’ data-row=’” + rowId + “’ ondblclick=‘this.methods.handleBlockOndblclick(event)’>” + val + “</td>";然后我们在 methods 对象里面添加一个 handleBlockOndblclick 的函数:handleBlockOndblclick(e) { if (!this.staticData.isOpenEdit) { //判断是否开启了edit this.staticData.isOpenEdit = true; //获取并设置目标格位置 this.data.edit = { row: parseInt(e.target.dataset.row), cell: parseInt(e.target.dataset.cell) } }}因为在 handleBlockOndblclick 函数里面,我们用到了静态数据的 isOpenEdit,所以我们需要定义一个:isOpenEdit: falseok,那么之后我们需要再改进一下输出 book 数据的那一行代码,把他改成这样:thisBook.forEach(function(val, i, arr) { cellId++; if (rowId === edit.row && cellId === edit.cell) { returnVal += “<td><form data-cell=’” + cellId + “’ data-row=’” + rowId + “’ onsubmit=‘this.methods.save(event)’><input type=‘text’ value=’” + val + “’></form></td>”; } else { returnVal += “<td data-cell=’” + cellId + “’ data-row=’” + rowId + “’ ondblclick=‘this.methods.handleBlockOndblclick(event)’>” + val + “</td>”; }})接下来让我们在上方定义一个名叫 edit 的变量吧://把var returnVal = “<table><thead><tr>”, getSortSign = this.methods.getSortSign, sortBy = this.staticData.sortBy, rowId = -1;//改为var returnVal = “<table><thead><tr>”, getSortSign = this.methods.getSortSign, sortBy = this.staticData.sortBy, rowId = -1, edit = this.data.edit;之后我们还需要在 methods 里添加一个 save 函数,用来保存修改后的结果:save(e) { e.preventDefault(); var input = e.target.firstChild; this.staticData.isOpenEdit = false; this.data.edit = { row: -1, cell: -1 } this.data.bookData[e.target.dataset.row][e.target.dataset.cell] = input.value; this.data.bookData = this.data.bookData;}好了,那么现在我们的编辑器就可以正式运作了,我们已经实现了本篇文章最开始时所做的功能!(按回车可以保存修改结果)这是我们目前全部的 js 代码:Ale(“excel”, { template() { return this.methods.handleTemplateRender(); }, methods: { handleTemplateRender() { //定义DOM基本结构 var returnVal = “<table><thead><tr>”, getSortSign = this.methods.getSortSign, sortBy = this.staticData.sortBy, rowId = -1, edit = this.data.edit; //循环遍历bookHeader数据并输出 this.data.bookHeader.forEach(function(val, i, arr) { returnVal += “<th onclick=‘this.methods.handleTheadOnclick(event)’>” + val + (sortBy === i ? getSortSign() : ‘’) + “</th>”; }) returnVal += “</thead></tr><tbody>”; //循环遍历bookData数据并输出 this.data.bookData.forEach(function(thisBook, i, arr) { var cellId = -1; rowId++; //输出一行 returnVal += “<tr>”; thisBook.forEach(function(val, i, arr) { cellId++; if (rowId === edit.row && cellId === edit.cell) { returnVal += “<td><form data-cell=’” + cellId + “’ data-row=’” + rowId + “’ onsubmit=‘this.methods.save(event)’><input type=‘text’ value=’” + val + “’></form></td>”; } else { returnVal += “<td data-cell=’” + cellId + “’ data-row=’” + rowId + “’ ondblclick=‘this.methods.handleBlockOndblclick(event)’>” + val + “</td>”; } }) returnVal += “</tr>”; }) returnVal += “</tbody></table>”; //返回DOM结构 return returnVal; }, handleTheadOnclick(e) { this.methods.changeSortType(e); this.methods.sortList(e); }, changeSortType(e) { this.staticData.sortBy = e.target.cellIndex; if (this.staticData.sortType === “up”) { this.staticData.sortType = “down”; } else { this.staticData.sortType = “up”; } }, sortList(e) { var index = e.target.cellIndex; if (this.staticData.sortType === “up”) { this.data.bookData.sort(function(a, b) { return a[index].charCodeAt(0) > b[index].charCodeAt(0) ? 1 : -1; }) } else { this.data.bookData.sort(function(a, b) { return a[index].charCodeAt(0) < b[index].charCodeAt(0) ? 1 : -1; }) } this.data.bookData = this.data.bookData; }, getSortSign() { if (this.staticData.sortType === “up”) { return ‘\u2191’; } else { return ‘\u2193’; } }, handleBlockOndblclick(e) { if (!this.staticData.isOpenEdit) { this.staticData.isOpenEdit = true; this.data.edit = { row: parseInt(e.target.dataset.row), cell: parseInt(e.target.dataset.cell) } } }, save(e) { e.preventDefault(); var input = e.target.firstChild; this.staticData.isOpenEdit = false; this.data.edit = { row: -1, cell: -1 } this.data.bookData[e.target.dataset.row][e.target.dataset.cell] = input.value; this.data.bookData = this.data.bookData; } }, data: { bookHeader: [ “Book”, “Author”, “Language”, “Published”, “Sales” ], bookData: [ [“The Lord of the Rings”, " J. R. R. Tolkien”, “English”, “1954-1955”, “150 million”], [“The Little Prince”, “Antoine de Saint-Exupéry”, “French”, “1943”, “140 million”], [“Dream of the Red Chamber”, “Cao Xueqin”, “Chinese”, “1791”, “100 million”] ], edit: { row: -1, cell: -1 } }, staticData: { sortBy: -1, sortType: ‘down’, isOpenEdit: false }})Ale.render(“excel”, { el: “#app”})如果想了解更多,欢迎关注我在明天推出的第四篇教程,同时也关注一下 alejs 哦,感谢各位!(非常重要:如果有能力的话不妨去 Github 或 码云 上 star 一下我们吧!不过如果您特别喜欢 alejs 的话也可以 watch 或 fork 一下哦!十分感谢!) ...

January 17, 2019 · 4 min · jiezi

使用 ale.js 制作一个小而美的表格编辑器(2)

今天来教大家如何使用 ale.js 制作一个小而美的表格编辑器,首先先上 gif:是不是还是有一点非常 cool 的感觉的?那么我们现在开始吧!这是我们这篇文章结束后完成的效果(如果想继续完成请访问第三篇文章):ok,那继续开始吧(本篇文章是表格编辑器系列的第二篇文章,如果您还没有看过第一篇,请访问 第一篇文章(开源中国)):首先我们写一个名叫 staticData 的 object,里面添加2个属性,分别是 sortBy 和 sortType:(关于 staticData 这里不做阐述,如果有需要请访问 cn.alejs.org)staticData: { sortBy: -1, //排序列索引,默认没有,所以为-1 sortType: ‘down’ //排序类型,默认为降序}之后我们在 th 标签里面增加一个 onclick 属性,指向 methods 里面的 handleTheadOnclick 函数,并传递一个 event 作为参数:(之前的代码)this.data.bookHeader.forEach(function(val, i, arr) { returnVal += “<th>” + val + “</th>”;})(改进后的代码)this.data.bookHeader.forEach(function(val, i, arr) { returnVal += “<th onclick=‘this.methods.handleTheadOnclick(event)’>” + val + “</th>”;})为了让他显示排序时的小箭头,我们需要再改进这行代码为这样:this.data.bookHeader.forEach(function(val, i, arr) { returnVal += “<th onclick=‘this.methods.handleTheadOnclick(event)’>” + val + (sortBy === i ? getSortSign() : ‘’) + “</th>”;})由于 sortBy 变量和 getSortSign 函数变量还未定义,所以我们要在之前的代码里引用一下:(原来的代码)var returnVal = “<table><thead><tr>";(改进后的代码)var returnVal = “<table><thead><tr>”, getSortSign = this.methods.getSortSign, sortBy = this.staticData.sortBy;其中,sortBy 变量指向的是静态 data 里的 sortBy 变量,这个我们已经定义了,所以先不管他。而另一个 getSortSign 函数还没有定义,所以我们在 methods 里面定义一下他:getSortSign() { if (this.staticData.sortType === “up”) { return ‘\u2191’; } else { return ‘\u2193’; }}其功能主要就是判断是正序还是倒叙,并分别输出正反小箭头。之后我们就需要完成 handleTheadOnclick 函数了。它分别引用了 changeSortType 和 sortList 函数:handleTheadOnclick(e) { this.methods.changeSortType(e); this.methods.sortList(e);}其中 changeSortType 函数是用来改变排序类型的,而 sortList 函数使用来排序的。那么我们先完成 changeSortType 函数吧:changeSortType(e) { this.staticData.sortBy = e.target.cellIndex; if (this.staticData.sortType === “up”) { this.staticData.sortType = “down”; } else { this.staticData.sortType = “up”; }}ok,这个函数的功能和实现都非常简单,其中 cellIndex 是用来获取这是属于表格中那一列的。那么 sortList 函数的实现则稍微有些复杂:sortList(e) { //获取列索引值 var index = e.target.cellIndex; //判断排序类型 if (this.staticData.sortType === “up”) { //使用数组的 sort 函数进行排序,分别按 charCode 进行排序 this.data.bookData.sort(function(a, b) { return a[index].charCodeAt(0) > b[index].charCodeAt(0) ? 1 : -1; }) } else { this.data.bookData.sort(function(a, b) { return a[index].charCodeAt(0) < b[index].charCodeAt(0) ? 1 : -1; }) } this.data.bookData = this.data.bookData;}这是我们目前的全部 js 代码:Ale(“excel”, { template() { return this.methods.handleTemplateRender(); }, methods: { handleTemplateRender() { //定义DOM基本结构 var returnVal = “<table><thead><tr>”, getSortSign = this.methods.getSortSign, sortBy = this.staticData.sortBy; //循环遍历bookHeader数据并输出 this.data.bookHeader.forEach(function(val, i, arr) { returnVal += “<th onclick=‘this.methods.handleTheadOnclick(event)’>” + val + (sortBy === i ? getSortSign() : ‘’) + “</th>”; }) returnVal += “</thead></tr><tbody>”; //循环遍历bookData数据并输出 this.data.bookData.forEach(function(thisBook, i, arr) { //输出一行 returnVal += “<tr>”; thisBook.forEach(function(val, i, arr) { //输出一列 returnVal += “<td>” + val + “</td>”; }) returnVal += “</tr>”; }) returnVal += “</tbody></table>”; //返回DOM结构 return returnVal; }, handleTheadOnclick(e) { this.methods.changeSortType(e); this.methods.sortList(e); }, changeSortType(e) { this.staticData.sortBy = e.target.cellIndex; if (this.staticData.sortType === “up”) { this.staticData.sortType = “down”; } else { this.staticData.sortType = “up”; } }, sortList(e) { var index = e.target.cellIndex; if (this.staticData.sortType === “up”) { this.data.bookData.sort(function(a, b) { return a[index].charCodeAt(0) > b[index].charCodeAt(0) ? 1 : -1; }) } else { this.data.bookData.sort(function(a, b) { return a[index].charCodeAt(0) < b[index].charCodeAt(0) ? 1 : -1; }) } this.data.bookData = this.data.bookData; }, getSortSign() { if (this.staticData.sortType === “up”) { return ‘\u2191’; } else { return ‘\u2193’; } } }, data: { bookHeader: [ “Book”, “Author”, “Language”, “Published”, “Sales” ], bookData: [ [“The Lord of the Rings”, " J. R. R. Tolkien”, “English”, “1954-1955”, “150 million”], [“The Little Prince”, “Antoine de Saint-Exupéry”, “French”, “1943”, “140 million”], [“Dream of the Red Chamber”, “Cao Xueqin”, “Chinese”, “1791”, “100 million”] ] }, staticData: { sortBy: -1, sortType: ‘down’ }})Ale.render(“excel”, { el: “#app”})然后效果就如下图所示啦:如果想了解更多,欢迎关注我在明天推出的第三篇教程,同时也关注一下 alejs 哦,感谢各位!(非常重要:如果有能力的话不妨去 Github 或 码云 上 star 一下我们吧!不过如果您特别喜欢 alejs 的话也可以 watch 或 fork 一下哦!十分感谢!) ...

January 16, 2019 · 3 min · jiezi