自己实现一个简化版的Vue关于数据的操作(双向绑定, 插值表达式, 虚拟节点)
-
编译模块
-
虚拟节点模块
-
渲染模块
-
数据响应模块(Vue的双向数据绑定)
-
Vue构造函数模块
编译模块
提供一个compile函数, 将一个模板文本和环境对象编译成一个结果
function compile(template, envObj)
例如:
const result = compile("英雄名称: {{ name }}, 英雄职业: {{ job }}, 英雄大招: {{ skills.powerSkill }}", {
name: '男枪',
job: 'ADC',
skills: {
powerSkill: '终极爆弹'
}
})
// 编译结果为 英雄名称: 男枪, 英雄职业: ADC, 英雄大招: 热情爆弹
注意我开始了
笔者新建了一个compile.js
// compile.js 代码
/**
* 根据环境和模板对象, 得到要渲染的编译结果
* @param {模板} template
* @param {环境对象} envObj
*/
export default function compile(template, envObj) {
// 提取template中的插值表达式
const frags = getFragments(template);
console.log(frags);
// 我们可能需要先保存一下这个template, 当我们将里面的插值表达式都替换以后直接返回result
let result = template;
frags.forEach(frag => {
// 每遍历一次, 就将当次循环的插值表达式拿去跟result进行比对并进行替换
let matchValue = getEnvValue(frag, envObj);
result = result.replace(frag, matchValue);
})
// result保存了最终的编译结果
return result;
}
// 提供一个getFragments方法, 该方法接收一个模板, 并将模板中所有插值表达式提取出来并且返回
// 如 "英雄名称: {{ name }}, 英雄职业: {{ job }}, 英雄大招: {{ skills.powerSkill }}"
// 调用该函数以后 返回 [ {{name}}, {{ job }}, {{ skills.powerSkill }} ], 如果没有检查到
// 插值表达式, 则返回空数组
const getFragments = function(template) {
// 正则匹配{{ }}
return template.match(/{{ [^}]+ }}/g) || [];
}
// 提供一个getEnvValue方法, 提供一个插值表达式和一个环境对象, 返回该表达式在环境对象中对应的值
const getEnvValue = function(frag, envOvj) {
// 将插值表达式中的变量真正的提取出来
const variableExp = frag.replace('{{', "").replace("}}", "").trim();
// 变量可能带.因此我们用split分割将它变成数组
const props = variableExp.split('.');
let result = envObj; // result为最后要返回出去的值
// 遍历props数组, 开始取值
// 每循环一次, 就将result的值重新赋值知道找到最后一个变量为止
props.forEach(variable => result = result[variable]);
return result || '';
}
至此, 编译模块compile.js就具备了将Vue的模板语法解析成正常文本的能力
虚拟节点模块
提供一个函数createVNode, 根据真实的dom对象构建出虚拟dom树
function createVNode(realDom);
例如:
<div id = '#app'>
<p>{{ name }}</p>
<p>{{ age }}</p>
</div>
const node = createVNode(document.querySelector('#app'));
然后我输出node 他要给我一个虚拟节点对象, 如下
上方是虚拟节点对象, 实际上他是一棵树型结构, 如图
注意我开始了
话不多说新建一个vNode.js
// 看上方的图打印出来的对象是一个对象前面还带一个vNode名字, 那么毋庸置疑这肯定是一个构造函数
// 构造虚拟节点的类,
// realDom -> 真实dom template -> 文字模板(只有遇到了文本节点才会安排template),
// 如果一直是dom节点那么我们要一直创建虚拟节点
class VNode {
constructor(realDom, template) {
this.realDom = realDom;
this.template = template;
this.children = []; // 存放虚拟子节点的数组, 因为在一个真实dom中会有很多子节点, 我们将这些子节点映射成虚拟dom放进children数组中, 整体形成一个树一样的结构
}
}
// 我们暴露出去一个createNode方法, 外界一调用这个方法必定可以创建一个虚拟dom树出来
// 该方法接收一个参数realDom -> 真实dom
export default function createVNode(realDom) {
const root = new VNode(realDom, ''); // 将根节点创建一个虚拟dom,先姑且将它的template着设置为''空,
// 来判断传进来的真实节点是不是文本节点如果是的话 那么就要给他的template设置值
if(realDom.nodeType === Node.TEXT_NODE) {
root.template = realDom.nodeValue;
}else {
// 如果不是文本节点, 直接开始循环真实dom的子节点
for(let i = 0, len = realDom.childNodes.length; i < len; i++) {
let realChildDom = realDom.childNodes[i];
// 直接递归创建虚拟子节点
let childNode = createVNode(realChildDom);
root.children.push(childNode); // 同时将该节点push进它虚拟父节点的children数组
}
}
}
至此, 虚拟节点模块vNode.js就具备了将真实dom映射出虚拟dom的能力
渲染模块
用于提取虚拟节点中的文本节点, 将起模板编译结果设置到真实dom中, 对虚拟节点的子节点也做同样的操作
function render(vNode, envObj)
这个模块的功能就是渲染页面都没啥好说的, 直接开干
新建一个render.js
import compile from './compile.js'; // 导入我们写的编译模块, 笔者是放在同一级目录下
// 渲染函数, 接收一个vNode -> 虚拟节点, envObj -> 环境对象
export default function render(vNode, envObj) {
// 如果传进来的vNode是文本节点, 我们直接开始编译, 并将编译结果直接赋值进去
if(vNode.realDom.nodeType === Node.TEXT_NODE) {
const result = compile(vNode.template, envObj);
// 如果两次的节点值一样则直接不渲染
if(result === vNode.realDom.nodeValue)return;
vNode.realDom.nodeValue = result;
}else {
如果不是文本节点, 那就找子节点中的子节点, 如此递归
for(let i = 0, len = vNode.children.length; i < len; i++) {
// 如果不是文本节点, 直接继续找
render(vNode.children[i], envObj);
}
}
}
如果以后需要重新渲染的话直接调用render就好
至此, 渲染模块render.js就具备了根据环境对象和虚拟节点渲染页面的能力
数据响应模块
主要负责将原始对象的数据附加到代理对象上, 代理对象能够监听到数据的更改, 当数据更改的时候, 执行某个回调函数
也就是Vue的核心功能双向绑定
function createResponsive(originalObj, targetObj, callback);
*前方高能~~~~*
新建一个dataResponsive.js
/**
* 将原始对象中所有的属性都提取到代理对象中来进行访问控制
* @param {原始对象} originalObj
* @param {代理对象} proxyObj
* @param {当代理对象上的属性被重新赋值以后触发的回调} callback
*/
export default function createResponsive(originalObj, proxyObj, callback) {
for(let prop in originalObj) {
proxyProp(originalObj, prop, proxyObj, callback);
}
}
/**
* 代理某个属性
* @param {原始对象} originalObj
* @param {该属性} prop
* @param {代理对象} proxyObj
* @param {当代理对象上的该属性被重新赋值以后触发的回调} callback
*/
const proxyProp = function(originalObj, prop, proxyObj, callback) {
// 当需要感知的是一个对象
if(typeof originalObj[prop] === 'object') {
let newTarget = {}; // 定义一个新的代理对象
createResponsive(originalObj[prop],newTarget, callback); // 递归调用自身
Object.defineProperty(proxyObj, prop, {
get() {
// 当然不要忘记对这个对象也要进行代理, 但是访问的时候就不能返回originalObj[prop]
// 了, 要返回newTarget, 因为当我们访问originalObj[prop]这个对象中某个属性的时候
// 也会触发originalObj[prop]的访问, 而如果这里返回的不是newTarget那么自然也就监控
// 不到newTarget中属性的变化了
return newTarget;
},
set(newVal) {
originalObj[prop] = newVal;
newTarget = newVal;
typeof callback === 'function' && callback(prop);
}
})
}else {
// 如果就是简单的原始值
Object.defineProperty(proxyObj, prop, {
get() {
return originalObj[prop];
},
set(newVal) {
// console.log('更改属性')
originalObj[prop] = newVal;
// 当修改值的时候触发callback回调
typeof callback === 'function' && callback(prop);
}
})
}
}
至此, 数据响应模块dataResponsive.js就具备了监听数据变化的能力
Vue构造函数模块
通过Vue构造函数可以创建一个Vue的实例, 在创建的过程中, 需要完成以下操作
-
保存el和data配置
-
根据el创建虚拟节点
-
将data中的数据附加到代理对象-vue实例中
有了之前的这些辅助函数以后我们Vue的构造函数会变得相当易写
import createVNode from './vNode.js';
import createResponsive from './dataResponsive.js';
import render from './render.js';
Vue.prototype.$render = render; // 将render方法注入到Vue的原型上
let _uid = 0;
export default function Vue(config) {
if(!config) {
throw new Error('expected a initail config');
}
// 保存el和data配置
this.$el = config.el;
this.$data = config.data;
this._isVue = true;
this._uid = ++ _uid;
// 根据el来创建虚拟节点
const realDom = document.querySelector(this.$el);
console.log(realDom, this.$el, document.querySelector('#app'));
this.$vnode = createVNode(realDom);
// 初始渲染一次
this.$render(this.$vnode, this.$data);
// 将data中的数据附加到代理对象 - vue实例中
createResponsive(this.$data, this, () => {
// 只要代理数据一改变就触发这个回调
console.log(this.$render);
this.$render(this.$vnode, this.$data);
})
}
至此, 构造函数Vue完成
试验
我们创建一个文件然后导入我们自己写好的Vue框架, 并且尝试接管html文档中的某块区域
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Vue</title>
</head>
<body>
<div id="app">
<p>英雄名称:{{ name }}</p>
<p>英雄职业:{{ job }}</p>
<p>英雄大招:{{ skills.powerSkill }}</p>
</div>
<script type="module">
import Vue from './Vue.js';
window.vm = new Vue({
el: '#app',
data: {
name: '男枪',
job: 'ADC',
skills: {
powerSkill: '终极爆弹'
}
}
})
</script>
</body>
</html>
页面成功渲染, 而Vue实例也成功建立