- 大约 5 分钟
理解Vue的设计思想 MVVM
将视图View的状态和行为抽象化,让我们将视图UI和业务逻辑分开。
MVVM的三要素:数据响应式、模板引擎、渲染 数据响应式:监听数据变化并在视图中更新
- Object.defineProperty()
- Proxy
模板引擎:提供描述视图的模板引擎
- 插值表达式:
- 指令:v-bind、v-on、v-model、v-for、v-if...
渲染:将模板转换成html
- 模板 => vDom => dom
数据响应式原理
数据变更能否响应在视图中,就是数据响应式。 利用Object.defineProperty()
实现变更检测。
// reactive.js
function defineReactive(obj, key, val) {
observe(val);
Object.defineProperty(obj, key, {
get() {
console.log('get', key);
return val;
},
set(v) {
if (v !== val) {
val = v;
observe(v);
console.log('set', key, val);
// update(val);
}
},
})
}
// 数组响应式
// 1.替换数组原型的7个方法
const rawProto = Array.prototype;
// 备份,修改备份
const arrayProto = Object.create(rawProto);
['push', 'pop', 'shift', 'unshift', 'sort', 'reverse', 'splice'].forEach(method => {
arrayProto[method] = function () {
// 原始操作
rawProto[method].apply(this, arguments);
// 覆盖
console.log('覆盖' + method + '操作');
observe(...arguments);
}
});
function set(obj, key, val) {
defineReactive(obj, key, val);
}
function observe(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
if (Array.isArray(obj)) {
// 覆盖原型,替换7个变更操作
obj.__proto__ = arrayProto;
for(let i = 0; i < obj.length; i++) {
observe(obj[i]);
}
} else {
// 对象
Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]));
}
}
const obj = {
foo: 'foo',
bar: 'bar',
baz: {
a: 1
},
arr: [1, 2, 3],
};
// 对obj进行响应式处理
observe(obj);
// defineReactive(obj, 'foo', ' foooooo');
obj.foo; // get
obj.foo = '111'; // set
obj.bar; // get
obj.bar = '222'; // set
obj.baz; // get
obj.baz.a = 'aaa'; // set
obj.baz = {
a: 10
}
obj.baz.a = '10aaa';
obj.doo = 'doo';
obj.doo;
set(obj, 'doo', 'doo');
obj.doo = 'doo1';
obj.doo;
// 数组 重写数组的7个方法实现拦截 push pop shift unshift sort splice reverse
obj.arr[0];
obj.arr[0] = 2;
const temp = {b: 1}
obj.arr.push(temp);
temp.b = 2;
数组使用索引访问和赋值可以被监听到,但是使用Array的方法操作数组就监听不到了。 要想实现监听,就需要重写数组的7个方法实现拦截。
简单实现将js
中对象的time
属性渲染到html
。
<div id="app"></div>
<script>
// 首先引入上面的script代码,即reactive.js
// 放开其中的Object.defineProperty 中set的update
const app = document.querySelector('#app');
function update(val) {
app.innerText = val;
}
const obj = {time: 0};
observe(obj);
setInterval(() => {
obj.time = new Date().toLocaleTimeString();
}, 1000);
</script>
vue中的数据响应式
原理分析:
new Vue()
首先执行初始化,对data执行响应式处理,这个过程发生在Observer中- 同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在Compile中
- 同时定义一个更新函数和Watcher,将来对应数据变化时Watcher会调用更新函数
- 由于data的某个key在一个视图中可能出现多次,所以每个key都需要一个管家Dep来管理多个watcher
- 将来data中数据一旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数
涉及类型介绍
- Kvue: 框架构造函数
- Observer: 执行数据响应化(分辨数据是对象还是数组)
- Compile: 编译模板,初始化视图,依赖收集(更新函数,watcher创建)
- Watcher:执行更新函数(跟新dom)
- Dep:管理多个watcher,批量更新
Kvue
框架构造函数:执行初始化 执行初始化,对data执行响应化处理,主要是下面代码中的proxy
function defineReactive(obj, key, 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(v) {
if (v !== val) {
val = v;
observe(v);
console.log('set', key, val);
// update();
dep.notify();
}
},
})
}
function observe(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]));
}
function proxy(vm) {
Object.keys(vm.$data).forEach(key => {
Object.defineProperty(vm, key, {
get() {
return vm.$data[key];
},
set(val) {
vm.$data[key] = val;
}
})
})
}
class Kvue {
constructor(options) {
// 1.保存选项
this.$options = options;
this.$data = options.data;
// 2.对data选项做响应式处理
observe(this.$data);
// 2.5代理
proxy(this);
// 3.编译
new Compile(options.el, this);
}
}
编译 Compile
编译模板中Vue模板特殊语法,初始化视图、更新视图 编译dom->遍历子节点->编译节点(编译文本)->遍历属性(事件)->监听input、处理textContent、innerHtml、绑定click等
class Compile {
constructor(el, vm) {
// 保存Kvue实例
this.$vm = vm;
// 编译模板树
this.compile(document.querySelector(el));
}
// el模板根节点
compile(el) {
// 遍历el
// 1.获取el所有子节点
el.childNodes.forEach(node => {
if (node.nodeType === 1) {
// 元素
// console.log('element', node.nodeName);
this.compileElement(node);
// 继续递归
if (node.childNodes.length > 0) {
this.compile(node);
}
} else if (this.isInter(node)) {
// 插值文本
// console.log('text', node.textContent);
this.compileText(node);
}
})
}
update(node, exp, dir) {
// 初始化
const fn = this[`${dir}Update`];
fn && fn(node, this.$vm[exp]);
// 更新
new Watcher(this.$vm, exp, function (val) {
fn && fn(node, val);
})
}
// 处理插值文本
compileText(node) {
this.update(node, RegExp.$1, 'text');
}
// 编译element
compileElement(node) {
// 1.获取当前元素的所有属性,并判断他们是不是动态属性
const nodeAttr = node.attributes;
Array.from(nodeAttr).forEach(attr => {
const attrName = attr.name;
const exp = attr.value;
// 判断attrName是否有指令
if (attrName.startsWith('k-')) {
// 指令
// 截取k-后面的部分,特殊处理
const dir = attrName.substring(2);
// 判断是否存在指令处理函数,若存在,则调用
this[dir] && this[dir](node, exp);
}
// 判断attrName是否有事件
if (attrName.startsWith('@')) {
// @click="onclick"
const dir = attrName.substring(1);
// 事件监听
this.eventHandler(node, exp, dir);
}
})
}
// k-text
text(node, exp) {
this.update(node, exp, 'text');
}
textUpdate(node, val) {
node.textContent = val;
}
// k-html
html(node, exp) {
this.update(node, exp, 'html');
}
htmlUpdate(node, val) {
node.innerHTML = val;
}
// k-model
model(node, exp) {
// update方法只完成赋值和更新
this.update(node, exp, 'model');
// 事件监听
node.addEventListener('input', e => {
this.$vm[exp] = e.target.value;
})
}
modelUpdate(node, val) {
node.value = val;
node.value = val;
}
// {{xxxx}}
isInter(node) {
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
eventHandler(node, exp, dir) {
const fn = this.$vm.$options.methods && this.$vm.$options.methods[exp];
node.addEventListener(dir, fn.bind(this.$vm));
}
}
依赖收集
视图中会用到data中某key,这称为依赖。同一个key可能出现多次,每次都需要收集出来用一个watcher来维护它们,此过程称为依赖收集。 多个watch需要一个dep来管理,需要更新时由dep统一通知。 看下面案例,理出思路:
new Vue({
template: `<div>
<p>{{name1}}</p>
<p>{{name2}}</p>
<p>{{name1}}</p>
</div>`,
data: {
name1: 'name1',
name2: 'name2',
}
});
依赖(带动态数据的三个p)-> 收集 -> 变成三个watcher1/2/3 -> dep1(watcher1、watcher3) dep2(watcher2) 实现思路:
- defineReactive时为每一个key创建一个Dep实例
- 初始化视图时读取某个key,例如name1,创建一个watcher1
- 由于触发name1的getter方法,便将watcher1添加到name1对应的dep中
- 当name1更新,setter触发时,便可通过对应dep通知其管理所有watcher更新
Watcher
// 负责具体更新任务的watcher
class Watcher {
constructor(vm, key, updateFn) {
this.$vm = vm;
this.key = key;
this.updateFn = updateFn;
// 触发依赖收集
Dep.target = this;
vm[key];
Dep.target = null;
}
update() {
this.updateFn.call(this.$vm, this.$vm[this.key]);
}
}
Dep
// 和data中响应式的key之间是一一对应关系
class Dep {
constructor() {
this.deps = [];
}
addDep(dep) {
this.deps.push(dep);
}
notify() {
this.deps.forEach(dep => dep.update());
}
}