一、Vue的执行步骤
1.首先会获得页面的模板,模板中会有 {{}} 这样的变量
2.利用Vue的构造函数中所提供的数据来替换 {{}} 中的变量,得到可以在页面中显示的 ”标签“
3.将 ”标签”替换之前页面中有 {{}} 的标签
Vue利用提供的数据和模板生成一个新的HTML标签(node节点),替换到页面放模板的地方
二、实现
1.步骤拆解:
拿到模板->拿到准备的数据->将数据和模板结合,得到HTML元素->将真正需要渲染的HTML元素挂载到页面中
2.具体实现 1.0版本
未解决的问题* 1.代码没有整合(vue中使用了一个构造函数)* 2.只考虑了单属性{{name}},而Vue中大量的使用层级,比如{{child.name.firstname}}* 3.vue使用的虚拟DOM
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="root"> <p>{{name}}</p> <p>{{message}}</p> </div> <script> /** * 考虑的问题: * 标签的嵌套:算法(递归、队列、深度优先....等等)+正则的方式来解决 * */ let parren = /\{\{(.+?)\}\}/g; /** 1.拿到模板*/ let tempNode = document.querySelector('#root'); /** 2.拿到准备数据*/ let data = { name: '名字', message: '消息' } /** 3.将数据放到模板当中,得到HTML元素*/ function compiler(template, data) { /** * 在这个案例中template是DOM元素 * 在vue的源码中是字符串,DOM->字符串模板->VNode->正则的DOM */ let childNodes = template.childNodes; for (let i = 0; i < childNodes.length; i++) { let type = childNodes[i].nodeType; if (type === 3) { /** * 文本节点:判断其内容有没有{{}} */ let text = childNodes[i].nodeValue; text = text.replace(/\{\{(.+?)\}\}/g, function (word, g) { /** * 每次匹配到文本都会调用后面的function * _中是匹配到的内容(有{{}}) * _后面的参数是分组的内容,也就是每组{{}}里面的内容 */ let key = g.trim();//{{}}中的内容 let value = data[key]; /** * 将{{}}及其内容替换为data中的数据 */ return value; } ); childNodes[i].nodeValue = text; } else if (type === 1) { /** * 元素:考虑是否有子元素,有的话考虑子元素进行是否要插值 */ compiler(childNodes[i], data) } } } let generateNode = tempNode.cloneNode(true)//保留原始的模板,将复制的模板去做操作(避免之后对数据的修改没有原始模板,就不知道哪里是需要修改数据的地方) console.log(tempNode) compiler(generateNode, data) console.log(generateNode) /** 4.将需要渲染的HTML元素放到页面中*/ root.parentNode.replaceChild(generateNode, root) </script> </body> </html>
3.具体实现 2.0版本
1)代码整合
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <!--模板--> <div id="root"> <p>{{name}}</p> <p>{{message}}</p> </div> <script> /** 习惯:在Vue中内部的数据一般使用下划线开头,只读数据使用$开头*/ let parren = /\{\{(.+?)\}\}/g function compiler(template, data) { let childNodes = template.childNodes; for (let i = 0; i < childNodes.length; i++) { let type = childNodes[i].nodeType; if (type === 3) { let text = childNodes[i].nodeValue; text = text.replace(/\{\{(.+?)\}\}/g, function (word, g) { let key = g.trim(); let value = data[key]; return value; }); childNodes[i].nodeValue = text; } else if (type === 1) { compiler(childNodes[i], data) } } } function JGVue(options) { /** * Vue构造函数 */ this._data = options.data; this._el = options.el; /** 准备工作(准备模板)*/ this.$el = this._templateDOM = document.querySelector(this._el); this._parent = this.$el.parentNode; /** 渲染工作*/ this.render() } JGVue.prototype.render = function () { /**渲染函数 将数据放到模板中,得到DOM元素,并加载到页面中*/ this.complier() } JGVue.prototype.complier = function () { /** 将数据放到模板中,得到真正DOM元素*/ let realHTMLDOM = this._templateDOM.cloneNode(true); compiler(realHTMLDOM, this._data); this.update(realHTMLDOM) } JGVue.prototype.update = function (realHTMLDOM) { /** 将DOM元素加载到页面中*/ this._parent.replaceChild(realHTMLDOM, document.querySelector('#root')) } /** *构造函数的使用 */ let app = new JGVue({ el: '#root', data: { name: 'luyiyi', message: 'info' } }) </script> </body> </html>
2)解决属性嵌套问题
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <!--模板--> <div id="root"> <p>{{name.firstName}}</p> <p>{{message}}</p> </div> <script> /**解决使用'xxx.yyy.zzz'访问对象的问题 * Vue使用到函数科里化的技巧: * 因为这个函数是在Vue编译模板的时候就生成了,所以可以在任何地方使用 * 在使用的时候可以减少函数的调用,提高一点性能 */ function getValueByPath(obj, path) { /** * 先取得obj.xxx * 再去结果中的yyy * 再取结果中的zzz */ let paths = path.split('.'); let res = obj; for (let i = 0; i < paths.length; i++) { res = res[paths[i]]; } return res; } let parren = /\{\{(.+?)\}\}/g function compiler(template, data) { let childNodes = template.childNodes; for (let i = 0; i < childNodes.length; i++) { let type = childNodes[i].nodeType; if (type === 3) { let text = childNodes[i].nodeValue; text = text.replace(/\{\{(.+?)\}\}/g, function (word, g) { let path = g.trim(); let value = getValueByPath(data, path); return value; }); childNodes[i].nodeValue = text; } else if (type === 1) { compiler(childNodes[i], data) } } } function JGVue(options) { /** * Vue构造函数 */ this._data = options.data; this._el = options.el; /** 准备工作(准备模板)*/ this.$el = this._templateDOM = document.querySelector(this._el); this._parent = this.$el.parentNode; /** 渲染工作*/ this.render() } JGVue.prototype.render = function () { /**渲染函数 将数据放到模板中,得到DOM元素,并加载到页面中*/ this.complier() } JGVue.prototype.complier = function () { /** 将数据放到模板中,得到真正DOM元素*/ let realHTMLDOM = this._templateDOM.cloneNode(true); compiler(realHTMLDOM, this._data); this.update(realHTMLDOM) } JGVue.prototype.update = function (realHTMLDOM) { /** 将DOM元素加载到页面中*/ console.log(this._parent) this._parent.replaceChild(realHTMLDOM, document.querySelector('#root')) } /** *构造函数的使用 */ let app = new JGVue({ el: '#root', data: { name: { firstName: 'lu', lastName: 'yiyi' }, message: 'info' } }) </script> </body> </html>
3)生成虚拟DOM
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="root"> <ul class="ul"> <li>li1</li> <li>li2</li> <li>li3</li> </ul> <div class="div1">div1</div> <div>div2</div> <div>div3</div> </div> <script> /** * 1.解决的问题:虚拟DOM * 2.为什么要使用虚拟DOM:提高性能,所有操作都在内存中,只需要最后操作一次 * 3.考虑: * 1)将真正的DOM转为虚拟DOM * 2)将虚拟DOM转为真正的DOM * 4.思想:与深拷贝类似 * 5.具体实现:使用js对象表示HTML标签=》 * tag:标签名 * value:文本节点的值 * data:属性名 * children:子元素 * type:这个元素的类型 */ class VNode { constructor(tag, value, data, type) { this.tag = tag && tag.toLowerCase(); this.value = value; this.data = data; this.children = []; this.type = type } appendChild(vnode) { this.children.push(vnode) } } /** * 使用递归来遍历DOM元素生成虚拟DOM,vue源码中使用栈存储父元素实现递归生成(因为需要解析字符串,这种方法更加便利) */ function getVNode(node) { let nodeType = node.nodeType; let _vnode = null; if (nodeType == 1) { let nodeName = node.nodeName; /** attrs返回所有属性组成的伪数组 * 需要将attrs变为一个对象 */ let attrs = node.attributes; let _attrObj = {}; for (let i = 0; i < attrs.length; i++) { _attrObj[attrs[i].nodeName] = attrs[i].nodeValue; } _vnode = new VNode(nodeName, undefined, _attrObj, nodeType); /** 考虑传进来node的子元素,将子元素加入到生成的虚拟节点下面*/ let childNodes = node.childNodes; for (let i = 0; i < childNodes.length; i++) { _vnode.appendChild(getVNode(childNodes[i])) } } else if (nodeType == 3) { _vnode = new VNode(undefined, node.nodeValue, undefined, nodeType); } return _vnode; } let root = document.querySelector('#root'); let vroot = getVNode(root); console.log(vroot) /** * 将虚拟DOM转为真实DOM */ function parseNode(vnode) { let _node = null if (vnode.type == 1) { /** 节点是元素节点的时候,创建元素节点*/ _node = document.createElement(vnode.tag); if (vnode.data) { /** 考虑是否有属性*/ for (let item in vnode.data) { _node.setAttribute(item, vnode.data[item]) } } /** 考虑是否有子元素*/ if (vnode.children) { for (let i = 0; i < vnode.children.length; i++) { _node.appendChild(parseNode(vnode.children[i])); } } } else if (vnode.type == 3) { /** 节点是文本节点的时候,创建文本节点*/ _node = document.createTextNode(vnode.value); } return _node; } </script> </body> </html>
4.完整版(接近源码,但是有很多地方被简化)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <!--模板--> <div id="root"> <p>{{name.firstName}}</p> <p>{{message}}</p> </div> <script> /** * 在Vue中使用了二次提交的设计结构 * 1.在页面中的DOM和虚拟DOM是一一对应的关系 * 2.先由AST和数据生成新的VNode(render函数) * 3.将旧的VNode和新的VNode进行比较(diff),更新(update函数) */ /** Vue构造函数*/ function JGVue(options) { this._data = options.data; let elm = document.querySelector('#root'); this._template = elm; this._parent = elm.parentNode; this.mount();//挂载函数 } /** 虚拟DOM构造函数*/ class VNode { constructor(tag, value, data, type) { this.tag = tag && tag.toLowerCase(); this.value = value; this.data = data; this.children = []; this.type = type } appendChild(vnode) { this.children.push(vnode) } } /** 由HTML DOM生成虚拟DOM,将这个函数当做complier函数使用,模拟抽象语法树 * 而在Vue中将真正的node作为一个字符串解析,得到一个抽象语法树 */ function getVNode(node) { let nodeType = node.nodeType; let _vnode = null; if (nodeType == 1) { let nodeName = node.nodeName; /** attrs返回所有属性组成的维数组 * 需要将attrs变为一个对象 */ let attrs = node.attributes; let _attrObj = {}; for (let i = 0; i < attrs.length; i++) { _attrObj[attrs[i].nodeName] = attrs[i].nodeValue; } _vnode = new VNode(nodeName, undefined, _attrObj, nodeType); /** 考虑传进来node的子元素,将子元素加入到生成的虚拟节点下面*/ let childNodes = node.childNodes; for (let i = 0; i < childNodes.length; i++) { _vnode.appendChild(getVNode(childNodes[i])) } } else if (nodeType == 3) { _vnode = new VNode(undefined, node.nodeValue, undefined, nodeType); } return _vnode; } function parseNode(vnode) { /** 将虚拟DOM转为真实DOM*/ let _node = null if (vnode.type == 1) { _node = document.createElement(vnode.tag); if (vnode.data) { for (let item in vnode.data) { _node.setAttribute(item, vnode.data[item]) } } if (vnode.children) { for (let i = 0; i < vnode.children.length; i++) { _node.appendChild(parseNode(vnode.children[i])); } } } else if (vnode.type == 3) { _node = document.createTextNode(vnode.value); } return _node; } function getValueByPath(obj, path) { /** 根据路径访问对象任意层级的属性*/ let paths = path.split('.'); let res = obj; for (let i = 0; i < paths.length; i++) { res = res[paths[i]]; } return res; } let parren = /\{\{(.+?)\}\}/g function combine(vnode, data) { /** 将带有{{}}的vnode与数据结合得到填充数据的vnode,模拟AST到vnode的行为*/ let _type = vnode.type; let _data = vnode.data; let _value = vnode.value; let _tag = vnode.tag; let _children = vnode.children; let _vnode = null; if (_type == 3) { _value = _value.replace(parren, function (str, path) { return getValueByPath(data, path.trim()); }); _vnode = new VNode(_tag, _value, _data, _type); } else if (_type == 1) { _vnode = new VNode(_tag, _value, _data, _type); _children.forEach(item => { _vnode.appendChild(combine(item, data)) }) } return _vnode; } /** 挂载函数*/ JGVue.prototype.mount = function () { this.render = this.createRenderFn()/** 1.根据Vue构造函数中的内容生成虚拟DOM*/ this.mountComponent();/** 2.将虚拟DOM渲染到页面*/ } JGVue.prototype.mountComponent = function () { /* let mount = () => { this.update(this.render())//渲染到页面上 mount.call(this)//本质上应该交给watcher来调用 */ this.update(this.render())//渲染到页面上 } JGVue.prototype.createRenderFn = function () { /** 生成render函数,目的是缓存AST(使用虚拟DOM模拟)*/ let ast = getVNode(this._template) return function render() { /** * Vue中:将AST+data=>VNode * 这里使用带{{}}的模板和数据来模拟=》含有数据的VNode */ return combine(ast, this._data) } } JGVue.prototype.update = function (vnode) { /** 将虚拟DOM渲染到页面中,diff算法(减少比较、减少操作)就在这里 * 简化:直接生成HTML DOM,再replaceChild到页面中 */ let realDOM = parseNode(vnode) this._parent.replaceChild(realDOM, document.querySelector('#root')) /** 每次会将页面中的DOM全部替换,会使每次ID都是新的,需要重新获取*/ } let app = new JGVue({ el: '#root', data: { name: { firstName: 'luyiyi' }, message: 'joaihia' } }) /** * 讨论问题 * 科里化:主要是用在判断是否为内置标签的地方,缓存了一部分行为 * 将标签生成一个Set对象,是内置标签值为true,否则为false * 只需要在第一次渲染的时候调用一次,后面就可以直接通过键名来获取值,如果是函数的话则需要每次都调用 * 因此能够提高一些性能 */ </script> </body> </html>