赞
踩
跟着本文,一起手写响应式原理。
先初始化一个项目,
npm init -y
安装一下webpack以及相关依赖,
npm install -D webpack webpack-cli webpack-dev-server html-webpack-plugin
安装过程中,新建一个webpack.config.js文件,
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin")
module.exports = {
entry: {
index: path.resolve(__dirname, "./src/index.js")
},
output: {
filename: "[name].js",
path: path.resolve(__dirname, "./dist")
},
devtool: "source-map",
resolve: {
modules: [path.resolve(__dirname, ""), path.resolve(__dirname, "./node_modules")],
},
mode: "none",
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, "./src/index.html")
})
]
}
最基本的webpack配置。
按照定义的入口文件路径,根目录下新建src,src下新建index.html,index.js
index.html
<!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>Document</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
在index.js中,vue项目一般写法这时候是先引入vue,再创建一个vue实例,我们这里进行模拟,有地方就需要小改一下,
import Vue from "./vue";
let vm = new Vue({
el: "#app",
data() {
return {
title: "Student List",
classNum: 1,
total: 2,
teachers: ["Mrs.Wang", "Mis.Chen"],
students: [
{ id: 1, name: "jack" },
{ id: 2, name: "tom" },
]
}
}
})
这里就代表着Vue是从一个叫vue的文件夹那引入的,如果想要完全和vue项目写法一直,如下面这样:
import Vue from "./vue";
这就依赖于webpack中配置:
resolve: {
modules: [path.resolve(__dirname, ""), path.resolve(__dirname, "./node_modules")],
},
这个配置表示,模块解析的路径先从node_modules找文件,如果找不到再从当前目录下找。
vm是Vue的一个new的实例对象,在ES6之下,说明Vue是个构造函数,函数中传入的参数都是一个一个像配置一样,正如此vue是option api。
现在来创建vue文件夹,里面创建个index.js
function Vue(options) {
this._init(options);
}
export default Vue;
在Vue的原型上挂载一个初始化方法,将options配置项作为参数传入。
Vue.prototype._init = function(options) {
var vm = this;
vm.$options = options;
initState(vm);
}
将this实例保存到vm上,避免this混乱,然后在实例上挂载一个$options,存放参数配置项,初始化的逻辑操作,单独一个文件init.js中处理,创建一个init.js引入initState方法。
那init.js中首先得有个initState函数,
function initState(vm) {
var options = vm.$options;
if (options.data) {
initData(vm);
}
}
export {
initState,
}
取实例对象上挂载的参数配置项,然后分模块处理props,methods,data,computed,watch,本文只写响应式,就只有个data,实际vue中的顺序执行:props > methods > data > computed > watch
那initData方法就是处理data响应式逻辑,这里做了两件事
function initData(vm) {
var data = vm.$options.data;
vm._data = data = typeof data === "function" ? data.call(vm) : data || {}
for(var key in data) {
proxyData(vm, "_data", key);
}
observe(vm._data);
}
先获取挂载在实例对象上参数配置项里的data数据,然后实例上挂载一个_data存放data中数据作为备份,到那时这里需要判断一下data的类型是不是函数,官方推荐data写成函数有返回值格式,因为这样能保证每一份实例对象都有独立的data,不至于组件复用时候造成数据污染。当然也允许直接写成对象的格式。
这时候data中存的就是data() 中return的值,_data作为备份方便实例直接获取,也就是vue中为啥能够直接this点上data中数据的原因。
循环data进行数据拦截,将数据拦截的处理单独放入一个文件中,新建一个proxy.js,这里的proxy不是ES6的Proxy,实际上是用的Object.defineProperty来实现的。
Object.defineProperty(obj, prop, descriptor)
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。参数:
obj:要定义属性的对象
prop:要定义或修改的属性的名称或 Symbol
descriptor:要定义或修改的属性描述符
proxyData接收三个参数,vm是当前实例对象,target是备份的data数据,key就是需要访问或修改的属性,那么通过target[key]就能拿到这个属性的值。
proxy.js
function proxyData(vm, target, key) {
Object.defineProperty(vm, key, {
get() {
return vm[target][key];
},
set(newValue) {
vm[target][key] = newValue;
},
})
}
export default proxyData;
descriptor中有一个get方法和一个set方法,但不止这两个。
每次访问vm的属性key,都会触发get方法,我们让它返回这个属性的值,这就相当于拦截了vm.key,得到的值是vm[’_data’].key
每次修改vm中属性key,都会触发set方法,set能拿到最新修改的值,将属性值设为这个最新值。
下面来实现响应式的核心observe,单独一个observe.js文件来处理,
function observe(data) {
if (typeof data !== "object" || data === null) return
return new Observer(data);
}
export default observe;
观察的一定是对象,注意typeof null也是object,这么说来其实也就是观察的object对象和array数组,这里返回了Observer构造函数,在其中进行了对象和数组的判断再处理,然后就是递归,一层一层的再回来判断是不是对象类型,一直到不是为止。
在observer.js中来写这个Observer构造函数,
function Observer(data) {
if (Array.isArray(data)) {
// 数组操作
} else {
// 对象操作
}
}
export default Observer;
Object.defineProperty只作用对象,如果对数组也使用这个方法进行观察,那数组极大时就很耗性能,vue采用的是重写数组的七个方法,所以这里区分了对象和数组的处理。
开始对象的拦截处理。
在Observer的原型上定义个walk的方法,传入实例对象中data的返回值。源码中很多方法都喜欢写成walk,代表有循环的意思。
Observer.prototype.walk = function (data) {
var keys = Object.keys(data);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var value = data[key];
defineReactiveData(data, key, value);
}
}
先拿到data中所有属性,循环拿到每个key和value,具体的逻辑在defineReactiveData中实现。
创建个reactive.js文件,
function defineReactiveData(data, key, value) {
Object.defineProperty(data, key, {
})
}
export default defineReactiveData;
太熟悉了,之前处理proxyData也是一样的套路,data是当前实例对象中data对象(函数返回对象),key是需要修改的属性,value是key的值。
具体的get和set处理如下:
get() {
console.log(`响应式数据获取:`, value);
return value;
},
set(newValue) {
console.log('响应式数据设置:', newValue);
if (newValue === value) return;
observe(newValue);
value = newValue;
},
获取data中key时,会触发它的get方法,直接返回这个key的value值,当然在实际源码中这儿要做好复杂的其他处理。
设置key的新值时,触发set方法能拿到最新修改的值,如果相同啥事不做,如果不同就把这个新值赋给key的value,但需要注意的是在赋值之前需要判断,如果设置的新值还是对象时,也需要再去观察,调用observe把当前的新值传进去,就又回到了这儿,
function observe(data) {
if (typeof data !== "object" || data === null) return
return new Observer(data);
}
这就形成了递归。但在刚进入defineReactiveData这个方法时,就需要先调用observe进行判断,因为当前观察的值也可能是对象时候,需要递归的去观察,这样递归观察对象更完善了。
完整的defineReactiveData方法如下:
function defineReactiveData(data, key, value) {
observe(value);
Object.defineProperty(data, key, {
get() {
return value;
},
set(newValue) {
if (newValue === value) return;
observe(newValue);
value = newValue;
},
})
}
开始数组的拦截处理。
数组的通过重写方法处理的,在原型链上新增自己写的方法,根据原型链的顺序,优先取这些方法处理数组,如果数组中还有数组也需要递归观察。
创建一个array.js
将数组的所有原型方法存在originArrMethods上,但这个是引用,需要新造一个包含是所有数组原型方法的对象,以方便改写。
var originArrMethods = Array.prototype;
var arrMethods = Object.create(originArrMethods);
然后就是7个会修改数组自身数据的方法,可以把方法名放在一个文件中,config.js
var ARR_METHODS = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse",
];
export {
ARR_METHODS,
}
就是如上这些方法操作数组会改变原数组,
push:尾部塞入
var a = [1,2,3];
a.push(666); // 4
console.log(a); // [1,2,3,666]
pop:尾部导出
var a = [1,2,3,666];
a.pop(); // 666
console.log(a); // [1,2,3]
shift:头部导出
var a = [1,2,3];
a.shift(); // 1
console.log(a); // [2,3]
unshift:头部塞入
var a = [2,3];
a.unshift(1); // 3
console.log(a); // [1,2,3]
splice
sort:排序
reverse:反转
回到源码,
找到config中定义的方法名进行方法重写,
ARR_METHODS.map(function (m) {
arrMethods[m] = function () {
var args = Array.prototype.slice.call(arguments);
var rt = originArrMethods[m].apply(this, args);
var newArr;
// 数组新增项可能是数组或对象
switch (m) {
case "push":
case "unshift":
newArr = args; // [{a: 1}] / [[1]]
break;
case "splice": // splice(0, 1, {})
newArr = args.slice(2); // [{a: 1}, {b: 2}]
break;
default:
break;
}
newArr && observeArr(newArr);
return rt;
}
});
这七个方法,每个参数个数都不是固定的,例如splice就可以有3个,将arguments由类数组转成数组,然后对数组方法的拦截,需要在原型方法执行后获取值后进行其他逻辑的操作。
特殊处理新增的方法,它们是需要传新增值,newArr存在时,排除了splice第三个参数没写情况,对新增加的值进行观察。
observeArr方法是遍历数组中每一项,进行观察。新建一个observeArr.js
function observeArr(arr) {
for (let i = 0; i < arr.length; i++) {
observe(arr[i]);
}
}
这里又调用了observe观察数组每一项,判断每项是不是object类型,再进而判断是对象还是数组,这又形成了递归,这样就能将数组中每项每层都响应式处理。
文中代码有的文件的引用没写,可以访问
https://github.com/Zuowendong/sourceCode-of-VueDemo/tree/master/Data
查看,和vue源码中有些方法名并不一致,可以配合vue源码一起学习。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。