第三节:函数柯里化与渲染模型
嘿,朋友们,本节是
Vue
源码阅读的第三讲。Vue
源码阅读系列得到了赞赏,我很高兴,同时希望大家可以给予反馈!我虚心接纳您的意见!
如果没有看之前的第一讲和第二讲的内容可以先去看看哦,好啦,让我们开始吧。
本节内容开始之前,我们先了解一些概念。
函数式编程(学有余力可以看看)这里不做展开。
概念
① 柯里化
一个函数原本有多个参数,只传入一个参数,生成一个函数,由新函数来接受剩余参数来运行得到结果。
② 偏函数
一个函数原本有多个参数,只传入一部分参数,生成一个函数,由新函数来接受剩余参数来运行得到结果。
③ 高阶函数
变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。
Q1
: 谈谈为什么要使用柯里化?
A1
: 为了提高性能,使用柯里化可以缓存一部分能力。
使用两个案例来说明:
Ⅰ. 判断元素
Vue
本质上是使用 HTML
的字符串作为模板的,将字符串的模板转换为抽象语法树(AST),再转换为虚拟DOM。
抽象语法树后面简写成 AST
。
下图为表示式 a+a*(b-c)+(b-c)*d
的AST
图示,当然 Vue
中 AST
不一定长这样。
过程:
模板 --> AST
AST
--> VNode
VNode
--> DOM
Q2: 哪个阶段最消耗性能?
A2: 模板 --> AST
,其中涉及对字符串进行解析。
例如,字符串拼接
。运行下面代码,在浏览器中检查
都打不开,所以说拼接字符串是非常消耗性能的,
let str = '';
for(let i = 0; i < 100000; i++){
str += i;
console.log(str);
}
再来个例子 : 有let s = "1 + 2 * ( 3 + 4 )"
,现在写一个程序,解析这个表达式,要求得到运算结果。
解
:一般会将这个表达式转换为“逆波兰式”,然后使用栈结构来运算。
这里就不细说了,涉及到编译原理中的语义分析…和数据结构中的表达式求值…
Q3
: 在 Vue
中每一个标签可以是真正的 HTML
标签 ,也可以是自定义组件
,那怎么区分?
A3
: 在 Vue
源码中将所有可用
的 HTML
标签已经存起来了。
Vue
源码具体实现部分代码:
var isHTMLTag = makeMap('html,body,base,head,link,meta,style,title,' + 'address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,' + '非常多此处省略10000字...');
先不管 makeMap
函数是用来干嘛的。
假设这里只考虑几个标签。
let tags = 'div,p,a,img,ul,li'.split(',');
//["div","p","img","ul","li"]
现需要一个函数,判断一个标签名是否为内置
的标签?
具体实现代码:
function isHtmlTag(tagName){
tagName = tagName.toLowerCase();//需要判断的标签
for( let i = 0; i < tags.length; i++ ){
/*
使用indexOf可以
if( tags[i].indexOf(tagName) > -1){
return true;
}
*/
if(tagName === tags[i]){
return true;
}
}
return false;
}
现有 1 个标签需要判断,如果有 6 个内置标签,最多要判断 6 次;现模板中有 10 个标签需要判断,那么最多需要执行 60 次循环,随着内置标签和模板中判断的标签增多,循环次数也是指数倍增长,所以说非常消耗性能!
优化: Vue
中使用 makeMap
函数,意思是创造映射(键值对),来进行优化。
let tags = 'div,p,a,img,ul,li'.split(',');
//["div","p","img","ul","li"]
function makeMap(keys) {
//keys数组
let set = {};//集合 键值对
tags.forEach(key => {
set[key] = true;
});
return function (tagName) {
//注意:! 取反 !! 强制转换 Boolean
//这里如果你不是内置标签 而是自定义组件 就是undefined
return !!set[tagName.toLowerCase()];
}
}
let isHtmlTag = makeMap(tags);//返回函数
//时间复杂度 O(n) ==> O(1)
这里我觉得老师讲的例子好像跟柯里化没有具体联系,该例子是用空间换时间
,进行代价转移,或许是我理解有误,欢迎讨论!
Ⅱ. 虚拟 DOM
的 render
方法
思考: vue
项目模板
转换为 AST
需要执行几次?
页面一开始加载需要渲染
每一个属性(响应式)数据在发生变化的时候要渲染
watch computed
等等函数要渲染
…
之前写的代码 render
每次渲染的时候,模板就会被解析一次(实际上我们简化了解析方法),我们是没有经过 AST
环节,而 Vue
是用字符串解析抽象语法树
。
render
函数作用是将虚拟 DOM
转换为真正的 DOM
加入到页面中,我们现在将虚拟 DOM
“降级”理解为 AST
,(这话说的还是有点毛病的,简化操作)。我们知道一个项目运行时,模板是不会变的,表明 AST
不会变的。所以我们可以将代码进行优化,将虚拟 DOM
缓存起来,生成一个函数,函数只需要传入数据,就可以得到真正的 DOM
。
思路有了,现在来重构代码!
function JGVue(options) {
this._data = options.data;
this._el = options.el;
//提供一个新的方法 挂载
this.mount();
}
//mount
JGVue.prototype.mount = function () {
//调用 mountComponent()之前需要提供一个render方法
this.render = this.createRenderFn();
//render 作用: 生成虚拟 DOM (与之前不一样了)
//todo render...
this.mountComponent();
}
mountComponent
方法的编写
解释:这么写在后面几期涉及到发布订阅模式
, mount
实际上是传给了 watcher
来进行调用,但是还没讲到,了解一下。
JGVue.prototype.mountComponent = function (){
//执行mountComponent()函数
let mount = () => {//一个函数 函数的this 默认全局对象 “函数调用模式”
this.update(this.render());
//render 用于生成虚拟 DOM update 负责渲染到页面上
}
//改变上下文
mount.call(this);//本质上应该交给 watcher 来调用
//箭头函数调用 call ? 有待思考
}
提供 render
方法
JGVue.prototype.mount = function () {
//调用 mountComponent()之前需要提供一个render方法
//作用: 生成虚拟 DOM
//需要提供一个 render
this.render = this.createRenderFn();//创建 render 函数
//带有缓存(Vue 本身是可以带有 render 成员)
this.mountComponent();
}
Q4
: 为什么需要这种方式创建?
A4
: 为了缓存虚拟 DOM
编写 createRenderFn
和 update
方法
//生成 render 函数 目的是为了缓存AST (使用虚拟 DOM 来模拟)
JGVue.prototype.createRenderFn = function() {
//todo...
}
//在真正的 Vue 中使用了二次提交的设计结构
//将虚拟 DOM 渲染到页面中,diff算法在这里
JGVue.prototype.update = function() {
// 简化, 直接生成 HTML DOM replaceChild 到页面中
//todo...
}
Vue
中使用了二次提交
的设计结构,类似数据库中事务
操作。比如 A 给 B 转账 1000 元,需要做两件事情,很简单,A 扣款 1000 元,B 收款 1000 元;如果只是做了其中一件事情,突然有内鬼中止交易,这样交易是错误的。
图解:
重点
❶ 页面中的 DOM
和 虚拟 DOM
是一一对应的关系
图解说明
:页面中 HTML
就是真正的 DOM
, 而就 ❶ 所说,页面中的 DOM
和 虚拟 DOM
是一一对应的关系,Vue
每一次改变数据的时候,都会生成一个含有新数据的 VNode
。数据发生变化,生成新的 VNode
, 这个新的VNode
是含有新数据(缓存了AST
),上节课,我们所写的 VNode
是使用模板来做的,模板与数据结合是更新的时候做的。现在新的VNode
和页面中的VNode
相比较,哪里不同就更新哪里(目的就是更新),实际上更新 DOM
。(diff
算法),更新到 VNode
上也就是更新 HTML
。
图解:
这个算法挺绕的,主要是搞懂几个函数之间的“职责”。
❷ render
函数所做的: AST
和数据生成 VNode
(新的)
❸ diff
算法所作的:将 oldVNode
和 NewVNode
比较
❹ update
:更新
现在所做的事情尽量的去模拟 “二次提交” 的设计结构。
回到 createRenderFn
,其作用:生成 render
函数 目的是为了缓存AST
(这里我们使用虚拟 DOM
来模拟)
//生成 render 函数 目的是为了缓存AST (使用虚拟 DOM 来模拟)
JGVue.prototype.createRenderFn = function() {
let ast = getVNode(this._template);
//ast 用虚拟 DOM 模拟
return function render () {
// 将 AST 和 data 结合 生成 VNode
// 现简化:待有“坑”的 VNode + data ==> 含有数据的VNode
//todo... combine
let _tmp = combine(ast,this._data);
return _tmp;
}
}
编写 combine
// Vue : 将 AST 和 data 结合 生成 VNode
// 现简化:待有“坑”的 VNode + data ==> 含有数据的VNode
// 模拟 AST --> VNode 行为
function combine(vnode, data) {
let _type = vnode.type;
let _data = vnode.data;
let _value = vnode.value;
let _tag = vnode.tag;
let _children = vnode.children;
let _vnode = null;
if (_type === 3) {
//文本结点
//填“坑”
_value = _value.replace(r,function (_,g) {
return getValueByPath(data,g.trim());
});
_vnode = new VNode(_tag, _data, _value, _type);
} else if (_type === 1) {
//元素结点
_vnode = new VNode( _tag, _data, _value, _type );
_children.forEach( (_subvnode) => _vnode.appendChild( combine( _subvnode, data ) ) );
}
return _vnode;
}
Q5
: 那在mount
为什么写了个render
?
A5
: Vue
本身是可以带有 render
成员
举个例子:
/*<div id="root">
<p>{{name}}</p>
</div>*/
//`Vue`中有一个元素 `elm` 是真正的 `DOM `, 其 `children`同样也是真正的`DOM`
let app = new Vue({
el: '#root',
data: {
name: 'xxx'
},
render: (createElement) => {
//自定义如何生成虚拟 DOM
return createElement('h1');//返回的就是虚拟DOM
//没有使用模板 而是替换模板
//页面:<h1></h1>
}
})
之前 mount
方法中需要提供一个 render
方法,其作用就是生成新的生成虚拟 DOM
,继续编写,
//在 JGVue 构造函数提供保存 options
//...
JGVue.prototype.mount = function () {
/*if(typeof this._options.render !== 'function'){
//todo...
}*/
//调用 mountComponent()之前需要提供一个render方法
//作用: 生成虚拟 DOM
//需要提供一个 render
this.render = this.createRenderFn();//创建 render 函数
//带有缓存(Vue 本身是可以带有 render 成员)
this.mountComponent();
}
JGVue.prototype.mountComponent = function (){
//执行mountComponent()函数
let mount = () => {//一个函数 函数的this 默认全局对象 “函数调用模式”
this.update(this.render());
//render 用于生成虚拟 DOM update 负责渲染到页面上
}
//改变上下文
mount.call(this);//本质上应该交给 watcher 来调用
//为什么
//this.update(this.render());//使用发布订阅模式,渲染和计算的行为 应该交给 watcher
}
下节课:编写 update
方法,响应式原理
//将虚拟 DOM 渲染到页面中,diff算法在这里
JGVue.prototype.update = function() {
// 简化, 直接生成 HTML DOM replaceChild 到页面中
// 父元素.replaceChild(newElement,oldElement)
// 需要拿到父元素
}
随着
Vue
源码深入学习,同时难度也在增大,所以说要求我们javascript
打下良好的基础。
为此,后面开一专题你应该掌握的 javascript 高阶技能
,为大家提供更多的优质学习内容,希望大家多多支持!