Glittering's blog Glittering's blog
Home
  • 学习手册

    • 《JavaScript教程》
    • 《ES6 教程》
    • 《TypeScript 从零实现 axios》
    • 《Git》
    • TypeScript
  • 技术文档
  • 算法
  • 工作总结
  • 实用技巧
  • collect
About
  • Classification
  • Label
GitHub (opens new window)

Glitz Ma

前端开发工程师
Home
  • 学习手册

    • 《JavaScript教程》
    • 《ES6 教程》
    • 《TypeScript 从零实现 axios》
    • 《Git》
    • TypeScript
  • 技术文档
  • 算法
  • 工作总结
  • 实用技巧
  • collect
About
  • Classification
  • Label
GitHub (opens new window)
  • 技术文档

  • 算法

  • 工作总结

    • 时区校正
    • 上传下载文件方式总结
    • webpack优化实践
    • webpack基础应用知识总结
    • vue常见原理总结
      • vue常见原理
        • 模板编译过程概述
        • 响应式
        • vdom
        • diff算法概述
        • 组件的渲染更新过程
        • 将props传递子组件
        • hash模式
        • hisotry模式
        • hash与hisotry
    • vue基础知识总结
    • react高级特性
    • react基础知识总结
    • 微前端总结
    • 今天总结一下用到的css吧
    • 地图标绘--射线法来计算点在多边形内
    • web异常监控和分析
    • 工作中遇到的css问题记录
    • axios 与 promise
    • 前端优化指南
    • 小程序笔记
    • JS随机打乱数组
    • 非常实用的JavaScript一行代码
  • 实用技巧

  • 收藏夹

  • 技术
  • 工作总结
mamingjuan
2021-03-04
目录

vue常见原理总结

# vue常见原理

  1. 编译原理
  2. 响应式原理,依赖收集
  3. 组件化开发(贯穿vue的流程)
  4. diff算法

# 模板编译过程概述

  1. 将模板编译成抽象语法树 抽象语法树是什么样的可以在这里查看 (opens new window)选择vue选项即可 解析模板通过正则表达式获取标签、属性、表达式等,当遇到开始标签放入栈中,遇到结束标签则出栈,拼接成AST抽象语法树,然后通过优化器遍历AST进行标记静态节点,主要用来做虚拟DOM的渲染优化。
  2. 通过generate根据抽象语法树生成代码生成render函数vue template compiler将模板编译为render函数,执行render 函数生成vnode。基于vnode进行patch和diff。

使用vue-loader在开发环境下编译模板。

width函数

const obj = { a: 100, b: 200}

console.log(obj.a)
console.log(obj.b)
console.log(obj.c) // undefined

with(obj){
    console.log(a);
    console.log(b);
    console.log(c); // 会报错
}

// with语法改变自由变量的查找规则,当做obj属性来查找,如果调用了查找不到的属性会报错
// with打破了作用域的规则,易读性变差
1
2
3
4
5
6
7
8
9
10
11
12
13
14

引入vue-template-compiler 编译字符串模板 在index.js中写入模板和输出,在terminal中执行node index.js即可查看输出

// 插值
const template = `<p>{{message}}</p>`
// 编译后的结果是
// with(this){return _c('p',[_v(_s(message))])}
// 替换成函数
// with(this){return createElement('p',[createTextVNode(toString(message))])}

// // 表达式
// const template = `<p>{{flag ? message : 'no message found'}}</p>`
// 编译后的结果是
// // with(this){return _c('p',[_v(_s(flag ? message : 'no message found'))])}

// // 属性和动态属性
// const template = `
//     <div id="div1" class="container">
//         <img :src="imgUrl"/>
//     </div>
// `
// 编译后的结果是
// with(this){return _c('div',
//      {staticClass:"container",attrs:{"id":"div1"}},
//      [
//          _c('img',{attrs:{"src":imgUrl}})])}

// // 条件
// const template = `
//     <div>
//         <p v-if="flag === 'a'">A</p>
//         <p v-else>B</p>
//     </div>
// `
// with(this){return _c('div',[(flag === 'a')?_c('p',[_v("A")]):_c('p',[_v("B")])])}

// 循环
// const template = `
//     <ul>
//         <li v-for="item in list" :key="item.id">{{item.title}}</li>
//     </ul>
// `
// with(this){return _c('ul',_l((list),function(item){return _c('li',{key:item.id},[_v(_s(item.title))])}),0)}

// 事件
// const template = `
//     <button @click="clickHandler">submit</button>
// `
// with(this){return _c('button',{on:{"click":clickHandler}},[_v("submit")])}

// v-model
// const template = `<input type="text" v-model="name">`
// 主要看 input 事件
// with(this){
//    return _c('input',
//    {
//      directives: [{name:"model",rawName:"v-model",value:(name),expression:"name"}],
//      attrs:{"type":"text"},domProps:{"value":(name)},
//      on:{"input":function($event){if($event.target.composing)return;name=$event.target.value}}})
// }

// 在模板渲染阶段v-model已经被挂载上了onInput的事件来更新数据,每次更新触发渲染


// 编译
const res = compiler.compile(template)
console.log(res.render)


// // 从 vue 源码中找到缩写函数的含义
// function installRenderHelpers (target) {
//     target._o = markOnce;
//     target._n = toNumber;
//     target._s = toString;
//     target._l = renderList;
//     target._t = renderSlot;
//     target._q = looseEqual;
//     target._i = looseIndexOf;
//     target._m = renderStatic;
//     target._f = resolveFilter;
//     target._k = checkKeyCodes;
//     target._b = bindObjectProps;
//     target._v = createTextVNode;
//     target._e = createEmptyVNode;
//     target._u = resolveScopedSlots;
//     target._g = bindObjectListeners;
//     target._d = bindDynamicKeys;
//     target._p = prependModifier;
// }
// this._c = (a, b, c, d) => createElement(contextVm, a, b, c, d, needNormalization)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87

# 响应式

实现原理:
通过Object.defineProperty来监听数据的改变,当数据变更后触发set更新视图。当遇到object对象时继续深度遍历实现对象的深度监听,当遇到数组时重新定义数组对象,扩展新的方法,实现数据的监听

Object.defineProperty的缺点:

  • 深度监听需要一次性递归到底,计算量大。如果数据太大会卡。
  • 无法监听新增属性/删除属性(可通过vue.set, vue.delete解决)
  • 无法原生监听数组,需要特殊处理

// 触发更新视图
function updateView() {
    console.log('视图更新')
}

// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向 oldArrayProperty ,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
    arrProto[methodName] = function () {
        updateView() // 触发视图更新
        oldArrayProperty[methodName].call(this, ...arguments)
        // Array.prototype.push.call(this, ...arguments)
    }
})

// 重新定义属性,监听起来
function defineReactive(target, key, value) {
    // 深度监听
    observer(value)

    // 核心 API
    Object.defineProperty(target, key, {
        get() {
            return value
        },
        set(newValue) {
            if (newValue !== value) {
                // 深度监听
                observer(newValue)

                // 设置新值
                // 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
                value = newValue

                // 触发更新视图
                updateView()
            }
        }
    })
}

// 监听对象属性
function observer(target) {
    if (typeof target !== 'object' || target === null) {
        // 不是对象或数组
        return target
    }

    // 污染全局的 Array 原型
    // Array.prototype.push = function () {
    //     updateView()
    //     ...
    // }

    if (Array.isArray(target)) {
        target.__proto__ = arrProto
    }

    // 重新定义各个属性(for in 也可以遍历数组)
    for (let key in target) {
        defineReactive(target, key, target[key])
    }
}

// 准备数据
const data = {
    name: 'zhangsan',
    age: 20,
    info: {
        address: '北京' // 需要深度监听
    },
    nums: [10, 20, 30]
}

// 监听数据
observer(data)

// 测试
// data.name = 'lisi'
// data.age = 21
// // console.log('age', data.age)
// data.x = '100' // 新增属性,监听不到 —— 所以有 Vue.set
// delete data.name // 删除属性,监听不到 —— 所有已 Vue.delete
// data.info.address = '上海' // 深度监听
data.nums.push(4) // 监听数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89

vue2.x

  • 核心api - object.defineProperty
  • 在监听对象需要深度遍历和监听数组时需要对数组属性做特殊处理,处理数组数据重新定义数组原型触发视图更新
  • 复杂对象的深度监听会有问题,因为处理监听时需要递归到底,一次性计算量大
  • 无法监听新增、删除属性(vue.set、vue.delete)

vue3

  • 核心api proxy 、reflect
  • 深度监听性能更好
  • 可监听 新增、删除属性
  • 可监听数组变化
  • proxy可规避object.defineProperty的问题,但proxy无法兼容所有浏览器
// proxy创建响应式
function reactive(target = {}) {
    if (typeof target !== 'object' || target == null) {
        // 不是对象或数组,则返回
        return target
    }

    // 代理配置
    const proxyConf = {
        get(target, key, receiver) {
            // 只处理本身(非原型的)属性
            const ownKeys = Reflect.ownKeys(target)
            if (ownKeys.includes(key)) {
                console.log('get', key) // 监听
            }
    
            const result = Reflect.get(target, key, receiver)
        
            // 深度监听
            // 性能如何提升的?
            return reactive(result)
        },
        set(target, key, val, receiver) {
            // 重复的数据,不处理
            if (val === target[key]) {
                return true
            }
    
            const ownKeys = Reflect.ownKeys(target)
            if (ownKeys.includes(key)) {
                console.log('已有的 key', key)
            } else {
                console.log('新增的 key', key)
            }

            const result = Reflect.set(target, key, val, receiver)
            console.log('set', key, val)
            // console.log('result', result) // true
            return result // 是否设置成功
        },
        deleteProperty(target, key) {
            const result = Reflect.deleteProperty(target, key)
            console.log('delete property', key)
            // console.log('result', result) // true
            return result // 是否删除成功
        }
    }

    // 生成代理对象
    const observed = new Proxy(target, proxyConf)
    return observed
}

// 测试数据
const data = {
    name: 'zhangsan',
    age: 20,
    info: {
        city: 'beijing',
        a: {
            b: {
                c: {
                    d: {
                        e: 100
                    }
                }
            }
        }
    }
}

const proxyData = reactive(data)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

# vdom

vdom是用js模拟dom结构,计算出最小的变更,操作dom

为什么会有vdom?

  • 正常的dom操作非常耗费性能,以前用jQuery操作dom全靠手动调整很费力。容易出错,不易于维护。
  • 当程序有了一定的复杂度,想减少计算次数比较难,用js描述dom节点,计算最小的变更再操作dom性能更高。
  • vdom可以实现数据驱动视图,控制dom操作
<!-- html -->
<div id="div1" class="container">
    <p>vdom</p>
    <ul style="font-size:20px">
        <li>a</li>
    </ul>
</div>
1
2
3
4
5
6
7
// 翻译成vdom
{
    tag: 'div',
    props: {
        className: 'container',
        id: 'div1'
    },
    children: [
        {
            tag: 'p',
            children: 'vdom'
        },
        {
            tag: 'ul',
            props: {
                style: 'font-size:20px'
            },
            children: [{
                tag: 'li',
                children: 'a',
            }]
        }
    ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# diff算法概述

下面是snabbdom的实现原理

  • diff算法是vdom的核心部分。
  • diff算法是一个比较广泛的概念,如:linux diff、git diff
  • 两个js对象也可以做diff,两个树也能做diff
  • vue dom diff主要是同一层级做比较,tag不同则直接删除掉重建,不再深入比较,tag与key相同则认为是相同节点(sameVnode的条件)。
  • 不使用key的dom更新时直接删除重新建,如果使用key可以通过判断key与tag的匹配度来确认保留还是新建。当在patch中确定为相同的vnode后会执行patchVnode
  • patchVnode。新旧都有children进行updateChildren。新旧children是否有值,旧的有,新的无则删除节点(revmoeVnodes)。如果新有旧无,则新建(addVnodes)
  • updateChildren 规则是 开始和开始对比,结束和结束对比,开始和结束对比,结束和开始对比。如果都未命中,用新节点的key能否对应旧节点的某个key,没对就上建新节点,对就上判断tag是否相同,不相同时创建新节点,相同时patchVnode。 这里面体现了key的重要性。

# 组件的渲染更新过程

  1. 初次渲染
  • 解析模板为render函数
  • 绑定响应式,通过getter setter 监听data属性
  • 执行render函数,生成vnode, 然后patch(elem,vnode)

注

执行render函数会触发getter

  1. 更新过程
  • 修改data触发setter(此前已被收集监听的getter)
  • 重新执行render函数,成生newVnode
  • patch(vnode,newVnode)

流程图

在render函数执行时会touch触发data中的getter,触发后会收集依赖(触发了哪个变量的getter,就会观察哪个变量),一旦修改data的时候触发re-render,然后重新渲染,重新生成virtual dom tree update-component-process

  1. 异步渲染
  • $nextTick待dom渲染完再回调。
  • 页面渲染时会将data的修改做整合,多次data修改只会渲染一次。
  • 减少dom操作次数,提高性能。

注

注: ajax请求要放到mounted中。vue本身不支持ajax请求,需要使用vue-resource、axios等插件实现。一个组件的created比mounted早调用不了几微秒。放到created性能提高不了多少,而且等异步渲染的时候,create可能被中途打断,中断之后渲染又要重做一遍,在created中做ajax调用,代码里只有一次调用,但实际上可能是n次调用。如果将ajax调用放到mounted阶段,不会有重复的调用,更合适。

# 将props传递子组件

<User v-bind="$Props"/>

因为在React的的高阶组件用的时候,最好将props全部传递给调用的父组件。所以vue这里也提一下。

# hash模式

  • hash变化会触发浏览器的跳转,但不会刷新页面
  • hash变化永远不会提交到服务端
// hash 变化,包括:
// a. JS 修改 url
// b. 手动修改 url 的 hash
// c. 浏览器前进、后退
window.onhashchange = (event) => {
    console.log('old url', event.oldURL)
    console.log('new url', event.newURL)

    console.log('hash:', location.hash)
}

// 页面初次加载,获取 hash
document.addEventListener('DOMContentLoaded', () => {
    console.log('hash:', location.hash)
})

// JS 修改 url
document.getElementById('btn1').addEventListener('click', () => {
    location.href = '#/user'
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# hisotry模式

history跳转时不刷新页面,主要通过window.pushState和window.onpopState来实现

因为对于所有路径都会返回 index.html 文件。为了避免这种情况,你应该在 Vue 应用里面覆盖所有的路由情况,然后在给出一个 404 页面。

// 页面初次加载获取hash
document.addEventListener('DOMContentLoaded', () =>{
    console.log('loaded',location.pathname)
})

// 打开一个新的路由
// 【注意】用 pushState 方式,浏览器不会刷新页面
document.getElementById('btn1').addEventListener('click', () => {
    const state = { name: 'page1' }
    console.log('切换路由到', 'page1')
    history.pushState(state, '', 'page1') // 重要!!
})

// 监听浏览器前进、后退
window.onpopstate = (event) => { // 重要!!
    console.log('onpopstate', event.state, location.pathname)
}

// 每次请求服务器都要保证返回index.html页面,然后端端通过onpopstate来控制和监听页面跳到
// 哪里才不会出现页面丢失的现象
// 需要 server 端配合,可参考
// https://router.vuejs.org/zh/guide/essentials/history-mode.html#%E5%90%8E%E7%AB%AF%E9%85%8D%E7%BD%AE%E4%BE%8B%E5%AD%90


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

在vue中实现vue-router需要通过组件的install方法,在方法里面可能通过调用object.defineProperty注册全局$route。通过Vue.component注册routerLink和routerView组件。

import View from './components/view'
import Link from './components/link'

export let _Vue

export function install (Vue) {
  if (install.installed && _Vue === Vue) return
  install.installed = true

  _Vue = Vue

  const isDef = v => v !== undefined

  const registerInstance = (vm, callVal) => {
    let i = vm.$options._parentVnode
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal)
    }
  }

  Vue.mixin({
    beforeCreate () {
      if (isDef(this.$options.router)) {
        this._routerRoot = this
        this._router = this.$options.router
        this._router.init(this)
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      registerInstance(this, this)
    },
    destroyed () {
      registerInstance(this)
    }
  })

  Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
  })

  Object.defineProperty(Vue.prototype, '$route', {
    get () { return this._routerRoot._route }
  })

  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)

  const strats = Vue.config.optionMergeStrategies
  // use the same hook merging strategy for route hooks
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

# hash与hisotry

  • hash模式url会出现
  • history模式需要服务端的配合
  • toB的系统推荐用hash,简单易用不需要server配合,对url不敏感
  • toC的系统考虑h5 history,但需要服务端支持,history有利于seo,因为搜索引擎对于#后面的内容(锚)点一般是不收录的。
上次更新: 2025/04/07, 01:42:58
webpack基础应用知识总结
vue基础知识总结

← webpack基础应用知识总结 vue基础知识总结→

Copyright © 2015-2025 Glitz Ma
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式