(一)变化侦测
该系列主要对Vue源码流程分析与简单实现,会省略源码中的一些细节
- 初始化
- Object变化侦测
- Array变化侦测
observe流程图
1、初始化
- 定义Vue构造函数
- 向Vue原型混入操作方法,方便后期扩展
- 在初始化函数中进行 state初始化 -> data初始化
// index.js
import {initMixin} from "init.js"
const Vue = function(options){
// 选项初始化
this._init(options);
}
// 向Vue原型混入操作方法
initMixin(Vue);
...
export default Vue;
// init.js
import {initState} from "state.js"
export function initMixin(Vue){
Vue.prototype._init = function(options){
const vm = this;
vm.$options = options;
// 初始化状态
initState(vm);
}
}
// state.js 初始化状态
import {observe} from 'observe/index.js'
export function initState(vm){
const opt = vm.$options;
if(opt.data){
// 初始化data
initData(vm);
}
}
function initData(vm){
let data = vm.$options.data;
// 判断data是否为函数
data = typeof data === 'function' ? data.call(vm) : data;
// 对统一后的data对象重新挂载在vm实例上
vm._data = data
// 数据侦测与劫持
observe(data);
}
2、Object变化侦测
Object.defineProperty()缺点
- 不能侦测新增与删除属性
2.1 数据劫持
// observe/index.js
// 对数据进行侦测/重写 返回可侦测对象
export function observe(data){
// 非对象类型不进行劫持
if(typeof data != 'object' || data == null) return;
return new Observe(data);
}
// 数据侦测类
class Observe {
constructor(data){
this.walk(data);
}
// 对象数据劫持 - 相当于重写 性能瓶颈
walk(){
Object.keys.forEach(key => {
defineReactive(data,key,data[key])
})
}
}
// 数据劫持公共方法
export const defineReactive(target,key,value){
Object.defineProperty(target,key,{
enumerable:true, // 默认也为true
configurable:true, // 同上
get(){
return value; // 闭包
},
set(newVal){
if(newVal === value) return;
value = newVal;
}
})
}
此时我们可以通过observe方法对传入的对象进行数据侦测,劫持数据的取值
与更改
但是数据是在_data
上的,为了开发模式语法尽量简洁,这里需要数据代理
2.2 数据代理
// state.js 对initData进行补充
function initData(vm){
//...other code
for(let key in data){
proxy(vm,'_data',key);
}
}
function proxy(vm,target,key){
Object.defineProperty(vm,key,{
get(){
return vm[target][key];
},
set(newVal){
vm[target][key] = newVal;
}
})
}
const vm = new Vue({ data(){ return { name:"foo", age:11 } } })
当我们执行以上代码时可以在vm上读取到name属性,并且name与age都拥有
getter
与setter
2.3 深度侦测与新值侦测
当data中的值是嵌套
的对象,以及对data属性设置对象
值时,我们希望仍然对其进行侦测,
并且对于已经侦测的数据不再进行重写
// obseve/index.js
export function observe(data){
...
// data已经存在了Observe的一个实例 说明已经被侦测过
if(data.__ob__ instanceof Observe) return data.__ob__
...
}
class Observe {
constructor(data){
Object.defineProperty(data,"__ob__",{
value:this, // 直接使用实例赋值 后面数组侦测需要该属性
enumerable:true // 防止深度侦测时 递归爆栈
})
...
}
}
export const defineReactive = function(target,key,value){
// 深度侦测
observe(value);
Object.defineProperty(target,key,{
...
set(newVal){
...
// 新值侦测
observe(value);
...
}
})
}
3、Array变化侦测
上述observe流程图中的hasMethod判断是指数组调用的方法是否在被重写列表中
数组一般数据元素较多,如果逐个下标进行侦测,会浪费性能,因为相较于对下标的修改我们更常使用的是数组方法修改
注意并不是Object.defineProperty
不能侦测,而是Vue在设计时抛弃了侦测下标这种方式
- 数组中
引用数据类型
的元素依然使用observe进行侦测 - 对能修改原数组的方法进行
切面补充
(原型链继承的方式)
// observe/index.js
import {newArrayProto} from "observe/array.js"
...
class Observe {
constructor(data){
...
//数组类型判断
if(Array.isArray(data)){
// 设置data的原型对象
Object.setPrototypeOf(data,newArrayProto)
this.observeArray(data)
}else {
}
}
// 数组劫持
observeArray(data){
// 元素为数组/对象进行递归劫持
data.forEach(item => observe(item))
}
}
...
// observe/array.js
let originArrayProto = Array.prototype
// 创建一个以originArrayProto为原型的对象
export let newArrayProto = Object.create(originArrayProto);
// 修改数组的7种方法
const methods = ["push","pop","unshift","shift","sort","splice","reverse"]
methods.forEach(method => {
newArrayProto[method] = function(..args){
// 调用原始方法
const result = originArrayProto[method].apply(this,args);
const ob = this.__ob__; // __ob__为上述添加的Observe实例
// 数据劫持新数据
let inserted;
switch(method){
case 'push':
case 'unshift':
inserted = args;
breake;
case 'splice':
inserted = args.slice(2);// 第三个参数为新数据
break;
}
// 新数据侦测
if(inserted){
ob.observeArray(inserted);
}
return result;
}
})
通过原型链继承将中间层对象设置为原数据的原型对象,是一种面向切面编程的方式,只重写部分方法__ob__
是原数据的一个属性,值为Observe的实例,可以通过它劫持新增的元素数组(非常优雅 也有点恶心)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。