vue是采用数据劫持配合发布订阅者模式的方式通过object.defineProperty()来劫持各个属性的setter和getter,在数据变动时发布消息给依赖收集器Dep(订阅者),去通知观察者作出对应的回调函数去更新视图。
语言整理:mvvm作为绑定的入口,整合了observer,compile,和watcher三者,通过observer监听model数据变化,compile解析模板指令,最终利用watcher搭起了observer和compile之间的世界名桥。达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。
一、MVue实例
//lev vm = new MVue() 将整个实例赋值给vm,控制台输入vm可拿到整个实例
let vm = new MVue({
el:'#app',
data:{
msg:'学习mvvm实现原理',
person:{
name:'小马',
age:18,
fav:'姑娘',
obj:{
a:'123'
}
},
htmlStr:'好好学习,天天向上'
},
methods:{
handlerClick(){
// const p = new Proxy(target, handler) 劫持
// Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。
// target
// 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
// handler
// 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。
// console.log(p)
console.log(this)
this.msg = '11111'
this.$data.person.name = '张三'
}
}
})
Mvue与proxyData代理
class MVue{
constructor(options) { //this指向的是MVue
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) //劫持this.$data 指向 this
}
}
proxyData(data){
for(const key in data){
Object.defineProperty(this,key,{ //this=MVue
get(){
return data[key]
// data:{ msg:'学习mvvm实现原理' }
},
set(newVal){
data[key] = newVal
}
})
}
}
}
proxyData代理前
proxyData代理后
二、Compile解析器
解析指令,页面重绘class Compile{
constructor(el,vm) {
console.log('compile')
this.el = this.isElementNode(el) ? el : document.querySelector(el)
// console.log(this.el,'el')
this.vm = vm
// 1.获取文档碎片对象 放入内存中会减少页面的回流和重绘
const fragment = this.node2Fragment(this.el)
// console.log(fragment,'fragment')
//2.编译模板
this.compile(fragment)
//3.追加子元素到根元素
this.el.appendChild(fragment)
}
compile(fragment){
// 1.获取子节点
const childNodes = fragment.childNodes
childNodes.forEach(child=>{
// console.log(child,'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){
const attributes = node.attributes
// console.log(Object.keys(attributes),'attributes') //{0: v-text, v-text: v-text, length: 1} <div v-text="msg"></div>
Object.keys(attributes).forEach(item=>{
let attr = attributes[item] //attributes 属性返回指定节点的属性集合,即 NamedNodeMap。您可以使用 length
//属性来确定属性的数量,然后您就能够遍历所有的属性节点并提取您需要的信息。
// v-text:name = msg:value v-on:click="handlerClick" //@click="handlerclick" v-bind:src = "img"
const {name,value} = attr //结构赋值 取得name,value
// console.log(attr)
if(this.isDirective(name)){ // 是一个指令 v-text,v-html,v-model,v-on:click
const [,directive] = name.split('-') // text,html,model,on:click
const [dirName,eventName] = directive.split(':') //dirName:text,html.model,on
//更新数据 数据驱动视图
compileUtil[dirName](node,value,this.vm,eventName)
//删除指令标签上的属性
node.removeAttribute('v-'+directive)
}else if(this.isEventName(name)){ //@click="handlerclick"
let [,eventName] = name.split('@')
compileUtil['on'](node,value,this.vm,eventName)
}
})
}
isEventName(attrName){
return attrName.startsWith('@')
}
compileText(node){ //{{}} v-text
// console.log(node.textContent)
const content =node.textContent
if(/\{\{(.+?)\}\}/.test(content)){
// console.log(content)
compileUtil['text'](node,content,this.vm)
}
}
isDirective(attrName){
// startsWith() 方法用于检测字符串是否以指定的子字符串开始。
// 如果是以指定的子字符串开头返回 true,否则 false。
// startsWith() 方法对大小写敏感。
return attrName.startsWith('v-')
}
node2Fragment(el){
//创建文档碎片
const f = document.createDocumentFragment()
//createDocumentFragment是用来创建一个虚拟的节点对象,或者说是创建一个文档碎片节点,它可以包含各种类型的节点,在创建之初是空的
//DocumentFragment不属于文档树,继承的parentNode属性总是null。当请求把一个DocumentFragment节点插入文档树时,插入的不是DocumentFragment自身,
//而是它的所有子孙节点,即插入的是括号里的节点。这个特性使得DocumentFragment成了占位符,暂时存放那些一次插入文档的节点。
//它还有利于实现文档的剪切、复制和粘贴操作。
//如果使用appendChid方法将原dom树中的节点添加到DocumentFragment中时,会删除原来的节点。
let firstChild;
while(firstChild = el.firstChild){ //while是条件为真的情况下才执行,也就是必须el.firstChild有值的情况下才执行当判定while(firstChild)为真的情况执行
f.appendChild(firstChild) //appendChild是剪切效果不是复制黏贴,dom会剪切到fragment
}
return f
}
isElementNode(node){
return node.nodeType === 1
}
}
CompileUtil
compile工具函数compileUtil={
getVal(expr,vm){ //取值
// [person,name]
return expr.split('.').reduce((data,currentVal)=>{ //[person,fav]
// console.log(data)
// console.log(currentVal)
return data[currentVal] //这里触发了object.defineProtype()的get方法 并将watcher推入subs
},vm.$data)
},
setVal(expr,vm,inputVal){ //expr:person.name 数据驱动视图
return expr.split('.').reduce((data,currentVal)=>{ //[person,fav]
if(typeof data[currentVal] == 'object'){ 优化修复源码留下的bug
return data[currentVal]
}
data[currentVal] = inputVal
},vm.$data)
},
getContent(expr,vm){
return expr.replace(/\{\{(.+?)\}\}/g,(...arguments)=>{
return this.getVal(arguments[1],vm)
})
},
text(node,expr,vm){
//node:节点,expr:msg=》data.msg,vm:整个实例 v-text:'person.fav' {{}}
let value;
if(expr.indexOf('{{')!=-1){
// {{person.name}} -- {{person.age}}
value = expr.replace(/\{\{(.+?)\}\}/g,(...arguments)=>{
// console.log(arguments[1])
//绑定观察者,将来数据发生变化触发这里的回调 进行更新
new Watcher(vm,arguments[1],()=>{ //watcher 更新视图 编译阶段建立watcher 先调用observer再调用的compile
// console.log(this.getContent(expr,vm))
this.updater.textUpdater(node,this.getContent(expr,vm)) //触发了html方法 回调回来新值
})
// 在函数调用时(不管是调用还是被调用),会自动在该函数内部生成一个名为 arguments 的隐藏对象,该对象是个类数组。
// console.log(arguments)
return this.getVal(arguments[1],vm)
})
}else{
value = this.getVal(expr,vm);
}
// const value = vm.$data[expr]
this.updater.textUpdater(node,value);
},
html(node,expr,vm){ //computil html方法
const value = this.getVal(expr,vm)
new Watcher(vm,expr,(newVal)=>{ //watcher 更新视图 编译阶段建立watcher 先调用observer再调用的compile
this.updater.htmlUpdater(node,newVal) //触发了html方法 回调回来新值
})
// watcher和数据绑定进行数据的监听
this.updater.htmlUpdater(node,value)
},
model(node,expr,vm){
const value = this.getVal(expr,vm)
// 绑定更新函数,数据=》视图
new Watcher(vm,expr,(newVal)=>{ //watcher 更新视图 编译阶段建立watcher 先调用observer再调用的compile
this.updater.modelUpdater(node,newVal) //触发了html方法 回调回来新值
})
//视图=》数据=》视图
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)
//fn.bind(vm) 指向vm实例:MVue
//fn.bind(this) 指向compileUtil类
//fn 指向调用者,button
},
updater:{
modelUpdater(node,value){
node.value = value
},
htmlUpdater(node,value){
node.innerHTML = value
},
textUpdater(node,value){
//textContent 属性设置或者返回指定节点的文本内容。
// 如果你设置了 textContent 属性, 任何的子节点会被移除及被指定的字符串的文本节点替换。
node.textContent = value
}
}
}
三、Observer
劫持数据监听通过观察数据,发现数据变化时发布消息给依赖收集器Dep,通过noticy()通知观察者更新视图class Observer{
constructor(data){
console.log('observer')
this.observer(data)
}
observer(data){
/*
{
person:{
name:'',
fav:{
a:''
}
}
}
*/
if(data && typeof data=='object' ){ //判断是否为对象遍历并劫持
// console.log(Object.keys(data))
Object.keys(data).forEach(key=>{
// console.log(key,'key')
this.defineReactive(data,key,data[key])
})
}
}
// 劫持并监听所有的属性
defineReactive(obj,key,value){ //修改数据先走set
// 递归遍历
this.observer(value)
const dep = new Dep() //建立dep依赖收集器
// console.log('push')
// console.log(Dep.target) 初始化拿到data没有编译时只是赋予get和set方法,target还为null,并没有执行get方法,在编译阶段getval取值时才执行了get方法
Object.defineProperty(obj,key,{ //做劫持
enumerable:true, // 枚举
configurable:false, // 修改
get(){
// 订阅数据变化时且当dep有watcher时往dep添加观察者
// 在编译阶段时建立watcher推入subs
Dep.target && dep.addSub(Dep.target)
// console.log(Dep.target,'target2')
return value
},
set:(newVal)=>{
// person = {age:1} 赋于age:get set
this.observer(newVal)
if(newVal!==value){
value = newVal
}
//告诉dep通知变化
dep.notify()
},
// get
// 属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。
//执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。
// set
// 属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。
//该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。
})
}
}
Dep依赖收集器
class Dep{ //依赖收集
constructor() {
this.subs = []
console.log('dep')
}
//收集观察者watcher
addSub(watcher){
// console.log(watcher)
this.subs.push(watcher)
}
//通知观察者更新
notify(){
// console.log('通知观察者',this.subs)
this.subs.forEach(w=>w.update())
}
}
四、Watcher观察者
class Watcher{ //初始化数据时就建立了watcher,绑定到dep.target,在每个数据更新的方法中绑定
constructor(vm,expr,cb) {
console.log('watcher')
this.vm = vm
this.expr = expr
this.cb = cb
//先把旧值保存
this.oldVal = this.getOldVal()
}
getOldVal(){
Dep.target = this
let oldVal = compileUtil.getVal(this.expr,this.vm) //通过compileUtil.getVal触发了object.defineProtype()的get方法 将watcher推入subs 可以使用debugger查看执行栈
Dep.target = null
return oldVal
}
update(){
let newVal = compileUtil.getVal(this.expr,this.vm)
if(newVal!==this.oldVal){
this.cb(newVal) //new watcher创建的cb
}
}
}
视频观看请在哔哩哔哩搜索vue源码设计