前言
之前断断续续地在研究vue原理,但还未动手实战过。本文从最简单的开始,vue如何将{{}}
与data
联系起来,并渲染出来,比如{{msg}}
,data(){ return {msg:333} }
,那么{{msg}}
将渲染成333
,现在让我们开始吧。
实战
为了实现该功能,我动手写了一个Zue
,已将其放在github
上,其链接地址为https://github.com/zwf193071/zue.git 。下面我将为大家详细地说明原理。
index.html
的代码如下所示:
<div id="app">{{msg}}</div>
<script type="module">
import Zue from './src/zue.js'
{
new Zue({
el: '#app',
data() {
return {
msg: 666
}
}
})
}
</script>
Zue
首先执行init
初始化操作,代码如下所示:
init(options) {
const el = query(options.el);
this.$options = options;
this._initState();
this._compile(el);
}
query
通过document.querySelector(el)
获取到dom
元素,_initState
将data(){ return {msg:666} }
函数里返回的对象挂载在Zue
对象上,_initState
的源码如下所示:
Object.defineProperty(Zue.prototype, '$data', {
get () {
return this._data;
},
set (newData) {
if (newData !== this._data) {
// this._setData(newData);
}
}
});
Zue.prototype._initState = function () {
this._initData();
}
Zue.prototype._initData = function () {
var dataFn = this.$options.data;
var data = this._data = dataFn ? ( typeof dataFn == 'function' ? dataFn() : dataFn ) : {}
// proxy data on instance
var keys = Object.keys(data);
var i, key;
i = keys.length;
while (i--) {
key = keys[i];
this._proxy(key);
}
}
Zue.prototype._proxy = function (key) {
var self = this;
Object.defineProperty(self, key, {
configurable: true,
enumerable: true,
get: function proxyGetter() {
return self._data[key];
},
set: function proxySetter(val) {
self._data[key] = val;
}
});
}
_compile
只做一件事,那便是将我们的{{msg}}
转换成666
,其代码如下所示:
Zue.prototype._compile = function (el) {
var vm = this;
el.childNodes.forEach(node => {
if (node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)) {
node.textContent = vm[RegExp.$1]
}
});
}
其中RegExp.$1
是捕获正则表达式/\{\{(.*)\}\}/
第一个匹配的结果msg
,通过前面的_initData
,我们已经将msg
挂载在Zue
上,Zue["msg"]
值为666
,将666
赋值给node.textContent
,即可实现值替换
若更改index.html
的代码,需要对子元素内部的{{}}
实现值替换
<div id="app">{{msg}}<span>{{text}}</span></div>
<script type="module">
import Zue from './src/zue.js'
{
new Zue({
el: '#app',
data() {
return {
msg: 666,
text: "lucy"
}
}
})
}
</script>
我们需要修改__compile
里的源码
import {
toArray,
isElementNode,
isTextNode
} from './utils.js'
export default function lifecycleMixin(Zue) {
Zue.prototype._compile = function (el) {
var vm = this;
var childNodes = toArray(el.childNodes);
childNodes.forEach(node => {
var text = node.textContent;
var reg = /\{\{(.*)\}\}/;
if (isElementNode(node)) {
} else if (isTextNode(node) && reg.test(text)) {
vm.compileText(node, RegExp.$1.trim());
}
if (node.childNodes && node.childNodes.length) {
vm._compile(node); // 迭代替换子元素内部的{{}}
}
});
}
Zue.prototype.compileText = function (node, exp) {
node.textContent = this[exp]
}
}
utils.js
的代码如下所示:
function toArray (list, start) {
start = start || 0
var i = list.length - start
var ret = new Array(i)
while (i--) {
ret[i] = list[i + start]
}
return ret
}
function isElementNode (node) {
return node.nodeType == 1;
}
function isTextNode (node) {
return node.nodeType == 3;
}
且慢,有同学可能会问,若修改index.html
如下所示,那么This is a test{{msg}}
会被覆盖为msg
的值吧
<div id="app">This is a test{{msg}}<span>{{text}}</span></div>
<script type="module">
import Zue from './src/zue.js'
{
new Zue({
el: '#app',
data() {
return {
msg: 666,
text: "lucy"
}
}
})
}
</script>
是的,所以我们需要修改_compile
方法,如下所示:
function compileTextNode(node, vm) {
const reg = /\{\{(.+?)\}\}/g;;
let txt = node.wholeText;
txt = txt.replace(reg, (_, gl) => {
let key = gl.trim();
let value = vm[key];
return value;
});
node.textContent = txt;
}
export default function lifecycleMixin(Zue) {
Zue.prototype._compile = function (el) {
const vm = this;
const childNodes = toArray(el.childNodes);
childNodes.forEach(node => {
const text = node.textContent;
if (isElementNode(node)) {
} else if (isTextNode(node)) {
compileTextNode(node, vm);
}
if (node.childNodes && node.childNodes.length) {
vm._compile(node);
}
});
}
}
循环遍历,并替换{{}}
里面的文本值,即可实现值替换
上面还有一个明显的问题,直接操作dom元素,对textContent进行文本替换,需创建Document Fragment
,将el
元素拷贝到Document Fragment
中,对Document Fragment
文本值替换完毕后,再将其appendChild
到el
元素内,代码如下所示:
function compileTextNode(node, vm) {
const reg = /\{\{(.+?)\}\}/g;;
let txt = node.wholeText;
txt = txt.replace(reg, (_, gl) => {
let key = gl.trim();
let value = vm[key];
return value;
});
node.textContent = txt;
}
function compileNode(frag, vm) {
const childNodes = toArray(frag.childNodes);
childNodes.forEach(node => {
if (isElementNode(node)) {
} else if (isTextNode(node)) {
compileTextNode(node, vm);
}
if (node.childNodes && node.childNodes.length) {
vm._compile(node);
}
});
}
function node2Fragment(el) {
let fragment = document.createDocumentFragment(), child;
// 将原生节点拷贝到fragment
while (child = el.firstChild) {
fragment.appendChild(child);
}
return fragment;
}
export default function lifecycleMixin(Zue) {
Zue.prototype._compile = function (el) {
const frag = node2Fragment(el);
compileNode(frag, this);
el.appendChild(frag);
}
}