手写Vue之核心梳理
本篇文件来记录Vue双向数据绑定的实现。
一、知识点
- 什么是双向数据绑定(MVVM)?
MVVM分别表示Model View View-Model,即模型(数据访问层)、视图(界面)、视图模型(模型和视图的通信),是一种软件架构模式。
View层接收到交互信息,通过View-Model更新Model数据,同样,当Model数据发生变化后(一般是请求后端数据)通知View-Model使得视图发生更新。从而实现双向数据监听,并修改视图或者模型,这就是MVVM模式。 - Vue是如何实现双向数据绑定的?
实现双向数据绑定的关键在于如何监听数据发生了变化,Vue2.x及以前版本通过Javascript
内置标准对象的Object.defineProperty()
方法实现。由于Object.defineProperty()
无法监听到数组更新(准确来说是通过length增加长度监听不到),所以Vue3.x使用Proxy
作为新的数据监听方案,Proxy
可以监听到整个对象的变更。 Object.defineProperty()
此方法可直接在一个对象上定义一个新的属性,或者修改对象的现有属性,并返回此对象,MDN传送门。// 添加属性 const obj = {} Object.defineProperty(obj, 'name', { value: 'hua', writable: false }) console.log(obj.name) // hua // getter setter,监听数据变化就借助这两个函数 const obj = {} let age = 18 Object.defineProperty(obj, 'age', { get() { console.log('get data') return age }, set(newValue) { console.log('set data') age = newValue } }) obj.age = 16 // 输出:set data console.log(obj.age) // 先输出:get data 再输出:16
Proxy
此对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)MDN传送门。// 赋值转发到target const target = {} const p = new Proxy(target, {}) p.name = 'hua' console.log(target.name) // hua // 拦截 const target = {} const p = new Proxy(target, { get(obj, prop) { return obj[prop] }, set(obj, prop, value) { if (prop == 'name' && typeof value == 'string') { obj[prop] = value } return true } }) p.name = 66 console.log(target.name) // undefined p.name = 'hua' console.log(target.name) // hua
二、实现
按照MVVM模式进行分解实现,即先实现View触发Model更新,再实现Model变化而更新View。
-
创建项目
我们先创建mvvm项目,在项目下创建vue.js
和index.html
,分别作为Vue双向数据绑定“引擎”和Vue的使用场景。 -
回忆如何使用Vue创建项目
我们先来看以下代码:<div id="app"> {{ message }} </div>
const app = new Vue({ el: '#app', data: { message: 'Hello Vue' } })
是不是很熟悉,这个是Vue官网提供的声明式渲染,我们也将以此开始我们Vue双向数据绑定的开始。
-
构造函数(类)Vue
我们发现,Vue其实就是一个构造函数,接受一个对象作为参数,然后将message的值赋值给了id为app的div,那么我们来实现function Vue(op) { const el = document.querySelector(op.el) el.innerHTML = op.data.meaasge }
至此,我们可以在页面上看到效果:
到这里,我们以及有了一丝丝进步,但是接下来我们要实现在输入框输入之后,输入信息在div中显示出来。 -
input输入改变div中的值
- 改造html
<input /> <div id="app"></div>
- js操作Dom实现
const app = new Vue({ el: '#app', data: { message: 'Hello Vue' } }) const input = document.querySelector('input') const el = document.querySelector('#app') input.oninput = function(e) { el.innerHTML = e.target.value }
至此,我们实现了input输入改变div内容的需求,但是会发现,这个改变与Vue并没有关系。那如果与Vue有关我们应该怎么做呢?我们发现,在Vue构造函数中,为div初始化内容的时候是这样一段代码
el.innerHTML = op.data.meaasge
,也就是说,我们可以通过改变op.data.meaasge
的值达到我们想要的结果。因此,我们可以这样改进代码:const app = new Vue({ el: '#app', data: { message: 'Hello Vue' } }) const input = document.querySelector('input') const el = document.querySelector('#app') input.oninput = function(e) { app.data.message = e.target.value }
但是,当我们这样改之后,并没有达到我们的效果,为什么呢?因为最终要改变div中的值,还是的借助
innerHTML
实现,那么我们在什么时候去调用该方法呢?这里就需要利用对data
进行劫持来触发。 -
Object.defineProperty
数据劫持
我们继续来改造我们的构造函数Vue,监听data的改变,然后调用innerHTML
进行赋值。function observe(obj, el) { if (typeof obj !== 'object') { return } for (let key in obj) { observe(obj[key]) let temp = obj[key] Object.defineProperty(obj, key, { get() { return temp }, set(newValue) { if (temp !== newValue) { console.log('data changed', newValue) temp = newValue el.innerHTML = temp } } }) } } function Vue(op) { const el = document.querySelector(op.el) this.data = op.data // 数据监听 observe(op.data, el) // 初始化 el.innerHTML = op.data.message }
至此,我们实现了通过监听data变化而改变div内容的需求,但是你会发现,这里只要有数据发生变化了,都会改变div中的内容,如果我们只想在message改变的时候才改变div中的内容,怎么办呢?接下来我们来引入我们的监听者Watcher,完善我们的Vue.
-
订阅者Watcher、订阅器Dep、拦截器observe
// 拦截器 function observe(obj, el) { if (typeof obj !== 'object') { return } for (let key in obj) { observe(obj[key]) let temp = obj[key] let dep = new Dep() Object.defineProperty(obj, key, { get() { // 将watcher添加到订阅器中 dep.depend() return temp }, set(newValue) { if (temp !== newValue) { temp = newValue dep.notify() } } }) } } // 会有多个订阅者,所以使用订阅器进行管理 class Dep { constructor() { this.subs = [] } // 添加订阅者 depend() { if (Dep.target) { this.addSubs(Dep.target) } } // 通知订阅者更新 notify() { this.subs.forEach(item => { item.update() }) } addSubs(sub) { this.subs.push(sub) } } Dep.target = null // 订阅者 class Watcher { constructor(vm, key, cb) { this.vm = vm this.key = key this.cb = cb this.value = this.get() } // 订阅者更新视图 update() { const newValue = this.vm.data[this.key] const oldValue = this.value if (newValue !== oldValue) { // 这里相当于将cb添加到vm对象,然后进行调用 this.cb.call(this.vm, newValue, oldValue) } } // 初始化value,并利用闭包形式将watcher绑定到订阅器中 get () { Dep.target = this const value = this.vm.data[this.key] Dep.target = null return value } } function Vue(op) { const el = document.querySelector(op.el) this.data = op.data // 数据监听 observe(op.data, el) // 初始化 el.innerHTML = op.data.message // 监听message变化,只有message变化才能触发 new Watcher(this, 'message', function(newValue, ondValue) { // 在这里对我们的div进行内容赋值 el.innerHTML = newValue }) }
到这里,我们的Vue双向数据绑定已经完成了,让我们一起测试我们的代码吧!
-
测试
- View改变Model
输入框输入,div内容随之变化 - Model改变View
const app = new Vue({ el: '#app', data: { message: 'Hello Vue' } }) const input = document.querySelector('input') const el = document.querySelector('#app') input.oninput = function(e) { app.data.message = e.target.value } setTimeout(function() { app.data.message = '过了两秒执行' }, 2000)
- View改变Model