一、深入研究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中定义
② 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);
情况二: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);
深拷贝
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);
情况三:可以代替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);
进一步优化初始化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 手写数据劫持
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: '小明'
}
]
}
}
});
当获取值时
当设置值时
vue实例
二、深入研究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');
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与依赖收集
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);
四、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);
}
})
}
}
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;
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;