Vue 源码阅读学习(二)

第二课:数据驱动

本节课内容要解决第一节课遗留的问题,如果说第一期没看的话,先看第二期有点困难。链接:第一期

首先,温故知新第一节的重要内容

步骤
1.获取模板 获取元素
2.获取数据 (data …)
3.将数据与模板结合,得到DOM元素
4.渲染页面

我们来理清一下思路。

DOM元素(就是模板)将来会换成虚拟 DOM

DOM是一个树结构,看个例子。

<div id="root">
    <div>
        <p>{{msg}}</p>
    </div>
    <p>{{msg}}</p>
</div>

如果不考虑其中的空白文本节点,那么用图来表示:

在这里插入图片描述


思路

模板要求是一直驻留在内存中,是渲染的根本。我们需要做的就是利用数据和模板结合生成真正的DOM。其中数据发生变化,DOM 变化,而模板不变,然后生成的 DOM 加入页面。

在这里插入图片描述


// 上节课部分代码解读
let generateNode = tmpNode.cloneNode(true);
// 利用模板生成一个需要被渲染的Html标签(准,真正的在页面显示的标签)

// compiler 目的在于利用数据和模板生成真正的 DOM
compiler(generateNode,data)//将"坑"替换

//此时generateNode是在内存当中,而不在页面当中
root.parentNode.replaceChild(generateNode,root)//渲染好的HTML加入页面

使用 Vue 构造函数

先来解决第一课数据驱动留下来的其中一个问题:代码没有整合,而Vue中使用的是构造函数

如果我们要使用构造函数?想想 Vue 的实例

let vm = new Vue({
    el: '#root',
    data: {
        msg: 'Hello'
    }
});

//仿照着写	书写一个 JGVue 的实例 app
let app = new JGVue({
    el: '#root',
    data: {
        msg: 'hello',
        name: 'world'
    }
})

第一步完成,是时候编写 Vue 构造函数

注意习惯 :内部数据使用下划线 _ 开头 只读数据使用 $ 开头

还是根据步骤

function JGVue(options){
    // 需要数据 获取元素 
    this._data = options.data;
    this._el = options.el;
    //todo...
    //准备工作(准备模板)
    //将来改良 this._templateDOM 要变 VNode
	//Vue内部当中保存数据 $el表示 (this.$el = this._tem...)
    this._templateDOM = document.querySelector(this._el);
    this._parent = this._templateDOM.parentNode;
    
    //渲染工作 --> 需要调用方法 
    this.render();
}

最终我们是要渲染到页面 书写一个 render 方法

//在原型当中提供方法
//将模板结合数据 得到HTML 加到页面中
JGVue.prototype.render = function () {
    //todo...
};

render 函数 拆解为 两个步骤

①将模板与数据结合得到真正的 DOM 元素

②将 DOM 元素加入页面,更新页面

分别写两个方法

//编译  将模板与数据结合 得到真正的DOM元素
JGVue.prototype.complier = function () {
	//todo...
};

//将DOM 元素加入页面 更新页面
JGVue.prototype.update = function (real) {
	//todo...
};

tips:后面可以改良成 class 语法

具体实现两个方法

//编译  将模板与数据结合 得到真正的 DOM 元素
JGVue.prototype.complier = function () {
	//用模板拷贝一个准 DOM
    let realHTMLDOM = this._templateDOM.cloneNode(true);
    //调用第一节课写好的 complier
    complier(realHTMLDOM,this._data);
    //todo...
    this.update(realHTMLDOM);
};

//将 DOM 元素加入页面 更新页面
JGVue.prototype.update = function (real) {
    // 拿到父元素 this._parent
			this._parent.replaceChild(real,document.querySelector("#root"));
   
};

最后完成 render 方法的编写

//原型当中提供方法
//将模板结合数据 得到 HTML 加入到页面中
JGVue.prototype.render = function () {
    this.complier();
};

注意:我们现在是所有内容“写死”,实际上 Vue 不是用 replaceChild 而是每次数据变化都会生成一个虚拟 DOM ,虚拟 DOM 会判断页面是否会被渲染 。如果页面中DOM 没有渲染,会把虚拟 DOM 转为真正 DOM 进行渲染到页面;如果已经被渲染了,只是更新DOM中文本。

完整的 vue 构造函数源代码见文章结尾。


第二个问题

现只考虑了单属性 {{name}} ,而 Vue 中大量的使用层级关系,例如{{child.name.firstName}}


深度属性处理(deepProps)

原先的处理方式:

txt = txt.replace(r, function (_, g) {
    debugger;
    let key = g.trim();
	let value = data[key];
	return value;
    //在对正则表达式的处理,是无法获取数据的
});

控制台输出 undefined

在断点测试中, _ : {{name.firstName}} g : name.firstName

此时 key 是一个带 . 的字符串,导致 value 拿不到数据 ,所以要解决的问题就是使用以 foo.bar.baz 的形式可以访问一个对象。

思路:使用字符串路径来访问对象成员。

function getValueByPath(obj, path) {
    /*	例: 
    let obj = {
    	foo: {
    		bar: {
    			bar: 'zhangsan'
    		}
    	}
    }
    */
    let paths = path.split('.');// [foo,bar,baz]
    //获取“键” 再获“值”
    //先获取 obj.foo 其次获取 foo.bar 再次获取 bar.baz...
    let res = null;
    res = obj[paths[0]];
    res = res[paths[1]];
    res = res[paths[2]];
    //思路没问题,但是对象的“层级”无法确定
    //考虑使用循环?
}

function getValueByPath(obj, path) {
    let paths = path.split('.');// [foo,bar,baz]
    let res = obj;
    let prop;
    while (prop = paths.shift()) {
        // paths.shift() 返回它的第0项 删除
        // 每次取一项
        res = res[prop];
    }
    return res;
    //我的思路 递归 循环 ?... reduce方法可尝试...
    //如果有 undefined ?需要解决一下... 
}

//我使用的forEach
function getValueByPath1(obj, path) {
    let paths = path.split('.');// [foo,bar,baz]
    paths.forEach(res => {
        obj = obj[res];
    });
    return obj;
    //如果有 undefined ?需要解决一下... 
}
//还有其他方法 欢迎讨论

感想: 这里我是真的没想到可以这么做!while循环是太,思想很关键,还需要对数组的方法十分熟练,不然哪来的功力?

插曲(夹带“私货”): 数组文章链接


上述讲到模板不变,数据是频繁改变的,Vue 对此改良并且使用了函数柯里化

简单的描述一下函数柯里化(Currying):将接受多个参数的函数,拆分成接受单个参数的函数。给自己挖个坑,下期一定

有兴趣的朋友可以先看看下面这篇文章。

函数柯里化文章


// Vue 改良
// 这个函数在 Vue 编译 模板的时候就已经生成了
function createGetValueByPath(path) {
    let paths = path.split('.');
    return function getValueByPath(obj) {
        let res = obj;
        let prop;
        while (prop = paths.shift()) {
            res = res[prop];
        }
        return res;
    }
}

let o = {
    foo: {
        bar: {
            baz:'hello' 
        }
    }
}

//调用也简便了
let getValueByPath = createGetValueByPath('foo.bar.baz');
let res = getValueByPath(o);//hello

我想了个问题: {{ msg1 + msg2 }} 等 类似 {{表达式}} 如何解决呢?

欢迎讨论!


第三个问题:虚拟 DOM

① 如何将真正的 DOM 转换为虚拟 DOM

② 如何将虚拟 DOM转换为真正的 DOM

思路:与深拷贝类似,比如深度遍历DOM,看到DOM节点把它转换为虚拟DOM,如果是虚拟DOM,有则使用比如createElement()或者createTextNode()…转换为真正的 DOM

那为什么要使用虚拟 DOM

​ 使用虚拟 DOM,简单的来讲就是提高性能,频繁直接操作 DOM,可能给浏览带来性能问题,比如页面刷新,重构和回流等等。而 虚拟 DOM 只是映射到真实 DOM 的渲染,因此不需要包含操作 DOM的方法。

tips: 重绘和回流,面试常客!再给自己挖个坑,下期一定


① 如何将真正的 DOM 转换为虚拟 DOM

<!-- <div> </div> ==> {tag:'div'}
	文本节点 ==> {tag: undefine,value:'文本节点'}
	<div title="1" class="c"></div>  
	==> {tag:'div',data:{title:'1',class:'c'}}
-->
<!--来个例子 真实 DOM -->
<div id="real"><span>i love jisoo</span></div>
<!-- 真实 DOM 对应的 JS 对象(虚拟 DOM ) -->
{
	tag: 'div',
	data: {
		id: 'real'
	}.
	children: [{
		tag: 'span',
		children: 'i love jisoo'
	}]
}

虚拟DOM是用 VNode 这个构造函数来描述这个 DOM节点。我们一起书写一下。

class VNode {
    constructor(tag,data,value,type) {
        //构造器
// tag 节点为 undefined 直接写 tag.toLowerCase()会报错 
        this.tag = tag && tag.toLowerCase();
        this.data = data;
        this.value = value;
        this.type = type;
        this.children = []
    }

    appendChild(vnode) {
        //考虑子元素追加
        this.children.push(vnode);
    }
}
//还有很多,这是极简版

//使用递归 来遍历 DOM 元素来生成虚拟 DOM
//Vue源码使用栈结构,使用栈存储父元素来实现递归生成(先不考虑,算法之痛)
function getVNode(node) {
	//获取虚拟DOM节点
}

完成getVNode的编写

function getVNode(node) {
	//获取虚拟DOM节点
	let nodeType = node.nodeType;//类型值
    let _vnode = null;
    if(nodeType === 1){//这里看过源码阅读(一)估计有印象吧
        //元素
        let nodeName = node.nodeName;
        let attrs = node.attributes;
     //attrs 返回所有属性构成的伪数组,而我们要把它包装成 data
     //伪数组转换成对象
        let _attrObj = {};
        for(let i = 0; i < attrs.length; i++ ){
            //attrs[i]属性节点(nodeType == 2)
            //用 nodeName 和 nodeValue 来描述这样的结构
            _attrObj[attrs[i].nodeName] = attrs[i].nodeValue;
        }
        //来初始化
        _vnode = new VNode(nodeName,_attrObj,undefined,nodeType);
        //考虑 node(真正的DOM) 子元素
        //todo...
    }else if(nodeType === 3){
        //todo...
    }
}

考虑 node(真正的 DOM)的子元素 (追加)

//考虑 node(真正的DOM) 子元素 对应19 20 行
let childNodes = node.childNodes;
for(let i = 0; i < childNodes.length; i++ ){
    //对每一个childNodes进行生成虚拟DOM 加入VNode
    _vnode.appendChild(getVNode(childNodes[i]));//递归
}
//...
//...
else if(nodeType === 3){
    //文本节点
    _vnode = new 	VNode(undefined,undefined,node.nodeValue,nodeType); 
}
return  _vnode;
//...

完成!

let root = document.querySelector('#root');
let vroot = getVNode(root);
console.log(vroot);

DOM 结构

<div id="root">
    <div>
        <div>hello1</div>
        <div>hello2</div>
        <div>hello3</div>
        <ul>
            <li>1</li>
            <li>2</li>
            <li>3</li>
        </ul>
    </div>
</div>

浏览器控制台输出
在这里插入图片描述

源码见文章结尾处。


小练习:将 VNode 转换为真正的 DOM

//将 vNode 转换为真正的 DOM
function parseVNode(vnode){
    //小练习
    //take a try!
}

嘿,答应我不要着急看答案,自己尝试着书写,相信你自己可以的!


//将 vNode 转换为真正的 DOM
function parseVNode(vnode){
    //创建真正的 DOM
    let type = vnode.type;//结点类型值
    let _node = null;//真正的 DOM 结点
    if(type === 3){
        //文本节点
        //创建文本结点 对这些原生api要熟悉
        return document.createTextNode(vnode.value);
    }else if(type === 1){
        _node = document.createElement(vnode.tag);
        //标签属性
        let data = vnode.data;
        //data是键值对 比如 data: {class:"1"}
        Object.keys(data).forEach((item) => {
            let attrName = item;
            let attrValue = data[item];
            //添加它本身的属性
            _node.setAttribute(attrName,attrValue);
        });
        //该结点的子元素
        let children = vnode.children;
        children.forEach((subvnode) => {
// appendChild 添加子节点 跟 VNode 类中方法 appendChild 不是同一个方法
            _node.appendChild(parseVNode(subvnode));
             递归转换子元素 ( 虚拟 DOM )
        });
        return _node;
    }
}

// 在真正的 Vue 中也是使用递归 + 栈 数据类型
// 我在 DOM 结构中 添加了类 
let dom2 = parseVNode( vroot );
// 验证
console.log( dom2 );

/*
<div id="root">
    <div class="1">
        <div class="2">hello1</div>
        <div>hello2</div>
        <div>hello3</div>
        <ul>
            <li>1</li>
            <li>2</li>
            <li>3</li>
        </ul>
    </div>
</div>
*/

浏览器控制台输出

在这里插入图片描述

没问题,验证成功!


vue 构造函数源码

let r = /\{\{(.+?)\}\}/g;
function compiler(template, data) {
	//判断template是什么数据类型
	//现在的案例 template 是 DOM 元素
	//在真正的 Vue 源码中 DOM --> 字符串模板 --> 抽象语法树 --> VNode --> 真正的DOM
	let childNodes = template.childNodes;//取出子元素
	for (let i = 0; i < childNodes.length; i++) {
    	let type = childNodes[i].nodeType;
		// type 值为 1 元素节点, 3为文本节点
		if (type === 3) {
        	//文本节点 可以判断里面是否有 {{}} 插值
        	let txt = childNodes[i].nodeValue; 
        	// 该属性只有文本节点 才有意义
			//判断是否有双花括号
        	txt = txt.replace(r, function (_, g) {
            	//replace 使用正则匹配一次 函数就会调用一次
            	//函数的第 0 个 参数 表示匹配的内容
                //函数的第 n 个 参数 表示正则中第n组
                let path = g.trim();//{{}}里面的的内容
                let value = getValueByPath(data, path);
                //将 {{xxxx}} 用这个值替换
                return value;
            });
            //注意:txt现在和DOM元素是没有关系的
            childNodes[i].nodeValue = txt;
        } else if (type === 1) {
            //元素 考虑它是否有子元素 是否需要将其子元素  判断是否要进行插值
            compiler(childNodes[i], data);
        }
    }
}
function JGVue(options) {
//习惯 :内部的数据使用下划线 _ 开头 ,只读数据 使用 $开头
// 需要啥那啥 需要 data 那就拿 data
    this._data = options.data;
    this._el = options.el;

    //准备工作 (准备模板)
    //将来改良 变 VNode

    //Vue内部当中保存数据 $el表示
    this._templateDOM = document.querySelector(this._el);
    this._parent = this._templateDOM.parentNode;

    //渲染工作
    //需要调用方法
    this.render();
}

//原型当中提供方法
//将模板结合数据 得到HTML 加到页面中
JGVue.prototype.render = function () {
    this.complier();
};

// render 拆解 两个步骤
//编译  将模板与数据结合 得到真正的DOM元素
JGVue.prototype.complier = function () {
    let realHTMLDOM = this._templateDOM.cloneNode(true);//用模板拷贝一个准DOM
    compiler(realHTMLDOM, this._data);
    this.update(realHTMLDOM);
};

//将DOM元素加入页面 更新页面
JGVue.prototype.update = function (real) {
    this._parent.replaceChild(real, document.querySelector("#root"));
};

//改良 class 语法
//想想怎么用:
let app = new JGVue({
    el: '#root',
    data: {
        msg: 'hello',
        name: 'world'
    }
})

function getValueByPath(obj, path) {
    let paths = path.split('.');// [foo,bar,baz]
    let res = obj;
    let prop;
    while (prop = paths.shift()) {
        // paths.shift() 返回它的第0项 删除
        // 每次取一项
        res = res[prop];
    }
    return res;
}

虚拟 DOM和真正的 DOM 的相互“转换”源代码:

class VNode {
    //tag 标签 data 描述属性  value 文本内容 type  (elm)
    constructor(tag, data, value, type) {
        this.tag = tag && tag.toLowerCase();
        this.data = data;
        this.value = value;
        this.type = type;
        this.children = []
    }

    appendChild(vnode) {
        this.children.push(vnode);
    }
}

//使用递归 来遍历 DOM 元素来生成虚拟 DOM
function getVNode(node) {
	//获取虚拟DOM节点
    let nodeType = node.nodeType;//类型值
    let _vnode = null;
    if (nodeType === 1) {
        //这里看过源码阅读(一)估计有印象吧
        //元素
        let nodeName = node.nodeName;
        let attrs = node.attributes;
        //attrs 返回所有属性构成的伪数组,而我们要把它包装成 data
        //伪数组转换成对象
        let _attrObj = {};
        for (let i = 0; i < attrs.length; i++) {
            //attrs[i]属性节点(nodeType == 2)
            //用 nodeName 和 nodeValue 来描述这样的结构
            _attrObj[attrs[i].nodeName] = attrs[i].nodeValue;
        }
        //来初始化
        _vnode = new VNode(nodeName, _attrObj, undefined, nodeType);
        //考虑 node(真正的DOM) 子元素
        let childNodes = node.childNodes;
        for (let i = 0; i < childNodes.length; i++) {
            //对每一个childNodes进行生成虚拟DOM 加入VNode
          _vnode.appendChild(getVNode(childNodes[i]));//递归
        }
    } else if (nodeType === 3) {
        _vnode = new VNode(undefined, undefined, node.nodeValue, nodeType);
    }
    return _vnode;
}

//将 vNode 转换为真正的 DOM
function parseVNode(vnode) {
    //创建真正的 DOM
    let type = vnode.type;
    let _node = null;//真正的 DOM 结点
    if (type === 3) {
        //文本节点
        return document.createTextNode(vnode.value);
    } else if (type === 1) {
        _node = document.createElement(vnode.tag);
        //标签属性
        let data = vnode.data;
        //data是键值对 比如 data: {class:"1"}
        Object.keys(data).forEach((item) => {
            let attrName = item;
            let attrValue = data[item];
            _node.setAttribute(attrName, attrValue);
        });
        //该结点的子元素
        let children = vnode.children;
        children.forEach((subvnode) => {
            // appendChild 添加子节点 跟 VNode 类中方法 appendChild 不是同一个方法
            _node.appendChild(parseVNode(subvnode));
            // 递归转换子元素 ( 虚拟 DOM )
        });
        return _node;
    }
}

let root = document.querySelector('#root');
let vroot = getVNode(root);
console.log(vroot);
let dom2 = parseVNode(vroot);
console.log(dom2);

第二期难度有点高,说实话,我刚开始看的时候也是有些懵的,所以朋友们不要灰心,自己尝试着来,一定动手实践,边看边听,课后做笔记,写出自己感悟与理解,相信你也能读懂 Vue 源码!


  • 8
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值