- computed会缓存。vue.use()参数如果是函数直接执行,是对象则调用。会给vue绑定$route、$router。
{
path: 'detail/:id',
name: 'Detail',
props: true, // 开启props会把url参数传给组件(推荐),也可以用$route.params.id获取
component: () => import("../views/Detail.vue"), // 需要才加载
}
- hash路由有#,history没有#需要服务端支持。history原理是h5的history api,history.pushState()IE10才支持,不会发起请求只会改地址并记录到历史记录。
- 由于服务端匹配不到对应页面会返回404,需要设置如果不是静态资源则全都返回index.html。
{path: '*', name: '404', component: '404.vue'},然后new VueRouter({mode: 'history'})。部署到服务器上后刷新页面会404。 - nginx存放的目录不能有中文,
start nginx启动,修改配置需要重启nginx -s reload,停止nginx -s stop。需要将打包的文件放在nginx安装目录里的html文件夹里。nginx默认启用80端口,nginx.conf可以修改server端口。修改后记得重启。
location / {
root html; // 根文件夹
index index.html index.html; // 默认访问页面
try_files $uri $uri/ /index.html; // 尝试去访问输入的url,如果有直接返回,没有访问index.html
}
- hash模式监听hashchange。history监听popstate,当调用push和replace不会触发,当调用back和forward或者点击前进后退的时候能触发。
- Vue的强大在于它的插件,可以通过Vue.use()传入对象就执行对象的install方法。
所以Vue-router是个类有该方法,而且能new VueRouter所以他的构造函数能传入一些规则。有以下字段options、data、routeMap,和方法Constructor(Options)、_install(Vue)、init()、initEvent()、createRouteMap()、initComponents(Vue) - Vue的构建版本:运行时版不支持template模板,需要打包的时候提前编译。完整版包含运行时和编译器,体积比运行时版本大10K左右,程序运行的时候把template转换成render函数(创建虚拟Dom)。vue-cli默认时创建用的运行时。
- 新增vue.config.js增加
module exports = {runtimeCompiler: true},默认是false不带编译器,改成true则当编译器不用自己写render。 - 数据驱动:数据响应式(改数据dom自动刷新)、双向绑定(视图改变数据改变,反之亦然)。vue2响应式原理:当访问和设置的时候,用
Object.defineProperty(vm, key, {})改set/get。vue3响应式原理:let vm = new Proxy(data, {get(targetObj, key), set(targetObj, key, newValue)})es6新增,ie不支持。proxy可以监听整个对象的属性,不需要像defineProperty要去循环遍历。 - 发布订阅模式:将事情发布给事件中心触发,订阅者收到做相应处理;
vm.$on('eventName', ()=>{}), vm.$emit('eventName', () =>{})。观察者模式:直接存储所有观察者(订阅者),当事件发生的时候调用所有观察者的update方法。前者用事件中心隔离了两者,后者没有事件中心相互依赖。 - Vue处理->Observer数据劫持(监听变化)->Dep发布者->调用Watcher观察者里的update(渲染视图);Vue->Compiler解析指令和插值表达式->Watcher处理。
- Vue类要做的事是负责接受参数选项,把data中的属性注入到Vue实例转换成setter和getter,调用observer监听data所有属性变化,调用compiler解析指令和差值表达式。
- compiler负责编译模板,解析指令/差值表达式;负责页面的首次渲染;当数据变化后重新渲染视图。
compile(el) {
let childNodes = el.childNodes // children是子元素,childNodes是子节点包括文本
Array.from(childNodes).forEach(node => { // 类数组对象需要转换为数组才能调用数组方法
if (this.isTextNode(node)) {
this.compileText(node)
} else if (this.isElementNode(node)) {
this.compileElement(node)
}
// 如果还有子节点,要递归遍历
if (node.childNodes && node.childNodes.length) {
this.compile(node)
}
})
}
compileText(node) {
console.dir(node) // 将节点以对象形式展示
// 处理 {{ msg }}
let reg = /\{\{(.+?)\}\}/
let value = node.textContent
if (reg.test(value)) {
let key = RegExp.$1.trim()
node.textContent = value.replace(reg, this.vm[key])
}
}
- 数据改变修改视图:Dep类的getter会收集依赖观察者,即收集所有依赖该属性的地方。setter通知依赖的地方变更触发观察者update。数据变更的时候要变更视图,所以要在视图变更的地方即操作dom的地方去创建watcher。
- 真实dom的对象特别多,可以创建一个对象来模拟某些必要属性。虚拟dom可以维护程序的状态,跟踪上次状态,对比状态差异只更新发生变化的dom。而以前是直接整个dom做替换。作用是:(1)维护视图和状态的关系(2)复杂视图下可以提升渲染性能,简单视图需要转化为虚拟dom对象可能更耗时(3)跨平台,因为虚拟dom是对象而不是dom,所以方便在不同平台上做处理(如小程序、原生应用,服务端渲染ssr、浏览器渲染)
- Snabbdom是虚拟dom开源库,代码只有200行易扩展易懂。
npm install parcel-bundler -D,然后配置scripts,新建src/basicusage.js导入init和h(注意node12/webpack5之后才支持exports所以路径引用需要自行修改为真实路径)
"scripts": {
"dev": "parcel index.html --open", // 自动打开浏览器
"build": "parcel build index.html"
}
const {h} = require('snabbdom/build/package/h')
const {init} = require('snabbdom/build/package/init')
const patch = init([])
// h函数和vue中的一样,第一个参数是标签+选择器(可用于更改类名),第二个参数是文本(字符串)/子元素(数组)
let vnode = h('div#container.cls', 'hello world')
let app = document.querySelector('#app')
// 第一个参数旧的vnode或者dom元素,第二个参数是新的vnode,返回一个新的vnode
let oldVnode = patch(app, vnode)
vnode = h('div#container.xxx', 'Hello Snabbdom')
/*
vnode = h('div#container', [
h('h1', 'hello world'),
h('p', 'hello p')
])
*/
patch(oldVnode, vnode) // 对比差异,更新到真实dom上
patch(oldVnode, h('!')) // 清除元素,!是空的注释节点
- snabbdom的核心库并不能处理dom的属性/样式/事件等,可以通过注册Snabbdom默认提供的模块来实现。模块可以用于扩展snabbdom的功能,是通过注册全局钩子函数来实现的。a.导入模块;b.init注册模块;c. h函数第二个参数(对象)处使用模块。
import {init} from 'snabbdom/build/init'
import {h} from 'snabbdom/build/h'
import {styleModule} from 'snabbdom/build/modules/style'
import {eventListenersModule} from 'snabbdom/build/modules/eventlisteners'
const patch = init([
styleModule,
eventListenersModule
])
let vnode = h('div', [
h('h1', {style: {backgroundColor: 'red'}}, 'hello world'),
h('p', {on: {click: eventHandle}}, 'click me')
])
function eventHandle() {
alert('你很棒')
}
const app = document.querySelector('#app')
patch(app, vnode)
- 看源码要带着问题去看,了解主线再看细节。Snabbdom的核心:
- init()设置模块,创建patch()函数
- 使用h()函数创建js对象(VNode)描述真实DOM
- patch()比较新旧两个VNode
- 把变化的内容更新到真实DOM树
- js没有重载,后面声明的会覆盖前面的函数。ts有重载,参数个数或者类型不同(即使名字相同)则认为两个不同函数。snabbdom的h函数定义了四个h函数和一个真正的h函数,真正的h函数通过判断参数个数和类型去决定调用前面四个哪个h函数。
- 按F12或者ctrl+鼠标左键可以跳转到定义位置;alt+←可以后退到上一页,alt+→可以前进。
-
patch(oldVnode, newVnode)把新旧节点中变化的内容渲染到真实DOM,最后返回新节点作为下一次处理的旧节点。
- 先判断新旧VNode是否相同节点(节点的key和sel选择器是否相同)。
- 如果不是相同节点,删除之前的内容,重新渲染。
- 如果是相同节点,再判断新VNode是否有text,如果有并且和oldVNode的text不同,直接更新文本内容。
- 如果新的VNode有children,判断子节点是否有变化。
-
init(modules, api)第二个参数可以指定VNode转换为对应平台的api,默认是转成DOM。第一个是钩子函数数组。返回一个patch函数,主要是先缓存了前两个配置项,这样后续用patch不用老是传。 - patch先看传入是否VNode,不是先生成VNode。ts变量加!表示一定有值,
obj.data!.hook!.fn()表示有值才调用后面方法。obj?.fn如果有obj返回obj.fn否则返回undefined。控制台点击行号打断点然后F5刷新运行,F11一句句运行,F10跳过某个方法。 - diff算法,虚拟dom不触发浏览器重绘重排,直接对比js描述的节点信息。最麻烦的是每个节点对比,snabbdom做了优化只对比树的同级节点,因为dom操作很少跨级别操作节点。
- 先比较oldStart和newStart,直到不相同;比较oldEnd和newEnd;再比较oldStart和newEnd;再比较oldEnd和newStart;都不同相同则遍历。当老节点或新节点的所有子节点全部遍历完(oldStart>oldEnd或者newStart>newEnd),则循环结束。
- 新旧开始节点如果是sameVnode(key和sel相同),会重用旧的dom节点只是修改内容,调用patchVnode()对比和更新节点。把旧开始和新开始索引往后移动。
- 旧开始和新结束如果相同,调用patchVnode()对比和更新,把oldStart对应的dom元素移动到oldEnd之后(因为旧开始和新结束是相同节点),并且oldStart++,newEnd--。
- 旧结束和新开始,调用patchVnode()对比和更新,把oldEnd对应的dom元素移动到oldStart之前,并且oldEnd--,newStart++。
- 如果都不相同,则遍历旧节点中是否有newStart相同节点,有的话对比更新,并把对应节点移到oldStart之前,清空旧节点索引;如果没有,则直接在oldStart之前创建新的节点。
全部遍历完之后若老节点有剩余则删除,若新节点有剩余则把新节点加入到老节点中。
- 不设置key,那么所有节点key都是undefined是相同的,会直接替换节点内容(sel也相同的前提下)。设置了key那么只有key才会对比和移动元素。如果不设置key可能会导致渲染错误,因为只是替换了节点的内容,但绑在节点上的属性数据都没变,如果checkbox的话那旧节点选中也会导致新节点选中。如果有key则认为不是相同节点会重新创建。
// 为啥watcher里不直接addSub而要通过Dep,compileText里每次都新建watcher吗,差值表达式如何支持表达式