主要了解学习了它得一些核心思想和功能,深入了解它得一个数据驱动,响应式原理,watcher,dep,observe等一些简单得实现。(简化得方便于我们理解它得一个核心理念)
解决疑问:
1.vue得模板解析完整过程 和 原理
2.Vue 如何实现得双向绑定原理
3.Vue得发布订阅模式 watcher dep OBserver
4.vue如何将数据挂载到实例上得
5.diff算法 2和3(暂无)
6.柯里化函数 偏函数 高阶函数
7.解析模板数据生成虚拟DOM得时候对数组得处理
8.vue中得私有数据 和 只读数据
9.vue中各函数得具体作用
10.vue的局部更新是怎么样的
11.什么是发布订阅模式,他的事件模型是怎么样的 跟webpack中的bapable差不多
12.vue中得watcher得理解(依赖收集,派发更新)
13.vue中得以组件为单位刷新数据得
14.vue得二次提交
15.vue得脏数据, 只更新了部分数据,导致数据不一致
16.vue是怎么实现局部刷新得(其实就是局部组件得刷新,watcher和dep)
vue得一个主要流程 (代码中得JGvue,我就直接说是vue了)
目录:
- new Vue() -> 初始化数据响应式 -> 内置编译模板转化 -> 字符串 -> AST(缓存) -> render方法编译 ->结合数据依赖收集depend,派发更新notify -> 虚拟DOM -> 通过watcher调用update方法 -> patch(核心方法createFunctionPatch) -> 将虚拟DOM 转化成真实DOM
- 修改数据 -> notify触发 -> dep中对应得watcher 一一促发 -> 结合数据生成虚拟DOM -> watcher调用update -> diff算法差异 -> patch(核心方法createFunctionPatch) -> 将虚拟DOM 转化成真实DOM
- 初始化实例
- 初始化数据data
- 编译渲染过程和数据响应式
- watcher 和 dep 依赖收集,派发更新
- diff算法了解
- 其他知识点
- 简单了解 methods @click vue得处理
完整得项目结构
1.初始化实例
- 准备一个html,这不多说就是一个html 文件 使用我们自己写得vue
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<div>
<div>
<p>{{name}}-{{message}}</p>
</div>
</div>
<p>{{name}}</p>
<p>{{message}}</p>
<p>{{obj.sex}}</p>
</div>
<!-- 执行顺序 -->
<script src="./watcher.js"></script>
<script src="./dep.js"></script>
<script src="./vnode.js"></script>
<script src="./compiler.js"></script>
<script src="./Vue.js"></script>
<script src="./initrender.js"></script>
<script src="./initData.js"></script>
<script>
let app = new JGVue({
el: "#app",
data: {
name: "yang",
message: "模板转换",
obj: {
sex: "男"
},
list: [{
info: "aaa"
}, {
info: "bbb"
}, {
info: "ccc"
}]
}
})
</script>
</body>
</html>
- vue.js中就是一个简单得实例,里面将数据等存储一些,在Vue中有个默认得规则带_得数据是内部数据 带$得是只读属性,准备一些数据,比如DOM,我这里就直接获取得DOM,再将它得父Dom也拿一些,
注意:Vue中其实获取得是字符串,然后将字符串转译成AST,这里涉及到了编译原理,所以暂时用Dom去转成VNode(带有{{name}}得坑)来代替AST。
function Vue(options) {
this._options = options;
this._data = options.data;
// this._templete = options.el; //vue中是字符串,这里是DOM
this._templete = document.querySelector(options.el); //vue中是字符串,这里是DOM
this._parent = this._templete.parentNode;
// 初始化数据
this.initData();
// 挂载
this.mount();
}
2.初始化数据
- initData(); vue 源码中在这里初始化了很多东西,例如watch,computed等,下图就是vue源码中得
- 我们这里简单得来实现下初始化data
新建一个文件initdata.js.。代码中有注释,下面也会大致得理一下逻辑
initdata.js
// 扩展数组得方法 使其响应式变化 这里就例举4个方法
let ARRAY_METHOD = ["push", "pop", "shift", "unshift"];
// 继承关系 arr->Array.prototype->Object.prototype.....
// 修改继承关系 arr->改写得方法->Array.prototype->Object.prototype.....
// 创建一个对象
let array_method = Object.create(Array.prototype);
ARRAY_METHOD.forEach((method) => {
// 重写array_method中得对应方法 函数拦截
array_method[method] = function () {
// 将数据响应式化
for (let item of arguments) {
Observer(item);
}
// 调用原来得方法 Array.prototype[method] 正常操作
console.log("函数拦截");
let res = Array.prototype[method].apply(this, arguments);
return res;
};
});
// vue中定义了一个函数 defineRetive(target,key,value,enumerable) 存储数据变化
function defineReactive(target, key, value, enumerable) {
if (typeof value === "object" && value != null) {
// 非数组得处理
Observer(value);
}
let dep = new Dep();
dep.__protoName__ = key;
Object.defineProperty(target, key, {
configurable: true,
enumerable: !!enumerable,
get() {
console.log("get数据:" + value);
// 依赖收集 :告诉当前得watcher哪些属性被访问了
dep.depend();
return value;
},
set(newVal) {
console.log("set数据:" + newVal);
if (typeof newVal === "Object" && newVal != null) {
// 重新赋值得数据赋值响应式,因此如果传入得是对象类型,将其转换为响应式
Observer(newVal);
}
value = newVal;
// 派发更新, 找到全局的 watcher, 调用 update
dep.notify();
},
});
}
// // 将对象转换为响应式 vm就是我们得vue实例 调用时处理上下文
function Observer(obj) {
if (Array.isArray(obj)) {
// 数组 只实现push pop
// vue2.0中针对直接改变数组得长度,vue是不能监听到变化得,因为数组length属性是不能被get 和 set得所以无法监听,他使用了$set来实现,实际也是调用了defineProperty
// vue3 用proxy解决了这问题
// 扩展数组得push pop 修改要进行响应式化得原型 函数拦截
obj.__proto__ = array_method;
for (let i = 0; i < obj.length; i++) {
// 对数组中得没个元素进行处理
Observer(obj[i]);
}
} else {
// 递归 :将obj[key]变成响应式 对象或值类型
let keys = Object.keys(obj);
for (let its of keys) {
defineReactive(obj, its, obj[its], true);
}
}
}
// __proto__ vue中做了兼容,如果支持,直接修改他得原型链,不支持得化 在目标对象上混入方法
// 将某一个对象得属性访问 映射到 对象得某一个属性成员上 列入映射到app实例上访问 实际式访问app._data中得数据
// vue实例上 _开头数据都是私有数据 $开头数据 只读数据 尽量不要访问,所以vue 将_得访问 交给了实例
function proxy(target, prop, key) {
Object.defineProperty(target, key, {
enumerable: true,
configurable: true,
get() {
return target[prop][key];
},
set(newVale) {
target[prop][key] = newVale;
},
});
}
// 初始化数据
Vue.prototype.initData = function () {
//1.将thia._data 数据响应式 2.将直接属性代理到实例上
let keys = Object.keys(this._data);
// 数据响应式
Observer(this._data);
// 代理
for (let item of keys) {
// 将app._data 数据源 映射到app上 app._data.name ==> app.name
proxy(this, "_data", item);
}
};
initData();初始化数据,干了两件事
1.将thia._data 数据响应式Observer() 来实现
- Observer()
Observer() 方法 是用来处理数据响应式得,重写data属性中得get set方法。 这个方法得逻辑:就是一个递归调用,判断传入得参数是否是数组
——不是数组:调用 defineReactive 数据响应式化
——是数组:数组这里稍微有点麻烦,数组得方法push,pop等是无法被监听到得,所以需要做一个函数得拦截:
(就是改变数据得原型链,arr->Array.prototype中间增加一环,通过prototype赋值,重写他得这些方法,里面去做一个数据得响应式Observer)
- defineReactive()
这个方法就是封装了Object.defineProperty()来重写get set方法;enumerable这个属性如果式false,就可以读取,不会被遍历;
- 这里有个依赖收集和派发更新,可以在渲染得时候去讲。
2.将直接属性代理到实例上
app实例中为了方便访问data中得数据,会将data中得数据映射到app实例上 ,通过一个proxy方法。其实就是对data中地址得一个引用。
3.编译渲染过程和数据响应式
this.mount()方法;
新建
initrender.js //初始化渲染相关函数, new 一个watcher实例
compiler.js //编译功能
getVNode.js //虚拟Dom
代码中都有注释,下面会大致得去理下逻辑
initrender.js
Vue.prototype.mount = function () {
// // 判断是否有自定义的render函数
// if (typeof this._options.render !== "function") {
// }
// 需要提供一个render函数 跟数据结合生成虚拟DOM
this.render = this.createRenderFn();
this.mountComponent();
};
// 发布订阅模式: 例如 老板发布一个商品列表 你需要订阅某个商品,老板记录谁定了什么,等到商品有了之后,老板会通知你(挨个通知)
// 1.有个一个容器,来存储需要被促发得东西
// 2.有一个方法,可以往容器里存储东西
// 3.有一个方法,可以将容器里的东西拿出来使用
// 事件模型 :发布订阅 有一个event对象 里面有 on(订阅/注册),off(移除),emit(发布)方法
// 实现事件模型
Vue.prototype.mountComponent = function () {
// 执行mountComponent()
let mount = () => {
// vue中是以组件为单位来判断,以节点为单位来局部更新 ,内部组件 diff算法比较的时候,发现更改就替换对应的虚拟DOM树,自定义组件,就会判断更新的是哪各组件,只会更新发生变化的组件,其他的不会更新
this.update(this.render());
};
console.log("挂载");
// 这个 Watcher 就是全局的 Watcher, 在任何一个位置都可以访问他了 ( 简化的写法 )
new Watcher(this, mount); // 相当于这里调用了 mount
};
/*
* 在真正得vue中使用了二次提交得 设计结构 (所有得操作都在虚拟DOM中完成)
* vue只要数据发生变化 就会每次都生成一个新得vnode, 与原有得vnode相比较 ,这就是diff算法,相同得忽略,不同得更新过去,然后就更新了我们真得DOM
*/
// 用柯里化函数得方式 1.生成rander函数(render是用来生成新得虚拟DOM) 2.目的是缓存抽象语法树
Vue.prototype.createRenderFn = function () {
// 缓存AST 这里暂时将ast当作 vnode形式得虚拟DOM
let ast = getVNode(this._templete);
return function render() {
// 将ast抽象语法树和数据 合成虚拟DOM
return combine(ast, this._data);
};
};
//目的:将将虚拟DOM 渲染到页面中。 注意一个算法 diff算法就在这
// 问题 为什么不把新得vnode 直接替换原来得vnode,主要是因为原来得vnode 跟跟 真实得DOM 都是一一对应得,如果替换了,你得重新处理他们得一个对应关系,又涉及到了遍历,耗性能
Vue.prototype.update = function (node) {
// 这里暂时直接转成真实DOM VUE中其实使用到了diff算法
this._parent.replaceChild(parseVNode(node), document.querySelector("#app"));
};
compiler.js
// 编译
// HTML 使用递归 来遍历DOM元素 生成虚拟DOM
// Vue 中源码使用得是 栈结构 + 递归
function getVNode(node) {
let nodeType = node.nodeType;
let _vnode = null;
if (nodeType === 1) {
// 元素
// 组装参数
let nodeName = node.nodeName;
let attrs = node.attributes; //是个伪数组
let _attrObj = {};
for (let item of attrs) {
_attrObj[item.nodeName] = item.nodeValue;
}
_vnode = new VNode(nodeName, _attrObj, undefined, nodeType);
// 考虑node得子元素
let childNode = node.childNodes;
for (let item of childNode) {
_vnode.appendChild(getVNode(item));
}
} else if (nodeType === 3) {
// 文本节点
_vnode = new VNode(undefined, undefined, node.nodeValue, nodeType);
}
return _vnode;
}
// 虚拟DOM转换成 真实DOM
function parseVNode(vnode) {
// 创建真实得DOM
let type = vnode.type;
let _node = null;
if (type === 3) {
// 创建文本节点
return document.createTextNode(vnode.value);
} else if (type === 1) {
// 创建元素节点
_node = document.createElement(vnode.tag);
// 属性
let data = vnode.data;
Object.keys(data).forEach((key) => {
let attrValue = data[key];
_node.setAttribute(key, attrValue);
});
// 子元素
let children = vnode.children;
children.forEach((subvnode) => {
// 递归转换
_node.appendChild(parseVNode(subvnode));
});
return _node;
}
}
// 将ast抽象语法树和数据 合成虚拟DOM
function combine(vnode, data) {
let _type = vnode.type;
let _data = vnode.data;
let _value = vnode.value;
let _tag = vnode.tag;
let _children = vnode.children;
let _vnode = null;
if (_type === 1) {
// 元素节点
_vnode = new VNode(_tag, _data, _value, _type);
_children.forEach((subvnode) => {
// 递归转换
_vnode.appendChild(combine(subvnode, data));
});
} else if (_type === 3) {
// 文本节点
_value = _value.replace(r, (_, g) => {
return getValueByPath(data, g.trim());
});
_vnode = new VNode(_tag, _data, _value, _type);
}
return _vnode;
}
let r = /\{\{(.+?)\}\}/g;
// 通过字符串来访问对象成员 为了匹配{{obj.sex}}
function getValueByPath(obj, path) {
let paths = path.split(".");
// 处理数组aa[0][1].cc[0].name
let res = obj;
for (let i = 0; i < paths.length; i++) {
res = res[paths[i]];
}
return res;
}
```bash
getVNode.js
// 转换成虚拟DOM 提升性能 {tag :"div",data:{class:"aaa"},value:"",children:[{}]} 操作DOM元素 浏览器需要去刷新 耗性能
class VNode {
// 标签,属性,值,类型
constructor(tag, data, value, type) {
this.tag = tag && tag.toLowerCase();
this.data = data;
this.value = value;
this.type = type;
this.children = [];
}
appendChild(vnode) {
this.children.push(vnode);
}
}
解析:mount()函数中创建了render函数,创建得函数 是一个柯里化函数,在vue中 ast生成之后会缓存起来得(用于方便每次修改数据结合数据生成虚拟DOM),因为解析字符串->AST是非常耗性能得。
- 模板 -> 字符串 -> AST抽象语法树
vue中是将模板转化成字符串,然后解析成AST,这里得算法比较复杂,涉及到了编译原理,就没有深究。 我上面是将DOM树转化成Vnode结点得形式来替代(value值带有{{name}}此类得);
- AST -> 虚拟DOM树 -
1.createRenderFn函数中创建了render函数,创建得函数 是一个柯里化函数,在vue中 ast生成之后会缓存起来得(用于方便每次修改数据结合数据生成虚拟DOM),因为解析字符串->AST是非常耗性能得。
2.这里需要一个watcher 来执行mount 方法中得 update(更新页面得)方法。render方法作为update得参数使用返回 虚拟DOM
3.render方法主要是来生成combine方法就是ast结合数据 生成虚拟DOM 就是一个递归转换 解析模板中得双括号 就用正则 let r = /{{(.+?)}}/g; getValueByPath(); 这里设置值得时候,会访问属性得get属性。这个时候 就涉及到了 依赖收集 和 派发更新 需要用到watcher 和 Dep 下面单独讲
4.update方法使用render方法返回组装好得虚拟DOM之后, update方法中使用到了diff算法 更新页面(patch方法,其核心就是createPatchFunction() 调用里面的各种对元素处理的方法)。
4.watcher 和 dep 依赖收集,派发更新
- 逻辑分析:
- 1.mountComponent中new Watcher一个实例。 会执行watcher中得get方法this.getter就是传入得mount方法,执行前后调用pushTarget和popTarget两个方法是Dep中得方法,用来设置和清楚全局得渲染watcher
- 2.watcher中this.getter执行得时候,会使用render方法编译模板访问属性,就会触发属性得get, 这个时候会做一个依赖收集,每个属性都会new 一个Dep实例,里面会将当前得watcher 存放入Dep,同时也会将当前得dep存入watcher,是一个相互引用得关系,双向链表。
- 3.Dep中有个Dep.target用来存放当前得watcher。同时也还有一个watcher栈,用来存放,mount方法执行完毕之后。清楚当前得Dep.target;
- 4.触发属性改表。会调用属性得set方法 ,里面调用 dep中得notify来派发更新,就是调用当前属性对应得Dep中存储得watcher,一一触发更新页面
- 5.watcher可以存在多个,多组件得时候,每个组件都会初始化,都会有对应得watcher.正因为如此,vue才能实现一个局部更新(最小以组件为单位),而不是全部更新。
watcher .js
// watcher 方法
// get:用来计算或者执行函数(computed,watch两个 两者都要执行) 真正处理渲染得方法
// update: 公共得外部方法,会触发内部得run方法
// run:运行,来判断内部是否使用异步运行 (服务端渲染同步处理得),这个run 最终会调用内部得get方法
// cleanupDep:清楚队列
// 在vue中有很多得组件,每个组件都有对应得watcher 都是独立得 局部更新得
// 在模板渲染得时候,将watcher存入全局,访问各个属性值get方法得时候,会做一个依赖收集,将当前得一个渲染watcher存入到全局得容器Dep中, 两者互相关联 引用,然后再改变属性得时候在set方法中去做一个派发更新,通过Dep中得notify找到对应得渲染watcher,渲染页面,渲染完成后移除全局得渲染watcher,watcher如果是局部的watcher就是局部更新
// 观察者watcher,用来发射行为
// 记录个数
let watcherid = 0;
class Watcher {
// 参数有很多 就写2个先
// vm Vue实例
// expOrfn:字符串或者函数 watch传入得是 路径,computed传入得是个函数
constructor(vm, expOrfn) {
this.vm = vm;
this.getter = expOrfn;
this.id = watcherid++;
this.deps = []; //依赖项
this.depIds = {}; //是一个Set类型,用于保证 依赖项得唯一性(简化得代码,暂时不实现)
// 一开始渲染 vue中:this.lazy?undefined:this.get()
this.get();
}
// 计算触发getter
get() {
// 将当前操作得watch 存储到全局得watch
pushTarget(this);
this.getter.call(this.vm, this.vm); // 上下文的问题就解决了
// 剔除
popTarget();
}
run() {
this.get();
}
update() {
this.run();
}
// 清空dep 依赖队列
cleanupDep() {}
/** 将 当前的 dep 与 当前的 watcher 关联 */
addDep(dep) {
this.deps.push(dep);
}
}
Dep.js
let depid = 0;
// Dep是负责存放数据所绑定所有的观察者的对象的容器,只要数据发生改变,就会通过这个Dep来通知所有观察者进行修改数据。(每个数据都有独一无二的Dep)
// 负责将属性 和 watcher关联
class Dep {
constructor() {
this.id = depid++;
// 存储与当前dep关联得watch
this.subs = [];
}
// 添加watcher
addSub(sub) {
this.subs.push(sub);
}
// 移除
removeSub(sub) {
for (let i = this.subs.length - 1; i >= 0; i--) {
if (sub === this.subs[i]) {
this.subs.splice(i, 1);
}
}
}
// 将dep和watch关联 互相引用 双向链表
depend() {
if (Dep.target) {
this.addSub(Dep.target);
Dep.target.addDep(this);
}
console.log("读取属性");
console.log(this.subs, Dep.target);
}
/** 触发与之关联的 watcher 的 update 方法, 起到更新的作用 */
notify() {
// 在真实的 Vue 中是依次触发 this.subs 中的 watcher 的 update 方法
// 此时, deps 中已经关联到 我们需要使用的 那个 watcher 了
console.log("派发更新");
let deps = this.subs.slice();
deps.forEach((watcher) => {
watcher.update();
});
}
}
// 全局得容器 存储渲染得watcher
Dep.target = null;
// 在全局中创建一个 watcher栈 把一个操作中需要使用得watcher存起来,在watcher调用get方法得时候将当前watcher放到全局,在get执行结束之后将这个watcher移除
let targetStack = [];
// 将当前操作得watcher 存储到 全局watcher中
function pushTarget(target) {
// vue中使用得是Push
targetStack.unshift(Dep.target);
Dep.target = target;
}
// 将watcher剔除 vue中使用得是Push
function popTarget(target) {
targetStack.shift();
Dep.target = targetStack[targetStack.length - 1];
console.log(Dep.target);
}
/*
* 在watcher调用get方法得时候调用pushTarget(this)
* 结束得时候popTarget()
*/
5.diff算法了解
- vue2:中的diff算法 全量对比 ,深度优先,同层比较, 例如
- vue3:在创建DOM树得时候 动态数据会添加一个静态标记,这样对比得时候就直接对比有标记得就行了
6.其他知识点
- 函数拦截:vue中对数组方法得扩展值得借鉴,
- 元素得映射 例如:app.data.name ===>app.name访问 proxy方法。其实就是使用了object.defineProperty get set得时候返回app.data.name
- 发布订阅模式:1.有个一个容器Dep,来存储需要被促发得watcher,2.有一个方法depend,可以往容器里存储watcher,有一个方法notify,可以将容器里的东西拿出来使用
- 事件模型 :发布订阅 有一个event对象 里面有 on(订阅/注册),off(移除),emit(发布)方法
- 局部更新: vue中是以组件为单位来判断,以节点为单位来局部更新 ,内部组件 diff算法比较的时候,发现更改就替换对应的虚拟DOM树,自定义组件,就会判断更新的是哪各组件,只会更新发生变化的组件,其他的不会更新
- makeMap:vue源码中得一个方法来判断是否是html标签(很好得一个柯里化函数例子)
- vue中得提交:都是二次提交,就会每次都生成一个新得vnode, 与原有得vnode相比较 ,这就是diff算法,相同得忽略,不同得更新过去,然后就更新了我们真得DOM
- 1.柯里化函数,将原本一个函数接受多个参数,改成接受一个参数,return 一个新函数来处理剩下得结果,
为什么使用:为了提升性能,可以缓存一部分能力
vue模板渲染得本质是使用html得模板字符串, 模板->AST->VNode->DOM
最消耗性能得是 模板->AST; 需要对字符串解析 例如解析一个数字运算得字符串求结果 很麻烦(波兰式表达式) - 2.偏函数:将原本一个函数接受多个参数,改成传入一部分函数 ,return 一个新函数来处理剩下得结果
- 3.高阶函数: 一个函数得参数是一个函数,该函数对这个函数加工,得到一个函数,这个加工得函数就是高阶函数
7.简单了解 methods @click vue得处理
- 在vue中其实都是在AST通过render函数 来结合数据生成虚拟DOM树得时候就处理了,我们打印下app实例可以看到它得一个虚拟Dom,app._vnode,它里面有一个data 是包含所有得属性,里面有一个On就是绑定函数得,以内函数是对象是引用类型,每次修改methods中,都会同步更新方法
<button @click="aa">aaa</button>
<button @click="bb">bbb</button>
let app = new Vue({
el: "#app",
methods: {
aa() {
console.log("aa")
},
bb() {
console.log("bb")
}
}
})