Vue2源码之渲染原理和异步更新(二)

1.渲染原理

  • 1.将更新的功能封装为一个Watcher的类;
  • 2.渲染页面前,会将当前watcher放到Dep类上;
  • 3.在Vue中页面渲染时使用的属性,需要进行依赖收集,收集对象的渲染watcher
  • 4.取值时,给每个属性都添加了个dep属性,用于存储这个渲染watcher「同一个watcher会对应多个dep」
  • 5.每个属性可能对应多个视图「多个视图肯定时多个watcher」,一个属性要对应多个watcher
  • 6.dep.depend()->通知dep存放watcher->Dep.target.addDep->通知watcher存放dep

1.1.测试实例

在2s后用户改变name的值,使试图自动刷新功能实现

<body>
    <div id="app" style="color:red;background:green">hello {{name}} world</div>
    <script src="./dist/vue.js"></script>
    <script>
        const vm=new Vue({
            el:'#app',
            data:{
                name:'zhangsan'
            }
        })
        setTimeout(() => {
            vm.name='lisi';
        }, 2000);
    </script>
</body>
复制代码

1.2.lifecycle.js

import Watcher from "./observer/watcher"; 
import {
    patch
} from "./vdom/patch";

export function lifecycleMixin(Vue) {
    Vue.prototype._update = function (vnode) {
        const vm = this;
        //需要给vm.$el赋值为新的虚拟DOM
        vm.$el = patch(vm.$el, vnode);
    }
}
/**
 * 组件挂载
 * @param {*} vm 
 * @param {*} el <div id='app'></div>
 */
export function mountComponent(vm, el) {
    /**
     * TODO:更新函数
     * 1.调用_render生成vdom
     * 2.调用_update进行更新操作
     */
    const updateComponent = () => {
        vm._update(vm._render());
    }

    //true代表渲染watcher
    const watcher=new Watcher(vm, updateComponent, () => {
        console.log('更新试图')
    }, true);
}
复制代码

1.3.observer/watcher.js

每个组件拥有一个渲染watcher

import {
    popTarget,
    pushTarget
} from "./dep";
let id = 0;
class Watcher {
    constructor(vm, exprOrFn, cb, options) {
        this.vm = vm;
        this.exprOrFn = exprOrFn;
        this.cb = cb;
        this.options = options;
        this.id = id++;

        this.getter = exprOrFn;
        this.deps = [];
        this.depsId = new Set();

        this.get(); //默认初始化要执行一次
    }

    get() {
        //Dep.target=watcher
        pushTarget(this);
        this.getter(); //TODO:this.getter->render()执行,vm取值,会在get
        popTarget();
    }

    update() {
        this.get();
    }
    addDep(dep) {
        //防止同一个watcher里存多个相同的dep
        if (!this.depsId.has(dep.id)) {
            this.depsId.add(dep.id);
            this.deps.push(dep);
            dep.addSub(this); //dep关联watcher
        }
    }
}
export default Watcher;
复制代码

1.4.observer/dep.js

  • 1.每个属性都拥有一个dep
  • 2.dep中存放多个watcher
let id = 0;
class Dep {
    constructor() {
        this.id = id++;
        this.subs = []; //存放watcher的
    }
    depend() {
        if (Dep.target) {
            //让watcher标记dep
            Dep.target.addDep(this);
        }
    }
    
    addSub(watcher) {
        this.subs.push(watcher);
    }
    notify(){
        this.subs.forEach(watcher=>watcher.update());
    }
}

Dep.target = null;

export function pushTarget(watcher) {
    Dep.target = watcher;
}

export function popTarget() {
    Dep.target = null;
}

export default Dep;
复制代码

1.5.observer/index.js

  • 每个属性拥有一个自己的dep
  • get时,将属性dep与watcher建立关系
  • set使,通知dep中存放的watcher更新
function defineReactive(data, key, value) {
    observe(value); //TODO:如果value是一个对象,需要对value进行深层次的劫持操作
    //TODO:每个属性都拥有一个dep属性
    const dep = new Dep();
    Object.defineProperty(data, key, {
        get() {
            if (Dep.target) {//render时,回来get中取值,在此处将dep与watcher建立双向关联关系
                dep.depend(); //让dep记住watcher
            }
            return value;
        },
        set(newVal) {
            if (newVal === value) return;
            observe(newVal); //TODO:重新设置的值可能是一个对象,这个时候需要重新对其进行劫持处理
            value = newVal;
            dep.notify(); //修改值时,通知当前的属性存放的watcher执行
        }
    })
}
复制代码

2.异步更新

<body>
    <div id="app" style="color:red;background:green">hello {{name}} world</div>
    <script src="./dist/vue.js"></script>
    <script>
        const vm=new Vue({
            el:'#app',
            data:{
                name:'zhangsan'
            }
        })
        setTimeout(() => {
            vm.name='lisi';
            vm.name='111';
            vm.name='2222';
            vm.name='333';
            console.log(vm.$el)
            vm.$nextTick(()=>{
                console.log(vm.$el);
            })
        }, 2000);
    </script>
</body>
复制代码

2.1.observer/watcher.js

update() {
    //多次调用update,希望先将watcher缓存,一起更新
    queueWatcher(this);
}
run() {
    console.log('run')
    this.get();
}
复制代码

2.2.observer/scheduler.js

import {
    nextTick
} from "../utils";
let queues = [];
let has = {};
let pending = false;

function flushSchedulerQueue() {
    for (let i = 0; i < queues.length; i++) {
        queues[i].run(); //更新视图
    }
    queues = [];
    has = {};
    pending = false;
}

export function queueWatcher(watcher) {
    const id = watcher.id;
    if (has[id] == null) { //去重
        has[id] = true;
        queues.push(watcher);
        if (!pending) { //防抖
            nextTick(flushSchedulerQueue);
            pending = true;
        }
    }
}
复制代码

2.3.lifecycle.js

export function lifecycleMixin(Vue) {
    Vue.prototype._update = function (vnode) {
        const vm = this;
        //需要给vm.$el赋值为新的虚拟DOM
        vm.$el = patch(vm.$el, vnode);
    }
    //用户自己调用nextTick时
    Vue.prototype.$nextTick = function (cb) {
        nextTick(cb);
    }
}
复制代码

2.4.utils.js

const callbacks = [];
let waiting = false;

function flushCallbacks() {
    callbacks.forEach(cb => cb());
    waiting = false
}

function timer(flushCallbacks) {
    let timerFn = () => {};
    if (Promise) { //是否支持Promise
        timerFn = () => {
            Promise.resolve().then(flushCallbacks);
        }
    } else if (MutationObserver) { //是否支持文本变化,微任务
        let textNode = document.createTextNode(0);
        //监听文本变化
        const observe = new MutationObserver(flushCallbacks);
        observe.observe(textNode, {
            characterData: true
        })
        timerFn = () => {
            textNode.textContent = 1;
        }
    } else if (setImmediate) { //只有IE支持
        timerFn = () => {
            setImmediate(flushCallbacks);
        }
    } else {
        timerFn = () => {
            setTimeout(flushCallbacks);
        }
    }
    timerFn();
}
export function nextTick(cb) {
    /**
     * TODO:
     * 1.属性赋值的nextTick
     * 2.用户调用vm.$nextTick
     */
    callbacks.push(cb);
    if (!waiting) {
        //vue2中考虑了兼容问题,vu3中直接Promise.resolve().then()
        timer(flushCallbacks);
        waiting = true;
    }
}
复制代码

3.数组更新原理

  • 1.Vue中嵌套层次不能太深,否则会有大量递归
  • 2.Vue中对象通过的是defineProperty实现的响应式,拦截了get和set。如果不存在的属性不会拦截,也不会相应。可以使用$set让对象自己notify,或者赋予一个新对象
  • 3.Vue中的数组改索引和长度是不会影响更新的,通过变异7种方法可以更新视图,数组中如果是对象类型,修改对象也可以更新视图

3.1.一维数组

3.1.1.测试
<body>
    <div id="app" style="color:red;background:green">{{arr}}</div>
    <script src="./dist/vue.js"></script>
    <script>
        const vm=new Vue({
            el:'#app',
            data:{
                arr:[1,2,3]
            }
        })
        setTimeout(() => {
           vm.arr.push(4);
        }, 2000);
    </script>
</body>
复制代码
3.1.2.observer/index.js
class Observer {
    constructor(data) {
        //TODO:给外层数据添加dep属性
        this.dep = new Dep();
    }
}

function defineReactive(data, key, value) {
    const childOb = observe(value); //TODO:如果value是一个对象,需要对value进行深层次的劫持操作
    Object.defineProperty(data, key, {
        get() {
            if (Dep.target) {
                dep.depend(); //让dep记住watcher
                /**
                 * TODO:childOb
                 * 1.数组情况,假如:value是arr时,childOb=new Observe()
                 * 2.对象情况,对象是无法对新增属性进行收集的,后续可以通过$set实现
                 */
                if (childOb) {
                    childOb.dep.depend(); //数组/对象记录watcher
                }
            }
            return value;
        }
    })
}

export function observe(data) {
    //TODO:data必须是一个对象,默认最外层必须是一个对象
    if (!isObject(data)) return;
    //如果观察的数据已经有了__ob__属性,说明这个数据已经被劫持过了,不用再劫持
    if (data.__ob__) return data.__ob__;
    return new Observer(data);
}
复制代码
3.1.3.observer/array.js
methods.forEach(method => {
    //用户调用的如果是上面的7种方法,会先走自己重新的方法
    arrayMethods[method] = function (...args) {
        //新增的数据需要对其进行劫持 「this.__ob__是Observer实例」
        if (inserted) this.__ob__.observeArray(inserted);
        //TODO:数组在调用7种方法时,通过外层的dep通知视图更新
        this.__ob__.dep.notify();
    }
})
复制代码

3.2.多维数组

3.2.1.测试
<body>
    <div id="app" style="color:red;background:green">{{arr}}</div>
    <script src="./dist/vue.js"></script>
    <script>
        const vm=new Vue({
            el:'#app',
            data:{
                arr:[[1,2,3]]
            }
        })
        setTimeout(() => {
           vm.arr[0].push(4);
        }, 2000);
    </script>
</body>
复制代码
3.2.2.observer/index.js
function dependArray(value) {
    for (let i = 0; i < value.length; i++) {
        const current = value[i];
        current.__ob__ && current.__ob__.dep.depend();
        if (Array.isArray(current)) {
            dependArray(current);
        }
    }
}

/**
 * TODO:Vue2为什么性能不好,主要原因就是数据的劫持的全量劫持
 * @param {*} data 原数据
 * @param {*} key key
 * @param {*} value 值
 */
function defineReactive(data, key, value) {
    const childOb = observe(value); //TODO:如果value是一个对象,需要对value进行深层次的劫持操作
    //TODO:每个属性都拥有一个dep属性
    const dep = new Dep();
    Object.defineProperty(data, key, {
        get() {
            if (Dep.target) {
                dep.depend(); //让dep记住watcher
                /**
                 * TODO:childOb
                 * 1.数组情况,假如:value是arr时,childOb=new Observe()
                 * 2.对象情况,对象是无法对新增属性进行收集的,后续可以通过$set实现
                 */
                if (childOb) {
                    childOb.dep.depend(); //数组/对象记录watcher
                    if (Array.isArray(value)) {
                        dependArray(value);
                    }
                }
            }
            return value;
        }
}
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享