前言
- 文章结构采用【指出阶段目标,然后以需解决问题为入口,以解决思路为手段】达到本文目标,若使诸君稍有启发,不枉此文心力^-^
- 文分【思路篇】和【实现篇】,本文是思路篇,建议看两个窗口同步阅读-》vue2.0|具体实现篇|数据劫持
目标
- 代码层面实现vue数据观测
第一阶段:实现rollup 环境安装
搭建vue编译环境,vue采用rollup进行打包
-
依赖安装
Rollup
npm install @babel/preset-env @babel/core rollup rollup-plugin-babel rollup-plugin-serve cross-env -Dbabel
npm i @babel/core @babel/preset-env rollup-plugin-serve -D -
rollup.config.js文件编写
import babel from "rollup-plugin-babel"; import serve from 'rollup-plugin-serve'; export default { input: './src/index.js', output: { format: 'umd', // 采用umd规范导出入口文件导出变量 name: 'Vue', file: 'dist/umd/vue.js', sourcemap: true }, plugins: [ babel({ exclude: 'node_modules/**' }), serve({ port: 3000, contentBase: '', openPage:'/index.html' }) ] }
第二阶段:搭建vue基本结构
src
├── index.js
├── init.js
├── observer
│ └── index.js
└── state.js
src/index.js 打包入口文件导出函数Vue
- 执行initMixin方法在Vue原型上挂载方法
- 执行初始化方法
index.js
import {initMixin} from './init';
function Vue(options){
console.log(options);
this._init(options);
}
initMixin(Vue);
export default Vue;
Init.js
- 挂载_init方法,进行初始化状态,即对data、watch、computed等进行对应处理
import { initState } from "./state";
export function initMixin(Vue) {
Vue.prototype._init = function (options) {
const vm = this;
vm.$options = options;
// 初始化数据
initState(vm)
}
}
state.js
export function initState(vm){
let opts = vm.$options;
let {methods,data,props,computed,watch} = opts;
if(methods){
initMethod(vm)
}
if(data){
initData(vm)
}
if(props){
initProps(vm)
}
if(computed){
initComputed(vm)
}
if(watch){
initWatch(vm)
}
}
function initMethod(vm) {
}
function initData(vm) {
}
function initProps(vm) {
}
function initComputed(vm) {
}
function initWatch(vm) {
}
第三阶段:实现初步数据劫持,监听用户对对象属性的赋值取值操作,触发同时执行自定义逻辑
先看使用
let vm = new Vue({
el: '#app',
data(){
return {
b: 1,
a: {
a: '111'
}
}
}
})
// _data是data的一层代理,在vue|具体实现篇|数据劫持 中可以明显看出
// 用户取值操作
console.log(vm._data.b);
// 某一属性赋值一个对象时,新对象中的属性被改变
vm._data.b = {x:1}
vm._data.b.x = 2
目标:打印出1同时执行vue定义的逻辑,此处我们定义为console.log('取值操作') 即要实现打印【1】同时打印【取值操作】
核心问题
- 如何监听取值赋值操作
- 多层(比如a.a)如何监听
- 当我们给某一属性赋值一个对象时,如何能让新对象中的属性也被监听到(如x改变)
解决思路
- 如何监听取值赋值操作:Object.defineProperty
- 多层(比如a.a)如何监听:深度递归监听
- 当我们给某一属性赋值一个对象时,如何能让新对象中的属性也被监听到(如x改变):在set中对新值也进行监听操作
扩展:解释了vue监听两大缺陷之一,对象属性的新增或删除无法被观测到
第四阶段 :数组数据劫持
先看使用
let vm = new Vue({
el: '#app',
data(){
return {
a: [1,2,3,{
b:11
}]
}
}
})
// 数组方法的劫持
vm._data.a.shift();
// 数组内对象的劫持
vm._data.a[2].b = 22;
核心问题
- 如果不对数组单独处理,在处理逻辑中会遍历数组,如果数组元素很多,则会导致性能很差
- 如果方法有新增的功能,需要对新增元素进行观测
- 如何实现数组中对象的修改要被监听
解决思路
不对数组的属性进行遍历,而是采用函数劫持的方式进行数组的劫持,即重写数组原型上会改写本身的七个方法,且遍历数组中是对象的元素进行深度观测,从而实现数组监听;
-
数组方法的劫持
- 获取数组原型,重写七个方法,将此对象导出
- 在Observer构造中判断,如果数据是数组特殊处理(observerArray),将数据的原型执行自定义对象
- 对有新增功能的方法进行处理,新增值进行观测处理
-
数组内对象的劫持
-
遍历数组中是对象的元素调用观测方法
-
扩展:解释了vue监听两大缺陷之一,通过数组索引改变数组无法被观测到
第五阶段 :小优化,代理实现用户直接从vm上处理数据
先看使用
let vm = new Vue({
el: '#app',
data(){
return {
a: [1,2,3,{
b:11
}]
}
}
})
// 效果相同,避免用户操作data还需要通过_data字段
// vm._data.a.shift();
vm.a.shift();
解决思路
通过Object.defineProperty进行一层代理,遍历_data在vm上对所有key定义getset,这样当用户取值赋值时就是操作_data