除了使用递归,还可以使用队列(深度优先转化为广度优先)

一、将属性转化为响应式的

1.首先需要使用Object.defineProperty()函数对每个属性进行get和set的转化
2.由于get和set之间需要一个中间变量来保存真实的值,所以将其封装在defineReactive()函数中,真正的数据存储在其局部变量value中
3.如果需要进行响应式的对象有多个需要循环,并且得判断属性值是否为数组
    1)是数组的话需要循环数组中每个元素,并对每个元素再进行判断是否为数组
    2)不是数组的话就调用封装defineReactive()函数对元素进行响应式化处理,defineProperty()函数对 对象 的某个属性进行转化,set和get,如果是引用属性的话需要递归对层级属性进行响应式处理,否则的话进行后面的操作
   function defineReactive(target, key, value, enumerable) {
      if (typeof value === 'object' && value != null && !Array.isArray(value)) {
        return reactify(value) /**  判断属性值是否为引用类型,引用类型的话需要递归*/
      }
      Object.defineProperty(target, key, {
        enumerable: !!enumerable,
        get() {
          return value
        },
        set(newVal) {
          value = newVal;
        }
      })
    }

    /** 将对象进行响应式化*/
    function reactify(o) {
      let keys = Object.keys(o);
      for (let i = 0; i < keys.length; i++) {
        let key = keys[i];
        let value = o[keys[i]];
        /** 如果值是数组的话,需要循环元素进行响应式化*/
        if (Array.isArray(value)) {
          for (let j = 0; j < value.length; j++) {
            reactify(value[j])
          }
        } else {
          defineReactive(o, key, value, true);/** 内部会判断是否为引用类型 是的话需要递归操作*/
        }
      }
    }


二、对数组的方法进行拦截


       1. 缺陷:如果使用数组的方法添加、删除....等操作是不会有响应式的操作的
        2.解决:对于对象可以使用递归进行响应式化,而对于数组也需要进行一些处理
          比如:push pop shift unshift reverse sort splice
        3.要做的事情:
          1)在改变数组数据的时候,需要发出通知,在Vue2.x中数组发送变化设置length没法通知(没有实现这个行为,在Vue 3.x中使用Proxy语法ES6的语法解决了这个问题)
          2)加入的元素应该变成响应式的
        4.技巧:如果一个函数已经定义了,但是需要扩展功能,一般的处理方法:
          1)使用一个临时的函数名存储函数
          2)重新定义原来的函数
          3)定义扩展的功能
          4)调用临时的那个函数
let ARRAY_METHOD = [
  'push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice'
]
  let array_methds = Object.create(Array.prototype);/** 继承自Array原型*/
  ARRAY_METHOD.forEach(method => {
    array_methds[method] = function () {/** 重写array_methds上之前定义的方法*/
        /** 将方法中传进来的每个数据进行响应式化处理*/
      for (let i = 0; i < arguments.length; i++) {
        reactify(arguments[i])
      }
      /** 调用原来的方法,改变函数的调用对象,并传入方法传入的参数*/
      let length = Array.prototype[method].apply(this, arguments);
      return length;
    }
  })

三、*响应式* 结合之前 *数据驱动* 的完整代码

<!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;
      reactify(this._data, this)/** 将Vue实例传入,后面需要使用到*/

      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;
    }

    /**
     * 将虚拟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;
    }

    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());/** 会触发get*/
        });
        _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;
    }



    /** 1.根据Vue构造函数中的内容生成虚拟DOM*/
    JGVue.prototype.mount = function () {
      this.render = this.createRenderFn()
      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)
      console.log(ast)
      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都是新的,需要重新获取
       * Vue使用了diff算法,只会进行局部替换
      */
    }


    /** 响应式原理部分*/
    let ARRAY_METHOD = [
      'push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice'
    ]

    let array_methds = Object.create(Array.prototype);/** 继承自Array原型*/
    ARRAY_METHOD.forEach(method => {
      array_methds[method] = function () {/** 重写array_methds上之前定义的方法*/
        /** 将方法中传进来的每个数据进行响应式化*/
        for (let i = 0; i < arguments.length; i++) {
          reactify(arguments[i])
        }

        /** 调用原来的方法,改变函数的调用对象,并传入方法传入的参数*/
        let length = Array.prototype[method].apply(this, arguments);
        return length;
      }
    })

    function defineReactive(target, key, value, enumerable) {
      /** 之前通过call调用,因此this就是vue实例*/
      let that = this;
      if (typeof value === 'object' && value != null && !Array.isArray(value)) {
        return reactify(value, that)
      }
      Object.defineProperty(target, key, {
        enumerable: !!enumerable,
        get() {
          console.log('get:' + value)
          return value;
        },
        set(newVal) {
          value = newVal;
          console.log(that)
          that.mountComponent();/** 刷新模板*/
          console.log('set:' + newVal)
        }
      })
    }
    function reactify(o, vm) {
      let keys = Object.keys(o);
      for (let i = 0; i < keys.length; i++) {
        let key = keys[i];
        let value = o[keys[i]];
        if (Array.isArray(value)) {
          value.__proto__ = array_methds;/** 数组响应式 */
          for (let j = 0; j < value.length; j++) {
            reactify(value[j], vm)
          }
        } else {
          defineReactive.call(vm, o, keys[i], o[keys[i]], true);
        }
      }
    }


    let app = new JGVue({
      el: '#root',
      data: {
        name: {
          firstName: 'luyiyi'
        },
        message: 'joaihia'
      }
    })
  </script>
</body>

</html>

未解决的问题:对象重新赋值为另一个对象后,就不是响应式的了