如果有关注本头条号的话,相信大家已经看过MVVM实现原理的视频版了,如果还没关注的话,希望大家动动手指点波关注,后面会陆续发一些质量相对比较高的教学视频(不限题材),如果有想看的教学视频,也可以私信或者留言本号,本号会尽量满足大家的需求。
言归正传,今天这篇文章就是对前面有关MVVM视频的一些总结。根据视频数量,本文也分为8个小节进行总结,方便那些没时间看视频的同学了解和学习MVVM的实现原理,让自己在工作或者面试中掌握主动权。
第一节:了解ES5的Object.defineProperty属性
// 使用Object.defineProperty来定义对象
let obj = {}
Object.defineProperty(obj,'person',{
value:'mike', // key:person,value:mike
configurable:true, // 是否可配置,默认false
writable:true, // 是否可写,默认false
enumerable:true, // 是否可枚举,默认false
get() { // 获取obj.person的值时,会调用get方法
return 'mike'
},
set(val) { // 更新obj.person的值时,会调用set方法
console.log(val)
}
})
// 需要注意:get和set方法不能与value和writable属性同时使用,否则出现报错如下
// Uncaught TypeError: Invalid property descriptor. Cannot both specify accessors and a value or writable attribute, #<Object>
// at Function.defineProperty (<anonymous>)
第二节:数据劫持
数据劫持:使用Object.defineProperty递归定义Vue实例中data的所有属性。
// 创建一个Vue构造函数
function Vue(options = {}) {
this.$options = options // 将所有属性挂着在$options
let data = this._data = this.$options.data
observe(data)
}
// 创建一个Observe构造函数,方便递归调用
function Observe(data) {
for(let key in data) {
let val = data[key]
observe(val) // 当前val可能也是个对象,所有需要递归进行数据劫持
Object.defineProperty(data,key,{
enumerable:true,
get() {
return val
},
set(newVal) {
if(newVal == val) return
val = newVal // 当赋值操作是,也可能赋值一个对象,也需要数据劫持
observe(newVal)
}
})
}
}
// 观察对象并给对象增加Object.defineProperty
function observe(data) {
if(typeof data !== 'object') return // 递归到不是对象的时候停止
return new Observe(data)
}
// new一个Vue实例
let vm = new Vue({
el:'#app',
data:{
a:1
}
})
// 调用
vm._data.a // 1
第三节:数据代理
数据代理:将vm._data.a的取值方式改为vm.a,即vm实例代理vm._data。
/// 创建一个Vue构造函数
function Vue(options = {}) {
this.$options = options // 将所有属性挂着在$options
let data = this._data = this.$options.data
observe(data)
// this代理this._data
for(let key in data) {
Object.defineProPerty(this,key,{
enumerable:true,
get() {
return this._data[key]
},
set(newVal){
this._data[key] = newVal
}
})
}
}
第四节:编译模板
编译模板:通过在内存中创建文档碎片来递归节点,然后通过正则匹配模板符号{{}}得到模板符号里面的值,最后匹配data里面的key,替换到节点的内容中。
// 创建一个Vue构造函数
function Vue(options = {}) {
this.$options = options // 将所有属性挂着在$options
let data = this._data = this.$options.data
observe(data)
// this代理this._data
....
// 编译模板
new Compile(options.el,this)
}
// 编译
function Compile(el,vm) {
vm.$el = document.querySelector(el)
let fragment = document.createDocumentFragment()
while(child = vm.$el.firstChild) { // 将#app中的内容移入内存中
fragment.appendChild(child)
}
replace(fragment)
//模板符号中的值替换称vm中对应的值
function replace(fragment) {
Array.form(fragment.childNodes).forEach((node) => { // 循环每一层节点
let text = node.textContent
let reg = /\{\{(.*)\}\}/
if(node.nodeType == 3 && reg.test(text)){
let arr = RegExp.$1.split('.'); // [a,a] [b]
let val = vm
arr.forEach((k) => {
val = val[k]
})
node.textContent = text.replace(reg,val)
}
if(node.childNodes) { // 存在子节点,递归
replace(node)
}
})
}
}
第五节:发布订阅模式原理
发布订阅:订阅就是往一个数组里面添加事件fn1,fn2,fn3...,发布就是遍历数组中的事件,逐个执行。
function Dep() {
this.subs = []
}
Dep.prototype.addSub = function(sub) { // 订阅
this.subs.push(sub)
}
Dep.prototype.notify = function() { // 发布
this.subs.forEach(sub => sub.update()) // 假定每个事件都是有个update事件
}
function Watcher(fn) {
this.fn = fn
}
Watcher.prototype.update = function() {
this.fn()
}
let watcher = new Watcher(function() { // 监听函数
console.log(1)
})
let dep = new Dep()
dep.add(watcher1) // 订阅watcher1
dep.add(watcher2) // 订阅watcher2
dep.notify()
第六节:连接数据和试图
function Dep() {
this.subs = []
}
Dep.prototype.addSub = function(sub) { // 订阅
this.subs.push(sub)
}
Dep.prototype.notify = function() { // 发布
this.subs.forEach(sub => sub.update()) // 假定每个事件都是有个update事件
}
function Watcher(vm,exp,fn) {
this.fn = fn
this.vm = vm
this.exp = exp // 添加到订阅中
Dep.target = this
let val = vm
let arr = exp.split('.').forEach((k) => { // 触发get方法
val = val[k]
})
Dep.target == null
}
Watcher.prototype.update = function() {
let val = this.vm
let arr = this.exp.split('.')
arr.forEach((k) => {
val = val[k]
})
this.fn(val) // newVal
}
//模板符号中的值替换称vm中对应的值
function replace(fragment) {
Array.form(fragment.childNodes).forEach((node) => {
let text = node.textContent
let reg = /\{\{(.*)\}\}/
if(node.nodeType == 3 && reg.test(text)){
let arr = RegExp.$1.split('.'); // [a,a] [b]
let val = vm
arr.forEach((k) => {
val = val[k]
})
// // 添加代码 : 订阅
new Watcher(vm,RegExp.$1,function(newVal){
node.textContent = text.replace(reg,val)
})
}
if(node.childNodes) {
replace(node)
}
})
}
// 创建一个Observe构造函数,方便递归调用
function Observe(data) {
for(let key in data) {
let val = data[key]
observe(val)
Object.defineProperty(data,key,{
enumerable:true,
get() {
Dep.target && dep.addSub(Dep.target) // 添加代码
return val
},
set(newVal) {
if(newVal == val) return
val = newVal
observe(newVal)
dep.notify() // 添加的代码
}
})
}
}
第七节:双向数据绑定的原理
//模板符号中的值替换称vm中对应的值
function replace(fragment) {
Array.form(fragment.childNodes).forEach((node) => { // 循环每一层节点
...
// 添加代码
if(node.nodeType == 1){
let nodeAttrs = node.attributes
Array.from(nodeAttrs).forEach((attr) => {
let name = attr.name
let exp = attr.value
if(name.indexOf('v-') == 0) { // v-model
node.value = vm[exp]
}
new Watcher(vm,exp,function(newVal){
node.value = newVal // 当watcher触发时会自动将内容放到输入框里
})
})
}
node.addEventListener('input',function(e){
let newVal = e.target.value
vm[exp] = newVal
})
})
...
}
第八节:computed实现原理
computed:可以缓存,只是把数据挂在vm上。
// 创建一个Vue构造函数
function Vue(options = {}) {
this.$options = options // 将所有属性挂着在$options
let data = this._data = this.$options.data
observe(data)
// this代理this._data
....
//初始化computed 属性
initComputed.call(this)
// 编译模板
new Compile(options.el,this)
}
initComputed() {
let vm = this
let computed = this.$options.computed
Object.keys(computed).forEach((key) => {
get:typeof computed[key] === 'function' ? computed[key] : computed[key].get,
set(){}
})
}