# 存储watcher
<!DOCTYPE html>
<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>
<!--
模板编译原理 通过AST语法树描述出html语法结构 ,在把这个语法转换成JS语法,包上with 生成render方法 ,调用这个方法返回虚拟dom, 再把虚拟dom变成真实dom渲染到页面上。
注意:虚拟dom和ast语法树不一样,虚拟dom是表示dom对象的,ast语法树这里表示html结构
1. 将模板变成一个render函数,
2. 去当前的的实例上取值
3. 虚拟dom(说白了就是一个对象)它的好处是可以描述DOM结构,diff算法,可以比对哪些属性变化了哪些属性没有变化,把需要变化的进行更新
4. 生成真实dom
-->
<div id="app" a=1 b=2 style="color:red;font-size:12px;">
<span style="color:red;">{{name}} aa {{age}} haha<a>hello</a></span>
</div>
<script src="/dist/vue.js"></script>
<script>
const vm = new Vue({
el: '#app',
data() {
return {
name: 'f',
age: 18,
address: ''
}
}
})
console.clear()
// 修改了数据后,手动调用_update方法更新dom
vm.name = 'dd'
vm._update(vm._render()) // 强制更新的方法,但是我们希望数据变化后可以自动更新视图,而不是手动触发
// 在这里取值,没有依赖模板,所以不需要收集依赖
vm.address
setTimeout(() => {
vm.name = 'ff'
}, 1000)
// console.log(vm)
/*
1. 默认渲染时会将渲染watcher放到Dep.target上
2. 当我们调用this.getter时就会对属性进行取值操作
3. 让Dep.target值为空,不在模板中访问的值不记录watcher
4. 修改name值,会找到name对应的dep,通知dep中的watcher执行
*/
</script>
</body>
</html>
src/lifeCycle.js
import { patch } from './observer/patch';
import Watcher from './observer/watcher';
export function lifecycleMixin(Vue) {
Vue.prototype._update = function(vnode) {
console.log('_udpate', vnode);
// 将虚拟节点转换成真实dom
const vm = this;
console.log('vm.$options.el', vm.$options.el);
// 首次渲染需要用虚拟节点更新真实dom
// vm.$el = patch(vm.$options.el, vnode);
// 第一次渲染的完毕后,拿到新的节点,下次再次渲染时替换上次渲染的结果
vm.$options.el = patch(vm.$options.el, vnode);
};
}
export function mountComponent(vm, el) {
// 默认 vue 通过 watcher 来渲染,这个watcher可以叫做渲染watcher,每个组件都有一个渲染watcher
vm.$el = el;
let updateComponent = () => {
// 将虚拟节点 渲染到页面上
// vm._render() 返回虚拟节点
// vm._update() 将虚拟节点转换成真实节点
vm._update(vm._render());
};
new Watcher(
vm,
updateComponent,
() => {
// 回调函数
},
true
); // true 表示这是一个渲染watcher
}
src/observer/watcher.js
import { popTarget, pushTarget } from './dep';
// id用于标识不同组件不同的watcher
let id = 0;
class Watcher {
constructor(vm, exprOrFn, cb, options) {
this.vm = vm;
this.exprOrFn = exprOrFn;
if (typeof exprOrFn == 'function') {
this.getter = exprOrFn;
}
this.cb = cb;
this.options = options;
// 每创建一个watcher就让id自增
this.id = id++;
// 让watcher记住dep
this.deps = [];
this.depsId = new Set();
this.get();
}
get() {
// 渲染之前,存储当前的watcher,这个watcher里有一个get方法
pushTarget(this); // 给 Dep.target 赋值为当前的 watcher,也就是将watcher放到全局上
this.getter(); // 这个方法会取data中的值,这个方法调用了render函数,会对模板的数据进行取值,触发Object.defineProperty的get方法
// 清除当前watcher,为什么要清除当前的watcher呢,这是因为当我们没有在模板中使用数据,而是在其他地方使用的数据,不需要收集依赖
popTarget(); // Dep.target = null
}
// 当属性取值时,需要记住这个watcher,等数据发生了变化,去执行自己记住的watcher,这个watcher会重新调用_update方法去更新
// 让watcher记住deep
addDep(dep) {
// 不能直接这么写,因为页面中可能多次调用同一个数据, 如:{{name}} {{name}} {{name}},这样当前的watcher就存放了多个相同的deep了,不合理
// this.deps.push(dep)
let id = dep.id;
// 使用set集合去重,过滤重复的dep
if (!this.depsId.has(id)) {
// dep是非重复的,watcher肯定也不会重
this.depsId.add(id);
// 让当前的watcher记住deep
this.deps.push(dep);
// 让dep记住当前watcher
dep.addSub(this);
}
}
update() {
this.get();
}
}
// 在取值之前把watcher暴露到全局上,让所有属性(这个属性必须在模板中使用到)的deep都记住这个watcher,等数据变了,就可以让属性记住的watcher去执行
export default Watcher;
src/observer/dep.js
// 可以把当前的watcher放到一个全局变量上
// id 用于表示dep的唯一性
let id = 0;
class Dep {
constructor() {
this.id = id++;
// 属性记住watcher
this.subs = [];
}
depend() {
// Dep.target指的是当前的watcher
if (Dep.target) {
// 调用watcher的addDep方法实现dep记住watcher,watcher记住dep的功能
Dep.target.addDep(this); // 让watcher,去存放dep,this指代的是当前dep
}
}
notify() {
this.subs.forEach((watcher) => watcher.update());
}
addSub(watcher) {
// 属性deep记住当前的watcher
this.subs.push(watcher);
}
}
Dep.target = null;
export function pushTarget(watcher) {
Dep.target = watcher;
}
export function popTarget() {
Dep.target = null;
}
export default Dep;
# 对象依赖收集
import { arrayMethods } from './array';
import Dep from './dep';
class Observer {
// value 观测的数据
constructor(value) {
// 为了方便数组拿到 observeArray 方法,对新增的每一项进行观测
// 不要这样写,会造成死循环
// value.__ob__ = this
Object.defineProperty(value, '__ob__', {
value: this,
enumerable: false, // 不能被枚举,表示不能被循环
configurable: false // 不能删除此属性
});
// value 可能是对象,可能是数组,需要分类来处理
if (Array.isArray(value)) {
// 数组不用 Object.defineProperty 进行代理,性能不好
// 有些浏览器不支持 __proto__, 所以使用 Object.setPrototypeOf
// value.__proto__ = arrayMethods // 当value是数组时,调用的数组方法为改写后的方法
Object.setPrototypeOf(value, arrayMethods);
// 处理原有数组中元素为对象的情况,将每一项变为响应式的,但是还处理不了后续加入的对象
this.observeArray(value);
} else {
this.walk(value);
}
}
observeArray(value) {
value &&
value.forEach((item) => {
observe(item);
});
}
walk(data) {
// 将对象中所有 key 重新用 Object.defineProperty 定义成响应式的
Object.keys(data).forEach((key) => {
defineReactive(data, key, data[key]);
});
}
}
// 没有放到原型上的原因是因为这个方法不一定是通过实例调用的,如Vue.util.defineReactive
export function defineReactive(data, key, value) {
// value有可能也是一个对象,需要递归遍历,所以vue2中数据尽量不要嵌套过深,会浪费性能
/*
data() {
return {
name: 'f',
obj: {
name: 1,
age: 2
}
}
}
*/
// Object.defineProperty只是重写了对象的get和set,Proxy是给对象设置代理不用改写对象性能高些,两者不一样
observe(value);
let dep = new Dep(); // 每次都会给属性创建deep,也就是每个属性都有一个deep属性,可以去记录当前的watcher
Object.defineProperty(data, key, {
get() {
// 给每个属性增加一个deep,Dep.target指的是当前的watcher
if (Dep.target) {
dep.depend(); // 让当前属性的dep记住当前的watcher,也要让当前的watcher记住这个dep,注意只是让模板中使用到属性记住watcher,模板中没有使用到的属性不用记录watcher,防止无用更新
}
return value;
},
set(newValue) {
// 取值时会打印输出当前dep
console.log(dep);
// 如果新值和旧值相等,则不做任何操作
if (newValue === value) return;
/*
如果用户重重新给数据赋值成新值且这个新值是对象,要将这个对象设置成响应式的,如:
vm.obj = {name:'d'}
*/
observe(newValue);
value = newValue;
// 通知dep中记录的watcher让它去执行,更新页面
dep.notify();
}
});
}
export function observe(data) {
// 只对对象类型进行观测,非对象类型无法观测
if (typeof data !== 'object' || data === null) return;
// 如果对象包含__ob__属性,说明已经被观测过了,可以防止循环引用
if (data.__ob__) return;
// 通过类实现对数据的观测,用类的方便扩展,会产生一个实例作为唯一标识,可以用这个实例来判断data是否被观测了
return new Observer(data);
}
# 批量更新
// src/observer/scheduler.js
import { nextTick } from '../util/next-tick';
let has = {};
let queue = [];
let pending = false;
function flushSchedulerQueue() {
for (let i = 0; i < queue.length; i++) {
// 获取watcher
let watcher = queue[i];
// 执行watcher
watcher.run();
}
queue = [];
has = {};
pending = false;
}
// 多次调用 queueWatcher ,如果watcher不是同一个,会多次调用nextTick,所以也需要加锁
export function queueWatcher(watcher) {
// 更新时对watcher进行去重操作
const id = watcher.id;
if (has[id] == null) {
has[id] = true;
queue.push(watcher);
if (!pending) {
nextTick(flushSchedulerQueue);
pending = true;
}
}
}
// src/observer/watcher.js
import { popTarget, pushTarget } from './dep';
import { queueWatcher } from './scheduler';
// id用于标识不同组件不同的watcher
let id = 0;
class Watcher {
constructor(vm, exprOrFn, cb, options) {
this.vm = vm;
this.exprOrFn = exprOrFn;
if (typeof exprOrFn == 'function') {
this.getter = exprOrFn;
}
this.cb = cb;
this.options = options;
// 每创建一个watcher就让id自增
this.id = id++;
// 让watcher记住dep
this.deps = [];
this.depsId = new Set();
this.get();
}
get() {
// 渲染之前,存储当前的watcher,这个watcher里有一个get方法
pushTarget(this); // 给 Dep.target 赋值为当前的 watcher,也就是将watcher放到全局上
this.getter(); // 这个方法会取data中的值,这个方法调用了render函数,会对模板的数据进行取值,触发Object.defineProperty的get方法
// 清除当前watcher,为什么要清除当前的watcher呢,这是因为当我们没有在模板中使用数据,而是在其他地方使用的数据,不需要收集依赖
popTarget(); // Dep.target = null
}
// 当属性取值时,需要记住这个watcher,等数据发生了变化,去执行自己记住的watcher,这个watcher会重新调用_update方法去更新
// 让watcher记住deep
addDep(dep) {
// 不能直接这么写,因为页面中可能多次调用同一个数据, 如:{{name}} {{name}} {{name}},这样当前的watcher就存放了多个相同的deep了,不合理
// this.deps.push(dep)
let id = dep.id;
// 使用set集合去重,过滤重复的dep
if (!this.depsId.has(id)) {
// dep是非重复的,watcher肯定也不会重
this.depsId.add(id);
// 让当前的watcher记住deep
this.deps.push(dep);
// 让dep记住当前watcher
dep.addSub(this);
}
}
// 如果多次同时更新一个数据,希望合并成一次去更新这个数据
update() {
// this.get();
queueWatcher(this);
}
run() {
this.get()
}
}
// 在取值之前把watcher暴露到全局上,让所有属性(这个属性必须在模板中使用到)的deep都记住这个watcher,等数据变了,就可以让属性记住的watcher去执行
export default Watcher;
# nexTick
let callbacks = [];
let pending = false;
function flushCallbacks() {
callbacks.forEach((cb) => cb());
pending = false;
callbacks = [];
}
let timerFunc;
if (Promise) {
// then方法是异步的
timerFunc = () => {
Promise.resolve().then(flushCallbacks);
};
} else if (MutationObserver) {
// MutationObserver 也是一个异步方法
let observe = new MutationObserver(flushCallbacks); // H5的api
let textNode = document.createTextNode(1);
observe.observe(textNode, {
characterData: true
});
timerFunc = () => {
textNode.textContent = 2;
};
} else if (setImmediate) {
timerFunc = () => {
setImmediate(flushCallbacks);
};
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0);
};
}
// 批处理, 第一次开定时器,后续只更新列表,最后执行清空逻辑
/*
举例: 第一次cb是渲染watcher(渲染watcher执行的过程是同步,更新是异步的),第二次cb是用户传入的回调
*/
export function nextTick(cb) {
callbacks.push(cb); // cb默认是渲染逻辑,将用户的逻辑放到渲染逻辑之后即可
if (!pending) {
pending = true;
timerFunc();
}
}
// src/init.js
import { compileToFunctions } from './compiler/index';
import { mountComponent } from './lifeCycle';
import { initState } from './state';
import { nextTick } from './util/next-tick';
export function initMixin(Vue) {
Vue.prototype._init = function(options) {
const vm = this;
vm.$options = options; // 实例上有个属性$options表示的是用户传入的属性,可以通过vm.$options获取到用户传入的所有属性
// 初始化状态
initState(vm);
// 有el属性的话,说明数据可以挂在到页面上
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
};
Vue.prototype.$nextTick = nextTick
Vue.prototype.$mount = function(el) {
const vm = this;
const options = vm.$options;
el = document.querySelector(el);
vm.$options.el = el;
// 如果有render就直接用render,没有render,看看有没有template属性,如果也没有template属性的话,就直接找外部模板
// 如果没有render方法
if (!options.render) {
let template = options.template;
// 如果没有模板但是有el
if (!template && el) {
template = el.outerHTML;
}
// 将模板编译成render函数
const render = compileToFunctions(template);
options.render = render;
console.log('render', render);
}
mountComponent(vm, el); // 组件挂载
};
}
nextTick的简单使用
<!DOCTYPE html>
<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>
<!--
模板编译原理 通过AST语法树描述出html语法结构 ,在把这个语法转换成JS语法,包上with 生成render方法 ,调用这个方法返回虚拟dom, 再把虚拟dom变成真实dom渲染到页面上。
注意:虚拟dom和ast语法树不一样,虚拟dom是表示dom对象的,ast语法树这里表示html结构
1. 将模板变成一个render函数,
2. 去当前的的实例上取值
3. 虚拟dom(说白了就是一个对象)它的好处是可以描述DOM结构,diff算法,可以比对哪些属性变化了哪些属性没有变化,把需要变化的进行更新
4. 生成真实dom
-->
<div id="app" a=1 b=2 style="color:red;font-size:12px;">
<span style="color:red;">{{name}} aa {{age}} haha<a>hello</a></span>
</div>
<script src="/dist/vue.js"></script>
<script>
const vm = new Vue({
el: '#app',
data() {
return {
name: 'f',
age: 18,
address: ''
}
}
})
// console.clear()
// 修改了数据后,手动调用_update方法更新dom
vm.name = 'dd'
vm._update(vm._render()) // 强制更新的方法,但是我们希望数据变化后可以自动更新视图,而不是手动触发
// 在这里取值,没有依赖模板,所以不需要收集依赖
vm.address
setTimeout(() => {
console.clear()
// 1.测试批量更新
vm.name = '1'
vm.name = '2'
vm.name = '3'
// 2.修改完成后立刻获取真实dom,结果和我们想象的不一样 因为我们更新的操作是异步的
// console.log(vm.$options.el.innerHTML)
// 使用setTimeout来获取更新后的结果
// setTimeout(() => {
// console.log(vm.$options.el.innerHTML)
// })
// 使用nextTick获取更新后的结果
vm.$nextTick(() => {
console.log(vm.$options.el.innerHTML)
})
vm.name = '4'
vm.name = '5'
}, 5000)
// console.log(vm)
/*
1. 默认渲染时会将渲染watcher放到Dep.target上
2. 当我们调用this.getter时就会对属性进行取值操作
3. 让Dep.target值为空,不在模板中访问的值不记录watcher
4. 修改name值,会找到name对应的dep,通知dep中的watcher执行
*/
</script>
</body>
</html>
# 数组的依赖收集
Vue的更新逻辑 通过nextTick异步执行更新视图逻辑。
默认当我们在模板中去取数组值的时候,需要让这个数组去收集依赖,数组怎么去收集依赖呢,就是给数组增加一个dep,如果我们取值了,就让这个数组通过这个dep记住这个依赖,当数组发生变化的时候,就调用notify去更新视图。
vue中的watcher分为渲染watcher、computed watcher和用户watcher。
<!DOCTYPE html>
<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>
<!--
模板编译原理 通过AST语法树描述出html语法结构 ,在把这个语法转换成JS语法,包上with 生成render方法 ,调用这个方法返回虚拟dom, 再把虚拟dom变成真实dom渲染到页面上。
注意:虚拟dom和ast语法树不一样,虚拟dom是表示dom对象的,ast语法树这里表示html结构
1. 将模板变成一个render函数,
2. 去当前的的实例上取值
3. 虚拟dom(说白了就是一个对象)它的好处是可以描述DOM结构,diff算法,可以比对哪些属性变化了哪些属性没有变化,把需要变化的进行更新
4. 生成真实dom
-->
<div id="app" a=1 b=2 style="color:red;font-size:12px;">
{{arr1}}
</div>
<!-- {{arr2[0].name}}{{arr2}} -->
<script src="/dist/vue.js"></script>
<script>
const vm = new Vue({
el: '#app',
data() {
return {
// 情况一 内部会给arr1这个属性增加依赖,但是我们更改数组时不会去通知更新
// arr1: [1, 2, 3],
// 情况二 外层数组进行了依赖收集,但是里层数组还没有进行依赖收集
arr1:[[1, 2, 3]]
// arr2: [{name: 'f'}]
}
}
})
setTimeout(() => {
// vm.arr1.push(4)
vm.arr1[0].push(4)
// _s(xx)里使用了JSON.stringify,所以改变页面会变化
// vm.arr2[0].name = 'dd'
}, 1000);
</script>
</body>
</html>
// src/observer/index.js
import { arrayMethods } from './array';
import Dep from './dep';
class Observer {
// value 观测的数据
constructor(value) {
this.dep = new Dep(); // 给数组本身和对象本身增加一个dep属性
// 为了方便数组拿到 observeArray 方法,对新增的每一项进行观测
// 不要这样写,会造成死循环
// value.__ob__ = this
Object.defineProperty(value, '__ob__', {
value: this,
enumerable: false, // 不能被枚举,表示不能被循环
configurable: false // 不能删除此属性
});
// value 可能是对象,可能是数组,需要分类来处理
if (Array.isArray(value)) {
// 数组不用 Object.defineProperty 进行代理,性能不好
// 有些浏览器不支持 __proto__, 所以使用 Object.setPrototypeOf
// value.__proto__ = arrayMethods // 当value是数组时,调用的数组方法为改写后的方法
Object.setPrototypeOf(value, arrayMethods);
// 处理原有数组中元素为对象的情况,将每一项变为响应式的,但是还处理不了后续加入的对象
this.observeArray(value);
} else {
this.walk(value);
}
}
// 处理数组中的元素是对象的情况,注意:数组也是对象,就会递归观测,观测的时候回增加__ob__属性 Object.defineProperty(value, '__ob__', {...}
observeArray(value) {
value &&
value.forEach((item) => {
observe(item);
});
}
walk(data) {
// 将对象中所有 key 重新用 Object.defineProperty 定义成响应式的
Object.keys(data).forEach((key) => {
defineReactive(data, key, data[key]);
});
}
}
// value指代的是数组
// 让里层数组收集外层数组的依赖(因为收集的都是同一个watcher),这样修改里层数组也可以更新视图
function dependArray(value) {
for (let i = 0; i < value.length; i++) {
let current = value[i];
current.__ob__ && current.__ob__.dep.depend(); // 让里层的数组和外层的数组收集的都是同一个watcher
if (Array.isArray(current)) {
dependArray(current);
}
}
}
// 没有放到原型上的原因是因为这个方法不一定是通过实例调用的,如Vue.util.defineReactive
export function defineReactive(data, key, value) {
// value有可能也是一个对象,需要递归遍历,所以vue2中数据尽量不要嵌套过深,会浪费性能
/*
data() {
return {
name: 'f',
obj: {
name: 1,
age: 2
}
}
}
*/
// Object.defineProperty只是重写了对象的get和set,Proxy是给对象设置代理不用改写对象性能高些,两者不一样
// 相当于给数组本身添加了一个dep属性,让数组通过childOb.dep.depend()收集了watcher,等到数组中的内容发生变化去通知watcher更新
let childOb = observe(value);
// 在这个例子中,这个dep是给数组加的,仅限于这个例子
console.log('childOb.dep', childOb.dep); // Dep {id: 1, subs: Array(0)}
let dep = new Dep(); // 每次都会给属性创建deep,也就是每个属性都有一个deep属性,可以去记录当前的watcher
Object.defineProperty(data, key, {
get() {
// 给每个属性增加一个deep,Dep.target指的是当前的watcher
if (Dep.target) {
dep.depend(); // 让当前属性的dep记住当前的watcher,也要让当前的watcher记住这个dep,注意只是让模板中使用到属性记住watcher,模板中没有使用到的属性不用记录watcher,防止无用更新
// childOb 可能是对象也可能是数组,比如我们给对象新增了一个属性,需要出发对象的更新,如我们有对象 {a:1} ,我们使用$set(obj, b, 2)给这个对象新增了属性
if (childOb) {
// 如果对数组取值,会将当前的watcher和数组进行关联
childOb.dep.depend();
if (Array.isArray(value)) {
// 如果内部还是数组
dependArray(value); // 不停的进行依赖收集
}
}
}
return value;
},
set(newValue) {
// 取值时会打印输出当前dep
console.log(dep);
// 如果新值和旧值相等,则不做任何操作
if (newValue === value) return;
/*
如果用户重重新给数据赋值成新值且这个新值是对象,要将这个对象设置成响应式的,如:
vm.obj = {name:'d'}
*/
observe(newValue);
value = newValue;
// 通知dep中记录的watcher让它去执行,更新页面
dep.notify();
}
});
}
export function observe(data) {
// 只对对象类型进行观测,非对象类型无法观测
if (typeof data !== 'object' || data === null) return;
// 如果对象包含__ob__属性,说明已经被观测过了,可以防止循环引用
if (data.__ob__) return;
// 通过类实现对数据的观测,用类的方便扩展,会产生一个实例作为唯一标识,可以用这个实例来判断data是否被观测了
return new Observer(data);
}
// src/observer/array.js
// 注意不能直接改写数组原有方法,只需要改写被 vue 控制的数组,因为代码中的数组有可能没被data直接使用
const oldArrayProtoMethods = Array.prototype;
export let arrayMethods = Object.create(oldArrayProtoMethods);
// 下面这些方法会改变原数组
let methods = [ 'push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice' ];
methods.forEach((method) => {
// 重写数组方法
arrayMethods[method] = function(...args) {
// 调用数组原来方法
const result = oldArrayProtoMethods[method].apply(this, args);
const ob = this.__ob__;
// 有可能用户新增的数据是对象,这个时候需要做特殊处理
// inserted 保存插入的值
let inserted;
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
inserted = args.slice(2);
default:
break;
}
if (inserted) ob.observeArray(inserted); // 对新增的每一项进行观测
// 数组变化后通知watcher更新
ob.dep.notify()
return result;
};
});
← 创建渲染Watcher 生命周期合并 →