前两章已经介绍vue中通过_init()进行了数据劫持,在$mount将模板编译为ast树然后生成render函数,执行mountedComponent,通过render函数生出虚拟dom,最后通过updata中的patch生成新的dom,本章来理解数据劫持的使用观察者模式
核心思想
使用观察者模式,watcher作为视图(组件),当使用的data更新后自动更新,创建一个dep,每个data在第一次render的时候,会调用data的get方法(在数据劫持时候new Dep()),将对应watcher存在dep上,当数据改变set时,调用该dep存放watcher调用他的更新方法。
执行执行mountedComponent
init.js
Vue.prototype.$mount = function (el) {
const vm = this;
el = document.querySelector(el);
let ops = vm.$options
if (!ops.render) { // 先进行查找有没有render函数
let template; // 没有render看一下是否写了tempate, 没写template采用外部的template
if (!ops.template && el) { // 没有写模板 但是写了el
template = el.outerHTML
}else{
if(el){
template = ops.template // 如果有el 则采用模板的内容
}
}
// 写了temlate 就用 写了的template
if(template && el){
// 这里需要对模板进行编译
const render = compileToFunction(template);
ops.render = render; // render挂在到vm.$options上
}
}
mountComponent(vm,el); // 组件的挂载
}
export function mountComponent(vm,el){ // 这里的el 是通过querySelector处理过的
vm.$el = el;
// 1.调用render方法产生虚拟节点 虚拟DOM
const updateComponent = ()=>{
vm._update(vm._render()); // vm.$options.render() 虚拟节点
// 这里可以直接使用_update和_render是通过下面代码文件实现的
}
const watcher = new Watcher(vm,updateComponent,true); // true用于标识是一个渲染watcher
console.log(watcher);
}
index.js
import { initMixin } from "./init";
import { initLifeCycle } from "./lifecycle";
// 将所有的方法都耦合在一起
function Vue(options) { // options就是用户的选项
this._init(options); // 默认就调用了init
}
initMixin(Vue); // 扩展了init方法
initLifeCycle(Vue); // 这里将render和update挂在了Vue的原型上,可以直接用了
lifecycle.js
export function initLifeCycle(Vue){
Vue.prototype._update = function(vnode){ // 将vnode转化成真实dom
const vm = this;
const el = vm.$el;
// patch既有初始化的功能 又有更新
vm.$el = patch(el,vnode);
}
// _c('div',{},...children)
Vue.prototype._c = function(){
return createElementVNode(this,...arguments)
}
// _v(text)
Vue.prototype._v = function(){
return createTextVNode(this,...arguments)
}
Vue.prototype._s = function(value){
if(typeof value !== 'object') return value
return JSON.stringify(value)
}
Vue.prototype._render = function(){
// 当渲染的时候会去实例中取值,我们就可以将属性和视图绑定在一起
return this.$options.render.call(this); // 通过ast语法转义后生成的render方法
}
}
在mountComponent函数中我们看到他将渲染封装了一个函数,传递给了watcher
const updateComponent = ()=>{
vm._update(vm._render()); // vm.$options.render() 虚拟节点
}
const watcher = new Watcher(vm,updateComponent,true); // true用于标识是一个渲染watcher
Watcher
Watcher是一个观察者,当data数据变化时,将传递进来的更新函数执行,这时就需要一个依赖收集者Dep来通知watchder执行重新渲染函数 watcher.js
import Dep from "./dep";
let id = 0;
// 1) 当我们创建渲染watcher的时候我们会把当前的渲染watcher放到Dep.target上
// 2) 调用_render() 会取值 走到get上
// 每个属性有一个dep (属性就是被观察者) , watcher就是观察者(属性变化了会通知观察者来更新) -》 观察者模式
class Watcher { // 不同组件有不同的watcher 目前只有一个 渲染根实例的
constructor(vm, fn, options) {
this.id = id++;
this.renderWatcher = options; // 是一个渲染watcher
this.getter = fn; // getter意味着调用这个函数可以发生取值操作
this.deps = []; // 后续我们实现计算属性,和一些清理工作需要用到
this.depsId = new Set();
this.get();
}
get() {
Dep.target = this; // 静态属性就是只有一份
this.getter(); // 会去vm上取值 vm._update(vm._render) 取name 和age
Dep.target = null; // 渲染完毕后就清空
},
addDep(dep) { // 一个组件 对应着多个属性 重复的属性也不用记录
let id = dep.id;
this.deps.push(dep);
this.depsId.add(id);
dep.addSub(this); // watcher已经记住了dep了而且去重了,此时让dep也记住watcher
},
update() {
queueWatcher(this); // 把当前的watcher 暂存起来
// this.get(); // 重新渲染
}
run() {
this.get(); // 渲染的时候用的是最新的vm来渲染的
}
}
// 需要给每个属性增加一个dep, 目的就是收集watcher
// 一个组件中 有多少个属性 (n个属性会对应一个视图) n个dep对应一个watcher
// 1个属性 对应着多个组件 1个dep对应多个watcher
// 多对多的关系
export default Watcher
这里会执行data的get()方法,我们先看下Dep
Dep
let id = 0;
class Dep{
constructor(){
this.id = id++; // 属性的dep要收集watcher
this.subs = [];// 这里存放着当前属性对应的watcher有哪些
}
depend(){
// 这里我们不希望放重复的watcher,而且刚才只是一个单向的关系 dep -> watcher
// watcher 记录dep
// this.subs.push(Dep.target);
Dep.target.addDep(this); // 让watcher记住dep
// dep 和 watcher是一个多对多的关系 (一个属性可以在多个组件中使用 dep -> 多个watcher)
// 一个组件中由多个属性组成 (一个watcher 对应多个dep)
}
addSub(watcher){
this.subs.push(watcher)
}
notify(){
this.subs.forEach(watcher=>watcher.update()); // 告诉watcher要更新了
}
}
Dep.target = null;
export default Dep;
如果调用depend方法,就会将当前watcher存入dep的数组中,在new watcher时会触发get,在get中进行如下操作
export function defineReactive(target,key,value){ // 闭包 属性劫持
observe(value); // 对所有的对象都进行属性劫持
let dep = new Dep(); // 每一个属性都有一个dep
Object.defineProperty(target,key,{
get(){ // 取值的时候 会执行get
if(Dep.target){
dep.depend(); // 让这个属性的收集器记住当前的watcher
}
return value
},
set(newValue){ // 修改的时候 会执行set
if(newValue === value) return
observe(newValue)
value = newValue
dep.notify(); // 通知更新
}
})
}
这样就记住了watcher,当属性更新时,执行对应dep的notify,依次dep的watcher数组中的watcher的更新渲染方法就能实现更新。
异步更新-渲染的优化
上述操作后已经,当数据更新后会自动执行render函数进行更新,但有问题,1.多次使用data中的一个属性,会执行多次get()方法,就会将同一个watcher push多次到dep数组中,指定渲染也会多次,可是只需要push就够了2.组件使用了很多不同属性,不同的dep就将相同的watcher push到数组中执行了多次渲染,也是只需要一次就够了。
同一个属性多次push的优化
watcher.js
addDep(dep) { // 一个组件 对应着多个属性 重复的属性也不用记录
let id = dep.id;
if (!this.depsId.has(id)) {
this.deps.push(dep);
this.depsId.add(id);
dep.addSub(this); // watcher已经记住了dep了而且去重了,此时让dep也记住watcher
}
}
不同属性同一watcher的优化
维护一个数组,当执行渲染更新的时候将执行过滤相同id的watcher push进数组中,做一个异步执行之后在依次将watcher数组中的watcher依次执行渲染函数
let queue = [];
let has = {};
let pending = false; // 防抖
function flushSchedulerQueue() {
let flushQueue = queue.slice(0);
queue = [];
has = {};
pending = false;
flushQueue.forEach(q => q.run()); // 在刷新的过程中可能还有新的watcher,重新放到queue中
}
function queueWatcher(watcher) {
const id = watcher.id;
// 执行过滤操作
if (!has[id]) {
queue.push(watcher);
has[id] = true;
// 不管我们的update执行多少次 ,但是最终只执行一轮刷新操作
if (!pending) {
nextTick(flushSchedulerQueue, 0)
pending = true;
}
}
}
let callbacks = [];
let waiting = false;
function flushCallbacks() {
let cbs = callbacks.slice(0);
waiting = false;;
callbacks = [];
cbs.forEach(cb => cb()); // 按照顺序依次执行
}
export function nextTick(cb) { // 先内部还是先用户的?
callbacks.push(cb); // 维护nextTick中的cakllback方法
if (!waiting) {
// timerFunc()
Promise.resolve().then(flushCallbacks)
waiting = true
}
}
上面引申出一个$nextTick
vm.a=1
app.innerHTML
这样不会取到更新后的dom,引为里面是一个异步循环,这时就统一了一个$nextTick来获取更新后的dom 可以左到谁在前面谁先执行的效果
let callbacks = [];
let waiting = false;
function flushCallbacks() {
let cbs = callbacks.slice(0);
waiting = false;;
callbacks = [];
cbs.forEach(cb => cb()); // 按照顺序依次执行
}
export function nextTick(cb) { // 先内部还是先用户的?
callbacks.push(cb); // 维护nextTick中的cakllback方法
if (!waiting) {
// timerFunc()
Promise.resolve().then(flushCallbacks)
waiting = true
}
}
nextTick不是创建了一个异步任务,而是将这个任务维护到了队列中而已 内部使用了优雅降级的方法,promise->....->settimeout