vue原理解析(一)
最近对Vue的原理进行学习,并对其部分原理进行模拟。通过学习,我发现Vue在html的使用中,一共由三个部分组成:M - V - VM,和原始的MVC框架的不同,VUE把controller层转换成ViewModel-连接Model和View,“桥”。我在学习的过程中,模拟了VUE的构建结构,希望通过我的分析,可以让我们在后面对VUE源码的解析更加的通俗易懂。
vue代码结构分析
我们在HTML中引入框架,通过JS引入vue.js
<script type="text/javascript" src="../assets/js/vue.js"></script>
在HTML页面代码中,我们使用Vue的API
<div id="root">
<div>
<div>
<p>{{name}} - {{message}}</p>
</div>
</div>
</div>
再在JS中,创建VUE实例
var outData={
name: 'hello',
message: 'world’
}
var app=new Vue({
el:'#root',
data:outData,
})
实现了如下效果
vue结构模拟
在模拟过程中,我们不需要引入JS,html代码也是和原来一致的只需要通过自己手写的方法,来模拟其结构。我们可以发现在VUE中,我们原始创建的DOM被生成的DOM替换了
我们可以发现,在浏览器中输出节点的时候,我们在调用Vue之前的节点,被替换了,生成后的节点被加载在了页面上。因此,我们需要模拟这种替换的过程
html
<div id="root">
<div>
<div>
<p>{{name}} - {{message}}</p>
</div>
</div>
</div>
js
<script>
// 打印root
console.log(root)
// 获取元素的dom
const dom = document.querySelector( '#root' )
// 设置data
let data = {
name: 'hello',
message: 'world'
}
//对存在的Dom进行替换
let complier = (template, data) => {
// 用于替换{{}}中数据的正则
const ruleForSupport = /\{\{(.+?)\}\}/g
let children = template.childNodes
for(let i = 0; i< children.length; i++) {
let type = children[i].nodeType
//对文本标签中的{{}}中的数据替换
if(type == 3) {
let value = children[i].nodeValue
value = value.replace(ruleForSupport, (_, g) => {
console.log(1)
let value = data[g.trim()]
return value
})
console.log(value)
children[i].nodeValue = value
}else {
// 对于非文本标签进行遍历
complier(children[i], data)
}
}
}
// 通过copy节点保留原始节点,方便后面刷新操作的替换
let copyedDom = dom.cloneNode(true)
// 调用方法 对标签进行渲染
complier(copyedDom, data)
// 对root标签进行替换
root.parentNode.replaceChild(copyedDom, root)
// 打印修改后的root
console.log(root)
</script>
效果如下
可以发现 我们已经简单实现了{{}}中信息的展示,但是还有一定的问题
-
vue 是通过虚拟dom来实现的
-
我们只考虑了 如{{name}}的简单数据模式,并未考虑如{{person.name}}的对象数据类型
-
未对代码进行整合
所以我们先对对象数据类型进行分析,现在我们希望通过自己写的类来实现类似
let app = new pfVue({
el: '#root',
data: {
name: 'ppppffff',
message: 'ok'
}
})
所以我们在页面中使用
<div id="root">
<div>
<div>
<p>{{name.firstName}} - {{message}}
--{{text.title.first}}</p>
</div>
</div>
</div>
通过创建pfVue来模拟实现
<script>
console.log(root)
//对存在的Dom进行替换
let complier = (template, data) => {
// 用于替换{{}}中数据的正则
const ruleForSupport = /\{\{(.+?)\}\}/g
let children = template.childNodes
for(let i = 0; i< children.length; i++) {
let type = children[i].nodeType
//对文本标签中的{{}}中的数据替换
if(type == 3) {
let value = children[i].nodeValue
value = value.replace(ruleForSupport, (_, g) => {
let arr = g.trim().split('.')
let getValueByKeli = createGetValueByPath(arr)
let arrValue = getValueByKeli(data)
console.log(arrValue)
return arrValue
})
children[i].nodeValue = value
}else {
// 对于非文本标签进行遍历
complier(children[i], data)
}
}
}
// 利用递归获取当前参数的层级
function getValue(value, array, index) {
value = value[array[index]]
if(index + 1 < array.length) {
return getValue(value, array, index + 1) // 递归函数中需要返回值需要逐级返回 ***重点
}else {
console.log(value)
return value
}
}
// 利用循环获取当前参数的层级
function getValue2( value, array) {
let res = value
for(let i = 0 ; i < array.length ; i ++) {
res = res[array[i]]
}
return res
}
// 利用函数的柯里化
function createGetValueByPath(path) {
let res = path
return function(value) {
let prop
while(prop = res.shift()) {
value = value[prop]
}
return value
}
}
function pfVue(obj) {
// 内部数据 _开头,只读通过$开头 类似Vue源码
this._data = obj.data
this._el = obj.el
console.log(this._el)
this._templaterDom = document.querySelector( this._el )
this.$parent = this._templaterDom.parentNode
this.render()
}
// 渲染
pfVue.prototype.render = function() {
this.complier()
}
pfVue.prototype.complier = function() {
//通过clone来的DOM进行操作 留下样本
let _copyedDom = this._templaterDom.cloneNode(true)
complier(_copyedDom, this._data)
this.update(_copyedDom)
}
// 替换标签
pfVue.prototype.update = function(real) {
this.$parent.replaceChild(real, this._templaterDom)
}
let app = new pfVue({
el: '#root',
data: {
name: {
firstName: 'aaaa'
},
message: 'ok',
text: {
title: {
first: '111'
}
}
}
})
console.log(root)
</script>
到了这里我们已经解决了对象数据的问题,我们下面主要需要解决两个问题
- 把页面中的DOM转换成虚拟DOM
- 把虚拟节点加载到页面当中
虚拟DOM的实现
我们需要把页面中的DOM,全部转换成虚拟DOM数据,方便我们后面加载到页面中,所以我们需要先创建一个虚拟的DOM类,VNode
// 创建VNode类
// nodeName data content nodeType children
class VNode {
constructor(nodeName, data, content, nodeType) {
this.nodeName = nodeName && nodeName.toLowerCase();
this.data = data;
this.content = content;
this.nodeType = nodeType;
this.childNodes = [];
}
appendChildren(vnode) {
this.childNodes.push(vnode)
}
}
然后只需要把页面中的对象全部读取出来
/**
* 把节点转换成虚拟节点
*/
function readNode(node) {
console.dir(node)
let comNode ;
const _nodeType = node.nodeType
if(_nodeType === 1) {
// 元素节点
const _nodeAttrs = node.attributes
const _nodeName = node.nodeName
let data = {}
// 对节点中的属性进行读取
for( let i = 0; i < _nodeAttrs.length; i++ ) {
data[_nodeAttrs[i].nodeName] = _nodeAttrs[i].nodeValue
}
console.log(data)
comNode = new VNode( _nodeName, data, undefined, _nodeType )
let childNodes = node.childNodes
for(let i = 0; i < childNodes.length; i++) {
comNode.appendChildren( ( readNode(childNodes[i]) ) )
}
}else if(_nodeType === 3) {
// 文本节点
comNode = new VNode( undefined, undefined, node.nodeValue, _nodeType )
}
return comNode
}
通过上面的步骤我们已经实现了虚拟DOM的转换,接下来只需要对DOM进行挂载就可以了。
/**
* 对虚拟DOM进行渲染
*/
function complierNode(v) {
console.dir(v)
let $nodeType = v.nodeType
let node = {}
if($nodeType === 1) {
node = document.createElement(v.nodeName)
node.nodeType = $nodeType
for(let item in v.data) {
node.setAttribute( item, v.data[item] )
}
if(v.childNodes.length > 0) {
let _children = v.childNodes //这里曾少写过一个Let导致递归错误
for( let i = 0; i < _children.length; i++ ) {
let child = complierNode(_children[i])
node.appendChild(child)
}
}
}
else if( $nodeType === 3 ) {
node = document.createTextNode(v.content)
}
return node
}
// 挂载DOM
function mount() {
let $DOM = document.querySelector( '#root' );
let $v = readNode($DOM)
let $compliedDOM = complierNode($v)
$DOM.parentNode.replaceChild( $compliedDOM, $DOM )
}
然后我们只需要调用mount方法,就可以实现虚拟DOM的转换了。