- function MyVue (options = {}) {
- //第一步:首先就是将实例化的对象给拿到,得到data对象
- this.$options = options;
- this._data = this.$options.data;
- //第二步:数据劫持,将data对象中每一个属性都设置get/set方法
- observe(this._data);
- //第三步:数据代理,这里就是将_data的对象属性放到myVue实例中一份,实际的数据还是_data中的
- for (let key in this._data) {
- //这里的this代表当前myVue实例对象
- Object.defineProperty(this, key, {
- enumerable: true,
- get () {
- return this._data[key];
- },
- set (newVal) {
- this._data[key] = newVal;
- }
- })
- }
- //第四步:compile模板,需要将el属性和当前myVue实例
- compile(options.el, this)
- }
- function compile (el, vm) {
- return new Compile(el, vm);
- }
- function Compile (el, vm) {
- //将el代表的那个dom节点挂载到myVue实例中
- vm.$el = document.querySelector(el);
- //创建虚拟节点容器树
- let fragment = document.createDocumentFragment();
- //将el下所有的dom节点都放到容器树中,注意appendChild方法,这里是将将dom节点移动到容器树中啊,不是死循环!
- while (child = vm.$el.firstChild) {
- // console.log('count:' + vm.$el.childElementCount);
- fragment.appendChild(child)
- };
- //遍历虚拟节点中的所有节点,将真实数据填充覆盖这种占位符{{}}
- replace(fragment, vm);
- //将虚拟节点树中内容渲染到页面中
- vm.$el.appendChild(fragment);
- }
- function replace (n, vm) {
- //遍历容器树中所有的节点,解析出{{}}里面的内容,然后将数据覆盖到节点中去
- Array.from(n.childNodes).forEach(node => {
- console.log('nodeType:' + node.nodeType);
- let nodeText = node.textContent;
- let reg = /\{\{(.*)\}\}/;
- // 节点类型常用的有元素节点,属性节点和文本节点,值分别是1,2,3
- //一定要弄清楚这三种节点,比如<p id="123">hello</p>,这个p标签整个的就是元素节点,nodeType==1
- //id="123"可以看作是属性节点,nodeType==2
- //hello 表示文本节点,nodeType==3
- //因为占位符{{}}只在文本节点中,所以需要判断是否等于3
- if (node.nodeType === 3 && reg.test(nodeText)) {
- // RegExp.$1是RegExp的一个属性,指的是与正则表达式匹配的第一个 子匹配(以括号为标志)字符串,以此类推,RegExp.$2。。。
- let arr = RegExp.$1.split(".");
- let val = vm;
- // 这个for循环就是取出这样的值:myVue[name][user]
- arr.forEach(i => {
- val = val[i];
- })
- // 创建Watcher,最主要的是传入的这个回调函数,会覆盖node节点中的占位符{{xxx}}
- new Watcher(vm, RegExp.$1, function (newVal) {
- node.textContent = nodeText.replace(reg, newVal);
- })
- // 把值覆盖到虚拟节点的占位符{{}}这里
- node.textContent = nodeText.replace(reg, val);
- }
- // 第一个遍历的节点是<div id="app">这一行后面的换行,nodeType等于3,但是没有占位符{{}},所以会进入到这里进行递归调用内部
- //的每一个节点,直到找到文本节点而且占位符{{}}
- if (node.childNodes) {
- replace(node, vm);
- }
- })
- }
- //数据劫持操作
- function observe (data) {
- // 如果data不是对象,就结束,不然递归调用会栈溢出的
- if (typeof data !== 'object') return;
- return new Observe(data);
- }
- function Observe (data) {
- let dep = new Dep();
- // 遍历data所有属性
- for (let key in data) {
- let val = data[key];
- //初始化的时候, data中就有复杂对象的时候,例如data: { message:{a:{b:1}}} ,就需要递归的遍历这个对象中每一个属性都添加get和set方法
- observe(val);
- Object.defineProperty(data, key, {
- enumerable: true,
- get () {
- // 订阅
- Dep.target && dep.addSub(Dep.target);
- return val;
- },
- set (newVal) {
- if (val === newVal) return;
- val = newVal;
- //当后续可能因为业务逻辑使得_data.message = {name: "小王"},设置对象类型的属性值,就需要递归的给对象中{name: "小王"}的每个属性也添加get和set方法
- //否则name是没有get/set方法的
- observe(val);
- dep.notify();
- }
- })
- }
- }
- // ===============================发布订阅===============================
- // 可以看做是公众号端
- function Dep () {
- // 存放每个用户的容器
- this.subs = [];
- }
- //对外提供的api之一,供用户订阅
- Dep.prototype.addSub = function (sub) {
- this.subs.push(sub);
- }
- // 对外提供的api之二,遍历每个用户,给每个用法发信息
- Dep.prototype.notify = function () {
- this.subs.forEach(sub => {
- sub.update();
- });
- }
- // 用户端
- function Watcher (vm, exp, fn) {
- // 这个可以看作是用户的标志;注意,这个fn一般是一个回调函数
- this.vm = vm;
- this.exp = exp;
- this.fn = fn;
- Dep.target = this;
- let val = vm;
- let arr = exp.split(".");
- arr.forEach(item => {
- val = val[item];
- })
- Dep.target = null;
- }
- // 用户端提供的对外api,让公众号端使用
- Watcher.prototype.update = function () {
- let val = this.vm;
- let arr = this.exp.split(".");
- arr.forEach(item => {
- val = val[item];
- })
- this.fn(val);
- }