导航


HTML

CSS

JavaScript

浏览器 & 网络

版本管理

框架

构建工具

TypeScript

性能优化

软实力

算法

UI、组件库

Node

业务技能

针对性攻坚

AI


简易版

vm.$data.xxxvm.xxx 的关系

  1. Vue 在创建实例的过程中调用了 data 函数
  2. data 函数返回数据对象
  3. Vue 将其包装成响应式对象后保存到 $data
  4. 而且实现了跨过 $data 还能访问属性
const App = Vue.createApp({
  data() {
    return {
      title: 'This is my TITLE',
    }
  },
  template: `
    <h1>{{ title }}</h1>
  `
});

const vm = App.mount('#app');
console.log(vm);
console.log(vm.$data.title);
console.log(vm.title);

vm.$data.title = 'This is your TITLE';
console.log(vm.title);

Untitled

$data 是响应式数据对象

后续在 vm 上添加 author ,不会出现在 vm.$data 上:

vm.author = 'Lance';

Untitled

在 $data 上添加属性,不会出现在 vm 上:

vm.$data.author = 'Lance';

两种添加方式都不能渲染到页面上:

const App = Vue.createApp({
  data() {
    return {
      title: 'This is my TITLE',
    }
  },
  template: `
    <div>
      <h1>{{ title }}</h1>
      <h1>{{ author }}</h1>
    </div>
  `
});
const vm = App.mount('#app');
// vm.author = 'Lance';
vm.$data.author = 'Lance';

Untitled

手写简易 Vue 读写 data

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

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

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

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

defineGetterdefineSetter

Object.defineProperty 在 IE8 下只支持DOM,可以使用 defineGetterdefineSetter 替换方案:

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

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

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

  var _this = this;
  for (var key in this.$data) {
  	(function(k) {
      _this.__defineGetter__(k, function() {
        return _this.$data[k];
      });
      _this.__defineSetter__(k, function(newVal) {
        _this.$data[k] = newVal;
      });
    })(key);
  }
}

data 为什么必须得是个函数

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

vm.a = 233;
console.log(vm, vm2);

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

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

Untitled

当然分别给两个新对象也没问题,但 Vue 为了避免出现上边情况,所以强制你得是个 function,返回一个新对象,这样在 Vue 内部执行它时,每次就是新对象了:

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

vm.a = 233;
console.log(vm, vm2);

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

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

Untitled

Object.defineProperty 无法监听能修改原数组的数组方法

/**
 * 数据变更检测
 */

const vm = {
  data: {
    a: 1,
    b: 2,
    list: [1, 2, 3, 4, 5]
  }
};

for (var key in vm.data) {
  (function(key) {
    Object.defineProperty(vm, key, {
      get() {
        console.log('数据获取');
        return vm.data[key];
      },
      set(newValue) {
        console.log('数据设置');
        vm.data[key] = newValue;
        // 视图更新操作
      }
    });
  })(key);
}

console.log(vm);

vm.a = 233;
console.log(vm);

// 重新赋值能够触发 set
vm.list = vm.list.map(item => item * 2);
console.log(vm);

// 新增一个数组之前没监听的成员,数组倒是push了999,但没能触发set,间接导致没法更新视图

/**
 * 下列方法,都不返回新数组(7种)
 * Object.defineProperty 没办法监听下列方法对数组的操作变更
 */
// vm.list.unshift(1);
// vm.list.push(999);
// vm.list.pop();
// vm.list.shift();
// vm.list.splice(2, 1);
// vm.list.sort((a, b) => b - a);
// vm.list.reverse();
console.log(vm.list);

// Vue对下列方法进行了包裹封装
// function push() {
//   vm.list.push(233);
//   // 视图更新
// }

Vue2 数组结构

const vm = new Vue({
  el: '#app',
  template: `<div></div>`,
  data() {
    return {
      list: [1, 2, 3, 4, 5]
    }
  }
});

console.log(vm.list);

Untitled

针对修改原数组,执行方法后不返回新数组的方法进行了封装

Vue 类似做了这个操作:

  1. Vue 先调用自己封装的 push 等方法
  2. 再在里边调用原生的
  3. 再进行一些更新视图操作

复杂版

思路

  1. _data 去接收 options.data() 函数返回的对象,保存到 vm 实例中
    1. 为了能直接 vm.title 的形式操作数据(跳过 vm._data.title 开发更方便),我们遍历 _data,通过 Object.definePropertyvm 实例上挂载了所有 _data 下的属性
  2. 有了 _data 后,我们就需要对 _data 本身的改变进行拦截观察了(为了更新值时做页面更新等操作)
    1. 观察者模式,new Observer(data)
    2. 递归观察,因为 _data 下的属性也有可能是个对象,属性的属性也有可能是个对象...
  3. Observer 观察者构造函数
    1. 观察者接收的 data 有可能是个数组也有可能是个对象,所以得分情况处理
    2. 是[] - 给数组的原型链上加中间桥梁
      1. [].__proto__ = arrMethodsarrMethods.__proto__ = Array.prototype
      2. arrMethods 下挂载了 7 个属性,分别对应能原地改变数组的 7 个方法
      3. 每个方法在执行的时候,先用原生方法执行一次,再执行自己的逻辑
        1. push、unshift、splice 三个方法有可能新增数组成员,而新增的成员也有可能是数组或对象,所以还得用一个 newArr 保存下新数组成员,然后对它们进行遍历观察
        2. 获取原生方法执行后的结果,返回出去
    3. 是{} - 遍历对象属性并用 Object.defineProperty 进行数据劫持
      1. 每个对象属性有可能还是个对象或数组,记得再次调用 new Observer(value) 观察它

文件夹结构