vue原理剖析
基础结构
模板 :界面展示代码
逻辑业务功能:
美化布局:
生命周期

语法和概念
- 差值表达式
- 指令
- 计算属性和侦听器
- class和style绑定
- 条件渲染/列表渲染
- 表单输入绑定
- 组件:可复用的vue实例
- 插槽
- 插件
- 混入mixin
- 深入响应式原理
模拟vue-router hash模式的实现
// 具体代码在目录code/homework_test文件夹下,
// src/router/index.js路由配置文件中修改配置模式
const router = new VueRouter({
mode: 'hash',
routes,
})
export default router;
- 具体的hash模式相关的知识点梳理: vue-router默认使用hash模式。 使用URL的hash来模拟一个完整的URL,当URL改变时,页面不会重新加载。hash(#)是URL的锚点,代表的是网页中的一个位置,值改版#后的部分,浏览器只会滚动到相应位置,不会重新加载网页,也就是说hash出现在URL中,但不会被包含在http请求中,对后端完全没有影响,因此改变hash不会重新加载页面;同时,每一次改变#后面的部分,都会在浏览器的访问历史中增加一个记录,使用后退按钮,就可以回到上一个位置;所以说Hash模式通过锚点值的改变,根据不同的值,渲染指定DOM位置的不同数据。
-
问题:如何监听hash变化 - hashchange()事件。
1.用class关键字初始化一个路由
class Routers {
constructor() {
// 以键值对的形式储存路由
this.routes = {};
// 当前路由的url
this.currentUrl = '';
}
}
2.实现路由hash存储与执行。在初始化完毕后,需要考虑两个问题
- 将路由的hash以及对应的callback函数储存
- 触发路由hash变化后,执行对应的callback函数
class Routers {
constructor() {
this.routes = {};
this.currentUrl = '';
}
// 将path路径与对应的callback函数储存
route (path, callback) {
this.routes[path] = callback || function () {};
}
// 刷新
refresh () {
// 获取当前URL中的hash路径
this.currentUrl = location.hash.slice(1) || '/';
// 执行当前hash路径的callback函数
this.routes[this.currentUrl]();
}
}
3.监听对应事件,只需要在实例化class的时候监听上边的事件即可。
class Routers {
constructor () {
this.routes = {};
this.currentUrl = '';
this.refresh = this.refresh.bind(this);
window.addEventListener('load', this.refresh, false);
window.addEventListener('hashchange', this.refresh, false);
}
route (path, callback) {
this.routes[path] = callback || function () {};
}
refresh () {
//location.hash = '#/about', currentUrl = '/about';
this.currentUrl = location.hash.slice(1) || '/';
this.routes[this.currentUrl]();
}
}
// 应用一下
window.Router = new Routers();
var content = document.querySelector('body');
function changeColor (color) {
content.style.backgroundColor = color;
}
Router.route('/', function () {
changeColor('red');
});
Router.route('/yellow', function () {
changeColor('yellow');
});
Router.route('/green', function () {
changeColor('green');
});
Hash 模式和 History 模式的区别
原理的区别:
- hash模式是基于锚点,以及onhashchange事件,当路由发生变化时触发该事件,根据当前路由地址找到对应组件重新渲染
- history模式是基于HTML5中的history API
- history.pushState():调用时路径发生变化,监听popstate事件,根据当前路由地址找到对应组件重新渲染,IE10以后才支持,存在兼容性问题
- history.replaceState():替换地址栏中的地址,并且把地址记录到历史记录中
history模式
- 需要服务器支持
- 单页面中,服务器不存在"http://www.someurl/login"这样的地址会返回找不到该页面,返回一个默认的404页面
- 在服务器端应该除了静态资源都返回单页应用的index.html
注:@vue/cli中已经配置好了对路由history模式的支持,所以本地开发的时候路由不会返回404,在生产环境需要上传到node服务器或者nginx服务中才会出现此类问题
模拟Vue.js响应式原理
准备工作
- 数据驱动
- 响应式的核心原理
- 发布订阅模式和观察者模式
数据驱动
1.数据响应式
- 数据模型仅仅是普通的JavaScript对象,而当我们修改数据时,视图会进行更新,避免了繁琐的DOM操作,提高开发效率
2.双向绑定
- 数据改变,视图改变;视图改变,数据也随之改变
- 我们可以使用v-model在表单元素上创建双向数据绑定
3.数据驱动
- 是Vue最独特的特性之一
- 开发过程中仅需要关注数据本身,不需要关心数据是如何渲染到视图
发布订阅模式
// 事件触发器
class EventEmitter {
constructor() {
// 结构为:{事件名:[事件处理函数1,事件处理函数2],事件2:[事件处理函数]}
this.subs = {}
}
// 注册事件
$on(eventType, handler) {
this.subs[eventType] = this.subs[eventType] || []
this.subs[eventType].push(handler)
}
// 触发事件
$emit(eventType) {
if (this.subs[eventType]) {
this.subs[eventType].forEach(handler => {
handler()
})
}
}
}
// 测试
let em = new EventEmitter()
em.$on('click', () => {
console.log(1)
})
em.$on('click', () => {
console.log(2)
})
em.$emit('click')
观察者模式
// 发布者-目标
class Dep {
constructor() {
// 记录所有的订阅者
this.subs = []
}
// 添加订阅者
addSub(sub) {
if (sub && sub.update) {
this.subs.push(sub)
}
}
// 当事件发生的时候,通知所有的订阅者,调用所有订阅者的update方法
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}
// 订阅者-观察者
class Watcher {
// 当事件发生的时候,由发布者来调用update,update内可以去更新视图或者做其他一些操作
update() {
console.log('update')
}
}
// 测试
let dep = new Dep()
let watcher = new Watcher()
dep.addSub(watcher)
dep.notify()
- 原理:Vue 的 data 中的成员实现响应式数据,是在创建 Vue 实例,将传入构造函数的 data 存放在实例的 data中,然后遍历data 的成员,利用 Object.defineProperty 它们转换成 getter/setter 并定义在 Vue 实例上(这是方便于在实例中使用 this.<成员名> 的操作来触发真正的响应式变化)
- 然后调用 observer 实现对 data数据劫持。observer对每个成员通过Object.defineProperty将该成员转化为getter/setter并定义在该成员上,真正的响应式就发生在这里。又因为Vue使用的是观察者模式,因此在data 的成员的 getter 中会收集该成员的所有观察者(收集依赖),在 setter 中发发送通知以触发观察者的 update 方法
Diff 算法的执行过程
- 判断Vnode和oldVnode是否相同,如果是,那么直接return;
- 如果他们都有文本节点并且不相等,那么将更新为Vnode的文本节点。
- 如果oldVnode有子节点而Vnode没有,则删除el的子节点
- 如果oldVnode没有子节点而Vnode有,则将Vnode的子节点真实化之后添加到el
- 如果两者都有子节点,则执行updateChildren函数比较子节点
- 当新旧节点的头部值得对比,进入patchNode方法,同时各自的头部指针+1;
- 当新旧节点的尾部值得对比,进入patchNode方法,同时各自的尾部指针-1;
- 当oldStartVnode,newEndVnode值得对比,说明oldStartVnode已经跑到了后面,那么就将oldStartVnode.el移到oldEndVnode.el的后边。oldStartIdx+1,newEndIdx-1;
- 当oldEndVnode,newStartVnode值得对比,说明oldEndVnode已经跑到了前面,那么就将oldEndVnode.el移到oldStartVnode.el的前边。oldEndIdx-1,newStartIdx+1;
- 当以上4种对比都不成立时,通过newStartVnode.key 看是否能在oldVnode中找到,如果没有则新建节点,如果有则对比新旧节点中相同key的Node,newStartIdx+1
- 当循环结束时
- oldStartIdx > oldEndIdx,可以认为oldVnode对比完毕,当然也有可能 newVnode也刚好对比完,一样归为此类。此时newStartIdx和newEndIdx之间的vnode是新增的,调 用addVnodes,把他们全部插进before的后边。
- newStartIdx > newEndIdx,可以认为newVnode先遍历完,oldVnode还有节点。此时oldStartIdx和oldEndIdx之间的vnode在新的子节点里已经不存在了,调用removeVnodes将它们从dom里删除。