面试题:
你所理解的MVVM响应式原理
简单版本:
MVVM
即模型、视图、视图模型。模型
指的是后端传递的数据。视图
指的是所看到的页面。视图模型
mvvm模式的核心,它是连接view和model的桥梁。它有两个方向:一是将模型
转化成视图
,即将后端传递的数据转化成所看到的页面。实现的方式是:数据绑定。二是将视图
转化成模型
,即将所看到的页面转化成后端的数据。实现的方式是:DOM 事件监听。这两个方向都实现的,我们称之为数据的双向绑定。
如果是第一次看到这样的话,肯定一脸茫然。上面很多概念都没有解析清楚,怎么实现的数据绑定
、双向绑定
。
代码是最简洁直白的,接下来所要做的就是通过实现代码去理解MVVM响应式原理。
MVVM Demo
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<h2>{{person.name}} -- {{person.age}}</h2>
<h3>{{person.fav}}</h3>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<h3 v-bind="person.child.name">{{msg}}</h3>
<div v-text="msg"></div>
<div v-html="htmlStr"></div>
<input type="text" v-model="msg">
<button v-on:click="ShowMsg">123</button>
</div>
<script src="./CrazyVue.js"></script>
<script>
let vm = new CrazyVue({
el: '#app',
data: {
person: {
name: '法外狂徒张三',
age: 28,
fav: "唱跳Rap",
child: {
name: "sadsa"
}
},
msg: '学习MVVM响应式原理',
htmlStr: '<p>Vue真香阿</p>'
},
methods: {
ShowMsg(){
console.log(this.$data)
this.person.name = '学习Vue'
}
}
})
</script>
</body>
</html>
入口函数
class CrazyVue{
constructor(options){
this.$el = options.el
this.$data = options.data
this.$options = options
if(this.$el){
// 1.实现一个数据观察者
new Observer(this.$data)
// 2.实现一个指令解析器
new Compile(this.$el,this)
this.proxyData(this.$data)
}
}
/*
proxy:this.$data.person => this.person
*/
proxyData(data){
for(const key in data){
Object.defineProperty(this,key,{
get(){
return data[key]
},
set(newVal){
data[key] = newVal
}
})
}
}
}
在这里实现一个数据观察者和指令解析器以及对$data数据的代理,先分析指令解析器Compile
做了些什么,
我们猜它对模版中含有v-
标签以及{{}}
这样的模版指令进行初始化模版渲染,在初始化的时候添加观察者以及数据改动触发的回调函数。
Compile指令解析器
class Compile{
constructor(el,vm){
this.el = this.isElementNode(el) ? el : document.querySelector(el)
this.vm = vm
// 1.获取文档碎片对象 放入内存中会减少页面的回流和重绘
const fragment = this.node2Fragment(this.el)
// 2.编译模板
this.compile(fragment)
// 3.追加子元素到根元素
this.el.appendChild(fragment)
}
/*
<h2>{{person.name}} -- {{person.age}}</h2>
<h3>{{person.fav}}</h3>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<h3>{{msg}}</h3>
<div v-text="msg"></div>
<div v-html="htmlStr"></div>
<input type="text" v-model="msg">
*/
compile(fragment){
// 1.获取子节点
const childNodes = fragment.childNodes;
[...childNodes].forEach(child => {
if(this.isElementNode(child)){
// 是元素节点
// 编译元素节点
// console.log("元素节点",child)
this.compileElement(child)
}else{
// 文本节点
// 编译文本节点
// console.log("文本节点",child)
this.compileText(child)
}
if(child.childNodes && child.childNodes.length){
this.compile(child)
}
})
}
compileElement(node){
// <div v-text="msg"></div>
const attributes = node.attributes;
[...attributes].forEach(attr=> {
const {name,value} = attr
if(this.isDirective(name)){
// 是一个指令 v-text v-model v-html v-on:click
const [,directive] = name.split('-') // text model html on:click
const [dirName,eventName] = directive.split(":") // test model html on
// 更新数据 数据驱动视图
compileUtil[dirName](node,value,this.vm,eventName)
// 删除有指令的标签上的属性
node.removeAttribute('v-'+directive)
}else if(this.isEventName(name)){
let [,eventName] = name.split('@')
compileUtil['on'](node,value,this.vm,eventName)
}
})
}
compileText(node){
// <h3>{{msg}}</h3>
const content = node.textContent
if(/\{\{(.*?)\}\}/.test(content)){
compileUtil['text'](node,content,this.vm)
}
}
node2Fragment(el){
// 创建文档碎片
const f = document.createDocumentFragment()
let firstChild
while(firstChild = el.firstChild){
f.appendChild(firstChild)
}
return f
}
isEventName(attrName){
return attrName.startsWith("@")
}
isDirective(attrName){
return attrName.startsWith("v-")
}
isElementNode(node){
return node.nodeType === 1
}
}
Compile
实现功能
- 获取DOM树,将DOM树写入文档碎片对象,减少页面的回流和重绘
- 解析模版,对元素节点、文本节点进行不同的模版渲染,对于含有子节点的DOM进行递归
- 追加子元素到根元素
const compileUtil = {
getVal(expr,vm){
// {person,name}
return expr.split('.').reduce((data,currentVal)=>{
return data[currentVal]
},vm.$data);
},
getContentVal(expr,vm){
expr.replace(/\{\{(.*?)\}\}/g,(...args)=> {
return this.getVal(args[1],vm)
})
},
setVal(expr,vm,inputVal){
return expr.split('.').reduce((data,currentVal)=>{
data[currentVal] = inputVal
},vm.$data);
},
text(node,expr,vm){ // expr:msg
let value
if(expr.indexOf('{{') !== -1){
// {{person.name}}--{{person.age}}
value = expr.replace(/\{\{(.*?)\}\}/g,(...args)=> {
// 绑定观察者,将来数据发生变化,触发这里的回调函数
new Watcher(vm,expr,()=>{
this.updater.textUpdater(node,this.getContentVal(expr,vm))
})
return this.getVal(args[1],vm)
})
}else{
const value = this.getVal(expr,vm)
}
this.updater.textUpdater(node,value)
},
html(node,expr,vm){
const value = this.getVal(expr,vm)
new Watcher(vm,expr,(newVal)=>{
this.updater.htmlUpdater(node,newVal)
})
this.updater.htmlUpdater(node,value)
},
model(node,expr,vm){
const value = this.getVal(expr,vm)
// 绑定更新函数 数据 => 视图
new Watcher(vm,expr,(newVal)=>{
this.updater.modelUpdater(node,newVal)
})
// 视图 => 数据 => 视图
node.addEventListener('input',(e)=>{
// 设置值
this.setVal(expr,vm,e.target.value)
})
this.updater.modelUpdater(node,value)
},
on(node,expr,vm,eventName){
let fn = vm.$options.methods && vm.$options.methods[expr]
node.addEventListener(eventName,fn.bind(vm),false)
},
bind(node,expr,vm){
const value = this.getVal(expr,vm)
console.log(expr)
if(/(.+?).(.+?)/.test(expr)){
keys = []
expr.split('.').reduce((data,currentVal)=>{
keys.push(currentVal)
});
console.log(keys.slice(-1))
expr = keys.slice(-1)
}
this.updater.bindUpdater(node,expr,value)
},
updater:{
textUpdater(node,value){
node.textContent = value
},
htmlUpdater(node,value){
node.innerHTML = value
},
modelUpdater(node,value){
node.value = value
},
bindUpdater(node,expr,value){
node.setAttribute(expr,value);
}
}
}
针对不同的指令调用指定的函数进行渲染模版,在初始化的时候添加观察者,将来数据发生变化的时候,触发这里的回调函数
Observer
class Observer {
constructor(data){
this.data = data
}
observer(data){
/*
{
person: {
name: '张三',
fav: {
dance: {
a: '小苹果'
}
}
}
}
*/
if(data && typeof data === "object"){
Object.keys(data).forEach(key => {
this.defineReactive(data,key,data[key])
})
}
}
defineReactive(obj,key,value){
// 递归遍历
this.observer(value)
const dep = new Dep()
// 劫持并监听所有的属性
Object.defineProperty(obj,key,{
enumerable: true,
configurable: true,
get(){
// 初始化
// 订阅数据变化时,往Dep中添加观察者
dep.addSub(Dep.target)
return value
},
set:(newVal)=>{
this.observer(newVal)
if(newVal !== value){
value = newVal
}
// 告诉dep通知变化
dep.notify()
}
})
}
}
Dep
class Dep{
constructor(){
this.subs = []
}
// 收集观察者
addSub(watcher){
this.subs.push(watcher)
}
// 通知观察者去更新
notify(){
this.subs.forEach(w=>w.update)
}
}
Watcher
class Watcher{
constructor(vm,expr,cb){
this.vm = vm
this.expr = expr
this.cb = cb
// 先把旧值保持起来
this.oldVal = this.getOldVal()
}
getOldVal(vm,expr){
Dep.target = this
const oldVal = compileUtil.getVal(this.expr,this.vm)
Dep.target = null
return oldVal
}
update(){
const newVal = compileUtil.getVal(this.expr,this.vm)
if(newVal !== this.oldVal){
this.cb(newVal)
}
}
}
Observer
判断传进来的data数据是否是对象类型,对对象的每个key进行数据劫持监听所有的属性,对于嵌套的对象进行递归遍历,创建了Dep
去存储所有的观察者,当初始化的时候向Dep
当中添加观察者,在改变属性的时候对新的属性创建观察者并告诉dep通知变化
阐述一下你所理解的MVVM响应式原理
vue是采用数据劫持配合发布者-订阅者模式,通过Object.defineProperty()
来劫持各个属性的setter
和getter
,在数据变动时,发布消息给依赖收集器,去通知观察者,做出对应的回调函数,去更新视图
MVVM作为绑定的入口,整合Observer
,Compile
,Watcher
三者,通过Observer
监听model
数据变化,通过Compile
解析编译模板指令,最终利用Watcher
搭起Observer
,Compile
之间的通信桥梁,达到数据变化->视图更新,视图交换变化->数据model变更的双向数据绑定
写在最后
问渠那得清如许
唯有源头活水来
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。