React & Vue
React的数据监听变化的原理是通过进行对虚拟Dom(对象)树和真实的Dom树来引入比较进行局部的渲染。(需要使用PureComponent/shouldComponent来优化。否则可能导致大量不必要的VDOM的重新渲染)。
vue的数据监听变化的原理是通过getter/setter以及一些函数的劫持,能够精确的知道数据的变化,不需要特别的优化能到很好的性能
1.v-key & Diff
所谓虚拟DOM,使我们可以不直接操作DOM元素,只操作数据便可以重新渲染页面。而隐藏在背后的原理便是其高效的Diff算法,它的核心是基于两个简单的假设:
-
两个相同的组件产生类似的DOM结构,不同的组件产生不同的DOM结构
-
同一个层级的一组节点(兄弟节点),他们可以通过唯一的id进行区分
当页面的数据发生变化时,Diff算法只会比较同一层级的节点:
如果节点类型不同,直接干掉前面的节点,再创建并插入新的节点,不会再比较这个节点以后的子节点了
如果节点类型相同,则会重新设置该节点的属性,从而实现节点的更新
//keys的作用,vue与React都一样
<ul>
<li>first</li>
<li>second</li>
</ul>
<ul>
<li>first</li>
<li>second</li>
<li>third</li>
</ul>
//比较上述两个列表,由于是直接在末尾新增一个元素,此时性能没有问题
//但若是下例,在列表头部新增一个元素,会使得所以的li元素被销毁与重建,即针对每个子元素产生mutate而不是移动重用,所以若简单实现性能开销就比较大了
<ul>
<li>Duke</li>
<li>Villanova</li>
</ul>
<ul>
<li>Connecticut</li>
<li>Duke</li>
<li>Villanova</li>
</ul>
//所以 keys 就诞生了,当子元素拥有 key 时,使用 key 来匹配原有树上的子元素以及最新树上的子元素,
//就知道只有带着 '2014' key 的元素是新元素,带着 '2015' 以及 '2016' key 的元素仅仅移动了
// keys 需要在同组兄弟节点中保持唯一
<ul>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
<ul>
<li key="2014">Connecticut</li>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
需要使用key来给每个节点做一个唯一的标识,Diff算法就可以正确的识别此节点,找到正确的位置区插入新的节点
所以一句话,key的作用主要是为了高效的更新虚拟DOM。另外vue的在使用相同标签名元素的过渡切换时,也会使用到key属性,其目的也是为了让vue可以区分他们,否则vue只会替换其内部属性而不会触发过渡效果。
2.父子组件通信
1.父向子
vue中数据流是从父到子的向下的数据流,当父组件的属性发生改变后,子组件使用了该属性会同步
2.子向父
/*
1.传递函数,将父组件的状态以及更新状态的函数作为子组件的props传入并按需调用即可
2.$emit / $on
- $emit和$on的事件必须在一个公共的实例上,才能够触发;
- 该机制其实就是保存父组件的函数,需要改变父组件的状态时调用其中对应的事件函数即可
*/
//公共实例对象
import Vue from 'vue';
let Bus = new Vue();
export default Bus;
//App.vue
import Bus from './EventBus.js';
created() { // 父监听
Bus.$on('changeStus',(data)=> { // 这里的this是Bus,即公共实例
console.log(this);
this.stus = data;
});
}
//Son.vue
import Bus from './EventBus.js';
methods:{
change() {
Bus.$emit('changeStus',[1,2,3]);// 触发 Bus 公共实例上的指定方法
}
}
3.router-view简单实现
1.history
- 只有调用
go||forward函数,才会根据历史记录也换页面;切换的过程会触发popstate事件 - 如果不希望看到丑陋的
#可以使用history模式 , 其原理依赖于history.pushState函数- a标签点击以后,如果没有
#必然会页面跳转发起请求 - 使用pushState函数可以改变 url,如
/abc但却不会发起请求 - 通过
location.pathname获取该值/abc做页面局部的替换
- a标签点击以后,如果没有
<a class="router-link" href="/" >Home</a>
<a class="router-link" href="/about" >About</a>
<div id="router-view"></div>
const links = document.querySelectorAll(".router-link");
for (const link of links) {
link.addEventListener("click",function(e){
e.preventDefault();
const href = e.target.getAttribute("href");
history.pushState({},"title",href);//变更地址栏
switchContent(href);//切换视图
});
}
function switchContent(href){
const view = document.querySelector("#router-view");
switch(href){
case "/":
view.innerHTML = `Home`;
break;
case "/about":
view.innerHTML = `About`;
break;
default:
view.innerHTML = `unknown`;
}
}
2.router-view实现原理
1.非hash方式
- Vue.use 初始化给 _route 添加监视,监听 popstate事件 ( 即 /xxx ) 或 hashchange( 即 #/xxx ) 事件,更改后更新视图
<router-link to="xxx">link</router-link>,即<a href=""#/index>index</a>,禁用组件其它事件,对click进行操作- 将
/xxx匹配所有路由规则 routers , 得到匹配结果 matchednew Router({path:'/index',name:'index',component:Index})
- 将 matched 保存到 _route 供未来
router-view使用 history.pushState(/xxx)改变地址栏- 触发更新,调用组件 render 函数,获取 _route 并渲染 _route 保存的matched组件
2.hash凡是
- 安装插件时监视 hashchange( 即 #/xxx ) 事件,监视 _router
- 处理 routers 获取到 path 关联组件
- 等待 hashchange 触发,匹配 routes 中的数据,最终得到 matched 赋值给 _router
- 触发 _router 的监视行为,router-view 这个组件此时获取到 _router 来作为渲染的内容
4.Vue 原理
0.脏检查/observe/Proxy
//1.[Angular v1.x] : 使用脏检查,性能差,全浏览器支持;
let obj = {a:1,b:22}
let obj_copy = Object.assign({},obj)
json.a = 333; //update
delete json.b; //delete
json.c = 666; //add
for(var name in obj_copy){
if(!obj[name]){
console.log(`delete ${name}`);
}else if(obj[name] !== obj_copy[name]){
console.log(`update ${name} from ${obj_copy[name]} to ${obj[name]}`);
}
}
for(var name in obj){
if(!obj_copy[name]) console.log(`add new attr ${name}`);
}
//[Vue] :Obejct.observe ---> ES6 Proxy,原生支持,性能最高,高级浏览器支持;给target对象套一层proxy壳,所有操作只能通过proxy对象或子类对象来完成,若直接操作target是无法被proxy监控到的。
//2.观察者 : Object.observe 已被启用
let json={};
Object.observe(json, (changes)=>{ //被监控
console.log('变了',changes);
});
json.a=12;
//3.proxy
let json={a: 12, b: 5};
let p=new Proxy(json, {
get(target, key, proxy){ //target-->json
if(key in target){
return target[key];
}else{
throw new Error('no this key');
}
},
set(target, key, val, proxy){
//console.log(target, key, val, proxy);
if(val>=0 && val<=100){
target[key]=val;
}else{
throw new Error('invaild value, only accept 0-100');
}
},
has(target, key){ //其实就是对代理对象p使用 in 操作时会调用has
console.log(target, key);
},
deleteProperty(target, key){
// throw new Error("read only");
delete target[key];
}
});
alert('c' in p); //has()
console.log(p);
p.c = 99; //right set
p.d = 1000; //error set
delete p.a; //delete
console.log(p);
1.my-vue - ES5
/**
1.观察者,可观察者 (应该是模拟 Obejct.observe )
2.Object.defineProperty的 setter/getter 实现数据的监控与响应
3.编译 template 为DOM元素后,将DOM元素映射为可观察对象,再正则匹配打补丁;setter监控到数据更新后,直接遍历所有可观察对象,全部data即不管有没有发生更新都用以更新可观察对象中的DOM元素属性。
即并没有模拟实现 虚拟DOM 与 真实DOM 的比对与按需局部更新DOM,只模拟实现了数据驱动视图,保持数据与视图的同步
注意:熟悉 setter/getter 的触发与运用
*/
//定义观察者
var tempObservable;
// 观察者
function Observer() {
this.observables = [];
}
Observer.prototype.notify = function () {
for (var i = this.observables.length - 1; i >= 0; i--) {
this.observables[i].update();
}
}
Observer.prototype.subscribe = function () {
this.observables.push(tempObservable);
}
// 可观察对象 具备事件触发的能力
function Observable(node, propName, data) {
this.$node = node;
this.$propName = propName;
this.$data = data;
}
Observable.prototype.update = function () {
if (this.$node.nodeType === 1) { //element
this.$node.value = this.$data[this.$propName];
} else if (this.$node.nodeType === 3) { //text
// 会触发get 在get内部判断
this.$node.nodeValue = this.$data[this.$propName];
}
}
//------------------------------------------------------------
//定义 Vue class
function Vue(options) { //接收 options 转存为 Vue 实例属性
this.$options = options;
this.$el = options.el;
this.$data = options.data; //接收 data function
this.?data = this.$data();
this.$template = options.template;
this.init();
}
//初始化 Vue 实例,监控 ?data
Vue.prototype.init = function () {
//1.遍历 ?data ,监视属性
this.defineReactive(this.?data); //????
//2.解析DOM
this.compiler(this.$template, this.?data);
}
//编译 template ,正则查找并打补丁
Vue.prototype.compiler = function (tempstr, data) {
// 将tempstr 插入到app中
var box = document.querySelector(this.$el);
box.innerHTML = tempstr;
// 分析box 获取到其中的标记
var nodes = box.children[0].childNodes;
// 正则匹配
var regexText = /.*\{\{(.*)\}\}.*/;
var regexV = /^v-(.*)$/;
for (var i = nodes.length - 1; i >= 0; i--) {
var node = nodes[i];
if (node.nodeType === 3) {
// 分类判断 nodeType === 3 文本节点 nodeValue
var result = regexText.exec(node.nodeValue);
if (result) {
// 获取结果与this.?data匹配 this.?data[xxx]
// 触发获取
this.textMatch(result[1].trim(), node); // text
}
} else if (node.nodeType === 1) {
// NodeTypes === 1 input标签 ele.value
// 获取元素属性名称
var nodeAttrs = node.attributes;
console.log(nodeAttrs);
for (var j = 0; j < nodeAttrs.length; j++) {
var attr = nodeAttrs[j]; // name,value
var result = regexV.exec(attr.name);
if (result) {
console.log(attr.name, attr.value, '被匹配了', result);
// 要根据指令名称干活
this.directive[result[1]](attr.value, node, data);
}
}
}
}
}
//Vue 指令处理
Vue.prototype.directive = {
model: function (propName, node, data) { // 参数是text
var self = this;
console.log(this)
tempObservable = new Observable(node, propName, data);
// 给一个初始值
node.value = data[propName]; // 触发get
// 给元素添加事件
node.addEventListener('input', function (e) {
// 触发set
data[propName] = e.target.value;
});
}
}
//使用 setter/getter 来监控 ?date 的数据,实现响应
Vue.prototype.defineReactive = function (object) {
var observer = new Observer();
for (var key in object) {
let tempValue = object[key];
Object.defineProperty(object, key, {
set: function (newValue) {
console.log("setter");
tempValue = newValue;
observer.notify();
},
get: function () {
console.log("getter");
if (tempObservable) {
observer.subscribe();
tempObservable = null;
}
return tempValue;
}
});
}
}
//专用于匹配处理文本补丁: {{attribute}}
Vue.prototype.textMatch = function (propName, node) {
// 看这里: 1:创建存储信息的行为 可观察对象
// 2:将其挂载全局
// 3: 触发get函数,并从全局中取出1
tempObservable = new Observable(node, propName, this.?data);
// console.log('找到文本节点啦', propName, node);
// 替换当前node的值,并赋值为data的数据
node.nodeValue = this.?data[propName]; // 触发get
}
//使用样例
let vm = new Vue({
el: "#app",
data() {
return { text: 'abc' }
},
template: `<div>
<input type="text" v-model="text" />
{{ text }}
</div>`
})
2.my-vue - ES6
/*
1.定义一个 Vue Class 并继承 Proxy
2.定义 Proxy 构造函数来监控 options.data 的数据变化,每监控到变化则触发 _render 重渲染
3.每次渲染更新都是先使用带指令原始模板替换上一次的渲染结果,然后用最新的 data 数据编译模板,匹配Vue指令并打补丁,其中{{}}是替换, : 和 @ 指令是遍历元素直接修改元素属性
即并没有模拟实现 虚拟DOM 与 真实DOM 的比对与按需局部更新DOM,只模拟实现了数据驱动视图,保持数据与视图的同步
*/
(function (global) {
//原型链顶端不能为 undefined , 必须是一个有效值
Proxy.prototype = Proxy.prototype || Object.prototype;
//使用 Proxy 来监控 data 变化,与其分别创建Vue实例和用于监控Vue实例中的data的Proxy两个对象
//不如让 Vue 直接继承 Proxy,Vue 实例自身就可以直接使用 Proxy API 监控 data
class MyVue extends Proxy {
constructor(options) {
let _this, _container = {};
let data = options.data || {};
//调用父类Proxy监控data
//为什么不直接监控 this 呢? 1.super之后才能使用 this;2.this容易造成setter死递归
super(data, {
get(target, property, receiver) {
if (property in target) return target[property];
throw new Error(`[Vue warn]: Property or method "${property}" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option.`);
},
set(target, property, newValue, receiver) {
target[property] = newValue;
//触发重新渲染
_container._render(_this);
}
});
_this = this;
//el / data / methods / render
_container.$el = document.querySelector(options.el);
//old_el保存的是含所有Vue指令和{{attribute}}的原始模板 template
_container.$old_el = _container.$el.cloneNode(true);//true:递归复制子孙节点
_container.$methods = options.methods || {};
_container.$data = data;
_container._render = render.bind(_container);
//首次渲染
_container._render(_this);
}
}
function render(_this) {
//this : _container ; _this : vm instance
//将上一次渲染的结果 替换回 原始template,因为原始template中才有 {{attribute}} 而不是 {{value}}
//重新渲染模板需要知道原有的Vue指令和{{attribute}}
this.$el.parentNode.replaceChild(this.$old_el, this.$el); // replaceChild(newnode,oldnode);
this.$el = this.$old_el;
//需要重新深拷贝一份原始template,因为上句代码会导致 this.$el 与 this.$old_el 引用同一份原始template
//而后续基于 this.$el 操作,从而导致 this.$old_el 中的原始template最终被破坏
this.$old_el = this.$old_el.cloneNode(true);
//处理{{}}
this.$el.innerHTML = this.$el.innerHTML.replace(/\{\{[^\}]+\}\}/g, (str) => {
let s = str.substring(2, str.length - 2);
return _eval_exp(s, this.$data);
});
let aEle = Array.from(this.$el.getElementsByTagName('*'));
aEle.push(this.$el);
aEle.forEach(el => {
Array.from(el.attributes).forEach(attr => {
//处理:xxx="xxx"
if (attr.name.startsWith(':')) {
let name = attr.name.substring(1);
let value = _eval_exp(attr.nodeValue, this.$data);
el.removeAttribute(attr.name);
el.setAttribute(name, value);
//处理@xxx="xxx"
} else if (attr.name.startsWith('@')) {
let name = attr.name.substring(1);
let fn = this.$methods[attr.nodeValue];
if (!fn) {
throw new Error(`no '${attr.nodeValue}' method`)
} else {
el.addEventListener(name, fn.bind(_this), false);
}
} else if (attr.name == 'v-for') {
//
} else if (attr.name == 'v-html') {
//
}
});
});
}
function _eval_exp(s, $data) {
s = s.replace(/\w+/g, (s) => {
return '$data.' + s;
});
return eval(s);
}
global.MyVue = MyVue;
})(window);
<div id="div">
<div :title="name" class="">我叫{{name}},我是:aaa,我今年{{age}}岁</div>
<div class="">{{a+b}}</div>
<input type="button" value="按钮" @click="fn">
</div>
<script src="./my-vue.js"></script>
<script>
let vm = new MyVue({
el: '#div',
data: {name: 'blue', age: 18, a: 12, b: 5},
methods: {
fn(){
console.log("click event");
this.a++; this.age++; this.name+='-';
}
}
});
</script>
5.practice : vue-ele
1.axios mount
Vue.prototype.axios = axios.create({
baseURL: 'http://localhost:8090/api/',
timeout: 1000
});
2.main entry & component
//main vue instance
new Vue({
el: '#app',
router, //instance of vue-router
store, //vuex
components: { App }, //root component
template: '<App/>'
})
//root component : 主要用于指定路由占位符 router-view 和 根节点 #app
<template>
<div id="app">
<router-view/>
</div>
</template>
<script>
export default {name: 'App'}
</script>
3.vue-router
Vue.use(Router);
export default new Router({
routes: [
{
path: '/',
name: 'index',
component: Index
},
{
path: '/detail/:id/',
name: 'detail',
component: Detail
}
]
})
6.Vuex
new Store --- state --- mutation(commit) --- action(dispatch) --- getter(computed)
//user.store.js
export default new Vuex.Store({
state: { },
mutations:{
addUserCount(state, arg){
state.user.count+=arg;
}
},
actions:{
addUser({commit, state}, arg){
commit('addUserCount', arg);
}
}
})
Vue.use(Vuex);
const store=new Vuex.Store({
strict: true, //强制全用mutation完成修改——如果在mutation之外修改报错
state: {
count: 0,
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false }
]
},
modules: {
user: require('./stores/user.store'), //this.$store.state.user
company: {count: 0} //this.$store.state.company
},
mutations: {
addCount(state, arg){
arg=arg||1;
state.count+=arg;
},
minusCount(state, arg){
arg=arg||1;
state.count-=arg;
}
},
actions: {
abc({commit, state}, arg){
commit('addCount', arg);
}
},
getters: { //类似 Vue 中的 Computed 用于派生属性
count(){
return state.count;
},
doneTodos: state => {
return state.todos.filter(todo => todo.done)
}
}
});
//组件内访问:index.vue
this.$store.state.count
this.$store.dispatch('abc', 3); //dispatch actions
this.$store.dispatch('user/addUser')
6.practice : MintUI
1.自定义Vue插件
// vue插件必须具备Install函数
function Installer () {
// 自身初始化行为
}
Installer.install = function (Vue) {
// 接收Vue的构造函数,给原型挂载属性或注册全局组件或过滤器
// console.log(Vue);
// 1: 注册全局组件
Vue.component('test',{
template:`<h1>哈哈</h1>`
});
// 2: 挂载属性
// Vue.prototype.$log = function() {
// console.log('hahaahhahaah')
// }
// this.$log = 'abxadksadas' 子类对象可以修改父类的属性
let log = function () {
console.log('我们自己插件的log函数')
}
// 给原型定义属性的获取和设置,设置:见鬼去吧,获取就给你
Object.defineProperty(Vue.prototype,'$log',{
// 设置 $log属性时的行为 || 不给,不能设置
set:function (newV) {
console.log('你做梦');
// log = newV;
},
get:function () {
// 获取方式
return log;
}
})
}
export default Installer;
//usage
import Installer from '@/plugins/installer';
Vue.use(Installer);
2.拦截器
// 定义拦截器
// 1: 请求发起前显示loading open();
Axios.interceptors.request.use(function(config) {
// 不变配置:可变,可以设置公共的请求头操作
MintUI.Indicator.open({
text: '玩儿命加载中...',
spinnerType: 'fading-circle'
});
// console.log(config);
return config; // config:{ headers}
})
// 2: 响应回来后关闭loading close()
Axios.interceptors.response.use(function(response) {
//reponse: { config:{ },data:{} ,headers }
// 接收响应头或者响应体中的数据,保存起来,供请求的拦截器中使用头信息操作
MintUI.Indicator.close();
// console.log(response);
return response;
})
3.注册全局组件
import MyUl from './components/common/MyUl';
import MyLi from './components/common/MyLi';
Vue.component(MyUl.name,MyUl);
Vue.component(MyLi.name,MyLi);
import MySwipe from './components/common/Swipe';
Vue.component(MySwipe.name,MySwipe);
4.全局过滤器
Vue.filter('convertTime',function(data,formatStr){
return Moment(data).format(formatStr); // 2015-04-16T03:50:28.000Z
});
// 相对时间过滤器
Vue.filter('relTime',function(time){
return Moment(time).fromNow(); // 2015-04-16T03:50:28.000Z
});