文章内容输出来源:拉勾教育大前端高薪训练营
vue-router实现
- 基本使用
- 创建路由组件
- vue.use()注册vue-router
- 注册router对象
- 设置占位router-view
- 创建链接 router-link
- 动态路由
- /detail/:id
- [获取]$route.params.id
- [获取]props:true — 推荐
- ()=> import() 按需引入
- /detail/:id
- 嵌套路由
- children
- 编程式导航
- $router.push()
- $router.replace()
- $router.go()
- hash和history模式的区别
- hash 带#
- 基于锚点以及onhashchange事件
- #后面的内容作为路径地址
- 根据当前路由地址找到对应组件重新渲染
- history
- 基于HTML5 API
- history.pushState()改变地址栏 IE10以后才支持
- history.replaceState()
- 监听popstate事件
- 根据当前路由地址找到对应组件重新渲染
- mode:‘history’
- 使用需要服务器端的支持
- 单页应用中,服务端不存在http://www.testurl.com/login这样的地址会返回找不到该页面
- 在服务端应该除了静态资源外都返回单页应用的index.html
- node中配置(express)
const history = require('connect-history-api-fall-back')
app.use(history())
- nginx中配置
- nginx配置步骤
- 下载nginx、解压(目录不能有中文)
- 命令行
- start nginx 启动
- nginx -s reload 重启
- nginx -s stop 停止
- html 文件夹中(打包后的资源)
- conf/nginx.conf
try_files $uri $uri/ /index.html
server{ location / { try_files $uri $uri/ /index.html } }
- nginx配置步骤
- 基于HTML5 API
- hash 带#
- vue-router实现原理
- 使用的vue前置知识
- 插件
- 混入
- Vue.observable()
- 插槽
- render函数
- 运行时和完整版的vue
- 模拟实现
- 分析
- 类图
- 类图
- install方法实现
let _Vue = null; export default class VueRouter{ static install(Vue){ // 1 判断当前插件是否已安装 if(VueRouter.install.installed) return; VueRouter.install.installed = true; // 2 把Vue构造函数记录到全局变量 _Vue = Vue // 3 把创建vue实例时候传入的router对象注入到Vue实例上 // 混入 _Vue.mixin({ beforeCreate(){ if(this.$options.router){ // 如果是组件的话 不存在router _Vue.prototype.$router = this.$options.router } } }) } }
- 构造函数
constructor(options){ this.options = options; this.routerMap = {};// key 路由地址 value 路由组件 this.data = _Vue.observable({ current:'/' }) }
- createRouteMap
createRouteMap(){ //把构造函数中传过来的routes转换为key-value形式,存放在routerMap中 this.options.routes.forEach(route=>{ this.routerMap[route.path] = route.component }) }
- initComponents(router-link)
initComponents(Vue){ Vue.component('router-link',{ props:{ to:String }, template:`<a :href='to'><slot></slot></a>` }) }
-
完整版Vue
- vue的构建版本
- 运行时版:不支持template模板,需要打包的时候提前编译
- 完整版 :包含运行时和编译器,体积比运行时版大10K左右,程序运行的时候把模板转换成render函数
- vue.config.js(解决1)
module.exports = { runtimeCompiler:true }
- render(解决2)
Vue.component('router-link', { props: { to: String }, // template: '<a :href=\'to\'><slot></slot></a>' render (h) { return h('a', { attrs: { href: this.to } }, [this.$slots.default]) } })
- vue的构建版本
-
router-view
Vue.component('router-view', { render (h) { const component = _this.routerMap[_this.data.current] return h(component) } }) // router-link配合点击事件 Vue.component('router-link', { props: { to: String }, // template: '<a :href=\'to\'><slot></slot></a>' render (h) { return h('a', { attrs: { href: this.to }, on: { click: this.clickHandler } }, [this.$slots.default]) }, methods: { clickHandler (e) { history.pushState({}, '', this.to) this.$router.data.current = this.to e.preventDefault() } } })
- initEvent
initEvent () { window.addEventListener('popstate', () => { this.data.current = window.location.pathname }) }
- 分析
- 使用的vue前置知识
模拟vue实现
-
数据驱动
- 数据响应式
- 数据模型仅仅是普通的JS对象,而当我们修改数据时,视图会进行更新,避免频繁的DOM操作,提高开发效率
- 双向绑定
- 数据改变,视图改变;视图改变,数据也随之改变
- 使用v-model在表单元素上创建双向数据绑定
- 数据驱动是vue最独特的特性之一
- 开发过程中仅需要关注数据本身,不需要关心数据是如何渲染到试图的
- 数据响应式
-
vue2响应式原理
- Object.defineProperty
<body> <div id="app"> hello </div> <script> let data = { msg : 'hello' } let vm = {}; Object.defineProperty(vm,'msg',{ enumerable:true, configurable:true, get(){ console.log('get'); return data.msg }, set(newVal){ console.log('set',newVal); if(newVal ===data.msg){ return } data.msg = newVal document.querySelector('#app').textContent = data.msg; } }) vm.msg = 'niahdas'; console.log(vm.msg); </script> </body>
- Object.defineProperty
-
vue3响应式原理
- proxy 直接监听对象
- 由浏览器进行性能优化
-
发布订阅模式
- 订阅者
- 发布者
- 信号中心
-
观察者模式
- 观察者(订阅者) - Watcher
- update() 当事件发生时,具体要做的事情
- 目标(发布者) - Dep
- subs数组 存储所有的观察者
- addSub() 添加观察者
- notify() 当事件发生,调用所有观察者的update()方法
- 没有事件(信号)中心
- 观察者(订阅者) - Watcher
-
观察者VS发布订阅
- 观察者模式:由具体目标调度,比如当事件触发、Dep就会去调用观察的方法,所以观察者模式的订阅者和发布者之间存在依赖的。
- 发布订阅:由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在。
-
模拟vue实现(学习版)
- 结构
- Vue类
-
功能
- 负责接收初始化参数
- 负责把Data中的属性注入到Vue实例,转换成getter/setter
- 负责调用observer监听data中所有属性的变化
- 负责调用compiler解析指令/插值表达式
-
结构
- $options
- $el
- $data
- _proxyData()
class Vue{ constructor(options){ // 1 通过属性保存 选项的数据 this.$options = options || {} this.$data = options.data|| {} this.$el = typeof options.el === 'string'?document.querySelector(options.el):options.el // 2 把data中的数据转换成getter和setter,注入到vue实例中 this._proxyData(this.$data) // 3 调用observer对象,监听数据变化 new Observer(this.$data) // 4 调用compuler对象,解析指令和差值表达式 new Compiler(this) } _proxyData(data){ // 遍历data中所有的属性 Object.keys(data).forEach(key=>{ // 把data的属性注入到vue实例中 Object.defineProperty(this,key,{ enumerable:true, configurable:true, get(){ return data[key] }, set(newValue){ if(newValue === data[key]){ return } data[key] = newValue } }) }) } }
-
- Observer类
-
功能
- 负责把data选择中的数据属性转换成响应式数据
- data中的某个属性也是对象,把该属性转换成响应式数据
- 数据变化发送通知
-
结构
- walk(data)
- defineReactive(data,key,value)
class Observer{ constructor(data){ this.walk(data) } walk(data){ // 判断data是否是对象 if(!data||typeof data!=='object') return // 遍历data对象所有属性 Object.keys(data).forEach(key=>{ this.defineReactive(data,key,data[key]) }) } // 传第三个参数val 不使用obj[key] 原因:会发生死递归 defineReactive(obj,key,val){ let _this = this; let dep = new Dep(); this.walk(val);//如果val是对象 会将val内部的数据转为响应式 Object.defineProperty(obj,key,{ enumerable:true, configurable:true, get(){ // 收集依赖 Dep.target && dep.addSub(Dep.target) return val }, set(newVal){ if(newVal === val) return; val = newVal; _this.walk(newVal) // 发送通知 dep.notify() } }) } }
-
- Compiler类
-
功能
- 负责编译模板,解析指令/插值表达式
- 负责页面的首次渲染
- 当数据变化后重新渲染视图
-
结构
- el
- vm
- compile(el)
- compileElement(node)
- compileText(node)
- isDirective(attrName)
- isTextNode(node)
- isElementNode(node)
class Compiler{ constructor(vm){ this.el = vm.$el; this.vm = vm; this.compile(this.el) } // 编译模块,处理文本节点和元素节点 compile(el){ let childNodes = el.childNodes; Array.from(childNodes).forEach(node=>{ if(this.isTextNode(node)){ this.compileText(node) }else if(this.isElementNode(node)){ this.compileElement(node) } // 判断node是否有子节点 if(node.childNodes&&node.childNodes.length){ this.compile(node) } }) } // 处理元素节点 compileElement(node){ // 遍历属性节点 Array.from(node.attributes).forEach(attr=>{ let attrName = attr.name; if(this.isDirective(attrName)){ attrName = attrName.substr(2) let key = attr.value; if (attrName.indexOf(':')!==-1) { let eventType = attrName.split(':')[1] this.handleEvent(this,node,eventType,key) } this.update(node, key, attrName) } }) } update(node,key, attrName){ let updateFn = this[attrName+'Updater'] updateFn && updateFn.call(this,node,this.vm[key],key) } handleEvent(vm,node,eventType,eventName){ console.log(vm,node,eventType); node.addEventListener(eventType,()=>{ vm.vm.$options.methods[eventName]() }) } htmlUpdater(node,value,key){ node.innerHTML = value; new Watcher(this.vm, key, (newValue)=>{ node.innerHTML = newValue }) } onUpdater(node,value,key){ console.log(node,value,key); } textUpdater(node,value,key){ node.textContent = value; new Watcher(this.vm, key, (newValue)=>{ node.textContent = newValue }) } modelUpdater(node,value,key){ node.value = value; new Watcher(this.vm, key, (newValue)=>{ node.value = newValue }) // 双向绑定 node.addEventListener('input',()=>{ this.vm[key] = node.value }) } // 处理文本节点 compileText(node){ // console.log(node); let reg = /\{\{(.+?)\}\}/ let value = node.textContent if(reg.test(value)){ let key = RegExp.$1.trim(); node.textContent = value.replace(reg,this.vm[key]) new Watcher(this.vm, key, (newValue)=>{ node.textContent = newValue }) } } // 判断元素属性是否是指令 isDirective(attrName){ return attrName.startsWith('v-') } // 判断节点是否是文本节点 isTextNode(node){ return node.nodeType === 3 } // 判断节点是否是元素节点 isElementNode(node){ return node.nodeType === 1 } }
-
- Dep
-
功能
- 收集依赖,添加观察者
- 通过所有的观察者
-
结构
- subs
- addSub(sub)
- notify()
class Dep{ constructor(){ this.subs = [] } addSub(sub){ if(sub&&sub.update){ this.subs.push(sub) } } notify(){ this.subs.forEach(sub=>{ sub.update() }) } }
-
- Wather
- 功能
- 当数据变化触发依赖,dep通知所有的Watcher实例更新视图
- 自身实例化的时候往dep对象中添加自己
- 结构
- vm
- key
- cb
- oldValue
- update()
class Watcher{ constructor(vm, key, cb){ this.vm = vm; this.key = key//data中属性的名称 this.cb = cb// 回调函数负责更新视图 // 把watcher对象记录到Dep类的静态属性target上 Dep.target = this; // 触发get方法 在get方法中调用addSub this.oldValue = vm[key] Dep.target = null; } update(){ let newValue = this.vm[this.key] if(this.oldValue === newValue){ return } this.cb(newValue) } }
- 功能
- 结构
VDOM的实现原理
-
VDOM
- 用JS对象描述DOM对象
- 为什么使用VDOM?
- 手动操作DOM麻烦,要考虑浏览器兼容
- 为了简化DOM的复杂操作出现了MVVM框架,MVVM框架解决了视图和状态的同步问题
- 为了简化视图的操作可以使用模板引擎,但模板引擎没有解决跟踪状态变化的问题,于是有了VDOM
- VDOM的好处是当状态改变时不需要立即更新DOM,只需要创建一个虚拟树来描述DOM。VDOM内部会弄清楚如何有效(diff)的更新DOM
- VDOM可以维护程序的状态,跟踪上一次的状态
- 通过比较前后两次状态的差异更新真实DOM
- VDOM作用
- 维护视图和状态的关系
- 复杂视图情况下提升渲染性能
- 除了渲染DOM以外,还可以实现SSR(nuxt/next)、原生应用(Weex/RN)、小程序(uni-app)等
-
VDOM库 - Snabbdom/virtual-dom
- Snabbdom
- Vue2 内部虚拟DOM就是改造后的Snabbdom
- 约200行
- 通过模块可扩展
- 源码使用TS
- 最快的VDOM之一
- Snabbdom使用
-
使用parcel打包
- npm init -y
- npm i parcel-bundler -D
- package.json
"scripts": { "dev":"npx parcel index.html --open", "build":"npm parcel build index.html" },
- 导入snabbdom
npm i snabbdom -D
- init() 高阶函数,返回patch()
- h() 返回虚拟节点
- thunk() 一种优化策略,可以在处理不可变数据时使用
snabbdom基本使用
// import snabbdom from 'snabbdom' //错误引入 // const snabbdom = require('snabbdom') // console.log(snabbdom); // import { h, thunk, init } from 'snabbdom' import { h, init } from 'snabbdom' // hello world let patch = init([]) // 参数1 标签+选择器 // 参数2 如果是字符串的话 就是标签中的内容 let vnode = h('div#container.cls',{ hook:{ init(vnode){ console.log(vnode.elm); }, create(emptyVnode,vnode){ console.log(vnode.elm); } } },'hello world') let app = document.querySelector('#app') // 参数1 可以是DOM元素,内部会把DOM元素转换为VNode 参数2 VNode let oldVnode = patch(app,vnode) vnode = h('div','hello snabbdom') patch(oldVnode,vnode)
- snabbdom模块
- attributes
- 设置DOM元素的属性,使用setAttribute()
- 处理布尔类型的属性
- props
- 和attributes模块类似,设置DOM属性 element[attr] = value
- 不处理布尔类型的属性
- class
- 切换类样式
- 主要:给元素设置类样式是通过sel选择器
- dataset
- 设置data-*的自定义样式
- eventlisteners
- 注册和移除事件
- style
- 设置行内样式,支持动画
- delayed/remove/destroy
- attributes
- 模块的使用
- 导入需要的模块
- init()中注册模块
- 使用h()创建VNode的时候,可以把第二个参数设置为对象,其他参数往后移
import {init, h} from 'snabbdom' import style from 'snabbdom/modules/style' import eventlisteners from 'snabbdom/modules/eventlisteners' let patch = init([ style, eventlisteners ]) let vnode = h('div',{ style:{ backgroundColor:'#f60' }, on:{ click:eventHandler } },[h('h1','this is h1'),h('p','this is a p')]) function eventHandler(){ console.log('click event'); } let app = document.querySelector('#app') patch(app,vnode)
- snabbdom模块核心(源码)
- 使用h()创建JS对象(VNode)描述真实DOM
- vnode() 返回JS对象 描述VDOM
- init() 设置模块,创建patch()
- patch(oldVnode,newVnode)比较新旧两个VNode ---- snabbdom.ts
- 把新节点中变化的内容渲染到真实的DOM。最后返回新节点作为下一次处理的旧节点
- 对比新旧VNode是否相同节点(节点的key和sel相同)
- 如果不是相同节点,删除之前的内容,重新渲染
- 如果是相同的节点,再判断新的VNode是否有text,如果有并且和oldVNode的text不同,直接更新文本内容
- 如果新的VNode有children,判断子节点是否有变化,判断子节点的过程使用的就是diff算法
- diff过程只进行同层级比较
- 把变化的内容更新到真实DOM树上
- 源码解析
-
h()函数 — src/h.ts
- 用来创建VNode(调用vnode函数返回虚拟节点)
-
patch()
- createElm() 把VDOM转为DOM,并触发init/create钩子函数
- 1、执行init函数
- 2、把 VNode转为真实DOM对象(没有渲染到页面)
- 3、 返回新创建的DOM
- removeVnodes() 移除老节点
- addVnodes()
- patchVnode() 对比新旧节点 更新差异
- updateChildren() diff算法核心,对比新旧节点的children,更新DOM
- createElm() 把VDOM转为DOM,并触发init/create钩子函数
-
init()
-
-
- Snabbdom