终于到了本系列的最后一篇文章了,这篇文章将会做一个简单的模板编译器,并结合上一篇文章的渲染watcher
来实现一个小demo
。
由于
observe,Observer,defineReactive
三个文件会互相引用,因此我把他们整合到了一个文件中,便于使用,并且也符合了vue
源码的结构
项目地址:gitee
系列文章地址:
MVVM类
我们会像vue
一样,建立一个类,叫做MVVM
,接收一个配置参数:
class MVVM {
constructor(options) {
this.$el = options.el
this.$data = options.data
// 将数据变为响应式
observe(this.$data)
// 模板编译
if (this.$el) new Compiler(this.$el, this)
}
}
在Vue
中,我们可以直接用vue
实例访问数据,所以,我们也可以实现这样的功能,定义一个方法:
proxyData(data) {
Object.keys(data).forEach((key) => {
Object.defineProperty(this, key, {
get() {
return data[key]
},
set(newVal) {
if (data[key] === newVal) return
data[key] = newVal
}
})
})
}
实例化时执行该方法即可
constructor(options) {
this.$el = options.el
this.$data = options.data
// 先代理数据,这样之后需要数据时就不需要用this.$data.prop了,直接使用this.prop即可
this.proxyData(this.$data)
observe(this.$data)
if (this.$el) new Compiler(this.$el, this)
}
模板编译器
将数据设置为响应式后,开始模板编译。模板编译思路如下:先将el
从页面上取出来放到fragment
中,编译完成后再放回页面中。
class Compiler {
constructor(el, vm) {
// el可以是选择器或元素节点
this.el = isElementNode(el) ? el : document.querySelector(el)
this.vm = vm
let fragment = node2Fragment(this.el)
this.compile(fragment)
this.el.appendChild(fragment)
}
}
function isElementNode(node) {
return node.nodeType === 1
}
function node2Fragment(node) {
let fragment = document.createDocumentFragment()
let firstChild = node.firstChild
while (firstChild) {
fragment.appendChild(firstChild)
firstChild = node.firstChild
}
return fragment
}
compile
方法就是主编译方法
compile(node) {
let childNodes = Array.from(node.childNodes)
childNodes.forEach((c) => {
if (isElementNode(c)) {
this.compileElementNode(c)
} else {
this.compileTextNode(c)
}
})
}
元素节点编译方法如下
compileElementNode(node) {
// 获取元素节点的属性来找出指令
Array.from(node.attributes).forEach(({ name, value: expression }) => {
if (isDirective(name)) {
// 指令以v-开头
const directive = name.split('-')[1]
// 渲染watcher
new Watcher(
this.vm,
// 相当于解析指令的渲染函数
directiveCompiler[directive](node, expression, this.vm)
)
}
})
// 如果该节点时元素节点,应该递归编译该节点内部的节点
this.compile(node)
}
function isDirective(str) {
return str.startsWith('v-')
}
指令解析器如下:
function setValue(vm, expression, value) {
const keys = expression.split('.')
let obj = vm.$data
for (let i = 0; i < keys.length - 1; i++) {
obj = obj[keys[i]]
}
obj[keys.slice(-1)] = value
}
// 只处理了v-model指令
export default {
model(node, expression, vm) {
// 监听输入框的input方法,实现双向绑定
node.addEventListener('input', (e) =>
setValue(vm, expression, e.target.value)
)
const value = parsePath(expression).call(vm, vm)
return function () {
node.value = value
}
}
}
文本解析方法
compileTextNode(node) {
if (/\{\{(.+?)\}\}/g.test(node.textContent)) {
// 渲染watcher
new Watcher(this.vm, textCompiler(node, this.vm))
}
}
textCompiler
方法如下
function textCompiler(node, vm) {
const text = node.textContent
return function () {
const content = text.replace(/\{\{(.+?)\}\}/g, (...args) => {
const path = args[1].trim()
const val = parsePath(path).call(vm, vm)
if (isObject(val)) {
// 如果模板内是对象,使用JSON.stringify来显示
// JSON.stringify也会访问对象内部的属性
// 这样就完成了对该对象所有属性的依赖收集
return JSON.stringify(val, null, 1) // 第三个参数:空格数量
}
return val
})
node.textContent = content
}
}
这样,我们就实现了一个简单模板编译器,实例化MVVM
后,就有一个响应式的应用了!!
// index.js
import MVVM from './MVVM'
window.vm = new MVVM({
el: '.app',
data: {
obj: {
a: 1,
b: 2
},
a: 1,
arr: [
{
a: 1
}
]
}
})
<div class="app">
{{ obj }}
<br />
{{ arr }}
<br />
<input type="text" v-model="obj.a" />
</div>
两个全局方法
不知道大家是否还记得,前面的文章中提到过Vue.$set
方法就使用了__ob__
属性来添加响应式数据,我们来看一下
$set(target, key, value) {
// 对于数组利用splice实现添加元素
if (Array.isArray(target)) {
// 如果splice索引超过数组长度会报错
target.length = Math.max(target.length, key)
target.splice(key, 1, value)
return value
}
// 对于对象,如果该属性已经存在,直接赋值
if (key in target && !(key in Object.prototype)) {
target[key] = value
return value
}
const ob = target.__ob__
// 如果目标对象不是响应式对象,直接赋值
if (!ob) {
target[key] = value
return value
}
// 设置响应式属性
defineReactive(target, key, value)
// 派发更新
ob.dep.notify()
return value
}
此外,Vue.$delete
方法也是如此
$delete(target, key) {
// 对于数组用splice方法删除元素
if (Array.isArray(target)) {
target.splice(key, 1)
return
}
const ob = target.__ob__
// 如果对象没有该属性,直接返回
if (!target.hasOwnProperty(key)) return
delete target[key]
// 如果不是响应式对象,则不需要派发更新
if (!ob) return
// 对于响应式对象,删除属性后要派发更新
ob.dep.notify()
}
因此,我们也可以总结出下面的结论
getter
和setter
闭包中保存的dep
用来存储依赖纯对象的属性的watcher
,只有这个闭包能够访问到这个变量getter
和setter
闭包中保存的childOb
,就是与这个属性同级的__ob__
属性,这个属性存储一个Observer
实例,实例上也有一个dep
属性,这个属性可以保存依赖数组的watcher
,并且外部的方法也可以通过__ob__
属性来派发更新
总结
这个系列文章的到此就告一段落了,其实,还有很多内容没有涉及到,比如计算属性watcher
等等(主要是我不会)。如果大家意见或者建议,欢迎评论区留言,如果大家看过其他好的文章,也可以留言分享。
之后还会更新vue
项目总结的系列文章,也欢迎大家关注,谢谢!!!