1. 参考文献
强烈推荐:双向数据绑定的 3 种实现方式
vue 源码学习:Object.defineProperty 对数组监听
深度剖析:如何实现一个 Virtual DOM 算法
构建一个使用 Virtual-DOM 的前端模版引擎
vue 底层实现分析
请说一下 vuex 工作原理
2.操作 Dom 更新时机
React:框架不主动追什么时候数据变了,需要手动通知框架状态变化
Vue:setter 监听变化
2.1 setter 监听变化
2.1.1 问题
问题:
1、给对象添新属性(data.newKey = ‘value’)时,setter 监听不到
2、delete 删掉现有属性,setter 监听不到
3、数组变化监听不到
解决方法:
1、通过 API 手动添加新属性 vm.$set('b', 2)
2、无关紧要,需要 delete 的场景很少,赋值 undefined 再添一点额外判断就能避免 delete 了
3、Vue 通过篡改原生 Array 方法-篡改原生 Array 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| var arrayPush = {};
(function (method) { var original = Array.prototype[method]; arrayPush[method] = function () { // this 指向可通过下面的测试看出 console.log(this); // 这里可以增加一些触发data变化的函数方法,达到触发的效果 return original.apply(this, arguments); }; })('push');
var testPush = []; testPush.__proto__ = arrayPush; // 通过输出,可以看出上面所述 this 指向的是 testPush // [] testPush.push(1); // [1] testPush.push(2);
|
2.1.2 原理
1、observe:主要是通过遍历 Object.defineProperty 为每个属性添加 get 和 set 方法
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
| function observe(obj) { if (!obj || typeof obj !== 'object') { return; } Object.keys(obj).forEach((key) => { defineReactive(obj, key, obj[key]); }); } export function defineReactive(obj, key, val) { var dep = new Dep(); var childOb = observe(val);
Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: () => { if (Dep.target) { dep.addSub(Dep.target); } return val; }, set: (newVal) => { var value = val; if (newVal === value) { return; } val = newVal; childOb = observe(newVal); dep.notify(); }, }); }
|
2.监听队列:
1 2 3 4 5 6 7 8 9 10 11
| export default class Dep { constructor() { this.subs = []; } addSub(sub) { this.subs.push(sub); } notify() { this.subs.forEach((sub) => sub.update()); } }
|
3、 订阅者–分辨是主动触发还是自动触发
单线程,当调用 watch 的时候会触发调用 get,这时候先标记一个全局变量,然后在普通属性里会判断是否含有这个变量,有的话就是自动触发,没有的话就是手动调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| export default class Watcher { constructor(obj, key, cb) { Dep.target = this; this.cb = cb; this.obj = obj; this.key = key; this.value = obj[key]; Dep.target = null; } update() { this.value = this.obj[this.key]; this.cb(this.value); } }
|
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 90 91 92 93 94 95 96 97
| <body> <input type="text" id="input" data-bind="value" /> <div id="output" data-bind="value"></div> <button id="btn">update value</button>
<script> var $ = document.querySelector.bind(document); var $input = $('#input'); var $output = $('#output');
var data = { value: 'default text', };
var cacheArr = []; var cacheData = function (data) { for (var i = 0; i < cacheArr.length; i++) { if (cacheArr[i]._cacheSource === data) { return cacheArr[i]; } }
var cache = { _cacheSource: data, }; for (var k in data) { if (data.hasOwnProperty(k)) { cache[k] = data[k]; } } cacheArr.push(cache); return cache; };
var updateView = function (cache, newValue) { var nodes = cache._cachedNodes; var VALUE_NODES = ['INPUT', 'TEXTAREA'];
nodes.forEach(function (node) { if (VALUE_NODES.indexOf(node.tagName) !== -1) { if (node.value !== newValue) { node.value = newValue; } } else { node.innerText = newValue; } }); };
var bind = function (node, data) { var key = node.getAttribute('data-bind'); if (!key) return console.error('no data-bind key');
var cache = cacheData(data);
if (cache._cachedNodes) { cache._cachedNodes.push(node); } else { cache._cachedNodes = [node]; Object.defineProperty(data, key, { enumerable: true, set: function (newValue) { cache[key] = newValue; updateView(cache, newValue); return cache[key]; }, get: function () { return cache[key]; }, }); }
updateView(cache, cache[key]); }; bind($input, data); bind($output, data);
$input.addEventListener('input', function () { data[$input.getAttribute('data-bind')] = this.value; });
$('#btn').onclick = function () { data.value = 'updated value ' + Date.now(); }; </script> </body>
|
2.1.3 依赖收集来源
data
template 中 解析为 render 函数的时候 会触发 get
computed 中 执行一个会触发 get
2.1.4 异步更新
set 触发 update,推入到执行队列
通过 nextTick 来进行一次性执行
微任务队列清空后执行 UI Render 渲染界面
兼容性尝试
Promise → MutationObserver → setImmediate → setTimeout
优势:修改数据,一次性更新 DOM
2.1.5 宏任务 + 微任务
宏任务:
setTimeout、setInterval、setImmediate、requestAnimationFrame、I/O、UI 交互事件、MessageChannel
微任务:
process.nextTick、MutationObserver、Promise
2.1.6 缓存 keep-alive
keep-alive 可以理解为一个函数式组件(render 组件),每次都会执行
1、取第一个子组件
2、获取 name 和其他参数,以及 include,exclude、max
3、命中缓存直接从缓存中取,并重置到最后一位。
4、如果没有缓存,将其缓存,并判断 max,超过则删除第一个
5、不会执行 created 和 mounted
6、在渲染后即 mounted 后,判断是否已经执行完了,才会调用 activated 并递归其子组件都执行这个钩子
7、在 destroyed 钩子后执行和 deactivated 钩子函数并递归执行子组件的这个钩子
2.1.7 computed 缓存 原理
1、initComputed 创建 watchers 的观察者对象
1
| var watchers = (vm._computedWatchers = Object.create(null));
|
2、将 computed 的 key 都创建 Watcher 实例,并赋值给 watchers。watchers[key] = new Watcher({getter: computed[key].get})
1 2 3 4
| watchers = Object.assign(watchers, { key1: new Watcher({ getter: computed[key1].get }), key2: new Watcher({ getter: computed[key2].get }), });
|
3、将 computed 的 key 都挂载到 data 上,并且修改 getter 为自定义的 getter
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
| var data= { key1:createComputedGetter(key1) key2:createComputedGetter(key2) }
defineComputed(vm, key); function defineComputed(target, key){ Object.defineProperty(target, key, createComputedGetter(key)); }
function createComputedGetter(key) { return function computedGetter() { var watcher = this._computedWatchers && this._computedWatchers[key]; if (watcher) { if (watcher.dirty) { watcher.evaluate(); } if (Dep.target) { watcher.depend(); } return watcher.value; } }; }
|
4、当修改了依赖的 data 属性时,执行 update,使 dirty = true
1 2 3 4 5 6 7 8 9 10
| Watcher.prototype.update = function update() { if (this.lazy) { this.dirty = true; } else if (this.sync) { this.run(); } else { queueWatcher(this); } };
|
5、页面更新,读取到 data 上的 computed 值,触发 watcher.evaluate,返回修改后的值
1 2 3 4 5 6 7
| function evaluate() { if (this.dirty) { this.value = this.get(); this.dirty = false; } return this.value; }
|
解析 Vue.js 中的 computed 工作原理-手写
Vue computed 源码解析
Vue computed 实现原理
2.1.8 Diff 原理
1、判断节点是否相同
2、如果老节点存在,新节点也存在
3、判断是否完全相同,是否是静态节点,文本节点,如果都不是则
4、while 循环中间靠拢比较
diff 算法:
还是和上面一样,依然先获取到最新的 listData
然后新的 data 进行_render 操作,得到新的 vnode
对比前后 vnode,也就是 patch 过程
对于同一层级的节点,会进行 updateChildren 操作(diff),进行最小的变动(while 循环)
不用 key 做索引的原因:
如果插入一个元素,造成 index 全部变化,就失去了虚拟 dom 的意义了
2.1.9 如何监听数组变化
利用 AOP 面向切面这种方式来重写了数组的 push 等方法,在调用 push 的时候执行了切面函数来触发视图更新
Vue2.0 响应式原理以及重写数组方法
3.DOM 更新效率
两种方式:
1、不做 diff 处理,当接受到更新指令时(setState)更新所有数据
React 中,setState()不关注数据是不是之前的,变了还是没变,只要传入了状态,就丢弃上一个状态。
也就是说,把原数据对象原封不动的再 setState()一遍,也必须向下检查整棵子树,才能得到数据没变,不需要更新视图的结论。
因为状态丢弃机制,不追踪数据变化细节,即便传入同一份数据,也没办法立即确定状态没变
解决:
React 的话,有简单的优化方案,可以跳过没变的数据检查:用不可变的数据结构,比如Immutable.js
用不可变的数据结构能够快速排除砍掉没变的,得到发生变化的部分,提升 diff 效率 2.通过 diff 处理,只更新变动部分的数据
不知道这次的状态更新对子树的影响。
但维护数据状态、追踪变化方案的明显优势就是知道每份数据所关联的视图,也就是说与指定数据相关的真实节点列表是已知的
3.1 虚拟 DOM
脑图总结
深度剖析:如何实现一个 Virtual DOM 算法
构建一个使用 Virtual-DOM 的前端模版引擎
4.Vuex
vuex 中的 store 本质就是没有 template 的隐藏着的 vue 组件
Vuex 和单纯的全局对象有以下两点不同:
1、支持 mvvm
2、修改属性需要通过 commit
流程:
dispatch(action) → commit(mutations) → state
源码:
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
| class Store { constructor(options) { this.state = options.state;
} }
Vue.mixin({ beforeCreate() { if (this.$options && this.$options.store) { this.$store = this.$options.store; } else { this.$store = this.$parent && this.$parent.$store;
} }, });
|
5.Redux
6.Fiber
解决两个问题:
a. 保证任务在浏览器空闲的时候执行;
b. 将任务进行碎片化;
1)、requestIdleCallback
requestIdleCallback(callback)是实验性 API,可以传入一个回调函数,回调函数能够收到一个 deadline 对象,通过该对象的 timeRemaining()方法可以获取到当前浏览器的空闲时间,如果有空闲时间,那么就执行一小段任务,如果时间不足了,则继续 requestIdleCallback,等到浏览器又有空闲时间的时候再接着执行。
2)、链表结构
目前的虚拟 DOM 是树结构,当任务被打断后,树结构无法恢复之前的任务继续执行,所以需要一种新的数据结构,即链表,链表可以包含多个指针,可以轻易找到下一个节点,继而恢复任务的执行。
Fiber 采用的链表中包含三个指针,parent 指向其父 Fiber 节点,child 指向其子 Fiber 节点,sibling 指向其兄弟 Fiber 节点。一个 Fiber 节点对应一个任务节点。
Fiber 架构的简单理解与实现
7.Nuxt
webpack:
入口:
server entry + client entry
打包后:
Server 部分:
Server Bundle + BundleRenderer
↓ Render
↓
Html
↑
↑ Hydrate
ClientBundle
以上为 Browser 部分
最佳实践:
lru 缓存
Redis
页面/接口/组件 三个维度
KeepAlive 开启避免资源浪费
lazy-hydration:只加载页面 交互后增加
docker
压测
优化、缓存
页面结构设计
容灾降级
性能监控
进程监控 monitor
错误监控 sentry
内网域名
参考文献