手写vue响应式原理
首先我们看看原生 vue 做了什么
<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.14/vue.js"></script>
<script>
const vm = new Vue({
data: {
name: '小米',
arr:[{a:2}] // 检测深度响应式
}
})
console.log('vm', vm)
</script>
可见 vm 第一层与 _data 内都能获取到 data 数据, 并且其数值都进行了 get set 响应式处理
接下来我们动手实现 vue 的这个步骤!
vue 响应式原理
目录结构:
├── dist // 打包存放的目录
├── public // 静态资源文件
│ └── index.html
├── src
│ ├── observe
│ │ ├── array.js // 操作数组数据响应式
│ │ └── index.js // 数据响应式
│ ├── index.js // 导出 vue 构造函数
│ ├── init.js // 初始化vue状态
│ └── state.js // 据不同属性进行初始化操作
├── .babelrc // babel打包配置
├── package.json
└── rollup.config.js
/src/index.js (导出vue
构造函数)
import {initMixin} from './init';
function Vue(options) {
this._init(options);
}
initMixin(Vue); // 给原型上新增_init方法
export default Vue;
/src/init.js (init
方法中初始化vue
状态)
import { initState } from "./state";
export function initMixin(Vue) {
Vue.prototype._init = function(options) {
const vm = this;
vm.$options = options // 所有后续的扩展方法都有一个$options选项可以获取用户的所有选项
// 对于实例的数据源 props data methods computed watch
initState(vm);
}
/src/state.js (根据不同属性进行初始化操作)
这里开始对数据进行响应式处理
import { observe } from "./observe/index";
export function initState(vm) {
const options = vm.$options
// 后续实现计算属性 、 watcher 、 props 、methods
if (options.data) {
initData(vm);
}
}
function proxy(vm, source, key) {
Object.defineProperty(vm, key, {
get() {
return vm[source][key]
},
set(newValue) {
vm[source][key] = newValue;
}
})
}
function initData(vm) {
let data = vm.$options.data;
// 如果是函数就拿到函数的返回值 否则就直接采用data作为数据源
data = vm._data = typeof data === 'function' ? data.call(vm) : data
// 属性劫持 采用defineProperty将所有的属性进行劫持
// 我期望用户可以直接通过 vm.xxx 获取值, 也可以这样取值 vm._data.xxx
for (let key in data) {
proxy(vm, '_data', key)
}
observe(data)
}
src/observe/index.js (实现数据响应式)
对数据递归操作实现所有数据都响应式
import arrayPrototype from "./array";
class Observer{
constructor(data){
// 如果是数组的话也是用defineProperty会浪费很多性能 很少用户会通过arr[1000] = 1234
// vue3 中的 polyfill 直接就给数组做代理了
// 改写数组的方法,如果用户调用了可以改写数组方法的api 那么我就去劫持这个方法
// 变异方法 push pop shift unshift reverse sort splice
Object.defineProperty(data,'__ob__',{
value:this,
enumerable:false
})
// 如果有__ob__属性 说明被观测过了
// 修改数组的索引和长度是无法更新视图的
if(Array.isArray(data)){
// 需要重写这7个方法
data.__proto__ = arrayPrototype;
// 直接将属性赋值给这个对象
// 如果数组里面放的是对象类型 我期望他也会被变成响应式的
this.observeArray(data);
}else{
this.walk(data)
}
}
observeArray(data){
data.forEach(item=> observe(item)); //如果是对象我才进行观测了
}
walk(data){ // 循环对象 尽量不用for in (会遍历原型链)
let keys = Object.keys(data); // [0,1,2]
keys.forEach(key=> { //没有重写数组里的每一项
defineReactive(data,key,data[key])
})
}
}
// 性能不好的原因在于 所有的属性都被重新定义了一遍
// 一上来需要将对象深度代理 性能差
function defineReactive(data,key,value){ // 闭包
// 属性会全部被重写增加了get和set
observe(value); // 递归代理属性
Object.defineProperty(data,key,{
get(){ // vm.xxx
return value;
},
set(newValue){ // vm.xxx = {a:1} 赋值一个对象的话 也可以实现响应式数据
if(newValue === value) return
observe(newValue)
value = newValue;
}
})
}
export function observe(data) {
if(typeof data !== 'object' || data == null){
return ; // 如果不是对象类型,那么不要做任何处理
}
if(data.__ob__){
// 说明这个属性已经被代理过了
return data
}
// 我稍后要区分 如果一个对象已经被观测了,就不要再次被观测了
// __ob__ 标识是否有被观测过
return new Observer(data)
};
src/observe/array.js (操作数组数据响应式)
目的就是对数组内新增的数据再次进行观测 避免里面出现没有监听到的对象数据
let oldArrayPrototype = Array.prototype;
// arrayProptotype.__proto__ = Array.prototype;
let arrayPrototype = Object.create(oldArrayPrototype);
let methods = [
'push',
'pop',
'shift',
'unshift',
'reverse',
'sort',
'splice'
]
// 重写数组 7 方法 (目的就是对新增的数据再次进行观测 避免里面出现没有监听到的对象数据)
// 如 arr[1000] = 1234 更改数组会响应式是通过 $set 实现的
methods.forEach(method => { // 用户调用push方法会先经历我自己重写的方法,之后调用数组原来的方法
arrayPrototype[method] = function(...args) {
let inserted;
let ob = this.__ob__;
switch (method) {
case 'push':
case 'unshift':
inserted = args; // 数组
break;
case 'splice': // arr.splice(1,1,xxx)
inserted = args.slice(2); // 接去掉前两个参数
default:
break
}
if (inserted) {
// 对新增的数据再次进行观测
ob.observeArray(inserted)
}
return oldArrayPrototype[method].call(this, ...args)
}
})
export default arrayPrototype
手写 vue 代码仓库 | 链接 |
---|---|
GitHub | https://github.com/shunyue1320/vue-resolve/tree/vue-01 |
Gitee | https://gitee.com/shunyue/vue-resolve/tree/vue-01/ |