实现vue中的数据驱动视图变化
自己实现一套简易框架来完成和vue相似的功能
jvue.js
// new JVue ({data:{...}})
class JVue {
constructor(options) {
this.$options = options;
// 数据响应化
this.$data = options.data;
this.observe(this.$data);
// // 模拟watcher创建
// new Watcher();
// this.$data.name;
// new Watcher();
// this.$data.foo.bar;
new Compile(options.el, this);
// created 执行
if (options.created) {
options.created.call(this);
}
}
observe (val) {
if (!val || typeof val !== 'object') {
return;
}
Object.keys(val).forEach(key => {
// 定义响应式
this.defineReactive(val, key, val[key]);
// 代理data中的属性到vue实例上
this.proxyData(key);
})
}
// 数据响应化
defineReactive (dataObj, key, val) {
this.observe(val); // 递归解决数据嵌套
const dep = new Dep();
Object.defineProperty(dataObj, key, {
get () {
Dep.target && dep.addDep(Dep.target);
return val;
},
set (newVal) {
if (newVal === val) {
return;
}
val = newVal;
// console.log(`${key}值发生了变化,该值所在地方需要发生更新:${val}`);
dep.notify();
}
})
}
proxyData (key) {
Object.defineProperty(this, key, {
get () {
return this.$data[key];
},
set (newVal) {
this.$data[key] = newVal;
}
})
}
}
// Dep 用来管理watcher
class Dep {
constructor() {
// 这里存放若干依赖(watcher),一个watcher对应一个属性
this.deps = [];
}
addDep (dep) {
this.deps.push(dep);
}
notify () {
this.deps.forEach(dep => dep.update());
}
}
// Watcher
class Watcher {
constructor(vm, key, cb) {
this.vm = vm;
this.key = key;
this.cb = cb;
// 将当前watcher实例指定到Dep静态属性 target
Dep.target = this;
this.vm[this.key]; // 出发getter,添加依赖
Dep.target = null;
}
update () {
// console.log('属性更新了');
this.cb.call(this.vm, this.vm[this.key]);
}
}
compile.js 编译
// 用法 new Compile(el,vm)
class Compile {
constructor(el, vm) {
// 要遍历的数组节点
this.$el = document.querySelector(el);
this.$vm = vm;
// 编译
if (this.$el) {
// 转换内部为片段Fragment
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;
// console.log(childNodes);
Array.from(childNodes).forEach(node => {
// 类型判断
if (this.isElement(node)) {
// 元素
// console.log('编译元素' + node.nodeName);
// 查找 j- , @ , :开头的
const nodeAttrs = node.attributes;
Array.from(nodeAttrs).forEach(attr => {
const attrName = attr.name; //属性名
const exp = attr.value; //属性值
// 判断是指令还是事件
if (this.isDirective(attrName)) {
// j-text
const dir = attrName.substring(2);
// 执行指令
this[dir] && this[dir](node, this.$vm, exp);
}
if (this.isEvent(attrName)) {
const dir = attrName.substring(1); // @click
this.eventHandle(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);
}
})
}
compileText (node) {
// 走到这说明是一个插值表达式
// console.log(RegExp.$1);
this.update(node, this.$vm, RegExp.$1, 'text');
}
// 更新函数
update (node, vm, exp, dir) {
const updaterFn = this[dir + 'Updater'];
// 初始化
updaterFn && updaterFn(node, vm[exp]);
// 依赖收集
new Watcher(vm, exp, function (value) {
updaterFn && updaterFn(node, value);
})
}
text (node, vm, exp) {
this.update(node, vm, exp, 'text');
}
// 双向数据绑定
model (node, vm, exp) {
// 要指定input 的value属性
this.update(node, vm, exp, "model");
// 视图对模型的响应
node.addEventListener("input", e => {
vm[exp] = e.target.value;
})
}
modelUpdater (node, value) {
node.value = value;
}
html (node, vm, exp) {
this.update(node, vm, exp, "html");
}
htmlUpdater (node, value) {
node.innerHTML = value;
}
textUpdater (node, value) {
node.textContent = value;
}
// 事件处理器
eventHandle (node, vm, exp, dir) {
// @click="onclick"
let fn = vm.$options.methods && vm.$options.methods[exp];
if (dir && fn) {
node.addEventListener(dir, fn.bind(vm));
}
}
isDirective (attrName) {
return attrName.indexOf('j-') === 0;
}
isEvent (attrName) {
return attrName.indexOf('@') === 0;
}
isElement (node) {
return node.nodeType === 1;
}
// 插值文本
isInterpolation (node) {
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
}
/**
* 彩蛋 1.vue编译过程是怎么样的 2.双向数据绑定的原理是什么
*
* 1.3w1h原则
* 先回答什么是编译,vue写的这些模板,我们的html根本不识别,我们通过编译的过程可以进行依赖收集,进行依赖收集后
* 我们就将我们data中的数据模型和视图之间产生了绑定关系,产生了依赖关系,以后如果模型发生变化的时候,我们就可以通知这些
* 依赖的地方,让他们进行更新,这就是我们施行编译的目的。 我们把这些依赖全部编译以后,更新操作,我们就可以做到模型驱动
* 视图的变化,这就是编译过程,这就是他的作用
*
* 2.编译的时候解析出v-model ,就和其他的指令一样。之后做操作一共又两件事情
* 1.把v-model这个元素上增加事件监听,将指定的事件的回调函数作为input事件的回调函数
* 2.去监听,如果input发生变化的时候,我们就可以把最新的值设置到vue实例上,因为vue实例上,
* 因为vue实例已经实现了数据响应化,他响应化的set函数会触发界面所有依赖的更新,所以界面跟这个数据相关的视图就更新了
*/
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<!-- 插值绑定 -->
<p>{{name}}</p>
<!-- 指令解析 -->
<p j-text="name"></p>
<p>{{age}}</p>
<p>
{{doubleAge}}
</p>
<!-- 双向绑定 -->
<input type="text" j-model="name" />
<!-- 事件处理 -->
<button @click="changeName">呵呵</button>
<!-- html 内容解析 -->
<div j-html="html"></div>
</div>
<script src="./compile.js"></script>
<script src="./jvue.js"></script>
<script>
const app = new JVue({
el: '#app',
data: {
name: 'I am Jerry',
age: 18,
html: "<button>这是一个按钮</button>",
doubleAge: 36
},
created() {
console.log('开始啦');
setTimeout(() => {
this.name = "我是测试"
}, 1500)
},
methods: {
changeName() {
this.name = "哈喽,codeWang";
this.age = 25;
}
}
})
</script>
</body>
</html>