Implementation of a simplified version of Vue
Implementation of MVVM
The three elements of the MVVM framework: data responsiveness, template engine and its rendering
Data responsiveness: monitor data changes and update them in the view
- Object.defineProperty(), implementation in Vue2
- Proxy, the implementation in Vue3
Template engine: Provides a template syntax for describing views
- Interpolation: { {}}
- 指令:v-bind, v-on, v-model, v-for, v-if
Rendering: how to convert templates to html
- template => vdom => dom
Code
Create vue_simple project
vue create vue_simple
project directory
Among them, hvue.html is the code for our test effect, and hvue.js is the code of the simplified version of Vue that I want to write.
writing process
Test code hvue.html
<!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>简版vue的实现</title>
</head>
<body>
<div id="app">
<p>{
{
counter}}
<span>{
{
sum}}</span>
</p>
</div>
</body>
<script src="../node_modules/vue/dist/vue.js"></script>
<script>
const app = new Vue({
el:'#app',
data:{
counter:1,
sum:1
}
})
</script>
</html>
Here you can see that vue is still used at this time, and we will refer to kvue.js we wrote next. The current page effect is as follows:
Clearly implement the process of hVue.js
- To achieve data responsiveness
- To implement a template engine, various interpolation and instructions of the template can be replaced with data and rendered
- After the data is updated, the template is automatically updated
data responsive
hvue.js implements the data responsive part
class Hvue{
constructor(options){
// 先保存相关参数
this.$options = options;
this.$data = options.data;
// 代理data,直接用this.xxx访问数据
proxy(this);
//1、实现数据响应式 遍历data
observe(options.data)
//2、编译
}
}
//遍历data 使其成为响应式数据
function observe(data){
// 判断类型
if(typeof data !== 'object' || data === null) return data;
Observe(data)
}
class Observe{
constructor(obj){
// 数组类型要进行特殊处理
if(Array.isArray(obj)){
}else{
this.walk(obj);
}
}
walk(obj){
Object.keys(obj).forEach((key)=>defineReactive(obj,key,obj[key]));
}
}
// 给 object 定义一个响应式属性
function defineReactive(obj,key,val){
//对象嵌套处理
//如果val是对象
observe(val)
Object.defineProperty(obj,key,{
get(){
return val;
},
set(newValue){
val = newValue;
return val;
}
})
}
//将所有的data代理到vue上,实现在代码中 this.xxx和this.data.xxx同样的效果
function proxy(vm){
Object.keys(vm.$data).forEach((key)=>{
Object.defineProperty(vm.$data,key,{
get(){
return vm.$data[key];
},
set(newValue){
vm.$data[key] = newValue;
return vm.$data[key];
}
})
})
}
Now although all the data has been converted into responsive, the page is still not bound to the data, because we still need to do template processing
template compilation
Now the template compilation is implemented. After this implementation is completed, we can initially check the page effect.
class Hvue{
constructor(options){
// 先保存相关参数
this.$options = options;
this.$data = options.data;
// 代理data,直接用this.xxx访问数据
proxy(this);
//1、实现数据响应式 遍历data
observe(options.data)
//2、编译 遍历节点
new Compile(options.el,this)
}
}
//遍历data 使其成为响应式数据
function observe(data){
// 判断类型
if(typeof data !== 'object' || data === null) return data;
new Observe(data)
}
class Observe{
constructor(obj){
// 数组类型要进行特殊处理
if(Array.isArray(obj)){
}else{
this.walk(obj);
}
}
walk(obj){
Object.keys(obj).forEach((key)=>defineReactive(obj,key,obj[key]));
}
}
// 给 object 定义一个响应式属性
function defineReactive(obj,key,val){
//对象嵌套处理
//如果val是对象
observe(val)
Object.defineProperty(obj,key,{
get(){
return val;
},
set(newValue){
val = newValue;
}
})
}
//将所有的data代理到vue上,实现在代码中 this.xxx和this.data.xxx同样的效果
function proxy(vm){
Object.keys(vm.$data).forEach((key)=>{
Object.defineProperty(vm,key,{
get(){
return vm.$data[key];
},
set(newValue){
vm.$data[key] = newValue;
}
})
})
}
// 模版引擎编译
class Compile{
constructor(el,vm){
this.$vm = vm;
this.$el = document.querySelector(el);
if(this.$el){
this.compile(this.$el);
}
}
compile(node){
const childNodes = node.childNodes;
Array.from(childNodes).forEach(n=>{
// 判断节点类型
//是元素节点
if(this.isElement(n)){
//继续遍历节点
if (n.childNodes.length > 0) {
this.compile(n);
}
//字符串 插值表达式
}else if (this.isInter(n)) {
n.textContent = this.$vm[RegExp.$1];
}
})
}
isElement(n) {
return n.nodeType === 1;
}
isInter(n){
console.log(n.textContent)
return n.nodeType === 3 && /\{\{(.*)\}\}/.test(n.textContent)
}
}
Now you can see the page, and now only interpolation processing is implemented. Next, we will improve the code and implement command processing. Now we add the relevant code of the instruction in the hvue.html file.
<!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>简版vue的实现</title>
</head>
<body>
<div id="app">
<p>{
{
counter}}</p>
<p h-text="counter"></p> //指令
<p h-html="htmlText"></p> //指令
</div>
</body>
<!-- <script src="../node_modules/vue/dist/vue.js"></script> -->
<script src="./hvue.js"></script>
<script>
const app = new Hvue({
el:'#app',
data:{
counter:1,
htmlText:'<span style="color:red">我成功啦</span>'
}
})
</script>
</html>
The page at this time does not display accurate data, and we will continue to deal with it below.
class Hvue{
constructor(options){
// 先保存相关参数
this.$options = options;
this.$data = options.data;
// 代理data,直接用this.xxx访问数据
proxy(this);
//1、实现数据响应式 遍历data
observe(options.data)
//2、编译 遍历节点
new Compile(options.el,this)
}
}
//遍历data 使其成为响应式数据
function observe(data){
// 判断类型
if(typeof data !== 'object' || data === null) return data;
new Observe(data)
}
class Observe{
constructor(obj){
// 数组类型要进行特殊处理
if(Array.isArray(obj)){
}else{
this.walk(obj);
}
}
walk(obj){
Object.keys(obj).forEach((key)=>defineReactive(obj,key,obj[key]));
}
}
// 给 object 定义一个响应式属性
function defineReactive(obj,key,val){
//对象嵌套处理
//如果val是对象
observe(val)
Object.defineProperty(obj,key,{
get(){
return val;
},
set(newValue){
val = newValue;
}
})
}
//将所有的data代理到vue上,实现在代码中 this.xxx和this.data.xxx同样的效果
function proxy(vm){
Object.keys(vm.$data).forEach((key)=>{
Object.defineProperty(vm,key,{
get(){
return vm.$data[key];
},
set(newValue){
vm.$data[key] = newValue;
}
})
})
}
// 模版引擎编译
class Compile{
constructor(el,vm){
this.$vm = vm;
this.$el = document.querySelector(el);
if(this.$el){
this.compile(this.$el);
}
}
compile(node){
const childNodes = node.childNodes;
Array.from(childNodes).forEach(n=>{
// 判断节点类型
//是元素节点
if(this.isElement(n)){
// ---------------处理指令编译--------------------------
this.compileElement(n);
//继续遍历节点
if (n.childNodes.length > 0) {
this.compile(n);
}
//字符串 插值表达式
}else if (this.isInter(n)) {
n.textContent = this.$vm[RegExp.$1];
}
})
}
isElement(n) {
return n.nodeType === 1;
}
isInter(n){
return n.nodeType === 3 && /\{\{(.*)\}\}/.test(n.textContent)
}
// 处理指令 编译
compileElement(n){
const attrs = n.attributes;
Array.from(attrs).forEach(attr=>{
const attrName = attr.name;
const exp = attr.value;
// 判断是否为特定的属性指令 h-
if(this.isDir(attrName)){
this.commandHandler(n,attrName,exp);
}
})
}
isDir(attrName){
return attrName&&attrName.startsWith('h-');
}
// 匹配对应的指令处理函数
commandHandler(node,attrName,exp){
const fn = this[attrName.replace('h-','')+'CommandHandler'];
fn&&fn.call(this,node,exp);
}
//h-text指令编译
textCommandHandler(node,exp){
node.textContent = this.$vm[exp];
}
//h-html
htmlCommandHandler(node,exp){
node.innerHTML= this.$vm[exp];
}
}
Let's take a look at the page effect.
Next, we need to change the data in js, and the page will be automatically updated. Simple idea to implement:
- When the template engine is compiling and processing, if an interpolation or instruction is encountered, a watcher is created to store the update operation of this node, and at the same time, the watcher and its corresponding response data are stored in the Dep, and a response data corresponds to a Dep .
- When the response data changes in the future, find the corresponding Dep, traverse and execute the watcher inside, and update the page.
Attach a schematic diagram:
Below we use code to achieve.
class Hvue{
constructor(options){
// 先保存相关参数
this.$options = options;
this.$data = options.data;
// 代理data,直接用this.xxx访问数据
proxy(this);
//1、实现数据响应式 遍历data
observe(options.data)
//2、编译 遍历节点
new Compile(options.el,this)
}
}
//遍历data 使其成为响应式数据
function observe(data){
// 判断类型
if(typeof data !== 'object' || data === null) return data;
new Observe(data)
}
class Observe{
constructor(obj){
// 数组类型要进行特殊处理
if(Array.isArray(obj)){
}else{
this.walk(obj);
}
}
walk(obj){
Object.keys(obj).forEach((key)=>defineReactive(obj,key,obj[key]));
}
}
// 给 object 定义一个响应式属性
function defineReactive(obj,key,val){
//对象嵌套处理
//如果val是对象
observe(val)
// 创建Dep实例
const dep = new Dep()
console.log(dep.watchers)
Object.defineProperty(obj,key,{
get(){
Dep.target && dep.add(Dep.target);
console.log(dep.watchers)
return val;
},
set(newValue){
console.log('set触发了')
val = newValue;
dep.emit()
}
})
}
//将所有的data代理到vue上,实现在代码中 this.xxx和this.data.xxx同样的效果
function proxy(vm){
Object.keys(vm.$data).forEach((key)=>{
Object.defineProperty(vm,key,{
get(){
return vm.$data[key];
},
set(newValue){
vm.$data[key] = newValue;
}
})
})
}
// 模版引擎编译
class Compile{
constructor(el,vm){
this.$vm = vm;
this.$el = document.querySelector(el);
if(this.$el){
this.compile(this.$el);
}
}
compile(node){
const childNodes = node.childNodes;
Array.from(childNodes).forEach(n=>{
// 判断节点类型
//是元素节点
if(this.isElement(n)){
// 处理指令编译
this.compileElement(n);
//继续遍历节点
if (n.childNodes.length > 0) {
this.compile(n);
}
//字符串 插值表达式
}else if (this.isInter(n)) {
// n.textContent = this.$vm[RegExp.$1];
this.commandHandler(n,'h-text',RegExp.$1)
}
})
}
isElement(n) {
return n.nodeType === 1;
}
isInter(n){
return n.nodeType === 3 && /\{\{(.*)\}\}/.test(n.textContent)
}
// 处理指令 编译
compileElement(n){
const attrs = n.attributes;
Array.from(attrs).forEach(attr=>{
const attrName = attr.name;
const exp = attr.value;
// 判断是否为特定的属性指令 h-
if(this.isDir(attrName)){
this.commandHandler(n,attrName,exp);
}
})
}
isDir(attrName){
return attrName&&attrName.startsWith('h-');
}
// 匹配对应的指令处理函数
commandHandler(node,attrName,exp){
const fn = this[attrName.replace('h-','')+'CommandHandler'];
fn&&fn.call(this,node,this.$vm[exp]);
// 创建watcher
new Watcher(this.$vm,exp,()=>fn(node,this.$vm[exp]))
}
//h-text指令编译
textCommandHandler(node,value){
node.textContent = value;
}
//h-html
htmlCommandHandler(node,value){
node.innerHTML= value;
}
}
// Watcher
class Watcher{
constructor(vm,key,updater){
this.vm = vm;
this.key = key;
this.updater = updater;
Dep.target = this;
console.log('Dep.target',Dep.target)
this.vm[key]; //触发对应data的get函数
Dep.target = null;
}
update(){
this.updater()
}
}
//保存Watcher 的 Dep 实例
class Dep{
constructor(){
this.watchers = [];
}
add(watcher){
this.watchers.push(watcher);
console.log('add',watcher,this.watchers)
}
emit(){
console.log('emit',this.watchers)
this.watchers.forEach(watcher=>watcher.update());
}
}
hvue.html, write a timer to change the value of counter.
<!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>简版vue的实现</title>
</head>
<body>
<div id="app">
<p>{
{
counter}}
<span>{
{
sum}}</span>
</p>
<p h-text="counter"></p>
<p h-html="htmlText"></p>
</div>
</body>
<!-- <script src="../node_modules/vue/dist/vue.js"></script> -->
<script src="./hvue.js"></script>
<script>
const app = new Hvue({
el:'#app',
data:{
counter:1,
sum:1,
htmlText:'<span style="color:red">我成功啦</span>'
}
})
setInterval(() => {
// console.log(app.counter)
app.counter++
}, 1000);
</script>
</html>
We can see that the page can be updated
The source code is here.
The next part will implement onClick and h-input.