初始化项目
// 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>
<script type="module" src="./index.js"></script>
</body>
</html>
// index.js
import MyVue from './src/index.js'
new MyVue({
el: '#app',
data() {
return {
name: 'MyVue name'
}
},
// 验证数据效果
resultCb() {
console.log(this._data.name)
}
})
// .src/index.js
import { initState } from './state.js'
function MyVue(options) {
this._init(options)
}
MyVue.prototype._init = function(op) {
const vm = this
vm.$options = op
initState(vm)
}
export default MyVue
获取data
data在组件内必须是function,并且function内可以通过this访问methods等,但是data在new Vue的时候也可以是个普通对象,所以在取值的时候我们需要区分这2种情况
// ./src/state.js
export function initState(vm) {
const opts = vm.$options
if(opts.data) initData(vm, opts.data)
}
function initData(vm, data) {
data = vm._data = typeof data === 'function' ? data.call(vm) : data || {}
vm.$options.resultCb.call(vm)
}
// 'MyVue name'
代理
此时我们想获取data中的数据,需要通过this._data.xxx获取,显然这么写非常麻烦,最理想状态肯定是this.xxx 代替this._data.xxx获取数据,所以我们可以通过代理(Object.defineProperty)的方式解决,修改initData
// ./src/state.js
function initData(vm, data) {
data = vm._data = typeof data === 'function' ? data.call(vm) : data || {}
// 遍历data的所有属性,给每个属性添加代理
for(let key in data) {
proxy(vm, '_data', key)
}
vm.$options.resultCb.call(vm)
}
function proxy(target, source, key) {
Object.defineProperty(target, key, {
enumerable: true,
configurable: true,
get() {
// this = target
return this[source][key]
},
set(val) {
this[source][key] = val
}
})
}
然后我们修改一下resultCb的获取data方式
// index.js
import MyVue from './src/index.js'
new MyVue({
el: '#app',
data() {
return {
name: 11
}
},
// 验证数据效果
resultCb() {
console.log(this.name)
}
})
// 'MyVue name'
观察者模式
在说原理之前我们先说一下观察者模式和发布订阅模式。 能够直接接触到被观察的对象。减少了模块间的耦合问题,两个分离的、毫不相关的模块也能进行通信,但并未完全解耦,被观察者必须去维护一套观察者的集合,观察者必须实现统一的方法供被观察者调用,两者相互联系,用一个简化版本demo实现一下观察者模式
// 被观察者列表
class Sub{
arr = []
add(target){
this.arr.push(target)
}
depend(){
const w = new Watcher()
w.add(this)
}
notify(){
this.arr.forEach(w => w.update())
}
}
let wid = 1
// 观察者
class Watcher{
constructor(){
this.id = wid++
}
// 关联
add(target){
target.add(this)
}
// 统一更新方法
update(){
// do something
console.log(this.id)
}
}
var sub = new Sub()
sub.depend()
sub.depend()
sub.notify()
对象劫持
首先我们需要监听data中的取值和赋值,理所当然的我们会想到Object.defineProperty的get和set,Object.defineProperty只能监听单个属性,所以我们需要递归遍历每个属性
// ./src/state.js
import { observe } from './observe.js'
function initData(vm, data) {
data = vm._data = typeof data === 'function' ? data.call(vm) : data || {}
// 遍历data的所有属性,给每个属性添加代理
for(let key in data) {
proxy(vm, '_data', key)
}
// 劫持
observe(data, vm)
vm.$options.resultCb.call(vm)
}
为了更好的log劫持后的数据,我把resultCb的执行放到了劫持之后,然后我们看看observe的实现
// ./src/observe.js
export function observe(data, vm) {
// 是对象或者数组就处理,为了递归拦截不是对象或者数组的值
if (isObjectAndArray(data)) {
new Observer(data)
}
}
Observer内是为了处理对象和数组的差异,这里只实现对象的处理。其实就是遍历对象属性,然后给每一个属性添加get set
// ./src/observe.js
class Observer {
constructor(data) {
if (Array.isArray(data)) {
this.observeArray(data)
} else {
this.walk(data)
}
}
// 处理对象
walk(data) {
let keys = Object.keys(data)
for (let i = 0; i < keys.length; i++) {
defineReactive(data, keys[i])
}
}
// 处理数组
observeArray() {
}
}
function defineReactive(data, key) {
let value = data[key]
// 递归处理:{a: {b: 3}}
observe(value)
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get() {
console.log(value, 'get')
return value
},
set(val) {
if(val === value) return
console.log(val, 'set')
value = val
}
})
}
然后我们修改一下resultCb的回调
// index.js
import MyVue from './src/index.js'
new MyVue({
el: '#app',
data() {
return {
name: 'MyVue name'
}
},
// 验证数据效果
resultCb() {
console.log(this.name)
setTimeout(() => {
this.name = 'change'
console.log(this.name)
}, 1000);
}
})
/*
MyVue name get
MyVue name
change set
change get
change
*/
get执行,set也执行,值也没毛病,所以接下我们实现Dep和Watcher,我们先来看一张图
解释一下这张图,我们在初始化的时候劫持了所有的数据,所以在编译器阶段,我们会读取到某些数据,然后走到Observer,这时Dep去收集当前的Watcher(编译器Watcher),Watcher也会保存当前的Dep(去重,收集依赖),然后当值改变的时候,会触发收集到的Watcher(编译器Watcher)执行update,重新更新
然后我们先来熟悉一下几个关键词
- Dep:被观察者,收集当前key对应的所有Watcher
- Watcher:观察者,收集关联的Dep,执行更新,于Dep是多对多的关系
- Dep.target:保存进行中的Watcher
首先我们先看看如何维护Dep.target,其实就是维护一个栈,push和pop target。
// ./src/dep.js
// 进行中的Watcher
Dep.target = null
// 栈
const targetStack = []
export function pushTarget(target) {
targetStack.push(target)
Dep.target = target
}
export function popTarget() {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
然后修改一下defineReactive,给每个key创建一个Dep,用来收集Watcher
// ./src/observe.js
import { Dep } from './dep.js'
function defineReactive(data, key) {
let value = data[key]
// 创建dep
const dep = new Dep()
// 递归处理:{a: {b: 3}}
observe(value)
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get() {
// 存在进行中的Watcher
if(Dep.target) {
// 触发收集
dep.depend()
}
return value
},
set(val) {
if(val === value) return
value = val
// 值修改触发Watcher更新
dep.notify()
}
})
}
然后我们来看看Dep如何实现,其实和上文中观察者模式中的Sub实现基本一摸一样,只不过增加了id和修改了Watcher的创建方式
// ./src/dep.js
let uid = 1
export class Dep{
constructor() {
// 唯一id,用来去重
this.id = uid++
// watcher依赖
this.subs = []
}
// Watcher添加dep
depend() {
if (Dep.target) {
Dep.target.addSub(this)
}
}
// 执行收集到的Watcher的update触发更新
notify() {
this.subs.forEach(w => w.update())
}
// 添加Watcher
addSub(w) {
this.subs.push(w)
}
}
接下来实现Watcher,其实就是一个立即执行回调的构造函数,然后执行前pushTarget,执行完成后popTarget
// ./src/watcher.js
import { popTarget, pushTarget } from "./dep.js"
let uid = 1
export class Watcher{
constructor(vm, expOrFn) {
// 唯一id
this.id = uid++
// dep id集合
this.depIds = new Set()
// dep
this.deps = []
// 回调
this.getter = expOrFn
this.get()
}
get() {
// Dep.target等于当前Watcher
pushTarget(this)
// 执行回调
this.getter()
// 交还Dep.target
popTarget()
}
// 更新就是重新执行回调
update() {
this.get()
}
// watcher保存dep,dep添加watcher
addSub(dep) {
const id = dep.id
// 去重
if(!this.depIds.has(id)) {
this.depIds.add(id)
this.deps.push(dep)
// dep添加当前watcher
dep.addSub(this)
}
}
}
然后我们在Observer之后用Watcher模拟一下compiler,接着修改initData
// .src/state.js
function initData(vm, data) {
data = vm._data = typeof data === 'function' ? data.call(vm) : data || {}
// 遍历data的所有属性,给每个属性添加代理
for(let key in data) {
// if(methods && methods.hasOwnProperty(key)) {
// console.log(`data ${key}和methods函数重名了`)
// }
proxy(vm, '_data', key)
}
// 劫持
observe(data, vm)
// 模拟compiler,执行回调
new Watcher(vm, () => {vm.$options.resultCb.call(vm)})
}
接下来修改一下resultCb
// index.js
import MyVue from './src/index.js'
new MyVue({
el: '#app',
data() {
return {
name: 'MyVue name'
}
},
resultCb() {
// 获取name,1s后修改name的值,重新触发resultCb
console.log(this.name)
setTimeout(() => {
this.name = 'change'
// console.log(this.name)
}, 1000);
}
})
/*
MyVue name
change 1s后输出
*/
最后我们看看数组的劫持
数组劫持
数组劫持就没法使用Object.defineProperty了,我们可以劫持数组提供的几个会改变自身的方法(push pop unshift shift splice sort reserve),实现劫持的功能,来实现,打个比方
var obj = {
names: ['zm']
}
obj.names.push('yg')
那么我们是不是只要知道这个数组是不是发生了push就可以了,那么我们就可以使用AOP(在执行原有代码的基础之上再拓展所需要的功能),着手开始实现切片的代码。 首先我们先完善数组的处理
// ./src/observe.js
class Observer {
constructor(data) {
if (Array.isArray(data)) {
this.observeArray(data)
} else {
this.walk(data)
}
}
// 处理对象
walk(data) {
let keys = Object.keys(data)
for (let i = 0; i < keys.length; i++) {
defineReactive(data, keys[i])
}
}
// 处理数组
observeArray(data) {
// 循环数组每一项,递归处理
data.forEach(i => observe(i))
}
}
然后我们按上面说的,我们需要处理['zm']的原型,也就是constructor里的data,达到监听push的目的
// ./src/observe.js
import { arrayMethods } from './array.js'
class Observer {
constructor(data) {
if (Array.isArray(data)) {
// 修改数据原型
data.__proto__ = arrayMethods
this.observeArray(data)
} else {
this.walk(data)
}
}
// 处理对象
walk(data) {
let keys = Object.keys(data)
for (let i = 0; i < keys.length; i++) {
defineReactive(data, keys[i])
}
}
// 处理数组
observeArray(data) {
// 循环数组每一项,递归处理
data.forEach(i => observe(i))
}
}
接着看一下arrayMethods的实现
// 数组的原型
const arrayProto = Array.prototype
// 继承数组原型
// 原型式继承
// function create(target) {
// function f() {}
// f.prototype = target
// return new f()
// }
export const arrayMethods = Object.create(arrayProto)
// 会修改原数据的数组方法
const list = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
list.forEach(method => {
// 原数组方法
const original = arrayProto[method]
Object.defineProperty(arrayMethods, method, {
enumerable: true,
configurable: true,
value(...args) {
// 原型数组方法取值
const value = original.apply(this, args)
// 返回值
return value
}
})
})
到这一步我们就可以监听到push等这种方法,而且正常返回了值。
想一个问题,如果新加入的数据是对象,对象内某个属性修改了怎么办? 答案是执行Watcher update更新啊。 那么执行Watcher update的前提是经过Observer劫持,所以我们需要先劫持新增的数据,然后新增的数据也是数组,我们就需要经过Observer.observeArray处理,但是如何获取Observer.observeArray方法呢?
给data添加不可枚举的Observer啊,而且这么做还有一点好处是可以证明这个数据经过了响应式处理!所以再次修改Observer方法
// ./src/observe.js
import { arrayMethods } from './array.js'
class Observer {
constructor(data) {
// 数组dep
this.dep = new Dep()
// 给data添加不可枚举的__ob__属性,标记为响应式
insetOb(data, this)
if (Array.isArray(data)) {
// 修改数据原型
data.__proto__ = arrayMethods
this.observeArray(data)
} else {
this.walk(data)
}
}
// 处理对象
walk(data) {
let keys = Object.keys(data)
for (let i = 0; i < keys.length; i++) {
defineReactive(data, keys[i])
}
}
// 处理数组
observeArray(data) {
// 循环数组每一项,递归处理
data.forEach(i => observe(i))
}
insetOb(data, value) {
Object.defineProperty(data, '__ob__', {
configurable: true,
enumerable: false,
value
})
}
}
还有一点当前的数组数据更新了,我们也需要触发当前数组的Dep更新,所以我们完善一下方法
// 数组的原型
const arrayProto = Array.prototype
// 继承数组原型
// 原型式继承
// function create(target) {
// function f() {}
// f.prototype = target
// return new f()
// }
export const arrayMethods = Object.create(arrayProto)
// 会修改原数据的数组方法
const list = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
list.forEach(method => {
// 原数组方法
const original = arrayProto[method]
Object.defineProperty(arrayMethods, method, {
enumerable: true,
configurable: true,
value(...args) {
// 原型数组方法取值
const value = original.apply(this, args)
// Observer,this代表数据本身,比如{a:[1,2]},this代表a,__ob__在Observer中添加过,代表响应式
const ob = this.__ob__
let inset
// 截取新的数据
switch(method) {
case 'push':
case 'unshift':
inset = args
break;
case 'splice':
inset = args.slice(2)
break;
}
// 新数据响应式处理
if(inset) ob.observeArray(inset)
// 触发更新
ob.dep.notify()
// 返回值
return value
}
})
})
接下来实现一个数组的Watcher是如何收集的
{
ages: [1, [2], [3, [4, 5]]]
}
如果我们执行了ages.push(8),那么是不是可以理解为ages变了。所以说,数组里的所有Watcher其实和ages这个key的Watcher完全是一致的,我们只需要不断的重写每一层数组,和收集每一层数据对应的的Watcher,得到类似这种映射关系
ages -> 渲染Watcher
[1, [2], [3, [4, 5]]] -> 重写,dep保存渲染Watcher
[2] -> 重写,dep保存渲染Watcher
[3, [4, 5]] -> 重写,dep保存渲染Watcher
[4, 5] -> 重写,dep保存渲染Watcher
所以这时我们只需要不断重写,不断收集key的Watcher即可
import { Dep } from './dep.js'
import { arrayMethods } from './array.js'
export function observe(data, vm) {
// 是对象或者数组就处理
if (!isObjectAndArray(data)) return
// 如果存在__ob__属性,就证明已经是响应式,直接返回实例就可以
let ob
if (data.hasOwnProperty('__ob__') && data.__ob__ instanceof Observer) {
ob = data.__ob__
} else {
ob = new Observer(data)
}
return ob
}
class Observer {
constructor(data) {
// 数组dep
this.dep = new Dep()
// 给data添加不可枚举的__ob__属性,标记为响应式
this.insetOb(data, this)
if (Array.isArray(data)) {
// 修改数据原型
data.__proto__ = arrayMethods
this.observeArray(data)
} else {
this.walk(data)
}
}
// 处理对象
walk(data) {
let keys = Object.keys(data)
for (let i = 0; i < keys.length; i++) {
defineReactive(data, keys[i])
}
}
// 处理数组
observeArray(data) {
// 循环数组每一项,递归处理
data.forEach(i => observe(i))
}
insetOb(data, value) {
Object.defineProperty(data, '__ob__', {
configurable: true,
enumerable: false,
value
})
}
}
function defineReactive(data, key) {
let value = data[key]
// 创建dep
const dep = new Dep()
// 值的Observer实例
let childOb = observe(value)
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get() {
// 存在进行中的Watcher
if(Dep.target) {
// 触发收集
dep.depend()
// 如果存在值的Observer实例,也收集key对应的Watcher
if(childOb) {
childOb.dep.depend()
// [1, [2, [3]]],可能存在无限个嵌套数组,就无限收集Watcher
if(Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set(val) {
if(val === value) return
value = val
// 值修改触发Watcher更新
dep.notify()
}
})
}
function dependArray(val) {
for(let i=0;i<val.length;i++) {
let e = val[i]
e && e.__ob__ && e.__ob__.dep.depend()
if(Array.isArray(e)) {
dependArray(e)
}
}
}
function isObjectAndArray(data) {
return Array.isArray(data) || Object.prototype.toString.call(data) === '[object Object]'
}