目的
实现类似vue的简版vue包含(数据响应化,双向绑定,指令解析,事件绑定,编译器实现)
思路
首先,先看一下vue的工作机制,如下图所示。
初始化Vue实例。Observer劫持监听所有属性并且遍历每个属性的getter,setter。Compile编译模板解析模板中的指令(m-text,m-html,m-model等),插值文本{{key}},@click事件等。然后通过创建Watcher 收集依赖 (添加订阅者)并且 通过update更新视图。最后当页面中数据发生改。会触发属性的setter 。然后通知订阅者的更新函数实现试图更新。
其次我们知道Vue.js 的核心是一个允许采用简洁的模板语法来声明式地将数据渲染进 DOM的系统,
所以我们重点围绕这一特点展开来实现相应的功能
<div id="app">
{{ message }}
</div>
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
})
具体实现
1.创建类MyVue
通过Object.defineProperty()实现依赖收集,通知更新。
首先,封装MyVue类,类的用法 new MyVue({ data:{} })。
在constructor中接收一个options参数。
然后通过observe() 方法遍历实例中的data属性,
注意,这里会有个错误判断 data属性必须存在并且data值是一个对象。
否则:提示console.error(‘data not is Object’);
通过defineReactive方法遍历data每一个属性
并且将data对象代理到vue实例。方便在实例中用this访问。
defineReactive方法内部是 Object.defineProperty(obj, key, { })
在get()方法中对每一个属性进行依赖收集。
Dep.target && dep.addDep(Dep.target)
并且返回属性值。
在set()方法中监听属性值得变化。并且通知更新。
class MyVue {
constructor(options) {
// 缓存options
this.$options = options;
// 数据响应化
this.$data = options.data;
// 观察监听$data
this.observe(this.$data);
// 模拟watcher
// new Watcher();
// this.$data.test;
// new Watcher();
// this.$data.foo.bar;
// console.log(options.el);
new Compile(options.el, this);
//判断created
if (options.created) {
// 执行created,并且将this指向created
options.created.call(this);
}
}
observe(val) {
// console.log(val)
// 判断val是否存在 已经类型是否是Object
if (!val || typeof val !== 'object') {
return console.error('data not is Object');
}
// 遍历对象
Object.keys(val).forEach(key => {
this.defineReactive(val, key, val[key]);
// 代理data中的属性到vue实例
this.proxyData(key);
});
}
// 数据响应化
defineReactive(obj, key, val) {
// 判断对象指为对象时,进行递归操作
if (typeof val === 'object') {
this.observe(val);
}
// this.observe(val);
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
Dep.target && dep.addDep(Dep.target)
return val;
},
set(newValue) {
if (newValue === val) {
return;
}
val = newValue;
// console.log(`${key}属性发生改变:${val}`)
dep.notify();
}
})
}
proxyData(key) {
Object.defineProperty(this, key, {
get() {
// 将当前实例中data属性值代理到实例下面直接访问
return this.$data[key];
},
set(newVal) {
// 通过改变实例中 的值来更新data中的值
this.$data[key] = newVal;
}
})
}
}
2.创建Dep类(订阅者)
Dep类用来将页面模板中的数据进行依赖收集。
// dep:用来管理watcher 依赖收集
class Dep {
constructor() {
// 存放若干依赖
this.deps = [];
}
// 添加依赖
addDep(dep) {
this.deps.push(dep)
}
// 通知依赖更新
notify() {
this.deps.forEach(dep => dep.update())
}
}
3.创建Watcher类(观察者)
在创建依赖的时候将当前依赖的实例指定到Dep的静态属性target 。以此来实现 在属性getter时 依赖收集的目的。
//watcher 观察者
class Watcher {
// console.log(this);
constructor(vm, key, cb) {
this.vm = vm;
this.key = key;
this.cb = cb;
// 将当前watcher实例指定到Dep静态属性target
Dep.target = this;
// 出发相对应的getter
this.vm[this.key];
Dep.target = null;
// console.log(this);
}
// 依赖更新函数
update() {
console.log('属性更新了')
// callback
this.cb.call(this.vm, this.vm[this.key]);
}
}
4.编译器实现,创建类Compile
1.我们为什么编译?
因为我们的vue模板里面的插值绑定,指令,事件等浏览器是不能直接识别的。所以我们需要将模板编译成浏览器可识别的dom。
2.编译流程
将dom节点通过node2Fragment()方法转化为虚拟dom,然后执行编译。
我们之所以选择将dom 转化为虚拟dom 。
是因为文档片段存在于内存中,并不在DOM树中,所以将子元素插入到文档片段时不会引起页面回流(reflow)(对元素位置和几何上的计算)。
因此,使用文档片段documentfragments 通常会起到优化性能的作用(better performance)。
执行编译时先foreach 元素所有子节点。
然后依次判断节点类型。
当节点有子节点就递归。
然后依据元素类型进行不同的编译。
在编译的过程中要通过update函数创建Watcher进行依赖收集。
最后将编译好的文档appendChild到dom中
// 编译模板 用法 new Compile(el, vm)
//
class Compile {
constructor(el, vm) {
// 需要遍历宿主节点
this.$el = document.querySelector(el);
this.$vm = vm;
//编译
console.log(this.$el)
if (this.$el) {
// 转化内容为片段
this.$fragment = this.node2Fragment(this.$el);
// 执行编译
this.compile(this.$fragment);
//将编译完的HTML结果追加至$el
this.$el.appendChild(this.$fragment);
}
}
// 将宿主元素中代码片段拿出来遍历。
node2Fragment(el) {
const frag = document.createDocumentFragment();
// 将el中所以子元素搬家到frag
let child;
while (child = el.firstChild) {
frag.appendChild(child)
}
return frag;
}
// 编译过程
compile(el) {
const childNodes = el.childNodes;
Array.from(childNodes).forEach(node => {
// node(DOM节点)
// 类型判断
if (this.isElement(node)) {
// 元素
console.log('编译元素' + node.nodeName)
// 查找m- @指令和事件,
const nodeAttrs = node.attributes;
// 拿出元素所有的元素
Array.from(nodeAttrs).forEach(attr => {
// console.log(attr)
const attrName = attr.name; //属性名
const exp = attr.value; //属性值
// 指令
if (this.isDirective(attrName)) {
// k-text
// console.log(attrName)
const dir = attrName.substring(2);
// 执行指令
this[dir] && this[dir](node, this.$vm, exp);
}
// 事件
if (this.isEvent(attrName)) {
const dir = attrName.substring(1);
// 执行事件
this.eventHandler(node, this.$vm, exp, dir);
}
})
} else if (this.isInterpolation(node)) {
//文本
console.log('编译文本' + node.textContent)
// 编译插值文本
this.compileText(node);
}
// 递归子节点
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node);
}
})
}
//事件处理器
eventHandler(node, vm, exp, dir) {
// 判断实例methods是否存在,并且methods 中事件方法是否存在
let fn = vm.$options.methods && vm.$options.methods[exp]
if (dir && fn) {
//dom 监听事件,并且执行实例中methods 方法。将this指向方法
node.addEventListener(dir, fn.bind(vm));
}
}
// 更新函数
update(node, vm, exp, dir) {
// 从当前类组合一个更新方法
const updaterFn = this[dir + 'Updater']
// 初始化
updaterFn && updaterFn(node, vm[exp]) && vm[exp];
// 依赖收集
new Watcher(vm, exp, function (val) {
updaterFn && updaterFn(node, val);
})
}
// 更新文本
textUpdater(node, val) {
// 将值传给dom节点
node.textContent = val;
}
//
modelUpdater(node, val) {
// 将值传给dom节点
node.value = val;
}
htmlUpdater(node, val) {
// 将值传给dom节点
node.innerHTML = val;
}
//m-text 指令
text(node, vm, exp) {
// console.log('text--', node, vm, exp)
this.update(node, vm, exp, 'text');
}
html(node, vm, exp) {
// console.log('text--', node, vm, exp)
this.update(node, vm, exp, 'html');
}
//m-model 指令
model(node, vm, exp) {
// 指定input的value属性
this.update(node, vm, exp, 'model')
// 视图对模型的响应
node.addEventListener('input', e => {
vm[exp] = e.target.value;
})
}
isElement(node) {
return node.nodeType == 1;
}
// 判断插值文本
isInterpolation(node) {
return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
// 编译文本
compileText(node) {
this.update(node, this.$vm, RegExp.$1, 'text');
}
// 判断属性是否为指令
isDirective(attr) {
return attr.indexOf('m-') == 0;
}
// 判断是否为事件
isEvent(attr) {
return attr.indexOf('@') == 0;
}
}