读懂此文的前置知识点: 递归、简单的正则、正则捕获组、nodetype
1. 模版替换
<div id="app">
<h2>{{msg}}</h2>
<h1>{{qwe}}</h1>
</div>
<script>
let obj = {
msg: 'Hello Vue!',
qwe: 'qwer'
}
new Vue({
el: '#app',
data: obj
})
</script>
先完成模版替换的功能,就是当我们new vue的时候,需要把模版里的内容替换成data里的值。所以接下来写一个compile函数
class Vue {
constructor(options) {
this.el = options.el;
this.data = options.data;
this.compile(this.data);
}
compile(data) {
// 正经的ast解析肯定不是这么简单的正则,而且一般不是用正则来做的
const appDom = document.querySelector(this.el);
const findElement = (dom) => {
dom.childNodes.forEach(e => {
// 1是元素节点
if (e.nodeType === 1) {
findElement(e)
}
// 3是文本节点
if (e.nodeType === 3) {
let textContent = e.textContent;
const match = textContent.match(/\{\{(.*)\}\}/);
if (match) {
e.textContent = textContent.replace(match[0], data[match[1]]);
}
}
})
}
findElement(appDom)
}
}
let obj = {
msg: 'Hello Vue!',
qwe: 'qwer'
}
new Vue({
el: '#app',
data: obj
})
这里有一个递归,终点是找到所有的文本节点,然后用正则去匹配{{}},然后进行文本替换。 可以在浏览器看下,我们已经成功实现了模版替换...
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<h2>{{msg}}</h2>
<h1>{{qwe}}</h1>
</div>
<script>
class Vue {
constructor(options) {
this.el = options.el;
this.data = options.data;
this.compile(this.data);
}
compile(data) {
// 正经的ast解析肯定不是这么简单的正则,而且一般不是用正则来做的
const appDom = document.querySelector(this.el);
const findElement = (dom) => {
dom.childNodes.forEach(e => {
// 1是元素节点
if (e.nodeType === 1) {
findElement(e)
}
// 3是文本节点
if (e.nodeType === 3) {
let textContent = e.textContent;
const match = textContent.match(/\{\{(.*)\}\}/);
if (match) {
e.textContent = textContent.replace(match[0], data[match[1]]);
}
}
})
}
findElement(appDom)
}
}
let obj = {
msg: 'Hello Vue!',
qwe: 'qwer'
}
new Vue({
el: '#app',
data: obj
})
</script>
</body>
</html>
2. 响应式
实现响应式的目标是当我们obj里的值变换后,模版也能更新为最新的值,其实就是两步
- 数据变化时候我知道
- 我知道去更新哪些东西(这个数据影响了哪里,然后我去更新他) 来实现第一步
class Vue {
constructor(options) {
this.el = options.el;
this.data = options.data;
this.compile(this.data);
}
reactiveData(obj) {
Object.keys(obj).forEach((key) => {
// 这里是个闭包,foreach的回调是个function,get函数在这个function的内部声明,在外部触发get set时调用,形成闭包机制
let val = obj[key];
Object.defineProperty(obj, key, {
get: () => {
console.log(`get ${key}`);
return val
},
set: (newVal) => {
console.log(`set ${key}`);
// set新值时,替换了闭包里的变量
val = newVal;
}
})
})
}
}
第二步的话,是有一个发布订阅模式,大概意思就是有一个双十一卖东西,有很多人都想买这个东西,于是提前订阅了,等到双十一开始,就会给每一个提前订阅的人发通知;对应到我们这里就是数据就是商品,用到这个数据的dom节点就是想买商品的人,等到数据变化时候需要去通知这些dom。接下来更新之前的响应式函数
reactiveData(obj) {
Object.keys(obj).forEach((key) => {
// 这里是个闭包,foreach的回调是个function,get函数在这个function的内部声明,在外部出发get set时调用,形成闭包机制
let val = obj[key];
// 利用闭包,给每个key都存储一个各自的依赖列表
let deps = [];
Object.defineProperty(obj, key, {
get: () => {
console.log(`get ${key}`);
if (window.callBack) {
// 有人订阅了
deps.push(callBack)
}
return val
},
set: (newVal) => {
console.log(`set ${key}`);
// set新值时,替换了闭包里的变量
val = newVal;
// 发布更新
deps.forEach(e => e())
}
})
})
}
在哪里去添加订阅呢?哪个dom用到了哪个key就添加订阅,接下来更新之前的compile函数
compile(data) {
// 正经的ast解析肯定不是这么简单的正则,而且一般不是用正则来做的
const appDom = document.querySelector(this.el);
const findElement = (dom) => {
dom.childNodes.forEach(e => {
if (e.nodeType === 1) {
findElement(e)
}
if (e.nodeType === 3) {
let textContent = e.textContent;
const match = textContent.match(/\{\{(.*)\}\}/);
function callBack() {
// 这个callbakc也是一个闭包,因此当callback被触发时,能回到当初定义时的作用域,更新对应的node
e.textContent = textContent.replace(match[0], data[match[1]]);
}
// 把callback挂到window上,方便添加依赖
window.callBack = callBack;
if (match) {
// 这里是一个对象get操作,会进入我们的defineproperty里
e.textContent = textContent.replace(match[0], data[match[1]]);
}
window.callBack = null
}
})
}
findElement(appDom)
}
到这里,我们的代码如下,去验证一下吧
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<h2>{{msg}}</h2>
<h1>{{qwe}}</h1>
</div>
<button>change data</button>
<script>
const btn = document.querySelector('button');
btn.addEventListener('click', function() {
obj.qwe = 'zzzzzz'
})
class Vue {
constructor(options) {
this.el = options.el;
this.data = options.data;
this.reactiveData(this.data)
this.compile(this.data);
}
compile(data) {
// 正经的ast解析肯定不是这么简单的正则,而且一般不是用正则来做的
const appDom = document.querySelector(this.el);
const findElement = (dom) => {
dom.childNodes.forEach(e => {
if (e.nodeType === 1) {
findElement(e)
}
if (e.nodeType === 3) {
let textContent = e.textContent;
const match = textContent.match(/\{\{(.*)\}\}/);
function callBack() {
// 这个callbakc也是一个闭包,因此当callback被触发时,能回到当初定义时的作用域,更新对应的node
e.textContent = textContent.replace(match[0], data[match[1]]);
}
// 把callback挂到window上,方便添加依赖
window.callBack = callBack;
if (match) {
// 这里是一个对象get操作,会进入我们的defineproperty里
e.textContent = textContent.replace(match[0], data[match[1]]);
}
window.callBack = null
}
})
}
findElement(appDom)
}
reactiveData(obj) {
Object.keys(obj).forEach((key) => {
// 这里是个闭包,foreach的回调是个function,get函数在这个function的内部声明,在外部出发get set时调用,形成闭包机制
let val = obj[key];
// 利用闭包,给每个key都存储一个各自的依赖列表
let deps = [];
Object.defineProperty(obj, key, {
get: () => {
console.log(`get ${key}`);
if (window.callBack) {
// 有人订阅了
deps.push(callBack)
}
return val
},
set: (newVal) => {
console.log(`set ${key}`);
// set新值时,替换了闭包里的变量
val = newVal;
// 发布更新
deps.forEach(e => e())
}
})
})
}
}
let obj = {
msg: 'Hello Vue!',
qwe: 'qwer'
}
new Vue({
el: '#app',
data: obj
})
</script>
</body>
</html>
3. 嵌套对象的响应式
目前我们的对象只是一层的,多层的话可能不好使了
<div id="app">
<h2>{{msg}}</h2>
<h1>{{qwe}}</h1>
<h1>{{info.name}}</h1>
</div>
改写响应式函数,如果值是object就再调用一次reactiveData函数
reactiveData(obj) {
Object.keys(obj).forEach((key) => {
if (typeof obj[key] === 'object') {
this.reactiveData(obj[key]);
}
// 这里是个闭包,foreach的回调是个function,get函数在这个function的内部声明,在外部出发get set时调用,形成闭包机制
let val = obj[key];
// 利用闭包,给每个key都存储一个各自的依赖列表
let deps = [];
Object.defineProperty(obj, key, {
get: () => {
console.log(`get ${key}`);
if (window.callBack) {
// 有人订阅了
deps.push(callBack)
}
return val
},
set: (newVal) => {
console.log(`set ${key}`);
// set新值时,替换了闭包里的变量
val = newVal;
// 发布更新
deps.forEach(e => e())
}
})
})
}
然后是模版部分,我们先写一个工具函数,如下:
function getValFromExpression(str, obj) {
const keys = str.split('.');
keys.forEach(key => {
obj = obj[key]
})
return obj
}
getValFromExpression('info.name', {info: {name: 'w'}})
改写compile函数
compile(data) {
// 正经的ast解析肯定不是这么简单的正则,而且一般不是用正则来做的
const appDom = document.querySelector(this.el);
const findElement = (dom) => {
dom.childNodes.forEach(e => {
if (e.nodeType === 1) {
findElement(e)
}
if (e.nodeType === 3) {
let textContent = e.textContent;
const match = textContent.match(/\{\{(.*)\}\}/);
function callBack() {
// 这个callbakc也是一个闭包,因此当callback被触发时,能回到当初定义时的作用域,更新对应的node
e.textContent = textContent.replace(match[0], getValFromExpression(match[1],data));
}
// 把callback挂到window上,方便添加依赖
window.callBack = callBack;
if (match) {
// 这里是一个对象get操作,会进入我们的defineproperty里
e.textContent = textContent.replace(match[0], getValFromExpression(match[1],data));
}
window.callBack = null
}
})
}
findElement(appDom)
}
好了,最终完整代码如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<h2>{{msg}}</h2>
<h1>{{qwe}}</h1>
<h1>{{info.name}}</h1>
</div>
<button>change data</button>
<script>
const btn = document.querySelector('button');
btn.addEventListener('click', function() {
obj.qwe = 'zzzzzz';
obj.info.name = 'wwww'
})
class Vue {
constructor(options) {
this.el = options.el;
this.data = options.data;
this.reactiveData(this.data)
this.compile(this.data);
}
compile(data) {
// 正经的ast解析肯定不是这么简单的正则,而且一般不是用正则来做的
const appDom = document.querySelector(this.el);
const findElement = (dom) => {
dom.childNodes.forEach(e => {
if (e.nodeType === 1) {
findElement(e)
}
if (e.nodeType === 3) {
let textContent = e.textContent;
const match = textContent.match(/\{\{(.*)\}\}/);
function callBack() {
// 这个callbakc也是一个闭包,因此当callback被触发时,能回到当初定义时的作用域,更新对应的node
e.textContent = textContent.replace(match[0], getValFromExpression(match[1],data));
}
// 把callback挂到window上,方便添加依赖
window.callBack = callBack;
if (match) {
// 这里是一个对象get操作,会进入我们的defineproperty里
e.textContent = textContent.replace(match[0], getValFromExpression(match[1],data));
}
window.callBack = null
}
})
}
findElement(appDom)
}
reactiveData(obj) {
Object.keys(obj).forEach((key) => {
if (typeof obj[key] === 'object') {
this.reactiveData(obj[key]);
}
// 这里是个闭包,foreach的回调是个function,get函数在这个function的内部声明,在外部出发get set时调用,形成闭包机制
let val = obj[key];
// 利用闭包,给每个key都存储一个各自的依赖列表
let deps = [];
Object.defineProperty(obj, key, {
get: () => {
console.log(`get ${key}`);
if (window.callBack) {
// 有人订阅了
deps.push(callBack)
}
return val
},
set: (newVal) => {
console.log(`set ${key}`);
// set新值时,替换了闭包里的变量
val = newVal;
// 发布更新
deps.forEach(e => e())
}
})
})
}
}
let obj = {
msg: 'Hello Vue!',
qwe: 'qwer',
info: {
name: 'w'
}
}
new Vue({
el: '#app',
data: obj
})
function getValFromExpression(str, obj) {
const keys = str.split('.');
keys.forEach(key => {
obj = obj[key]
})
return obj
}
getValFromExpression('info.name', {info: {name: 'w'}})
</script>
</body>
</html>