现在用来手写实现vue的简单功能,主要就是可以和Vue一样渲染页面,这个是很简单的一个,没有实现指令操作,想看指令操作的可以看一下我的github:github地址
这是html代码
<div id="app">
<p>姓名:{{name}}</p>
<p>年龄:{{age}}</p><button onclick="vm.age++">增加年龄</button>
<dl>
<dt>住址</dt>
<dd>
省份:{{addr.province}}
</dd>
<dd>
城市:{{addr.city}}
</dd>
</dl>
</div>
页面上的展示效果:
很简单,但是挺锻炼思想的,所以就写了下来。
模块划分
总体分为五个模块:编译模块、虚拟节点模块、渲染模块、数据响应模块以及vue构造函数模块
html页面中使用js的时候,要使用type=module,防止变量污染
编译模块
编译模块提供了一个compile函数,将一个模板文本和环境编译成一个结果。getTmeplate函数负责提取
{{}}模板,用正则表达式来匹配。getValue函数负责读取环境变量里的值。最后在compile中进行替换。
compile模块是比较简单的
//获取模板的函数
function getTmeplate(template){
//匹配{{}里面的内容}
var matches = template.match(/{{[^}]+}}/g);
return matches ||[] //如果一个都没匹配到就返回[]
}
//获取环境变量中的值
function getValue(frag, envObj){
//去掉模板两边的括号
let exp = frag.replace("{{", "").replace("}}", "");
let props = exp.split('.');//将表达式分解成数组
let obj = envObj;//将envObj赋值给obj
for(let i = 0; i < props.length; i++){
obj = obj[props[i]];
}
return obj;
}
export default function compile(template, envObj){
let frags = getTmeplate(template);
let result = template
for(let i = 0; i < frags.length; i++){
let frag = frags[i];
result = result.replace(frag, getValue(frag, envObj));
}
return result;
}
虚拟节点模块
虚拟节点模块提供一个createVNode函数,根据真实的DOM,构建一个虚拟DOM树。
VNode是一个构造函数,负责创建节点。
function VNode(realDom, template){
this.realDom = realDom;
this.template = template;
//真实dom的子节点
this.children = [];
}
export default function createVNode(realDom){
let root = new VNode(realDom, "");
//判断真实dom是否为文本节点
//记录本节点的值到虚拟节点
if(realDom.nodeType === 3){
root.template = realDom.nodeValue
}else{ //不是文本节点
//不能使用realDom.children,因为这样只能得到标签元素
for(let i = 0; i < realDom.childNodes.length; i++){
let childRealNode = realDom.childNodes[i]; //拿到真实节点的真实子节点
let vnode = createVNode(childRealNode); //进行递归
//将虚拟节点添加到虚拟dom树中
root.children.push(vnode);
}
}
return root;
}
渲染模块
编译模块和虚拟节点模块都创建好了,接下来就是渲染模块了。用于提取虚拟节点中的文本节点,将其模板编译结果设置到真实dom中,对虚拟节点的子节点同样的操作。
import compile from './compile.js'
//渲染文本节点
export default function render(vnode, envObj){
//判断是否为文本节点
if(vnode.realDom.nodeType === 3){
//是文本节点,进行编译
//将编译结果设置在真实dom中
let result = compile(vnode.template, envObj);
if(result !== vnode.realDom.nodeValue){ //每次改变的值与原本的值不同再进行赋值
vnode.realDom.nodeValue = result;
}
}else{ //如果不是文本节点
for(let i = 0; i < vnode.children.length; i++){
//拿到子节点在进行渲染
let childNode = vnode.children[i];
render(childNode, envObj);
}
}
}
数据响应模块
主要负责两原始对象的数据附加到代理对象上,代理对象可以监听到数据的更改,当数据更改时,执行某个回调函数。
vue2.x版本中使用的是Object.defineProperty来进行数据响应的处理,因此这里我们也是这样来实现
稍微复杂一点的地方就是在对属性的判断为对象,然后进行的操作。如果属性值是一个对象,那么就要新创建一个对象来对他代理,targetObj再代理newTarget。
import render from "./render.js";
//将原始对象添加到代理对象中
function proxyProp(originalObj, targetObj, prop, callback){
if(typeof originalObj[prop] === 'object'){
//要代理的属性是一个对象
let newTarget = {};//新的要代理该属性的对象
createResponse(originalObj[prop], newTarget, callback);
Object.defineProperty(targetObj, prop, {
get:function(){
return newTarget;
},
set:function(value){
originalObj[prop] = value;
newTarget = value;
callback && callback(prop);
}
})
}else{
//要代理的属性不是一个对象
Object.defineProperty(targetObj, prop, {
get:function(){
return originalObj[prop]
},
set:function(value){
originalObj[prop] = value;
callback && callback(prop);
}
})
}
}
//将原始对象的所有属性,提取到代理对象中
export default function createResponse(originalObj, targetObj, callback){
for(let prop in originalObj){
proxyProp(originalObj, targetObj, prop, callback)
}
}
vue构造函数模块
通过Vue构造函数可以创建一个vue实例,在创建的过程中,需要完成以下操作:
1. 保存el和data配置
2. 根据el创建虚拟节点
3. 将data中的数据附加到代理对象—vue实例中
import createVNode from "./vnode.js";
import createResponse from "./dataResponse.js";
import render from './render.js'
export default function Vue(options){
//保存el和data
this.$el = options && options.el;
this.$data = options && options.data;
//根绝el创建虚拟节点
this.$vnode = createVNode(document.querySelector(this.$el));
//将data中的数据附加到代理对象--vue实例中
let that = this;
createResponse(this.$data, this, function (){
that.render();
})
//初始加载时渲染一次
this.render();
}
Vue.prototype.render = function(){
render(this.$vnode, this);
}
最后再创建一个index.js
这个文件负责接收和导出,当然也可以不用这样写,但是文件多了以后,这样写还是比较方便的,一目了然
import Vue from './Vue.js'
export default Vue;
这样就完成了基本功能,在控制台修改数据也是可以渲染到页面,如果想添加一些功能,也可以继续写。