Vue源码分析-响应式原理

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

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

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>

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

全部评论

相关推荐

03-05 17:03
已编辑
浙江工商大学 C++
陈好好wy:整体看下来有点空空的感觉,可以把每一段项目经历都再完善一下,然后用小标题的形式写个两到三条,目前看有点太简单了,不太能看出具体在这个项目里做了什么工作。还是要尽量把自己做的工作以量化的形式体现在简历上呢。
双非本科求职如何逆袭
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
正在热议
更多
# 长得好看会提高面试通过率吗? #
5492次浏览 54人参与
# 百度工作体验 #
316382次浏览 2233人参与
# 米连集团26产品管培生项目 #
7751次浏览 236人参与
# 沪漂/北漂你觉得哪个更苦? #
1908次浏览 46人参与
# 离家近房租贵VS离家远但房租低,怎么选 #
16962次浏览 137人参与
# 春招至今,你的战绩如何? #
17005次浏览 154人参与
# 巨人网络春招 #
11603次浏览 232人参与
# 你的实习产出是真实的还是包装的? #
3660次浏览 61人参与
# HR最不可信的一句话是__ #
1223次浏览 34人参与
# AI面会问哪些问题? #
1120次浏览 30人参与
# 你做过最难的笔试是哪家公司 #
1459次浏览 24人参与
# AI时代,哪个岗位还有“活路” #
3186次浏览 56人参与
# 不考虑薪资和职业,你最想做什么工作呢? #
153005次浏览 889人参与
# 简历第一个项目做什么 #
32276次浏览 371人参与
# 军工所铁饭碗 vs 互联网高薪资,你会选谁 #
8080次浏览 44人参与
# 简历中的项目经历要怎么写? #
311392次浏览 4289人参与
# XX请雇我工作 #
51168次浏览 171人参与
# 投格力的你,拿到offer了吗? #
178463次浏览 891人参与
# 你最满意的offer薪资是哪家公司? #
77053次浏览 375人参与
# AI时代,哪些岗位最容易被淘汰 #
65091次浏览 921人参与
# 秋招白月光 #
731753次浏览 5441人参与
# 当下环境,你会继续卷互联网,还是看其他行业机会 #
187722次浏览 1123人参与
牛客网
牛客网在线编程
牛客网题解
牛客企业服务