我的青铜版vue代码地址: 【GitHub | 码云】
【GitHub | 码云】——青铜版vue代码都是结核vue源码简化实现注释详细可放心品尝
实现原理图:
vue.js初始化流程图:对应vue源码
数据响应式 Observer 原理:
Observer 作用:通过Object.defineProperty
给 data
内的所有层级的数据都进如下操作:
class Observer {
constructor(data) {
//__ob__ 一个响应式标记 作用:将当前this'继承'给需响应的对象或数组
Object.defineProperty(data, '__ob__', {
value: this, //指向this
enumerable: false, //不可枚举
configurable: false
})
//判断数组响应式
if (Array.isArray(data)) {
data.__proto__ = arrayMethods //替换封装的原型方法
this.observeArray(DataCue)
} else {
this.walk(data)
}
}
observeArray(data) {
for (let i = 0; i < data.length; i++) {
observe(data[i])
}
}
walk(data) {
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
defineReactive(data, key, value) {
observe(value) //递归 所有数据响应式
let dep = new Dep //每个属性一个
Object.defineProperty(data, key, {
get() {
if (Dep.target) { //将Dep.target赋值后再调用get方法就可以给该属性添加一个wacher
dep.depend() //添加watcher
}
return value
},
set(newValue) {
if (newValue === value) return
observe(newValue) //给新数据响应式
value = newValue
//视图更新
dep.notify()
}
})
}
}
export function observe(data) {
//不是对象或=null不监控
if (!isObject(data)) {
return
}
//对象已监控 则跳出
if (data.__ob__ instanceof Observer) {
return
}
return new Observer(data)
}
observe 方法的作用是遍历对象,在内部对数据进行劫持添加 get 和 set方法
,把劫持的逻辑单独抽取成 defineReactive 方法
,observe 方法作用是对数据类型验证,符合需求后会调用Observer
方法进行属性响应式,然后再循环对象每一个属性进行劫持,当数据为数组时,通过重写改变数组的7
个方法来实现监听数组改变而触发指定更新watcher, set
方法内部的 observe
作用是将新赋值的对象进行深度劫持,确保该插入数据转换成响应式。
在 defineReactive
方法中,对每一个属性创建了 Dep 的实例, 当页面上出现 {{data}}响应式数据时该属性Dep内就会新增一个 watcher
,当render
函数执行就会触发了 get,在 get 中就可以将这个 watcher
添加到 Dep 的 subs 数组中进行统一管理,因为在代码中获取 data 中的值操作比较多,会经常触发 get,我们又要保证 watcher
不会被重复添加,所以在 Watcher
类中,获取旧值并保存后,立即将 Dep.target 赋值为 null,并且在触发 get 时对Dep.target
进行了短路操作,存在才调用 Dep 的 depend 进行添加
dep 和 watcher 是一个多对多的关系
每个组件一个diff
的逻辑 也就是每个组件一个watcher 也就是组件页面内多个响应式属性指向一个watcher
每个属性对应一个dep
,而dep
内存储多个watcher
也就是该dep
出现在多个watcher
内 说明该属性存在多个组件页面内响应式显示
详情请异步源码
模板编译器 compiler 原理:
如果你通过document.querySelector("div").outerHTML
获取一个节点的outerHTML你会发现它就是你在html文件内写的标签代码字符串
- 第一步我们需通过
parseHTML
方法把outerHTML
转换成ast
树 - 第二步我们我们需把ast树转换成
render
函数的字符串形式 - 最后我们通过
new Function()
方法 将render函数的字符串形式转换成真正的render
方法,render
方法的作用就是生成vNode
, 最终通过diff
算法比对新老vNode
从而完成了页面的更新渲染
export function compileToFunctions(template) {
//1. 将outerHTML 转换成 ast树
let ast = parseHTML(template) // { tag: 'div', attrs, parent, type, children: [...] }
// console.log("AST:", ast)
//2. ast树 => 拼接字符串
let code = generate(ast) //return _c('div',{id:app,style:{color:red}}, ...children)
code = `with(this){ \r\n return ${code} \r\n }`
// console.log("code:", code)
//3. 字符串 => 可执行方法
let render = new Function(code)
/**如下:
* render(){
* with(this){
* return _c('div',{id:app,style:{color:red}},_c('span',undefined,_v("helloworld"+_s(msg)) ))
* }
* }
*
*/
return render
/**
* 编译原理的3个步骤:
* 1. outerHTML => ast树
* 2. ast树 => render字符串
* 3. render字符串 => render方法
*/
}
patch.js diff算法实现原理:
通过比对新旧vNode
的不同而更新 dom 渲染页面
Vnode是什么? 下面就是一个Vnode的雏形:
el
: 就是当前节点对应dom上的真实节点,当比对两个vNode
不同时就直接通过el
操作真实dom渲染页面
它对应的HTML
如下:
<div id="app">
<h1 class="h1">标题一 姓名: {{name}} </h1>
<h2 style="color: red;">标题二 年龄: {{age}}</h2>
<div>
<h3 class="h2">标题三</h3>
<span style="color: pink;font-size: 30px;">姓名: {{name}} ,年龄: {{age}}</span>
</div>
</div>
首先进行树级别比较,可能有三种情况:增删改。
new VNode
不存在就删 old VNode
不存在就增
都存在就执行diff执行更新:
//diff算法核心 vnode比较得出最终dom
//对应vue源码 src\core\vdom\patch.js 424行
oldStartIndex // 老的开始的索引
oldStartVnode // 老的开始
oldEndIndex // 老的尾部索引
oldEndVnode // 获取老的孩子的最后一个
newStartIndex // 老的开始的索引
newStartVnode // 老的开始
newEndIndex // 老的尾部索引
newEndVnode // 获取老的孩子的最后一个
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
//1与2解决数组塌陷时设置节点为null的问题
//1. 旧开始节点是否存在 不存在下一个
if (!oldStartVnode) {
oldStartVnode = oldChildren[++oldStartIndex]
//2. 旧结束节点是否存在 不存在前一个
} else if (!oldEndVnode) {
oldEndVnode = oldChildren[--oldEndIndex]
//3. 新老 开始 节点是否相同 是递归patch比较子节点
} else if (isSameVnode(oldStartVnode, newStartVnode)) {
patch(oldStartVnode, newStartVnode)
oldStartVnode = oldChildren[++oldStartIndex]
newStartVnode = newChildren[++newStartIndex]
//4. 新老 结束 节点是否相同 是递归patch比较子节点
} else if (isSameVnode(oldEndVnode, newEndVnode)) {
patch(oldEndVnode, newEndVnode);
oldEndVnode = oldChildren[--oldEndIndex]; // 移动尾部指针
newEndVnode = newChildren[--newEndIndex];
//5. 老开始 新结束 节点是否相同 是递归patch比较子节点
} else if (isSameVnode(oldStartVnode, newEndVnode)) { // 正序 和 倒叙 reverst sort
//头不一样 尾不一样 头移尾 倒序操作
patch(oldStartVnode, newEndVnode);
parent.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling); // 具备移动性
oldStartVnode = oldChildren[++oldStartIndex];
newEndVnode = newChildren[--newEndIndex];
//6. 老结束 新开始 节点是否相同 是递归patch比较子节点
} else if (isSameVnode(oldEndVnode, newStartVnode)) { // 老的尾 和新的头比对
patch(oldEndVnode, newStartVnode)
parent.insertBefore(oldEndVnode.el, oldStartVnode.el)
oldEndVnode = oldChildren[--oldEndIndex]
newStartVnode = newChildren[++newStartIndex]
}
...
watcher异步更新队列原理:
事件循环Event Loop: 浏览器为了协调事件处理、脚本执行、网络请求和渲染等任务而制定的工
作机制。
宏任务Task: 代表一个个离散的、独立的工作单元。浏览器完成一个宏任务,在下一个宏任务执
行开始前,会对页面进行重新渲染。主要包括创建文档对象、解析HTML
、执行主线JS
代码以及各
种事件如页面加载
、输入
、网络事件
和定时器
等。
微任务: 微任务是更小的任务,是在当前宏任务执行结束后立即执行的任务。如果存在微任务,浏
览器会清空微任务之后再重新渲染。微任务的例子有 Promise回调函数
、DOM变化
等。
体验宏微任务处理流程
异步: 只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变
更。
批量: 如果同一个 watcher
被多次触发,只会被推入到队列中一次
。去重对于避免不必要的计算
和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列执行实际工作。
异步策略: Vue 在内部对异步队列尝试使用原生的 Promise.then
、 MutationObserver
或 setImmediate
,如果执行环境都不支持,则会采用 setTimeout
代替。
let has = {}; // vue源码里有的时候去重用的是set 有的时候用的是对象来实现的去重
let queue = [];
// 这个队列是否正在等待更新
function flushSchedulerQueue() {
for (let i = 0; i < queue.length; i++) {
queue[i].run() // 执行 watcher 内部的 updateComponent 方法 更新页面
}
queue = []
has = {}
}
//由于多个元素指向同一个 watcher 所以更新的时候需要把这些 watcher 集中起来 去重后一起执行
//原因:如果每匹配一个元素就执行一个 watcher 这样重复执行了许多相同的 watcher 性能大大下降
export function queueWatcher(watcher) {
const id = watcher.id;
if (has[id] == null) {
has[id] = true // 如果没有注册过这个watcher,就注册这个watcher到队列中,并且标记为已经注册
queue.push(watcher) // watcher 存储了updateComponent方法 用来更新页面
console.log("queuequeue---", queue);
nextTick(flushSchedulerQueue); // flushSchedulerQueue 调用渲染watcher
}
}
// 1. callbacks[0] 是flushSchedulerQueue函数 当监听组件data数据改变时会执行dep.notify()方法
// 2. dep.notify()方法将所有触发的 watcher 传递给 queueWatcher 方法
// 3. queueWatcher方法会对 watcher 进行去重 当所有组件data改变都监听完后 flushCallbacksQueue 开始工作
let callbacks = []; // [flushSchedulerQueue,fn]
let pending = false
function flushCallbacksQueue() {
callbacks.forEach(fn => fn())
pending = false
}
//上面22行第一次进入nextTick就开启了一个定时器 执行 nextTick 进来的回调函数
//由于js定时器为宏观任务,定时器会等到所有微观任务都执行后才会执行定时器
//所以当组件内的nextTick回调都一个个添加 callbacks 内且页面完全渲染后会触发 flushCallbacksQueue 方法
export function nextTick(fn) {
callbacks.push(fn) // 防抖
if (!pending) { // true 事件环的概念 promise mutationObserver setTimeout setImmediate
setTimeout(() => {
flushCallbacksQueue() //清除回调队列
}, 0)
pending = true
}
}