MVVM是将数据模型数据双向绑定的思想作为核心,因此在View和Model之间没有联系,通过ViewModel进行交互,而且Model和ViewModel之间的交互是双向的,因此视图的数据的变化会同时修改数据源,而数据源数据的变化也会立即反应到View上。
了解
① 目的
- 能够掌握Vue中的双向数据绑定原理以及核心代码模块
- 实现一个简单的vue框架
- 学习ES6语法
- 对于Vue框架掌握的更加牢固
- 使用Vue开发时,遇到bug可以快速定位
- 了解框架底层原理,面试加分
- 代码摘抄自Vue的源码
② 概念
M(Model,模型层 ),
V(View,视图层),
VM(ViewModel,视图模型,V与M连接的桥梁)
MVVM框架实现了数据双向绑定
当M层数据进行修改时,VM层会监测到变化,并且通知V层进行相应的修改
修改V层则会通知M层数据进行修改
MVVM框架实现了视图与模型层的相互解耦
③ 双向数据绑定方式
-
发布-订阅者模式
Backbone一般通过pub、sub的方式来实现数据和视图的绑定,但是使用起来比较麻烦
-
脏值检查
Angular 是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图。类似于通过定时器轮训检测数据是否发生了改变。
-
数据劫持
Vue 则是采用数据劫持结合发布者-订阅者模式的方式。通过Object.defineProperty()来劫持各个属性的setter和getter**,在数据变动时发布消息给订阅者,触发相应的监听回调。
④ 需求
- 实现一个Compiler模板解析器,能够对模版中的指令和插值表达式进行解析,并且赋予不同的操作
- 实现一个Observer数据监听器,能够对数据对象的所有属性进行监听
- 实现一个Watcher观察者,将Compile的解析结果,与Observer所观察的对象连接起来,建立关系,在Observer观察到对象数据变化时,接收通知,同时更新DOM
- 创建一个公共的入口对象,接收初始化的配置并且协调上面三个模块,既Vue
小架子
index.html
<!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>MVVM-dome</title>
</head>
<body>
<div id="app">
<p></p>
<p v-text = 'msg'> </p>
</div>
<script src="./src/vue.js"></script>
<script src="./src/compile.js"></script>
<script>
// const vm1 = new Vue()
const vm = new Vue({
el: '$app',
data: {
msg: 'hello'
}
})
console.log(vm)
</script>
</body>
</html>
src/vue.js
/**
* 定义一个类 Vue的构造函数,用于创建Vue实例
*/
class Vue {
constructor(options) {
// 设值默认参数
options = options || {}
// 给Vue添加属性
this.$el = options.el
this.$data = options.data
// Compiler主要负责解析模板指令和插值表达式,将模板中的变量转换成数据. 然后渲染整个页面和视图
if(this.$el) {
new Compile(this.$el, this)
}
}
}
src/compile.js
/**
* 负责解析模板内容
*/
class Compile{
constructor(el ,vm ) { }
}
compile
① fragment文档碎片
语法
let fragment = document.createDocumentFragment();
fragment
是一个指向空DocumentFragment
对象的引用。
描述
DocumentFragments` 是DOM节点。它们不是主DOM树的一部分。通常的用例是创建文档片段,将元素附加到文档片段,然后将文档片段附加到DOM树。在DOM树中,文档片段被其所有的子元素所代替。
因为文档片段存在于内存中,并不在DOM树中,所以将子元素插入到文档片段时不会引起页面回流(对元素位置和几何上的计算)。因此,使用文档片段通常会带来更好的性能。
compile.js
/**
* 负责解析模板内容
* @param el {object} new Vue 选择器
* @param vm {object} new Vue Vue实例
*/
class Compile{
constructor(el ,vm ) {
// 参数
this.el = typeof el === 'string' ? document.querySelector(el) : el
this.vm = vm
// 编模模板
if(this.el) {
}
}
}
vue.js
/**
* 定义一个类 Vue的构造函数,用于创建Vue实例
*/
class Vue {
constructor(options) {
// 设值默认参数
options = options || {}
// 给Vue添加属性
this.$el = options.el
this.$data = options.data
this.$methods = options.methods
// Compiler主要负责解析模板指令和插值表达式,将模板中的变量转换成数据. 然后渲染整个页面和视图
if(this.$el) {
// this == 整个Vue实例
let c = new Compile(this.$el, this)
console.log(c)
}
}
}
compile.js
- 把el中所有的子节点放入内存中 fragmen
- 在内存中编译fragment
- 创建node2Fragment的核心方法
- 创建toArray的工具方法
- 打印consol.dir(fragment) 查看控制台内容把fragment一次性的添加到页面
/**
* 负责解析模板内容
* @param el {object} new Vue 选择器
* @param vm {object} new Vue Vue实例
*/
class Compile{
constructor(el ,vm ) {
// 参数
this.el = typeof el === 'string' ? document.querySelector(el) : el
this.vm = vm
// 编模模板
if(this.el) {
// 1. 把el中所有的子节点放入内存中 fragment
// 1-1 创建node2Fragment的核心方法
// 1-2 创建toArray的工具方法
// 1-3 打印consol.dir(fragment) 查看控制台内容
let fragment = this.node2Fragment(this.el)
consol.dir(fragment)
// 2. 在内存中编译fragment
// 3. 把fragment一次性的添加到页面
}
}
/**
* 核心方法
*/
node2Fragment(node){
let fragment = document.createDocumentFragment()
// 把el中所有的子节点添加到文档碎片中
let childNodes = node.childNodes
console.log(childNodes)
this.toArray(childNodes).forEach(node => {
// 把所有的字点添加到fragment
fragment.appendChild(node)
})
}
/**
* 工具方法
*/
toArray(likeArray) {
return [].slice.call(likeArray)
}
}
② frament子节点
- 在内存中编译文档碎片
/**
* 在内存中编译文档碎片
* @param {string} fragment
*/
compile(fragment) {
let childNodes = fragment.childNodes
this.toArray(childNodes).forEach(node=>{
// 编译子节点
console.log(node)
// 是元素节点 需要解析指令
// 是文本节点 需要解析插值表达式
})
}
- 需要工具函数来区分元素/文本节点
nodeType 节点的类型 1. 元素节点 3. 文本节点
isElementNode(node) { return node.nodeType === 1 }
isTextNode(node) { return node.nodeType === 3 }
-
如果是元素节点,需要解析指令
如果是文本节点,需要解析插值表达式
/**
* 在内存中编译文档碎片
* @param {string} fragment
*/
compile(fragment) {
let childNodes = fragment.childNodes
this.toArray(childNodes).forEach(node=>{
if(this.isElementNode(node)) {
// 是元素节点 需要解析指令
this.compileElement(node)
}
if(this.isTextNode(node)) {
// 是文本节点 需要解析插值表达式
this.compileText(node)
}
})
}
- 当前节点还有子节点,如
<div></div>
,就需要递归进行解析
/**
* 在内存中编译文档碎片
* @param {string} fragment
*/
compile(fragment) {
let childNodes = fragment.childNodes
this.toArray(childNodes).forEach(node=>{
if(this.isElementNode(node)) { }
if(this.isTextNode(node)) { }
if(node.childNodes && node.childNodes.length > 0) {
// 当前节点还有子节点 需要递归解析
this.compile(node)
}
})
}
③ 解析指令
- 获取当前节点下所有的属性
/**
* 解析html标签
* @param {string} node
*/
compileElement(node){
// 1. 获取当前节点下所有的属性
let attributes = node.attributes
// console.log(attributes)
this.toArray(attributes).forEach(attr=> {
// console.log(attr)
// 2. 解析Vue的指令(所有以v-开头的指令)
})
}
- 是否是v-开头的指令
isDirective(attrName) { return attrName.startsWith('v-') }
- 解析Vue的指令(所有以v-开头的指令)
V-TEXT
compile.js
/**
* 解析html标签
* @param {string} node
*/
compileElement(node){
// 1. 获取当前节点下所有的属性
let attributes = node.attributes
// console.log(attributes)
this.toArray(attributes).forEach(attr=> {
// console.dir(attr)
// 2. 解析Vue的指令(所有以v-开头的指令)
let attrName = attr.name
if(this.isDirective(attrName)) {
let type = attrName.slice(2)
let expr = attr.value
// 如果是v-text指令
if(type === 'text') {
node.textContent = this.vm.$data[expr]
// console.log(node)
}
}
})
index.html
....
<body>
<div id="app">
<p v-text="msg" title="1"> </p>
</div>
<script src="./src/vue.js"></script>
<script src="./src/compile.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
msg: 'hello'
}
})
</script>
</body>
</html>
V-HTML
compile.js
/**
* 解析html标签
* @param {string} node
*/
compileElement(node){
// 1. 获取当前节点下所有的属性
let attributes = node.attributes
// console.log(attributes)
this.toArray(attributes).forEach(attr=> {
// console.dir(attr)
// 2. 解析Vue的指令(所有以v-开头的指令)
let attrName = attr.name
if(this.isDirective(attrName)) {
let type = attrName.slice(2)
let expr = attr.value
// 如果是v-text指令
if(type === 'text') {
node.textContent = this.vm.$data[expr]
// console.log(node)
}
// 如果是v-html指令
if(type === "html") {
node.innerHTML = this.vm.$data[expr]
}
}
})
index.html
....
<body>
<div id="app">
<p v-text="msg" title="1"> </p>
<p v-html="tag" title="2"> </p>
</div>
<script src="./src/vue.js"></script>
<script src="./src/compile.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
msg: 'hello',
tag: '<h3>tag<h3>'
}
})
</script>
</body>
</html>
V-MODEL
compile.js
/**
* 解析html标签
* @param {string} node
*/
compileElement(node){
// 1. 获取当前节点下所有的属性
let attributes = node.attributes
// console.log(attributes)
this.toArray(attributes).forEach(attr=> {
// console.dir(attr)
// 2. 解析Vue的指令(所有以v-开头的指令)
let attrName = attr.name
if(this.isDirective(attrName)) {
let type = attrName.slice(2)
let expr = attr.value
// 如果是v-text指令
if(type === 'text') {
node.textContent = this.vm.$data[expr]
// console.log(node)
}
// 如果是v-html指令
if(type === "html") {
node.innerHTML = this.vm.$data[expr]
}
// 如果是v-model指令
if(type === "model") {
node.value = this.vm.$data[expr]
}
}
})
index.html
....
<body>
<div id="app">
<p v-text="msg" title="1"> </p>
<p v-html="tag" title="2"> </p>
<input type="text" v-model="msg">
</div>
<script src="./src/vue.js"></script>
<script src="./src/compile.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
msg: 'hello',
tag: '<h3>tag<h3>'
}
})
</script>
</body>
</html>
④ v-on指令
index.html
...
<body>
<div id="app">
<p v-text="msg" title="1"> </p>
<p v-html="tag" title="2"> </p>
<input type="text" v-model="msg">
<button v-on:click="clickFn">Button</button>
</div>
<script src="./src/vue.js"></script>
<script src="./src/compile.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
msg: 'hello',
tag: '<h3>tag<h3>'
},
methods:{
clickFn(){
console.log(this, this.msg)
}
}
})
</script>
</body>
</html>
-
if(type === "on") { console.log("on") }
在控制台无法打印出来,需要解析以下V-ON指令,既if(type.split(":")[0] === "on"){ console.log("on")}
-
为了简化代码,创建
isEventDirective
的工具方法 -
由于页面打印的是
this.name
,this的指向是node
,需要使用bind更改一下this
的指向,即node.addEventListener(eventType, this.vm.$methods[expr].bind(this.vm)
。这样打印出来的
this
就是vm
实例,但是this.msg
还是understand
,但是打印this.$data.msg
是可以打印出来hello
的。
/**
* 解析html标签
* @param {string} node
*/
compileElement(node){
....
// if(type === "on") { console.log("on") }
if(this.isEventDirective(type)){
// console.log("on")
// 给当前元素注册时间
let eventType = type.split(":")[1]
// console.log(expr)
node.addEventListener(eventType, fn.bind(vm))
}
}
})
isEventDirective(type) { return type.split(":")[0] === "on" }
⑤ 通过compileUtil简化代码
- 创建
CompileUtil
let CompileUtil = {
text(node, vm, expr) {
node.textContent = vm.$data[expr]
},
html(node, vm, expr) {
node.innerHTML = vm.$data[expr]
},
model(node, vm, expr) {
node.value = vm.$data[expr]
}
}
}
- 简化
compileElement
中的v-text指令
compileElement(node){
....
if(type === 'text') {
// node.textContent = expr
// node.textContent = this.vm.$data[expr]
// 简化1
CompileUtil['text'](node, this.vm, expr)
}
})
- 因重复性比较多,就不需要进行判断,直接在V-ON之后用
else
compileElement(node){
....
if(this.isEventDirective(type)){
let eventType = type.split(":")[1]
node.addEventListener(eventType, this.vm.$methods[expr].bind(this.vm))
}else {
CompileUtil[type] && CompileUtil[type](node. this.vm, expr)
}
})
- 再次简化, 将v-on 中的方法提取出来,并且添加判断。
compileElement(node){
....
if(this.isEventDirective(type)){
CompileUtil["eventHandler"](node, this.vm, type, expr)
}else {
CompileUtil[type] && CompileUtil[type](node. this.vm, expr)
}
})
let CompileUtil = {
text(node, vm, expr) {
node.textContent = vm.$data[expr]
},
html(node, vm, expr) {
node.innerHTML = vm.$data[expr]
},
model(node, vm, expr) {
node.value = vm.$data[expr]
},
eventHandler(node, vm, type, expr) {
let eventType = type.split(":")[1]
// node.addEventListener(eventType, vm.$methods[expr].bind(vm))
let fn = vm.$methods && vm.$methods[expr]
if(eventType && fn) {
node.addEventListener(eventType, fn.bind(vm))
}
}
}
}
⑥ 插值表达式
index.html
...
<body>
<div id="app">
<p>hi</p>
<p>msg的内容是</p>
<p v-text="msg" title="1"></p>
<p v-html="tag" title="2"></p>
<input type="text" v-model="msg">
<button v-on:click="clickFn">Button</button>
</div>
<script src="./src/vue.js"></script>
<script src="./src/compile.js"></script>
<script src="./src/observe.js"></script>
<script src="./src/watcher.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
msg: 'hello',
tag: '<h3>tag<h3>'
},
methods:{
clickFn(){
console.log("this", this)
console.log("click", this.$data.msg)
}
}
})
</script>
</body>
</html>
compile.js
/**
* 解析文本节点
* @param {string} node
*/
compileText(node) {
let txt = node.textContent
let reg = /\{\{(.+)\}\}/
if(reg.test(txt)){
let expr = RegExp.$1
// console.log(expr)
node.textContent = txt.replace(reg, this.vm.$data[expr])
}
}
⑦ 显示复杂数据
index.html
...
<body>
<div id="app">
<p>hi</p>
<p>msg的内容是</p>
<p v-text="msg" title="1"></p>
<p></p>
<p v-html="tag" title="2"></p>
<input type="text" v-model="msg">
<button v-on:click="clickFn">Button</button>
</div>
<script src="./src/vue.js"></script>
<script src="./src/compile.js"></script>
<script src="./src/observe.js"></script>
<script src="./src/watcher.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
msg: 'hello',
tag: '<h3>tag<h3>',
car: {
brand: '宝马',
color: 'red'
}
},
methods:{
clickFn(){
console.log("this", this)
console.log("click", this.$data.msg)
}
}
})
console.log(vm.$data['msg']) // hello
console.log(vm.$data['car.color']) // understand
</script>
</body>
</html>
以上代码无法解析data
中的car
compile.js
/**
* 解析文本节点
* @param {string} node
*/
compileText(node) {
let txt = node.textContent
let reg = /\{\{(.+)\}\}/
if(reg.test(txt)){
let expr = RegExp.$1
node.textContent = txt.replace(reg, this.getVMValue(vm, expr))
}
}
let CompileUtil = {
...
getVMValue(vm, expr) {
let data = vm.$data
expr.split(".").forEach( key=> {
data = data[key]
})
return data
}
}
进行简化
/**
* 解析文本节点
* @param {string} node
*/
compileText(node) {
CompileUtil.mustache(node, this.vm)
}
let CompileUtil = {
...
mustache(node, vm) {
let txt = node.textContent
let reg = /\{\{(.+)\}\}/
if (reg.test(txt)) {
let expr = RegExp.$1
// debugger
node.textContent = txt.replace(reg, this.getVMValue(vm, expr))
new Watcher(vm, expr, newValue => {
node.textContent = txt.replace(reg, newValue)
})
}
},
}
observe
observe用于给data中的所有数据添加getter / settert,方便在获取或者设置data中数据的时候,实现逻辑
vue.js
class Vue {
constructor(options) {
....
// 监视data中的数据
new Observer(this.$data)
}
}
① 数据劫持
main.html
....
<body>
<script>
let obj = { name: 'qaz'}
let temp = obj['name']
Object.defineProperty(obj, 'name', {
configurable: true, // 表示属性可以配置
enumerable: true, // 表示这个属性可以遍历
get() {
// 每次获取对象的这个属性的时候,就会被这个get方法给劫持到
// getter
console.log('get执行了')
return temp
},
// 每次设置这个对象的属性的时候,就会被set方法劫持到
// 设置的值也会劫持到
// setter
set(newValue) {
console.log('set方法执行了')
temp = newValue
}
})
</script>
</body>
</html>
② 对data中的数据进行劫持
- walk遍历data中所有的数据,都添加上getter和setter
- 给data对象的key设置getter和setter
- 如果data[key]是一个复杂的类型,递归的walk
- defineReactive定义响应式的数据(数据劫持)
- data中的每一个数据都应该维护一个dep对象
- dep保存了所有的订阅了该数据的订阅者
/**
* observe用于给data中的所有数据添加getter / settert
*/
class Observer {
constructor(data) {
this.data = data
this.walk(data)
}
/**
* 核心方法
* 遍历data中的数据,都添加上getter / settert
* @param {string} data
*/
walk(data) {
if(!data || typeof data != 'object') { return }
Object.keys(data).forEach(key => {
console.log(key)
// 给data对象的key设置getter和setter
this.defineReactive(data, key, data[key])
// 如果data[key]是一个复杂的类型,递归的walk
this.walk(data[key])
})
}
/**
* 定义响应式的数据(数据劫持)
* data中的每一个数据都应该维护一个dep对象
* @param {object} obj
* @param {string} key
* @param {string} value
*/
defineReactive(obj, key, value) {
let that = this
let dep = new dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// console. log('获取到的数据的值', value)
Dep.target && dep.addSub(Dep.target)
return value
},
set (newValue) {
if(value === newValue ) { return }
value = newValue
this.walk(newValue)
}
})
}
}
watcher
① watcher对象的创建
/**
* watcher模块负责把compile模块与observe模块关联起来
*/
class Watcher{
/**
*
* @param {string} vm 当前Vue实例
* @param {string} expr data中数据的名字
* @param {string} cb 当数据发生改变,需要调用cb
*/
constructor (vm, expr, cb) {
this.vm = vm
this.expr = expr
this.cb = cb
// 需要把wxpr的旧值保存起来
this.oldValue = this.getVMValue(vm, expr)
}
/**
* 对外暴漏的一个方法,这个方法用于更新页面
*/
update() {
// 对比expr是否发生了改变,有改变 ==> 调用cb
let oldValue = this.oldValue
let newValue = this.newValue
if(oldValue != newValue) {
this.cb(newValue, oldValue)
}
}
/**
* 用于获取vm中的数据
* @param {string} vm
* @param {string} expr
*/
getVMValue(vm, expr) {
// 获取到data中的数据
let data = vm.$data
expr.split(".").forEach(key => {
data = data[key]
})
return data
}
}
② 关联observe与compile
通过watcher对象,监听expr的数据变化,当数据发生变化,就执行回调函数
compile.js
/**
* 解析文本节点
* @param {string} node
*/
compileText(node) {
CompileUtil.mustache(node, this.vm)
// 通过watcher对象,监听expr的数据变化,当数据发生变化,就执行回调函数
new Watcher(vm, expr, (newValue) => {
node.textContent = newValue
})
}
还有CompileUtil
中的mustache, text,hrml,model需要进行watcher
let CompileUtil = {
mustache(node, vm) {
let txt = node.textContent
let reg = /\{\{(.+)\}\}/
if(reg.test(txt)){
let expr = RegExp.$1
node.textContent = txt.replace(reg, this.getVMValue(vm, expr))
// 添加监听watcher
new Watcher(vm, expr, newValue => {
node.innerHTML = txt.replace(reg, newValue)
})
}
},
text(node, vm, expr) {
node.textContent = vm.$data[expr]
// 添加监听watcher
new Watcher(vm, expr, (newValue) => {
node.textContent = newValue
})
},
html(node, vm, expr) {
node.innerHTML = vm.$data[expr]
// 添加监听watcher
new Watcher(vm, expr, (newValue) => {
node.innerHTML = newValue
})
},
model(node, vm, expr) {
node.value = vm.$data[expr]
// 添加监听watcher
new Watcher(vm, expr, (newValue) => {
node.value = newValue
})
},
eventHandler(node, vm, type, expr) {
let eventType = type.split(":")[1]
let fn = vm.$methods && vm.$methods[expr]
if(eventType && fn) {
node.addEventListener(eventType, fn.bind(vm))
}
},
getVMValue(vm, expr) {
let data = vm.$data
expr.split(".").forEach( key=> {
data = data[key]
})
return data
}
}
observe.js
defineReactive(obj, key, value) {
let that = this
let dep = new dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() { Dep.target && dep.addSub(Dep.target) return value },
set (newValue) {
if(value === newValue ) { return }
value = newValue
this.walk(newValue)
// 调用watcher update方法
// window.watcher.update()
}
})
}
- 调用watcher update方法。
window.watcher.update()
- window会覆盖,选择用发布者模式
③ 订阅-发布者模式
也叫观察者模式
定义了一种一对多的依赖关系,即当一个对象的状态发生改变的时候,所有依赖于它的对象都会得到通知并自动更新,解决了主体对象与观察者之间功能的耦合。
例子:
微信公众号
- 订阅者:只需要要订阅微信公众号
- 发布者(公众号):发布新文章的时候,推送给所有订阅者
优点:
- 解耦合
- 订阅者不用每次去查看公众号是否有新的文章
- 发布者不用关心谁订阅了它,只要给所有订阅者推送
watcher.js
/**
* dep对象用于管理所有的订阅者和通知这些订阅者
*/
class Dep {
constructor() {
// 用于管理订阅者
this.subs = []
}
// 添加订阅者
addSub(watcher) {
this.subs.push(watcher)
}
// 通知
notify() {
// 遍历所有的订阅者,调用watcher的update方法
this.subs.forEach(sub => {
sub.update()
})
}
}
④ 通过订阅-发布者模式更新数据
- this表示的就是新创建的watcher对象
- 需要把expr的旧值给存储起来
- 清空Dep.target
class Watcher{
constructor (vm, expr, cb) {
this.vm = vm
this.expr = expr
this.cb = cb
// 需要把wxpr的旧值保存起来
// this.oldValue = this.getVMValue(vm, expr)
// this表示的就是新创建的watcher对象
// 存储到Dep.target属性上
Dep.target = this
// 需要把expr的旧值给存储起来
this.oldValue = this.getVMValue(vm, expr)
// 清空Dep.target
Dep.target = null
}
}
⑤ 修复复杂类型数据的bug
model中实现双向的数据绑定, 给node注册input事件,当前元素的value值发生改变,修改对应的数据
compile.js
let CompileUtil = {
model(node, vm, expr) {
node.value = vm.$data[expr]
// 实现双向的数据绑定, 给node注册input事件,当前元素的value值发生改变,修改对应的数据
node.addEventListener("input", function() {
// this.$data[expr] = this.value
self.setVMValue(vm, expr, this.value)
})
// 添加监听watcher
new Watcher(vm, expr, (newValue) => { node.value = newValue })
},
setVMValue(vm, expr, value) {
let data = vm.$data
// car brand
let arr = expr.split(".")
arr.forEach((key, index) => {
// 如果index是最后一个
if (index < arr.length - 1) {
data = data[key]
} else {
data[key] = value
}
})
}
}
⑥ 代理数据到vm实例
vue.js
class Vue {
constructor(options = {}) {
this.$el = options.el
this.$data = options.data
this.$methods = options.methods
// 监视data中的数据
new Observer(this.$data)
// 把data中所有的数据代理到了vm上
this.proxy(this.$data)
// 把methods中所有的数据代理到了vm上
this.proxy(this.$methods)
// 如果指定了el参数,对el进行解析
if (this.$el) {
// compile负责解析模板的内容 模板和数据
let c = new Compile(this.$el, this)
}
}
proxy(data) {
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
return data[key]
},
set(newValue) {
if (data[key] == newValue) {
return
}
data[key] = newValue
}
})
})
}
}
总结
- 将指令/插值表达式仅次于解析
- 通过observe对数据进行劫持
- 使用watcher关联observe与compile
- 由于watcher监视的地方较多,使用订阅-发布者模式
完
本文首次发布于 Azr的博客, 作者 @azrrrrr ,转载请保留原文链接.
原文链接: http://amor9.cn/2019/03/01/mvvm