虚拟DOM发展的前世与今身
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了虚拟DOM发展的前世与今身相关的知识,希望对你有一定的参考价值。
参考技术A
web这几年蓬勃发展。经历了几个比较大的转变。我们先来大概回顾一下。
在虚拟Dom被提出来之前,我们前端框架 Jquery 凭借着良好的兼容性和简单易用的特性征服了大量的前端开发者,从而统治着大量的网站UI界面的Dom操作,jquery一度成为了神一般的框架存在着。从而广大的前端人被后台程序员所鄙视。认为jquery就等同于前端……前端一度处于程序员的最底层……
然而随着移动互联网的不断发展。UI界面越来越复杂。用户对于UI界面的要求也越来越高。jquery这时候显得有些力不从心。直到09年, google 推出了自家的框架angularjs1.0(ng是被google收购而来)版本。首次提出了前端的MVC概念。一时间在前端火了起来。
用习惯了jquery的前端开发者,突然用上了angularjs,是相当不适应的。angularjs提出的一些概念相当的前卫,比如 MVC、$scope依赖注入、服务、指令、双向数据绑定、模块 等新名词一捅而上。入门之后,我们知道了这个框架将开发者的精力由之前的dom直接转向了具体的业务层面上,虽说angulajs踩坑不断,但是其数据驱动UI的思想一时大火,我们前端人也可以骄傲的对后台说,我们也有MVC。可以说angularjs是前端开发的里程碑。
时间飞逝,时间来到了14年。脸书内部的一个项目的诞生。 Reactjs 横空出世。虚拟Dom被首次提出并且采用了与NG完全不同的思想:单向数据流,随着 Nodejs,babel,ES6 日渐成熟,jsx一度成为开发者的新宠儿。Vue作为后起之秀,吸收了两位前辈的思想:虚拟Dom、双向数据绑定于一身,使得 Vue 成为了国内最火热的前端框架。
Vue双向数据绑定我已经在之前的文章 Vue2.x 与Vue3.x 双向数据绑定区别 中提到过。今天着重就是说说这个今天的主题:虚拟Dom
先来思考一个问题:为什么会出现虚拟的Dom,前面我说到,随着web2.0不断衍变,用户对于UI界面的口味越来越挑剔。而传统的dom操作的代价十分昂贵。使得用户对于界面的交互体验越来越差。从而才有了优化的手段。
而虚拟DOM不会立即操作DOM,而是将N次更新的diff内容保存到本地一个JSON对象中,最终将这个JS对象一次性attch到DOM树上,再进行后续操作,避免大量无用的计算量。所以我们用JS对象模拟DOM节点的好处是,页面的更新可以先全部反映在JS对象( 虚拟DOM )上,操作内存中的JS对象的速度显然要更快,等更新完成后,再将最终的JS对象映射成真实的DOM,再由浏览器去渲染。
那虚拟dom这么神秘,它究竟是什么呢?其实很容易理解。大家想下,人家Vue也好,react也罢,为什么要定义一些.vue的文件或者.jsx的文件呢。其实结果应该很明显。就是在render生成真实的dom之前。先形成一份虚拟的dom结构出来。这个dom结构就是存在于我们的内存当中的一个JSON对象。这个json对象就是我们的虚拟的dom。
我们先来构建一个简单的虚拟Dom类。
那么这个虚拟的Dom有什么优缺点呢?
从上面的分析,我们可以看到。虚拟的dom的确可以解决浏览器的性能问题,只不过是把一些操作移到了在内存中操作,因此,虚拟的dom一定会加大内存的消耗。那么我们框架应该是加大力度去优化虚拟dom的diff算法了,节省内存。才是王道 。
Vue3.0即将推出。全部采用了typescript来重构。对性能做了较大的优化。大家可一睹新的内存优化技巧了。
ServiceLoader和DriverManager的前世今生
ServiceLoader和DriverManager的前世今身
JDBC获取数据库连接的方式
我们先来看看JDBC获取数据库连接有哪几种做法:
- 直接实例化出特定的驱动,不经过DriverManager,这样的好处是方便,坏处是需要提前知道存在哪些驱动可以使用,因此该方式不推荐。
Driver driver=new com.mysql.jdbc.Driver();
String url="jdbc:mysql://localhost:3306/test1";
Properties info=new Properties();
info.setProperty("user","root");
info.setProperty("password","xxx");
Connection conn=driver.connect(url,info);
- 和第一种方式没有区别,这里不再多说
Class clazz=Class.forName("com.mysql.jdbc.Driver");
Driver driver=(Driver)clazz.newInstance();
String url="jdbc:mysql://localhost:3306/test1";
Properties info=new Properties();
info.setProperty("user","root");
info.setProperty("password","xxx");
Connection conn=driver.connect(url,info);
- 下面就是把Driver注册到了DriverManager进行管理,但是也不推荐这种方式
String url="jdbc:mysql://localhost:3306/test1";
String user="root";
String password="xxx";
Class clazz=Class.forName("com.mysql.jdbc.Driver");
Driver driver=(Driver)clazz.newInstance();
//注册驱动
DriverManager.registerDriver(driver);
//获取连接
Connection conn=DriverManager.getConnection(url,user,password);
- 下面这种方式相对来说就舒服很多,但是其实还可以更简单
String url="jdbc:mysql://localhost:3306/test1";
String user="root";
String password="xxx";
//加载Driver
Class clazz=Class.forName("com.mysql.jdbc.Driver");
//获取连接
Connection conn=DriverManager.getConnection(url,user,password);
- 下面一行代码就够了
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test1",
"root", "xxx");
相信大家看着我一步步简化到最后,已经蒙了,为什么可以这样写,别急,下面我们就来看看DriverManager到底是怎么实现的
ServiceLoader
因为DriverManager实现主要依靠了ServiceLoader来完成,因此这里先来看看ServiceLoader干了什么:
在Java应用中存在着很多服务提供者接口(Service Provider Interface,SPI),这些接口允许第三方为它们提供实现,如常见的 SPI 有 JDBC、JNDI等,这些 SPI 的接口属于 Java 核心库,一般存在rt.jar包中,由Bootstrap类加载器加载,而 SPI 的第三方实现代码则是作为Java应用所依赖的 jar 包被存放在classpath路径下,由于SPI接口中的代码经常需要加载具体的第三方实现类并调用其相关方法,但SPI的核心接口类是由引导类加载器来加载的,而Bootstrap类加载器无法直接加载SPI的实现类,同时由于双亲委派模式的存在,Bootstrap类加载器也无法反向委托AppClassLoader加载器SPI的实现类。在这种情况下,我们就需要一种特殊的类加载器来加载第三方的类库,而线程上下文类加载器就是很好的选择。
线程上下文类加载器(contextClassLoader)是从 JDK 1.2 开始引入的,我们可以通过java.lang.Thread类中的getContextClassLoader()和 setContextClassLoader(ClassLoader cl)方法来获取和设置线程的上下文类加载器。
如果没有手动设置上下文类加载器,线程将继承其父线程的上下文类加载器,初始线程的上下文类加载器是系统类加载器(AppClassLoader),在线程中运行的代码可以通过此类加载器来加载类和资源,如下图所示,以jdbc.jar加载为例
从图可知rt.jar核心包是有Bootstrap类加载器加载的,其内包含SPI核心接口类,由于SPI中的类经常需要调用外部实现类的方法,而jdbc.jar包含外部实现类(jdbc.jar存在于classpath路径)无法通过Bootstrap类加载器加载,因此只能委派线程上下文类加载器把jdbc.jar中的实现类加载到内存以便SPI相关类使用。显然这种线程上下文类加载器的加载方式破坏了“双亲委派模型”,它在执行过程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器,当然这也使得Java类加载器变得更加灵活。
例如我们这里要介绍的DriverManager,它是Java核心rt.jar包中的类,该类用来管理不同数据库的实现驱动即Driver,它们都实现了Java核心包中的java.sql.Driver接口,如mysql驱动包中的com.mysql.jdbc.Driver。
而因为DriverManager是由启动类加载器进行加载的,启动类加载器无法去加载类路径下的Driver接口实现类,因此需要将加载这些实现类的需求委托给线程上下文类加载器来完成,实际是通过ServiceLoader调用线程上下文类加载器去加载这些接口实现类的。
下面就来看看ServiceLoader是如何加载的吧。
源码分析
该类中有一个静态变量,指明了ServiceLoader会去哪里寻找需要被加载的类:
//会去每个类路径下的META-INF/services/包下,寻找需要被加载的类
private static final String PREFIX = "META-INF/services/";
还有其他一些属性如下:
//需要被加载的类,这里给出的是接口,然后在类路径下寻找其实现类
private final Class<S> service;
//用来寻找和加载实现类的类加载器,即为线程上下文类加载器
private final ClassLoader loader;
//存放所找到的所有可用的实现类
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 用来遍历上面这个集合的迭代器,当然没那么简单,它还会负责类的加载
private LazyIterator lookupIterator;
下面来看看其中的一些方法,首先是关于如何获取一个ServiceLoader实例的方法:
//重新加载ServiceLoader
public void reload()
//清空集合
providers.clear();
lookupIterator = new LazyIterator(service, loader);
//构造器为私有,说明是单例模式
private ServiceLoader(Class<S> svc, ClassLoader cl)
service = Objects.requireNonNull(svc, "Service interface cannot be null");
//默认是线程上下文类加载器
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
//重新加载一下
reload();
那么如何获取这个单例的ServiceLoader呢?
//我们一般调用该方法来加载某个接口提供的所有实现类
public static <S> ServiceLoader<S> load(Class<S> service)
//默认就是线程上下文类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
//返回一个 ServiceLoader实例,当然这里不算是单例模式
return new ServiceLoader<>(service, loader);
ServiceLoader的创建过程分析完了,下面来分析一下它是如何定位到那些实现类并进行初始化的吧:
一般都是调用它的iterator()方法返回一个LazyIterator迭代器:
public Iterator<S> iterator()
return new Iterator<S>()
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
//是否还有下一个元素
public boolean hasNext()
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext();
//拿到下一个元素
public S next()
if (knownProviders.hasNext())
return knownProviders.next().getValue();
//真正的核心在这里
return lookupIterator.next();
public void remove()
throw new UnsupportedOperationException();
;
下面看看LazyIterator是怎么实现的吧:
public boolean hasNext()
...
//核心方法
return hasNextService();
....
public S next()
...
//核心方法
return nextService();
...
核心方法
private boolean hasNextService()
if (nextName != null)
return true;
if (configs == null)
try
//PREFIX就是"META-INF/services/"
//service.getName()就是接口全类名,如果是Drive接口
//那么就是去类路径下寻找所有的
//META-INF/services/java.sql.Driver
String fullName = PREFIX + service.getName();
//去类路径下加载,这里类加载器就是线程上下文类加载器
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
catch (IOException x)
fail(service, "Error locating configuration files", x);
while ((pending == null) || !pending.hasNext())
if (!configs.hasMoreElements())
return false;
pending = parse(service, configs.nextElement());
nextName = pending.next();
return true;
private S nextService()
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try
//他会实例化对应实现类,但是不会进行类初始化
//即相关静态变量和静态代码块不会被赋值
c = Class.forName(cn, false, loader);
catch (ClassNotFoundException x)
fail(service,
"Provider " + cn + " not found");
if (!service.isAssignableFrom(c))
fail(service,
"Provider " + cn + " not a subtype");
try
//只有上面实例化没出错,这里才会创建实例,然后返回
//在这里类会被初始化---注意这里---对应的实现类被初始化
//那么对应的实现类内部的静态代码块会被执行--一行有很大作用
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
catch (Throwable x)
fail(service,
"Provider " + cn + " could not be instantiated",
x);
throw new Error(); // This cannot happen
小结
ServiceLoader核心思路去是去类路径寻找META-INF/services/接口全类名
这样一个文件,该文件里面记录了实现类的全类名,拿到实现类的全类名后,再去实例化实现类,然后返回所有实现类集合。
//只有上面实例化没出错,这里才会创建实例,然后返回
//在这里类会被初始化---注意这里---对应的实现类被初始化
//那么对应的实现类内部的静态代码块会被执行--一行有很大作用
S p = service.cast(c.newInstance());
实例化过程中,对应的实现类的静态代码块会被调用,因此我们可以在实现类的静态代码块中做些手脚,而DriverManager实际上就是靠这个手脚,完成实现类到DriverManager的注册的,下面我们会看到。
DriverManager
DriverManager中用一个List集合记录了所有注册到自己身上的驱动类:
// List of registered JDBC drivers
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
DriverManager类中有一个静态代码块:
static
//加载并初始化所有驱动类,就是靠上面的ServiceLoader完成的
loadInitialDrivers();
println("JDBC DriverManager initialized");
loadInitialDrivers–加载并初始化所有驱动类
private static void loadInitialDrivers()
//尝试去环境变量中寻找对应的驱动类全类名,如果有多个,按照:分割
String drivers;
try
drivers = AccessController.doPrivileged(new PrivilegedAction<String>()
public String run()
return System.getProperty("jdbc.drivers");
);
catch (Exception ex)
drivers = null;
AccessController.doPrivileged(new PrivilegedAction<Void>()
public Void run()
//通过ServiceLoader按照SPI规范去寻找所有可能存在的驱动实现类
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try
//hasNext实际调用hasNextService-去类路径下
//寻找所有的META-INF/services/java.sql.Driver文件
while(driversIterator.hasNext())
//next实际调用nextService
//该方法会实例化实现类
driversIterator.next();
catch(Throwable t)
// Do nothing
return null;
);
println("DriverManager.initialize: jdbc.drivers = " + drivers);
//如果环境变量中存在实现类的配置
if (drivers == null || drivers.equals(""))
return;
//按照:分割
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList)
try
println("DriverManager.Initialize: loading " + aDriver);
//初始化这些实现类,第二个参数为true,表示会执行初始化过程
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
catch (Exception ex)
println("DriverManager.Initialize: load failed: " + ex);
loadInitialDrivers方法执行过后,registeredDrivers 集合中就已经有了所有能找到的驱动实现类集合,
但是上面都没看到有往集合中添加驱动的代码,怎么集合就有元素了呢?
那是因为驱动实现类的静态代码块会在初始化的时候被调用,然后往registeredDrivers注册自己。
registerDriver—注册驱动
registerDriver就是用来将驱动实现类放入DriverManager的registeredDrivers集合中的
public static synchronized void registerDriver(java.sql.Driver driver,
DriverAction da)
throws SQLException
/* Register the driver if it has not already been added to our list */
if(driver != null)
registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
else
// This is for compatibility with the original DriverManager
throw new NullPointerException();
println("registerDriver: " + driver);
该方法会在实现类的静态代码块中被调用,例如:
package com.mysql.jdbc;
import java.sql.DriverManager;
import java.sql.SQLException;
public class Driver extends NonRegisteringDriver implements java.sql.Driver
public Driver() throws SQLException
static
try
//当Mysql的驱动被初始化时,静态代码块会执行,然后就会向DriverManager注册自己
DriverManager.registerDriver(new Driver());
catch (SQLException var1)
throw new RuntimeException("Can't register driver!");
getConnection—获取数据库连接
从DOM到虚拟DOM——前端DOM发展史性能与产能双赢背后的思考