[fed-task-03-01]手写 Vue Router、手写响应式、虚拟 DOM 和 Diff 算法

文章内容输出来源:拉勾教育大前端高薪训练营
文章内容包括:模块作业、学习笔记

简答题

1、当我们点击按钮的时候动态给 data 增加的成员是否是响应式数据,如果不是的话,如果把新增成员设置成响应式数据,它的内部原理是什么。

let vm = new Vue({
    el: '#el'
    data: {
        o: 'object',
        dog: {}
    },
    method: {
        clickHandler () {
            // 该 name 属性是否是响应式的
            this.dog.name = 'Trump'
        }
    }
})

答:题目中通过 this.dog.name = 'Trump' 给 dog 增加的成员 不是响应式数据。

在 Vue 中可以通过 Vue.set( target, propertyName/index, value ) 或者 this.$set( target, propertyName/index, value ) 的方式为 target 对象动态添加响应式数据。Vue 2.x 中的原理是:类似于调用 defineReactive(obj, key, val) 方法,利用 Object.defineProperty 的 getter 和 setter 实现响应式数据。

2、请简述 Diff 算法的执行过程

答:

DOM操作是很耗性能的,因此需要尽量减少DOM操作。找出本次DOM必须更新的节点来更新,其他的不更新,这个“找出”的过程,就需要diff算法。

diff算法主要执行过程:

  • patch(container, vnode) ,首次渲染,将 container 转为 vnode,并对比新旧 VNode 是否相同节点然后更新DOM
  • patch(vnode, newVnode) ,数据改变二次渲染,对比新旧 VNode 是否相同节点然后更新DOM
  • createElm(vnode, insertedVnodeQueue),先执行用户的 init 钩子函数,然后把 vnode 转换成真实 DOM(此时没有渲染到页面),最后返回新创建的 DOM
  • updateChildren(elm, oldCh, ch, insertedVnodeQueue), 如果 VNode 有子节点,并且与旧VNode子节点不相同则执行 updateChildren(),比较子节点的差异并更新到DOM

编程题

项目地址 https://github.com/luxiancan/...

1、模拟 VueRouter 的 hash 模式的实现,实现思路和 History 模式类似,把 URL 中的 # 后面的内容作为路由的地址,可以通过 hashchange 事件监听路由地址的变化。

答:参考项目 ./vue-router-lxc ,文件路径: ./vue-router-lxc/src/vuerouter_hash/index.js

2、在模拟 Vue.js 响应式源码的基础上实现 v-html 指令,以及 v-on 指令。

答:参考项目 ./mini-vue-lxc ,文件路径: ./mini-vue-lxc/js/compiler.js

部分代码

    // 处理 v-html 指令
    htmlUpdater (node, value, key) {
        node.innerHTML = value
        new Watcher(this.vm, key, newValue => {
            node.innerHTML = newValue
        })
    }
    // 处理 v-on 指令
    onUpdater (node, value, key, eventType) {
        node.addEventListener(eventType, value)
        new Watcher(this.vm, key, newValue => {
            node.removeEventListener(eventType, value)
            node.addEventListener(eventType, newValue)
        })
    }

3、参考 Snabbdom 提供的电影列表的示例,利用 Snabbdom 实现类似的效果

答:参考项目 ./snabbdom-demo

项目文件说明

  • notes : 笔记
  • mini-vue-lxc : 小型 vue 框架,实现了响应式、插值表达式、部分指令等功能
  • vue-router-lxc : 基于 vue-cli 模拟实现了 vue-router 的两种模式 history 和 hash
  • snabbdom-demo : 使用 snabbdom 开发的小 demo,用于学习和巩固 snabbdom 的基本用法
    • *

学习笔记

Vue.js 基础回顾

基础结构

  • 使用 new Vue({ el: '#app', data: {} })
  • 使用 new Vue({ data: {}, render(h) {} }).$mount('#app')

生命周期

  • new Vue() 新建 Vue 实例
  • beforeCreate 初始化事件 & 生命周期
  • created 初始化注入 & 校验
  • 是否指定 “el” 选项和 “template” 选项
  • beforeMount
  • mounted 创建 vm.$el并用其替换 el
  • 当 data 被修改时,触发 beforeUpdate
  • 虚拟 DOM 重新渲染并应用更新,触发 updated
  • 当调用 vm.$destroy() 函数时,触发 beforeDestroy
  • destroyed 解除绑定,销毁子组件以及事件监听器
  • 销毁完毕

Vue 语法和概念

  • 插值表达式
  • 指令
  • 计算属性和侦听器
  • Class 和 Style
  • 条件渲染/列表渲染
  • 表单输入绑定
  • 组件
  • 插槽
  • 插件
  • 混入 mixin
  • 深入响应式原理
  • 不同构建版本的 Vue

Vue-Router 原理实现

Hash 模式和 History 模式的区别

  • 不管哪种方式,都是客户端路由的实现方式,当路径发生变化不会向服务器发送请求
  • 表现形式的区别

  • 原理的区别

    • Hash 模式是基于锚点,以及 onhashchange 事件
    • History 模式是基于 HTML5 中的 History API,history.pushState() IE 10 以后才支持, history.replaceState

History 模式的使用

  • History 需要服务器的支持
  • 单页应用中,服务端不存在 http://www/testurl.com/login 这样的地址会返回找不到该页面
  • 在服务端应该除了静态资源外都返回单页应用的 index.html

History 模式 - Node.js

/* app.js */
const path = require('path')
// 导入处理 history 模式的模块
const history = require('connect-history-api-fallback')
// 导入 express
const express = require('express')

const app = express()
// 注册处理 history 模式的中间件
app.use(history())
// 处理静态资源的中间件,网站根目录 ../web
app.use(express.static(path.join(__dirname, '../web')))

// 开启服务器,端口是 3000
app.listen(3000, () => {
  console.log('服务器开启,端口:3000')
})

History 模式 - nginx

  • 从官网下载 nginx 压缩包 http://nginx.org/en/download....
  • 把压缩包解压到 c 盘根目录,c:nginx-1.18.0 文件夹
  • 打开命令行,切换到目录 c:nginx-1.18.0

nginx 相关命令

# 启动
start nginx
# 重启
nginx -s reload
# 停止
nginx -s stop

nginx.conf 文件

server {
  # ...
  location / {
    root   html;
    index  index.html index.htm;
    try_files $uri $uri/ /index.html;
  }
}

VueRouter 实现原理

Hash 模式

  • URL 中 # 后面的内容作为路径地址
  • 监听 hashchange 事件
  • 根据当前路由地址找到对应组件重新渲染

History 模式

  • 通过 history.pushState() 方法改变地址栏
  • 监听 popstate 事件
  • 根据当前路由地址找到对应组件重新渲染

Vue 的构建版本

  • 运行时版:不支持 template 模板,需要打包的时候提前编译
  • 完整版:包含运行时和编译器,体积比运行时版大 10K 左右,程序运行的时候把模板转换成 render 函数

模拟 Vue.js 响应式原理

数据驱动

数据响应式、双向绑定、数据驱动

数据响应式

  • 数据模型仅仅是普通的 JS 对象,而当我们修改数据时,视图会进行更新,避免了繁琐的DOM操作,提高开发效率

双向绑定

  • 数据改变,视图改变;试图改变,数据也随之改变
  • 我们可以使用 v-model 在表单元素上创建双向数据绑定

数据驱动

  • 数据驱动是 vue 最独特的特性之一
  • 开发过程中仅需要关注数据本身,不需要关心数据是如何渲染到视图的

响应式的核心原理

Vue 2.x

  • Object.defineProperty
  • 浏览器兼容 IE8 以上(不兼容 IE8)

Vue 3.x

  • Proxy
  • 直接监听对象,而非属性
  • ES6 中新增,IE 不支持,性能由浏览器优化

发布订阅模式和观察者模式

发布/订阅模式

  • 订阅者
  • 发布者
  • 信号中心

我们假定,存在一个“信号中心”,某个人物执行完成,就向信号中心“发布”(publish)一个信号,其他任务可以向信号中心“订阅”(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做“发布/订阅模式”(publish-subscribe pattern)

Vue 的自定义事件

let vm = new Vue()

vm.$on('dataChange', () => {
  consloe.log('dataChange1')
})

vm.$on('dataChange', () => {
  consloe.log('dataChange2')
})

vm.$emit('dataChange')

兄弟组件通信过程

// eventBus.js
// 事件中心
let eventHub = new Vue()

// ComponentA.vue
// 发布者
addTodo: function () {
  // 发布消息(事件)
  eventHub.$emit('add-todo', { text: this.newTodoText })
  this.newTodoText = ''
}

// ComponentB.vue
// 订阅者
created: function () {
  // 订阅消息(事件)
  eventHub.$on('add-todo', this.addTodo)
}

模拟 Vue 自定义事件的实现

class EventEmitter {
  constructor () {
      // { 'click': [fn1, fn2], 'change': [fn] }
      this.subs = Object.create(null)
  }
  $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()
          })
      }
  }
}

观察者模式

  • 观察者(订阅者) -- Watcher

    • update(): 当事件发生时,具体要做的事情
  • 目标(发布者) -- Dep

    • subs 数组: 存储所有的观察者
    • addSub(): 添加观察者
    • notify(): 当事件发生时,调用所有观察者的 update() 方法
  • 没有事件中心

总结

  • 观察者模式是由具体目标调度,比如当事件触发,Dep 就会去调用观察者的方法,所以观察者模式的订阅者与发布者之间是存在依赖的
  • 发布/订阅模式由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在

模拟Vue响应式原理

整体分析

  • Vue 基本结构
  • 打印 Vue 实例观察
  • 整体结构
  • 把 data 中的成员注入到 Vue 实例,并且把 data 成员转成 getter/setter
  • Observer: 能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知 Dep

Vue

  • 负责接收初始化的参数
  • 负责把 data 中的属性注入到 Vue 实例,转换成 getter/setter
  • 负责调用 observer 监听 data 中所有属性的变化
  • 负责调用 compiler 解析指令/插值表达式

Observer

  • 负责把 data 选项中的属性转换成响应式数据
  • data 中的某个属性也是对象,把该属性转换成响应式数据
  • 数据变化发送通知

Compiler

  • 负责编译模板,解析指令/插值表达式
  • 负责页面的首次渲染
  • 当数据变化后重新渲染视图

Dep (Dependency)

  • 收集依赖,添加观察者(watcher)
  • 通知所有观察者

Watcher

  • 当数据变化触发依赖,dep 通知所有的 Watcher 实例更新视图
  • 自身实例化的时候往 dep 对象中添加自己

总结

问题

  • 给 data 中某个属性重新赋值成对象,是否是响应式的? --- 是
  • 给 Vue 实例新增一个成员是否是响应式的? --- 否

Virtual DOM 的实现原理

什么是 Virtual DOM

  • Virtual DOM(虚拟 DOM),是由普通的 JS 对象来描述 DOM 对象,因为不是真实的 DOM 对象,所以叫 Virtual DOM
  • 可以使用 Virtual DOM 来描述真实的 DOM

为什么使用 Virtual DOM

  • 手动操作 DOM 比较麻烦,还需要考虑浏览器兼容问题,虽然 jQuery 等库简化了 DOM 操作,但是随着项目的复杂 DOM 操作复杂提升
  • 为了简化 DOM 的复杂操作于是出现了各种 MVVM 框架,MVVM 框架解决了视图和状态的同步问题
  • 为了简化视图的操作我们可以使用模板引擎,但是模板引擎没有解决跟踪状态变化的问题,于是 Virtual DOM 出现了
  • Virtual DOM 的好处是当状态改变时不需要立即更新 DOM,只需要创建一个虚拟树来描述 DOM,Virtual DOM 内部将弄清楚如何有效(diff)的更新 DOM

虚拟 DOM 的作用

  • 维护视图和状态的关系
  • 复杂视图情况下提升渲染性能
  • 除了渲染 DOM 外,还可以实现 SSR(Nuxt.js/Next.js)、原生应用(Weex/React Native)、小程序(mpvue/uni-app) 等
  • 并不是所有情况使用虚拟DOM都能提升性能,只有在视图比较复杂的情况下,使用虚拟DOM才会提高渲染性能

你可能感兴趣的:(vue.js,vue-router)