Skip to content

Vue源码解析(一)实现一个双向绑定 #13

@jmx164491960

Description

@jmx164491960

前言

双向绑定是Vue这类MVVM框架最基础的功能,Vue使用了劫持Object.defineProperty属性的方法去实现,本文将用一个简单版的双向绑定去解读Vue的源码。

简单版的源码放在我的仓库,目录:/example/双向绑定

解析

Object.defineProperty劫持

根据传入对象,属性,对get和set方法劫持。当

  • 进行数据取值,收集依赖(dep.depend())
  • 进行数据赋值,触发更新(dep.notify())

下面会详细讲解dep

function defineReactive(obj, key, val) {

    let dep = new Dep();
    dep.obj = obj;
    dep.key = key;

    const property = Object.getOwnPropertyDescriptor(obj, key)
    if (property && property.configurable === false) {
        return
    }

    // cater for pre-defined getter/setters
    const getter = property && property.get;
    const setter = property && property.set;
    if ((!getter || setter) && arguments.length === 2) {
        val = obj[key]
    }
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function() {
            const value = getter ? getter.call(obj) : val
            if (Dep.target) {
                dep.depend()
            }
            return value;
        },
        set: function(newVal) {
            const value = getter ? getter.call(obj) : val
            if (newVal === value || (newVal !== newVal && value !== value)) {
                return
            }
            if (setter) {
                setter.call(obj, newVal);
            } else {
                val = newVal;
            }
            dep.notify()
        }
    });
}

Dep和Watcher

Vue为了实现双向绑定,其中有两个类扮演什么重要的角色:

  1. Dep

Dep负责管理Vue实例里每个属性的依赖关系。方法depend负责收集依赖,把目标节点的渲染所依赖的属性关联起来;方法notify负责通知依赖,当某个属性变更,通知该属性关联的所有视图执行更新

class Dep {
    subs = [];
    notify() {
        this.subs.forEach((sub) => {
            sub.update(this.obj, this.key);
        });
    }
    depend() {
        if (Dep.target) {
            Dep.target.addDep(this)
        }
    }

    addSub (sub) {
        this.subs.push(sub)
    }
}
Dep.target = undefined;
  1. Watcher

addDep方法用于把Wacher实例watcher和Dep实例dep关联起来,使得dep可以通过notify()通知对应的watcher更新视图;

每当Watcher初始化的时候,主要做了两个事:

定义update方法,update方法用于更新视图。
触发get方法,把Dep.target设置为undefined,结束掉依赖的收集

class Watcher {
    node = null;
    constructor(getTpl, parentNode, vm) {
        this.get(this);
        this.node = document.createElement('div');
        parentNode.append(this.node);
        this.update = (obj, key) => {
            const node  = parseDom(getTpl.call(vm));
            node.addEventListener('change', (event) => {
                debugger
                obj[key] = event.target.value;
            })
            parentNode.replaceChild(node, this.node);
            this.node = node;
        }
        this.update();
        this.get();
    }

    get(target) {
        Dep.target = target;
    }

    update() {

    }

    addDep(dep) {
        dep.addSub(this);
    }
}

这里有点需要强调的是收集依赖的depend函数。简单版里面执行过程比较简单,而Vue里面实际是很绕的

// watcher.js
Watcher {
 addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }
}

Dep {
    depend() {
        Dep.target.addDep(this);
    }
    addSub (sub: Watcher) {
        this.subs.push(sub)
    }
}

Dep.target是一个全局对象,在每个Watcher实例的构造函数里开始时设置,结束时删除。当触发了Wacher的构造函数,

  1. 会先设置Dep.target
  2. 执行depend,此时会把调用depend的对象dep作为参数,执行Dep.target.addDep。然后该方法内部判断depId是否重复,假如重复,就放弃这个依赖

渲染时创建Watcher

上面提到了,Wacher构造函数会收集依赖,那么什么时候执行Watcher的构造函数的?在视图的渲染的时候,即render函数。(无论我们写的是Template,还是函数,Vue最终都会转化为render函数),而render在Vue实例创造的时候执行

class Vue {
    rootNode = null;
    data = {};
    constructor(params) {
        if (params.el) {
            this.rootNode = document.querySelector(params.el);
        }

        if (params.data) {
            this.data = params.data();

            Object.keys(this.data).forEach(key => {
                defineReactive(this.data, key, this.data[key]);
            });
        }
        this.render(params.render);

        return this;
    }
    
    render(getTpl) {
        const watcher = new Watcher(getTpl, this.rootNode, this);
        watcherList.push(watcher);
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions