最近一段时间一直在研究 Vue 的源码,突然间想写一个乞丐的 Vue 实现,为了理一下自己的思路,同时也作为一个阶段性的总结.
Vue 双向绑定看这里
Vue2.0/1.0 双向数据绑定简单来说就是利用了 Object.defineProperty()和观察者模式对 data 数据进行数据劫持.
废话不多说,直接上代码
//Watcher,观察者,真正执行更新操作的角色
class Watcher{
constructor(vm,key,update){
//保存传入的选项
this.vm = vm
this.key = key
this.update = update
//将当前watcher添加到Dep中
Dep.target = this
this.vm[this.key]
Dep.target = null
}
//用来执行我们的更新函数
update(){
this.update&&this.update(this.vm[this.key])
}
}
//Dep负责维护一个更新队列,
class Dep{
constructor(){
this._dep = []
}
//添加更新队列(这是一个乞丐版的)
addDep(watcher){
this._dep.push(watcher)
}
//通知队列更新
notify(){
this._dep.forEach(item=>item())
}
}
class Vue{
//构造函数接收配置项,将其保存起来
constructor(options){
this.$options = options||{}
this.$data = options.data||{}
//执行响应化处理
this.$data&&this.observe(this.$data)
//执行编译
new Compile(options.el, this)
}
observe(obj){
//响应化的数据必须是对象
if(!obj||typeof obj !=='object')return
//遍历
Object.keys(obj).forEach(key=>{
//执行响应化
this.defineReactive(obj,key,obj[key])
//代理数据(将data数据代理到当前vue实例上)
this.proxyData(key)
})
}
//数据劫持
defineReactive(obj,key,val){
//当前val为对象时,递归处理
this.observe(val)
//每一个key都对应一个dep,用来维护更新队列
const dep = new Dep()
Object.defineProperty(obj,key,{
get(){
Dep.target&&dep.addDep(Dep.target)
return val
},
set(newVal){
if(newVal!==val){
val = newVal
//通知更新队列更新
dep.notify()
}
}
})
}
//代理数据
proxyData(key){
Object.defineProperty(this,key,{
get(){
return this.$data[key]
},
set(newVal){
this.$data[key]=newVal
//如果新添加的是一个对象,继续响应化处理
this.observe(this.$data[key])
}
})
}
}
编译的实现原理很简单,我们可以简化为三步:
现在看一下代码的实现
class Compile{
//接收一个宿主元素,确定挂载目标,同时接收一个当前vue实例
constructor(vm,el){
this.$vm = vm
this.$el = document.querySelector(el)
//如果el存在,则执行编译
this.$el&&this.compile(this.$el)
}
//编译
compile(el){
//获取所有子元素,获取DOM树
const childNodes = el.childNodes
//遍历所有子元素
Array.from(childNodes).forEach(node=>{
//判断节点类型
if(this.isElement(node)){
//如果是元素节点,执行元素节点更新
this.compileElement(node)
}else if(this.isText(node)){
//如果是文本节点,执行文本节点更新
this.compileText(node)
}
//如果子元素下面还有子元素,递归处理
if(node.childNodes&&node.childNodes.length>0){
this.compile(node)
}
})
}
//编译元素节点
compileElement(node){
//获取节点特性
const attrs = node.attributes
//遍历处理特性
Array.from(attrs).forEach(item=>{
//获取特性信息
const name = item.name
const val = item.value
//如果包含“v-”,我们认为是指令
if(name.indexOf('v-')>-1){
//截取指令名称
const dir = name.substring('2')
//如果存在指令更新函数,则进行更新
this.update(node,val,dir)
}else if(name.indexOf('@')>-1){
//@认为是监听事件
const dir = name.substring(1)
node.addEventListener(dir,this.$vm[val].bind(this.$vm));
}
})
}
//编译文本节点
compileText(node){
//获取数据内容
const exp = RegExp.$1
//执行更新
this.text(node,exp)
}
text(node,key){
this.update(node,key,'text')
}
//双向数据绑定
model(node,key){
//v-model的原理就是一个监听事件+“类似v-text”的语法糖
this.update(node,key,'model')
//添加input的监听事件
node.addEventListener('input',event=>{
this.$vm[key]=event.target.value
})
}
html(node,key){
this.update(node,key,'html')
}
//update函数
update(node,key,dir){
//获取具体的更新函数
const updater =this[dir+'Updater']
updater&&updater.call(this,node,this.$vm[key])
//添加Watcher
new Watcher(this.$vm,key,val=>{
updater&&updater.call(this,node,val)
})
}
//更新text文本
textUpdater(node,val){
node.textContent =val
}
//更新v-html
htmlUpdater(node,val){
node.innerHTML= val
//响应式的编译新添加的子节点
this.compile(node)
}
//model更新函数
modelUpdater(node,val){
//value属性赋值
node.value = val
}
//判断是否为元素节点
isElement(node){
//元素节点的nodeType类型为1
return node.nodeType===1
}
//判断是否为文本节点
isText(node){
//不仅要判断元素类型,并且文本内容包含{{}}
const res = node.nodeType===3&&/\{\{.*\}\}/.test(node.textContent)
return res
}
}
以上,就是一个乞丐版的 Vue,简单的实现了 Vue 的双向数据绑定和 DOM 编译解析更新.这里只是实现了一个简单的函数更新,Vue2.0 里面的 Watcher.run()函数是进行虚拟 DOM 的更新.
虽然是乞丐版的实现,但是感觉思路是相通的:通过 Object.defineProperty 实现数据劫持,通过 Compile 模块实现 DOM 的更新.
源码在这里