前言
本文不扒源码,只需要基本得js知识就可以跟着写。跟着写可以体会一下Vue2得响应式实现。话不多说马上开始。
需求
首先我们要思考,响应式大概的效果是当js代码直接改变一个值时,页面中相应的内容需要及时更新。那要怎么知道js代码改变了一个值呢?没错,就是用defineProperty,用这个方法就可以在一个值改变时触发一个set的方法,我们可以在这个set方法里更新页面。当然,用Proxy也是一样的,这里就用defineProperty了。
响应式核心——defineProperty
想必看过点文章的都知道这个了,这里就不详细解释了,放个mdn的链接 defineProperty
先封装个方法
function defineReactive(obj, key) {
let val = obj[key]
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
//有的同学可能要问为什么要另外建一个变量来操作
//因为如果这里返回obj[key]会再次触发reactiveGetter方法,会套娃递归到爆栈为止
return val
},
set: function reactiveSetter (newVal) {
const value = val
//这里的判断是避免一样的值会重复触发后续的行为
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
val = newVal
}
})
}
响应方法有了,第二个问题是怎么知道要更新哪些视图,怎么更新。
如果我们把所有key对应要更新的视图保存一个对象,set的时候去找可以嘛。
{key:View}
当然可以,但是你写vue的时候有做过这种事嘛?没有,这太麻烦了。
其实我们拦截了set方法同时我们也拦截了get方法,当视图渲染时是必然会访问到变量而且触发get方法的,那我们就可以在get方法里把对应的视图关系保存好,在set里用。
响应式核心设计——观察者模式
很多人听到设计模式会有点懵,我们先忘掉这个词,把响应式实现出来时也许你就能理解了。
我们先实现一个类,在get里收集对应关系,在set里更新视图
function Dep() {
this.subs = []//视图列表
}
Dep.target = null
Dep.prototype.depend = function() {//收集对应关系
if(Dep.target) {
this.subs.push(Dep.target)
}
}
Dep.prototype.notify = function() {//通知视图更新
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
Dep就是depend的缩写,依赖的意思。
这里要特别讲解一下Dep.target这个全局属性,因为在get方法里,是没法传递视图进去的,所以必须通过一个全局属性来传递。update是视图更新的方法。
调整一下defineReactive方法
function defineReactive(obj, key) {
let val = obj[key]
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
dep.depend()
return val
},
set: function reactiveSetter (newVal) {
const value = val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
val = newVal
dep.notify()
}
})
}
接着我们来简单实现一个vue
function Vue(options) {
this._data = options.data
this.__ob__ = new Observer(this._data)//把_data设为响应式
Dep.target = this //把当前vue对象设为收集对象
this.render()
}
Vue.prototype.render = function () {
let app = document.getElementById('app')
app.innerHTML = null
let title = document.createElement('h4')
title.innerText = this._data.title
let content = document.createElement('p')
content.innerText = this._data.content
app.appendChild(title)
app.appendChild(content)
}
Vue.prototype.update = function () {
this.render()
}
function Observer(obj) {
this.obj = obj
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {//遍历key,把属性转为响应式
defineReactive(obj, keys[i])
}
}
html文件
<!DOCTYPE html>
<html>
<body>
<div id="app"></div>
</body>
</html>
<script src="index1.js"></script>
<script>
let vm = new Vue({
data: {
title: 'Hello world!',
content: 'Tech change the world'
}
})
</script>
ok我们看看效果
nice,基本效果已经有了。不过出现了第一个问题。
在Dep.prototype.notify方法里打印subs数组时发现,同一个vue对象被重复添加了。实际上,也只有在初次渲染时需要去添加vue对象,所以在Vue方法里需要在render后把Dep.target置空
function Vue(options) {
this._data = options.data
this.__ob__ = new Observer(this._data)//把_data设为响应式
Dep.target = this //把当前vue对象设为收集对象
this.render()
Dep.target = null
}
Ok问题解决。
接下来的问题是我们希望它角色分工更加明确,不要直接取vue对象来操作,那么我们新建一个类
function Watcher(vm) {
this.vm = vm
Dep.target = this
vm.render()
Dep.target = null
}
Watcher.prototype.update = function() {
this.vm.render()
}
这个就是观察者了,感受一下这个角色的逻辑。观察数据变化,通知视图更新。
调整一下vue方法
function Vue(options) {
this._data = options.data
this.__ob__ = new Observer(this._data)
this.watcher = new Watcher(this)
}
ok没问题。
相信大家大概可以理解这个观察者模式了把。