相关概念
- 数据驱动
- 数据响应式
数据模型仅仅是普通js
对象,当我们修改数据时,更图会自动更新,避免了繁琐的DOM
操作,提高开发效率。 - 双向绑定
数据改变,视图随之发生改变;视图改变,数据也会发生改变。使用v-model在表单上创建双向绑定。 - 数据驱动
是Vue
最独特的特性之一,开发过程中只需要关注数据,不需要关心数据时如何渲染到视图。
- 数据响应式
- 响应式核心原理
-
Vue2.x
响应式核心原理
当把一个普通的JavaScript
对象传入Vue
实例作为data
选项,Vue
将遍历此对象所有的property
,并使用Object.defineProperty
把这些property
全部转为getter/setter
。Object.defineProperty
是ES5
中一个无法shim
的特性,这也就是Vue
不支持IE8
以及更低版本浏览器的原因。
Object.defineProperty(obj, prop, descriptor)
会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。obj
:要定义属性的对象prop
:要定义或修改的属性的名称或Symbol
descriptor
:要定义或修改的属性描述符。- 属性描述符有两种主要形式:数据描述符和存取描述符。一个描述符只能是这两者其中之一;不能同时是两者。
- 两种描述符都是对象。它们共享以下可选键值:
configurable
:键值为true
时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为false
。enumerable
:键值为true
时,该属性才会出现在对象的枚举属性中。默认为false
。
- 数据描述符还具有以下可选键值:
value
:该属性对应的值。可以是任何有效的JavaScript
值(数值,对象,函数等)。默认为undefined
。writable
:键值为true
时,属性的值,也就是上面的value
,才能被赋值运算符改变。默认为false
。
- 存取描述符还具有以下可选键值:
get
:属性的getter
函数,如果没有getter
,则为undefined
。当访问该属性时,会调用此函数。该函数的返回值会被用作属性的值。默认为undefined
。set
:属性的setter
函数,如果没有setter
,则为undefined
。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值)。默认为undefined
。
var data = { msg: 'Hello World' }; var vm = {} Object.defineProperty(vm, 'msg', { configurable: true, // 是否可配置 enumerable: true, // 是否可枚举 set(val) { console.log('set', val); if (val === data.msg) return; data.msg = val; document.querySelector('#app').textContent = data.msg; }, get() { console.log('get', data.msg); return data.msg; } })
-
Vue3.x
响应式核心原理
使用ES6
中新增的Proxy
,能直接监听对象,而不是属性,性能由浏览器优化,比Object.defineProperty
更好,IE
不支持。
Proxy
对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。const p = new Proxy(target, handler)
target
要使用Proxy
包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。handler
一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理p
的行为。是一个容纳一批特定属性的占位符对象。它包含有Proxy
的各个捕获器(trap)
。所有的捕捉器是可选的。如果没有定义某个捕捉器,那么就会保留源对象的默认行为。handler.defineProperty()
:Object.defineProperty
方法的捕捉器。handler.has()
:in
操作符的捕捉器。handler.get()
:属性读取操作的捕捉器。handler.set()
:属性设置操作的捕捉器。handler.deleteProperty()
:delete
操作符的捕捉器。handler.ownKeys()
:Object.getOwnPropertyNames
方法和Object.getOwnPropertySymbols
方法的捕捉器。handler.apply()
:函数调用操作的捕捉器。handler.construct()
:new
操作符的捕捉器。
var data = { msg: 'Hello World' }; const p = new Proxy(data, { get(target, key) { console.log('set', target, key); return target[key] }, set(target, key, val) { console.log('set', target, key, val); if (target[key] === val) reutrn; target[key] = val; document.querySelector('#app').textContent = val } })
-
- 发布订阅模式和观察者模式
-
发布订阅模式
假定存在一个信号中心
,某个任务执行完成时,就像信号中心发布
一个信号,其他任务可以向信号中心订阅
这个信号,从而知道自己什么时候开始执行。
在Vue
中可以简单的认为$.on
注册的事件是订阅者
,而$emit
注册的事件是发布者
。
模拟实现发布订阅模式:- 定义一个类,类中包含一个
$on
和$emit
函数,一个存储$on
定义的事件和回调函数的映射对象subs
,一个事件可能存在多个回调函数。 $on
订阅事件,接收事件名以及一个事件处理函数,然后将事件名和函数存储到subs
中。// 订阅 $on(eventType, handler) { // 将注册的事件和回调函数放到subs对象中 this.subs[eventType] = this.subs[eventType] || []; // 确保事件对应的值是一个数组。 this.subs[eventType].push(handler); }
$emit
发布事件,接受一个事件名,然后挨个执行对象中对应的所有的处理函数。// 发布,执行事件函数 $emit(eventType) { if (this.subs[eventType]) { this.subs[eventType].forEach(handler => { handler(); }); } }
- 定义一个类,类中包含一个
-
观察者模式
观察者模式和发布订阅模式类似,只是没有事件中心,只有发布者和订阅者,并且发布者需要知道订阅者的存在。- 在观察者模式中,订阅者,又叫
观察者(Watcher)
。观察者自身具有update
方法,负责当数据改变时更新视图。当事件发生时,会调用所有观察者的update
方法。// 观察者 class Watcher{ update() { console.log('watcher'); } }
- 在观察者模式中,发布者,又叫
观察目标(Dep)
。在观察目标内会记录所有观察者,当事件发生时,会调用所有观察者的update()
方法。// 观察目标 class Dep{ constructor() { this.subs = [] // 保存观察者 } addWatcher(sub) { if (sub.update) { this.subs.push(sub) } } notify() { this.subs.forEach(sub => { sub.update() }) } }
- 观察者模式怎么工作的呢?首先需要调用观察目标的添加观察者的方法,将观察者记录到观察目标中,然后当事件发生时,观察目标会通知所有记录中的观察者。
const watcher = new Watcher() const dep = new Dep() dep.addWatcher(watcher) dep.notify()
- 在观察者模式中,订阅者,又叫
-
模拟实现Vue
Vue
类的实现:-
Vue
类的功能:- 负责接收初始化的选项参数
- 负责把
data
中的属性注入到Vue
实例,转换成getter/setter
- 负责调用
observer
监听data
中所有属性的变化 - 负责调用
compiler
解析指令/差值表达式
-
Vue
类的结构:
$options
:是选项参数
$el
:实例的根DOM元素
$data
:data
副本
_proxyData()
:将data
中的属性转换成getter/setter
注入到Vue
实例中。 -
编码实现
- 实现构造函数,保存变量到实例,
data
属性转换并注入Vue
实例constructor(options) { // 保存选项参数 this.$options = options || {} // 保存data this.$data = options.data || {} // 保存 el,如果el是字符串,表示传入另一个选择器,否则人为传入了DOM元素 this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el // 将data中的属性转换为getter/setter,并注入到实例中 this._proxyData(this.$data) }
- 实现
data
属性转换并注入Vue
实例 的方法_proxyData(data) { Object.keys(data).forEach(key => { Object.defineProperty(this, key, { // 是否可配置 configurable: true, // 是否可枚举 enumerable: true, // 拦截赋值 set(newVal) { if (newVal === data[key]) return data[key] = newVal }, // 拦截获取值 get() { return data[key] } }) }) }
- 实现构造函数,保存变量到实例,
-
Observer
类- 负责把
vue.$data
对象中的属性转换成响应式数据(getter/setter) vue.$data
中的某个属性是对象时,也需要把该属性转换成响应式数据- 数据变化发送通知
- 类结构
walk
:遍历data
defineReactive
:将data
中的属性转换成getter/setter
。value === data[key]
。这里为什么要传递value
呢,因为经过Object.defineProperty
之后,每次obj[key]
都会去调用get
方法,如果get
里面return obj[key]
,就会出现get
的循环调用。- 当
data
中的属性为一个对象时,该对象也要是响应式的。 - 如果给data中的属性重新赋值为一个对象,该对象也要是响应式的。
- 代码实现
/** * 将vm.$data转换为响应式数据 */ class Observer { constructor(data) { this.walk(data) } /** * 遍历data * @param {} data */ walk(data) { // 判断data是否为空和是否为对象 if(!data && typeof data !== 'object') return Object.keys(data).forEach(key => { this.defineReactive(data, key, data[key]) }) } /** * 将对象的属性转化为getter/setter * @param {对象} obj * @param {要转化的属性} key * @param {对应的旧值} val */ defineReactive(obj, key, val) { // val === obej[key] 这里为什么要传递val,因为经过Object.defineProperty之后,每次obj[key]都会去调用get方法,如果get里面return obj[key],就会出现get的循环调用。 const _this = this // 如果val是一个对象,需要把val也转换成响应式的对象 this.walk(val) Object.defineProperty(obj, key, { configurable: true, enumerable: true, set(newVal) { if (newVal === val) return val = newVal // 如果newVal是一个对象,需要把newVal也转换成一个响应式的对象 _this.walk(newVal) // 发送通知 }, get() { return val } }) } }
- 负责把
compiler
类-
功能
- 负责编译模板,解析指令/差值表达式
- 负责页面的首次渲染
- 当数据变化时重新渲染视图
-
结构
-
代码实现
- 构造函数
constructor
:保存vm
和el
,调用compile
编译el
constructor(vm) { this.vm = vm this.el = vm.$el this.compile(this.el) this.initDirectives() } // 初始化指令处理函数,为每个处理函数绑定当前作用域 initDirectives() { Object.keys(directives).forEach(d => { directives[d] = directives[d].bind(this) }); }
compile
:获取子节点,遍历子节点,判断子节点类型,并使用对应的编译函数编译子节点。compile(el) { // 获取根节点下所有子节点 const childNodes = el.childNodes || [] if (!childNodes.length) return // 遍历子元素,判断子节点类型,并根据类型解析子节点。childNodes是个类数组,需要通过Array.from转化为数组 Array.from(childNodes).forEach(node => { if (this.isTextNode(node)) {// 处理文本节点 this.compileText(node) } else if(this.isElementNode(node)) { // 处理元素节点 this.compileElement(node) } // 调用一下compile编译node节点的子节点 this.compile(node) }) }
compileElement
:编译元素节点。编译指令,先获取所有属性,然后判断是否是指令,如果是指令,则调用响应的指令处理函数处理。这两把指令处理函数放到一个指令与处理函数对应的键值对象中,这样在判断属性是指令之后可以直接调用指令对应的处理函数,不过执行构造函数时需要将处理函数的作用域改为当前compiler
对象。compileElement(node) { // 获取元素属性列表, const attrs = node.attributes console.log(attrs); Array.from(attrs).forEach(attr => { const attrName = attr.name // 属性名 if (this.isDirective(attrName)) { // 判断是否是指令 const attrValue = attr.value // 属性值 directives[attrName](node, this.vm[attrValue]) } }) }
compileText
:编译文本节点,处理差值表达式。定义差值表达式正则,并通过正则捕获来获取到差值表达式内的属性名,然后将属性名对应的值替换差值表达式的位置放入到显示文本中。compileText(node) { const reg = /\{\{(.+?)\}\}/ const text = node.textContent if (reg.test(text)) { // 如果是差值表达式,则通过RegExp.$1来获取匹配到的key const key = RegExp.$1.trim() // 将值替换掉差值表达式显示在视图上 node.textContent = text.replace(reg, this.vm[key]) } }
isDirective
:判断某个属性是否是指令。isDirective(attr) { return attr.startsWith('v-') }
isTextNode
:判断某个节点是否是文本节点,文本节点的nodeType
为3
。isTextNode(node) { return node.nodeType === 3 }
isElementNode
:判断某个节点是否是元素节点,元素节点的nodeType
为1
。isElementNode(node) { return node.nodeType === 1 }
- 构造函数
-
Dep类
观察者模式中的观察目标- 功能
- 记录所有的观察者
- 数据更新时通知所有观察者
- 使用
- 首先
Vue
中的观察目标是响应式对象的属性,所以在Observer
中defineReactive
将对象的属性转换为getter/setter
时,需要为此属性创建观察者对象dep
, - 然后在
defineReactive
转化的getter
方法中,将观察者添加到观察目标的观察者数组中。 - 在数据更新时,也就是
setter
方法中需要调用dep.notify
方法来通知观察者。
- 首先
- 代码实现,及使用代码
更新后的/** * 观察者, * 1、数据更新时更新视图 * 2、自身实例化时向Dep添加自己 */ class Watcher { constructor(vm, key, cb) { this.vm = vm this.key = key this.cb = cb // 向Dep添加自己 Dep.target = this // 将Dep的target设置为自己 // 需要触发添加,vm[key]时会触发observer中的get this.oldVal = vm[key] Dep.target = null // 防止重复添加 } update() { const newValue = this.vm[this.key] if(this.oldVal === newValue) return this.cb(newValue) } }
defineReactive
defineReactive(obj, key, val) { // val === obej[key] 这里为什么要传递val,因为经过Object.defineProperty之后,每次obj[key]都会去调用get方法,如果get里面return obj[key],就会出现get的循环调用。 const _this = this // 如果val是一个对象,需要把val也转换成响应式的对象 this.walk(val) // 每个双向绑定数据都是观察目标 const dep = new Dep() Object.defineProperty(obj, key, { configurable: true, enumerable: true, set(newVal) { if (newVal === val) return val = newVal // 如果newVal是一个对象,需要把newVal也转换成一个响应式的对象 _this.walk(newVal) // 发送通知 dep.notify() }, get() { // 添加观察者 Dep.target && dep.addSub(Dep.target) return val } }) }
- 功能
Watcher类
观察者模式的观察者-
功能
- 数据更新时更新视图
- 自身实例化时向
Dep
添加自己
-
使用
- 观察者的功能主要是当数据更新时更新视图, 所以
Watcher
应该在处理响应式视图时创建,及Compiler
类中。 Compiler
类在处理文本节点时,将差值表达式替换为对应属值时,若数据更新,需要将属性的最新值替换到差值表达式的位置。Compiler
类在处理元素节点时,数据的渲染在于指令的处理方法中,所以需要在指令的处理方法中添加数据更新时的处理,即创建一个Watcher
对象。
- 观察者的功能主要是当数据更新时更新视图, 所以
-
代码实现
/** * 观察者, * 1、数据更新时更新视图 * 2、自身实例化时向Dep添加自己 */ class Watcher { constructor(vm, key, cb) { this.vm = vm this.key = key this.cb = cb // 向Dep添加自己 Dep.target = this // 将Dep的target设置为自己 // 需要触发添加,vm[key]时会触发observer中的get this.oldVal = vm[key] Dep.target = null // 防止重复添加 } update() { const newValue = this.vm[this.key] if(this.oldVal === newValue) return this.cb(newValue) } }
Compiler类
中文本节点的处理// 编译文本节点 compileText(node) { const reg = /\{\{(.+?)\}\}/ const text = node.textContent if (reg.test(text)) { // 如果是差值表达式,则通过RegExp.$1来获取匹配到的key const key = RegExp.$1.trim() // 将值替换掉差值表达式显示在视图上 node.textContent = text.replace(reg, this.vm[key]) new Watcher(this.vm, key, newValue => { node.textContent = text.replace(reg, newValue) }) } }
指令处理函数中创建
Watcher
对象。const directives = { 'v-text' : function(node, key, value) { // 更新node的nodeContent属性 node.textContent = value console.log(this); new Watcher(this.vm, key, value => { node.textContent = value }) }, 'v-model': function(node, key, value) { // 更新node的value属性 node.value = value new Watcher(this.vm, key, value => { node.value = value }) } }
-
- 数据的双向绑定
数据的双向绑定包含两部分:
1. 数据更新,视图随之更新。上面的代码已经实现这一部分。
2. 视图更新,数据随之更新。视图更新主要通过v-mode
l绑定的表单元素来实现,只要在v-model
的处理函数中,为node
节点添加一个input
事件的监听即可,在事件的回调函数中,将node
节点的值赋值给属性即可。'v-model': function(node, key, value) { // 更新node的value属性 node.value = value new Watcher(this.vm, key, value => { node.value = value }) node.addEventListener('input', () => { this.vm[key] = node.value }) }