etch简介

首先我们有必要介绍一下etch。

etch是atom团队下的开源项目,是一套非常简洁然而功能十分完善的virtualDOM机制。我在偶然的情况下接触到了这个开源项目,在读README时为它简洁的设计而惊叹,而在阅读源码的过程中也为它巧妙的实现而赞叹。

个人觉得etch针对是一个非常好的学习内容,实际代码才七百来行,逻辑极度清晰,很适合作为想了解vdom的人的入门项目。
etch项目地址

源码解读

我将个人对etch源码的实践和理解写成了一个项目,地址为源码解读地址

个人建议是直接去我这个项目看,我在项目中整理的整体的流程,也对具体的代码添加的笔记,应该很好懂,不过,如果你只是想简单了解一下,那么可以继续看这篇文章。

首先我们看一下项目的文件结构

正常来说我们应该从index.js开始看,但是index.js只是负责将函数汇总了一下,所以我们从真正的开始——component-helpers文件的initialize函数开始。

这个函数负责以一个component实例为参数(具体表现形式为在一个component的constructor中调用,参数为this。
举个栗子

/** @jsx etch.dom */const etch = require('etch')class MyComponent {// Required: Define an ordinary constructor to initialize your component.constructor (props, children) {// perform custom initialization here...// then call `etch.initialize`:etch.initialize(this)}// Required: The `render` method returns a virtual DOM tree representing the// current state of the component. Etch will call `render` to build and update// the component's associated DOM element. Babel is instructed to call the// `etch.dom` helper in compiled JSX expressions by the `@jsx` pragma above.render () {return <div></div>}// Required: Update the component with new properties and children.update (props, children) {// perform custom update logic here...// then call `etch.update`, which is async and returns a promisereturn etch.update(this)}// Optional: Destroy the component. Async/await syntax is pretty but optional.async destroy () {// call etch.destroy to remove the element and destroy child componentsawait etch.destroy(this)// then perform custom teardown logic here...}
}

上面就是一个非常标准的etch组件,在constructor中使用etch.initialize就保证了当一个组件被实例化的时候必然会调用initialize然后完成必要的初始化)。接下来我们深入initialize函数,看看它干了什么。

function initialize(component) {if (typeof component.update !== 'function') {throw new Error('Etch components must implement `update(props, children)`.')}let virtualNode = component.render()if (!isValidVirtualNode(virtualNode)) {let namePart = component.constructor && component.constructor.name ? ' in ' + component.constructor.name : ''throw new Error('invalid falsy value ' + virtualNode + ' returned from render()' + namePart)}applyContext(component, virtualNode)component.refs = {}component.virtualNode = virtualNodecomponent.element = render(component.virtualNode, {refs: component.refs, listenerContext: component})
}

我们可以清楚的看到initialize干的非常简单——调用component实例的render函数返回jsx转成的virtualNode,然后调用render将virtualNode转化为DOM元素,最后将virtualNode和DOM元素都挂载在component上。在我们写的代码里,我们会手动将DOM元素挂载到dom树上。

接下来我们分两条线看,一条是jsx如何如何变成virtualNode。很简单,babel转码器,react就是用的这个。然而transform-react-jsx插件的默认入口是React.createElement,这里需要我们配置一下,将其改成etch.dom。(入口的意思是jsx转码后的东西应该传到哪里)。

以下是.babelrc配置文件内容
{"presets": ["env"],"plugins": [["transform-react-jsx", {"pragma": "etch.dom" // default pragma is React.createElement}],"transform-object-rest-spread","transform-regenerator"]
}

dom文件下的dom函数所做的就是将传入的参数进行处理,然后返回一个货真价实的virtualNode,具体实现如下

function dom (tag, props, ...children) {let ambiguous = []//这里其实就是我之前在bl写的flatternChildren,作用就是对children进行一些处理,将数组或者是字符串转化为真正的vnodefor (let i = 0; i < children.length;) {const child = children[i]switch (typeof child) {case 'string':case 'number':children[i] = {text: child}i++break;case 'object':if (Array.isArray(child)) {children.splice(i, 1, ...child)} else if (!child) {children.splice(i, 1)} else {if (!child.context) {ambiguous.push(child)if (child.ambiguous && child.ambiguous.length) {ambiguous = ambiguous.concat(child.ambiguous)}}i++}break;default:throw new Error(`Invalid child node: ${child}`)}}//对于props进行处理,props包括所有在jsx上的属性if (props) {for (const propName in props) {const eventName = EVENT_LISTENER_PROPS[propName]//处理事件挂载if (eventName) {if (!props.on) props.on = {}props.on[eventName] = props[propName]}}//处理css类挂载if (props.class) {props.className = props.class}}return {tag, props, children, ambiguous}
}

到此,我们应该明白了,当我们碰到一个jsx时候,我们实际收到的是一个经过dom函数处理过的virtualNode(没错,我说的就是每个component的render返回的东西,另外所谓virtualNode说到底就是一个拥有特定属性的对象)。

接下来我们看另一条线,那就是render如何将virtualNode转化为一个真正的DOM元素。

unction render (virtualNode, options) {let domNodeif (virtualNode.text != null) {domNode = document.createTextNode(virtualNode.text)} else {const {tag, children} = virtualNodelet {props, context} = virtualNodeif (context) {options = {refs: context.refs, listenerContext: context}}if (typeof tag === 'function') {let refif (props && props.ref) {ref = props.ref}const component = new tag(props || {}, children)virtualNode.component = componentdomNode = component.element// console.log(domNode,"!!!",virtualNode)if (typeof ref === "function") {ref(component)} else if (options && options.refs && ref) {options.refs[ref] = component}} else if (SVG_TAGS.has(tag)) {domNode = document.createElementNS("http://www.w3.org/2000/svg", tag);if (children) addChildren(domNode, children, options)if (props) updateProps(domNode, null, virtualNode, options)} else {domNode = document.createElement(tag)if (children) addChildren(domNode, children, options)if (props) updateProps(domNode, null, virtualNode, options)}}virtualNode.domNode = domNodereturn domNode
}

其实很简单,通过对virtualNode的tag进行判断,我们可以轻易的判断virtualNode是什么类型的(比如组件,比如基本元素,比如字符元素),然后针对不同的类型进行处理(基本的好说),组件的话,要再走一遍组件的创建和挂载流程。若为基础元素,则我们可以将对应的属性放到DOM元素上,最后返回创建好的DOM元素(其实virtualNode上的所有元素基本最后都是要反映到基础DOM元素上的,可能是属性,可能是子元素)。

到这里,我们已经完成了DOM元素挂载的全过程,接下来我们看一看更新的时候会发生什么。


更新的话,我们会在自己写的update函数中调用component-helpers的update函数(后面我们叫它etch.update),而etch.update和initialize一样会以component实例作为参数,具体来说就是组件class中的this。然后在etch.update中会以异步的形式来进行更新,这样可以保证避免更新冗余,极大的提升性能

function update (component, replaceNode=true) {if (syncUpdatesInProgressCounter > 0) {updateSync(component, replaceNode)return Promise.resolve()}//这是一个可以完成异步的机制let scheduler = getScheduler()//通过这个判断保证了再一次DOM实质性更新完成之前不会再次触发if (!componentsWithPendingUpdates.has(component)) {componentsWithPendingUpdates.add(component)scheduler.updateDocument(function () {componentsWithPendingUpdates.delete(component)//而根据这个我们可以很清楚的发现真正的更新还是靠同步版updateupdateSync(component, replaceNode)})}return scheduler.getNextUpdatePromise()
}

。但是etch.update真正进行更新的部分却是在etch.updateSync。看函数名我们就知道这是这是一个更新的同步版。这个函数会让component实时更新,而etch.update实际上是以异步的形式调用的这个同步版。

接下来我们深入etch.updateSync来看看它到底是怎么做的。

function updateSync (component, replaceNode=true) {if (!isValidVirtualNode(component.virtualNode)) {throw new Error(`${component.constructor ? component.constructor.name + ' instance' : component} is not associated with a valid virtualNode. Perhaps this component was never initialized?`)}if (component.element == null) {throw new Error(`${component.constructor ? component.constructor.name + ' instance' : component} is not associated with a DOM element. Perhaps this component was never initialized?`)}let newVirtualNode = component.render()if (!isValidVirtualNode(newVirtualNode)) {const namePart = component.constructor && component.constructor.name ? ' in ' + component.constructor.name : ''throw new Error('invalid falsy value ' + newVirtualNode + ' returned from render()' + namePart)}applyContext(component, newVirtualNode)syncUpdatesInProgressCounter++let oldVirtualNode = component.virtualNodelet oldDomNode = component.elementlet newDomNode = patch(oldVirtualNode, newVirtualNode, {refs: component.refs,listenerContext: component})component.virtualNode = newVirtualNodeif (newDomNode !== oldDomNode && !replaceNode) {throw new Error('The root node type changed on update, but the update was performed with the replaceNode option set to false')} else {component.element = newDomNode}// We can safely perform additional writes after a DOM update synchronously,// but any reads need to be deferred until all writes are completed to avoid// DOM thrashing. Requested reads occur at the end of the the current frame// if this method was invoked via the scheduler. Otherwise, if `updateSync`// was invoked outside of the scheduler, the default scheduler will defer// reads until the next animation frame.if (typeof component.writeAfterUpdate === 'function') {component.writeAfterUpdate()}if (typeof component.readAfterUpdate === 'function') {getScheduler().readDocument(function () {component.readAfterUpdate()})}syncUpdatesInProgressCounter--
}

事实上由于scheduler的骚操作,在调用updateSync之前实质性的更新已经全部调用,然后我们要做的就是调用component.render获取新的virtualNode,然后通过patch函数根据新旧virtualNode判断哪些部分需要更新,然后对DOM进行更新,最后处理生命周期函数,完美。

那么scheduler的骚操作到底是什么呢?其实就是靠requestAnimationFrame保证所有的更新都在同一帧内解决。另外通过weakSet机制,可以保证一个组件在它完成自己的实质性更新之前绝不会再重绘(这里是说数据会更新,但不会反映到实际的DOM元素上,这就很完美的做到了避免冗余的更新)

最后我们看一看组件的卸载和销毁部分。这部分应该是destroy负责的,我们要在组件的destory方法中调用etch.destory。要说一下,etch.destory和etch.update一样是异步函数.然后我们可以根据update很轻松的猜出一定含有一个同步版的destroySync。没错,就是这样,真正的卸载是在destroySync中完成的。逻辑也很简单,组件上的destory会被调用,它的子组件上具有destory的也会被调用,这样一直递归。最后从DOM树上删除掉component对应的DOM元素。

unction destroySync (component, removeNode=true) {syncDestructionsInProgressCounter++destroyChildComponents(component.virtualNode)if (syncDestructionsInProgressCounter === 1 && removeNode) component.element.remove()syncDestructionsInProgressCounter--
}/*** 若为组件直接摧毁,否则摧毁子元素中为组件的部分* @param {*} virtualNode */
function destroyChildComponents(virtualNode) {if (virtualNode.component && typeof virtualNode.component.destroy === 'function') {virtualNode.component.destroy()} else if (virtualNode.children) {virtualNode.children.forEach(destroyChildComponents)}
}

到这里我们就走完全部流程了。这就是一套etch virtualNode,很简单,很有趣,很巧妙。


整篇文章絮絮叨叨的,而且还是源码这种冷门的东西,估计没什么人愿意看。不过我还是想发上来,作为自己的笔记,也希望能对他人有用。这篇文章是我在segmentfault上发的第一篇技术文章,生涩的很,我会努力进步。另外,我真的建议直接去我那个项目看笔记,应该比这篇文章清晰的多。

2018.4.11于学校

当我们谈论Virtual DOM时,我们在说什么——etch源码解读相关推荐

  1. pbp 读取 mysql数据_SqlAlchemy 中操作数据库时session和scoped_session的区别(源码分析)...

    原生session: from sqlalchemy.orm import sessionmaker from sqlalchemy import create_engine from sqlalch ...

  2. 【Spring框架】 ☞ 项目启动时执行特定处理及ApplicationListener源码分析

    1.背景 在一些业务场景中,在容器启动完成后,需要处理一些诸如:kafka业务注册,数据处理,初始化缓存等的操作. 本文重点介绍如何在服务启动中,或启动完成时执行相关处理. 2.针对上述场景,有如下实 ...

  3. oracle匿名代码块执行insert,MyBatis+Oracle在执行insert时空值报错之从源码寻找解决办法...

    mybatis-oracle-config.xml 复制代码 1 <?xml version="1.0" encoding="UTF-8"?> 2 ...

  4. 使用U盘传数据时操作系统做了什么(源码分析)

    一.背景 学习linux文件系统时考虑一个具体的问题:我们经常会用U盘传输东西到计算机中.当我们把U盘插入到一台计算机上,并且从U盘中复制文件到计算机里,然后卸载U盘,将U盘拔出.操作系统在这一连串过 ...

  5. Java源码解读--CopyOnWriteList写时复制集合容器

    加元素时复制,适用于写少读多的场景. 读的时候不加锁,写的时候加锁.Vector的实现是不论读写都加锁. 写的时候复制出一个新的数组,将新添加的元素添加进新的数组,然后将引用指向新的数组地址,因此写的 ...

  6. eclipse 使用jetty调试时,加依赖工程的源码调试方法

    [1] 添加source eclipse-->debug as-->debug configurations-->source [2]若source不起作用 重新编译一下,mvn c ...

  7. matlab求相关系数输出nan,Matlab:为什么使用'corrcoef'时相关NaN? - matlab代码 - 源码查...

    问题 When I run corrcoef to find correlation coefficients among two data arrays, I get NaNs. It only d ...

  8. C语言/C++基础之元旦新年倒计时程序包含天、时、分、秒(附源码)

    C语言/C++基础之元旦新年倒计时程序 程序之美 前言 主体 运行效果 代码实例 结束语 程序之美 前言 元旦将至,新年将至.转眼间2022年即将过去,崭新的一年正向我们缓缓走来.对那些帮助过你的,激 ...

  9. 在Virtual Box的shared folder中编译android源码。

    一些诡异的选择会带来诡异的问题,比如android源码sync到一个shared folder里面,然后在这个Folder里面编译android源码. 问题:在SharedFolder下使用make ...

最新文章

  1. 数字汽车钥匙的安全性增强技术
  2. LeetCode 309. Best Time to Buy and Sell Stock with Cooldown--Java解法-卖股票系列题目
  3. java中Arrays的用法
  4. perl 对ENV环境变量的使用
  5. 阿里云视频点播-视频上传失败(一直显示上传中)
  6. solr源码导入eclipse
  7. chrome调试工具高级不完整使用指南(基础篇)
  8. LeetCode 1829. 每个查询的最大异或值(前缀异或 + 位运算)
  9. oracle 中此处列不允许,oracle-序列 ora-02287 此处不允许序号
  10. springmvc框架原理分析
  11. 2019二级c语言模拟考试软件,全国计算机等级考试超级模拟软件(二级c)v2019.3
  12. 使用JAVASCRIPT进行全屏显示页面,就像触摸屏显示效果
  13. 谁动过你的电脑?小姐姐们要学会保护好自己电脑里的小秘密呀
  14. pytorch 实现Gradient Flipping 各种坑
  15. 开放源代码是如何吞噬软件的
  16. 深入了解Spring IoC
  17. emi滤波matlab,【原创】EMI 滤波器设计从入门到精通(三)
  18. 了解虚拟化,常用的虚拟化软件,虚拟化架构,kvm介绍
  19. 青龙面板-花花阅读6.25 最新修复版
  20. 中信银行总行信息科技岗(成都)2020届校招/秋招面经+薪资待遇(更新完,已offer)

热门文章

  1. 重构机器学习算法的知识体系 - 《终极算法》读书笔记
  2. Elasticsear使用文档
  3. 想在公众号上做一个测试软件,公众号测试新功能想要扭转乾坤?
  4. 系统分析师的必备素质和技能
  5. EMAC和PHY层之间的关系以及在通信架构划分情况
  6. 美团上交开源PromptDet:无需标注,开放世界的目标检测器
  7. 微信分享网页链接自定义图片和文字描述
  8. 6个让您获得更佳的移动分析体验的提示
  9. Mybatis关联查询的两种方式
  10. mac 如何快速生成SSH key,配置github SSH公钥连接(解决git push 413问题)