vue的设计思想(MVVM模式)
MVVM框架的三要素包括: 数据响应式 模板引擎及其渲染
- 数据响应式: 监听数据变化并在视图中更新
Object.defineProperty() ---- vue2中采用的数据劫持,数据拦截的策略
Proxy — vue3中采用的是代理模式 - 模板引擎: 提供描述视图的模板语法
插值: {{}}
指令: v–on, v-bind, v-model, v-for, v-if 等等 - 渲染: 如何将模板转换成为html
模板 => vdom => dom
数据响应式原理
数据变更能够响应在视图中,就是数据数据响应式. vue2中利用Object.defineProperty()实现变更检测.
简单实现
// Object.defineProperty()
// 对传入对象的key进行一次拦截, 对某个对象的某个key做拦截
function definReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log('get', key)
return val
},
set(newValue) {
if(val !== newValue) {
val = newValue
console.log('set', key, val)
}
}
})
}
const obj = {}
definReactive(obj, 'foo', 'foo')
obj.foo
obj.foo = 'foooooooooooooo1212'
结合视图
<meta charset="UTF-8">
<div id="app"></div>
<script>
function definReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log('get', key)
return val
},
/**
在上面定义函数的时候放了一个val, val是一个形参,
但是对一个形参的赋值看起来是没有意义的,但是这里却产生了影响,
产生影响的原因是这里形成了闭包(因为局部变量val的存在,其次内部
get函数将val返回出去形成了闭包)使val在内存中驻留
**/
set(newValue) {
if(val !== newValue) {
val = newValue
console.log('set', key, val)
update()
}
}
})
}
const obj = {}
definReactive(obj, 'foo', new Date().toLocaleTimeString())
function update() {
app.innerHTML = obj.foo
}
setInterval(() => {
obj.foo = new Date().toLocaleTimeString()
}, 1000);
</script>
遍历需要响应化的对象
// 遍历指定数据对象中的每个key,并拦截他们
function observe(obj) {
// 判断是否是一个对象
if(typeof obj !== 'object' || obj === null) {
return obj
}
Object.keys(obj).forEach(key => {
definReactive(obj, key, obj[key])
})
}
测试结果
const obj = {foo:'foo',bar:'bar',baz:{a:1}}
observe(obj)
obj.foo
obj.foo = 'foooooooooooo'
obj.bar
obj.bar = 'barrrrrrrrrrr'
obj.baz.a = 10 // 嵌套对象no ok 当有嵌套对象的时候,拦截不到,
// 需要另外处理
解决嵌套对象问题
function defineReactive(obj, key, val) {
observe(val)
Object.defineProperty(obj, key, {
//...
解决赋的值是对象的情况, 当出现下面的这种赋值,又会出现问题,因此在set操作的时候,进行一次响应式处理
obj.baz = {a:1}
obj.baz.a = 10 // no ok
set(newVal) {
if (newVal !== val) {
observe(newVal) // 新值是对象的情况
notifyUpdate()
但是如果添加/删除了新属性⽆法检测,例如下面的情况
obj.dong = 'dong'
obj.dong // 并没有get信息
因此需要写set方法,进行一次响应式处理
function set(obj, key, val) {
defineReactive(obj, key, val)
}
测试代码:
set(obj, 'dong', 'dong')
obj.dong
但是 defineProperty() 对数组的支持并不友好,在vue源码中,对数组的七个方法进行了重写,这里先不进行讲解
Vue中的数据响应化
目标代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="app">
<p>{{counter}}</p>
</div>
<script src="../node_modules/vue/dist/vue.js"></script> <script>
const app = new Vue({
el: '#app',
data: {
counter: 1
},
})
setInterval(() => {
app.counter++
}, 1000);
</script>
</body>
</html>
原理分析
new Vue()
⾸先执⾏初始化,对data执⾏响应化处理,这个过程发⽣在Observer中- 同时对模板执⾏编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发⽣在Compile中
- 同时定义⼀个更新函数和Watcher,将来对应数据变化时Watcher会调⽤更新函数
- 由于data的某个key在⼀个视图中可能出现多次,所以每个key都需要⼀个管家Dep来管理多个Watcher
- 将来data中数据⼀旦发⽣变化,会⾸先找到对应的Dep,通知所有Watcher执⾏更新函数
涉及的类型介绍
- kVue: 框架的构造函数
- Observe: 执行数据响应化(分辨数据是对象还是数组, 这里我只对对象进行处理)
- Compile: 编译模板,初始化视图, 收集依赖(更新函数, watcher创建)
- Watcher: 执行更新函数(更新dom)
- Dep: 管理多个Watcher, 批量更新
KVue
框架构造函数:执⾏初始化
执⾏初始化,对data执⾏响应化处理,kvue.js
// 遍历指定数据对象中的每个key,并拦截他们
function observe(obj) {
// 判断是否是一个对象
if(typeof obj !== 'object' || obj === null) {
return obj
}
// 每遇到一个对象,就创建一个Observer的实例
// 创建一个Observer 实例去做拦截操作
new Observer(obj)
}
// 根据value 类型做不同的操作(数组和对象)
class Observer {
constructor(value) {
this.value = value
// 应该需要判断value 类型, 然后对对象和数组进行不同的操作
// 这里只处理对象
// 遍历对象
this.walk(value)
}
walk(obj) {
Object.keys(obj).forEach(key => {
definReactive(obj, key, obj[key])
})
}
}
class kvue {
constructor(options) {
// 0: 保存options
this.$options = options
this.$data = options.data
// 1. 将data做响应式处理
observe(this.$data)
// 2. 为$data做代理
proxy(this, '$data')
}
}
为$data做代理, 代码如下,若不做代理则需要app.$data.counter++ 进行访问, 代理代码如下
// proxy代理函数: 让用户可以直接访问data里的属性
function proxy(vm, key) {
Object.keys(vm[key]).forEach(k => {
Object.defineProperty(vm, k, {
get() {
return vm[key][k]
},
set(v) {
vm[key][k] = v
}
})
})
}
编译 - Compile
编译模板中vue模板特殊语法,初始化视图、更新视图
初始化视图
根据节点类型编译,compile.js
class Compile {
// el - 宿主元素, vm - kvue 实例
constructor(el, vm) {
this.$el = document.querySelector(el)
this.$vm = vm
// 解析模板
if (this.$el) {
// 执行编译方法
this.compile(this.$el)
}
}
compile(el) {
// el是宿主元素
// 遍历,判断当前遍历元素的类型
el.childNodes.forEach(node => {
if (node.nodeType === 1) {
console.log('编译元素', node.nodeName)
} else if(this.isInter(node)) {
// 文本, {{xxxx}}
console.log('编译文本', node.textContent, RegExp.$1)
}
// 递归 ,判断node是否还有child
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node)
}
})
}
// 判断插值表达式
isInter(node) {
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
}
}
编译插值,compile.js
compile(el) {
// ...
} else if (this.isInerpolation(node)) {
// console.log("编译插值⽂本" + node.textContent);
this.compileText(node);
}
});
}
compileText(node) {
console.log(RegExp.$1);
node.textContent = this.$vm[RegExp.$1];
}
编译元素
compile(el) {
//...
if (this.isElement(node)) {
// console.log("编译元素" + node.nodeName);
this.compileElement(node)
}
}
compileElement(node) {
let nodeAttrs = node.attributes;
Array.from(nodeAttrs).forEach(attr => {
let attrName = attr.name;
let exp = attr.value;
if (this.isDirective(attrName)) {
let dir = attrName.substring(2);
this[dir] && this[dir](node, exp);
}
});
}
// 判断是否是指令
isDirective(attr) {
return attr.indexOf("k-") == 0;
}
// k-text 对应的操作函数
text(node, exp) {
node.textContent = this.$vm[exp];
}
// k-html 对应的操作函数
html(node, exp) {
node.innerHTML = this.$vm[exp]
}
依赖收集
视图中会⽤到data中某key,这称为依赖。同⼀个key可能出现多次,每次都需要收集出来⽤⼀个Watcher来维护它们,此过程称为依赖收集。
多个Watcher需要⼀个Dep来管理,需要更新时由Dep统⼀通知。看下⾯案例,理出思路:
new Vue({
template:
`<div>
<p>{{name1}}</p>
<p>{{name2}}</p>
<p>{{name1}}</p>
<div>`,
data: {
name1: 'name1',
name2: 'name2'
}
});
上面代码出现了两次name1和一次name2,总共有三个依赖,会有3个Watcher, 但是每个key只会是一个Dep
实现思路
- defineReactive时为每⼀个key创建⼀个Dep实例
- 初始化视图时读取某个key,例如name1,创建⼀个watcher1
- 由于触发name1的getter⽅法,便将watcher1添加到name1对应的Dep中
- 当name1更新,setter触发时,便可通过对应Dep通知其管理所有Watcher更新
创建Watcher,kvue.js
const watchers = [];//临时⽤于保存watcher测试⽤
// 监听器:负责更新视图
class Watcher {
constructor(vm, key, updateFn) {
// kvue实例
this.vm = vm;
// 依赖key
this.key = key;
// 更新函数
this.updateFn = updateFn;
// 临时放⼊watchers数组
watchers.push(this)
}
// 更新
update() {
this.updateFn.call(this.vm, this.vm[this.key]);
}
}
编写更新函数、创建watcher
// 调⽤update函数执插值⽂本赋值
compileText(node) {
// console.log(RegExp.$1);
// node.textContent = this.$vm[RegExp.$1];
this.update(node, RegExp.$1, 'text')
}
text(node, exp) {
this.update(node, exp, 'text')
}
html(node, exp) {
this.update(node, exp, 'html')
}
update(node, exp, dir) {
const fn = this[dir+'Updater']
fn && fn(node, this.$vm[exp])
new Watcher(this.$vm, exp, function(val){
fn && fn(node, val)
})
}
textUpdater(node, val) {
node.textContent = val;
}
htmlUpdater(node, val) {
node.innerHTML = val
}
声明Dep
class Dep {
constructor () {
this.deps = []
}
addDep (dep) {
this.deps.push(dep)
}
notify() {
this.deps.forEach(dep => dep.update());
}
}
创建watcher时触发getter
class Watcher {
constructor(vm, key, updaterFn) {
this.vm = vm
this.key = key
this.updaterFn = updaterFn
// 依赖收集
// watchers.push(this)
Dep.target = this
this.vm[this.key] // 触发上面的get
Dep.target = null
}
update() {
this.updaterFn.call(this.vm, this.vm[this.key])
}
}
依赖收集,创建Dep实例
// 对传入对象的key进行一次拦截, 对某个对象的某个key做拦截
function definReactive(obj, key, val) {
// 如果val 是对象, 则需要继续递归处理
observe(val)
// Dep创建的最佳时刻
const dep = new Dep()
Object.defineProperty(obj, key, {
get() {
console.log('get', key)
// 依赖收集
Dep.target && dep.addDep(Dep.target)
return val
},
set(newValue) {
if(val !== newValue) {
// 如果newValue 是对象也需要做响应式处理
observe(newValue)
val = newValue
console.log('set', key, val)
// console.log(watchers)
// watchers.forEach(w => w.update())
// 通知更新
dep.notify()
}
}
})
}
下面附上kvue,js 的完整代码
// 实现kvue 的构造函数
// 1. 将data做响应式处理
// Object.defineProperty()
// 对传入对象的key进行一次拦截, 对某个对象的某个key做拦截
function definReactive(obj, key, val) {
// 如果val 是对象, 则需要继续递归处理
observe(val)
// Dep创建的最佳时刻
const dep = new Dep()
Object.defineProperty(obj, key, {
get() {
console.log('get', key)
// 依赖收集
Dep.target && dep.addDep(Dep.target)
return val
},
set(newValue) {
if(val !== newValue) {
// 如果newValue 是对象也需要做响应式处理
observe(newValue)
val = newValue
console.log('set', key, val)
// console.log(watchers)
// watchers.forEach(w => w.update())
// 通知更新
dep.notify()
}
}
})
}
// 遍历指定数据对象中的每个key,并拦截他们
function observe(obj) {
// 判断是否是一个对象
if(typeof obj !== 'object' || obj === null) {
return obj
}
// Object.keys(obj).forEach(key => {
// definReactive(obj, key, obj[key])
// })
// 每遇到一个对象,就创建一个Observer的实例
// 创建一个Observer 实例去做拦截操作
new Observer(obj)
}
function set(obj, key, val) {
// 进行响应式处理
definReactive(obj, key, val)
}
// proxy代理函数: 让用户可以直接访问data里的属性
function proxy(vm, key) {
Object.keys(vm[key]).forEach(k => {
Object.defineProperty(vm, k, {
get() {
return vm[key][k]
},
set(v) {
vm[key][k] = v
}
})
})
}
// 根据value 类型做不同的操作(数组和对象)
class Observer {
constructor(value) {
this.value = value
// 应该需要判断value 类型, 然后对对象和数组进行不同的操作
// 这里只处理对象
// 遍历对象
this.walk(value)
}
walk(obj) {
Object.keys(obj).forEach(key => {
definReactive(obj, key, obj[key])
})
}
}
class kvue {
constructor(options) {
// 0: 保存options
this.$options = options
this.$data = options.data
// 1. 将data做响应式处理
observe(this.$data)
// 2. 为$data做代理
proxy(this, '$data')
// 3. 编译模板
new Compile('#app', this)
}
}
class Compile {
// el - 宿主元素, vm - kvue 实例
constructor(el, vm) {
this.$el = document.querySelector(el)
this.$vm = vm
// 解析模板
if (this.$el) {
// 执行编译方法
this.compile(this.$el)
}
}
compile(el) {
// el是宿主元素
// 遍历,判断当前遍历元素的类型
el.childNodes.forEach(node => {
if (node.nodeType === 1) {
// console.log('编译元素', node.nodeName)
// 节点编译
this.compileElement(node)
} else if(this.isInter(node)) {
// 文本, {{xxxx}}
// console.log('编译文本', node.textContent, RegExp.$1)
// 将文本转义一下
this.compileText(node)
}
// 递归 ,判断node是否还有child
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node)
}
})
}
// 判断插值表达式
isInter(node) {
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
}
// 编译文本
compileText(node) {
// node.textContent = this.$vm[RegExp.$1]
this.update(node, RegExp.$1, 'text')
}
// 编译元素节点: 分析指令, @事件
compileElement(node) {
// 获取属性遍历值
const nodeAttrs = node.attributes
Array.from(nodeAttrs).forEach(attr => {
// 指令: k-xxx 开头
const attrName = attr.name
const exp = attr.value
if (this.isDirective(attrName)) {
const dir = attrName.substring(2) // 得到xxx
// 指令的实际的操作方法
this[dir] && this[dir](node, exp)
}
})
}
// 判断是否是指令
isDirective(attrName) {
return attrName.indexOf('k-') === 0
}
// 执行text指令对应的更新函数
text(node, exp) {
// node.textContent = this.$vm[exp]
//需要调用update方法
this.update(node, exp, 'text')
}
// k-text 对应的操作函数
textUpdater(node, val) {
node.textContent = val
}
// k-html 对应的操作函数
html(node, exp) {
// node.innerHTML = this.$vm[exp]
this.update(node, exp, 'html')
}
htmlUpdater(node, val) {
node.innerHTML = val
}
// 提取update, 初始化和更新函数创建
update(node, exp, dir) {
const fn = this[dir+'Updater']
// 初始化过程
fn && fn(node, this.$vm[exp])
// 更新
new Watcher(this.$vm, exp, function(val) {
fn && fn(node, val)
})
}
}
// Watcher: 跟视图中的依赖是一对一的关系 1:1
// const watchers = []
class Watcher {
constructor(vm, key, updaterFn) {
this.vm = vm
this.key = key
this.updaterFn = updaterFn
// 依赖收集
// watchers.push(this)
Dep.target = this
this.vm[this.key] // 触发上面的get
Dep.target = null
}
update() {
this.updaterFn.call(this.vm, this.vm[this.key])
}
}
// 和某个key, 一一对应, 管理多个Watcher, 数据更新时通知他们做更新工作
class Dep {
constructor() {
this.deps = []
}
addDep(watcher) {
this.deps.push(watcher)
}
notify() {
this.deps.forEach(watcher => watcher.update())
}
}
该版本并没有涉及到虚拟dom, 虚拟dom讲解, 会放到后面.