20230624----重返学习-vue-响应式处理思路-仿源码

45 篇文章 0 订阅
37 篇文章 0 订阅

day-098-ninety-eight-20230624-vue-响应式处理思路-仿源码

vue

vue大体概念

  • Vue是渐进式框架

    • 所谓渐进式框架,就是把一套全面的框架设计体系,拆分成为多个框架,项目中需要用到那些需求,再导入对应的框架,以此来保证外部资源的最小化!
  • Vue2全家桶

    • Vue@2:vue框架的核心!含单个组件状态管理、组件的管理。
      • vue-cli:用于创建项目的脚手架工具。管控webpack等打包功能。
    • vuex@3:实现vue组件间的公共状态管理。
      • vuex-persist 公共状态持久化存储插件。
    • vue-router@3:SPA单页面应用中的路由管理!
    • UI组件库:
      • PC端:饿了么团队element-ui、阿里antd of vue@1、京东iview。
      • 移动端:有赞vant@2、蚂蚁金服cube…
  • Vue3全家桶

    • vue@3
      • vite:用于创建项目的脚手架工具。
    • vuex@4、pinia
    • vue-router@4
    • UI组件库:
      • PC端:element-plus、antd of vue@3…
      • 移动端:vant@3…
  • Vue生态中,完善的项目解决方案:

vue常见面试题

  • Vue2框架常见的面试题
    1. 谈谈你对 MVVM / MVC 模式的理解
    2. Vue2框架怎么实现对象和数组的监听?「Vue2响应式原理」
    3. v-model指令实现的原理
    4. v-show 与 v-if 有什么区别?
    5. Class 与 Style 如何动态绑定?
    6. computed 和 watch 的区别和运用的场景?
    7. 谈谈你对 Vue2 生命周期的理解?
    8. Vue怎么用 vm.$set() 解决对象新增属性不能响应的问题 ?
    9. 开发中常用的Vue指令有哪些

MVVM与MVC

  • 面试题:谈谈你对 MVVM / MVC 模式的理解?
    • MVVM模式:双向数据驱动,如Vue2与Vue3。

      • model:数据层。

        • 在数据层,我们需要构建出:项目中需要的各种数据与方法。例如:响应式状态、属性、计算属性、监听器、过滤器、方法、钩子函数…
          • 说明:
            • 在vue2中:基于OptionsAPI(配置项)方式,来管理这些内容。

              export default {
                data(){ return { ... } },
                props:[...],
                computed:{},
                watch:{},
                filters:{},
                methods:{},
                ...
              }
              
              <script>
              export default {
                data(){ return { ...响应式状态 } },
                props:[...属性],
                computed:{...计算属性},
                watch:{...监听器},
                filters:{...过滤器},
                methods:{...方法},
                ...
              }
              </script>
              
            • vue3中:基于CompositionAPI(聚合式)&函数式编程方式,来管理这些内容。

      • view 视图层。

        • 视图层的原理:在Vue框架中,我们基于<template>jsx语法构建需要的视图,最后把视图编译为VirtualDOM(虚拟DOM),再经过DOM-diff进行差异化对比,最后把VirtualDOM/补丁包渲染为真实的DOM
          • 步骤说明:
            1. 基于<template>jsx语法构建需要的视图。
              • 这个主要是用户自己手写的,绑定响应式数据与绑定事件。
              • 还基于指令控制数据与视图的联系。
            2. 视图编译为VirtualDOM
              • 在vue2中:基于vue-template-compiler插件,把视图编译为VirtualDOM
              • 在vue3中:基于@vue/compiler-sfc插件,把视图编译为VirtualDOM
            3. 经过DOM-diff进行差异化对比。
              • 这个是vue内部做的,diff算法。
            4. VirtualDOM/补丁包渲染为真实的DOM
              • 渲染周期步骤:
                • 第一次渲染VirtualDOM直接渲染为真实的DOM
                • 非初次渲染补丁包渲染为真实的DOM
                  • 补丁包是通过DOM-diff这一步来对比新旧数据来生成的。性能好,能更快渲染。
              • 这一步基本上都是vue内部做的。
      • viewModel:监听层,Vue框架的核心。

        1. 这个是vue框架内部自动做的。正常不用关心。
        • 监听响应式数据的变化,当数据发生改变后,通知视图更新。
          • Vue2中基于Object.defineProperty对数据进行劫持。
          • Vue3中基于ES6中的Proxy对数据进行劫持。
          • 基于观察者模式通知视图更新。
        • 监听视图的变化(一般指的是Form表单内容的改变),当视图内容改变后,自动修改对应的数据(数据一改,视图紧接着跟着更新)。
          • 监听视图变化主要是基于v-model指令。
    • MVC模式:单向数据驱动框架,如React。

      • model 数据层。

        • 构建项目中需要的数据和方法。例如:状态、属性、钩子函数、普通函数等。
          • 类组件中:基于 state/props/实例 构建状态和属性。
          • 函数组件中:基于useState/useEffectHooks函数,完成上述内容的管理。
      • view 视图层。

        • 在React中,基于jsx语法构建需要的视图React会基于babel-preset-react-appjsx语法编译为React.createElement格式createElement方法执行,会创建出对应的VirtualDOM,经过DOM-diff对比,最后把VirtualDOM/补丁包,基于ReactDOM中的render方法,渲染为真实的DOM
      • controller 控制层。

        • 实现事件绑定和相关的业务逻辑。
        • React框架实现了数据更改可以让视图自动更新的机制。但是React不同于Vue,并没有对状态做数据劫持。如果打算修改状态后,让视图更新,需要基于特定的方法去修改状态才可以!
          • 类组件中:可以用setState/forceUpdate方法。
          • 函数组件中,可以用useState等Hook函数。
        • 但是React中默认并没有实现对视图的监听,这样导致,视图内容改变,对应的状态也不会自动更改!
          • 不过我们可以自己给表单元素做事件绑定,当内容改变后,手动去修改对应的状态。
    • 总结:无论是MVVM还是MVC,都是目前前端主流的框架思想,都是以数据驱动视图渲染核心,告别传统直接操作DOM的方式,转而操作VirtualDOM!再配合对应的生态体系,让项目开发既高效,又提高了性能!…

Vue的学习路线

  • 如何学习Vue?
    • 第一条线:视图线
      • <template>JSX语法
        • 指令「内置的14个指令和自定义指令」
        • JSX语法
      • VirtualDOM编译的机制
      • 掌握DOM-DIFF算法
    • 第二条线:数据线
      • 学习 OptionsAPI/CompositionAPI 中的:语法、原理、区别等内容
        1. OptionsAPI选项
      • 学习 MVVM 的原理
        • 数据是如何被监听的「Vue2和Vue3是不一样的」
        • 监听数据变化后,如何通知视图更新「观察者模式」
        • 如何监听视图的变化,以及如何让状态跟着更改「v-model」
    • 第三条线:组件化开发
      • 单文件组件「含样式私有化方案的原理」
      • 类组件和函数组件
      • 复合组件通信
      • 组件封装的技巧「各种封装技巧」
        • 通用业务组件
        • UI组件库的二次封装
        • 通用功能组件
        • Vue.mixin
        • Vue.directive
        • Vue.extend
    • 第四条线:实战线
      • vuex / vue-router
      • <keep-alive>
      • <transition>
      • <component>
      • 上拉刷新、下拉加载
      • 超长列表性能优化
      • 登录/权限管理模型
      • 前后端数据通信管理方案

OptionsAPI选项式数据

  1. OptionsAPI选项-数据
  2. OptionsAPI选项-DOM
  3. OptionsAPI选项-生命周期钩子
  4. OptionsAPI选项-资源
  5. OptionsAPI选项-组合
  6. OptionsAPI选项-其它

对象和数组的监听

  • 面试题:Vue2框架怎么实现对象和数组的监听?「Vue2响应式原理」

数据初始化

  • vue2源码在/node_modules/vue/dist/vue.js中。
  • new Vue()的时候,OptionsAPI中的data是用来构建响应式数据-即状态的。
    • 特点
      1. 在data中构建的状态,会直接挂载到实例上。
        • 在js中,可以基于实例去访问对应的状态 -> vm.msg/this.xxx;
        • 而挂载到实例上的信息,可以直接在视图中访问 -> {{msg}};
      2. 在data中构建的状态,会被进行数据劫持,即get/set。数据劫持的目的是让其变为响应式的,这样以后修改此状态信息,会触发set劫持函数,在此劫持函数中,不仅修改了状态值,而且还会通知视图更新!
        - 只有在new的时候,写在data中的状态,才会默认被数据劫持,变为响应式状态。
    • Vue2响应式源码:
      1. new Vue()后,首先执行Vue.prototype._init方法,在此方法中做了很多事情,例如:
        • 向实例上挂载很多内置的私有属性。
          • 带$xxx是我们开发者后续要用到的。
          • 带_xxx是给Vue内部用的。
        • 基于callHook$1方法,触发beforeCreate()钩子函数执行。
        • 初始化上下文中的信息。
        • 执行initState方法,初始化属性、状态、计算属性、监听器等信息。
        • 触发created钩子函数执行。
      2. 执行initState方法的时候
        • 基于initProps$1初始化属性。注册接收属性与属性规则校验。
        • 基于initMethods初始化普通函数。
        • 基于initComputed$1初始化计算属性。
        • 基于initWatch初始化监听器。
        • 基于initData初始化状态。
      3. 执行initData方法的时候,主要目的就是初始化状态-也就是把信息做响应式数据劫持。
        • 先判断data是否是一个函数(组件中的data都是函数),如果是函数,先把函数执行(函数中的this是实例,并且传递实例),把执行的返回值,重新赋值给data。

          var data = vm.$options.data;
          data = vm._data = isFunction(data) ? getData(data, vm) : data || {};
          
        • 接下来要确保data是一个纯粹的对象。

          if (!isPlainObject(data)) {
            data = {};
            warn$2('data functions should return an object:\n' + 'https://v2.vuejs.org/v2/guide/components.html#data-Must-Be-a-Function', vm);
          }
          
        • 然后基于Object.keys方法,获取data对象中的可枚举、非Symbol类型的私有属性,然后判断这些属性,是否出现在methods和props中,如果出现了则报错!原因:methods/pprops中编写的信息,也会直接挂在实例上,如果名字,则相互冲突了!

          var keys = Object.keys(data);
          var props = vm.$options.props;
          var methods = vm.$options.methods;
          var i = keys.length;
          while (i--) {
            ...
          }
          
          var keys = Object.keys(data);
          var props = vm.$options.props;
          var methods = vm.$options.methods;
          var i = keys.length;
          while (i--) {
            var key = keys[i];
            {
                if (methods && hasOwn(methods, key)) {
                    warn$2("Method \"".concat(key, "\" has already been defined as a data property."), vm);
                }
            }
            if (props && hasOwn(props, key)) {
                warn$2("The data property \"".concat(key, "\" is already declared as a prop. ") +
                        "Use prop default value instead.", vm);
            }
            else if (!isReserved(key)) {
                proxy(vm, "_data", key);
            }
          }
          
        • 最后基于observe函数,对data对象中的信息进行数据劫持!

          var ob = observe(data);
          ob && ob.vmCount++;
          
        • 学习总结:真实项目中,建议把状态数据,全部事先写在data中(即便不清楚其值,也先写上,可以赋值初始值)。因为只有写在data中的数据,在最开始渲染阶段,才会被做响应式的数据劫持

      4. 执行observe方法的时候,把data对象传递进去。
        • 如果data对象已经被处理过,则不会重新处理。

          if (value && hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
            return value.__ob__;
          }
          
        • 而且data对象必须符合好多条件,才可以去处理:是数组或者对象、并且没有被冻结/密封/阻止扩展、并且不是ref对象,也不是VirtualDOM(vnode)…

          if (shouldObserve &&
            (ssrMockReactivity || !isServerRendering()) &&
            (isArray(value) || isPlainObject(value)) &&
            Object.isExtensible(value) &&
            !value.__v_skip /* ReactiveFlags.SKIP */ &&
            !isRef(value) &&
            !(value instanceof VNode)) {
            return new Observer(value, shallow, ssrMockReactivity);
          }
          

          简洁处理:

          if (... && (isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value.__v_skip  && !isRef(value) && !(value instanceof VNode)) {
            return new Observer(value, shallow, ssrMockReactivity);
          }
          
        • 如果符合了全部条件,则创建Obsever类的实例,把data对象传递进去进行处理。

        • 学习总结:如果某个写在data中的对象,我们不期望对其内部做劫持处理,此时我们只需要把这个对象基于Object.freeze()冻结即可!

          • 因为劫持处理的过程是需要消耗性能和时间的。
            • 例如:从服务器获取的数据,我们并没有修改其内部某一项值),让视图更新的需求,那么这些数据压根就不需要做劫持。
      5. 执行new Observer(data),对data对象中的每一项进行数据劫持
        • 但凡被处理过的对象,都会设置一个__ob__属性,属性值是Observer类的实例。

          def(value, '__ob__', this);
          
        • 然后判断data是数组还是对象,两者处理的方式是不一样的。

          • 如果是对象:

            • 基于Object.keys()获取对象所有可枚举、非Symbol类型的私有属性。

            • 然后迭代这些成员,对每一个成员,基于defineReactive()做数据劫持。

              var keys = Object.keys(value);
              for (var i = 0; i < keys.length; i++) {
                  var key = keys[i];
                  defineReactive(value, key, NO_INIITIAL_VALUE, undefined, shallow, mock);
              }
              
          • 如果是数组:

            • 在Vue2中,有一个对象arrayMethods,这个对象的特点:
              • 对象中有7个方法:push/pop/shift/unshift/splice/sort/reverse;
              • 对象.__proto__指向Array.prototype
            • 接下来让data这个数组,拥有arrayMethods上的这七个方法。
              • 在非IE浏览器中,就是让 data数组.__proto__=arrayMethods
              • 在IE浏览器中,迭代arrayMethods中的每一个方法,把这些方法作为data数组的私有方法。
            • 当我们以后调用数组这7个方法的时候,用的都是arrayMethods中的这七个方法。
              • 调用重写的这7个方法,其内部:
                • 获取传递的实参。
                • 基于Array.prototype内置的方法实现对应的功能。
                • 如果调用的是push/unshift/splice,需要把新增的内容,基于observeArray进行递归处理,实现深度的监听劫持。
                • 最后通知视图更新。
            • 执行observeArray对传递的data数组,再次进行递归处理。
      6. 在defineReactive函数中。
        • 首先又对此对象中的某个成员进行校验,验证是否是冻结/密封的,如果是,则不进行数据劫持。

          var property = Object.getOwnPropertyDescriptor(obj, key);
          if (property && property.configurable === false) {
              return;
          }
          
        • 然后对对象中此成员的值,进行递归处理,目的是进行尝试的监听劫持!

          var childOb = !shallow && observe(val, false, mock);
          
        • 最后基于Object.defineProperty()对此对象中的这个成员做get/set劫持。

      7. 在observeArray方法中:
        • 迭代数组中的每一项,对每一项再基于observe进行递归处理,实现深度的监听劫持。

          for (var i = 0, l = value.length; i < l; i++) {
              observe(value[i], false, this.mock);
          }
          
          • 深度的监听劫持:

            let vm = new Vue({
              data: {
                msg: "哈哈",
                obj:{
                  x:10,
                }
              },
            });
            
            • 不仅对msgobj做了数据劫持,还对obj这个对象的x属性也做了数据劫持
      • 总结:Vue2响应式原理,针对数组和对象,有不同的处理情况:
        • 如果是对象:基于Object.defineProperty对对象中的每个成员(成员特点是可枚举、非Symbol类型),进行深度的监听劫持。
          • 当修改成员值的时候,触发set劫持函数。在set函数中,不仅修改了成员值,而且还对新修改的值做监听劫持。最主要的是通知视图更新!
        • 如果是数组:并不像对象一样,没有对数组中的每个索引项做监听劫持。所以基于索引修改数组某一项的值,视图是不会更新的。而是重写了数组的7个方法-push/shift/unshift/pop/splice/sort/reverse,基于这7个方法修改数组的内容,不仅仅修改了内容,而且对新修改的内容也会做劫持,也会通知视图的更新!最后对数组中的每一项内容,也基于递归的方式,看看是否需要劫持!
      • 代码示例:
        • fang/f20230624/day0624/test.html

          <!DOCTYPE html>
          <html>
            <head>
              <meta charset="UTF-8" />
              <title>Document</title>
            </head>
            <body>
              <div id="app">{{msg}}-{{text}}</div>
            </body>
          </html>
          <!-- <script src="./node_modules/vue/dist/vue.min.js"></script> -->
          <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
          <script>
            let vm = new Vue({
              data: {
                msg: "哈哈",
              },
            });
            vm.text = "嘿嘿";
            console.log(`实例:vm-->`, vm);
          
            setTimeout(() => {
              vm.text = "hhh";
              console.log(2000, `text改值了,但视图并没有自动更新`);
            }, 2000);
            setTimeout(() => {
              vm.msg = "方";
              console.log(10000, `msg改值了,但视图会自动更新`);
            }, 10000);
          
            vm.$mount("#app");
          </script>
          

响应式处理思路-仿源码

  • fang/f20230624/day0624/test.html

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="UTF-8" />
        <title>Document</title>
      </head>
      <body>
        <div id="app">{{msg}}-{{text}}</div>
      </body>
    </html>
    <!-- <script src="./node_modules/vue/dist/vue.min.js"></script> -->
    <!-- <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script> -->
    <script>
      // let vm = new Vue({
      //   data: {
      //     msg: "哈哈",
      //     obj:{
      //       x:10,
      //     }
      //   },
      // });
      // vm.text = "嘿嘿";
      // console.log(`实例:vm-->`, vm);
    
      // setTimeout(() => {
      //   vm.text = "hhh";
      //   console.log(2000, `text改值了,但视图并没有自动更新`);
      // }, 2000);
      // setTimeout(() => {
      //   vm.msg = "方";
      //   console.log(10000, `msg改值了,但视图会自动更新`);
      // }, 10000);
    
      // vm.$mount("#app");
    </script>
    
    <script src="./test.js"></script>
    
  • fang/f20230624/day0624/test.js

    // 检测是否为纯粹对象。
    const toString = Object.prototype.toString;
    const isPlainObject = function isPlainObject(obj) {
      if (toString.call(obj) !== "[object Object]") return false;
      let proto = Object.getPrototypeOf(obj);
      if (!proto) return true;
      let Ctor = "constructor" in obj && obj.constructor;
      return Ctor === Object;
    };
    
    // 给对象设置不可枚举的属性。
    const define = function define(obj, key, value) {
      Object.defineProperty(obj, key, {
        value,
        enumerable: false,
        writable: true,
        configurable: true,
      });
      return obj;
    };
    
    // 通知视图更新的方法。
    const notify = function notify() {
      console.log(`视图更新`);
    };
    
    // 重写数组7个方法的对象。
    const arrayProto = Array.prototype;
    const arrayMethods = Object.create(arrayProto);
    let methods = ["push", "pop", "shift", "unshift", "splice", "sort", "reverse"];
    methods.forEach((method) => {
      let original = arrayProto[method]; //对应Array.prototype上的内置方法。
      define(arrayMethods, method, function mutator(...args) {
        // 基于内置的方法,把功能先实现。this-我们要操作的数组
        let result = original.call(this, ...args);
    
        // 对于新增或修改的信息,需要基于递归,进行深层次的监听劫持。
        let inserted;
        switch (method) {
          case "push":
          case "unshift":
            inserted = args;
            break;
          case "splice":
            inserted = args.slice(2);
            break;
          default:
            break;
        }
        if (inserted) {
          observeArray(inserted);
        }
    
        // 通知视图更新。
        notify();
    
        return result;
      });
    });
    // ary.push(100, 200, 300);
    
    // 数据劫持的处理。
    const defineReactive = function defineReactive(obj, key, proxy) {
      // 对成员的规则再次校验。
      let property = Object.getOwnPropertyDescriptor(obj, key);
      if (property && property.configurable === false) {
        return;
      }
    
      // 对此成员的值进行深度处理。
      observe(obj[key]);
    
      // 对此成员进行数据劫持。
      Object.defineProperty(obj, key, {
        get: function reactiveGetter() {
          return proxy[key];
        },
        set: function reactiveSetter(newVal) {
          // 新老值相同,则不进行任何的处理。
          if (Object.is(newVal, obj[key])) {
            return;
          }
    
          // 修改值
          proxy[key] = newVal;
    
          // 对新设置的值也要进行深度处理。
          observe(newVal);
    
          // 通知视图更新
          notify();
        },
      });
    };
    
    // 对数组中的每一项进行响应式处理。
    const observeArray = function observeArray(arr) {
      // 对传递数组中的每一项,都基于observe进行响应式处理。
      // debugger;
      arr.forEach((item) => {
        observe(item);
      });
    };
    // 对数组/对象进行响应式处理。
    const observe = function observe(data) {
      let isArray = Array.isArray(data);
      let isObject = isPlainObject(data);
    
      // 如果是数组/对象,并且不是被冻结/密封/阻止扩展的,我们才处理。
      if ((isArray || isObject) && Object.isExtensible(data)) {
        // 防止套娃操作。
        if (data.hasOwnProperty("__ob__")) {
          return data;
        }
        define(data, "__ob__", true);
    
        // 数组:重定向其原型指向 & 对数组每一项进行深度处理。
        if (isArray) {
          data.__proto__ = arrayMethods; // Object.setPrototypeOf(data, arrayMethods);
          observeArray(data);
        }
    
        // 对象:迭代对象中的每一项,对每一项都基于defineProperty进行数据劫持。
        if (isObject) {
          let keys = Object.keys(data);
          let proxy = { ...data };
          keys.forEach((key) => {
            defineReactive(data, key, proxy);
          });
        }
      }
      // console.log(`data-->`, data);
    
      return data;
    };
    
    // -----做测试。
    let data = {
      msg: "哈哈",
      obj: {
        x: 10,
        y: {
          z: [100, 200],
        },
      },
      arr: [1, 2, { n: 1000 }],
    };
    data.data = data;
    observe(data);
    
    console.log(`响应式数据:data-->`, data);
    
    setTimeout(() => {
      console.log(`data.arr[1] = "改"`);
      data.arr[1] = "改"; //视图不更新;
    }, 2000);
    setTimeout(() => {
      console.log(`data.msg = "改"`);
      data.msg = "改"; //视图更新;
    }, 1000);
    setTimeout(() => {
      console.log(`data.arr.push(4, 5, 6)`);
      data.arr.push(4, 5, 6); //视图更新;
    }, 5000);
    setTimeout(() => {
      console.log(`data.obj.x = "改了data.obj.x"`);
      data.obj.x = "改了data.obj.x"; //视图更新;
    }, 10000);
    
    // console.log(`data.arr-->`, data.arr);
    

进阶参考

  1. OptionsAPI选项-数据
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值