第二课:数据驱动
本节课内容要解决第一节课遗留的问题,如果说第一期没看的话,先看第二期有点困难。链接:第一期
首先,温故知新第一节的重要内容
步骤 |
---|
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
源码!