相关概念
数据驱动
学习 Vue 的时候经常能看到三个词:数据响应式、双向绑定、数据驱动。
- 数据响应式
- 数据模型仅仅是普通的 JavaScript 对象,而当我们改变数据的时候,视图会进行更新,避免了繁琐的 DOM 操作,提高开发效率。
- 双向绑定
- 数据改变,视图改变;视图改变,数据改变。
- 可以使用 v-model 在表单元素上创建双向数据绑定。
- 数据驱动
- Vue 最独特的特性之一,开发过程仅需要关注数据本身,不需要关心数据是如何渲染到视图的。
数据响应式的核心原理
Vue2.x
官方介绍:当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。
用 Object.defineProperty 定义的对象的属性,如果实现它的 getter 函数,在访问该属性时,会调用此函数;如果实现它的 setter 函数,当属性值被修改时,会调用此函数,函数被接收新值作为参数。
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>defineProperty 多个成员</title></head><body><div id="app">hello</div><script>// 模拟 Vue 中的 data 选项let data = {msg: 'hello',count: 10}// 模拟 Vue 的实例let vm = {}proxyData(data)function proxyData(data) {// 遍历 data 对象的所有属性Object.keys(data).forEach(key => {// 把 data 中的属性,转换成 vm 的 setter/setterObject.defineProperty(vm, key, {enumerable: true,configurable: true,get () {console.log('get: ', key, data[key])return data[key]},set (newValue) {console.log('set: ', key, newValue)if (newValue === data[key]) {return}data[key] = newValue// 数据更改,更新 DOM 的值document.querySelector('#app').textContent = data[key]}})})}// 测试vm.msg = 'Hello World'console.log(vm.msg)</script></body></html>
Vue3
官方介绍:当我们从一个组件的 data 函数中返回一个普通的 JavaScript 对象时,Vue 会将该对象包裹在一个带有 get 和 set 处理程序的 Proxy 中。Proxy 是在 ES6 中引入的,它使 Vue 3 避免了 Vue 早期版本中存在的一些响应性问题。Proxy 是一个对象,它包装了另一个对象,并允许你拦截对该对象的任何交互。
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>Proxy</title></head><body><div id="app">hello</div><script>// 模拟 Vue 中的 data 选项let data = {msg: 'hello',count: 0}// 模拟 Vue 实例let vm = new Proxy(data, {// 执行代理行为的函数// 当访问 vm 的成员会执行get (target, key) {console.log('get, key: ', key, target[key])return target[key]},// 当设置 vm 的成员会执行set (target, key, newValue) {console.log('set, key: ', key, newValue)if (target[key] === newValue) {return}target[key] = newValuedocument.querySelector('#app').textContent = target[key]}})// 测试vm.msg = 'Hello World'console.log(vm.msg)</script></body></html>
发布/订阅模式和观察者模式
发布/订阅模式
发布订阅模式有三个角色:发布者、订阅者、信号中心。发布者不会直接把信息发给订阅者,而是传给信号中心,由信号中心按需发给订阅者。
<!DOCTYPE html><html lang="cn"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>发布订阅模式</title></head><body><script>// 事件触发器class EventEmitter {constructor () {// { 'click': [fn1, fn2], 'change': [fn] }this.subs = Object.create(null)}// 注册事件(订阅消息) 的方法$on (eventType, handler) {this.subs[eventType] = this.subs[eventType] || []this.subs[eventType].push(handler)}// 触发事件(发布消息) 的方法$emit (eventType) {if (this.subs[eventType]) {this.subs[eventType].forEach(handler => {handler()})}}}// 测试let em = new EventEmitter()em.$on('click', () => {console.log('click1')})em.$on('click', () => {console.log('click2')})em.$emit('click') // click1 click2</script></body></html>
观察者模式
观察者模式没有中间的事件中心,只有发布者和观察者,并且发布者需要知道观察者的存在。所以发布者要存储观察者,当发布信息时,调用方法通知观察者。
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>观察者模式</title></head><body><script>// 发布者-目标class Dep {constructor () {// 记录所有的订阅者this.subs = []}// 添加订阅者addSub (sub) {if (sub && sub.update) {this.subs.push(sub)}}// 发布通知notify () {this.subs.forEach(sub => {sub.update()})}}// 订阅者-观察者class Watcher {update () {console.log('update')}}// 测试let dep = new Dep()let watcher = new Watcher()dep.addSub(watcher)dep.notify()</script></body></html>
总结
- 观察者模式是由具体目标调度,比如当事件触发,Dep 就会去调用观察者的方法,所以观察者模式的订阅者与发布者之间是存在依赖的。
- 发布/订阅模式由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在。

分析
Vue 的基本结构
首先 new 一个 Vue 实例,构造函数接收一个对象,这个对象有 el 和 data 属性。
<!DOCTYPE html><html lang="cn"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>Vue 基础结构</title></head><body><div id="app"><h1>插值表达式</h1><h3>{{ msg }}</h3><h3>{{ count }}</h3><h1>v-text</h1><div v-text="msg"></div><h1>v-model</h1><input type="text" v-model="msg"><input type="text" v-model="count"></div><script src="./js/vue.js"></script><script>let vm = new Vue({el: '#app',data: {msg: 'Hello Vue',count: 20,items: ['a', 'b', 'c']}})</script></body></html>
打印出这个 Vue 实例,我们要实现的是在实例上挂载一些属性:
- data中的成员以及它们的 getter 和 setter 函数。
- $data:这个对象里面存放了原来 data 中的成员以及它们的 getter 和 setter,这里的 setter 是真正监视数据变化的函数。
- $options:记录传入 Vue 构造函数的参数。
- $el:传入的参数中的 el 可以是选择器,也可以是 DOM 对象。$el 是一个 DOM 对象,如果传入的是选择器,需要获取对应的 DOM 对象。
一个最小版本的 Vue 的整体结构:

- Vue
- 把 data 中的成员注入到 Vue 实例,并且把 data 中的成员转成 getter/setter
- Observer
- 监听 data 中的所有属性,如有变动可拿到最新值并通知 Dep
- Compiler
- 解析每个元素中的指令/插值表达式,并替换成相应的数据
- Dep
- 添加观察者,当数据变化时通知观察者
- Watcher
- 数据变化时更新视图
Vue
功能
- 负责接收初始化的参数(选项)
- 负责把 data 的属性注入到 Vue 实例中,转换成 getter/setter
- 负责调用 observer 监听 data 中所有属性的变化
- 负责调用 compiler 解析指令/插值表达式
结构
Vue------------+ $options+ $el+ $data-------------- _proxyData()
实现
class Vue {constructor (options) {// 1. 通过属性保存选项的数据this.$options = options || {}this.$data = options.data || {}this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el// 2. 把data中的成员转换成getter和setter,并注入到vue实例中this._proxyData(this.$data)// 3. 调用observer对象,监听数据的变化new Observer(this.$data)// 4. 调用compiler对象,解析指令和差值表达式new Compiler(this)}_proxyData (data) {// 遍历data中的所有属性Object.keys(data).forEach(key => {// 把data的属性注入到vue实例中Object.defineProperty(this, key, {enumerable: true,configurable: true,get () {return data[key]},set (newValue) {if (newValue === data[key]) {return}data[key] = newValue}})})}}
Observer
功能
- 负责把 data 选项中的属性转换成响应式数据
- data 中的某个属性也是对象,把该属性也转换成响应式数据
- 数据变化发送通知
结构
Observer---------------+ walk(data)+ defineReactive(data, key, value)
实现
class Observer {constructor (data) {this.walk(data)}walk (data) {// 1. 判断data是否是对象if (!data || typeof data !== 'object') {return}// 2. 遍历data对象的所有属性Object.keys(data).forEach(key => {this.defineReactive(data, key, data[key])})}defineReactive (obj, key, val) {let that = this// 负责收集依赖,并发送通知let dep = new Dep()// 如果val是对象,把val内部的属性转换成响应式数据this.walk(val)Object.defineProperty(obj, key, {enumerable: true,configurable: true,get () {// 收集依赖Dep.target && dep.addSub(Dep.target)return val},set (newValue) {if (newValue === val) {return}val = newValuethat.walk(newValue) // 1. 这里直接使用this指向的是data 2.当值从一个基础类型改成对象,对象里的属性也要转换 getter/setter// 发送通知dep.notify()}})}}
Compiler
功能
- 负责编译模板,解析指令/插值表达式
- 负责页面的首次渲染
- 当数据变化后重新渲染视图
结构
Compiler---------------+ el+ vm--------------+ compile(el)+ compileElement(node)+ compileText(node)+ isDirective(attrName)+ isTextNode(node)+ isElementNode(node)
实现
class Compiler {constructor (vm) {this.el = vm.$elthis.vm = vmthis.compile(this.el)}// 编译模板,处理文本节点和元素节点compile (el) {let childNodes = el.childNodesArray.from(childNodes).forEach(node => {// 处理文本节点if (this.isTextNode(node)) {this.compileText(node)} else if (this.isElementNode(node)) {// 处理元素节点this.compileElement(node)}// 判断node节点,是否有子节点,如果有子节点,要递归调用compileif (node.childNodes && node.childNodes.length) {this.compile(node)}})}// 编译元素节点,处理指令compileElement (node) {// console.log(node.attributes)// 遍历所有的属性节点Array.from(node.attributes).forEach(attr => {// 判断是否是指令let attrName = attr.nameif (this.isDirective(attrName)) {// v-text --> textattrName = attrName.substr(2)let key = attr.valuethis.update(node, key, attrName)}})}update (node, key, attrName) {let updateFn = this[attrName + 'Updater']updateFn && updateFn.call(this, node, this.vm[key], key)}// 处理 v-text 指令textUpdater (node, value, key) {node.textContent = valuenew Watcher(this.vm, key, (newValue) => {node.textContent = newValue})}// v-modelmodelUpdater (node, value, key) {node.value = valuenew Watcher(this.vm, key, (newValue) => {node.value = newValue})// 双向绑定node.addEventListener('input', () => {this.vm[key] = node.value})}// 编译文本节点,处理差值表达式compileText (node) {// console.dir(node)// {{ msg }}let reg = /\{\{(.+?)\}\}/let value = node.textContentif (reg.test(value)) {let key = RegExp.$1.trim()node.textContent = value.replace(reg, this.vm[key])// 创建watcher对象,当数据改变更新视图new Watcher(this.vm, key, (newValue) => {node.textContent = newValue})}}// 判断元素属性是否是指令isDirective (attrName) {return attrName.startsWith('v-')}// 判断节点是否是文本节点isTextNode (node) {return node.nodeType === 3}// 判断节点是否是元素节点isElementNode (node) {return node.nodeType === 1}}
Dep

功能
- 收集依赖,添加观察者(watcher)
- 通知所有观察者
结构
Dep---------------+ subs--------------+ addSub(sub)+ notify()
实现
class Dep {constructor () {// 存储所有的观察者this.subs = []}// 添加观察者addSub (sub) {if (sub && sub.update) {this.subs.push(sub)}}// 发送通知notify () {this.subs.forEach(sub => {sub.update()})}}
Wacther

功能
- 当数据变化触发依赖,dep 通知所有的 Watcher 实例更新视图
- 自身实例化的时候往 dep 对象中添加自己
结构
Watcher---------------+ vm+ key+ cb+ oldValue--------------+ update()
实现
class Watcher {constructor (vm, key, cb) {this.vm = vm// data中的属性名称this.key = key// 回调函数负责更新视图this.cb = cb// 把watcher对象记录到Dep类的静态属性targetDep.target = this// 触发get方法,在get方法中会调用addSubthis.oldValue = vm[key]Dep.target = null}// 当数据发生变化的时候更新视图update () {let newValue = this.vm[this.key]if (this.oldValue === newValue) {return}this.cb(newValue)}}
总结

- Vue
- 记录传入的选项,设置 $data/$el
- 把 data 的成员注入到 Vue 实例
- 负责调用 Observer 实现数据响应式处理(数据劫持)
- 负责调用 Compiler 编译指令/插值表达式等
- Observer
- 数据劫持
- 负责把 data 中的成员转换成 getter/setter
- 负责把多层属性转换成 getter/setter
- 如果给属性赋值为新对象,把新对象的成员设置为 getter/setter
- 添加 Dep 和 Watcher 的依赖关系
- 数据变化发送通知
- 数据劫持
- Compiler
- 负责编译模板,解析指令/插值表达式
- 负责页面的首次渲染过程
- 当数据变化后重新渲染
- Dep
- 收集依赖,添加订阅者(watcher)
- 通知所有订阅者
- Watcher
- 自身实例化的时候往dep对象中添加自己
- 当数据变化dep通知所有的 Watcher 实例更新视图
Demo
<!DOCTYPE html><html lang="cn"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>Mini Vue</title></head><body><div id="app"><h1>差值表达式</h1><h3>{{ msg }}</h3><h3>{{ count }}</h3><h1>v-text</h1><div v-text="msg"></div><h1>v-model</h1><input type="text" v-model="msg"><input type="text" v-model="count"></div><script src="./js/dep.js"></script><script src="./js/watcher.js"></script><script src="./js/compiler.js"></script><script src="./js/observer.js"></script><script src="./js/vue.js"></script><script>let vm = new Vue({el: '#app',data: {msg: 'Hello Vue',count: 100,person: { name: 'zs' }}})console.log(vm.msg)// vm.msg = { test: 'Hello' }vm.test = 'abc'</script></body></html>
