简易版vue实现及源码(数据响应式及常用指令实现)
1. 简易vue工作机制分析
简易版vue重点实现以下两个功能:
- 数据响应式:改变data等中的数据,页面重新进行渲染;页面中的数据改变data中的数据同步发生改变
- 模板解析:提取模板中的插值表达式和指令,按照特定的语法进行解析,解析完成后插入el中,DOM进行重绘
实现分4个部分:Watcher类,Dep类,数据劫持,模板解析
本案例new Vue()初始化时主要做了3件事,保存options(vue配置)和data,进行数据拦截,创建compile实例
实际 new Vue() 时会调⽤用_init()进⾏行行初始化,会初始化各种实例例⽅方法、全局⽅方法、执⾏行行⼀一些⽣生命周期、初始化props、data等状态。其中最重要的是data的「响应化」处理。
- Watcher类:模板解析时,所有的插值表达式或指令都会创建一个watcher实例,每个watchers实例存放了其vue实例,插值表达式/指令,相应的模板替换方法
- Dep类:在每个data中的key开始进行数据劫持时,会创建一个dep实例,每个dep实例存放和当前key相同的watcher实例
- 数据拦截:getter时通知dep实例执行addDep方法,保存相同key值的watchers实例,setter时通知dep实例执行notify方法(重新更新DOM)
- 模板解析:使⽤用正则解析template中的vue的指令(v-xxx) 变量量等等
实际Vue编译模块分为三个阶段
- parse:使⽤用正则解析template中的vue的指令(v-xxx) 变量量等等 形成抽象语法树AST
- optimize:标记⼀些静态节点,⽤用作后⾯面的性能优化,在diff的时候直接略略过
- generate:把第⼀部生成的AST 转化为渲染函数 render function
Virtual DOM 是react⾸首创,Vue2开始⽀支持,就是用 JavaScript 对象来描述dom结构,数据修改的时候,我们先修改虚拟dom中的数据,然后数组做diff,最后再汇总所有的diff,力求做最少的dom操作,毕竟js里对比很快,而真实的dom操作太慢
2.源码实现
1.数据响应式
本文中,重点实现data的「响应化」处理
细节:怎么将data挂载到vue实例上(源码中找答案)
property/Dep/Watcher对应关系图
class KVue {
constructor(options) {
//保存vue配置
this.$options = options;
//保存data
this.$data = options.data;
//响应化处理
this.observe(this.$data);
new Compile(options.el, this);
//钩子函数
if (options.created) {
options.created.call(this);
}
}
observe(obj) {
if (!obj || typeof obj !== 'object') {
return
}
//观察data中每一个数据
for (const key in obj) {
this.defineReactive(obj, key, obj[key]);
this.proxyData(key);
}
}
defineReactive(obj, key, val) { //数据劫持,完成响应式
//深层次遍历
this.observe(key);
let _val = val;
//为data中的每个key创建dep实例
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
// 依赖收集
Dep.target && dep.addDep(Dep.target);
return _val
},
set(newVal) {
if (newVal !== val) {
_val = newVal;
dep.notify();
}
}
})
}
// 在vue根上定义属性代理data中的数据
proxyData(key) {
// this指的KVue实例
Object.defineProperty(this, key, {
get() {
return this.$data[key];
},
set(newVal) {
this.$data[key] = newVal;
}
});
}
}
class Dep { //建立依赖类,
constructor() {
this.watchers = []
}
addDep(watcher) {
this.watchers.push(watcher)
}
notify() {
this.watchers.forEach(watcher => watcher.update())
}
}
class Watcher { // 创建Watcher:保存data中数值和页面中的挂钩关系
constructor(vm, key, cb) {
// 创建实例时立刻将该实例指向Dep.target便于依赖收集
this.vm = vm;
this.key = key;
this.cb = cb;
//用来存放解析到插值或者指令及对应的vm实例
Dep.target = this;
this.vm[this.key];//触发依赖收集
Dep.target = null;
}
// 更新
update() {
// console.log(this.key + "更新了!");
this.cb.call(this.vm, this.vm[this.key])
}
}
2.模板解析
编译过程图
//遍历dom结构,解析插值表达式或者指令
class Compile {
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el);
// 把模板中的内容移到片段操作
this.$fragment = this.node2Fragment(this.$el);
// 执行编译
this.compile(this.$fragment);
// 放回$el中
this.$el.appendChild(this.$fragment);
}
node2Fragment(el) {
// 创建元素片段
const fragment = document.createDocumentFragment();
let child;
while ((child = el.firstChild)) {
fragment.appendChild(child);
}
return fragment;
}
compile(el) {
const childNodes = el.childNodes;
Array.from(childNodes).forEach(node => {
if (node.nodeType == 1) { //表示元素节点
console.log(`编译${node.nodeName}元素:`)
// // console.log(node)
//只编译v-xxx
this.compileElement(node);
} else if (this.isInter(node)) { //是否为插值表达式
// //console.log(node)
//编译{{}}
this.compileText(node)
}
// 递归子节点
if (node.children && node.childNodes.length > 0) {
this.compile(node);
}
})
}
isInter(node) {
return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
compileText(node) { //文本替换
// 取出插值表达式内中的表达式
const exp = RegExp.$1;
this.update(node, exp, 'text')
}
update(node, exp, type) {
const updator = this[type + "Updator"];
updator && updator(node, this.$vm[exp]); // 首次初始化
// 创建Watcher实例,收集依赖
new Watcher(this.$vm, exp, function (val) {
updator && updator(node, val);
})
}
textUpdator(node, val) {
console.log(node, val)
//将插值表达式变为vm.$data的值
node.textContent = val;
}
compileElement(node) {
//只关心元素属性
const nodeAttrs = node.attributes;
console.log(node,nodeAttrs)
Array.from(nodeAttrs).forEach(attr => {
//k-xxx="aaa"
const attrName = attr.name; //取出指令k-xxx
const exp = attr.value; //取出指令值aaa
console.log(attrName,exp)
if (attrName.indexOf("k-") == 0) {
// 指令
const type = attrName.substring(2); //xxx
// 执行
this[type] && this[type](node, exp);
} else if (attrName.indexOf('@') == 0) {
//事件类型
const type = attrName.substring(1);
console.log(type);
//执行
this.handleEvent(node, this.$vm, exp, type)
}
})
}
handleEvent(node, vm, exp, type) {
const fn = vm.$options.methods && vm.$options.methods[exp]
if (type && fn) {
node.addEventListener(type, fn.bind(vm))
}
}
text(node, exp) { //k-text
console.log(node, exp, 'k-text')
this.update(node, exp, 'text')
}
html(node, exp) { //k-html
this.update(node, exp, "html");
console.log(node, exp, this.$vm[exp], 'k-html')
}
htmlUpdator(node, val) {
node.innerHTML = val;
}
model(node, exp) {//k-model
this.update(node, exp, 'model')
node.addEventListener('input', e => {
this.$vm[exp] = e.target.value;
})
}
modelUpdator(node, val) {
node.value = val;
}
}
测试代码
<body>
<div id="app">
{{name}}
<p>{{name}}</p>
<p k-text="name"></p>
<p>{{age}}</p>
<input type="text" k-model="name">
<button @click="changeName">点我</button>
<div k-html="html"></div>
</div>
<script src="./kvue.js"></script>
<script src="./compile.js"></script>
<script>
const kvm = new KVue({
el: '#app',
data: {
name: "I am test.",
age: 12,
html: '<button>这是一个按钮</button>'
},
created() {
console.log('开始啦')
setTimeout(() => {
this.name = '我是测试'
}, 1500)
},
methods: {
changeName() {
this.name = '哈喽,我来了'
this.age = 1
}
}
})
</script>
</body>
效果图:
结果 | created执行结果 | 点击事件 | v-model |
---|---|---|---|
![]() | ![]() | ![]() | ![]() |
码云地址:
https://gitee.com/huang_canmin/VueExercise/tree/origin/02.%E6%BA%90%E7%A0%81/Vue%E5%AE%9E%E7%8E%B0