我们都知道Vue是响应式的,Vue就是基于数据劫持+发布者-订阅者模式
实现的数据响应式,数据变化驱动视图更新,而通过操作视图也能改变数据。
数据劫持是通过Object.defineProperty
把Vue中的data转化成getter
和setter
实现,ES6中也有Proxy
对数据进行拦截处理。
首先来着一个图,大概总结一个流程。
代码开始
class MyVue{
constructor(options){
this.$el = options.el
this.$data = options.data
this.$options = options
if(this.$el){
//数据劫持-发布者+订阅者
new Observer(this.$data)
//编译模板
new Compile(this.$el,this)
//数据代理 可通过 this.person 直接访问数据
this.proxyData(this.$data)
}
}
proxyData(data){
for(const key in data){
Object.defineProperty(this,key,{
get(){
return data[key]
},
set(newVal){
data[key] = newVal
}
})
}
}
}
- 数据响应式:
对data的所有数据进行劫持,转化为getter
和setter
。 - 模板编译
模板解析,处理指令和事件绑定,比如{{msg}}
,v-text="msg"
,v-on:click="handler"
- 数据双向绑定与数据代理
v-model
实现;数据代理实现
数据响应式
class Observer{
constructor(data){
this.observe(data)
}
observe(data){
if(data && typeof data === 'object'){
Object.keys(data).forEach(key=>{
this.defineReactive(data,key,data[key])
})
}
}
defineReactive(obj,key,value){
//递归遍历
this.observe(value)
//创建依赖
const dep = new Dep()
Object.defineProperty(obj,key,{
configurable:true,
enumerbale:true,
get(){
//模板编译的时候会取数据时会创建Watcher 然后收集进dep中
Dep.target && dep.addSub(Dep.target)
return value
},
set:(newVal)=>{
//重新监听新值,解决对属性赋值时监听不到的问题 如 this.$data.person = {a:1}
this.observe(newVal)
if(newVal !== value){
value = newVal
}
//数据修改时,通知订阅者,更新视图
dep.notify()
}
})
}
}
class Dep{
constructor() {
this.subs = []
}
addSub(watcher){
this.subs.push(watcher)
}
notify(){
this.subs.forEach(w=>w.update())
}
}
class Watcher{
constructor(vm,expr,cb) {
this.vm = vm
this.expr = expr
this.cb = cb
this.oldVal = this.getOldVal()
}
getOldVal(){
//依赖
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(this.oldVal !== newVal){
//数据变化,回调更新视图
this.cb(newVal)
}
}
}
- 数据响应式实现了啥?
- 对数据进行劫持
- 对依赖收集
- 数据变化,回调更新
模板编译
class Compile{
constructor(el,vm){
//判断是否为元素节点,否则获取
this.el = this.isElementNode(el) ? el : document.querySelector(el)
this.vm = vm
//获取文档碎片对象,减少页面的回流和重绘
const fragment = this.node2Fragment(this.el)
//编译模板
this.compile(fragment)
//追加到#app上
this.el.appendChild(fragment)
}
isElementNode(node){
return node.nodeType === 1
}
node2Fragment(el){
const f = document.createDocumentFragment()
let firstChild
while(firstChild = el.firstChild){
f.appendChild(firstChild)
}
return f
}
compile(fragment){
const childNodes = fragment.childNodes
;[...childNodes].forEach(child=>{
if(this.isElementNode(child)){
//元素节点
this.compileElement(child)
}else{
//文本节点
this.compileText(child)
}
//递归遍历子节点
if(child.childNodes && child.childNodes.length){
this.compile(child)
}
})
}
compileElement(node){
const attrs = node.attributes
;[ ...attrs].forEach(attr=>{
//取出名和其值,v-html = 'htmlStr' , v-on:click='handler'
const {name,value} = attr
//是否为指令
if(this.isDirective(name)){
const [ ,directive] = name.split('-') //分离出指令名,text,html,on,model
const [dirName,eventName] = directive.split(':') //分离出事件名[html,undefined] [on,click]
// console.log(dirName,eventName)
//value 为 expr 也就是 htmlStr/person.name/person.age
compileUtil[dirName](node,value,this.vm,eventName)
//删除标签上的指令属性
node.removeAttribute('v-'+directive)
}else if(this.isShortOn(name)){
let [,eventName] = name.split('@')
compileUtil['on'](node,value,this.vm,eventName)
node.removeAttribute('@'+eventName)
}else if(this.isShortBind(name)){
let [,eventName] = name.split(':')
compileUtil['bind'](node,value,this.vm,eventName)
node.removeAttribute(':'+eventName)
}
})
}
compileText(node){
const content = node.textContent
//检测是否有 {{msg}}此类指令
if(/\{\{(.+?)\}\}/.test(content)){
compileUtil['text'](node,content,this.vm)
}
}
isDirective(name){
//是否以 v-开头
return name.startsWith('v-')
}
isShortOn(name){
//是否以 @开头
return name.startsWith('@')
}
isShortBind(name){
//是否以 :开头
return name.startsWith(':')
}
}
//编译工具对象
const compileUtil = {
text(node,expr,vm){
let value
if(expr.indexOf('{{') !== -1){
value = expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
new Watcher(vm,args[1],(newVal)=>{
this.updater.textUpdate(node,this.getContentVal(expr,vm))
})
return this.getVal(args[1],vm)
})
}else{
value = this.getVal(expr,vm)
}
this.updater.textUpdate(node,value)
},
html(node,expr,vm){
const value = this.getVal(expr,vm)
new Watcher(vm,expr,(newVal)=>{
this.updater.htmlUpdate(node,newVal)
})
this.updater.htmlUpdate(node,value)
},
model(node,expr,vm){
const value = this.getVal(expr,vm)
new Watcher(vm,expr,(newVal)=>{
this.updater.modelUpdate(node,newVal)
})
node.addEventListener('input',(e)=>{
this.setVal(vm,expr,e.target.value)
})
this.updater.modelUpdate(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,propertyName){
const value = this.getVal(expr,vm)
new Watcher(vm,expr,(newVal)=>{
this.updater.bindUpdate(node,propertyName,newVal)
})
this.updater.bindUpdate(node,propertyName,value)
},
updater:{
textUpdate(node,value){
node.textContent = value
},
htmlUpdate(node,value){
node.innerHTML = value
},
modelUpdate(node,value){
node.value = value
},
bindUpdate(node,propertyName,value){
node.setAttribute(propertyName,value)
}
},
getVal(expr,vm){
return expr.split('.').reduce((data,currentVal)=>data[currentVal],vm.$data)
},
getContentVal(expr,vm){
return value = expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
return this.getVal(args[1],vm)
})
},
setVal(vm,expr,inputVal){
return expr.split('.').reduce((data,currentVal)=>{
data[currentVal] = inputVal
},vm.$data)
}
}
- 模板编译实现了啥?
- 对模板中的指令、语法进行解析赋值,绑定事件
- 对每个使用了
data
中的数据添加一个Watcher
(观察者) - 在模板编译赋值的过程中会取值,就会触发
data
中的getter
,这时也就会把Watcher
push进dep(收集依赖的容器)中。 - 一个旦有数据变化,就会触发
data
中的setter
,从而会触发dep.notify
通知所有watcher
更新,从而驱动视图更新。 - 双向数据绑定,无非就是对表单添加事件
input
,onchange
等