1、缘起
2、Prototype 与 jQuery
Prototype
jQuery
3、模板引擎
实现原理
jquery.tmpl
4、Virtual DOM
简史
初探
传统 diff
React
Vue
复制代码
1994 年,网景公司成立,发布了第一款商业浏览器 Navigator。以后,微软也推出了自家的 IE 浏览器。javascript
同年,W3C 小组成立,负责 HTML 的发展路径,其宗旨是促进通用协议的发展。html
以后的 1995 年,JavaScript
诞生了。前端
有传闻说是网景工程师布兰登·艾克(Brendan Eich)只花了 10 天时间就设计出来的。但也由于工期过短的缘故,还存在许多瑕疵,所以一直被 “正统” 传序员所嫌弃。java
早期 JavaScript
没有包管理机制,也没有像 Java、C++ 那样的打辅助用的 SDK,内置的方法也不多。node
还有就是性能问题,关于使用 eval
仍是 Function
,使用哪一种循环方式,该用parseInit
仍是 ~~
等等的讨论都是为了提高那一点点的性能。react
JavaScript 主要语言特征:jquery
- 借鉴 C 语言的基本语法;
- 借鉴 Java 语言的数据类型和内存管理;
- 借鉴 Scheme 语言,将函数提高到"第一等公民"(first-class citizen)的地位;
- 借鉴 Self 语言,使用基于原型(Prototype)的继承机制。
Prototype、jQuery 等 js 库的出现,在完善 JavaScript 的语言特性的同时也提升了 JavaScript 的性能。webpack
这两个 js 库均采用直接操做 Dom 的方式更新页面。git
这里说的 Prototype 不是咱们如今熟知的对象的原型,而是一个名为 Prototype 的 js 基础类库。由 Ruby 语言的大牛 Sam Stephenson 所写。github
在 prototype.js 中,prototype 对象是实现面向对象的一个重要机制。同时 Prototype 还创造了 Function.prototype.bind
,并在数组上增长了一大堆方法,其中不少方法也已经被标准化了。
2006 年,jQuery 发布。jQuery 发掘出大量的 DOM/BOM 兼容方案,解决了最头疼的浏览器兼容性问题。
2009 年,jQuery 成功研发出 Sizzle 选择器引擎,使其在性能上力压一众竞品,Dojo、Prototype、ExtJS、MooTools 等。同时在处理 DOM 兼容上,发掘出大量的 DOM/BOM 兼容方案。
jQuery 以 DOM 为中心,开发者能够选一个或多个 DOM,转变为 jQuery 对象,而后进行 链式操做。
开发者们已开始注重先后端分离,并要求不能污染 Object 原型对象,不能污染 window 全局变量。jQuery 仅占用两个全局变量。jQuery 精巧的源码实现使其大小压缩后不到 30KB,网上涌现出大量关于其源码详解的书藉。
jQuery 的出现也大大下降了前端门槛,让更多人进入了这个行业,我也是经过 jQuery 入的前端这个坑。
当时还有很多段子,“超市收银员边工做边看前端书籍,一个月后就直接去互联网公司作前端了”,诸如此类。
在咱们使用 jQuery 时须要解决大段 HTML 的生成问题,虽然有 $.html
、$.append
、$before
等方法,可是为了更好地管理不一样的 HTML,咱们想将 HTML 分离出来,让 HTML 独立到不一样的文件中,而后直接插数据。
1994 年 PHP 诞生,实现了将动态内容嵌入 HTML,提高了编写效率和可读性,其界定符、循环语句等的发明,直接或间接地影响了 JavaScript 前端模板引擎的出现。
模板引擎能够简单用一个公式里描述:HTML = template(vars)
模板引擎的实现须要解决 模板存放、模板获取、模板解析编译 的问题
textarea/input
等表单控件,或 script
等标签中{{...}}
或是 <%...%>
new Function()
的方式转化成所须要的函数。这里以 jquery.tmpl 为例,先来个小栗子
...
<body>
<div id="div_demo"></div>
</body>
<!-- 模板1,测试${}、{{=}}标签的使用 -->
<script id="demo" type="text/x-jquery-tmpl"></script>
<script type="text/javascript"> //手动初始化数据 var users = [ { id: 1, name: "xiaoming", age: 12, number: "001" }, { id: 2, name: "xiaowang", age: 13, number: "002" }, ]; //调用模板进行渲染 $("#demo").tmpl(users).appendTo("#div_demo"); </script>
...
复制代码
jquery.tmpl 使用的模板存放于 id 为 demo 的 script 标签内
模板的读取依靠 jQuery 的选择器,直接以模板为主体,调用 tmpl 解析数据,调用 jQuery 自带的 appendTo 方法插入到父节点中
这里模板的解析结合源码看一下 (篇幅缘由,省略了部分代码,完整代码看这里 buildTmplFn)
function buildTmplFn(markup) {
return new Function(
"jQuery",
"$item",
"var $=jQuery,call,__=[],$data=$item.data;" +
"with($data){__.push('" +
jQuery
.trim(markup) // 去先后空格
.replace(/([\\'])/g, "\\$1") // 替换单引号
.replace(/[\r\t\n]/g, " ") // 替换掉换行、退格符
.replace(/\$\{([^\}]*)\}/g, "{{= $1}}") // 将 {{}} 语法统统换成 {{= }} 语法
.replace(
/\{\{(\/?)(\w+|.)(?:\(((?:[^\}]|\}(?!\}))*?)?\))?(?:\s+(.*?)?)?(\(((?:[^\}]|\}(?!\}))*?)\))?\s*\}\}/g,
function (all, slash, type, fnargs, target, parens, args) {
...
return (
"');" +
tag[slash ? "close" : "open"]
.split("$notnull_1")
.join(
target
? "typeof(" +
target +
")!=='undefined' && (" +
target +
")!=null"
: "true"
)
.split("$1a")
.join(exprAutoFnDetect)
.split("$1")
.join(expr)
.split("$2")
.join(fnargs || def.$2 || "") +
"__.push('"
);
}
) +
"');}return __;"
);
}
复制代码
buildTmplFn 也是经过处理模板字符,最终生成一个可执行的函数。模板的解析依靠正则实现,代码虽少但却实现了十分强大的模板能力。
最后返回的函数的函数体以下
var $ = jQuery,
call,
__ = [],
$data = $item.data;
with ($data) {
// === buildTmplFn 最后一个替换生成以下代码====
__.push('<div style="margin-bottom:10px;"> <span>');
if (typeof id !== "undefined" && id != null) {
__.push($.encode(typeof id === "function" ? id.call($item) : id));
}
__.push('</span> <span style="margin-left:10px;">');
if (typeof name !== "undefined" && name != null) {
__.push($.encode(typeof name === "function" ? name.call($item) : name));
}
__.push('</span> <span style="margin-left:10px;">');
if (typeof age !== "undefined" && age != null) {
__.push($.encode(typeof age === "function" ? age.call($item) : age));
}
__.push('</span> <span style="margin-left:10px;">');
if (typeof number !== "undefined" && number != null) {
__.push(
$.encode(typeof number === "function" ? number.call($item) : number)
);
}
__.push(
"</span> </div>"
// =======
);
}
return __;
复制代码
最后生成的函数被执行,输出带有数据的 html 字符串,再插入到指定父节点中。
模板引擎更新视图的方式即 替换指定 Dom 元素的全部子节点。
固然也存在其弊端,有部分的替换会引发 回流。而且若是只是修改个别数据,使用模板时须要从新渲染整片区域,这是没有必要的,也是耗性能的。
时间来到 2009 年 NodeJs 诞生,随着 NodeJS 的发展冒出一大堆模块、路由、状态管理、数据库、MVC 框架(Backbone.js 也属于 MVC 框架,强依赖于 jQuery)
以后大公司开始入局,MVVM 框架出现,比较有表明性的如:谷歌的 Angular,微软的 Knockout.js,苹果的 Ember.js,Facebook 的 React。
MVVM 的视图模型是一个值转换器,包括四个部分:
- 模型 模型是指表明真实状态内容的领域模型(面向对象),或指表明内容的数据访问层(以数据为中心)。
- 视图 就像在 MVC 和 MVP 模式中同样,视图是用户在屏幕上看到的结构、布局和外观(UI)。
- 视图模型 视图模型是暴露公共属性和命令的视图的抽象。MVVM 没有 MVC 模式的控制器,也没有 MVP 模式的 presenter,有的是一个绑定器。在视图模型中,绑定器在视图和数据绑定器之间进行通讯。
- 绑定器 声明性数据和命令绑定隐含在 MVVM 模式中。在 Microsoft 解决方案堆中,绑定器是一种名为 XAML 的标记语言。绑定器使开发人员免于被迫编写样板式逻辑来同步视图模型和视图。在微软的堆以外实现时,声明性数据绑定技术的出现是实现该模式的一个关键因素。
2013 Facebook 将 React 开源,支持 JSX 语法,一开始这种写法让人难以接受,在 2017 年 Facebook 推出 React Native,人们才开始接受 JSX 这种写法,也开始研究其背后的 虚拟 DOM 技术。
(因为 JSX 须要额外编译,又间接促成了 Babel 与 webpack 的壮大)
谷歌在发布 Angular 时,同时发布了一个名为 Polymer 的框架,使用 Web Components 的浏览器自定义组件技术;虽然这个框架最后没火起来,可是它将 Script、Style、Template 三种内容混在一个文件的设计,成功启发了一个留美华人,搞出了 Vue.js,这人就是 尤雨溪。
最后提一下国内的特点终端——小程序
- 底层运行的迷你 React 的虚拟 DOM
- 内置组件是使用 Web Component
- API 来源于 Hybird 的桥方法
- 打包使用 webpack
- 调试台是 Chrome console 的简化版
- WXML、WXSS 的语法高亮也应该是 webpack 或 VS Code 的插件
- 模块机制是 Node.js 的 CommonJS
(为了方便介绍,后文将使用 VD 指代 Virtual DOM)。
本质上来讲,VD 只是一个简单的 JS 对象,基础属性包括 标签名(tag)、属性(props) 和 子元素对象(children)。不一样的框架对这三个属性的命名会有点差异,但表达的意思基本是一致的。
如下是 Vue 中的 VD 结构
export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;
text: string | void;
elm: Node | void;
ns: string | void;
context: Component | void; // rendered in this component's scope
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node
// strictly internal
...
}
复制代码
下面是截取的 React 的 VD 结构,也就是 Fiber
export type Fiber = {
tag: WorkTag,
key: null | string,
elementType: any,
type: any,
stateNode: any,
return: Fiber | null,
child: Fiber | null,
sibling: Fiber | null,
index: number,
...
|};
复制代码
两边都存在 tag
属性,不一样的地方是 Vue 中子节点存放于 children
中,而 React 经过 child
指向子节点,若是存在多个子节点,子节点再经过 sibling
属性链接上其他的子节点,以下图
VD 与 Dom 对象具备一一对应的关系,借助 VD 将页面的状态抽象为 JS 对象,再配合不一样的渲染工具,便可达到 跨平台渲染 的效果。
在进行页面更新的时候真实 DOM 的改变能够借助 VD 在内存中进行比较,也就是常说的 diff。
使用 VD 的框架,通常的设计思路都是 页面等于页面状态的映射,即 UI = render(state)
。当须要更新页面的时候,无需关心 DOM 具体的变换方式,只须要改变 state 便可,剩下的事情(render)将由框架代劳。
当 state 发生变化时,咱们从新生成整个 VD ,触发比较的操做。
上述过程大体可分为如下四步:
这里咱们讲一下传统 diff 算法,就是将新旧两棵 VD 树的节点依次进行对比,最后再进行真实节点的更新。
如上图所示,左侧新 VD 上的节点须要一一与右侧旧 VD 的节点对比。为了后续方便计算时间复杂度,咱们假设理想情况下新 VD 树的节点个数与旧 VD 树的节点个数都为 n。
不少文章都会直接告诉你,传统 diff 算法时间复杂度为 O(n^3),至于为何,那是众说纷纭,这个说法的出处已经无从考证(有了解的小伙伴欢迎留言或私信)
有两种广泛的说法:
第一种是常规思路
第二种就复杂了,涉及到两棵树的编辑距离问题,讲从 1979 到 2011,将树的编辑距离算法的时间复杂度降到了 O(n^3),详情戳这里
最后说一下个人见解,我认为 O(n^3)这个值应该是取的早期主流 diff 算法的时间复杂度的均值,毕竟咱们也不知道所谓的传统 diff 算法到底长什么样,哪些算法能被称为传统 diff 算法。
不打算展开,实在是篇幅不容许,后面再单独出一篇 React diff 算法的。
React 的 diff 算法有个核心思路,即:结合框架的事务机制将屡次比较的结果合并后一次性更新到页面,从而有效地减小页面渲染的次数,提升渲染效率
这个也不打算展开,理由同上,Vue 还有个 3.0 版,更有的聊了。
Vue 的 diff 算法采用多指针(这里指索引下标非内存地址),有的文章说双指针,其实不止,严格来说有四个指针:
首尾两个指针向中心移动,借助原生 JS 的内置方法,“实时” 地更新真实节点
同时与 React 同样,采用 key
来提高算法效率,借助 map
以空间换时间来下降 diff 算法的时间复杂度
这些介绍都比较笼统,顺手点个关注,来蹲一下 React/Vue diff 算法的解析?
文章同时发在我的公众号 浅析前端框架如何更新视图,欢迎关注 MelonField