vue2源码简单实现stage4

上一节我们已经实现了diff和比较两个vnode之后手动更新视图,这一节我们除了要给数据进行proxy劫持之外,还要给数据添加watcher,监听数据改变时更新视图。下面是响应式原理的图解:

;(function () {
  function defineReactive (obj, key, val) {
    var dep = new Dep(); // 每首歌数据添加一个dep对象
    Object.defineProperty(obj, key, {
      get: function () {
      //注意的是在数据初始化阶段,依赖并没有收集,mount时候将数据的watcher添加到dep队列里面
        if (Dep.target) { 
          Dep.target.addDep(dep);
        }
        return val
      },
      set: function (newVal) {
        if (newVal === val) return;
        val = newVal;
        dep.notify();
      }
    })
  }

  function observe (obj) {
    for (var key in obj) {
      defineReactive(obj, key, obj[key])
    }
  }

  var uid$1 = 0;

  function Dep () {
    this.subs = [];
    this.id = uid$1++;
  }

  Dep.target = null;

  Dep.prototype.addSub = function (sub) {
    this.subs.push(sub)
  }

  Dep.prototype.notify = function () {
    var subs = this.subs;
    for (var i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }

  function Watcher (vm, expOrFn, cb) {
    this.vm = vm;
    this.getter = expOrFn;
    this.cb = cb;
    this.depIds = [];
    this.value = this.get();
  }

  Watcher.prototype.get = function () {
   //将watcher赋值给Dep.target一个全局变量,这是为了在依赖收集阶段做的准备。
    Dep.target = this; /* ! */
    var value = this.getter.call(this.vm);
    Dep.target = null;
    return value
  }

  Watcher.prototype.update = function () {
    var value = this.get();
    if (this.value !== value) {
      var oldValue = this.value;
      this.value = value;
      this.cb.call(this.vm, value, oldValue);
    }
  }

  Watcher.prototype.addDep = function (dep) {
    var id = dep.id;
    // to avoid depending the watcher to the same dep more than once
    if (this.depIds.indexOf(id) === -1) {
      this.depIds.push(id);
      dep.addSub(this);
    }
  }

  function vnode (tag, data, children, text, elm) {
    this.tag = tag;
    this.data = data;
    this.children = children;
    this.text = text;
    this.elm = elm;
  }

  function normalizeChildren (children) {
    if (typeof children === 'string') {
      return [createTextVNode(children)]
    }
    return children
  }

  function createTextVNode (val) {
    return new vnode(undefined, undefined, undefined, String(val))
  }

  function createElement (tag, data, children) {
    return new vnode(tag, data, normalizeChildren(children), undefined, undefined);
  }

  function createElm (vnode) {
    var tag = vnode.tag;
    var data = vnode.data;
    var children = vnode.children;

    if (tag !== undefined) {
      vnode.elm = document.createElement(tag);

      if (data.attrs !== undefined) {
        var attrs = data.attrs;
        for (var key in attrs) {
          vnode.elm.setAttribute(key, attrs[key])
        }
      }

      if (children) {
        createChildren(vnode, children)
      }
    } else {
      vnode.elm = document.createTextNode(vnode.text);
    }

    return vnode.elm;
  }

  function createChildren (vnode, children) {
    for (var i = 0; i < children.length; ++i) {
      vnode.elm.appendChild(createElm(children[i]));
    }
  }

  function sameVnode (vnode1, vnode2) {
    return vnode1.tag === vnode2.tag
  }

  function emptyNodeAt (elm) {
    return new vnode(elm.tagName.toLowerCase(), {}, [], undefined, elm)
  }

  function patchVnode (oldVnode, vnode) {
    var elm = vnode.elm = oldVnode.elm;
    var oldCh = oldVnode.children;
    var ch = vnode.children;

    if (!vnode.text) {
      if (oldCh && ch) {
        updateChildren(oldCh, ch);
      }
    } else if (oldVnode.text !== vnode.text) {
      elm.textContent = vnode.text;
    }
  }

  function updateChildren (oldCh, newCh) {
    // assume that every element node has only one child to simplify our diff algorithm
    if (sameVnode(oldCh[0], newCh[0])) {
      patchVnode(oldCh[0], newCh[0])
    } else {
      patch(oldCh[0], newCh[0])
    }
  }

  function patch (oldVnode, vnode) {
    var isRealElement = oldVnode.nodeType !== undefined; // virtual node has no `nodeType` property
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
      patchVnode(oldVnode, vnode);
    } else {
      if (isRealElement) {
        oldVnode = emptyNodeAt(oldVnode);
      }
      var elm = oldVnode.elm;
      var parent = elm.parentNode;

      createElm(vnode);

      parent.insertBefore(vnode.elm, elm);
      parent.removeChild(elm);
    }

    return vnode.elm
  }

  function initData (vm) {
    var data = vm.$data = vm.$options.data;
    var keys = Object.keys(data);
    var i = keys.length
    // proxy data so you can use `this.key` directly other than `this.$data.key`
    while (i--) {
      proxy(vm, keys[i])
    }
    // 初始化的时候给每个数据添加一个dep对象
    observe(data)
  }

  function proxy (vm, key) {
    Object.defineProperty(vm, key, {
      configurable: true,
      enumerable: true,
      get: function () {
        return vm.$data[key]
      },
      set: function (val) {
        vm.$data[key] = val
      }
    })
  }

  function Vue (options) {
    var vm = this;
    vm.$options = options;
    
    initData(vm);
    vm.mount(document.querySelector(options.el))
  }

  Vue.prototype.mount = function (el) {
    var vm = this;
    vm.$el = el;
    //  // 在组件实例生命周期的挂载阶段。可以看到Watcher被创建
    new Watcher(vm, function () {
      vm.update(vm.render());
    });
  }

  Vue.prototype.update = function (vnode) {
    var vm = this;
    var prevVnode = vm._vnode;
    vm._vnode = vnode;
    if (!prevVnode) {
      vm.$el = vm.patch(vm.$el, vnode);
    } else {
      vm.$el = vm.patch(prevVnode, vnode);
    }
  }

  Vue.prototype.patch = patch;

  Vue.prototype.render = function () {
    var vm = this;
    return vm.$options.render.call(vm)
  }

  var vm = new Vue({
    el: '#app',
    data: {
      message: 'Hello world',
      isShow: true
    },
    render () {
      return createElement(
        'div',
        {
          attrs: {
            'class': 'wrapper'
          }
        },
        [
          this.isShow
          ? createElement(
            'p',
            { 
              attrs: {
                'class': 'inner'
              }
            },
            this.message
          )
          : createElement(
            'h1',
            { 
              attrs: {
                'class': 'inner'
              }
            },
            'Hello world'
          )
        ]
      )
    }
  })

  // test
  setTimeout(function () {
    vm.message = 'Hello';
  }, 1000)

  setTimeout(function () {
    vm.isShow = false;
  }, 2000)

})();

依赖在哪里收集,如何收集

1.依赖收集在组件渲染阶段,当组件需要用到(touch)数据时,通过获取前面数据监听定义的值在getter阶段收集依赖。
2.dep是一个依赖收集器,将依赖收集起来,然后在数据变化时通知每个watcher。
疑问点,为什么在getter阶段运行dep.depend(),而不是在创建完watcher后可以在任意地方运行将此收集起来,然后等到数据发生变化时通知wathcer。

如果可以在任意地方收集,则dep的subs成员需要是全局对象,这样当某个数据变化后将会通知所有的watcher,这是不合理的。正确的做法是每个数据属性都有对应得一个Dep依赖收集器,收集相关的watcher。因此只能在getter阶段运行dep.depend(),从设计逻辑上订阅(touch)某个数据的时候才开始收集依赖才是合理的。那么能不能在watcher创建给 Dep.target赋值后,使用obj.defindProperty劫持对象属性呢?显然也是不现实的,当其它Watcher依赖当前data属性时也需要重新重写getter/setter,这会覆盖原来的dep。Data和watcher之间的关系是一对多的依赖关系,而不是一对一。

posted @ 2021-12-21 09:18  自在一方  阅读(39)  评论(0编辑  收藏  举报