1.双向绑定和单向数据流
vue是单向数据流,父组件通过属性props传值给子组件,子组件也是通过emit修改父组件的属性
双向绑定(单个 v-model 多个 .sync )
双向绑定是一种语法糖,依然是遵循单向数据流,是通过Object.defineProperty来做响应式更新
2. 列表的key值不要用index,用唯一值(例如id)
当你用index作为key时,虚拟dom是以key作为凭证,如果数组出现插入,删除等操作时候,有可能会出现问题,所以要极力避免
3. 响应式更新
getter进行依赖的收集,当data里面的数据页面没有用到的时候,就是没有收集到依赖,当setter的时候,是不会更新的
1. Vue框架对数组的push、pop、shift、unshift、sort、splice、reverse方法进行了改造,
在调用数组的这些方法时,Vue会Notify Wacher并更新视图
2. Object.defineProperty是不会响应上述的方法的
3. Vue 不能检测对象属性的添加或删除,因为vue只会将data里面存在的key值进行get,set的处理,
可以使用vm.$set 实例方法来设置添加或删除的值,确保被监听到
4. vue不能检测索引导致的数组的变动, 解决方案是使用 vm.$set 实例方法,或者用第一步的方法
具体参考vue官方注意事项:响应式注意事项
4. 仿造elementui的form表单的功能,简易功能的demo
4-1. MInput 输入框 实现v-model需要的逻辑,就是value和input事件的结合
<template>
<input :type="type" :value="value" @input="textChange" />
</template>
<script>
import emitter from "element-ui/src/mixins/emitter"; //引用elementui内部的
export default {
componentName: "MInput",
mixins: [emitter], //进行组合到当前组件
components: {},
props: {
value: {
type: String,
required: true
},
type: {
type: String,
default: "text"
}
},
data() {
return {};
},
methods: {
/**
* 这里做了个防抖
*/
textChange(e) {
this.debounce(() => {
let txt = e.target.value;
this.$emit("input", txt);
//触发父级MFormItem的监听事件m.form.item.validate
this.dispatch("MFormItem", "m.form.item.validate");
}, 300)();
},
/**
* 防抖
*/
debounce(fn, wait) {
let timeout = null;
return () => {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(fn, wait);
};
}
}
};
</script>
<style lang="scss" scoped></style>
4-2. MFormItem 包住input,实现错误消息的验证和提示的功能,展示label
<template>
<div class="fm-item-wrapper">
<div class="fm-item-content">
<p class="fm-item-label">{{ label }}</p>
<slot></slot>
</div>
<p class="error-txt" v-if="errorStatus">{{ errorText }}</p>
</div>
</template>
<script>
import emitter from "element-ui/src/mixins/emitter"; //引用elementui内部的
import Schema from "async-validator";
export default {
componentName: "MFormItem",
mixins: [emitter], //进行组合到当前组件
components: {},
props: {
prop: String,
label: String
},
inject: ["mForm"], //获取上面传下来的数据mForm
data() {
return {
errorText: "", //错误消息文案
errorStatus: false //错误消息状态
};
},
mounted() {
if (this.prop) {
// 这里是做了一个循环,找到指定的组件componentName名称,进行emit
// dispatch(componentName, eventName, params) {
// var parent = this.$parent || this.$root;
// var name = parent.$options.componentName;
// while (parent && (!name || name !== componentName)) {
// parent = parent.$parent;
// if (parent) {
// name = parent.$options.componentName;
// }
// }
// if (parent) {
// parent.$emit.apply(parent, [eventName].concat(params));
// }
// },
this.dispatch("MForm", "m.form.addField", [this]); //触发[增加要验证的属性]的事件,用于总体验证是否成功
}
this.$on("m.form.item.validate", this.validate); //监听input数据变化了的发送的要验证的事件
},
methods: {
// 这里用到了async-validator
validate(callback = () => {}) {
const rule = this.mForm.rules[this.prop];
const value = this.mForm.model[this.prop];
const schema = new Schema({ [this.prop]: rule });
schema.validate({ [this.prop]: value }, err => {
if (err) {
this.errorText = err[0].message;
this.errorStatus = true;
callback();
} else {
this.errorText = "";
this.errorStatus = false;
}
});
}
}
};
</script>
<style lang="scss" scoped>
.fm-item-wrapper {
padding: 10px;
border: 1px solid #999;
}
.fm-item-content {
display: flex;
align-items: center;
}
.fm-item-label {
font-size: 14px;
font-weight: bold;
padding-right: 15px;
}
.error-txt {
font-size: 12px;
color: red;
margin-top: 5px;
}
</style>
4-3. MForm 表单组件,实现总体的验证,是否成功
<template>
<form>
<slot></slot>
</form>
</template>
<script>
export default {
componentName: "MForm",
components: {},
props: {
model: Object,
rules: Object
},
/**
* 向下传入数据,当前MForm,方便子级组件获取数据
*/
provide() {
return {
mForm: this
};
},
data() {
return {
fields: [] //需要监听错误的MFormItem的实例数组
};
},
created() {
// 监听m.form.addField事件,由子级MFormItem触发
this.$on("m.form.addField", field => {
if (field) {
this.fields.push(field);
}
});
},
methods: {
//总体验证是否通过
validate(callback) {
if (!this.model) {
return;
}
let valid = true;
//没有验证,直接返回
if (this.fields.length === 0 && callback) {
callback(true);
return;
}
this.fields.forEach(field => {
//调用MFormItem的验证方法
field.validate(() => {
valid = false;
});
});
callback(valid);
}
}
};
</script>
<style lang="scss" scoped></style>
4-4. 使用组件的页面
<template>
<div class="home">
<m-form :model="model" :rules="rules" ref="login">
<m-form-item prop="userName" label="用户名">
<m-input v-model="model.userName"></m-input>
</m-form-item>
<m-form-item prop="password" label="密码">
<m-input v-model="model.password"></m-input>
</m-form-item>
</m-form>
<el-button @click="submitMsg">提交数据</el-button>
</div>
</template>
<script>
// @ is an alias to /src
import MForm from "@/components/form/MForm";
import MInput from "@/components/form/MInput";
import MFormItem from "@/components/form/MFormItem";
export default {
name: "home",
components: {
MForm,
MInput,
MFormItem
},
data() {
return {
model: {
userName: "",
password: ""
},
rules: {
userName: [
{ required: true, message: "用户名不能为空" },
{ min: 3, max: 5, message: "长度在 3 到 5 个字符" }
],
password: [{ required: true, message: "密码不能为空" }]
}
};
},
methods: {
submitMsg() {
this.$refs.login.validate(valid => {
if (valid) {
alert("开始去请求登录");
} else {
console.log("有错误信息");
}
});
}
}
};
</script>
5. Vue源码(响应式原理) 手动实现 data的简易模式
m.html: 实现如下的功能([插值,指令text,双向绑定model,事件,指令html])
<!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>{{ name }}</p>
<p m-text="name"></p>
<p>{{ age }}</p>
<p>
{{ doubleAge }}
</p>
<input type="text" m-model="name">
<p><button @click="changeName">改变名字</button></p>
<div m-html="html"></div>
</div>
<script src="compile.js"></script>
<script src="mVue.js"></script>
<script>
const app=new MVue({
el:'#app',
data:{
name:'张三',
age: 18,
html:'<button>这是个html形成的按钮</button>'
},
created(){
console.log("开始");
setTimeout(()=>{
this.name="异步后的名字";
},500)
},
methods:{
changeName(){
this.name="修改后的名字";
this.age=100;
}
}
})
</script>
</body>
</html>
mVue.js:
class MVue{
//构造函数
constructor(options){
this.$options=options;
this.$data=options.data;
this.observe(this.$data); //监听数据
new Compile(options.el,this);//编译
options.created && options.created.call(this); //执行生命周期,要绑定作用域
}
// 监听数据
observe(value){
if(value && typeof value!=='object'){
return;
}
Object.keys(value).forEach((key)=>{
this.defineData(value,key,value[key]);
this.proxyData(key); //代理vm.$data,针对每个key属性
})
}
//做个代理,vm.$data里面属性映射到vm上,可以用vm[key]直接访问
proxyData(key){
Object.defineProperty(this,key,{
get(){
return this.$data[key]; //访问的时候返回$data的值
},
set(newVal){
this.$data[key]=newVal; //这里面进行赋值,就会触发defineData里面的defineProperty的set
}
})
}
// 设置值的setter和getter 用 Object.defineProperty
defineData(obj,key,val){
this.observe(val); //递归循环遍历,到深层次
const dep=new Dep();
Object.defineProperty(obj,key,{
get(){
Dep.target && dep.addDep(Dep.target);//访问属性时候,进行依赖收集
//这就解释了为何只有template里面使用的属性才会有更新机制
return val;
},
set(newVal){
if(newVal===val){
return;
}
val=newVal;
dep.notify();//值变化了,通知更新
}
});
}
}
//依赖 用来管理watcher
class Dep{
constructor(){
this.deps=[]; //存放若干watcher,一个属性对应一个watcher,比如foo.bar,foo是一个,bar是一个
}
//添加依赖
addDep(dep){
this.deps.push(dep);
}
//通知进行更新
notify(){
this.deps.forEach((dep)=>dep.update()); //这里的dep就是watcher
}
}
class Watcher{
constructor(vm,key,cb){
Dep.target=this; //将watcher的当前实例,指定为Dep的静态属性target
this.vm=vm; //实例
this.key=key; //属性
this.cb=cb; //compile.js更新那边的回调函数
this.vm[this.key];//访问当前的属性,则调用getter,进行Dep的依赖收集
Dep.target=null; //释放
}
//更新方法,执行回调,在compile.js里面进行node的更新
update(){
this.cb && this.cb.call(this.vm,this.vm[this.key]);
}
}
compile.js: 编译器 [对非html的特殊形式进行解析]
// new Compile(el,vm)
class Compile{
constructor(el,vm){
//宿主节点
this.$el=document.querySelector(el);
this.$vm=vm;
if(this.$el){
this.$fragment=this.nodeToFragment(this.$el); //获取html片段
this.compile(this.$fragment); //执行后已经是html
this.$el.appendChild(this.$fragment);
}
}
// 移动dom,进行搬家
nodeToFragment(node){
const frag=document.createDocumentFragment();
let child;
while(child=node.firstChild){
frag.appendChild(child);
}
return frag;
}
//编译过程
compile(el){
const childNodes=el.childNodes;
Array.from(childNodes).forEach((node)=>{
if(this.isElement(node)){
//元素
const nodeAttrs=node.attributes;
Array.from(nodeAttrs).forEach((attr)=>{
const name=attr.name; //属性
const exp=attr.value; //值
//指令
if(this.isDirective(name)){
//m-text
const dir=name.substring(2);
this[dir] && this[dir](node,this.$vm,exp);//执行指令
}
//事件
if(this.isEvent(name)){
const dir=name.substring(1);
this.eventHandler(node,this.$vm,dir,exp);//执行指令
}
})
}
if(this.isInterpolation(node)){
//插值文本
this.compileText(node);
}
node.childNodes && node.childNodes.length>0 && this.compile(node);
})
}
// 事件处理函数
eventHandler(node,vm,dir,exp){
const method=vm.$options && vm.$options.methods[exp];
if(dir && method){
node.addEventListener(dir,method.bind(vm));
//要绑定this,这里不是立即执行,只是函数的引用,所以用bind
}
}
//是否是元素
isElement(node){
return node.nodeType===1;
}
//是否是指令
isDirective(attr){
return attr.indexOf('m-')===0
}
//是否是事件
isEvent(attr){
return attr.indexOf('@')===0
}
//指令的m-text的text方法
text(node,vm,exp){
this.update(node,vm,exp,'text');
}
//指令的m-html的html方法
html(node,vm,exp){
this.update(node,vm,exp,'html');
}
//指令m-model的model执行方法
model(node,vm,exp){
this.update(node,vm,exp,'model');
node.addEventListener('input',(e)=>{
vm[exp]=e.target.value; //赋值,触发依赖更新,会执行Dep的notify
})
}
//是否是插值
isInterpolation(node){
return node.nodeType===3 && /\{\{(\s*)(.*)(\s*)\}\}/.test(node.textContent);
}
//编译文本
compileText(node){
const reg=RegExp.$2.trim();
this.update(node,this.$vm,reg,'text');
}
// 更新node,通用方法
update(node,vm,exp,dir){
const updateFn= this[dir+'Updater'];
updateFn(node,vm[exp]); //初始化更新
//依赖收集
new Watcher(vm,exp,(val)=>{
updateFn(node,val);
})
}
//文本的更新
textUpdater(node,val){
node.textContent=val;
}
//model双向绑定的数据赋值的更新
modelUpdater(node,val){
node.value=val;
}
//html的更新
htmlUpdater(node,val){
node.innerHTML=val;
}
}