# 前言
大家都知道Vue的设计思想就是视图View的状态和行为抽象化,让我们将视图UI和业务逻辑分开,即M-V-VM
。
MVVM
的三大要素:
- 数据响应式:监听数据变化并在视图中更i性能
- 模板引擎:提供描述视图的模板语法
- 渲染:将模板转换成Html
今天目标:可创建自己的MyVue实例、数据响应式、双花括号及模板引擎的模拟实现。
# 实现 defineReactive
这里运用的是闭包
的思想
// 数据响应式
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() { // 取值时通过 get 函数 return 出去,则形成闭包
return val;
},
set(newVal) {
if (newVal !== val)
val = newVal;
}
})
}
小试牛刀
简单的实现一个试图更新:时钟
这里需要再增加一个update
函数,在set
函数中设置新值时去更新Dom
// 数据响应式
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() { // 取值时通过 get 函数 return 出去,则形成闭包
return val;
},
set(newVal) {
if (newVal !== val){
val = newVal;
update();
}
}
})
}
// 更新视图
function update(){
app.innerHtml = obj.foo;
}
const obj = {};
defineReactive(obj, 'foo', '')
obj.foo = new Date().toLocaleTimeString();
setInterval(() => {
obj.foo = new Date().toLocaleTimeString();
}, 1000)
如上:小小时钟就实现了,这就是简单是数据驱动更新试图
# 实现 observe
在上面的例子中有个问题:就是每次只能实现一个属性的响应式,即
defineReactive(obj, 'foo', '')
如果有很多属性怎么办???
所以这时需要一个函数来遍历对象的所有属性,即observe
函数
// 遍历属性
function observe(obj ) {
if (typeof obj !== 'object' || obj === null) return
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
小试牛刀
const obj = { foo: '' };
observe(obj)
obj.foo = new Date().toLocaleTimeString();
setInterval(() => {
obj.foo = new Date().toLocaleTimeString();
}, 1000)
如上:小时钟依然可以正常运行
写到这里大家可能发现一个问题:如果
obj
的值还是对象,则不能形成数据响应式,该怎么办呢???
其实很简单,就是利用递归去遍历,那在哪加上递归呢???
一般会选择在defineReactive
中,如下:
// 数据响应式
function defineReactive(obj, key, val) {
// 递归
observe(val);
// 响应式处理
Object.defineProperty(obj, key, {
get() {
return val;
},
set(newVal) {
if (newVal !== val) {
// 保证如果newVal是对象,再次做响应式处理
observe(val);
val = newVal;
}
}
})
}
# 实现 set
之所以会有set
函数是因在初始化遍历的时候,会遍历所有属性并做响应式处理,但在后来动态新增的属性是没有相应式的。
// 动态添加属性
function set(obj, key, val) {
defineReactive(obj, key, val)
}
# 实现 new MyVue
准备工作到此咱们就准备结束了,今天咱们的主要任务就是实现一个自己的Vue实例,即如下:
{{count}}
const app = new MyVue({
el: '#appMyVue',
data: {
count: 0
}
})
setInterval(() => {
app.count++
})
我们首先分析下原理:
-
new Vue()
首先执行初始化,对data
执行响应化处理,这个过程发生在Observe
中 - 同时对模板执行编译,找到其中动态绑定的数据,从
data
中获取并初始化试图,这个过程发生在Compile
中 - 由于
data
的某个key
在一个视图中可能出现多次,多以每个key
都需要一个Dep
来管理多个Watcher
- 将来
data
中数据一旦发生变化,会首先找到对应的Dep
,通知所有Watcher
执行更新函数
# 实现 MyVue 类
class MyVue {
constructor(options) {
this.$options = options;
this.$data = options.data;
// data 响应式处理
observe(this.data);
}
}
为了更贴近源码,我们更改下observe
方法,如下
function observe(obj ) {
if (typeof obj !== 'object' || obj === null) return;
new Observer(obj )
}
创建Observer
观测类
// 根据传入的value的类型做形影的响应式处理
class Observer {
constructor(value) {
this.value = value;
if (Array.isArray(value)) {
// to do
} else {
this.walk(this.value)
}
};
// 对象的响应式处理
walk(obj) {
// 遍历obj的key,做响应式处理
Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]))
}
}
为了方便观察,我们输出两个命令,如下:
// 数据响应式
function defineReactive(obj, key, val) {
// 递归
observe(val);
// 响应式处理
Object.defineProperty(obj, key, {
get() {
console.log('get', key, val);
return val;
},
set(newVal) {
if (newVal !== val) {
console.log('set', key, val);
// 保证如果newVal是对象,再次做响应式处理
observe(val);
val = newVal;
}
}
})
}
运行我们的代码,如下:
const app = new MyVue({
el: '#appMyVue',
data: {
count: 0
}
});
setInterval(() => {
app.count++
}, 1000)
问题:可以发现并没有如期去输出我们打印的那两条,即没有走到
defineReactive
方法里的Object.defineProperty
中的get
和set
方法,这是为什么呢?
原因就是:我们把数据放在了MyVue
类中的$data
上了,并没有放在实例上,换成如下访问方式,再试试看:
const app = new MyVue({
el: '#appMyVue',
data: {
count: 0
}
});
setInterval(() => {
app.$data.count++
}, 1000)
这种方式虽然能实现,但如何能像Vue
一样可直接在实例上访问data
呢,即app.count
的形式?
这里就需要通过代理
的方式把数据代理
到实例上,如下:
class MyVue {
constructor(options) {
this.$options = options;
this.$data = options.data;
// data 响应式处理
observe(this.$data);
// 数据代理
proxy(this);
}
}
// 将数据代理到实例上
function proxy(vm) {
Object.keys(vm.$data).forEach(key => {
Object.defineProperty(vm, key, {
get() {
return vm.$data[key];
},
set(val) {
vm.$data[key] = val
}
})
})
}
运行代码测试下
const app = new MyVue({
el: '#appMyVue',
data: {
count: 0
}
});
setInterval(() => {
app.count++
}, 1000)
可以发现,正常输出没有问题 ~~~
# 实现 Comlile
我们已经实现了数据的响应式,接下来就是怎么把数据显示在页面上,即完成编译,这时我们需要实现一个新的类:Complie
// 解析模板
class Compile {
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el);
if (this.$el) {
this.compile(this.$el)
}
}
compile(el) {
// 遍历el的所有子节点,判断他们的类型做相应的处理
const childNodes = el.childNodes;
const reg = /\{\{(.*)\}\}/;
childNodes.forEach(node => {
// 如果子元素是 元素
if (node.nodeType === 1) {
}
// 如果子元素是 文本
else if (this.isInter(node, reg)) {
this.compileText(node, reg)
}
// 递归遍历
if (node.childNodes) {
this.compile(node, reg)
}
})
};
// 编译模板
compileText(node, reg) {
const regMap = reg.exec(node.textContent);
node.textContent = this.$vm[regMap[1]];
};
// 判断是否为插值表达式
isInter(node, reg) {
return node.nodeType === 3 && reg.test(node.textContent);
}
}
测试我们的代码
{{count}}
{{count}}
{{count}}
{{count}}
如我们所想,页面已经渲染了初始值,如下:
实现 my-text 和 my-html 指令
compile(el) {
// 遍历el的所有子节点,判断他们的类型做相应的处理
const childNodes = el.childNodes;
const reg = /\{\{(.*)\}\}/;
childNodes.forEach(node => {
// 如果子元素是 元素
if (node.nodeType === 1) {
// 获得元素的所有指令
const attrs = node.attributes;
// 遍历所有属性,拿到我们想要的指令
Array.from(attrs).forEach(attr => {
// 属性名称
const attrName = attr.name;
// 属性值
const exp = attr.value;
// 判断得到我们的指令
if (attrName.startsWith('my-')) {
// 得到指令对应的函数
const dir = attrName.substring(3);
// 如果函数存在,则执行
this[dir] && this[dir](node, exp)
}
})
}
// 如果子元素是 文本
else if (this.isInter(node, reg)) {
this.compileText(node, reg)
}
// 递归遍历
if (node.childNodes) {
this.compile(node, reg)
}
})
};
// 指令 my-text 处理函数
text(node, exp) {
node.textContent = this.$vm[exp];
};
// 指令 myhtml 处理函数
html(node, exp) {
node.innerHTML = this.$vm[exp];
};
问题是:虽然我们实现了模板编译和部分指令,但在定时器里更新
count
的时候,页面没有实时更新?
解决这个问题也很简单,就是接下来要实现依赖收集Watcherd
# 实现 Watcher
实现思路:
-
defineReactive
时为每一个key创建一个Dep
实例 - 初始化视图时读取某个
key
并创建对应的Watcher
- 由于触发某个
key
对应的getter
方法,边将对应的Watcher
添加到key
对应的Dep
中 - 当
key
更新,setter
触发时,便可以通过对应的Dep
通知管理所有的Watcher
进行更新
# 实现 Watcher 与 Dep
这里首先得先改造一下Compile
中的指令函数,增加一个update
统一的处理函数,如下:
// 解析模板
class Compile {
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el);
if (this.$el) {
this.compile(this.$el)
}
};
compile(el) {
// 遍历el的所有子节点,判断他们的类型做相应的处理
const childNodes = el.childNodes;
const reg = /\{\{(.*)\}\}/;
childNodes.forEach(node => {
// 如果子元素是 元素
if (node.nodeType === 1) {
// 获得元素的所有指令
const attrs = node.attributes;
// 遍历所有属性,拿到我们想要的指令
Array.from(attrs).forEach(attr => {
// 属性名称
const attrName = attr.name;
// 属性值
const exp = attr.value;
// 判断得到我们的指令
if (attrName.startsWith('my-')) {
// 得到指令对应的函数
const dir = attrName.substring(3);
// 如果函数存在,则执行
this[dir] && this[dir](node, exp)
}
})
}
// 如果子元素是 文本
else if (this.isInter(node, reg)) {
this.compileText(node, reg)
}
// 递归遍历
if (node.childNodes) {
this.compile(node, reg)
}
})
};
// 统一的处理函数
update(node, exp, dir) {
// 1. 初始化
const fn = this[dir + 'Updater'];
fn && fn(node, this.$vm[exp]);
// 更新
}
// 指令 my-text 处理函数
text(node, exp) {
this.update(node, exp, 'text')
};
textUpdater(node, value) {
node.textContent = value;
};
// 指令 myhtml 处理函数
html(node, exp) {
this.update(node, exp, 'html')
};
htmlUpdater(node, value) {
node.innerHTML = value;
};
// 编译模板
compileText(node, reg) {
const regMap = reg.exec(node.textContent);
this.update(node, regMap[1], 'text')
};
// 判断是否为插值表达式
isInter(node, reg) {
return node.nodeType === 3 && reg.test(node.textContent);
}
}
测试代码,我们改造完成后页面依然渲染没有问题。
接下来我们要在update
函数里,做统一的依赖收集操作,如下:
// 统一的处理函数
update(node, exp, dir) {
// 1. 初始化
const fn = this[dir + 'Updater'];
fn && fn(node, this.$vm[exp]);
// 更新
new Watcher(this.$vm, exp, function(val) {
fn && fn(node, val)
})
}
创建Watcher
类
// 监听器:负责依赖更新
class Watcher {
constructor(vm, key, updateFn) {
this.vm = vm;
this.key = key;
this.updateFn = updateFn;
};
// 未来被 Dep 调用
update() {
// 执行实际的更新操作
this.updateFn.call(this.vm, this.vm[this.key])
}
}
问题:现在还没有依赖做关联 ???
创建依赖关联就需要大管家Dep
了,如下
class Dep {
constructor() {
this.deps = [];
}
addDep(dep) {
this.deps.push(dep)
}
notify() {
this.deps.forEach(dep => dep.update());
}
}
想要每个key
和对应的Watcher
对应起来,可以在defineReactive
函数里做处理。这时还要在Watcher
里做一次手动触发,以便收集依赖,如下:
// 监听器:负责依赖更新
class Watcher {
constructor(vm, key, updateFn) {
this.vm = vm;
this.key = key;
this.updateFn = updateFn;
// 触发依赖收集
Dep.target = this;
this.vm[this.key]
Dep.target = null;
};
// 未来被 Dep 调用
update() {
// 执行实际的更新操作
this.updateFn.call(this.vm, this.vm[this.key])
}
}
在defineReactive
函数创建依赖收集的关系, 如下:
// 数据响应式
function defineReactive(obj, key, val) {
// 递归
observe(val);
// 创建Dep实例
const dep = new Dep();
// 响应式处理
Object.defineProperty(obj, key, {
get() {
console.log('get', key, val);
// 依赖收集
Dep.target && dep.addDep(Dep.target);
return val;
},
set(newVal) {
if (newVal !== val) {
console.log('set', key, val);
// 保证如果newVal是对象,再次做响应式处理
observe(val);
val = newVal;
// 更新
dep.notify()
}
}
})
}
这时再去测试我们的代码,发现已经可以实时更新了,是不是很神奇
今天我们只是实现了简版的Vue
的数据响应式、依赖收集及编译模板,希望对大家的理解Vue
能有所帮助,欢迎评论留言 ~~~