前文中我们在修改 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
进行重新渲染
如下图所示,每个组件都会有自己的 watcher
,当只有 num
变化,而 name
和 age
没有变化时,其实只用更新 侧边栏
一个组件,所以为了做区分,需要给每个 watcher
分配一个唯一 id
。
这也侧面说明了 Vue 组件化除了 复用
、 方便维护
外的另一个好处:局部渲染更新
dep
和 watcher
的关系是 多对多关系
:
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有哪些
}
}