vue2
是options API
的,在vue3
中 保留了
options api
无法tree-shaking
vue2
是一个构造函数
不是类(class),通过原型的方式
给vue
实现功能:vue.prototype = xxx
1、vue初始化
当用户 new Vue的时候 通过扩展原型的方法 调用_init方法进行vue的初始化
// vue.js
import {initMixin} from './init';
function Vue(options) {
this._init(options);
}
initMixin(Vue); // 给原型上新增_init方法
export default Vue;
复制代码
2、将用户输入的选项放到 vm.$options
代表 用户传入的所有属性
// init.js
import {initState} from './state';
export function initMixin(Vue){
// 后续组件开发的时候 Vue.extend 可以创建一个子组件。子组件可以继承Vue,子组件可以调用_init方法
Vue.prototype._init = function (options) {
const vm = this;
// 把用户的选项放到vm上,这样在其他方法中可以获取到options
vm.$options = options
// 初始化状态
initState(vm)
// 如果有 el要将数据挂载到页面上
if(vm.$options.el){
console.log('页面要挂载');
}
}
}
复制代码
3、初始化状态initState(vm)
(包含data、计算属性、方法等)
export function initState(vm){
const opts = vm.$options;
if(opts.props){
initProps(vm);
}
if(opts.methods){
initMethod(vm);
}
if(opts.data){
// 初始化data
initData(vm);
}
if(opts.computed){
initComputed(vm);
}
if(opts.watch){
initWatch(vm);
}
}
function initProps(){}
function initMethod(){}
function initData(){}
function initComputed(){}
function initWatch(){}s
复制代码
3.1、数据的初始化initData
,观测的是用户设置的data,和vm没关系
data
有两种情况 :要么是函数
要么是对象
,对data
类型进行判断 如果是 函数就获取函数返回值获取函数执行结果的时候 用
data.call(vm)
保证 data函数中的this
指向vue
实例 其实和返回值无关只有根实例中的data才能是对象
将data定义在vm._data
属性上将data和_data关联起来,方便在外部能用vm._data
获取到被观测过的数据,不写成 vm.data 是为了区分用户获取场景
// state.js initData 函数
import {observe} from './observer/index.js'
function initData(vm){
let data = vm.$options.data;
// 将data都定义在vm._data属性上
// 不写成 vm.data 是为了区分用户获取场景
data = vm._data = typeof data === 'function' ? data.call(vm) : data;
// 需要将data变成响应式的 使用Object.defineProperty 重写data中的所有属性
observe(data);
}
复制代码
使用defineProperty将_data
中的所有属性都代理到vm上,方便直接获取,’vm.nam === vm._data.name’
取值的时候做代理,不是直接把属性添加到vm上,而且直接赋值会出现命名冲突问题
// state.js proxy 函数
function proxy(vm,key,source){ //取值的时候做代理,不是直接把属性添加到vm上,而且直接赋值会出现命名冲突问题
Object.defineProperty(vm,key,{
get(){
return vm[source][key]
},
set(newValue){
vm[source][key] = newValue //vm.nam === vm._data.name
}
})
}
function initData(vm){
let data = vm.$options.data;
// 将data都定义在vm._data属性上
// 不写成 vm.data 是为了区分用户获取场景
data = vm._data = typeof data === 'function' ? data.call(vm) : data;
// 循环代理所有属性
for(let key in data){
proxy(vm,key,'_data')
}
// 需要将data变成响应式的 使用Object.defineProperty 重写data中的所有属性
observe(data);
}
复制代码
3.2、observe观测数据将data变成响应式的(对象类型拦截)
props
初始化在data
之前
只对
对象类型
进行观测非对象类型
无法观测直接return
通过
Observe类
来实现对数据的观测,类方便扩展,类会产生实例以作为唯一标识将对象中的所有
key
重新用defineProperty
定义为响应式的如果一个数据已经被观测过,就不要在进行观测,用类来实现,观测过一个数据就添加一个标识,说明观测过了,在观测的时候先检测是否观测过,如果观测过了就跳过
// 观测数据
export function observe(data){
// console.log(data,'observe')
if(typeof data !== 'object' || data == null){
return;
}
if(data.__ob__){ //数据上有这个属性表示已经观测过了 防止 循环引用
return;
}
// 通过类来实现对数据的观测,类方便扩展,类会产生实例以作为唯一标识
return new Observe(data)
}
复制代码
3.2.1、递归观测数据
vue2 应用了
defineProperty
需要一加载的时候
就进行递归操作,所以耗费性能,如果层次过深也会浪费性能如果给vm设置了一个新的对象类型数据,应该在
Object.defineProperty
的set
中再次劫持变成响应式的
Object.defineProperty
监控的是对象的属性,proxy
监控的是对象新设置的可能也是对象 也要递归观测数据
性能优化原则:
- 1)不要把所有的数据都放在data中,因为所有的数据都会增加get和set
- 2)数据尽量扁平化,不要嵌套过深,递归会耗费性能
- 3)不要频繁的获取被观测的数据,数据的get拦截器中会有很多操作
- 4)如果数据不需要响应式,用
Object.freeze
冻结属性,原理是将属性的configurable
设为false
// observe.js
class Observer { // 观测值
constructor(value){
this.walk(value);
}
walk(data){ // 使用 defineProperty 对对象上的所有属性依次进行观测
let keys = Object.keys(data);
for(let i = 0; i < keys.length; i++){
let key = keys[i];
let value = data[key];
defineReactive(data,key,value);
}
}
}
function defineReactive(data,key,value){// vue2慢的他原因主要就是这个方法的原因
// 形成了闭包 因为return了上个作用域的value
observe(value); // value可能也是对象 递归观测数据
Object.defineProperty(data,key,{
get(){
return value
},
set(newValue){
if(newValue == value) return;
observe(newValue); // 新设置的可能也是对象 递归观测数据
value = newValue
}
})
}
export function observe(data) {
if(typeof data !== 'object' || data == null){
return;
}
if(data.__ob__){ //数据上有这个属性表示已经观测过了 防止 循环引用
return;
}
// 通过类来实现对数据的观测;类:方便扩展,类会产生实例以作为唯一标识
return new Observer(data);
}
复制代码
3.3、数组类型拦截:通过改写数组原型,监听(改写)数组自身的方法来实现push、pop、shift、unshift、soplice、reverse、sort 七种方法
按照
3.2
的逻辑也用循环defineProperty
的话,虽然可以实现修改索引触发更新,但是,给数组的每一项都进行了数据劫持,这样是有性能问题的,而且数组的属性可能是各种各样的,比如: 函数、length因此源码中没有采用这个方法,所以 修改数组的索引或者length也不会触发视图更新(直接
arr[0]=xx
不能被监控)
class Observe {
constructor(value){
//* 数组如果也用循环defineProperty的话 每一项都会设置get和set 性能比较差
if(isArray(value)){
//更改数组原型方法
value.__proto__ = arrayMethods // 重写数组的方法
} else {
this.walk(value) //核心就是循环对象
}
}
walk(data){
Object.keys(data).forEach(key => { //使用 defineProperty从新定义
defineReactive(data,key,data[key])
})
}
}
复制代码
3.3.1、在监测数据的时候 对数组和对象分类处理,不能直接改写数组的方法,只有被vue控制的数组才需要改写
让arrayMethods 继承于Array.prototype,arrayMethods找不到的方法可以通过原型链(__proto__)去Array.prototype找
let oldArrayPrototype = Array.prototype;
let arrayMethods = Object.create(oldArrayPrototype); // 让arrayMethods 继承于Array.prototype,arrayMethods找不到去Array.prototype找
复制代码
不能直接改变原型方法
// 不能用 这是直接改变不是 继承
arrayMethods = oldArrayPrototype
复制代码
改写方法时使用 AOP切片编程,做一些操作以后 还是要调用数组原来的方法
比如新增,我们需要只需要在对应方法呗调用的时候去,将新增的数据放到一个数组中(也就是arguements),然后再调用observeArray就好了
let methods = [
`push`,
`pop`,
`shift`,
`unshift`,
`reverse`,
`sort`
]
methods.forEach(method => { //AOP切片编程
arrayMethods[method] = function(...args){
//重写数组方法
//todu...
let result = oldArrayPrototypeMethods[method].call(this,...args); //做一些操作以后 还是要调用数组原来的方法
return result
}
})
复制代码
3.3.2、数组中原有的对象或者数组,通过observeArray循环调用3.2
的observe方法
不用判断每一项的数据类型,因为上边的
observe
函数中已经做了判断如果数组中的属性是引用数据类型 那么是响应式的
// state.js 中 class Observe的constructor中
class Observer { // 观测值
constructor(value){
if(Array.isArray(value)){
value.__proto__ = arrayMethods; // 重写数组原型方法
this.observeArray(value); // 递归遍历数组,对数组内部的对象进行观测
}else{
this.walk(value);
}
}
observeArray(value){
for(let i = 0 ; i < value.length ;i ++){
observe(value[i]);
}
}
// 或者
observeArray(value){
value.forEach(item => observe(item))
}
}
复制代码
vm.arr[0].name = “aaa” 可以触发更新,走的是对象的逻辑,vm.arr[0] = “bb” 不行
vue2 无法劫持到不存在的属性,新增不存在的属性不会更新视图
vm.message = {name: 'aaa'}
vm.message.age = 18 // 不会触发更新
复制代码
3.3.3、 数组新增的对象需要被监测到
新增有两种:一种是
push、unshift
这种参数只有增加项的方法,一种是splice
这种参数有好项的增加方式因此需要获取到加入的数据,并通过
observeArray
拦截对数据进行拦截的时候是先赋值再通过
observeArray
拦截
增加__ob__属性 方便别的地方获取
value.__ob__ = this
; 这种写法会导致死循环 因为__ob__会被重复监听 所以要用Objecy.defineproperty改成不可枚举的
// index.js
class Observe {
constructor(value){
//给对象和数组 添加一个自定义属性 指向 察观类的实例 就可以通过__ob__获取到私有和公有方法
//value.__ob__ = this;
Object.defineProperty(value,'__ob__',{
enumerable:false, // 表示属性不能被枚举
configurable:false,
value:this
});
//* 数组如果也用循环defineProperty的话 每一项都会设置get和set 性能比较差
if(isArray(value)){
//更改数组原型方法
value.__proto__ = arrayMethods // 重写数组的方法
this.observeArray(value) // 处理数组中的对象
} else {
this.walk(value) //核心就是循环对象
}
}
observeArray(value){
...
}
walk(data){
...
}
}
复制代码
监听push、unshift、splice方法,如果有新增需要用observeArray 监听
// array.js
methods.forEach(method => { //AOP切片编程
arrayMethods[method] = function(...args){ //重写数组方法
//todu...
console.log('数组改变了');
// 数组新增的属性 要看一下是不是对象 是对象继续进行劫持
let inserted = null
switch(method){
case `push`: // 修改 删除 添加 arr.splice(位置,个数,添加内容)
inserted = args.slice(2)
case `pop`:
case `unshift`:
inserted = args //调用 push unshift 传递的参数 就是 新增的逻辑
break;
}
// 根据inserted是否有内容 来确定是否需要二次劫持
// 如果有内容 需要用observeArray 监听
// 通过 this.__ob__ 获取到 察观类的实例 然后再获取到公有方法 observeArray
// this是当前调用方法的数据(对象和数组)
let ob = this.__ob__
if(inserted) ob.observeArray(inserted)
let result = oldArrayPrototype[method].call(this,...args); //做一些操作以后 还是要调用数组原来的方法
return result
}
})
复制代码