组件化
组件通信
组件常用通信方式
- props
- EventBus
- Vuex
- 自定义事件
其他
- $parent
- $children
- $root
- $refs
- provide/inject
非prop特性
- $attrs
- $listeners
props
// child props: { msg: String } // parent <Helloworld msg="hello world" />
事件总线
任意两个组件之间传值常用事件总线或vuex的方式// Bus 事件派发、监听和回调管理,实际使用中会用Vue代替Bus class Bus { constructor() { this.callbacks = {}; } $on(name, fn) { this.callbacks[name] = this.callbacks[name] || []; this.callbacks[name].push(fn); } $emit(name, args) { if (this.callbacks[name]) { this.callbacks[name].forEach((cb) => cb(args)); } } } // main.js Vue.prototype.$bus = new Bus(); // child1 this.$bus.$on('foo', handle); // child2 this.$bus.$emit('foo')
- vuex
通过创建唯一的全局数据管理者store,通过它管理数据并通知组件状态变更 自定义事件
子给父传值// child this.$emit('add', good); // parent <Cart @add="cartAdd($event)"></Cart>
$parent/$root
兄弟组件之间通信可通过共同祖辈搭桥,$parent或$root// brother1 this.$parent.$on('foo', handle); // brother2 this.$parent.$emit('foo')
$children
父组件可以通过$children访问子组件实现父子通信// parent this.$children[0].xxx = 'xxx';
$attrs/$listeners
包含了父作用域中不作为prop被识别(且获取)的特性绑定(class和style除外)。当一个组件没有声明任何prop时,这里会包含所有父作用域的绑定(class和style除外),并且可以通过v-bind="$attrs"传入内部组件。这些特性在创建高级别的组件时非常有用。// child: 没有在props中声明foo <p>{{$attrs.foo}}</p> // parent <Helloworld foo="foo">
$refs
获取子节点引用// parent <Helloworld ref="hw"> mounted() { this.$refs.hw.xx = 'xxx'; }
provide/inject
能实现祖先和后代之间传值// ancestor provide() { return { foo: 'foo' } } // descendant inject: ['foo']
插槽
插槽语法是Vue实现的内容分发API,用于复合组件开发。
匿名插槽
// comp1 <div> <slot></slot> </div> // parent <comp>hello</comp>
具名插槽
将内容化分发到子组件指定位置// comp2 <div> <slot></slot> <slot name="content"></slot> </div> // parent <comp2> <!-- 默认插槽用default做参数 --> <template v-slot:default>默认插槽</template> <!-- 具名插槽用插槽名做参数 --> <template v-slot:content>具名插槽</template> </comp2>
作用域插槽
分发内容要用到子组件中的数据// comp3 <div> <slot :foo="foo"></slot> </div> // parent <comp3> <!--把v-slot的值指定为作用域上下文对象--> <template v-slot:default="slotProps"> 来自子组件数据:{{slotProps.foo}} </template> </comp3>
MVVM
MVVM 框架的三要素:数据响应式、模板引擎和渲染
数据响应式:监听数据变化并在视图中更新
- Object.defineProperty()
- Proxy
模板引擎:提供描述视图的模板语法
- 插值: {{}}
- 指令:v-bind,v-on,v-model,v-for,v-if
渲染:如何将模板转换为html
- 模板=》vdom=》dom
实现
- 数据响应式原理
// defProp.js
// 响应式
const obj = {};
function defineReactive(obj, key, val) {
// 对传入obj进行访问拦截
Object.defineProperty(obj, key, {
get() {
console.log('get' + key);
return val;
},
set(newVal) {
if (newVal !== val) {
console.log('set ' + key + ':' + newVal);
val = newVal;
}
}
});
}
defineReactive(obj, 'foo', 'foo');
obj.foo;
obj.foo = 'foooo';
- 和视图关联
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script>
const obj = {};
function defineReactive(obj, key, val) {
// 对传入obj进行访问拦截
Object.defineProperty(obj, key, {
get() {
console.log('get' + key);
return val;
},
set(newVal) {
if (newVal !== val) {
console.log('set ' + key + ':' + newVal);
val = newVal;
// 更新函数
update();
}
}
});
}
function update() {
app.innerText = obj.foo;
}
defineReactive(obj, 'foo', '');
obj.foo = new Date().toLocaleTimeString();
setInterval(() => {
obj.foo = new Date().toLocaleTimeString();
}, 1000);
</script>
</body>
</html>
- 优化
function defineReactive(obj, key, val) {
// 递归
observe(val);
// 对传入obj进行访问拦截
Object.defineProperty(obj, key, {
get() {
console.log('get ' + key);
return val;
},
set(newVal) {
if (newVal !== val) {
console.log('set ' + key + ':' + newVal);
// 如果传入的newVal依然是obj,需要做响应化处理
observe(newVal);
val = newVal;
}
}
});
}
function observe(obj) {
if (typeof obj !== 'object' || obj == null) {
return;
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key]);
})
}
function set(obj, key, val) {
defineReactive(obj, key, val);
}
const obj = { foo: 'foo', bar: 'bar', baz: { a: 1 } };
// defineReactive(obj, 'foo', 'foo');
oba=observe(obj);
obj.foo;
obj.foo = 'foooo';
obj.bar;
obj.bar = 'barrrr';
obj.baz.a;
obj.baz.a = 3;
obj.baz = {a:1,b:2}
obj.baz.b = 99
set(obj, 'dong', 'dong');
obj.dong;
Vue
思路
- new Vue()首先执行初始化,对data执行响应化处理,这个过程发生在Observer中
- 同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在compile中
- 同时定义一个更新函数和Watcher,将来对应数据变化时Watcher会调用更新函数
- 由于data的某个key在视图中可以出现多次,所以每个key都需要一个管家Dep来管理多个Watcher
- 将来data中的数据一旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数
职责划分
- CVue:框架构造函数
- Observer:执行数据响应化(分辨数据是对象还是数组)
- Compile:编译模板,初始化视图,收集依赖(更新函数、watcher创建)
- Watcher:执行更新函数(更新dom)
- Dep:管理多个Watcher,批量更新
实现
- 响应化处理及数据代理
// cvue.js
class CVue {
constructor(options) {
// 保存选项
this.$options = options;
this.$data = options.data;
// 响应化处理
observe(this.$data);
// 代理
Proxy(this, '$data')
}
}
// 代理函数,提供对$data中数据的直接访问
function Proxy(vm, sourceKey) {
Object.keys(vm[sourceKey]).forEach(key => {
Object.defineProperty(vm, key, {
get() {
return vm[sourceKey][key];
},
set(newVal) {
vm[sourceKey][key] = newVal;
}
})
})
}
function defineReactive(obj, key, val) {
// 递归
observe(val);
// 对传入obj进行访问拦截
Object.defineProperty(obj, key, {
get() {
console.log('get ' + key);
return val;
},
set(newVal) {
if (newVal !== val) {
console.log('set ' + key + ':' + newVal);
// 如果传入的newVal依然是obj,需要做响应化处理
observe(newVal);
val = newVal;
}
}
});
}
function observe(obj) {
if (typeof obj !== 'object' || obj == null) {
return;
}
// 创建Observer实例
new Observer(obj);
}
// 根据对象类型决定如何做响应化
class Observer {
constructor(value) {
this.value = value;
// 判断其类型
if (typeof value === 'object') {
this.walk(value);
}
}
// 对象数据响应化
walk(obj) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key]);
})
}
// TODO 数组数据响应化
}
- 编译
编译模板中vue模板特殊语法,初始化视图、更新视图
编译器
// compile.js
class Compiler {
/**
* @param el 宿主元素
* @param vm CVue实例
*/
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el);
if (this.$el) {
// 执行编译
this.compile(this.$el);
}
}
compile(el) {
// 递归遍历DOM
const childNodes = el.childNodes;
Array.from(childNodes).forEach((node) => {
// 判断节点类型
// 如果是元素,则遍历其属性判断是否是指令或事件,然后递归子元素
if (this.isElement(node)) {
// console.log('编译元素' + node.nodeName);
this.compileElement(node);
} else if(this.isInter(node)) { // 如果是文本,则判断是否插值绑定
// console.log('编译插值绑定' + node.textContent);
this.compileText(node);
}
// 递归子节点
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node);
}
});
}
isElement(node) {
return node.nodeType === 1;
}
isInter(node) {
// 文本标签且内容为{{xx}}
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
compileElement(node) {
// 遍历属性列表
const nodeAttrs = node.attributes;
Array.from(nodeAttrs).forEach(attr => {
// 约定指令格式 c-xx="yy"
const attrName = attr.name; // c-xx
const exp = attr.value; // yy
if (this.isDirective(attrName)) {
const dir = attrName.substring(2); // xx
// 执行指令
this[dir] && this[dir](node, exp);
}
})
}
isDirective(attr) {
return attr.indexOf('c-') === 0;
}
// k-text
text(node, exp) {
node.textContent = this.$vm[exp];
}
// k-html
html(node, exp) {
node.innerHTML = this.$vm[exp];
}
compileText(node) {
node.textContent = this.$vm[RegExp.$1];
}
}
使用编译器
// cvue.js
class CVue {
constructor(options) {
// 保存选项
this.$options = options;
this.$data = options.data;
// 响应化处理
observe(this.$data);
// 代理
Proxy(this, '$data');
// 创建编译器
new Compiler(this.$options.el, this);
}
}
// ...
测试
<div id="app">
<!-- 插值 -->
<p>{{counter}}</p>
<!-- 指令 -->
<p c-text="counter"></p>
<p c-html="desc"></p>
</div>
<script src="cvue.js"></script>
<script src="compile.js"></script>
<script>
const app = new CVue({
el: '#app',
data: {
counter: 1,
desc: '<span style="color:blue;">hello cvue</span>',
},
});
setInterval(() => {
app.counter++;
}, 1000);
</script>
- 数据监听
视图中会用到data中某key,叫依赖。同一个key可能出现多次,每次都需要收集出来用一个watcher来维护它,这个过程为依赖收集。多个watcher需要一个Dep来管理,需要更新时由Dep统一通知。
定义watcher并在数据更新时触发watcher的update
// cvue.js
// 观察者:保存更新函数,值发生变化调用更新函数
const watchers = [];
class Watcher {
constructor(vm, key, updateFn) {
this.vm = vm;
this.key = key;
this.updateFn = updateFn;
watchers.push(this);
}
update() {
this.updateFn.call(this.vm, this.vm[this.key]);
}
}
function defineReactive(obj, key, val) {
// 递归
observe(val);
// 对传入obj进行访问拦截
Object.defineProperty(obj, key, {
get() {
console.log('get ' + key);
return val;
},
set(newVal) {
if (newVal !== val) {
console.log('set ' + key + ':' + newVal);
// 如果传入的newVal依然是obj,需要做响应化处理
observe(newVal);
val = newVal;
// 执行更新函数
watchers.forEach(w => w.update());
}
}
});
}
编译时绑定watcher
// compile.js
// 编译器
class Compiler {
// ...
compileText(node) {
// node.textContent = this.$vm[RegExp.$1];
this.update(node, RegExp.$1, 'text');
}
textUpdater(node, value) {
node.textContent = value;
}
// k-text
text(node, exp) {
// node.textContent = this.$vm[exp];
this.update(node, exp, 'text');
}
// k-html
html(node, exp) {
// node.innerHTML = this.$vm[exp];
this.update(node, exp, 'html');
}
htmlUpdater(node, value) {
node.innerHTML = value;
}
update(node, exp, dir) {
// 初始化
// 指令对应的更新函数xxUpdater
const fn = this[dir + 'Updater'];
fn && fn(node, this.$vm[exp]);
// 更新 封装一个更新函数,可以更新对应dom元素
new Watcher(this.$vm, exp, function(val) {
fn && fn(node, val);
});
}
}
- 依赖收集
// cvue.js
function defineReactive(obj, key, val) {
// 递归
observe(val);
// 创建一个Dep和当前的key一一对应
const dep = new Dep();
// 对传入obj进行访问拦截
Object.defineProperty(obj, key, {
get() {
console.log('get ' + key);
// 依赖收集
Dep.target && dep.addDep(Dep.target);
return val;
},
set(newVal) {
if (newVal !== val) {
console.log('set ' + key + ':' + newVal);
// 如果传入的newVal依然是obj,需要做响应化处理
observe(newVal);
val = newVal;
// 执行更新函数
// watchers.forEach(w => w.update());
dep.notify();
}
}
});
}
class Watcher {
constructor(vm, key, updateFn) {
this.vm = vm;
this.key = key;
this.updateFn = updateFn;
// Dep.target静态属性上设置为当前watcher实例
Dep.target = this;
this.vm[this.key]; // 读取触发getter
Dep.target = null; // 收集完就置空
}
update() {
this.updateFn.call(this.vm, this.vm[this.key]);
}
}
// Dep 依赖,管理某个key相关所有的watcher实例
class Dep {
constructor() {
this.deps = [];
}
addDep(dep) {
this.deps.push(dep);
}
notify() {
this.deps.forEach(dep => dep.update());
}
}
- 事件绑定
// compile.js
// 编译器
class Compiler {
// ...
compileElement(node) {
// 遍历属性列表
const nodeAttrs = node.attributes;
Array.from(nodeAttrs).forEach(attr => {
// 约定指令格式 c-xx="yy"
const attrName = attr.name; // c-xx
const exp = attr.value; // yy
if (this.isDirective(attrName)) {
const dir = attrName.substring(2); // xx
// 执行指令
this[dir] && this[dir](node, exp);
}
// 事件处理
if (this.isEvent(attrName)) {
// @click="onClick"
const dir = attrName.substring(1) // "click"
// 事件监听
this.eventHandler(node, exp, dir);
}
})
}
isEvent(dir) {
return dir.indexOf('@') === 0;
}
eventHandler(node, exp, dir) {
// 在实例中 methods: { onClick: function(){} }
const fn = this.$vm.$options.methods && this.$vm.$options.methods[exp];
node.addEventListener(dir, fn.bind(this.$vm));
}
// ...
}
- c-model:语法糖,value设定,事件监听
// compile.js
// 编译器
class Compiler {
// ...
// c-model="xx"
model(node, exp) {
// update方法
this.update(node, exp, 'model');
// 事件监听
node.addEventListener('input', e => {
// 新的值赋值给数据
this.$vm[exp] = e.target.value;
});
}
modelUpdater(node, value) {
// 表达元素赋值
node.value = value;
}
}