前文中我们在修改 data 属性后,手动调用了 _update 更新视图,这是不方便的,所以需要实现监听 data 的变化,自动触发页面的更新:

<div id="app" style="color:red;background:yellow">
    {{name}}   {{age}}   {{name}}  {{name}}  {{name}} 
</div>

所以我们想到给模板中的属性(e.g. {{name}}{{age}} …),都添加一个对应的收集器dep

然后页面渲染的时候,将渲染逻辑封装到 watcher 中(vm._update(vm._render()))

dep 记住这些 watcher 即可, 等到属性变化了可以找到对应的 dep 中存放的 watcher 进行重新渲染

Dep 收集者和 Watcher 观察者

如下图所示,每个组件都会有自己的 watcher ,当只有 num 变化,而 nameage 没有变化时,其实只用更新 侧边栏 一个组件,所以为了做区分,需要给每个 watcher 分配一个唯一 id

这也侧面说明了 Vue 组件化除了 复用方便维护 外的另一个好处:局部渲染更新

image.png

Dep 和 Watcher 之间是 多对多关系

depwatcher 的关系是 多对多关系:

image.png

src/observe/watch.js

let id = 0;

class Watcher { // 不同组件有不同的watcher   目前只有一个 渲染根实例的
  constructor(vm, fn, options) {
    this.id = id++;
    this.renderWatcher = options; // options = true 表明 watcher 是一个渲染 watcher
    this.getter = fn; // getter意味着调用这个函数可以发生取值操作
    this.get();
  }
  get() {
    this.getter(); // 会去vm上取值  vm._update(vm._render) 取name 和age
  }
}

// 需要给每个属性增加一个dep, 目的就是收集watcher
// 一个组件(视图)中 有多少个属性 (n个属性会对应一个组件(视图)) n个dep对应一个watcher
// 1个属性 对应着多个组件(视图)  1个dep对应多个watcher
// 多对多的关系
export default Watcher;

lifecycle.js 中调用 watcher 更新视图

src/lifecycle.js

**import Watcher from "./observe/watcher";**
import { createElementVNode, createTextVNode } from "./vdom";

function createElm(vnode) {
  let { tag, data, children, text } = vnode;
  if (typeof tag === "string") {
    // 标签
    vnode.el = document.createElement(tag); // 这里将真实节点和虚拟节点对应起来,后续如果修改属性了
    patchProps(vnode.el, data);
    children.forEach((child) => {
      vnode.el.appendChild(createElm(child));
    });
  } else {
    vnode.el = document.createTextNode(text);
  }
  return vnode.el;
}
function patchProps(el, props) {
  for (let key in props) {
    if (key === "style") {
      // style{color:'red'}
      for (let styleName in props.style) {
        el.style[styleName] = props.style[styleName];
      }
    } else {
      el.setAttribute(key, props[key]);
    }
  }
}
function patch(oldVNode, vnode) {
  // 写的是初渲染流程
  const isRealElement = oldVNode.nodeType;
  if (isRealElement) {
    const elm = oldVNode; // 获取真实元素
    const parentElm = elm.parentNode; // 拿到父元素
    let newElm = createElm(vnode); // 根据 vnode 创建真实 dom
    parentElm.insertBefore(newElm, elm.nextSibling);
    parentElm.removeChild(elm); // 删除老节点

    return newElm;
  } else {
    // diff算法
  }
}
export function initLifeCycle(Vue) {
  Vue.prototype._update = function (vnode) {
    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方法
  };
}
export function mountComponent(vm, el) {
  // 这里的 el 是通过 querySelector 处理过的
  vm.$el = el;

  // 1.调用 render 方法产生虚拟节点 虚拟DOM

  **const updateComponent = () => {
    vm._update(vm._render()); // vm.$options.render() 虚拟节点
  }
  new Watcher(vm, updateComponent, true); // true 用于标识是一个渲染 watcher**

  // 2.根据 虚拟DOM 产生真实 DOM

  // 3.插入到el元素中
}

/**
 * vue核心流程
 *  1) 创造了响应式数据   defineProperty 劫持 用户 data => 绑定到 vm._data => 为了能 vm.xxx 把 vm._data 代理到 vm 上
 *  2) 模板转换成ast语法树   分析 template 模板字符串, 从前往后通过正则匹配逐词分析转 ast 树, 单节点结构: { tag, type, attrs, parent, children }
 *  3)  将ast语法树转换了render函数   _c('div', {id: 'app', style: {...} }, _v("world" + _s(name)))
 *  4)  后续每次数据更新可以只执行render函数 (无需再次执行ast转化的过程)
 *
 * render函数会去产生虚拟节点(使用响应式数据)
 * 根据生成的虚拟节点创造真实的DOM
 */

我们还需要创建一个 Dep 类,专门用来创建收集者/器:

src/observe/dep.js

let id = 0;
class Dep {
  constructor() {
    this.id = id++; // 属性的dep要收集watcher
    this.subs = [];// 这里存放着当前属性对应的watcher有哪些
  }
}