Vue 源码阅读学习(三)

第三节:函数柯里化与渲染模型

嘿,朋友们,本节是 Vue 源码阅读的第三讲。Vue 源码阅读系列得到了赞赏,我很高兴,同时希望大家可以给予反馈!我虚心接纳您的意见!
如果没有看之前的第一讲第二讲的内容可以先去看看哦,好啦,让我们开始吧。

本节内容开始之前,我们先了解一些概念。

函数式编程(学有余力可以看看)这里不做展开。


概念

柯里化

​ 一个函数原本有多个参数,只传入一个参数,生成一个函数,由新函数来接受剩余参数来运行得到结果。

偏函数

​ 一个函数原本有多个参数,只传入一部分参数,生成一个函数,由新函数来接受剩余参数来运行得到结果。

高阶函数

​ 变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。


Q1: 谈谈为什么要使用柯里化?

A1: 为了提高性能,使用柯里化可以缓存一部分能力。

使用两个案例来说明:

Ⅰ. 判断元素

Vue 本质上是使用 HTML 的字符串作为模板的,将字符串的模板转换为抽象语法树(AST),再转换为虚拟DOM。

​ 抽象语法树后面简写成 AST

​ 下图为表示式 a+a*(b-c)+(b-c)*dAST图示,当然 VueAST不一定长这样。

在这里插入图片描述


​ 过程:

​ 模板 --> 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)

​ 这里我觉得老师讲的例子好像跟柯里化没有具体联系,该例子是用空间换时间,进行代价转移,或许是我理解有误,欢迎讨论!


Ⅱ. 虚拟 DOMrender 方法

思考: 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


编写 createRenderFnupdate 方法

//生成 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 算法所作的:将 oldVNodeNewVNode比较

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 高阶技能,为大家提供更多的优质学习内容,希望大家多多支持!


  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值