一、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>