前言:最近由于疫情原因,导致久久未开学,在家本着闲着也是闲着的心情,就开始研究起了vue的源码。看了一下,才发现所谓的源码并没有想象的那么难,就是代码量比较多,都是一些基础的代码,难就难在作者的设计思想,挺佩服作者的设计思想的。话不多说,直接进入正题:
vue渲染页面的两种方式:
第一种:通过{{}}的形式
第二种:通过指令的形式:如v-text、v-html、v-model等
实现的细节:
1.利用文档碎片,减少页面的重绘和回流
解释:因为vue在渲染页面的时候,需要把{{}}
和指令替换成对面的变量值,这样就需要进行大量的dom的操作,很消耗性能。所以vue就把要渲染的区域先获取过来(这就是为什么vue要让我们申明一个区域),把该区域放进文档碎片中,对该区域渲染完后再放回页面中,这样我们整个过程就操作了两次dom,大大的优化了浏览器性能。(文档碎片:在我看来,就是用来存储dom元素的变量,该变量的所有的属性和方法都和dom元素的一样。)
2.利用元素节点和文本节点去区分{{}}和指令
元素节点:例如:<h1 v-text='msg'>2121</h1>
,这就是一个元素节点。
文本节点:上面那个例子的2121
,这就是一个文本节点。
解释:当我们知道元素节点和文本节点的基本概念,我们就可以这么想了:{{}}
是在文本节点中,指令一般以属性的形式放在元素节点中的。我们使用dom元素.nodeType
属性来区分它是元素节点还是文本节点。区分出来就可以进行对应得渲染。
3.渲染指令
明确一下我们需要的东西:
- 指令的名称:例如text、html、model。
- 变量名:例如
v-text=’msg‘
中的msg变量名。
需要灵活的使用操作字符串:
- 使用字符串的
split()
方法:例如:我们需要在v-text
中分离出text
,我们就可以使用'v-text'.split('-')
,把字符串'v-text'
分割成数组['v','text']
这种形式。
需要理解es6中的结构赋值:
- 我们获取元素节点的时候,使用
dom.attributes
的属性,就元素节点的所有属性都获取过来,但是你要知道,attributes
这个属性返回的是一个伪数组,由于我们需要遍历这个数组,所以要把这个数组转成真数组,使用es6的解构赋值:[...dom.attributes]
。
需要灵活的使用数组的reduce方法:
- 我们在获取到变量名的时候,有时候获取到的是
msg
这样一层的,有时候会遇到person,name
这样两层的。这个时候如果我们直接vm.$data[变量名]
(vm.$data
是用户传过来的data),获取
msg是获取的到的,但是
person.name`是获取不到的。 - 这时我们就需要利用
split('.')
以点的形式把获取到的变量名弄成数组,利用reduce方法去一层一层的获取。
3.渲染页面上的{{}}
我们需要的的知识点:
- 字符串的
replace()
方法 - 正则表达式
我在这说一下replace()和正则表达式中的括号的配合使用:
注:我们在使用replace方法的时候,我们一般都认为第一个参数是要替换的字符或者正则表达式,第二个参数是换成什么的字符串。其实第二个参数,还可以写成函数的形式。我举个例子:
let span = '<span>123abc'
span.replace(/<.+?>/g, (...args) => {
console.log(args)
})
结果:
我们会发现第二参数写成函数的形式的时候,这个函数有个默认参数,这个参数我们扩展运算符来接收,不限制参数的个数。我们发现:
- 第一个参数是截取到的字符串
- 第二参数是截取到字符串的开始下标
- 第三个参数是整个参数
有些时候,我们有以下需求,我们需要获取<>包裹中的内容,即上面的例子的span。我们就可以结合正则表达式的括号来获取了。(在我们需要的地方加个括号)
let span = '<span>123abc'
span.replace(/<(.+?)>/g, (...args) => {
console.log(args)
})
结果:
正则表达式加了括号之后,我们获取到的参数就多了一个,获取到的args[1]就是我们需要的<>包裹的内容。
如果你能理解上面的知识点,你就可以看的懂渲染的代码了:
html代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title></title>
</head>
<body>
<div id="app">
<h2>{{person.name}} -- {{person.age}}</h2>
<h3>{{person.fav}}</h3>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<h3>{{msg}}</h3>
<div v-text='msg'></div>
<div v-text='person.fav'></div>
<div v-html='htmlStr'></div>
<div v-html='person.fav'></div>
<input type="text" v-model='person.name'>
<button v-on:click='add'>v-on</button>
<button @click='add'>@click</button>
<p v-text='af'></p>
</div>
</body>
<script src="./My.js"></script>
<script>
let vm = new Vue({
el: '#app',
data: {
person: {
name: '小明',
age: 15,
fav: '女'
},
msg: '你好',
htmlStr: '<h3>html字符串</h3>'
},
methods: {
add() {
console.log('add方法调用了')
}
}
})
</script>
</html>
vue渲染实现js代码:
const compileUtil = {
getVal (arg, vm) {
// 使用reduce一层一层的取.如果你直接vm.$data[arg],其中arg是person.fav这样的话,取不到的。一层就可以取到。
return arg.split('.').reduce((data, currentData) => {
return data[currentData]
}, vm.$data)
},
on (node, methodName, vm, eventName) {
// 1.获取方法
// 2.添加事件
let method = vm.options.methods && vm.options.methods[methodName]
node.addEventListener(eventName, method)
},
text (node, arg, vm) {
// 1.获取值
// 2.更新页面
let textContent
if (arg.indexOf('{{') !== -1) {
// 正则表达式中的()是为了标识子字符串,replace函数的第二个参数用得到。
textContent = arg.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getVal(args[1], vm)
})
} else {
textContent = this.getVal(arg, vm)
}
node.textContent = textContent
},
html (node, arg, vm) {
// 1.获取值
// 2.更新页面
const htmlContent = this.getVal(arg, vm)
node.innerHTML = htmlContent
},
model (node, arg, vm) {
// 1.获取值
// 2.更新页面
const inputVal = this.getVal(arg, vm)
node.value = inputVal
}
}
class Compile {
constructor(el, vm) {
this.el = el.nodeType === 1 ? el : document.querySelector(el)
this.vm = vm
// 1.创建一个文档碎片,方便操作document,减少性能消耗
this.f = this.createFragment(this.el)
// 2.编译模板
this.compile(this.f)
// 3.将文档碎片放回容器中
this.el.appendChild(this.f)
}
// 创建文档碎片
createFragment (node) {
let f = document.createDocumentFragment()
let firstChild
// 循环遍历根元素的子节点添加到文档碎片中
while (firstChild = node.firstChild) {
// 注:appendChild()方法会把已存在的节点删除
f.appendChild(firstChild)
}
return f
}
compile (node) {
// 编译文档步骤:
// 1.遍历每一个子节点
// 2.编译指令
// 3.编译文本节点中的'{{}}'
const childNodes = node.childNodes
// 遍历每一个子节点
childNodes.forEach(item => {
// 元素节点
if (item.nodeType === 1) {
// 递归遍历每一个子节点
this.compile(item)
// 编译属性上的指令:v-text、v-html、v-model、v-on、@等等
this.compileDir(item)
} else { // 文本节点
// 编译文本中的{{}}
this.compileText(item)
}
})
}
// 编译指令
compileDir (node) {
// node.attributes是伪数组,使用es6的解构赋值弄成数组
const attributes = [...node.attributes]
attributes.forEach(item => {
// item是个object属性,有name和value,所以我们再结构赋值。item:{name:v-text,value: msg}
let { name, value } = item
if (name.startsWith('v-')) { // 处理v-开头的指令。假如指令是v-on:click,下面的值则是:
let directive = name.split('-')[1] // on:click
let dirName = directive.split(':')[0] // on
let eventName = directive.split(':')[1] // click
compileUtil[dirName](node, value, this.vm, eventName)
} else if (name.startsWith('@')) { // @click
let eventName = name.split('@')[1] // click
compileUtil.on(node, value, this.vm, eventName)
}
})
}
// 编译{{}}
compileText (node) {
// 获取文本
const text = node.textContent
if ((/\{\{.+?\}\}/).test(text)) { // 匹配有{{}}的字符串
compileUtil.text(node, text, this.vm)
}
}
}
class Vue {
constructor(options) {
// 绑定数据
this.$el = options.el
this.$data = options.data
this.options = options
if (this.$el) {
// 2.实现编译器
new Compile(this.$el, this)
}
}
}