1.双向绑定原理
Vue内部通过Object.defineProperty方法属性拦截的方式,把data对象里每个数据的读写转化成getter/setter,当数据变化时通知视图更新。
实现MVVM的数据双向绑定,是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来给各个属性添加setter,getter并劫持监听,在数据变动时发布消息给订阅者,触发相应的监听回调。就必须要实现以下几点:
1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
2、实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
3、实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
实现双向绑定原理
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<h1 id="name"></h1>
<input type="text">
<input type="button" value="改变data内容" onclick="changeInput()">
<script src="observer.js"></script>
<script src="watcher.js"></script>
<script>
function myVue (data, el, exp) {
this.data = data;
observable(data); //将数据变的可观测
el.innerHTML = this.data[exp]; // 初始化模板数据的值
new Watcher(this, exp, function (value) {
el.innerHTML = value;
});
return this;
}
var ele = document.querySelector('#name');
var input = document.querySelector('input');
var myVue = new myVue({
name: 'hello world'
}, ele, 'name');
//改变输入框内容
input.oninput = function (e) {
myVue.data.name = e.target.value
}
//改变data内容
function changeInput(){
myVue.data.name = "难凉热血"
}
</script>
</body>
</html>
observer.js
/**
* 把一个对象的每一项都转化成可观测对象
* @param { Object } obj 对象
*/
function observable (obj) {
if (!obj || typeof obj !== 'object') {
return;
}
let keys = Object.keys(obj);
keys.forEach((key) =>{
defineReactive(obj,key,obj[key])
})
return obj;
}
/**
* 使一个对象转化成可观测对象
* @param { Object } obj 对象
* @param { String } key 对象的key
* @param { Any } val 对象的某个key的值
*/
function defineReactive (obj,key,val) {
let dep = new Dep();
Object.defineProperty(obj, key, {
get(){
dep.depend();
console.log(`${key}属性被读取了`);
return val;
},
set(newVal){
val = newVal;
console.log(`${key}属性被修改了`);
dep.notify() //数据变化通知所有订阅者
}
})
}
class Dep {
constructor(){
this.subs = []
}
//增加订阅者
addSub(sub){
this.subs.push(sub);
}
//判断是否增加订阅者
depend () {
if (Dep.target) {
this.addSub(Dep.target)
}
}
//通知订阅者更新
notify(){
this.subs.forEach((sub) =>{
sub.update()
})
}
}
Dep.target = null;
watcher.js
class Watcher {
constructor(vm,exp,cb){
this.vm = vm;
this.exp = exp;
this.cb = cb;
this.value = this.get(); // 将自己添加到订阅器的操作
}
get(){
Dep.target = this; // 缓存自己
let value = this.vm.data[this.exp] // 强制执行监听器里的get函数
Dep.target = null; // 释放自己
return value;
}
update(){
let value = this.vm.data[this.exp];
let oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal);
}
}
}
2.nextTick原理
在vue中有nextTick,官方解释,它可以在DOM更新完毕之后执行一个回调。
涉及到vue的DOM更新,下面解释一下vue的更新数据原理
Vue异步更新队列(数据驱动视图)
由于Vue DOM更新是异步执行的,即修改数据时,视图不会立即更新,
只要监听数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更,在缓冲时会去除重复数据,从而避免不必要的计算和DOM操作。
然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。
等同一数据循环中的所有数据变化完成之后,再统一进行视图更新。
为了确保得到更新后的DOM,所以设置了 Vue.nextTick()方法。
nextTick内部原理
- nextTick主要使用了
宏任务
和微任务
- Vue 在内部对异步队列尝试使用原生的
Promise.then
、MutationObserver
和setImmediate
,如果执行环境不支持,则会采用setTimeout(fn, 0)
代替。
vue是如何知道DOM更新完成
vue源码:
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
步骤:
- 先判断Promise
- 在判断MutationObserver
- 在判断setImmediate
- 最后setTimeout
vue更新DOM是异步操作,等所有数据更新后才会渲染DOM。
所以要在更新DOM后操作,vue提供了nextTick。nextTick方法在源码内会通过各种方法检测DOM是否更新完成。
如果有promise.then就用promise监听,没有就降级成MutationObserver,还不支持就降级setImmediate,都不支持就用setTimeout
总结
vue的nextTick方法的实现原理:
- vue用异步队列的方式来控制DOM更新和nextTick回调先后执行
- 微任务因为其高优先级特性,能确保队列中的微任务在一次事件循环前被执行完毕
- 因为浏览器和移动端兼容问题,vue不得不做了微任务(microtask)和宏任务(macrotask)的兼容(降级)方案
3.vue-router原理
实现原理:vue-router 的原理就是更新视图而不重新请求页面
两种路由模式
- hash 模式。默认是 hash 模式,基于浏览器 history api,使用 window.addEventListener("hashchange", callback, false) 对浏览进行监听。当调用 push 时,把新路由添加到浏览器访问历史的栈顶。使用 replace 时,把浏览器访问历史的栈顶路由替换成新路由 hash 的值(等于 url 中 # 及其以后的内容)。浏览器是根据 hash 值的变化,将页面加载到相应的 DOM 位置。锚点变化只是浏览器的行为,每次锚点变化后依然会在浏览器中留下一条历史记录,可以通过浏览器的后退按钮回到上一个位置。
- history 模式。基于浏览器 history api,使用 window.onpopstate 对浏览器地址进行监听。对浏览器 history api 中的 pushState()、replaceState() 进行封装,当方法调用,会对浏览器的历史栈进行修改。从而实现 URL 的跳转而无需加载页面,但是它的问题在于当刷新页面的时候会走后端路由,所以需要服务端的辅助来完成,避免 url 无法匹配到资源时能返回页面。
两种模式的比较,history模式更有优势
1)hash模式有#,history模式没有#,更美观
2)pushState设置的新url是和当前url同源的任意url,hash模式只可以修改#后面的内容,也就是只可以设置与当前同文档的url
3)pushState设置的新url和当前url相同时也会把记录添加进记录栈中,而hash只有新的和当前的不同的时候才会添加到栈中
4)如果是history模式,他要匹配/,如果匹配不到,就会报错