关于virtual-dom:教你在excel表格如何快速提取名字和电话号码

很多时候退职场的办公中会遇到题目中的问题,能够借助软件,金芝号码提取整顿助手,来实现,你能够栢渡搜一下它,软件作者的徽veve188。有时候在办公时,咱们会接管到他人发过来的一个excel表格,因为对方图一时不便输出的起因,电话号码和地址等都被输出在了同一个单元格中,而且数量很多。然而为了便于统计和计算,咱们想excel表格如何疾速提取名字和电话号码,该如何整顿操作呢?去下一个软件,金芝号码提取整顿助手,轻松洁净地整顿好。本篇就以图文解说的模式来通知大家怎么做到。 关上你的excel表格,常见的格局是,右边是公司,两头是名字,后左边是含有电话号码的芜杂文本(手机号码、固话、汉字、字母等混合)。咱们的目标是名字和电话号码一一对应提取下来。 第一步:咱们必须严格把资料整顿成咱们规定的格局:放在excel表格外面,成为两列相邻,右边一列是名字,左边是含有电话号码芜杂文本的一列,必须肯定要是这个格局,见下图。 第二步:你用鼠标点一下这两列文本,把两列文本全副复制好(技巧:鼠标点击所在列的头部字母而后往右拉,可全选两列,而后鼠标右击,复制)。 第三步:关上软件,“金芝号码提取整顿助手”,选第二个性能“对应名字提取”,把方才复制好的文本,鼠标右击点“粘贴”到软件上。 第四步:点“提取手机号”,就能看到提取后果展现在软件上,名字在右边一列,手机号码在左边一列,两头有个空格离开,排的洁净参差。坐等提醒你“提取实现”,而后点“导出excel”即可,整顿好的就是下图的成果。 当初是互联网时代,很多的问题都能够通过技术来解决,尤其是像这类数量宏大的问题:excel表格如何疾速提取名字和电话号码,咱们不可能手动一个个去整顿,能够通过软件,金芝号码提取整顿助手,能够栢渡搜一下它,用它来疾速实现,操作简略,省时省力。应用辅助工具,当你的工作效率高了,那么你一天的工作将会播种感满满,身心轻松,也将更元气满满投入第二天的工作。

November 4, 2021 · 1 min · jiezi

关于virtual-dom:vue-源码解析322虚拟dom

后面写过一个snabbdom的解析,vue2.0版本用的就是这个,而后在他的根底上增加了一些性能vue-clic生成我的项目中的render 中的 h 函数,就是createElement()src/core/instance/render.js中创立h函数Vue.init 的时候会调用initRender 初始化_render, 在 vm._render() 中调用了,用户传递的或者编译生成的 render 函数,这个时候传递了 createElement。 vm.c 和 vm.$createElement 外部都调用了 createElement,不同的是最初一个参数。vm.c 在编译生成的render 函数外部会调用,vm.$createElement 在用户传入的 render 函数外部调用。当用户传入render 函数的时候,要对用户传入的参数做解决 通过前一篇vm.$mount咱们晓得vue 外部通过这个挂载了元素,这个办法最初执行了mountComponent办法 这个办法定义了updateComponent,updateComponent中通过vm._render()(外面调用vm._c或者vm.$createElement)生成虚构dom,而后vm._update比照 两次的虚构dom进行更新.创立渲染Watcher时会调用一次updateComponent来渲染dom.后续属性变动时 放入队列的渲染watcher,最初执行run()的时候,会调用updateComponent来进行比照更新,能够联合我上一篇一起看. 而后该看vm._c和vm.$createElement也就是h函数 外部调用createElement生成vNode并返回。 createElement 相似snabbdom的h函数咱们关注重点不同局部 export function _createElement ( context: Component, tag?: string | Class<Component> | Function | Object, data?: VNodeData, children?: any, normalizationType?: number): VNode | Array<VNode> { if (isDef(data) && isDef((data: any).__ob__)) { process.env.NODE_ENV !== 'production' && warn( `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` + 'Always create fresh vnode data objects in each render!', context ) return createEmptyVNode() } // <component v-bind:is="currentTabComponent"></component> // object syntax in v-bind if (isDef(data) && isDef(data.is)) { tag = data.is } if (!tag) { // in case of component :is set to falsy value return createEmptyVNode() } // warn against non-primitive key if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.key) && !isPrimitive(data.key) ) { if (!__WEEX__ || !('@binding' in data.key)) { warn( 'Avoid using non-primitive value as key, ' + 'use string/number value instead.', context ) } } // support single function children as default scoped slot if (Array.isArray(children) && typeof children[0] === 'function' ) { data = data || {} data.scopedSlots = { default: children[0] } children.length = 0 } if (normalizationType === ALWAYS_NORMALIZE) { //返回一维数组,解决用户手写的 render //判断children的类型,如果是原始值的话转换成VNode的数组 //如果是数组的话,持续解决数组中的元素 //如果数组中的子元素又是数组(slottemplate),递归解决 //如果间断两个节点都是字符串会合并文本节点 children = normalizeChildren(children) } else if (normalizationType === SIMPLE_NORMALIZE) { // 把二维数组,转换成一维数组 //如果children中有函数组件的话,函数组件会返回数组模式 //这时候children就是一个二维数组,只须要把二维数组转换为一维数组 children = simpleNormalizeChildren(children) } let vnode, ns //判断tag是字符串还是组件 if (typeof tag === 'string') { let Ctor ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag) // 是否是 html 的保留标签 if (config.isReservedTag(tag)) { // platform built-in elements if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) { warn( `The .native modifier for v-on is only valid on components but it was used on <${tag}>.`, context ) } ////如果是浏览器的保留标签,创立对应的VNode vnode = new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context ) // 判断是否是 自定义组件 } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { // 查找自定义组件构造函数的申明 // 依据 Ctor 创立组件的 VNode // component vnode = createComponent(Ctor, data, context, children, tag) } else { // unknown or unlisted namespaced elements // check at runtime because it may get assigned a namespace when its // parent normalizes children vnode = new VNode( tag, data, children, undefined, undefined, context ) } } else { // direct component options / constructor vnode = createComponent(tag, data, context, children) } if (Array.isArray(vnode)) { return vnode } else if (isDef(vnode)) { if (isDef(ns)) applyNS(vnode, ns) if (isDef(data)) registerDeepBindings(data) return vnode } else { return createEmptyVNode() }}这里虚构dom 和snabbdom不太一样,vue中的vNode属性更多,咱们目前只关注咱们要的就能够了(关注和snabbdom一样)。 ...

December 29, 2020 · 6 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

Vue基于snabbdom做了哪些事

文章首发于 http://shuaizhang.top 前言之前有简单看过 Vue patch 部分的源码,了解了是基于 Snabbdom 库实现的。最近想详细了解下 Vue 处理 vnode patch 的整个过程,想知道它在 Snabbdom 之上做了哪些事情?所以带着这个问题,写了这篇文章来记录。 Snabbdom 做了哪些事?A virtual DOM library with focus on simplicity, modularity, powerful features and performance. (一个虚拟 DOM 库,专注于简单性,模块化,强大的功能和性能。)Snabbdom 核心代码大约只有 200 行。它提供了模块化架构,具有丰富的功能,可通过自定义模块进行扩展。在了解核心 patch 前,需要先了解 snabbdom 的模块化架构思想。 modules在节点的生命周期里做一些任务,来扩展 Snabbdom ,就可以称之为模块。Snabbdom 在 patch 的过程中会注入很多钩子(hooks)。模块实现就是基于这些钩子,钩子可以理解为 vnode 节点的生命周期。 比如 eventlisteners 模块: create: 节点创建时添加时间监听(addEventListener)update: 节点更新时移除老节点事件,添加新事件destroy: 节点销毁时,移除老节点事件HooksHooks 是一种挂载 vnode 生命周期的方式。软件开发领域有类似这样的设计思想,比如版本管理工具 git。Snabbdom 中的模块就是基于此来实现扩展的。当然,也可以传递 hook 配置,实现在 vnode 生命周期里做一些事(这种只针对单个节点,而模块针对所有节点),例如: h('div.row', { key: movie.rank, hook: { insert: vnode => { movie.elmHeight = vnode.elm.offsetHeight } }})Vue 中的指令就是基于此实现的。具体的生命周期可以参考官方文档:https://github.com/snabbdom/snabbdom#hooks ...

June 21, 2019 · 2 min · jiezi

虚拟-DOM-到底是什么

是什么?虚拟 DOM (Virtual DOM )这个概念相信大家都不陌生,从 React 到 Vue ,虚拟 DOM 为这两个框架都带来了跨平台的能力(React-Native 和 Weex)。因为很多人是在学习 React 的过程中接触到的虚拟 DOM ,所以为先入为主,认为虚拟 DOM 和 JSX 密不可分。其实不然,虚拟 DOM 和 JSX 固然契合,但 JSX 只是虚拟 DOM 的充分不必要条件,Vue 即使使用模版,也能把虚拟 DOM 玩得风生水起,同时也有很多人通过 babel 在 Vue 中使用 JSX。 很多人认为虚拟 DOM 最大的优势是 diff 算法,减少 JavaScript 操作真实 DOM 的带来的性能消耗。虽然这一个虚拟 DOM 带来的一个优势,但并不是全部。虚拟 DOM 最大的优势在于抽象了原本的渲染过程,实现了跨平台的能力,而不仅仅局限于浏览器的 DOM,可以是安卓和 IOS 的原生组件,可以是近期很火热的小程序,也可以是各种GUI。 回到最开始的问题,虚拟 DOM 到底是什么,说简单点,就是一个普通的 JavaScript 对象,包含了 tag、props、children 三个属性。 <div id="app"> <p class="text">hello world!!!</p></div>上面的 HTML 转换为虚拟 DOM 如下: ...

June 18, 2019 · 16 min · jiezi

如何实现一个虚拟 DOM——virtual-dom 源码分析

概述本文通过对virtual-dom的源码进行阅读和分析,针对Virtual DOM的结构和相关的Diff算法进行讲解,让读者能够对整个数据结构以及相关的Diff算法有一定的了解。Virtual DOM中Diff算法得到的结果如何映射到真实DOM中,我们将在下一篇博客揭晓。本文的主要内容为:Virtual DOM的结构Virtual DOM的Diff算法注:这个Virtual DOM的实现并不是React Virtual DOM的源码,而是基于virtual-dom)这个库。两者在原理上类似,并且这个库更加简单容易理解。相较于这个库,React对Virtual DOM做了进一步的优化和调整,我会在后续的博客中进行分析。Virtual DOM的结构VirtualNode作为Virtual DOM的元数据结构,VirtualNode位于vnode/vnode.js文件中。我们截取一部分声明代码来看下内部结构:function VirtualNode(tagName, properties, children, key, namespace) { this.tagName = tagName this.properties = properties || noProperties //props对象,Object类型 this.children = children || noChildren //子节点,Array类型 this.key = key != null ? String(key) : undefined this.namespace = (typeof namespace === “string”) ? namespace : null … this.count = count + descendants this.hasWidgets = hasWidgets this.hasThunks = hasThunks this.hooks = hooks this.descendantHooks = descendantHooks}VirtualNode.prototype.version = version //VirtualNode版本号,isVnode()检测标志VirtualNode.prototype.type = “VirtualNode” // VirtualNode类型,isVnode()检测标志上面就是一个VirtualNode的完整结构,包含了特定的标签名、属性、子节点等。VTextVText是一个纯文本的节点,对应的是HTML中的纯文本。因此,这个属性也只有text这一个字段。function VirtualText(text) { this.text = String(text)}VirtualText.prototype.version = versionVirtualText.prototype.type = “VirtualText"VPatchVPatch是表示需要对Virtual DOM执行的操作记录的数据结构。它位于vnode/vpatch.js文件中。我们来看下里面的具体代码:// 定义了操作的常量,如Props变化,增加节点等VirtualPatch.NONE = 0VirtualPatch.VTEXT = 1VirtualPatch.VNODE = 2VirtualPatch.WIDGET = 3VirtualPatch.PROPS = 4VirtualPatch.ORDER = 5VirtualPatch.INSERT = 6VirtualPatch.REMOVE = 7VirtualPatch.THUNK = 8module.exports = VirtualPatchfunction VirtualPatch(type, vNode, patch) { this.type = Number(type) //操作类型 this.vNode = vNode //需要操作的节点 this.patch = patch //需要操作的内容}VirtualPatch.prototype.version = versionVirtualPatch.prototype.type = “VirtualPatch"其中常量定义了对VNode节点的操作。例如:VTEXT就是增加一个VText节点,PROPS就是当前节点有Props属性改变。Virtual DOM的Diff算法了解了虚拟DOM中的三个结构,那我们下面来看下Virtual DOM的Diff算法。这个Diff算法是Virtual DOM中最核心的一个算法。通过输入初始状态A(VNode)和最终状态B(VNode),这个算法可以得到从A到B的变化步骤(VPatch),根据得到的这一连串步骤,我们就可以知道哪些节点需要新增,哪些节点需要删除,哪些节点的属性有了变化。在这个Diff算法中,又分成了三部分:VNode的Diff算法Props的Diff算法Vnode children的Diff算法下面,我们就来一个一个介绍这些Diff算法。VNode的Diff算法该算法是针对于单个VNode的比较算法。它是用于两个树中单个节点比较的场景。具体算法如下,如果不想直接阅读源码的同学也可以翻到下面,会有相关代码流程说明供大家参考:function walk(a, b, patch, index) { if (a === b) { return } var apply = patch[index] var applyClear = false if (isThunk(a) || isThunk(b)) { thunks(a, b, patch, index) } else if (b == null) { // If a is a widget we will add a remove patch for it // Otherwise any child widgets/hooks must be destroyed. // This prevents adding two remove patches for a widget. if (!isWidget(a)) { clearState(a, patch, index) apply = patch[index] } apply = appendPatch(apply, new VPatch(VPatch.REMOVE, a, b)) } else if (isVNode(b)) { if (isVNode(a)) { if (a.tagName === b.tagName && a.namespace === b.namespace && a.key === b.key) { var propsPatch = diffProps(a.properties, b.properties) if (propsPatch) { apply = appendPatch(apply, new VPatch(VPatch.PROPS, a, propsPatch)) } apply = diffChildren(a, b, patch, apply, index) } else { apply = appendPatch(apply, new VPatch(VPatch.VNODE, a, b)) applyClear = true } } else { apply = appendPatch(apply, new VPatch(VPatch.VNODE, a, b)) applyClear = true } } else if (isVText(b)) { if (!isVText(a)) { apply = appendPatch(apply, new VPatch(VPatch.VTEXT, a, b)) applyClear = true } else if (a.text !== b.text) { apply = appendPatch(apply, new VPatch(VPatch.VTEXT, a, b)) } } else if (isWidget(b)) { if (!isWidget(a)) { applyClear = true } apply = appendPatch(apply, new VPatch(VPatch.WIDGET, a, b)) } if (apply) { patch[index] = apply } if (applyClear) { clearState(a, patch, index) }}代码具体逻辑如下:如果a和b这两个VNode全等,则认为没有修改,直接返回。如果其中有一个是thunk,则使用thunk的比较方法thunks。如果a是widget且b为空,那么通过递归将a和它的子节点的remove操作添加到patch中。如果b是VNode的话,如果a也是VNode,那么比较tagName、namespace、key,如果相同则比较两个VNode的Props(用下面提到的diffProps算法),同时比较两个VNode的children(用下面提到的diffChildren算法);如果不同则直接将b节点的insert操作添加到patch中,同时将标记位置为true。如果a不是VNode,那么直接将b节点的insert操作添加到patch中,同时将标记位置为true。如果b是VText的话,看a的类型是否为VText,如果不是,则将VText操作添加到patch中,并且将标志位设置为true;如果是且文本内容不同,则将VText操作添加到patch中。如果b是Widget的话,看a的类型是否为widget,如果是,将标志位设置为true。不论a类型为什么,都将Widget操作添加到patch中。检查标志位,如果标识为为true,那么通过递归将a和它的子节点的remove操作添加到patch中。这就是单个VNode节点的diff算法全过程。这个算法是整个diff算法的入口,两棵树的比较就是从这个算法开始的。Prpps的Diff算法看完了单个VNode节点的diff算法,我们来看下上面提到的diffProps算法。该算法是针对于两个比较的VNode节点的Props比较算法。它是用于两个场景中key值和标签名都相同的情况。具体算法如下,如果不想直接阅读源码的同学也可以翻到下面,会有相关代码流程说明供大家参考:function diffProps(a, b) { var diff for (var aKey in a) { if (!(aKey in b)) { diff = diff || {} diff[aKey] = undefined } var aValue = a[aKey] var bValue = b[aKey] if (aValue === bValue) { continue } else if (isObject(aValue) && isObject(bValue)) { if (getPrototype(bValue) !== getPrototype(aValue)) { diff = diff || {} diff[aKey] = bValue } else if (isHook(bValue)) { diff = diff || {} diff[aKey] = bValue } else { var objectDiff = diffProps(aValue, bValue) if (objectDiff) { diff = diff || {} diff[aKey] = objectDiff } } } else { diff = diff || {} diff[aKey] = bValue } } for (var bKey in b) { if (!(bKey in a)) { diff = diff || {} diff[bKey] = b[bKey] } } return diff}代码具体逻辑如下:遍历a对象。当key值不存在于b,则将此值存储下来,value赋值为undefined。当此key对应的两个属性都相同时,继续终止此次循环,进行下次循环。当key值对应的value不同且key值对应的两个value都是对象时,判断Prototype值,如果不同则记录key对应的b对象的值;如果b对应的value是hook的话,记录b的值。上面条件判断都不同且都是对象时,则继续比较key值对应的两个对象(递归)。当有一个不是对象时,直接将b对应的value进行记录。遍历b对象,将所有a对象中不存在的key值对应的对象都记录下来。整个算法的大致流程如下,因为比较简单,就不画相关流程图了。如果逻辑有些绕的话,可以配合代码食用,效果更佳。Vnode children的Diff算法下面让我们来看下最后一个算法,就是关于两个VNode节点的children属性的diffChildren算法。这个个diff算法分为两个部分,第一部分是将变化后的结果b的children进行顺序调整的算法,保证能够快速的和a的children进行比较;第二部分就是将a的children与重新排序调整后的b的children进行比较,得到相关的patch。下面,让我们一个一个算法来看。reorder算法该算法的作用是将b节点的children数组进行调整重新排序,让a和b两个children之间的diff算法更加节约时间。具体代码如下:function reorder(aChildren, bChildren) { // O(M) time, O(M) memory var bChildIndex = keyIndex(bChildren) var bKeys = bChildIndex.keys // have “key” prop,object var bFree = bChildIndex.free //don’t have “key” prop,array // all children of b don’t have “key” if (bFree.length === bChildren.length) { return { children: bChildren, moves: null } } // O(N) time, O(N) memory var aChildIndex = keyIndex(aChildren) var aKeys = aChildIndex.keys var aFree = aChildIndex.free // all children of a don’t have “key” if (aFree.length === aChildren.length) { return { children: bChildren, moves: null } } // O(MAX(N, M)) memory var newChildren = [] var freeIndex = 0 var freeCount = bFree.length var deletedItems = 0 // Iterate through a and match a node in b // O(N) time, for (var i = 0 ; i < aChildren.length; i++) { var aItem = aChildren[i] var itemIndex if (aItem.key) { if (bKeys.hasOwnProperty(aItem.key)) { // Match up the old keys itemIndex = bKeys[aItem.key] newChildren.push(bChildren[itemIndex]) } else { // Remove old keyed items itemIndex = i - deletedItems++ newChildren.push(null) } } else { // Match the item in a with the next free item in b if (freeIndex < freeCount) { itemIndex = bFree[freeIndex++] newChildren.push(bChildren[itemIndex]) } else { // There are no free items in b to match with // the free items in a, so the extra free nodes // are deleted. itemIndex = i - deletedItems++ newChildren.push(null) } } } var lastFreeIndex = freeIndex >= bFree.length ? bChildren.length : bFree[freeIndex] // Iterate through b and append any new keys // O(M) time for (var j = 0; j < bChildren.length; j++) { var newItem = bChildren[j] if (newItem.key) { if (!aKeys.hasOwnProperty(newItem.key)) { // Add any new keyed items // We are adding new items to the end and then sorting them // in place. In future we should insert new items in place. newChildren.push(newItem) } } else if (j >= lastFreeIndex) { // Add any leftover non-keyed items newChildren.push(newItem) } } var simulate = newChildren.slice() var simulateIndex = 0 var removes = [] var inserts = [] var simulateItem for (var k = 0; k < bChildren.length;) { var wantedItem = bChildren[k] simulateItem = simulate[simulateIndex] // remove items while (simulateItem === null && simulate.length) { removes.push(remove(simulate, simulateIndex, null)) simulateItem = simulate[simulateIndex] } if (!simulateItem || simulateItem.key !== wantedItem.key) { // if we need a key in this position… if (wantedItem.key) { if (simulateItem && simulateItem.key) { // if an insert doesn’t put this key in place, it needs to move if (bKeys[simulateItem.key] !== k + 1) { removes.push(remove(simulate, simulateIndex, simulateItem.key)) simulateItem = simulate[simulateIndex] // if the remove didn’t put the wanted item in place, we need to insert it if (!simulateItem || simulateItem.key !== wantedItem.key) { inserts.push({key: wantedItem.key, to: k}) } // items are matching, so skip ahead else { simulateIndex++ } } else { inserts.push({key: wantedItem.key, to: k}) } } else { inserts.push({key: wantedItem.key, to: k}) } k++ } // a key in simulate has no matching wanted key, remove it else if (simulateItem && simulateItem.key) { removes.push(remove(simulate, simulateIndex, simulateItem.key)) } } else { simulateIndex++ k++ } } // remove all the remaining nodes from simulate while(simulateIndex < simulate.length) { simulateItem = simulate[simulateIndex] removes.push(remove(simulate, simulateIndex, simulateItem && simulateItem.key)) } // If the only moves we have are deletes then we can just // let the delete patch remove these items. if (removes.length === deletedItems && !inserts.length) { return { children: newChildren, moves: null } } return { children: newChildren, moves: { removes: removes, inserts: inserts } }}下面,我们来简单介绍下这个排序算法:检查a和b中的children是否拥有key字段,如果没有,直接返回b的children数组。如果存在,初始化一个数组newChildren,遍历a的children元素。如果aChildren存在key值,则去bChildren中找对应key值,如果bChildren存在则放入新数组中,不存在则放入一个null值。如果aChildren不存在key值,则从bChildren中不存在key值的第一个元素开始取,放入新数组中。遍历bChildren,将所有achildren中没有的key值对应的value或者没有key,并且没有放入新数组的子节点放入新数组中。将bChildren和新数组逐个比较,得到从新数组转换到bChildren数组的move操作patch(即remove+insert)。返回新数组和move操作列表。通过上面这个排序算法,我们可以得到一个新的b的children数组。在使用这个数组来进行比较厚,我们可以将两个children数组之间比较的时间复杂度从o(n^2)转换成o(n)。具体的方法和效果我们可以看下面的DiffChildren算法。DiffChildren算法function diffChildren(a, b, patch, apply, index) { var aChildren = a.children var orderedSet = reorder(aChildren, b.children) var bChildren = orderedSet.children var aLen = aChildren.length var bLen = bChildren.length var len = aLen > bLen ? aLen : bLen for (var i = 0; i < len; i++) { var leftNode = aChildren[i] var rightNode = bChildren[i] index += 1 if (!leftNode) { if (rightNode) { // Excess nodes in b need to be added apply = appendPatch(apply, new VPatch(VPatch.INSERT, null, rightNode)) } } else { walk(leftNode, rightNode, patch, index) } if (isVNode(leftNode) && leftNode.count) { index += leftNode.count } } if (orderedSet.moves) { // Reorder nodes last apply = appendPatch(apply, new VPatch( VPatch.ORDER, a, orderedSet.moves )) } return apply}通过上面的重新排序算法整理了以后,两个children比较就只需在相同下标的情况下比较了,即aChildren的第N个元素和bChildren的第N个元素进行比较。然后较长的那个元素做insert操作(bChildren)或者remove操作(aChildren)即可。最后,我们将move操作再增加到patch中,就能够抵消我们在reorder时对整个数组的操作。这样只需要一次便利就得到了最终的patch值。总结整个Virtual DOM的diff算法设计的非常精巧,通过三个不同的分部算法来实现了VNode、Props和Children的diff算法,将整个Virtual DOM的的diff操作分成了三类。同时三个算法又互相递归调用,对两个Virtual DOM数做了一次(伪)深度优先的递归比较。下面一片博客,我会介绍如何将得到的VPatch操作应用到真实的DOM中,从而导致HTML树的变化。 ...

March 14, 2019 · 6 min · jiezi

vue:虚拟dom的实现

那么为什么要用 VDOM:现代 Web 页面的大多数逻辑的本质就是不停地修改DOM,但是 DOM 操作太慢了,直接导致整个页面掉帧、卡顿甚至失去响应。然而仔细想一想,很多 DOM 操作是可以打包(多个操作压成一个)和合并(一个连续更新操作只保留最终结果)的,同时 JS 引擎的计算速度要快得多,能不能把 DOM 操作放到 JS 里计算出最终结果来一发终极 DOM 操作?答案——当然可以!Vitual DOM是一种虚拟dom技术,本质上是基于javascript实现的,相对于dom对象,javascript对象更简单,处理速度更快,dom树的结构,属性信息都可以很容易的用javascript对象来表示:let element={ tagName:‘ul’,//节点标签名 props:{//dom的属性,用一个对象存储键值对 id:’list’ }, children:[//该节点的子节点 {tagName:’li’,props:{class:‘item’},children:[‘aa’]}, {tagName:’li’,props:{class:‘item’},children:[‘bb’]}, {tagName:’li’,props:{class:‘item’},children:[‘cc’]} ]}对应的html写法是:<ul id=‘list’> <li class=‘item’>aa</li> <li class=‘item’>aa</li> <li class=‘item’>aa</li></ul>Virtual DOM并没有完全实现DOM,Virtual DOM最主要的还是保留了Element之间的层次关系和一些基本属性. 你给我一个数据,我根据这个数据生成一个全新的Virtual DOM,然后跟我上一次生成的Virtual DOM去 diff,得到一个Patch,然后把这个Patch打到浏览器的DOM上去。我们可以通过javascript对象表示的树结构来构建一棵真正的dom树,当数据状态发生变化时,可以直接修改这个javascript对象,接着对比修改后的javascript对象,记录下需要对页面做的dom操作,然后将其应用到真正的dom树,实现视图的更新,这个过程就是Virtual DOM的核心思想。VNode的数据结构图:VNode生成最关键的点是通过render有2种生成方式,第一种是直接在vue对象的option中添加render字段。第二种是写一个模板或指定一个el根元素,它会首先转换成模板,经过html语法解析器生成一个ast抽象语法树,对语法树做优化,然后把语法树转换成代码片段,最后通过代码片段生成function添加到option的render字段中。ast语法优的过程,主要做了2件事:会检测出静态的class名和attributes,这样它们在初始化渲染后就永远不会再被比对了。会检测出最大的静态子树(不需要动态性的子树)并且从渲染函数中萃取出来。这样在每次重渲染时,它就会直接重用完全相同的vnode,同时跳过比对。src/core/vdom/create-element.jsconst SIMPLE_NORMALIZE = 1const ALWAYS_NORMALIZE = 2function createElement (context, tag, data, children, normalizationType, alwaysNormalize) { // 兼容不传data的情况 if (Array.isArray(data) || isPrimitive(data)) { normalizationType = children children = data data = undefined } // 如果alwaysNormalize是true // 那么normalizationType应该设置为常量ALWAYS_NORMALIZE的值 if (alwaysNormalize) normalizationType = ALWAYS_NORMALIZE // 调用_createElement创建虚拟节点 return _createElement(context, tag, data, children, normalizationType)}function _createElement (context, tag, data, children, normalizationType) { /** * 如果存在data.ob,说明data是被Observer观察的数据 * 不能用作虚拟节点的data * 需要抛出警告,并返回一个空节点 * 被监控的data不能被用作vnode渲染的数据的原因是: * data在vnode渲染过程中可能会被改变,这样会触发监控,导致不符合预期的操作 / if (data && data.ob) { process.env.NODE_ENV !== ‘production’ && warn( Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n + ‘Always create fresh vnode data objects in each render!’, context ) return createEmptyVNode() } // 当组件的is属性被设置为一个falsy的值 // Vue将不会知道要把这个组件渲染成什么 // 所以渲染一个空节点 if (!tag) { return createEmptyVNode() } // 作用域插槽 if (Array.isArray(children) && typeof children[0] === ‘function’) { data = data || {} data.scopedSlots = { default: children[0] } children.length = 0 } // 根据normalizationType的值,选择不同的处理方法 if (normalizationType === ALWAYS_NORMALIZE) { children = normalizeChildren(children) } else if (normalizationType === SIMPLE_NORMALIZE) { children = simpleNormalizeChildren(children) } let vnode, ns // 如果标签名是字符串类型 if (typeof tag === ‘string’) { let Ctor // 获取标签名的命名空间 ns = config.getTagNamespace(tag) // 判断是否为保留标签 if (config.isReservedTag(tag)) { // 如果是保留标签,就创建一个这样的vnode vnode = new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context ) // 如果不是保留标签,那么我们将尝试从vm的components上查找是否有这个标签的定义 } else if ((Ctor = resolveAsset(context.$options, ‘components’, tag))) { // 如果找到了这个标签的定义,就以此创建虚拟组件节点 vnode = createComponent(Ctor, data, context, children, tag) } else { // 兜底方案,正常创建一个vnode vnode = new VNode( tag, data, children, undefined, undefined, context ) } // 当tag不是字符串的时候,我们认为tag是组件的构造类 // 所以直接创建 } else { vnode = createComponent(tag, data, context, children) } // 如果有vnode if (vnode) { // 如果有namespace,就应用下namespace,然后返回vnode if (ns) applyNS(vnode, ns) return vnode // 否则,返回一个空节点 } else { return createEmptyVNode() }}方法的功能是给一个Vnode对象对象添加若干个子Vnode,因为整个Virtual DOM是一种树状结构,每个节点都可能会有若干子节点。然后创建一个VNode对象,如果是一个reserved tag(比如html,head等一些合法的html标签)则会创建普通的DOM VNode,如果是一个component tag(通过vue注册的自定义component),则会创建Component VNode对象,它的VnodeComponentOptions不为Null.创建好Vnode,下一步就是要把Virtual DOM渲染成真正的DOM,是通过patch来实现的,源码如下:src/core/vdom/patch.js return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) { // oldVnoe:dom||当前vnode,vnode:vnoder=对象类型,hydration是否直接用服务端渲染的dom元素 if (isUndef(vnode)) { if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return } let isInitialPatch = false const insertedVnodeQueue = [] if (isUndef(oldVnode)) { // 空挂载(可能是组件),创建新的根元素。 isInitialPatch = true createElm(vnode, insertedVnodeQueue, parentElm, refElm) } else { const isRealElement = isDef(oldVnode.nodeType) if (!isRealElement && sameVnode(oldVnode, vnode)) { // patch 现有的根节点 patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) } else { if (isRealElement) { // 安装到一个真实的元素。 // 检查这是否是服务器渲染的内容,如果我们可以执行。 // 成功的水合作用。 if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) { oldVnode.removeAttribute(SSR_ATTR) hydrating = true } if (isTrue(hydrating)) { if (hydrate(oldVnode, vnode, insertedVnodeQueue)) { invokeInsertHook(vnode, insertedVnodeQueue, true) return oldVnode } else if (process.env.NODE_ENV !== ‘production’) { warn( ‘The client-side rendered virtual DOM tree is not matching ’ + ‘server-rendered content. This is likely caused by incorrect ’ + ‘HTML markup, for example nesting block-level elements inside ’ + ‘<p>, or missing <tbody>. Bailing hydration and performing ’ + ‘full client-side render.’ ) } } // 不是服务器呈现,就是水化失败。创建一个空节点并替换它。 oldVnode = emptyNodeAt(oldVnode) } // 替换现有的元素 const oldElm = oldVnode.elm const parentElm = nodeOps.parentNode(oldElm) // create new node createElm( vnode, insertedVnodeQueue, // 极为罕见的边缘情况:如果旧元素在a中,则不要插入。 // 离开过渡。只有结合过渡+时才会发生。 // keep-alive + HOCs. (#4590) oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm) ) // 递归地更新父占位符节点元素。 if (isDef(vnode.parent)) { let ancestor = vnode.parent const patchable = isPatchable(vnode) while (ancestor) { for (let i = 0; i < cbs.destroy.length; ++i) { cbs.destroyi } ancestor.elm = vnode.elm if (patchable) { for (let i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, ancestor) } // #6513 // 调用插入钩子,这些钩子可能已经被创建钩子合并了。 // 例如使用“插入”钩子的指令。 const insert = ancestor.data.hook.insert if (insert.merged) { // 从索引1开始,以避免重新调用组件挂起的钩子。 for (let i = 1; i < insert.fns.length; i++) { insert.fnsi } } } else { registerRef(ancestor) } ancestor = ancestor.parent } } // destroy old node if (isDef(parentElm)) { removeVnodes(parentElm, [oldVnode], 0, 0) } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode) } } } invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) return vnode.elm }patch支持的3个参数,其中oldVnode是一个真实的DOM或者一个VNode对象,它表示当前的VNode,vnode是VNode对象类型,它表示待替换的VNode,hydration是bool类型,它表示是否直接使用服务器端渲染的DOM元素,下面流程图表示patch的运行逻辑:patch运行逻辑看上去比较复杂,有2个方法createElm和patchVnode是生成dom的关键,源码如下:/* * @param vnode根据vnode的数据结构创建真实的dom节点,如果vnode有children则会遍历这些子节点,递归调用createElm方法, * @param insertedVnodeQueue记录子节点创建顺序的队列,每创建一个dom元素就会往队列中插入当前的vnode,当整个vnode对象全部转换成为真实的dom 树时,会依次调用这个队列中vnode hook的insert方法 / let inPre = 0 function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) { vnode.isRootInsert = !nested // 过渡进入检查 if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return } const data = vnode.data const children = vnode.children const tag = vnode.tag if (isDef(tag)) { if (process.env.NODE_ENV !== ‘production’) { if (data && data.pre) { inPre++ } if ( !inPre && !vnode.ns && !( config.ignoredElements.length && config.ignoredElements.some(ignore => { return isRegExp(ignore) ? ignore.test(tag) : ignore === tag }) ) && config.isUnknownElement(tag) ) { warn( ‘Unknown custom element: <’ + tag + ‘> - did you ’ + ‘register the component correctly? For recursive components, ’ + ‘make sure to provide the “name” option.’, vnode.context ) } } vnode.elm = vnode.ns ? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode) setScope(vnode) / istanbul ignore if / if (WEEX) { // in Weex, the default insertion order is parent-first. // List items can be optimized to use children-first insertion // with append=“tree”. const appendAsTree = isDef(data) && isTrue(data.appendAsTree) if (!appendAsTree) { if (isDef(data)) { invokeCreateHooks(vnode, insertedVnodeQueue) } insert(parentElm, vnode.elm, refElm) } createChildren(vnode, children, insertedVnodeQueue) if (appendAsTree) { if (isDef(data)) { invokeCreateHooks(vnode, insertedVnodeQueue) } insert(parentElm, vnode.elm, refElm) } } else { createChildren(vnode, children, insertedVnodeQueue) if (isDef(data)) { invokeCreateHooks(vnode, insertedVnodeQueue) } insert(parentElm, vnode.elm, refElm) } if (process.env.NODE_ENV !== ‘production’ && data && data.pre) { inPre– } } else if (isTrue(vnode.isComment)) { vnode.elm = nodeOps.createComment(vnode.text) insert(parentElm, vnode.elm, refElm) } else { vnode.elm = nodeOps.createTextNode(vnode.text) insert(parentElm, vnode.elm, refElm) } }方法会根据vnode的数据结构创建真实的DOM节点,如果vnode有children,则会遍历这些子节点,递归调用createElm方法,InsertedVnodeQueue是记录子节点创建顺序的队列,每创建一个DOM元素就会往这个队列中插入当前的VNode,当整个VNode对象全部转换成为真实的DOM树时,会依次调用这个队列中的VNode hook的insert方法。/* * 比较新旧vnode节点,根据不同的状态对dom做合理的更新操作(添加,移动,删除)整个过程还会依次调用prepatch,update,postpatch等钩子函数,在编译阶段生成的一些静态子树,在这个过程 * @param oldVnode 中由于不会改变而直接跳过比对,动态子树在比较过程中比较核心的部分就是当新旧vnode同时存在children,通过updateChildren方法对子节点做更新, * @param vnode * @param insertedVnodeQueue * @param removeOnly */ function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) { if (oldVnode === vnode) { return } const elm = vnode.elm = oldVnode.elm if (isTrue(oldVnode.isAsyncPlaceholder)) { if (isDef(vnode.asyncFactory.resolved)) { hydrate(oldVnode.elm, vnode, insertedVnodeQueue) } else { vnode.isAsyncPlaceholder = true } return } // 用于静态树的重用元素。 // 注意,如果vnode是克隆的,我们只做这个。 // 如果新节点不是克隆的,则表示呈现函数。 // 由热重加载api重新设置,我们需要进行适当的重新渲染。 if (isTrue(vnode.isStatic) && isTrue(oldVnode.isStatic) && vnode.key === oldVnode.key && (isTrue(vnode.isCloned) || isTrue(vnode.isOnce)) ) { vnode.componentInstance = oldVnode.componentInstance return } let i const data = vnode.data if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) { i(oldVnode, vnode) } const oldCh = oldVnode.children const ch = vnode.children if (isDef(data) && isPatchable(vnode)) { for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode) if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode) } if (isUndef(vnode.text)) { if (isDef(oldCh) && isDef(ch)) { if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) } else if (isDef(ch)) { if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, ‘’) addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) } else if (isDef(oldCh)) { removeVnodes(elm, oldCh, 0, oldCh.length - 1) } else if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, ‘’) } } else if (oldVnode.text !== vnode.text) { nodeOps.setTextContent(elm, vnode.text) } if (isDef(data)) { if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode) } }updateChildren方法解析在此:vue:虚拟DOM的patch ...

February 21, 2019 · 6 min · jiezi

如何编写自己的虚拟DOM

要构建自己的虚拟DOM,需要知道两件事。你甚至不需要深入 React 的源代码或者深入任何其他虚拟DOM实现的源代码,因为它们是如此庞大和复杂——但实际上,虚拟DOM的主要部分只需不到50行代码。有两个概念:Virtual DOM 是真实DOM的映射当虚拟 DOM 树中的某些节点改变时,会得到一个新的虚拟树。算法对这两棵树(新树和旧树)进行比较,找出差异,然后只需要在真实的 DOM 上做出相应的改变。用JS对象模拟DOM树首先,我们需要以某种方式将 DOM 树存储在内存中。可以使用普通的 JS 对象来做。假设我们有这样一棵树:<ul class=”list”> <li>item 1</li> <li>item 2</li></ul>看起来很简单,对吧? 如何用JS对象来表示呢?{ type: ‘ul’, props: { ‘class’: ‘list’ }, children: [ { type: ‘li’, props: {}, children: [‘item 1’] }, { type: ‘li’, props: {}, children: [‘item 2’] }] }这里有两件事需要注意:用如下对象表示DOM元素{ type: ‘…’, props: { … }, children: [ … ] }用普通 JS 字符串表示 DOM 文本节点但是用这种方式表示内容很多的 Dom 树是相当困难的。这里来写一个辅助函数,这样更容易理解:function h(type, props, …children) { return { type, props, children };}用这个方法重新整理一开始代码:h(‘ul’, { ‘class’: ‘list’ }, h(‘li’, {}, ‘item 1’), h(‘li’, {}, ‘item 2’),);这样看起来简洁多了,还可以更进一步。这里使用 JSX,如下:<ul className=”list”> <li>item 1</li> <li>item 2</li></ul>编译成:React.createElement(‘ul’, { className: ‘list’ }, React.createElement(‘li’, {}, ‘item 1’), React.createElement(‘li’, {}, ‘item 2’),);是不是看起来有点熟悉?如果能够用我们刚定义的 h(…) 函数代替 React.createElement(…),那么我们也能使用JSX 语法。其实,只需要在源文件头部加上这么一句注释:/** @jsx h /<ul className=”list”> <li>item 1</li> <li>item 2</li></ul>它实际上告诉 Babel ’ 嘿,小老弟帮我编译 JSX 语法,用 h(…) 函数代替 React.createElement(…),然后 Babel 就开始编译。‘综上所述,我们将DOM写成这样:/* @jsx h /const a = ( <ul className=”list”> <li>item 1</li> <li>item 2</li> </ul>);Babel 会帮我们编译成这样的代码:const a = ( h(‘ul’, { className: ‘list’ }, h(‘li’, {}, ‘item 1’), h(‘li’, {}, ‘item 2’), ););当函数 “h” 执行时,它将返回普通JS对象-即我们的虚拟DOM:const a = ( { type: ‘ul’, props: { className: ‘list’ }, children: [ { type: ‘li’, props: {}, children: [‘item 1’] }, { type: ‘li’, props: {}, children: [‘item 2’] } ] });从Virtual DOM 映射到真实 DOM好了,现在我们有了 DOM 树,用普通的 JS 对象表示,还有我们自己的结构。这很酷,但我们需要从它创建一个真正的DOM。首先让我们做一些假设并声明一些术语:使用以’ $ ‘开头的变量表示真正的DOM节点(元素,文本节点),因此 $parent 将会是一个真实的DOM元素虚拟 DOM 使用名为 node 的变量表示 就像在 React 中一样,只能有一个根节点——所有其他节点都在其中那么,来编写一个函数 createElement(…),它将获取一个虚拟 DOM 节点并返回一个真实的 DOM 节点。这里先不考虑 props 和 children 属性:function createElement(node) { if (typeof node === ‘string’) { return document.createTextNode(node); } return document.createElement(node.type);}上述方法我也可以创建有两种节点分别是文本节点和 Dom 元素节点,它们是类型为的 JS 对象:{ type: ‘…’, props: { … }, children: [ … ] }因此,可以在函数 createElement 传入虚拟文本节点和虚拟元素节点——这是可行的。现在让我们考虑子节点——它们中的每一个都是文本节点或元素。所以它们也可以用 createElement(…) 函数创建。是的,这就像递归一样,所以我们可以为每个元素的子元素调用 createElement(…),然后使用 appendChild() 添加到我们的元素中: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;}哇,看起来不错。先把节点 props 属性放到一边。待会再谈。我们不需要它们来理解虚拟DOM的基本概念,因为它们会增加复杂性。完整代码如下:/** @jsx h /function h(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;}const a = ( <ul class=“list”> <li>item 1</li> <li>item 2</li> </ul>);const $root = document.getElementById(‘root’);$root.appendChild(createElement(a));比较两棵虚拟DOM树的差异现在我们可以将虚拟 DOM 转换为真实的 DOM,这就需要考虑比较两棵 DOM 树的差异。基本的,我们需要一个算法来比较新的树和旧的树,它能够让我们知道什么地方改变了,然后相应的去改变真实的 DOM。怎么比较 DOM 树?需要处理下面的情况:添加新节点,使用 appendChild(…) 方法添加节点移除老节点,使用 removeChild(…) 方法移除老的节点节点的替换,使用 replaceChild(…) 方法如果节点相同的——就需要需要深度比较子节点编写一个名为 updateElement(…) 的函数,它接受三个参数—— $parent、newNode 和 oldNode,其中 $parent 是虚拟节点的一个实际 DOM 元素的父元素。现在来看看如何处理上面描述的所有情况。添加新节点function updateElement($parent, newNode, oldNode) { if (!oldNode) { $parent.appendChild( createElement(newNode) ); }}移除老节点这里遇到了一个问题——如果在新虚拟树的当前位置没有节点——我们应该从实际的 DOM 中删除它—— 这要如何做呢? 如果我们已知父元素(通过参数传递),我们就能调用 $parent.removeChild(…) 方法把变化映射到真实的 DOM 上。但前提是我们得知道我们的节点在父元素上的索引,我们才能通过 $parent.childNodes[index] 得到该节点的引用。好的,让我们假设这个索引将被传递给 updateElement 函数(它确实会被传递——稍后将看到)。代码如下:function updateElement($parent, newNode, oldNode, index = 0) { if (!oldNode) { $parent.appendChild( createElement(newNode) ); } else if (!newNode) { $parent.removeChild( $parent.childNodes[index] ); }}节点的替换首先,需要编写一个函数来比较两个节点(旧节点和新节点),并告诉节点是否真的发生了变化。还有需要考虑这个节点可以是元素或是文本节点:function changed(node1, node2) { return typeof node1 !== typeof node2 || typeof node1 === ‘string’ && node1 !== node2 || node1.type !== node2.type}现在,当前的节点有了 index 属性,就可以很简单的用新节点替换它: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] ); }}比较子节点最后,但并非最不重要的是——我们应该遍历这两个节点的每一个子节点并比较它们——实际上为每个节点调用updateElement(…)方法,同样需要用到递归。当节点是 DOM 元素时我们才需要比较( 文本节点没有子节点 )我们需要传递当前的节点的引用作为父节点我们应该一个一个的比较所有的子节点,即使它是 undefined 也没有关系,我们的函数也会正确处理它。最后是 index,它是子数组中子节点的 indexfunction 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) { 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 ); } }}完整的代码Babel+JSX/ @jsx h /function h(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;}function changed(node1, node2) { return typeof node1 !== typeof node2 || typeof node1 === ‘string’ && node1 !== node2 || node1.type !== node2.type}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) { 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 ); } }}// ———————————————————————const a = ( <ul> <li>item 1</li> <li>item 2</li> </ul>);const b = ( <ul> <li>item 1</li> <li>hello!</li> </ul>);const $root = document.getElementById(‘root’);const $reload = document.getElementById(‘reload’);updateElement($root, a);$reload.addEventListener(‘click’, () => { updateElement($root, b, a);});HTML<button id=“reload”>RELOAD</button><div id=“root”></div>CSS#root { border: 1px solid black; padding: 10px; margin: 30px 0 0 0;}打开开发者工具,并观察当按下“Reload”按钮时应用的更改。总结现在我们已经编写了虚拟 DOM 实现及了解它的工作原理。作者希望,在阅读了本文之后,对理解虚拟 DOM 如何工作的基本概念以及在幕后如何进行响应有一定的了解。然而,这里有一些东西没有突出显示(将在以后的文章中介绍它们):设置元素属性(props)并进行 diffing/updating处理事件——向元素中添加事件监听让虚拟 DOM 与组件一起工作,比如React获取对实际DOM节点的引用使用带有库的虚拟 DOM,这些库可以直接改变真实的 DOM,比如 jQuery 及其插件原文:https://medium.com/@deathmood…你的点赞是我持续分享好东西的动力,欢迎点赞!一个笨笨的码农,我的世界只能终身学习!更多内容请关注公众号《大迁世界》! ...

January 8, 2019 · 4 min · jiezi

vue虚拟dom原理剖析

在vue2.0渲染层做了根本性的改动,那就是引入了虚拟DOM。vue的虚拟dom是基于 snabbdom 改造过来的。了解 snabbdom的原理之后再回过头来看 vue的虚拟dom结构的实现。就难度不大了!于是,这里将自己写的 snabbdom 源码解析的一系列文章做一个汇总。snabbdom源码解析(一) 准备工作snabbdom源码解析(二) h函数snabbdom源码解析(三) vnode对象snabbdom源码解析(四) patch 方法snabbdom源码解析(五) 钩子snabbdom源码解析(六) 模块snabbdom源码解析(七) 事件处理换种方式阅读 ? 请查看:个人博客:snabbdom源码解析系列其他:GitHub个人博客常用代码片段整理

December 28, 2018 · 1 min · jiezi

snabbdom源码解析(七) 事件处理

事件处理我们在使用 vue 的时候,相信你一定也会对事件的处理比较感兴趣。 我们通过 @click 的时候,到底是发生了什么呢!虽然我们用 @click绑定在模板上,不过事件严格绑定在 vnode 上的 。eventlisteners 这个模块,就是定义了一些钩子,在 patch 的时候,能够进行事件的绑定以及解绑。建议阅读这个篇章之前,先阅读 模块 了解简单的模块之后,再回来eventlisteners 模块首先我们看下暴露出来的内容:// 导出时间监听模块,创建、更新、销毁export const eventListenersModule = { create: updateEventListeners, update: updateEventListeners, destroy: updateEventListeners} as Module;这里我们能够知道,在 create 、 update 、 destroy 的时候,便会触发 ,调用 updateEventListeners;接下来我们来详细了解下 updateEventListeners;updateEventListeners阅读之前加两个小的知识点,有助于理解vnode.data.on : 这个保存了一系列的绑定事件。 例如 on[‘click’] ,里面保存了绑定的 click 事件vnode.listener : 作为实际绑定到元素上的回调 。 elm.addEventListener(name, listener, false);。所有的事件触发后都是先回调到 listener ,再分发给不同的事件处理器updateEventListeners 函数的主要逻辑如下 :删除新事件列表上不存在的事件添加新增的事件/** * 更新事件监听器 /function updateEventListeners(oldVnode: VNode, vnode?: VNode): void { var oldOn = (oldVnode.data as VNodeData).on, oldListener = (oldVnode as any).listener, oldElm: Element = oldVnode.elm as Element, on = vnode && (vnode.data as VNodeData).on, elm: Element = (vnode && vnode.elm) as Element, name: string; // optimization for reused immutable handlers if (oldOn === on) { return; } // remove existing listeners which no longer used // 删除多余的事件 if (oldOn && oldListener) { // if element changed or deleted we remove all existing listeners unconditionally if (!on) { // 如果新的节点没有绑定事件,则删除所有的事件 for (name in oldOn) { // remove listener if element was changed or existing listeners removed // 删除监听器 oldElm.removeEventListener(name, oldListener, false); } } else { for (name in oldOn) { // remove listener if existing listener removed // 删除在新事件列表上不存在的监听器 if (!on[name]) { oldElm.removeEventListener(name, oldListener, false); } } } } // add new listeners which has not already attached if (on) { // reuse existing listener or create new // 重用老的监听器 var listener = ((vnode as any).listener = (oldVnode as any).listener || createListener()); // update vnode for listener listener.vnode = vnode; // if element changed or added we add all needed listeners unconditionally if (!oldOn) { for (name in on) { // add listener if element was changed or new listeners added elm.addEventListener(name, listener, false); } } else { for (name in on) { // add listener if new listener added // 添加新增的监听器 if (!oldOn[name]) { elm.addEventListener(name, listener, false); } } } }}createListener这里我们看到,事件触发之后都会先回调到 listener ,那它是怎么回调的呢。首先看下创建 listener/* * 创建监听器 /function createListener() { // 事件处理器 return function handler(event: Event) { handleEvent(event, (handler as any).vnode); };}handleEvent当事件触发的时候,会调用 handleEvent(event, (handler as any).vnode);handleEvent 主要负责转发 , 去除 on 里面对应的事件处理函数,进行调用// 处理事件function handleEvent(event: Event, vnode: VNode) { var name = event.type, on = (vnode.data as VNodeData).on; // call event handler(s) if exists // 如果存在回调函数,则调用对应的函数 if (on && on[name]) { invokeHandler(on[name], vnode, event); }}invokeHandler执行响应的事件处理程序。主要是处理几种情况:handler 为函数的情况handler 为 object , 但是第一个元素为 function 的情况 ,eg: handler = [fn,arg1,arg2] ;handler 为 object ,第一个元素不为 function 的情况 , eg: handler = [[fn1,arg1],[fn2]]/* * 调用事件处理 */function invokeHandler(handler: any, vnode?: VNode, event?: Event): void { if (typeof handler === ‘function’) { // call function handler // 函数情况下直接调用 handler.call(vnode, event, vnode); } else if (typeof handler === ‘object’) { // call handler with arguments if (typeof handler[0] === ‘function’) { // handler为数组的情况。 eg : handler = [fn,arg1,arg2] // 第一项为函数说明后面的项为想要传的参数 // special case for single argument for performance if (handler.length === 2) { // 当长度为2的时候,用call,优化性能 handler[0].call(vnode, handler[1], event, vnode); } else { // 组装参数,用 apply 调用 var args = handler.slice(1); args.push(event); args.push(vnode); handler[0].apply(vnode, args); } } else { // call multiple handlers // 处理多个handler的情况 for (var i = 0; i < handler.length; i++) { invokeHandler(handler[i]); } } }}小结这里通过 listener 来作为统一的事件接收, 更方便的对事件绑定以及解绑进行处理 ,在元素创建的时候绑定事件, 在销毁的时候解绑事件,防止内存泄露。 这种解决方式也是相当优雅,值得学习 :)snabbdom源码解析系列snabbdom源码解析(一) 准备工作snabbdom源码解析(二) h函数snabbdom源码解析(三) vnode对象snabbdom源码解析(四) patch 方法snabbdom源码解析(五) 钩子snabbdom源码解析(六) 模块snabbdom源码解析(七) 事件处理个人博客地址 ...

December 26, 2018 · 3 min · jiezi

snabbdom源码解析(六) 模块

模块在 ./src/modules 里面,定义了一系列的模块 , 这些模块定义了相应的钩子 。这些钩子会在 patch 的不同阶段触发,以完成相应模块的功能处理了解生命周期更多的内容,请查看 钩子主要的模块有 :attributes.tsclass.tsdataset.tseventlisteners.tshero.tsmodule.tsprops.tsstyle.ts其中 attributes class dataset props 四个比较简单,都是定义了 create update 两个钩子,eventlisteners hero style 这三个模块就复杂一点。另外 module.ts 只是定义了这些模块所用到的一些钩子// 定义模块的钩子export interface Module { pre: PreHook; create: CreateHook; update: UpdateHook; destroy: DestroyHook; remove: RemoveHook; post: PostHook;}接下来我们来看看其他的模块attributes 模块文件位置 : ./src/modules/attributes.ts我们先拉到最后// 创建以及更新的钩子export const attributesModule = { create: updateAttrs, update: updateAttrs} as Module;export default attributesModule;attributesModule 导出了两个方法, 都是调用了 updateAttrs 。这个表示,在创建元素的时候,以及更新的时候,都会触发这两个钩子,来更新 attribute。updateAttrsupdateAttrs 主要接受两个参数,oldVnode、vnode 。主要逻辑如下:遍历新 vnode 所有的属性,判断在 oldVnode 中是否相等,修改不相等的属性删除不存在于 vnode 的属性代码如下:/** * 更新属性 /function updateAttrs(oldVnode: VNode, vnode: VNode): void { var key: string, elm: Element = vnode.elm as Element, oldAttrs = (oldVnode.data as VNodeData).attrs, attrs = (vnode.data as VNodeData).attrs; if (!oldAttrs && !attrs) return; if (oldAttrs === attrs) return; oldAttrs = oldAttrs || {}; attrs = attrs || {}; // update modified attributes, add new attributes // 遍历新的属性,修改不相等的 for (key in attrs) { const cur = attrs[key]; const old = oldAttrs[key]; if (old !== cur) { if (cur === true) { elm.setAttribute(key, ‘’); } else if (cur === false) { elm.removeAttribute(key); } else { if (key.charCodeAt(0) !== xChar) { // 如果不是 x 开头 elm.setAttribute(key, cur); } else if (key.charCodeAt(3) === colonChar) { // Assume xml namespace elm.setAttributeNS(xmlNS, key, cur); } else if (key.charCodeAt(5) === colonChar) { // Assume xlink namespace elm.setAttributeNS(xlinkNS, key, cur); } else { elm.setAttribute(key, cur); } } } } // remove removed attributes // use in operator since the previous for iteration uses it (.i.e. add even attributes with undefined value) // the other option is to remove all attributes with value == undefined // 删除多余的属性 for (key in oldAttrs) { if (!(key in attrs)) { elm.removeAttribute(key); } }}class 模块文件位置 : ./src/modules/class.ts与 attribute 类似 , class 也是定义了 create 和 update 两个钩子,统一由 updateClass 处理这块逻辑比较简单 ,直接看代码吧function updateClass(oldVnode: VNode, vnode: VNode): void { var cur: any, name: string, elm: Element = vnode.elm as Element, oldClass = (oldVnode.data as VNodeData).class, klass = (vnode.data as VNodeData).class; // 新老的 className 都没有 if (!oldClass && !klass) return; // 新老的 className 没变 if (oldClass === klass) return; oldClass = oldClass || {}; klass = klass || {}; // 删除不存在与新的 classList 的 className for (name in oldClass) { if (!klass[name]) { elm.classList.remove(name); } } // 新增 或删除 class for (name in klass) { cur = klass[name]; if (cur !== oldClass[name]) { (elm.classList as any)cur ? ‘add’ : ‘remove’; } }}dataset 模块文件位置 : ./src/modules/dataset.ts与 attribute 类似 , dataset 也是定义了 create 和 update 两个钩子,统一由 updateDataset 处理这块逻辑比较简单 ,直接看代码吧const CAPS_REGEX = /[A-Z]/g;/* * 更新或创建 dataset */function updateDataset(oldVnode: VNode, vnode: VNode): void { let elm: HTMLElement = vnode.elm as HTMLElement, oldDataset = (oldVnode.data as VNodeData).dataset, dataset = (vnode.data as VNodeData).dataset, key: string; // 不变的情况下不处理 if (!oldDataset && !dataset) return; if (oldDataset === dataset) return; oldDataset = oldDataset || {}; dataset = dataset || {}; const d = elm.dataset; // 删除多余的 dataset for (key in oldDataset) { if (!dataset[key]) { if (d) { if (key in d) { delete d[key]; } } else { // 将驼峰式改为中划线分割 eg: userName —-> user-name elm.removeAttribute( ‘data-’ + key.replace(CAPS_REGEX, ‘-$&’).toLowerCase() ); } } } // 修改有变化的 dataset for (key in dataset) { if (oldDataset[key] !== dataset[key]) { if (d) { d[key] = dataset[key]; } else { elm.setAttribute( // 将驼峰式改为中划线分割 eg: userName —-> user-name ‘data-’ + key.replace(CAPS_REGEX, ‘-$&’).toLowerCase(), dataset[key] ); } } }}props 模块文件位置 : ./src/modules/props.ts与 attribute 类似 , props 也是定义了 create 和 update 两个钩子,统一由 updateProps 处理这块逻辑比较简单 ,直接看代码吧function updateProps(oldVnode: VNode, vnode: VNode): void { var key: string, cur: any, old: any, elm = vnode.elm, oldProps = (oldVnode.data as VNodeData).props, props = (vnode.data as VNodeData).props; if (!oldProps && !props) return; if (oldProps === props) return; oldProps = oldProps || {}; props = props || {}; // 删除多余的属性 for (key in oldProps) { if (!props[key]) { delete (elm as any)[key]; } } // 添加新增的属性 for (key in props) { cur = props[key]; old = oldProps[key]; // key为value的情况,再判断是否value有变化 // key不为value的情况,直接更新 if (old !== cur && (key !== ‘value’ || (elm as any)[key] !== cur)) { (elm as any)[key] = cur; } }}eventlisteners 模块eventlisteners 这一块内容稍微多一点,故将其独立出来一个章节。 传送门 : 事件style 模块待续。。。hero 模块待续。。。snabbdom源码解析系列snabbdom源码解析(一) 准备工作snabbdom源码解析(二) h函数snabbdom源码解析(三) vnode对象snabbdom源码解析(四) patch 方法snabbdom源码解析(五) 钩子snabbdom源码解析(六) 模块snabbdom源码解析(七) 事件处理个人博客地址 ...

December 26, 2018 · 4 min · jiezi

snabbdom源码解析(五) 钩子

文件路径 : ./src/hooks.ts这个文件主要是定义了 Virtual Dom 在实现过程中,在其执行过程中的一系列钩子。方便外部做一些处理// 钩子export interface Hooks { // 在 patch 开始执行的时候调用 pre?: PreHook; // 在 createElm,进入的时候调用init // vnode转换为真实DOM节点时触发 init?: InitHook; // 创建真实DOM的时候,调用 create create?: CreateHook; // 在patch方法接近完成的时候,才收集所有的插入节点,遍历调用响应的钩子 // 可以认为插入到DOM树时触发 insert?: InsertHook; // 在两个节点开始对比前调用 prepatch?: PrePatchHook; // 更新过程中,调用update update?: UpdateHook; // 两个节点对比完成时候调用 postpatch?: PostPatchHook; // 删除节点的时候调用,包括子节点的destroy也会被触发 destroy?: DestroyHook; // 删除当前节点的时候调用。元素从父节点删除时触发,和destory略有不同,remove只影响到被移除节点中最顶层的节点 remove?: RemoveHook; // 在patch方法的最后调用,也就是patch完成后触发 post?: PostHook;}snabbdom源码解析系列snabbdom源码解析(一) 准备工作snabbdom源码解析(二) h函数snabbdom源码解析(三) vnode对象snabbdom源码解析(四) patch 方法snabbdom源码解析(五) 钩子snabbdom源码解析(六) 模块snabbdom源码解析(七) 事件处理个人博客地址

December 26, 2018 · 1 min · jiezi

snabbdom源码解析(一) 准备工作

准备工作前言虚拟 DOM 结构概念随着 react 的诞生而火起来,之后 vue2.0 也加入了虚拟 DOM 的概念。阅读 vue 源码的时候,想了解虚拟 dom 结构的实现,发现在 src/core/vdom/patch.js 的地方。作者说 vue 的虚拟 DOM 的算法是基于 snabbdom 进行改造的。于是 google 一下,发现 snabbdom 实现的十分优雅,代码更易读。 所以决定先去把 snabbdom 的源码啃了之后,再回过头来啃 vue 虚拟 DOM 这一块的实现。什么是虚拟 DOM 结构(Virtual DOM)为什么需要 Virtual DOM在前端刀耕火种的时代,jquery 可谓是一家独大。然而慢慢的人们发现,在我们的代码中布满了一系列操作 DOM 的代码。这些代码难以维护,又容易出错。而且也难以测试。所以,react 利用了 Virtual DOM 简化 dom 操作,让数据与 dom 之间的关系更直观更简单。实现 Virtual DOMVirtual DOM 主要包括以下三个方面:使用 js 数据对象 表示 DOM 结构 -> VNode比较新旧两棵 虚拟 DOM 树的差异 -> diff将差异应用到真实的 DOM 树上 -> patch下面开始来研究 snabbdom 是如何实现这些方面的目录项目路径 : https://github.com/snabbdom/snabbdom首先看一下整体的目录结构,源码主要是在 src 里面,其他的目录:test 、examples 分别是测试用例以及例子。这里我们先关注源码部分── h.ts 创建vnode的函数── helpers └── attachto.ts── hooks.ts 定义钩子── htmldomapi.ts 操作dom的一些工具类── is.ts 判断类型── modules 模块 ├── attributes.ts ├── class.ts ├── dataset.ts ├── eventlisteners.ts ├── hero.ts ├── module.ts ├── props.ts └── style.ts── snabbdom.bundle.ts 入口文件── snabbdom.ts 初始化函数── thunk.ts 分块── tovnode.ts dom元素转vnode── vnode.ts 虚拟节点对象snabbdom.bundle.ts 入口文件我们先从入口文件开始看起import { init } from ‘./snabbdom’;import { attributesModule } from ‘./modules/attributes’; // for setting attributes on DOM elementsimport { classModule } from ‘./modules/class’; // makes it easy to toggle classesimport { propsModule } from ‘./modules/props’; // for setting properties on DOM elementsimport { styleModule } from ‘./modules/style’; // handles styling on elements with support for animationsimport { eventListenersModule } from ‘./modules/eventlisteners’; // attaches event listenersimport { h } from ‘./h’; // helper function for creating vnodes// 入口文件// 初始化,传入需要更新的模块。var patch = init([ // Init patch function with choosen modules attributesModule, classModule, propsModule, styleModule, eventListenersModule]) as (oldVNode: any, vnode: any) => any;// 主要导出 snabbdomBundle , 主要包含两个函数,一个是 修补函数 , 一个是 h 函数export const snabbdomBundle = { patch, h: h as any };export default snabbdomBundle;我们可以看到,入口文件主要导出两个函数 ,patch函数 , 由 snabbdom.ts 的 init 方法,根据传入的 module 来初始化h函数 ,在 h.ts 里面实现。看起来 h函数比 patch 要简单一些,我们去看看到底做了些什么。 ...

December 26, 2018 · 2 min · jiezi

snabbdom源码解析(三) vnode对象

vnode 对象vnode 是一个对象,用来表示相应的 dom 结构代码位置 :./src/vnode.ts定义 vnode 类型/** * 定义VNode类型 /export interface VNode { // 选择器 sel: string | undefined; // 数据,主要包括属性、样式、数据、绑定时间等 data: VNodeData | undefined; // 子节点 children: Array<VNode | string> | undefined; // 关联的原生节点 elm: Node | undefined; // 文本 text: string | undefined; // key , 唯一值,为了优化性能 key: Key | undefined;}定义 VNodeData 的类型/* * 定义VNode 绑定的数据类型 */export interface VNodeData { // 属性 能直接用 . 访问的 props?: Props; // 属性 attrs?: Attrs; // 样式类 class?: Classes; // 样式 style?: VNodeStyle; // 数据 dataset?: Dataset; // 绑定的事件 on?: On; hero?: Hero; attachData?: AttachData; // 钩子 hook?: Hooks; key?: Key; ns?: string; // for SVGs fn?: () => VNode; // for thunks args?: Array<any>; // for thunks [key: string]: any; // for any other 3rd party module}创建 VNode 对象// 根据传入的 属性 ,返回一个 vnode 对象export function vnode( sel: string | undefined, data: any | undefined, children: Array<VNode | string> | undefined, text: string | undefined, elm: Element | Text | undefined): VNode { let key = data === undefined ? undefined : data.key; return { sel: sel, data: data, children: children, text: text, elm: elm, key: key };}export default vnode; ...

December 26, 2018 · 1 min · jiezi

snabbdom源码解析(二) h函数

介绍这里是 typescript 的语法,定义了一系列的重载方法。h 函数主要根据传进来的参数,返回一个 vnode 对象代码代码位置 : ./src/h.ts/** * 根据选择器 ,数据 ,创建 vnode /export function h(sel: string): VNode;export function h(sel: string, data: VNodeData): VNode;export function h(sel: string, children: VNodeChildren): VNode;export function h(sel: string, data: VNodeData, children: VNodeChildren): VNode;export function h(sel: any, b?: any, c?: any): VNode { var data: VNodeData = {}, children: any, text: any, i: number; /* * 处理参数 */ if (c !== undefined) { // 三个参数的情况 sel , data , children | text data = b; if (is.array(c)) { children = c; } else if (is.primitive(c)) { text = c; } else if (c && c.sel) { children = [c]; } } else if (b !== undefined) { // 两个参数的情况 : sel , children | text // 两个参数的情况 : sel , data if (is.array(b)) { children = b; } else if (is.primitive(b)) { text = b; } else if (b && b.sel) { children = [b]; } else { data = b; } } if (children !== undefined) { for (i = 0; i < children.length; ++i) { // 如果children是文本或数字 ,则创建文本节点 if (is.primitive(children[i])) children[i] = vnode( undefined, undefined, undefined, children[i], undefined ); } } // 处理svg if ( sel[0] === ’s’ && sel[1] === ‘v’ && sel[2] === ‘g’ && (sel.length === 3 || sel[3] === ‘.’ || sel[3] === ‘#’) ) { // 增加 namespace addNS(data, children, sel); } // 生成 vnoe return vnode(sel, data, children, text, undefined);}export default h;其他h 函数比较简单,主要是提供一个方便的工具函数,方便创建 vnode 对象:::tip详细了解 vnode 对象 ,请查看 vnode::: ...

December 26, 2018 · 2 min · jiezi

snabbdom源码解析(四) patch 方法

patch 方法前言在开始解析这块源码的时候,先给大家补一个知识点。关于 两颗 Virtual Dom 树对比的策略diff 策略同级对比对比的时候,只针对同级的对比,减少算法复杂度。就近复用为了尽可能不发生 DOM 的移动,会就近复用相同的 DOM 节点,复用的依据是判断是否是同类型的 dom 元素init 方法在 ./src/snabbdom.ts 中,主要是 init 方法。init 方法主要是传入 modules ,domApi , 然后返回一个 patch 方法注册钩子// 钩子 ,const hooks: (keyof Module)[] = [ ‘create’, ‘update’, ‘remove’, ‘destroy’, ‘pre’, ‘post’];这里主要是注册一系列的钩子,在不同的阶段触发,细节可看 钩子将各个模块的钩子方法,挂到统一的钩子上这里主要是将每个 modules 下的 hook 方法提取出来存到 cbs 里面初始化的时候,将每个 modules 下的相应的钩子都追加都一个数组里面。create、update….在进行 patch 的各个阶段,触发对应的钩子去处理对应的事情这种方式比较方便扩展。新增钩子的时候,不需要更改到主要的流程 // 循环 hooks , 将每个 modules 下的 hook 方法提取出来存到 cbs 里面 // 返回结果 eg : cbs[‘create’] = [modules[0][‘create’],modules[1][‘create’],…]; for (i = 0; i < hooks.length; ++i) { cbs[hooks[i]] = []; for (j = 0; j < modules.length; ++j) { const hook = modules[j][hooks[i]]; if (hook !== undefined) { (cbs[hooks[i]] as Array<any>).push(hook); } } }这些模块的钩子,主要用在更新节点的时候,会在不同的生命周期里面去触发对应的钩子,从而更新这些模块。例如元素的 attr、props、class 之类的!详细了解请查看模块:模块sameVnode判断是否是相同的虚拟节点/** * 判断是否是相同的虚拟节点 /function sameVnode(vnode1: VNode, vnode2: VNode): boolean { return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;}patchinit 方法最后返回一个 patch 方法 。patch 方法主要的逻辑如下 :触发 pre 钩子如果老节点非 vnode, 则新创建空的 vnode新旧节点为 sameVnode 的话,则调用 patchVnode 更新 vnode , 否则创建新节点触发收集到的新元素 insert 钩子触发 post 钩子 /* * 修补节点 / return function patch(oldVnode: VNode | Element, vnode: VNode): VNode { let i: number, elm: Node, parent: Node; // 用于收集所有插入的元素 const insertedVnodeQueue: VNodeQueue = []; // 先调用 pre 回调 for (i = 0; i < cbs.pre.length; ++i) cbs.prei; // 如果老节点非 vnode , 则创建一个空的 vnode if (!isVnode(oldVnode)) { oldVnode = emptyNodeAt(oldVnode); } // 如果是同个节点,则进行修补 if (sameVnode(oldVnode, vnode)) { patchVnode(oldVnode, vnode, insertedVnodeQueue); } else { // 不同 Vnode 节点则新建 elm = oldVnode.elm as Node; parent = api.parentNode(elm); createElm(vnode, insertedVnodeQueue); // 插入新节点,删除老节点 if (parent !== null) { api.insertBefore( parent, vnode.elm as Node, api.nextSibling(elm) ); removeVnodes(parent, [oldVnode], 0, 0); } } // 遍历所有收集到的插入节点,调用插入的钩子, for (i = 0; i < insertedVnodeQueue.length; ++i) { (((insertedVnodeQueue[i].data as VNodeData).hook as Hooks) .insert as any)(insertedVnodeQueue[i]); } // 调用post的钩子 for (i = 0; i < cbs.post.length; ++i) cbs.posti; return vnode; };整体的流程大体上是这样子,接下来我们来关注更多的细节!patchVnode 方法首先我们研究 patchVnode 了解相同节点是如何更新的patchVnode 方法主要的逻辑如下 :触发 prepatch 钩子触发 update 钩子, 这里主要为了更新对应的 module 内容非文本节点的情况 , 调用 updateChildren 更新所有子节点文本节点的情况 , 直接 api.setTextContent(elm, vnode.text as string);这里在对比的时候,就会直接更新元素内容了。并不会等到对比完才更新 DOM 元素具体代码细节: /* * 更新节点 / function patchVnode( oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue ) { let i: any, hook: any; // 调用 prepatch 回调 if ( isDef((i = vnode.data)) && isDef((hook = i.hook)) && isDef((i = hook.prepatch)) ) { i(oldVnode, vnode); } const elm = (vnode.elm = oldVnode.elm as Node); let oldCh = oldVnode.children; let ch = vnode.children; if (oldVnode === vnode) return; // 调用 cbs 中的所有模块的update回调 更新对应的实际内容。 if (vnode.data !== undefined) { for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode); i = vnode.data.hook; if (isDef(i) && isDef((i = i.update))) i(oldVnode, vnode); } if (isUndef(vnode.text)) { if (isDef(oldCh) && isDef(ch)) { // 新老子节点都存在的情况,更新 子节点 if (oldCh !== ch) updateChildren( elm, oldCh as Array<VNode>, ch as Array<VNode>, insertedVnodeQueue ); } else if (isDef(ch)) { // 老节点不存在子节点,情况下,新建元素 if (isDef(oldVnode.text)) api.setTextContent(elm, ‘’); addVnodes( elm, null, ch as Array<VNode>, 0, (ch as Array<VNode>).length - 1, insertedVnodeQueue ); } else if (isDef(oldCh)) { // 新节点不存在子节点,情况下,删除元素 removeVnodes( elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1 ); } else if (isDef(oldVnode.text)) { // 如果老节点存在文本节点,而新节点不存在,所以清空 api.setTextContent(elm, ‘’); } } else if (oldVnode.text !== vnode.text) { // 子节点文本不一样的情况下,更新文本 api.setTextContent(elm, vnode.text as string); } // 调用 postpatch if (isDef(hook) && isDef((i = hook.postpatch))) { i(oldVnode, vnode); } }一开始,看到这种写法总有点不习惯,不过后面看着就习惯了。if (isDef((i = data.hook)) && isDef((i = i.init))) {i(vnode);}约等于if(data.hook.init){data.hook.init(vnode)}updateChildren 方法patchVnode 里面最重要的方法,也是整个 diff 里面的最核心方法updateChildren 主要的逻辑如下:优先处理特殊场景,先对比两端。也就是旧 vnode 头 vs 新 vnode 头旧 vnode 尾 vs 新 vnode 尾旧 vnode 头 vs 新 vnode 尾旧 vnode 尾 vs 新 vnode 头首尾不一样的情况,寻找 key 相同的节点,找不到则新建元素如果找到 key,但是,元素选择器变化了,也新建元素如果找到 key,并且元素选择没变, 则移动元素两个列表对比完之后,清理多余的元素,新增添加的元素不提供 key 的情况下,如果只是顺序改变的情况,例如第一个移动到末尾。这个时候,会导致其实更新了后面的所有元素具体代码细节: /* * 更新子节点 / function updateChildren( parentElm: Node, oldCh: Array<VNode>, newCh: Array<VNode>, insertedVnodeQueue: VNodeQueue ) { 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: any; let idxInOld: number; let elmToMove: VNode; let before: any; while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (oldStartVnode == null) { // 移动索引,因为节点处理过了会置空,所以这里向右移 oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left } 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, insertedVnodeQueue); oldStartVnode = oldCh[++oldStartIdx]; newStartVnode = newCh[++newStartIdx]; } else if (sameVnode(oldEndVnode, newEndVnode)) { // 从右对比 patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue); oldEndVnode = oldCh[–oldEndIdx]; newEndVnode = newCh[–newEndIdx]; } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right // 最左侧 对比 最右侧 patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue); // 移动元素到右侧指针的后面 api.insertBefore( parentElm, oldStartVnode.elm as Node, api.nextSibling(oldEndVnode.elm as Node) ); oldStartVnode = oldCh[++oldStartIdx]; newEndVnode = newCh[–newEndIdx]; } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left // 最右侧对比最左侧 patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue); // 移动元素到左侧指针的后面 api.insertBefore( parentElm, oldEndVnode.elm as Node, oldStartVnode.elm as Node ); oldEndVnode = oldCh[–oldEndIdx]; newStartVnode = newCh[++newStartIdx]; } else { // 首尾都不一样的情况,寻找相同 key 的节点,所以使用的时候加上key可以调高效率 if (oldKeyToIdx === undefined) { oldKeyToIdx = createKeyToOldIdx( oldCh, oldStartIdx, oldEndIdx ); } idxInOld = oldKeyToIdx[newStartVnode.key as string]; if (isUndef(idxInOld)) { // New element // 如果找不到 key 对应的元素,就新建元素 api.insertBefore( parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node ); newStartVnode = newCh[++newStartIdx]; } else { // 如果找到 key 对应的元素,就移动元素 elmToMove = oldCh[idxInOld]; if (elmToMove.sel !== newStartVnode.sel) { api.insertBefore( parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node ); } else { patchVnode( elmToMove, newStartVnode, insertedVnodeQueue ); oldCh[idxInOld] = undefined as any; api.insertBefore( parentElm, elmToMove.elm as Node, oldStartVnode.elm as Node ); } newStartVnode = newCh[++newStartIdx]; } } } // 新老数组其中一个到达末尾 if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) { if (oldStartIdx > oldEndIdx) { // 如果老数组先到达末尾,说明新数组还有更多的元素,这些元素都是新增的,说以一次性插入 before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm; addVnodes( parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue ); } else { // 如果新数组先到达末尾,说明新数组比老数组少了一些元素,所以一次性删除 removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); } } }addVnodes 方法addVnodes 就比较简单了,主要功能就是添加 Vnodes 到 真实 DOM 中/* * 添加 Vnodes 到 真实 DOM 中 /function addVnodes( parentElm: Node, before: Node | null, vnodes: Array<VNode>, startIdx: number, endIdx: number, insertedVnodeQueue: VNodeQueue) { for (; startIdx <= endIdx; ++startIdx) { const ch = vnodes[startIdx]; if (ch != null) { api.insertBefore( parentElm, createElm(ch, insertedVnodeQueue), before ); } }}removeVnodes 方法删除 VNodes 的主要逻辑如下:循环触发 destroy 钩子,递归触发子节点的钩子触发 remove 钩子,利用 createRmCb , 在所有监听器执行后,才调用 api.removeChild,删除真正的 DOM 节点/* * 创建一个删除的回调,多次调用这个回调,直到监听器都没了,就删除元素 /function createRmCb(childElm: Node, listeners: number) { return function rmCb() { if (–listeners === 0) { const parent = api.parentNode(childElm); api.removeChild(parent, childElm); } };}/* * 删除 VNodes /function removeVnodes( parentElm: Node, vnodes: Array<VNode>, startIdx: number, endIdx: number): void { for (; startIdx <= endIdx; ++startIdx) { let i: any, listeners: number, rm: () => void, ch = vnodes[startIdx]; if (ch != null) { if (isDef(ch.sel)) { invokeDestroyHook(ch); listeners = cbs.remove.length + 1; // 所有监听删除 rm = createRmCb(ch.elm as Node, listeners); for (i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm); // 如果有钩子则调用钩子后再调删除回调,如果没,则直接调用回调 if ( isDef((i = ch.data)) && isDef((i = i.hook)) && isDef((i = i.remove)) ) { i(ch, rm); } else { rm(); } } else { // Text node api.removeChild(parentElm, ch.elm as Node); } } }}createElm 方法将 vnode 转换成真正的 DOM 元素主要逻辑如下:触发 init 钩子处理注释节点创建元素并设置 id , class触发模块 create 钩子 。处理子节点处理文本节点触发 vnodeData 的 create 钩子/** VNode ==> 真实DOM*/function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node { let i: any, data = vnode.data; if (data !== undefined) { // 如果存在 data.hook.init ,则调用该钩子 if (isDef((i = data.hook)) && isDef((i = i.init))) { i(vnode); data = vnode.data; } } let children = vnode.children, sel = vnode.sel; // ! 来代表注释 if (sel === ‘!’) { if (isUndef(vnode.text)) { vnode.text = ‘’; } vnode.elm = api.createComment(vnode.text as string); } else if (sel !== undefined) { // Parse selector // 解析选择器 const hashIdx = sel.indexOf(’#’); const dotIdx = sel.indexOf(’.’, hashIdx); const hash = hashIdx > 0 ? hashIdx : sel.length; const dot = dotIdx > 0 ? dotIdx : sel.length; const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel; // 根据 tag 创建元素 const elm = (vnode.elm = isDef(data) && isDef((i = (data as VNodeData).ns)) ? api.createElementNS(i, tag) : api.createElement(tag)); // 设置 id if (hash < dot) elm.setAttribute(‘id’, sel.slice(hash + 1, dot)); // 设置 className if (dotIdx > 0) elm.setAttribute(‘class’,sel.slice(dot + 1).replace(/./g, ’ ‘)); // 执行所有模块的 create 钩子,创建对应的内容 for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode); // 如果存在 children ,则创建children if (is.array(children)) { for (i = 0; i < children.length; ++i) { const ch = children[i]; if (ch != null) { api.appendChild( elm, createElm(ch as VNode, insertedVnodeQueue) ); } } } else if (is.primitive(vnode.text)) { // 追加文本节点 api.appendChild(elm, api.createTextNode(vnode.text)); } // 执行 vnode.data.hook 中的 create 钩子 i = (vnode.data as VNodeData).hook; // Reuse variable if (isDef(i)) { if (i.create) i.create(emptyNode, vnode); if (i.insert) insertedVnodeQueue.push(vnode); } } else { // sel 不存在的情况, 即为文本节点 vnode.elm = api.createTextNode(vnode.text as string); } return vnode.elm;}其他想了解在各个生命周期都有哪些钩子,请查看:钩子想了解在各个生命周期里面如何更新具体的模块请查看:模块 ...

December 26, 2018 · 7 min · jiezi

你不知道的Virtual DOM(五):自定义组件

前言目前最流行的两大前端框架,React和Vue,都不约而同的借助Virtual DOM技术提高页面的渲染效率。那么,什么是Virtual DOM?它是通过什么方式去提升页面渲染效率的呢?本系列文章会详细讲解Virtual DOM的创建过程,并实现一个简单的Diff算法来更新页面。本文的内容脱离于任何的前端框架,只讲最纯粹的Virtual DOM。敲单词太累了,下文Virtual DOM一律用VD表示。前四篇文章介绍了VD的基本概念,并讲解了如何利用JSX编译HTML标签,然后生成VD,进而创建真实dom,最后再利用VD更新页面的过程,同时还支持key特性。以下是传送门:你不知道的Virtual DOM(一):Virtual Dom介绍你不知道的Virtual DOM(二):Virtual Dom的更新你不知道的Virtual DOM(三):Virtual Dom更新优化你不知道的Virtual DOM(四):key的作用今天,我们继续在之前项目的基础上扩展功能。现在流行的前端框架都支持自定义组件,组件化开发已经成为提高前端开发效率的银弹。下面我们就将自定义组件功能加到项目中去,目标是正确的渲染和更新自定义组件。JSX对自定义组件的支持要想正确的渲染组件,第一步就是要告诉JSX某个标签是自定义组件。这个实现起来很简单,只要标签名的首字母大写就可以了。下面的例子里,MyComp就是一个自定义组件。<div> <div>普通标签</div> <MyComp></MyComp></div>经过JSX编译后,是下面这个样子。h( ‘div’, null, h( ‘div’, null, ‘u666Eu901Au6807u7B7E’ ), h(MyComp, null));当首字母大写当时候,JSX会将标签名当作变量处理,而不是像普通标签一样当字符串处理。解决了识别自定义标签的问题,下一步就是定义标签了。定义基类Component在React中,所有自定义组件都要继承Component基类,它为我们提供了一系列生命周期方法和修改组件的方法。我们也对应的定义一个自己的Component类:class Component { constructor(props) { this.props = props; this.state = {}; } setState(newState) { this.state = {…this.state, …newState}; const vdom = this.render(); diff(this.dom, vdom, this.parent); } render() { throw new Error(‘component should define its own render method’) }};如果用一句话描述Component,那就是属性和状态的UI表达。我们先不考虑生命周期函数,先定义一个最精简版的Component。首先在初始化的时候,需要传入props属性,然后提供一个setState方法来改变组件的状态,最后就是子类必须要实现的render函数。如果子类没有实现,就会沿着原型链查找到Component类,然后会抛出一个错误。有了Component基类后,我们就可以定义自己的组件了。我们来定义一个最简单的显示属性和状态信息的组件。class MyComp extends Component { constructor(props) { super(props); this.state = { name: ‘Tina’ } } render() { return( <div> <div>This is My Component! {this.props.count}</div> <div>name: {this.state.name}</div> </div> ) }}定义好组件后,就要考虑渲染的逻辑了。组件渲染逻辑在对VD进行diff操作的时候,要对tag为函数类型(自定义组件)的节点做特殊处理,同时对新建的节点,也要加入一些额外的逻辑。function diff(dom, newVDom, parent, componentInst) { if (typeof newVDom == ‘object’ && typeof newVDom.tag == ‘function’) { buildComponentFromVDom(dom, newVDom, parent); return false; } // 新建node if (dom == undefined) { const dom = createElement(newVDom); // 自定义组件 if (componentInst) { dom._component = componentInst; dom._componentConstructor = componentInst.constructor; componentInst.dom = dom; } parent.appendChild(dom); return false; } …}function buildComponentFromVDom(dom, vdom, parent) { const cpnt = vdom.tag; if (!typeof cpnt === ‘function’) { throw new Error(‘vdom is not a component type’); } const props = getVDomProps(vdom); let componentInst = dom && dom._component; // 创建组件 if (componentInst == undefined) { try { componentInst = new cpnt(props); setTimeout(() => {componentInst.setState({name: ‘Dickens’})}, 5000); } catch (error) { throw new Error(component creation error: ${cpnt.name}); } } // 组件更新 else { componentInst.props = props; } const componentVDom = componentInst.render(); diff(dom, componentVDom, parent, componentInst);}function getVDomProps(vdom) { const props = vdom.props; props.children = vdom.children; return props;}如果是自定义组件,会调用buildComponentFromVDom方法。先通过getVDomProps方法获取vdom最新的属性,包括children。如果dom对象有_component属性,说明是组件更新的过程,否则为组件创建的过程。如果是创建过程则直接实例化一个对象,setTimeout部分主要为了验证setState能不能正常工作,可以先忽略。如果是更新过程,则传入最新的props。最后通过组件的render方法得到最新的vdom后,再进行diff操作。diff多了一个componentInst的参数,在新建dom节点的时候,如果有这个参数,说明是自定义组件创建的节点,需要用_component和_componentConstructor做一下标识。其中_component上面就用到了,用来判断是组件更新过程还是组件创建过程。_componentConstructor用在组件更新过程中判断组件的类型是否相同。function isSameType(element, newVDom) { if (typeof newVDom.tag == ‘function’) { return element._componentConstructor == newVDom.tag; } …}到此为止,自定义组件的被动更新过程已经完成了,下面来看看主动更新的逻辑。setStatesetState的逻辑很简单,就是更新state后再render一次,获取到最新的vdom,再走一遍diff的过程。setState的前提是组件已经实例化并且已经渲染出来了,this.dom就是组件渲染出来的dom的顶级节点。setState(newState) { this.state = {…this.state, …newState}; const vdom = this.render(); diff(this.dom, vdom, this.parent);}function buildComponentFromVDom(dom, vdom, parent) { … // 创建组件 if (componentInst == undefined) { … setTimeout(() => {componentInst.setState({name: ‘Dickens’})}, 5000); …}为了验证setState能否按预期运行,在创建组件的时候我们在5秒后更新一下state,看看名字能否正确更新。我们的页面是长这个样子的:function view() { const elm = arr.pop(); // 用于测试能不能正常删除元素 if (state.num !== 9) arr.unshift(elm); // 用于测试能不能正常添加元素 if (state.num === 12) arr.push(9); return ( <div> Hello World <MyComp count={state.num}/> <ul myText=“dickens”> { arr.map( i => ( <li id={i} class={li-${i}} key={i}> 第{i} </li> )) } </ul> </div> );}刚开始渲染出来是这个样子:5秒之后是这个样子:可以看的props和state都得到了正确都渲染。总结本文基于上一个版本的代码,加入了对自定义组件的支持,大大提高代码的复用性。基于当前这个版本的代码还能做怎样的优化呢,敬请期待下一篇的内容。P.S.: 想看完整代码见这里,如果有必要建一个仓库的话请留言给我:代码 ...

September 3, 2018 · 2 min · jiezi

你不知道的Virtual DOM(四):key的作用

前言目前最流行的两大前端框架,React和Vue,都不约而同的借助Virtual DOM技术提高页面的渲染效率。那么,什么是Virtual DOM?它是通过什么方式去提升页面渲染效率的呢?本系列文章会详细讲解Virtual DOM的创建过程,并实现一个简单的Diff算法来更新页面。本文的内容脱离于任何的前端框架,只讲最纯粹的Virtual DOM。敲单词太累了,下文Virtual DOM一律用VD表示。前三篇文章介绍了VD的基本概念,并讲解了如何利用JSX编译HTML标签,然后生成VD,进而创建真实dom,最后再利用VD更新页面的过程。以下是传送门:你不知道的Virtual DOM(一):Virtual Dom介绍你不知道的Virtual DOM(二):Virtual Dom的更新你不知道的Virtual DOM(三):Virtual Dom更新优化今天,我们继续在之前项目的基础上进行优化。用过React或者Vue的朋友都知道在渲染数组元素的时候,编译器会提醒加上key这个属性,那么key是用来做什么的呢?key的作用在渲染数组元素时,它们一般都有相同的结构,只是内容有些不同而已,比如:<ul> <li> <span>商品:苹果</span> <span>数量:1</span> </li> <li> <span>商品:香蕉</span> <span>数量:2</span> </li> <li> <span>商品:雪梨</span> <span>数量:3</span> </li></ul>可以把这个例子想象成一个购物车。此时如果想往购物车里面添加一件商品,性能不会有任何问题,因为只是简单的在ul的末尾追加元素,前面的元素都不需要更新:<ul> <li> <span>商品:苹果</span> <span>数量:1</span> </li> <li> <span>商品:香蕉</span> <span>数量:2</span> </li> <li> <span>商品:雪梨</span> <span>数量:3</span> </li> <li> <span>商品:橙子</span> <span>数量:2</span> </li></ul>但是,如果我要删除第一个元素,根据VD的比较逻辑,后面的元素全部都要进行更新的操作。dom结构简单还好说,如果是一个复杂的结构,那页面渲染的性能将会受到很大的影响。<ul> <li> <span>商品:香蕉</span> <span>数量:2</span> </li> <li> <span>商品:雪梨</span> <span>数量:3</span> </li> <li> <span>商品:橙子</span> <span>数量:2</span> </li></ul>有什么方式可以降低这种性能的损耗呢?最直观的方法肯定是直接删除第一个元素然后其它元素保持不变了。但程序没有这么智能,可以像我们一样一眼就看出变化。程序能做到的是尽量少的修改元素,通过移动元素而不是修改元素来达到更新的目的。为了告诉程序要怎么移动元素,我们必须给每个元素加上一个唯一标识,也就是key。<ul> <li key=“apple”> <span>商品:苹果</span> <span>数量:1</span> </li> <li key=“banana”> <span>商品:香蕉</span> <span>数量:2</span> </li> <li key=“pear”> <span>商品:雪梨</span> <span>数量:3</span> </li> <li key=“orange”> <span>商品:橙子</span> <span>数量:2</span> </li></ul>当把苹果删掉的时候,VD里面第一个元素是香蕉,而dom里面第一个元素是苹果。当元素有key属性的时候,框架就会尝试根据这个key去找对应的元素,找到了就将这个元素移动到第一个位置,循环往复。最后VD里面没有第四个元素了,才会把苹果从dom移除。代码实现在上一个版本代码的基础上,主要的改动点是diffChildren这个函数。原来的实现很简单,递归的调用diff就可以了:function diffChildren(newVDom, parent) { // 获取子元素最大长度 const childLength = Math.max(parent.childNodes.length, newVDom.children.length); // 遍历并diff子元素 for (let i = 0; i < childLength; i++) { diff(newVDom.children[i], parent, i); }}现在,我们要对这个函数进行一个大改造,让他支持key的查找:function diffChildren(newVDom, parent) { // 有key的子元素 const nodesWithKey = {}; let nodesWithKeyCount = 0; // 没key的子元素 const nodesWithoutKey = []; let nodesWithoutKeyCount = 0; const childNodes = parent.childNodes, nodeLength = childNodes.length; const vChildren = newVDom.children, vLength = vChildren.length; // 用于优化没key子元素的数组遍历 let min = 0; // 将子元素分成有key和没key两组 for (let i = 0; i < nodeLength; i++) { const child = childNodes[i], props = child[ATTR_KEY]; if (props !== undefined && props.key !== undefined) { nodesWithKey[props.key] = child; nodesWithKeyCount++; } else { nodesWithoutKey[nodesWithoutKeyCount++] = child; } } // 遍历vdom的所有子元素 for (let i = 0; i < vLength; i++) { const vChild = vChildren[i], vProps = vChild.props; let dom; vKey = vProps!== undefined ? vProps.key : undefined; // 根据key来查找对应元素 if (vKey !== undefined) { if (nodesWithKeyCount && nodesWithKey[vKey] !== undefined) { dom = nodesWithKey[vKey]; nodesWithKey[vKey] = undefined; nodesWithKeyCount–; } } // 如果没有key字段,则找一个类型相同的元素出来做比较 else if (min < nodesWithoutKeyCount) { for (let j = 0; j < nodesWithoutKeyCount; j++) { const node = nodesWithoutKey[j]; if (node !== undefined && isSameType(node, vChild)) { dom = node; nodesWithoutKey[j] = undefined; if (j === min) min++; if (j === nodesWithoutKeyCount - 1) nodesWithoutKeyCount–; break; } } } // diff返回是否更新元素 const isUpdate = diff(dom, vChild, parent); // 如果是更新元素,且不是同一个dom元素,则移动到原先的dom元素之前 if (isUpdate) { const originChild = childNodes[i]; if (originChild !== dom) { parent.insertBefore(dom, originChild); } } } // 清理剩下的未使用的dom元素 if (nodesWithKeyCount) { for (key in nodesWithKey) { const node = nodesWithKey[key]; if (node !== undefined) { node.parentNode.removeChild(node); } } } // 清理剩下的未使用的dom元素 while (min <= nodesWithoutKeyCount) { const node = nodesWithoutKey[nodesWithoutKeyCount–]; if ( node !== undefined) { node.parentNode.removeChild(node); } }}代码比较长,主要是以下几个步骤:将所有dom子元素分为有key和没key两组遍历VD子元素,如果VD子元素有key,则去查找有key的分组;如果没key,则去没key的分组找一个类型相同的元素出来diff一下,得出是否更新元素的类型如果是更新元素且子元素不是原来的,则移动元素最后清理删除没用上的dom子元素diff也要改造一下,如果是新建、删除或者替换元素,返回false。更新元素则返回true:function diff(dom, newVDom, parent) { // 新建node if (dom == undefined) { parent.appendChild(createElement(newVDom)); return false; } // 删除node if (newVDom == undefined) { parent.removeChild(dom); return false; } // 替换node if (!isSameType(dom, newVDom)) { parent.replaceChild(createElement(newVDom), dom); return false; } // 更新node if (dom.nodeType === Node.ELEMENT_NODE) { // 比较props的变化 diffProps(newVDom, dom); // 比较children的变化 diffChildren(newVDom, dom); } return true;}为了看效果,view函数也要改造下:const arr = [0, 1, 2, 3, 4];function view() { const elm = arr.pop(); // 用于测试能不能正常删除元素 if (state.num !== 9) arr.unshift(elm); // 用于测试能不能正常添加元素 if (state.num === 12) arr.push(9); return ( <div> Hello World <ul myText=“dickens”> { arr.map( i => ( <li id={i} class={li-${i}} key={i}> 第{i} </li> )) } </ul> </div> );}通过变换数组元素的顺序和适时的添加/删除元素,验证了代码按照我们的设计思路正确运行。总结本文基于上一个版本的代码,加入了对唯一标识(key)的支持,很好的提高了更新数组元素的效率。基于当前这个版本的代码还能做怎样的优化呢,敬请期待下一篇的内容。P.S.: 想看完整代码见这里,如果有必要建一个仓库的话请留言给我:代码 ...

August 30, 2018 · 3 min · jiezi