3. vue基础 ②

211 阅读5分钟

一、深入研究data属性以及数据响应式实现

1.1 data属性

① data必须是一个函数

  • Vue在创建实例的过程中调用data函数,返回数据对象
  • 通过响应式包装后存储在实例的$data上,并且实例可以直接越过$data访问属性
const app = Vue.createApp({
  data(){
    return {
      title: 'This is my TITLE'
    }
  },
  template: `
    <h1>{{ title }}</h1>
  `
});

const vm = app.mount('#app');

console.log(vm);
// vm 和 vm.$data存储了相同的数据
console.log(vm.$data.title);// 'This is my TITLE'
console.log(vm.title);// 'This is my TITLE'

vm.author = 'hezi';
console.log(vm);// 直接给实例加属性,$data上没有

vm.$data.author = 'hezi';
console.log(vm);// 实例和$data都有,但是没有在实例上不能render渲染,还是要在data中定义

image.png

② data为什么必须要是一个函数

  • 如果不是一个函数就会报类型错误:dataOptions.call is not a function

1.2 vue2数据响应式实现

情况一:data是一个函数

var vm = new Vue({
  data() {
    return {
      a: 1,
      b: 2
    }
  }
});

function Vue(options) {
  this.$data = options.data();

  var _this = this;

  // this.a => this.$data.a

  for (var key in this.$data) {
    (function (k) {
      // 独立的作用域
      // k => 当前作用域的临时的局部变量
      Object.defineProperty(_this, k, {
        get: function () {
          return _this.$data[k];
        },
        set: function (newValue) {
          _this.$data[k] = newValue;
        }
      })
    })(key);
  }
}

此时访问a,改变b成功

console.log(vm.a);
vm.b = 3;
console.log(vm.b);

image.png

情况二:data是一个对象

但是当我们把data改为对象的时候,共用同一个data内存地址,当一个实例修改值的时候,另一个实例的data也会被修改

  • 所以,data必须是一个函数,保证每次返回不同的地址实例之间不会冲突
  • 如果硬要data是一个对象,那么在挂载到实例的$data的时候必须深拷贝data即可
// 不是函数就去掉括号
this.$data = options.data;

var data = {
  a: 1,
  b: 2
}
var vm1 = new Vue({
  data: data
});

var vm2 = new Vue({
  data: data
});

vm1.b = 3;

console.log(vm1, vm2);

image.png 深拷贝

function Vue(options) {
  this.$data = deepClone(options.data);

  var _this = this;

  // this.a => this.$data.a

  for (var key in this.$data) {
    (function (k) {
      // 独立的作用域
      // k => 当前作用域的临时的局部变量
      Object.defineProperty(_this, k, {
        get: function () {
          return _this.$data[k];
        },
        set: function (newValue) {
          _this.$data[k] = newValue;
        }
      })
    })(key);
  }
}

function deepClone(origin, target) {
  // target是否存在 如果不存在创建空对象
  var tar = target || {},
    // 判断是否为引用数据类型
    toStr = Object.prototype.toString,
    arrType = '[object Array]';

  // 循环获取对象的key
  for (var k in origin) {
    // 排除origin的公有属性
    if (origin.hasOwnProperty(k)) {
      // 判断是一个对象
      if (typeof origin[k] === 'object' && origin[k] !== null) {
        tar[k] = toStr.call(origin[k]) === arrType ? [] : {};
        // 再次深克隆
        deepClone(origin[k], tar[k]);
      } else {
        // 如果不是对象就直接复制
        tar[k] = origin[k];
      }
    }
  }
  return tar;
}

var data = {
  a: 1,
  b: 2
}
var vm1 = new Vue({
  data: data
});

var vm2 = new Vue({
  data: data
});

vm1.b = 3;

console.log(vm1, vm2);

image.png

情况三:可以代替Object.defineProperty的方法(拓展)

Object.defineProperty在IE8只支持DOM,可以用Mozilla出的__defineGetter____defineSetter__两个方法,兼容性好

  • defineProperty是Object构造函数上的静态方法
  • __defineGetter__是实例继承Object.prototype上的方法
function Vue(options) {
  this.$data = deepClone(options.data);

  var _this = this;

  // this.a => this.$data.a

  for (var key in this.$data) {
    (function (k) {
      // 独立的作用域
      // k => 当前作用域的临时的局部变量
      // Object.defineProperty(_this, k, {
      //   get: function () {
      //     return _this.$data[k];
      //   },
      //   set: function (newValue) {
      //     _this.$data[k] = newValue;
      //   }
      // })

      // firefox开发的方法 实例继承Object上的方法
      _this.__defineGetter__(k, function (){
        return _this.$data[k];
      });
      _this.__defineSetter__(k, function (newValue){
        _this.$data[k] = newValue;
      })
    })(key);
  }
}

function deepClone(origin, target) {
  // target是否存在 如果不存在创建空对象
  var tar = target || {},
    // 判断是否为引用数据类型
    toStr = Object.prototype.toString,
    arrType = '[object Array]';

  // 循环获取对象的key
  for (var k in origin) {
    // 排除origin的公有属性
    if (origin.hasOwnProperty(k)) {
      // 判断是一个对象
      if (typeof origin[k] === 'object' && origin[k] !== null) {
        tar[k] = toStr.call(origin[k]) === arrType ? [] : {};
        // 再次深克隆
        deepClone(origin[k], tar[k]);
      } else {
        // 如果不是对象就直接复制
        tar[k] = origin[k];
      }
    }
  }
  return tar;
}

var data = {
  a: 1,
  b: 2
}
var vm1 = new Vue({
  data: data
});

var vm2 = new Vue({
  data: data
});

vm1.b = 3;

console.log(vm1, vm2);

image.png

进一步优化初始化data数据

var vm = new Vue({
  data() {
    return {
      a: 1,
      b: 2
    }
  }
});

function Vue(options) {
  var data = options.data;

  data = typeof data === 'function' ? data.call(this) : data;
  this.$data = data;

  // observe(data) 对数据进行劫持

  var _this = this;

  for (var key in this.$data) {
    (function (k) {
      Object.defineProperty(_this, k, {
        get: function () {
          return _this.$data[k];
        },
        set: function (newValue) {
          _this.$data[k] = newValue;
        }
      })
    })(key);
  }
}

console.log(vm.a);
vm.b = 3;
console.log(vm.b);

1.3 Vue2数据驱动视图渲染的底层机制:

1. 基于Object.defineProperty对data中的数据/状态,进行监听劫持「get/set」;当修改状态
值,会触发set函数;在set函数中,除了修改了状态值,而且还会通知视图重新渲染!!

2. 在“new Vue的时候”
● 会把data中设定的响应式数据,挂载到实例的私有属性上
    ○ 可以基于 vm.xxx(this.xxx)去访问这些状态
    ○ 挂载到实例上的数据,是可以在视图中直接渲染的 -> {{msg}}
● 对data中的数据及内部深层次对象中的成员,基于Object.defineProperty进行深度的监听劫
持!!
    ○ 数组比较特殊,数组本身会被劫持,但是数组中的每一个索引不会被劫持!
    ○ Vue2是重构了数组的原型链指向「数组 -> 重构的原型对象 -> Array.prototype」,在重
    构的原型对象中,重写了7个方法「pop/push/shift/unshift/splice/sort/reverse」,而
    基于这7个方法去修改数组中某一项的值,不仅会修改值,而且还会通知视图更新!!

3. 只有在new Vue期间,写在data中的内容才会被劫持,自己单独向实例上挂载的数据不会被
劫持所以在使用vue开发的时候,我们建议把所需要的数据,全部在data中进行声明(哪怕此时
还不知道啥值,也可以用null先占位),目的是让其先变为响应式的数据(也就是做了劫持),
这样后期再修改值的时候,才能通知视图更新!!
    ● 但是可以基于Vue.prototype上的$forceUpdate/$set实现更新!!
    ● $forceUpdate:强制更新
    ● $set:给对象的某个成员设置值,设置完毕后,可以通知视图渲染
        ○ 不能操作实例对象 vm.$set(vm, xxx, xxx) -> 报错
        ○ 如果对象中已经具备这个成员(且非响应式的),基于$set只能改其值,但是不会让视图
        更新!
        ○ 如果不具备这个成员,基于$set是给对象,新设置一个,做了监听劫持的成员,并且让
        视图更新!!
        ○ 但是基于$set去修改数组中某一项的值,也是可以触发视图更新的!!

1.4 手写数据劫持

image.png

1.4.1 vue/index.js

import {initState} from "./init";

function Vue(options) {
    this._init(options);
}

Vue.prototype._init = function (options) {
    var vm = this;
    vm.$options = options;

    // 初始化各种状态
    initState(vm);
    //...
}

export default Vue;

1.4.2 init.js

import proxyData from "./proxy";
import observe from './observe'

function initState(vm) {
    var options = vm.$options;

    if (options.data) {
        // 初始化data
        initData(vm);
    }
}

function initData(vm) {
    //data可能是函数和对象
    var data = vm.$options.data;
    // 我将返回的对象放到了_data上
    vm._data = data = typeof data === 'function' ? data.call(vm) : data || {};
    // 将vm._data 用vm来代理就可以了
    for (var key in data) {
        // 做代理
        proxyData(vm, '_data', key);
    }
    // 对数据进行劫持 vue2里采用了一个api defineProperty
    observe(vm._data);
}


export {
    initState
}

1.4.3 proxy.js

function proxyData(vm, target, key) {

    Object.defineProperty(vm, key, {
        get() {
            // vm.title -> vm._data.title
            return vm[target][key];
        },
        set(newValue) {
            vm[target][key] = newValue;
        }
    });
}

export default proxyData;

1.4.4 observe.js

import Observer from "./observer";

function observe(data) {
    if (typeof data !== 'object' || data === null) return;
    return new Observer(data);
}

export default observe;

1.4.5 observer.js

import defineReactiveData from "./reactive";
import { arrMethods } from "./array";
import observeArr from "./observeArr";

// 对象属性劫持
function Observer(data) {
    if (Array.isArray(data)) {
        // 需要保留数组原有的特性,并且可以重写部分方法
        // 会在数组的原型之前加一层,操作的时候先找自己写的7个方法
        data.__proto__ = arrMethods;

        //如果数组中放的是对象 可以监控到对象的变化
        observeArr(data);
    } else {
        this.walk(data)
    }
}

// 循环对象对属性依次劫持
Observer.prototype.walk = function (data) {
    // 拿到所有的 key 循环处理
    var keys = Object.keys(data);

    for(var i=0; i<keys.length; i++){
        var key = keys[i],
            value = data[key];

        // "重新定义"属性,defineReactiveData把某个数据重新定义成响应式的
        defineReactiveData(data, key, value);
    }
}

export default Observer;

1.4.6 reactive.js

import observe from "./observe";

// vue响应式原理defineProperty 深度属性劫持
function defineReactiveData(data, key, value) {
    observe(value);
    Object.defineProperty(data, key, {
        get() {
            console.log('响应式数据:获取:', value);
            return value;
        },
        set(newValue) {
            if (newValue === value) return;
            // value也可能是数组或对象需要递归观测
            // 赋值的时候也有可能是一个数组或对象
            observe(newValue);
            value = newValue;
            console.log('响应式数据:设置:', newValue);
        }
    });
}

export default defineReactiveData;

1.4.7 config.js

var ARR_METHODS = [
    "push",
    "pop",
    "shift",
    "unshift",
    "reverse",
    "sort",
    "splice"
];

export {
    ARR_METHODS
}

1.4.8 array.js

import {ARR_METHODS} from "./config";
import observe from "./observe";
import observeArr from "./observeArr";

var originArrMethods = Array.prototype,
    arrMethods = Object.create(originArrMethods);

ARR_METHODS.map(function (m) {
    // arr.push(...)
    // 重写数组的方法
    arrMethods[m] = function () {

        // 把arguments变为数组
        var args = Array.prototype.slice.call(arguments),
            // 功能用原来的方法完成
            result = originArrMethods[m].apply(this, args);
        console.log('数组新方法', args);
        // 我们需要对新增的再次进行劫持
        var inserted;

        switch (m) {
            case 'push':
            case 'unshift': // arr.unshift(1,2,3)
                inserted = args;
                break;
            case 'splice': // arr.splice(0,1,{a:1},{a:1})
                inserted = args.slice(2);
                break;
            default:
                break;
        }
        // console.log(inserted); // 新增的内容

        if (inserted) {
            observeArr(inserted);
        }
        return result;
    }
});

export {
    arrMethods
}

1.4.9 observeArr.js

import observe from "./observe";

function observeArr(arr) {
    for (let i = 0; i < arr.length; i++) {
        observe(arr[i]);
    }
}

export default observeArr;

1.4.10 要实现的vue代码 src/index.js

// webpack配置可以直接找根目录下的模块
import Vue from 'vue';

// options
let vm = new Vue({
    el: '#app',
    data() {
        return {
            title: '学生列表',
            classNum: 1,
            teacher: ['张三', '李四'],
            info: {
                a: {
                    b: 1
                }
            },
            students: [
                {
                    id: 1,
                    name: '小红'
                },
                {
                    id: 2,
                    name: '小明'
                }
            ]
        }
    }
});

当获取值时

image.png

image.png

当设置值时

image.png
image.png

vue实例 image.png

二、深入研究methods属性以及实例方法挂载实现

2.1 methods属性: 向组件实例添加方法

var app = Vue.createApp({
  data(){
    return {
      title: 'This is myTITLE'
    }
  },
  template: `
    <h1>{{ title }}</h1>
    <h2>{{ yourTitle() }}</h2>
    <button @click="changeTitle('This is your TITLE')">Change title</button>
  `,
  methods: {
    /**
     * 1. Vue创建实例时,会自动为methods绑定当前实例this
     *   保证在事件监听时,回调始终指向当前组件实例
     *   方法要避免使用箭头函数,箭头函数会阻止Vue正确绑定组件实例this
     */

    /**
     * @click="changeTitle('This is your TITLE')"
     * 
     * 函数名 + (): 不是执行符号,传入实参的容器
     * 
     * onclick = "() => changeTitle('This is your TITLE')"
     *   只有箭头函数执行,里面的才能执行
     * onClick = { () => changeTitle('This is your TITLE')}
     *   Vue是这样做的
     * 
     * onClick = { changeTitle.bind(this, This is your TITLE)}
     */
    changeTitle(title){
      this.title = title;
    },
    // 模板直接调用的方法尽量避免副作用操作
    yourTitle(){
      return 'This is your TITLE'
    }
  }
});

// 实例中直接挂载methods中的方法
const vm = app.mount('#app');

2.2 实例方法挂载实现

var Vue = (function () {

  function Vue(options) {
    this.$data = options.data();
    this._methods = options.methods;

    this._init(this);
  }

  Vue.prototype._init = function (vm) {
    initData(vm);
    initMethods(vm);
  }

  function initData(vm) {
    for (var key in vm.$data) {
      (function (key) {
        Object.defineProperty(vm, key, {
          get: function () {
            return vm.$data[key];
          },
          set: function (newValue) {
            vm.$data[key] = newValue;
          }
        })
      })(key);
    }
  }

  function initMethods(vm) {
    for (var key in vm._methods) {
      vm[key] = vm._methods[key];
    }
  }

  return Vue;
})();

var vm = new Vue({
  data() {
    return {
      a: 1,
      b: 2
    }
  },
  methods: {
    increaseA(num) {
      this.a += num;
    },
    increaseB(num) {
      this.b += num;
    },
    getTotal() {
      console.log(this.a + this.b);
    }
  }
});

vm.increaseA(1);
vm.increaseA(1);
vm.increaseA(1);
vm.increaseA(1);
// a 5

vm.increaseB(2);
vm.increaseB(2);
vm.increaseB(2);
vm.increaseB(2);
// b 10

vm.getTotal(); // 15
console.log(vm);

2.3 methods处理全选和非全选

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>methods处理全选</title>
</head>

<body>
  <div id="app">
    <input type="checkbox" value="OK" v-model="all" @click='handle'>全选/全不选
    <br />
    <div @change='delegate'>
      <input type="checkbox" value="song" v-model='hobby'>唱歌
      <input type="checkbox" value="dance" v-model='hobby'>跳舞
      <input type="checkbox" value="read" v-model='hobby'>读书
      <input type="checkbox" value="javascript" v-model='hobby'>编程
    </div>
    <button @click='submit'>提交</button>
  </div>
</body>

</html>
<script src="../node_modules/vue/dist/vue.js">
  let vm = new Vue({
    el: '#app',
    data: {
      hobby: ['javascript'],
      all: []
    },
    methods: {
      handle() {
        //=>click事件处理比视图更新后数据的更改要先去做
        if (!this.all.includes('OK')) {
          this.hobby = ['song', 'dance', 'read', 'javascript'];
        } else {
          this.hobby = [];
        }
      },
      delegate() {
        //=>change事件处理,要晚于数据更新
        this.all = this.hobby.length >= 4 ? ['OK'] : [];
      },
      submit() {
        console.log(this.hobby);
      }
    }
  });
</script>

三、深入研究计算属性以及应用场景分析

3.1 复杂数据的处理方式-插值语法

const App = {
  data (){
    return {
      studentCount: 1
    }
  },
  /**
   * 模板逻辑样式尽可能的绝对分离
   * 运算结果需要被复用
   */
  template:`
    <h1>{{ studentCount > 0 ? ('学生数:' + studentCount) : '暂无学生'}}</h1>
  `
};

const vm = Vue.createApp(App).mount('#app');

image.png

3.2 解决模板中复杂的逻辑运算-computed

语法:编写的时候写一个函数,但后期当做状态使用「它不是函数」

  • computed中创建的计算属性(其实也是个状态)会挂载到实例上,并且也做了get/set数据劫持
  • 计算属性依赖于其它的状态值,经过计算,得到一个新的状态
    • 依赖的状态值发生改变后,相关的计算属性会重新计算新的值
    • 如果依赖的状态值没有发生变化,会缓存其依赖的上一次计算出的数据结果
  • 多次复用一个相同值的数据,计算属性只调用一次
  • 计算属性设置的名字,不能和data中的状态名冲突「原因:它也算创建一个新的状态、也会挂载到实例上」
const App = {
  data (){
    return {
      studentCount: 1
    }
  },
  template:`
    <h1>{{ studentCountInfo }}</h1>
    <h2>{{ studentCountInfo }}</h2>
  `,
  computed: {
    studentCountInfo(){
      return this.studentCount > 0 ? ('学生数:' + this.studentCount) : '暂无学生'
    }
  }
};

const vm = Vue.createApp(App).mount('#app');

3.3 computed实现全选和非全选

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>computed实现全选</title>
</head>

<body>
    <div id="app">
        <input type="checkbox" v-model="slected">全选/非全选
        <br>
        <span v-for="(item) in hobbyList">
            <input type="checkbox" :id="item.id|handleID" :value="item.value" v-model="checkList">
            <label :for="item.id|handleID" v-text="item.name"></label>
        </span>      
    </div>
</body>

</html>
<script src="../node_modules/vue/dist/vue.js"></script>
<script>
    let vm = new Vue({
        el: '#app',
        data: {
            hobbyList: [{
                id: 1,
                name: '唱歌',
                value: 'song'
            },{
                id: 2,
                name: '跳舞',
                value: 'dance'
            },{
                id: 3,
                name: '阅读',
                value: 'read'
            },{
                id: 4,
                name: '睡觉',
                value: 'sleep'
            }],
            //存储选中的兴趣爱好
            checkList:[],
            //存储全选按钮的选中状态
            //slected:false
        },
        computed: {
            slected:{
                get(){//首先得到值是否是全选 
                    return this.checkList.length === this.hobbyList.length;
                },
                set(value){
                    //=>点击全选框会修改selected的值
                    //=>value存储的是选中的状态 true/false
                    if(value){
                        this.hobbyList.forEach(item => {
                            this.checkList.push(item.value);
                        });
                        return;
                    }
                    this.checkList = [];
                }
            }
        },
        filters:{
            handleID(value){
                return 'hobby' + value;
            }
        }
    });
</script>

3.4 computed VS methods:

  • computed具备计算缓存
  • 依赖的状态值不变,computed中编写的方法不会执行,只会用之前计算的结果进行处理;而methods是,只要调用方法,不会管任何东西,方法一定会执行!!
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>vue列表渲染</title>
</head>

<body>
  <div id="app">
    <!-- methods -->
    <h2>{{ getFullname() }}</h2>
    <h2>{{ getFullname() }}</h2>
    <h2>{{ getFullname() }}</h2>

    <!-- computed -->
    <!-- 
      计算属性会基于他们的依赖关系进行缓存,
      在数据不发生变化时,计算属性是不需要重新计算的,
      但是如果依赖的数据发生变化,在使用时,计算属性依然会重新进行计算
     -->
    <h2>{{ fullname }}</h2>
    <h2>{{ fullname }}</h2>
    <h2>{{ fullname }}</h2>

    <!-- 修改name值 -->
    <button @click="changeLastname">修改lastname</button>
  </div>

  <script src="./js/vue.js"></script>
  <script>
    const app = Vue.createApp({
      data() {
        return {
          firstName: 'kobe',
          lastName: 'bryant'
        }
      },
      computed: {
        fullname(){
          console.log('computed fullname'); // 执行一次
          return this.firstName + ' ' + this.lastName
        }
      },
      methods: {
        getFullname(){
          console.log('methods getFullname...'); // 执行三次
          return this.firstName + ' ' + this.lastName
        },
        changeLastname(){
          this.lastName = 'wx'
        }
      }
    });
    app.mount("#app");
  </script>
</body>

</html>

3.5 计算属性的get和set

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>vue列表渲染</title>
</head>

<body>
  <div id="app">
    <h2>{{ fullname }}</h2>
    <button @click="setFullname">设置fullname</button>
  </div>

  <script src="./js/vue.js"></script>
  <script>
    const app = Vue.createApp({
      data() {
        return {
          firstName: 'kobe',
          lastName: 'bryant'
        }
      },
      computed: {
        fullname: {
          get(){
            return this.firstName + ' ' + this.lastName;
          },
          set(value){
            const names = value.split(' ');
            this.firstName = names[0];
            this.lastName = names[1];
          }
        }
      },
      methods: {
        setFullname(){
          this.fullname = 'james kobe'
        }
      }
    });
    app.mount("#app");
  </script>
</body>

</html>

3.6 实现computed与依赖收集

计算属性的处理机制.png

var Vue = (function () {

  var reg_var = /\{\{(.+?)\}\}/g,
    /**
     * total: {
     *   value: 函数执行返回的结果
     *   get: get
     *   dep: ['a', 'b']
     * }
     */
    // 保存computed中的数据
    computedData = {},
    // {{ a }}里面的 a 对应一个节点,如果属性a改变,更新节点即可 
    dataPool = {};

  var Vue = function (options) {
    this.$el = document.querySelector(options.el);
    this.$data = options.data();

    this._init(this, options.computed, options.template);
  }

  Vue.prototype._init = function (vm, computed, template) {
    dataReactive(vm);
    computedReactive(vm, computed);
    render(vm, template);
  }

  function render(vm, template) {
    var container = document.createElement('div'),
      _el = vm.$el;

    container.innerHTML = template;

    var domTree = _compileTemplate(vm, container);

    _el.appendChild(domTree);
  }

  function update(vm, key) {
    dataPool[key].textContent = vm[key];
  }

  function _compileTemplate(vm, container) {
    var _allNodes = container.getElementsByTagName('*'),
      nodeItem = null;

    for (var i = 0; i < _allNodes.length; i++) {
      //<span>{{ a }}</span> 每一个节点
      nodeItem = _allNodes[i];

      //['{{ a }}'] null ['{{ b }}'] null ['{{ total }}']
      var matched = nodeItem.textContent.match(reg_var);
      if (matched) {
        nodeItem.textContent = nodeItem.textContent.replace(reg_var, function (node, key) {
          // node:{{ a }} key: a 
          // dataPool 循环最后结果{a: span, b: span, total: span}
          dataPool[key.trim()] = nodeItem;
          return vm[key.trim()];
        })
      }
    }

    return container;
  }

  // 数据响应式劫持
  function dataReactive(vm) {
    var _data = vm.$data;

    for (var key in _data) {
      (function (key) {
        Object.defineProperty(vm, key, {
          get: function () {
            return _data[key];
          },
          set: function (newValue) {
            _data[key] = newValue;
            update(vm, key);

            // key 是computedData中的total,和上面的key无关
            _updateComputedData(vm, key, function (key) {
              update(vm, key);
            });
          }
        })
      })(key);
    }
  }

  // computed响应式
  function computedReactive(vm, computed) {
    _initComputedData(vm, computed);

    for (var key in computedData) {
      (function (key) {
        Object.defineProperty(vm, key, {
          get: function () {
            return computedData[key].value
          },
          set: function (newValue) {
            computedData[key].value = newValue;
          }
        })
      })(key);
    }
  }

  // 初始化computedData
  function _initComputedData(vm, computed) {
    for (var key in computed) {
      var descriptor = Object.getOwnPropertyDescriptor(computed, key),
        descriptorFn = descriptor.value.get
          ? descriptor.value.get
          : descriptor.value;

      computedData[key] = {};
      computedData[key].value = descriptorFn.call(vm);
      computedData[key].get = descriptorFn.bind(vm);

      computedData[key].dep = _collectDep(descriptorFn);

      /**
       * console.log(computedData);
          {
            total: {
              dep: ['a', 'b'],
              get: ƒ total(),
              value: 3
            }
          }
       */
    }
  }

  // 收集依赖
  function _collectDep(fn) {
    //  ['this.a', 'this.b']
    var _collection = fn.toString().match(/this.(.+?)/g);

    if (_collection.length > 0) {
      for (var i = 0; i < _collection.length; i++) {
        _collection[i] = _collection[i].split('.')[1];
      }
    }

    return _collection;// ['a', 'b']
  }

  function _updateComputedData(vm, key, update) {
    var _dep = null;

    for (var _key in computedData) {
      // 得到数组['a', 'b']
      _dep = computedData[_key].dep;

      for (var i = 0; i < _dep.length; i++) {
        // 说明要更新,就执行函数
        if (_dep[i] === key) {
          // vm.total
          vm[_key] = computedData[_key].get();
          update(_key);
        }
      }
    }
  }

  return Vue;
})();

export default Vue;
import Vue from '../modules/Vue'

var vm = new Vue({
  el: '#app',
  data() {
    return {
      a: 1,
      b: 2
    }
  },
  template: `
    <span>{{ a }}</span>
    <span>+</span>
    <span>{{ b }}</span>
    <span>=</span>
    <span>{{ total }}</span>
  `,
  computed: {
    total() {
      // 初始化执行一次 computed total
      console.log('computed total');
      return this.a + this.b;
    },
    // total: {
    //   get() {
    //     return this.a + this.b;
    //   }
    // }
  }
});

console.log(vm.total);//3
console.log(vm.total);//3
console.log(vm.total);//3
console.log(vm.total);//3

// 修改依赖执行两次 computed total
// 修改几次执行几次,重复调用不会再执行
vm.a = 100;
vm.b = 200;

console.log(vm.total);//300
console.log(vm.total);//300
console.log(vm.total);//300
console.log(vm.total);//300

console.log(vm);

image.png

四、watch专题

4.1 vue的data侦听

computed关注点在模板:抽离复用模板中的复杂逻辑运算

  • 特点:当函数内的依赖更新后,重新调用

watch的关注点在数据更新:给数据增加侦听器,监听的方法第一次不会立即被执行,当数据更新时,侦听器函数执行

  • 特点:在数据更新时,需要完成什么样的逻辑
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>vue watch</title>
</head>

<body>
  <div id="app">
    <h2>{{ message }}</h2>
    <button @click="changeMessage">修改message</button>
  </div>

  <script src="./js/vue.js"></script>
  <script>
    const app = Vue.createApp({
      data() {
        return {
         message: 'Hello Vue',
         info: { name: 'wx', age: 18}
        }
      },
      methods: {
        changeMessage(){
          this.message = 'Hello World'
          this.info = {name: 'kobe'}
        }
      },
      watch: {
        // 1.默认有两个参数 newValue 、 oldValue
        message(newVal, oldVal){
          console.log('message数据发生变化', newVal, oldVal);
        },
        info(newVal, oldVal){
          // 2.如果是对象,获取的是代理对象
          // console.log('info数据发生变化', newVal, oldVal);
          // console.log(newVal.name, oldVal.name);

          // 3.获取原生对象
          console.log({ ...newVal }); //{name: 'kobe'}
          console.log(Vue.toRaw(newVal)); //{name: 'kobe'}
        }
      }
    });
    app.mount("#app");
  </script>
</body>

</html>

4.2 vue的watch侦听选项

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>vue watch</title>
</head>

<body>
  <div id="app">
    <h2>{{ info.name }}</h2>
    <button @click="changeInfo">修改info</button>
  </div>

  <script src="./js/vue.js"></script>
  <script>
    const app = Vue.createApp({
      data() {
        return {
         info: { name: 'wx', age: 18}
        }
      },
      methods: {
        changeInfo(){
          // 1.创建一个新对象,赋值给info
          // this.info = {name: 'kobe'}

          // 2.直接修改原对象的一个属性
          this.info.name = 'kobe'
        }
      },
      watch: {
        // 默认watch监听不会进行深度监听
        // info(newVal, oldVal){
        //   console.log('侦听到info改变', newVal, oldVal);
        // }

        info: {
          handler(newVal, oldVal){
            console.log('侦听到info改变', newVal, oldVal);
            console.log(newVal === oldVal); // true
          },
          // 深度监听
          deep: true,
          // 第一次渲染就直接执行一次监听器, {name: 'wx', age: 18} undefined
          immediate: true
        },
        // vue2中有,vue3中没有
        "info.name": function(newVal, oldVal){
          console.log('name发生了改变', newVal, oldVal);
        }
      }
    });
    app.mount("#app");
  </script>
</body>

</html>

4.3 Vue的$watch侦听

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>vue watch</title>
</head>

<body>
  <div id="app">
    <h2>{{ message }}</h2>
    <button @click="changeMessage">修改message</button>
  </div>

  <script src="./js/vue.js"></script>
  <script>
    const app = Vue.createApp({
      data() {
        return {
         message: "Hello Vue"
        }
      },
      methods: {
        changeMessage(){
          this.message = 'Hello World'
        }
      },

      // 生命周期回调函数:当前的组件创建时自动执行
      // 一般在该函数中,会进行网络请求
      created(){
        console.log('created');
        this.$watch("message", (newVal, oldVal)=>{
          console.log('message数据变化');
        },{deep: true});
      }
    });
    app.mount("#app");
  </script>
</body>

</html>

4.4 watch VS computed:

+ watch是监听现有的状态,所以watch中写的状态在data中是存在的;而computed是依赖其他状态创建新的状态,不能data中现有的状态名冲突;
+ watch一次只能监听一个状态,而computed可以同时对多个状态进行依赖(任何一个改变都会重新计算);
+ watch本意是:监听某个状态值变化,从而做啥事「啥事是自己定义的」
  computed本意是:依赖于其他状态,算出一个新的状态值「事情相对比较固定」

所以真实项目中的很多需求,用watch和computed都可以处理;但是因为computed具备计算缓存,所以建议大家此时,优先使用computed!!

4.5 实现数据响应式、computed和watch

分析思路: 数据响应式

- data() 
- vm.$data
- reactive -> vm.xxx
- get vm[key] -> vm.$data[key]
- set vm[key] -> vm.$data[key] = newValue
- ? - updataComputedProp - value
- ? - updatawachProp - callback

分析思路:computed

- props total
- 给props加属性
{
    value: get出来的value
    get: 方法method
    dep: [a, b]
}

分析思路:watch

watch里面的props对应一个fn,data的set执行了,执行fn

用原生JS实现以下vue代码

import Vue from '../modules/Vue';

const vm = new Vue({
  data() {
    return {
      a: 1,
      b: 2
    }
  },
  computed: {
    total(){
      console.log('computed');
      return this.a + this.b;
    }
  },
  watch: {
    total(newValue, oldValue){
      console.log('total',newValue, oldValue);
    },
    a (newValue, oldValue){
      console.log('a', newValue, oldValue);
    },
    b (newValue, oldValue){
      console.log('b', newValue, oldValue);
    }
  }
});

console.log(vm);

console.log(vm.total);
console.log(vm.total);
console.log(vm.total);

vm.a = 100;

console.log(vm.total);
console.log(vm.total);
console.log(vm.total);

vm.b = 200;

console.log(vm.total);
console.log(vm.total);
console.log(vm.total);

1. 实现响应式与暴露回调接口

index.js

import { reactive } from "./reactive";

class Vue {
  constructor(options) {
    const { data, computed, watch } = options;

    this.$data = data();

    this.init(this, computed, watch);
  }

  init(vm, computed, watch) {
    this.initData(vm);
    const computedIns = this.initComputed(vm, computed);
    const watcherIns = this.initWatcher(vm, watch);

  }

  // 数据响应式
  initData(vm) {
    reactive(vm, (key, value) => {
       console.log(key, value);
    }, (key, newValue, oldValue) => {
       console.log(key, newValue, oldValue);
    });
  }

  initComputed(vm, computed) {
    // 枚举computed 然后增加computed data
    // 返回实例 实例里有update方法 更新computedData 的value
 
  }

  initWatcher(vm, watch) {
    // 枚举watcher  增加侦听器
    // 返回实例 实例里有调用watch的方法  执行侦听器
  }
}

export default Vue;

reactive.js

// __get__, __set__暴露回调接口
export function reactive(vm, __get__, __set__) {
  const _data = vm.$data;

  for (let key in _data) {
    // 把data中的状态key挂载到vm实例上
    Object.defineProperty(vm, key, {
      get() {
        __get__(key, _data[key]);
        return _data[key];
      },
      set(newValue) {
        const oldVlaue = _data[key];
        _data[key] = newValue;
        __set__(key, newValue, oldVlaue);
      }
    })
  }
}

image.png

image.png

2. 实现计算属性特性

index.js

import Computed from "./computed";
import { reactive } from "./reactive";

class Vue {
  constructor(options) {
    const { data, computed, watch } = options;

    this.$data = data();

    this.init(this, computed, watch);
  }

  init(vm, computed, watch) {
    this.initData(vm);
    const computedIns = this.initComputed(vm, computed);
    const watcherIns = this.initWatcher(vm, watch);

    this.$computed = computedIns.update.bind(computedIns);
  }

  // 数据响应式
  initData(vm) {
    reactive(vm, (key, value) => {
      // console.log(key, value);
    }, (key, newValue, oldValue) => {
      // console.log(key, newValue, oldValue);

      this.$computed(key);
    });
  }

  initComputed(vm, computed) {
    // 枚举computed 然后增加computed data
    // 返回实例 实例里有update方法 更新computedData 的value
    const computedIns = new Computed();

    for (let key in computed) {
      computedIns.addComputed(vm, computed, key);
    }

    return computedIns;
  }

  initWatcher(vm, watch) {
    // 枚举watcher  增加侦听器
    // 返回实例 实例里有调用watch的方法  执行侦听器
  }
}

export default Vue;

computed.js


class Computed {
  constructor() {
    /**
     * total(){
     *   return this.a + this.b;
     * }
     * 
     * {
     *  key: total,
     *  value: 3,
     *  get: total fn,
     *  dep: [a, b]
     * }
     */
    this.computedData = [];
  }

  addComputed(vm, computed, key) {
    const descriptor = Object.getOwnPropertyDescriptor(computed, key),
      // @ts-ignore
      descriptorFn = descriptor.value.get
        // @ts-ignore
        ? descriptor.value.get
        // @ts-ignore
        : descriptor.value,
      // @ts-ignore
      // computed第一次执行
      value = descriptorFn.call(vm),
      // @ts-ignore
      get = descriptorFn.bind(vm),
      // ['a', 'b']
      // @ts-ignore
      dep = this._collectDep(descriptorFn);

    this._addComputedProp({
      key,
      value,
      get,
      dep
    });

    // 返回的就是this.computedData
    const dataItem = this.computedData.find(item => item.key === key);

    // 挂载到实例上 vm.xxx
    // 增加
    Object.defineProperty(vm, key, {
      get() {
        return dataItem.value
      },
      set(newValue) {
        // vm.total = 500在这里不起作用
        dataItem.value = dataItem.get();
      }
    });
  }
  // key: data中变更的key
  // 更新
  update(key, cb) {
    this.computedData.map(item => {
      const dep = item.dep;
      const _key = dep.find(el => el == key);

      if (_key) {//依赖变更就重新执行get,如果没有变更就不管,拿vm上的即可
        item.value = item.get();
        cb && cb(item.key, item.value);
      }
    });
  }

  _addComputedProp(computedProp) {
    this.computedData.push(computedProp);
  }

  _collectDep(fn) {
    // ["this.a", "thia.b"]
    const matched = fn.toString().match(/this\.(.+?)/g);

    return matched.map(item => item.split('.')[1]);
  }
}

export default Computed;

image.png

image.png

3. 实现侦听器特性

computed.js

update(key, watch) {
    this.computedData.map(item => {
      const dep = item.dep;
      const _key = dep.find(el => el == key);

      if (_key) {//依赖变更就重新执行get,如果没有变更就不管,拿vm上的即可
        const oldValue = item.value;
        item.value = item.get();
        watch(item.key, item.value, oldValue);
      }
    });
  }

index.js

import Computed from "./computed";
import { reactive } from "./reactive";
import Watcher from "./watcher";

class Vue {
  constructor(options) {
    const { data, computed, watch } = options;

    this.$data = data();

    this.init(this, computed, watch);
  }

  init(vm, computed, watch) {
    this.initData(vm);
    const computedIns = this.initComputed(vm, computed);
    const watcherIns = this.initWatcher(vm, watch);

    this.$computed = computedIns.update.bind(computedIns);
    this.$watch = watcherIns.invoke.bind(watcherIns);
  }

  // 数据响应式
  initData(vm) {
    reactive(vm, (key, value) => {
      // console.log(key, value);
    }, (key, newValue, oldValue) => {
      if (newValue === oldValue) return;
      // console.log(key, newValue, oldValue);

      // 注入watch计算total
      this.$computed(key, this.$watch);
      this.$watch(key, newValue, oldValue);
    });
  }

  initComputed(vm, computed) {
    // 枚举computed 然后增加computed data
    // 返回实例 实例里有update方法 更新computedData 的value
    const computedIns = new Computed();

    for (let key in computed) {
      computedIns.addComputed(vm, computed, key);
    }

    return computedIns;
  }

  initWatcher(vm, watch) {
    // 枚举watcher  增加侦听器
    // 返回实例 实例里有调用watch的方法  执行侦听器
    const watcherIns = new Watcher();

    for (let key in watch) {
      watcherIns.addWatcher(vm, watch, key);
    }

    return watcherIns;
  }
}

export default Vue;

watcher.js

class Watcher {
  /**
   * addWatcher [vm, watcher, key]
   * 
   * 每一次给this.watchers 注入新的watch
   * {
   *    key: ? 看可以是否相同
   *    fn: key fn  相同就执行函数
   * }
   */
  constructor() {
    this.watchers = [];
  }

  addWatcher(vm, watcher, key) {
    this._addWatcherProp({
      key,
      fn: watcher[key].bind(vm)
    });

    // console.log(this.watchers);
  }

  // 调用
  invoke(key, newValue, oldValue) {
    this.watchers.map(item => {
      if (item.key === key) {
        item.fn(newValue, oldValue);
      }
    });
  }

  _addWatcherProp(watchProp) {
    this.watchers.push(watchProp);
  }


}

export default Watcher;

image.png

image.png