天天看点

手写 Vue Router、手写响应式实现、虚拟 DOM 和 Diff 算法vue原理剖析

vue原理剖析

基础结构

模板 :界面展示代码

逻辑业务功能:

美化布局:

生命周期

手写 Vue Router、手写响应式实现、虚拟 DOM 和 Diff 算法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里删除。