从零开始的Vue世界-03

323 阅读5分钟

前两章已经介绍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