手写Vue2.0源码

Posted by ZhuLang on February 23, 2025

title: 手写 Vue2.0 源码 description: 核心逻辑以及功能的实现


一.使用 Rollup 搭建开发环境

Rollup 是一个 JavaScript 模块打包器,可以将小块代码编译成大块复杂的代码, rollup.js更专注于Javascript类库打包。

1.安装 rollup 环境

1
npm install @babel/preset-env @babel/core rollup rollup-plugin-babel rollup-plugin-serve cross-env -D

2.rollup.config.js 编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import babel from 'rollup-plugin-babel';
import serve from 'rollup-plugin-serve';

// 常见的模块规范 import export (ESModule) module.exports require (CommonJS)
// AMD 比较老的模块规范 systemjs 模块规范
// ES6Module commonjs umd 支持amd 和 cmd Vue)

export default {
  input: './src/index.js', // 打包项目的入口文件
  output: {
    format: 'umd', // 打包后的结果是umd模块规范
    file: 'dist/vue.js', // 打包出的文件结果放在哪个目录
    name: 'Vue', // 打包后的全局变量的名字
    sourcemap: true,
  },
  plugins: [
    babel({
      exclude: 'node_modules/**',
    }),
    process.env.ENV === 'development'
      ? serve({
          open: true,
          openPage: '/public/index.html',
          port: 3000,
          contentBase: '',
        })
      : null,
  ],
};

3.配置.babelrc

1
2
3
{
  "presets": ["@babel/preset-env"]
}

4.执行脚本配置

1
2
3
4
5
// package.json
"scripts": {
  "build:dev": "rollup -c",
  "serve": "cross-env ENV=development rollup -c -w"
}

二.Vue 响应式原理

导出 Vue 构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 整个自己编写的Vue入口

import { initMixin } from './init';

// es6的类, 要求所有的扩展都在类的内部来进行扩展

function Vue(options) {
  this._init(options);
}

initMixin(Vue); // 后续再扩展都可以采用这种方式

// 给Vue添加原型方法我们通过文件的方式来添加, 防止所有的功能都在一个文件中来处理
export default Vue;

init 方法初始化 Vue 状态

1
2
3
4
5
6
7
8
9
10
11
12
13
import { initState } from './state';

export function initMixin(Vue) {
  Vue.prototype._init = function (options) {
    const vm = this;
    // vue中会判断如果是$开头的属性不会被变成响应式数据
    this.$options = options; // 所有后续的扩展方法都有一个$options选项可以获取用户的所有选项

    // 对于实例的数据源 props data methods computed watch
    // prop data
    initState(vm);
  };
}

根据不同属性进行初始化操作

1
2
3
4
5
6
7
8
9
10
11
export function initState(vm) {
  // 获取传入的数据对象
  const options = vm.$options;

  // 后续实现计算属性 watch props methods
  if (options.data) {
    // 初始化data
    initData(vm);
  }
}
function initData(vm) {}

1. 初始化数据

1
2
3
4
5
6
7
8
import { observe } from './observer/index';
function initData(vm) {
  let data = vm.$options.data;
  // 如果是函数就拿到函数的返回值, 否则就直接采用data作为数据源
  data = vm._data = typeof data === 'function' ? data.call(vm) : data;
  // 属性劫持, 采用defineProperty将所有的属性进行劫持
  observe(data);
}

2. 递归属性劫持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import arrayPrototype from './array';

class Observer {
  constructor(data) {
    // 如果有__ob__属性, 说明被观测过了
    Object.defineProperty(data, '__ob__', {
      value: this,
      enumerable: false, // 不可枚举
      writable: true,
      configurable: true,
    });
    if (Array.isArray(data)) {
      // 如果是数组的话也是用defineProperty回浪费很多性能, 并且很少用户会通过索引操作 arr[666] = 777
      // vue3中的Polyfill直接就给数组做代理了
      // 改写数组的方法, 勇敢用户调用了可以改写数组方法的api, 那么就去劫持这个方法
      // 变异方法 push pop shift unshift reverse sort splice
      // 修改数组的索引和长度无法更新视图
      data.__proto__ = arrayPrototype;
      // 如果数组里面放的是对象类型, 期望它也会被变成响应式的
      this.observeArray(data);
    } else {
      this.walk(data);
    }
  }
  observeArray(data) {
    data.forEach((item) => observe(item)); // 如果是对象才观测
  }
  walk(data) {
    // 循环对象, 尽量不用for in 会遍历原型链
    let keys = Object.keys(data);
    keys.forEach((key) => {
      // 没有重写数组里的每一项
      defineReactive(data, key, data[key]);
    });
  }
}

// 性能不好的原因, 所有的属性都被重新定义了一遍
// 一上来需要将对象深度代理, 性能差
function defineReactive(data, key, value) {
  observe(value); // 递归代理属性
  // 属性会全部被重写添加了get和set
  Object.defineProperty(data, key, {
    get() {
      return value;
    },
    set(newValue) {
      observe(newValue); // 赋值一个对象, 也可以实现响应式数据
      if (newValue !== value) {
        value = newValue;
      }
    },
  });
}

export function observe(data) {
  if (typeof data !== 'object' || data == null) {
    // 如果不是对象类型, 那么不需要做任何处理
    return;
  }
  if (data.__ob__) {
    // 说明这个属性已经被代理过了
    return;
  }
  // 如果一个对象已经被观测了, 就不要再次被观测了
  // __ob__ 标识是否又被观测过
  return new Observer(data);
}

3. 数组方法劫持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Observer {
  constructor(data) {
    if (Array.isArray(data)) {
      // 如果是数组的话也是用defineProperty回浪费很多性能, 并且很少用户会通过索引操作 arr[666] = 777
      // vue3中的Polyfill直接就给数组做代理了
      // 改写数组的方法, 勇敢用户调用了可以改写数组方法的api, 那么就去劫持这个方法
      // 变异方法 push pop shift unshift reverse sort splice
      // 修改数组的索引和长度无法更新视图
      data.__proto__ = arrayPrototype;
      // 如果数组里面放的是对象类型, 期望它也会被变成响应式的
      this.observeArray(data);
    } else {
      this.walk(data);
    }
  }
  observeArray(data) {
    data.forEach((item) => observe(item)); // 如果是对象才观测
  }
}
重写数组原型方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
let oldArrayPrototype = Array.prototype;
// arrayPrototype.__proto__ = Array.prototype

let arrayPrototype = Object.create(oldArrayPrototype);
let methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'reverse', 'sort'];
methods.forEach((method) => {
  // 用户调用push方法会先自己重写的方法, 之后调用数组原来的方法
  arrayPrototype[method] = function (...args) {
    let inserted;
    const ob = this.__ob__;
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args; // 数组
        break;
      case 'splice': // arr.splice(1, 1, xxx)
        inserted = args.slice(2);
        break;
      default:
        break;
    }
    if (inserted) {
      // 对新增的数据再次进行观测
      ob.observeArray(inserted);
    }
    return oldArrayPrototype[method].call(this, ...args);
  };
});

export default arrayPrototype;
增加ob属性
1
2
3
4
5
6
7
8
9
10
11
class Observer {
  constructor(data) {
    // 如果有__ob__属性, 说明被观测过了
    Object.defineProperty(data, '__ob__', {
      value: this,
      enumerable: false, // 不可枚举
      writable: true,
      configurable: true,
    });
  }
}

给所有响应式数据增加标识,并且可以在响应式上获取Observer实例上的方法

4. 数据代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function proxy(vm, source, key) {
  Object.defineProperty(vm, key, {
    get() {
      return vm[source][key];
    },
    set(newValue) {
      vm[source][key] = newValue;
    },
  });
}

function initData(vm) {
  let data = vm.$options.data;
  // 如果是函数就拿到函数的返回值, 否则就直接采用data作为数据源
  data = vm._data = typeof data === 'function' ? data.call(vm) : data;
  // 期望用户可以直接通过 vm.xxx 获取值, 也可以这样取值 vm._data.xxx
  for (let key in data) {
    proxy(vm, '_data', key);
  }
  // 属性劫持, 采用defineProperty将所有的属性进行劫持
  observe(data);
}

三.模板编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Vue.prototype._init = function (options) {
  const vm = this;
  // vue中会判断如果是$开头的属性不会被变成响应式数据
  this.$options = options; // 所有后续的扩展方法都有一个$options选项可以获取用户的所有选项

  // 对于实例的数据源 props data methods computed watch
  // prop data
  initState(vm);

  // 状态初始化完毕后需要进行页面挂载
  if (vm.$options.el) {
    // el属性和直接调用$mount是一样的
    vm.$mount(vm.$options.el);
  }
};

Vue.prototype.$mount = function (el) {
  const vm = this;
  el = document.querySelector(el);
  const options = vm.$options;
  if (!options.render) {
    let template = options.template;
    if (!template && el) {
      template = el.outerHTML;
    }
    // 将template变成render函数
    // 创建render函数 => 虚拟dom => 渲染真实dom
    const render = compileToFunctions(template); // 开始编译
    options.render = render;
  }
  // 将当前组件实例挂载到真实的el节点上面
  return mountComponent(vm, el);
};

将 template 编译成 render 函数

1.解析标签和内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`; //匹配标签名 形如 abc-123
const qnameCapture = `((?:${ncname}\\:)?${ncname})`; //匹配特殊标签 形如 abc:234 前面的abc:可有可无
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 匹配标签开始 形如 <abc-123 捕获里面的标签名
const startTagClose = /^\s*(\/?)>/; // 匹配标签结束  >
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 匹配标签结尾 如 </abc-123> 捕获里面的标签名
const attribute =
  /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // 匹配属性  形如 id="app"

export function parse(html) {
  let root; // 树的操作,需要根据开始标签和结束标签产生一棵树
  // 如何构建树的父子关系
  let stack = [];
  while (html) {
    // 一个个字符来解析将结果跑出去
    let textEnd = html.indexOf('<');
    if (textEnd === 0) {
      const startTagMatch = parseStartTag(); // 解析开始标签
      if (startTagMatch) {
        start(startTagMatch.tagName, startTagMatch.attrs);
        continue;
      }
      let matches;
      if ((matches = html.match(endTag))) {
        // </div> 不是开始就会走到结束
        end(matches[1]);
        advance(matches[0].length);
        continue;
      }
    }
    let text;
    if (textEnd >= 0) {
      text = html.substring(0, textEnd);
    }
    if (text) {
      advance(text.length);
      chars(text);
    }
  }
  function createASTElement(tagName, attrs) {
    return {
      tag: tagName,
      attrs,
      children: [],
      parent: null,
      type: 1,
    };
  }
  function start(tagName, attrs) {
    const element = createASTElement(tagName, attrs);
    if (root == null) {
      root = element;
    }
    const parent = stack[stack.length - 1]; // 取到栈中最后一个
    if (parent) {
      element.parent = parent; // 让这个元素记住自己的父亲是谁
      parent.children.push(element); // 让父亲记住儿子是谁
    }
    // 将原素放到栈中
    stack.push(element);
  }
  function end(tagName) {
    stack.pop();
  }
  function chars(text) {
    text = text.replace(/\s/g, '');
    if (text) {
      const parent = stack[stack.length - 1];
      parent.children.push({
        type: 3,
        text,
      });
    }
  }
  function parseStartTag() {
    const matches = html.match(startTagOpen);
    if (matches) {
      const match = {
        tagName: matches[1],
        attrs: [],
      };
      advance(matches[0].length);
      // 继续解析开始标签的属性
      let end, attr;
      while (
        !(end = html.match(startTagClose)) &&
        (attr = html.match(attribute))
      ) {
        // 只要没有匹配到结束标签就一直匹配
        match.attrs.push({
          name: attr[1],
          value: attr[3] || attr[4] || attr[5] || true,
        });
        advance(attr[0].length); // 解析一个属性删除一个
      }
      if (end) {
        advance(end[0].length);
        return match;
      }
    }
  }
  function advance(n) {
    html = html.substring(n); // 每次根据传入的长度截取html
  }
  return root;
}

2.生成 ast 语法树

语法树就是用对象描述 js 语法

1
2
3
4
5
6
7
{
    tag:'div',
    type:1,
    children:[{tag:'span',type:1,attrs:[],parent:'div对象'}],
    attrs:[{name:'pf',age:10}],
    parent:null
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
let root; // 树的操作,需要根据开始标签和结束标签产生一棵树
// 如何构建树的父子关系
let stack = [];
while (html) {
  // 一个个字符来解析将结果跑出去
  let textEnd = html.indexOf('<');
  if (textEnd === 0) {
    const startTagMatch = parseStartTag(); // 解析开始标签
    if (startTagMatch) {
      start(startTagMatch.tagName, startTagMatch.attrs);
      continue;
    }
    let matches;
    if ((matches = html.match(endTag))) {
      // </div> 不是开始就会走到结束
      end(matches[1]);
      advance(matches[0].length);
      continue;
    }
  }
  let text;
  if (textEnd >= 0) {
    text = html.substring(0, textEnd);
  }
  if (text) {
    advance(text.length);
    chars(text);
  }
}
function createASTElement(tagName, attrs) {
  return {
    tag: tagName,
    attrs,
    children: [],
    parent: null,
    type: 1,
  };
}
function start(tagName, attrs) {
  const element = createASTElement(tagName, attrs);
  if (root == null) {
    root = element;
  }
  const parent = stack[stack.length - 1]; // 取到栈中最后一个
  if (parent) {
    element.parent = parent; // 让这个元素记住自己的父亲是谁
    parent.children.push(element); // 让父亲记住儿子是谁
  }
  // 将原素放到栈中
  stack.push(element);
}
function end(tagName) {
  stack.pop();
}
function chars(text) {
  text = text.replace(/\s/g, '');
  if (text) {
    const parent = stack[stack.length - 1];
    parent.children.push({
      type: 3,
      text,
    });
  }
}

3.生成代码

template 转换成 render 函数的结果

1
2
3
4
<div style="color:red">hello  <span></span></div>
render(){
   return _c('div',{style:{color:'red'}},_v('hello'+_s(name)),_c('span',undefined,''))
}

实现代码生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g; //匹配花括号  捕获花括号里面的内容

// 处理attrs属性
function genProps(attrs) {
  let str = '';
  for (let i = 0; i < attrs.length; i++) {
    let attr = attrs[i];
    // 特殊属性 style
    if (attr.name === 'style') {
      const obj = {};
      attr.value.split(';').reduce((memo, current) => {
        const [key, value] = current.split(':');
        memo[key] = value;
        return memo;
      }, obj);
      attr.value = obj;
    }
    str += `${attr.name}:${JSON.stringify(attr.value)},`;
  }
  return `{${str.slice(0, -1)}}`;
}

function gen(node) {
  if (node.type === 1) {
    return generate(node);
  } else {
    const text = node.text;
    if (!defaultTagRE.test(text)) {
      return `_v(${JSON.stringify(text)})`; // 不带表达式
    } else {
      const tokens = [];
      let match;
      // exec 遇到全局匹配会有 lastIndex 问题,每次匹配谦虚要将lastIndex置为0
      let startIndex = (defaultTagRE.lastIndex = 0);
      while ((match = defaultTagRE.exec(text))) {
        const endIndex = match.index; // 匹配到的索引 abc {{aa}}  cd
        if (endIndex > startIndex) {
          tokens.push(JSON.stringify(text.slice(startIndex, endIndex)));
        }
        tokens.push(`_s(${match[1].trim()})`);
        startIndex = endIndex + match[0].length;
      }
      if (startIndex < text.length) {
        // 将最后的尾巴也丢进去
        tokens.push(JSON.stringify(text.slice(startIndex)));
      }
      return `_v(${tokens.join('+')})`; // 组合成最终的语法
    }
  }
}

// 生成子节点,递归创建
function genChildren(el) {
  const children = el.children;
  if (children) {
    return `${children.map((child) => gen(child)).join(',')}`;
  }
}

export function generate(el) {
  // 字符串拼接
  const children = genChildren(el);
  let code = `_c('${el.tag}',${
    el.attrs.length ? `${genProps(el.attrs)}` : 'undefined'
  }${children ? `,${children}` : ''})`; // _c('div', {className: 'xxx'}, _v('hello world'))
  return code;
}

4.生成 render 函数

1
2
3
4
5
6
7
8
9
10
11
// 将模板变成render函数,通过with+new Function的方式让字符串变成js语法来执行
export function compileToFunctions(template) {
  const ast = parse(template);

  // 通过ast语法树转换成render函数
  const code = generate(ast);

  // 使用with改变作用域为this
  const renderFn = new Function(`with(this) {return ${code}}`);
  return renderFn;
}

五.依赖收集

每个属性都要有一个dep,每个dep中存放着watcher,同一个watcher会被多个dep所记录。

1. 在渲染是存储 watcher

1
2
3
4
5
6
get() {
  // debugger;
  Dep.target = this; // 静态属性就是只有一份
  this.getter(); // 会去vm上取值  vm._update(vm._render) 取name 和age
  Dep.target = null; // 渲染完毕后就清空
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
let id = 0;
class Dep {
  constructor() {
    this.id = id++; // 属性的dep要收集watcher
    this.subs = []; // 这里存放着当前属性对应的watcher有哪些
  }
  depend() {
    // 这里我们不希望放重复的watcher,而且刚才只是一个单向的关系 dep -> watcher
    // watcher 记录dep
    // this.subs.push(Dep.target);

    Dep.target.addDep(this); // 让watcher记住dep

    // dep 和 watcher是一个多对多的关系 (一个属性可以在多个组件中使用 dep -> 多个watcher)
    // 一个组件中由多个属性组成 (一个watcher 对应多个dep)
  }
  addSub(watcher) {
    this.subs.push(watcher);
  }
  notify() {
    this.subs.forEach((watcher) => watcher.update()); // 告诉watcher要更新了
  }
}
Dep.target = null;

export default Dep;

2. 对象依赖收集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 性能不好的原因, 所有的属性都被重新定义了一遍
// 一上来需要将对象深度代理, 性能差
// 闭包 属性劫持
function defineReactive(data, key, value) {
  const childOb = observe(value); // 递归代理属性, childOb就是当前的实例
  // 属性会全部被重写添加了get和set
  let dep = new Dep(); // 每一个属性都有一个dep
  Object.defineProperty(data, key, {
    // 取值的时候 会执行get
    get() {
      if (Dep.target) {
        dep.depend(); // 让这个属性的收集器记住当前的watcher
        if (childOb) {
          childOb.dep.depend(); // 让数组和对象本身也实现依赖收集
          if (Array.isArray(value)) {
            dependArray(value);
          }
        }
      }
      return value;
    },
    // 修改的时候 会执行set
    set(newValue) {
      // 修改的时候 会执行set
      if (newValue === value) return;
      observe(newValue);
      value = newValue;
      dep.notify(); // 通知更新
    },
  });
}
  • Dep 实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    
    let id = 0;
    class Dep {
      constructor() {
        this.id = id++; // 属性的dep要收集watcher
        this.subs = []; // 这里存放着当前属性对应的watcher有哪些
      }
      depend() {
        // 这里我们不希望放重复的watcher,而且刚才只是一个单向的关系 dep -> watcher
        // watcher 记录dep
        // this.subs.push(Dep.target);
    
        Dep.target.addDep(this); // 让watcher记住dep
    
        // dep 和 watcher是一个多对多的关系 (一个属性可以在多个组件中使用 dep -> 多个watcher)
        // 一个组件中由多个属性组成 (一个watcher 对应多个dep)
      }
      addSub(watcher) {
        this.subs.push(watcher);
      }
      notify() {
        this.subs.forEach((watcher) => watcher.update()); // 告诉watcher要更新了
      }
    }
    Dep.target = null;
    
    export default Dep;
    
  • watcher

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    
    import Dep from './dep';
    
    let id = 0;
    
    // 1)当我们创建渲染watcher的时候我们会把当前的渲染watcher放到Dep.target上
    // 2) 调用_render() 会取值 走到get上
    // 每个属性有一个dep(属性就是被观察者),watcher就是观察者(属性变化了会通知观察者来更新) -> 观察者模式
    class Watcher {
      // 不同组件有不同的watcher 目前只有一个渲染根实例的
      constructor(vm, fn, options) {
        this.id = id++;
        this.renderWatcher = options; // 是一个渲染watcher
        this.getter = fn; // getter意味着调用这个函数可以发生取值操作
        this.deps = []; // 后续我们实现计算属性,和一些清理工作需要用到
        this.depsId = new Set();
        this.get();
      }
      addDep(dep) {
        // 一个组件 对应着多个属性 重复的属性也不用记录
        let id = dep.id;
        if (!this.depsId.has(id)) {
          this.deps.push(dep);
          this.depsId.add(id);
          dep.addSub(this); // watcher已经记住了dep了而且去重了,此时让dep也记住watcher
        }
      }
      get() {
        // debugger;
        Dep.target = this; // 静态属性就是只有一份
        this.getter(); // 会去vm上取值  vm._update(vm._render) 取name 和age
        Dep.target = null; // 渲染完毕后就清空
      }
      update() {
        console.log('update');
        queueWatcher(this); // 把当前的watcher 暂存起来
        // this.get(); // 重新渲染
      }
      run() {
        this.get(); // 渲染的时候用的是最新的vm来渲染的
      }
    }
    
    export default Watcher;
    

3. 数组依赖收集

  • 递归收集数组依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    
    function defineReactive(data, key, value) {
      const childOb = observe(value); // 递归代理属性, childOb就是当前的实例
      // 属性会全部被重写添加了get和set
      let dep = new Dep(); // 每一个属性都有一个dep
      Object.defineProperty(data, key, {
        // 取值的时候 会执行get
        get() {
          if (Dep.target) {
            dep.depend(); // 让这个属性的收集器记住当前的watcher
            if (childOb) {
              childOb.dep.depend(); // 让数组和对象本身也实现依赖收集
              if (Array.isArray(value)) {
                dependArray(value);
              }
            }
          }
          return value;
        },
        // ...
    }
    
    // 深层次嵌套会递归, 递归多了性能差, 不粗案子属性监控不到, 存在的属性要重写方法
    function dependArray(value) {
      for (let i = 0; i < value.length; i++) {
        let current = value[i];
        current.__ob__ && current.__ob__.dep.depend();
        if (Array.isArray(current)) {
          dependArray(current);
        }
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    
    // 获取数组原型
    let oldArrayPrototype = Array.prototype;
    // arrayPrototype.__proto__ = Array.prototype
    let arrayPrototype = Object.create(oldArrayPrototype);
    // 数组变异方法
    let methods = [
      'push',
      'pop',
      'shift',
      'unshift',
      'splice',
      'reverse',
      'sort',
    ];
    methods.forEach((method) => {
      // 用户调用push方法会先自己重写的方法, 之后调用数组原来的方法
      arrayPrototype[method] = function (...args) {
        // 需要对新增的数据再次进行劫持
        let inserted;
        const ob = this.__ob__;
        switch (method) {
          case 'push':
          case 'unshift':
            inserted = args; // 数组
            break;
          case 'splice': // arr.splice(1, 1, xxx)
            inserted = args.slice(2);
            break;
          default:
            break;
        }
        if (inserted) {
          // 对新增的数据再次进行观测
          ob.observeArray(inserted);
        }
        const result = oldArrayPrototype[method].call(this, ...args);
        ob.dep.notify(); // 数据变化了, 通知对应的watcher实现更新逻辑
        return result;
      };
    });
    
    export default arrayPrototype;
    

六.Vue 异步更新之 nextTick

1. 实现队列机制

1
2
3
4
5
update() {
  console.log('update');
  queueWatcher(this); // 把当前的watcher 暂存起来
  // this.get(); // 重新渲染
}
  • scheduler

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    // watcher.js
    let queue = [];
    let has = {};
    let pending = false; // 防抖
    function flushSchedulerQueue() {
      let flushQueue = queue.slice(0);
      queue = [];
      has = {};
      pending = false;
      flushQueue.forEach((q) => q.run()); // 在刷新的过程中可能还有新的watcher,重新放到queue中
    }
    function queueWatcher(watcher) {
      const id = watcher.id;
      if (!has[id]) {
        queue.push(watcher);
        has[id] = true;
        // 不管我们的update执行多少次 ,但是最终只执行一轮刷新操作
        if (!pending) {
          nextTick(flushSchedulerQueue, 0);
          pending = true;
        }
      }
    }
    

2. nextTick 实现原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// watcher.js
let callbacks = [];
let waiting = false;
function flushCallbacks() {
  let cbs = callbacks.slice(0);
  waiting = false;
  callbacks = [];
  cbs.forEach((cb) => cb()); // 按照顺序依次执行
}
// nextTick 没有直接使用某个api 而是采用优雅降级的方式
// 内部先采用的是promise(ie不兼容)MutationObserver(h5的api) 可以考虑ie专享的 setImmediate setTimeout
let timerFunc;
if (Promise) {
  timerFunc = () => {
    Promise.resolve().then(flushCallbacks);
  };
} else if (MutationObserver) {
  let observer = new MutationObserver(flushCallbacks); // 这里传入的回调是异步执行的
  let textNode = document.createTextNode(1);
  observer.observe(textNode, {
    characterData: true,
  });
  timerFunc = () => {
    textNode.textContent = 2;
  };
} else if (setImmediate) {
  timerFunc = () => {
    setImmediate(flushCallbacks);
  };
} else {
  timerFunc = () => {
    setTimeout(flushCallbacks);
  };
}
// 异步任务分为两种,宏任务/微任务
// 宏任务 setTimeout setImmediate(IE下支持性能优于setTimeout)
// 微任务 promise.then mutationObserver
// Vue在更新的时候希望尽快的更新页面 Promise.then MutationObserver setImmediate setImmediate
// Vue3不再考虑兼容性问题了, 所以后续Vue3中直接使用Promise.then
export function nextTick(cb) {
  // 先内部还是先用户的?
  callbacks.push(cb); // 维护nextTick中的cakllback方法
  console.log(callbacks);
  if (!waiting) {
    timerFunc();
    // Promise.resolve().then(flushCallbacks);
    waiting = true;
  }
}

七.Vue 中 Diff 算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// index.js
// ------------- 为了方便观察前后的虚拟节点-- 测试的-----------------
let vm0 = new Vue({
  data: {
    name: 'pf',
    age: 22,
  },
});
let render0 = compileToFunctions(
  `<div class="a" style="color: lightblue;" b="1"><span></span><span></span></div>`
);
let oldVnode = render0.call(vm0);
let ele = createElm(oldVnode);
document.body.appendChild(ele);

let vm = new Vue({
  data: {
    message: 'hello world',
  },
});
let render = compileToFunctions(
  `<div class="b" style="color: red" c=33><span></span></div>`
);
let newVnode = render.call(vm);

let render1 = compileToFunctions(`<ul  a="1" style="color:blue">
    <li key="A">a</li>
    <li key="B">b</li>
    <li key="C">c</li>
    <li key="D">d</li>
</ul>`);
let vm1 = new Vue({ data: { name: 'pf' } });
let prevVnode = render1.call(vm1);
let el = createElm(prevVnode);
document.body.appendChild(el);

let render2 = compileToFunctions(`<ul  a="1"  style="color:red;">
    <li key="C">c</li>
    <li key="A">a</li>
    <li key="D">d</li>
    <li key="E">e</li>
    <li key="Q">q</li>
</ul>`);
let vm2 = new Vue({ data: { name: 'zf' } });
let nextVnode = render2.call(vm2);

// 直接将新的节点替换掉了老的,不是直接替换 而是比较两个人的区别之后在替换 diff算法
// diff算法是一个平级比较的过程 父亲和父亲比对,儿子和儿子比对
// 主要比对标签名和key来判断是不是同一个元素, 如果标签和key都一样说明两个元素使同一个元素
setTimeout(() => {
  // patch(oldVnode, newVnode);

  patch(prevVnode, nextVnode);
}, 2000);

我们想掌握 vue 中的 diff 算法就先构建出两个虚拟dom 之后做 patch

vuediff

diff 算法:采用双指针的方式比较两个节点

  1. 比较开头节点
  2. 比较结尾节点
  3. 比较尾头节点
  4. 比较头尾节点
  5. 乱序比对:根据老的列表做一个映射关系 用新的去找 找到则移动 找不到则添加 最后多余的删除

1.基本 Diff 算法

比对标签

1
2
3
4
// case1: 前后两个虚拟节点不是相同节点 直接替换掉即可
if (!isSameVnode(oldVnode, vnode)) {
  return oldVnode.el.parentNode.replaceChild(createElm(vnode), oldVnode.el);
}

在 diff 过程中会先比较标签是否一致,如果标签不一致用新的标签替换掉老的标签

1
2
3
4
5
6
// case2: 两个元素虚拟节点都是文本的情况下 用新文本替换到老文本
if (!oldVnode.tag) {
  if (oldVnode.text !== vnode.text) {
    return (el.textContent = vnode.text);
  }
}

如果标签一致,有可能都是文本节点,那就比较文本的内容即可

比对属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// case3: 两个都是标签 比对标签属性
patchProps(el, oldVnode.data, vnode.data);

function patchProps(el, oldProps = {}, props = {}) {
  // 老的属性中有 新的没有 要删除老的
  const oldStyles = oldProps.style || {};
  const newStyles = props.style || {};
  for (let key in oldStyles) {
    // 老的样式有 新的没有则删除
    if (!newStyles[key]) {
      el.style[key] = '';
    }
  }
  for (let key in oldProps) {
    // 老的属性有 新的没有删除属性
    if (!props[key]) {
      el.removeAttribute(key);
    }
  }
  for (let key in props) {
    // 用新的覆盖老的
    if (key === 'style') {
      for (let styleName in props.style) {
        el.style[styleName] = props.style[styleName];
      }
    } else {
      el.setAttribute(key, props[key]);
    }
  }
}

当标签相同时,我们可以复用老的标签元素,并且进行属性的比对

比对子元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// case4: 比较儿子节点 一方有儿子 一方没儿子 两方都有儿子
const oldChildren = oldVnode.children || [];
const newChildren = vnode.children || [];
if (oldChildren.length > 0 && newChildren.length > 0) {
  // 完整diff算法 比对两个儿子 一层层比较, 不涉及夸级比较
  updateChildren(el, oldChildren, newChildren);
} else if (newChildren.length > 0) {
  // 没有老的 有新的
  mountChildren(el, newChildren);
} else if (oldChildren.length > 0) {
  // 新的没有 老的有 直接删除
  el.innerHTML = ''; // 可以循环删除
}

function mountChildren(el, newChildren) {
  for (let i = 0; i < newChildren.length; i++) {
    let child = newChildren[i];
    el.appendChild(createElm(child));
  }
}

这里要判断新老节点儿子的状况

2.Diff 优化策略

在开头和结尾新增元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
function isSameVnode(n1, n2) {
  // 如果两个人的标签和key 一样我认为是同一个节点 虚拟节点一样我就可以复用真实节点了
  return n1.tag === n2.tag && n1.key === n2.key;
}
// Vue2.0中采用双指针的方式比较两个节点
// 操作列表经常会有push shift pop unshift reverse sort这些方法(针对镇邪情况做一些优化)
function updateChildren(el, oldChildren, newChildren) {
  // Vue中创建了4个指针 分别指向老孩子和新孩子的头尾
  // 分别一次进行比较有一方先比较完毕就结束
  let oldStartIndex = 0;
  let newStartIndex = 0;
  let oldEndIndex = oldChildren.length - 1;
  let newEndIndex = newChildren.length - 1;

  let oldStartVnode = oldChildren[0];
  let newStartVnode = newChildren[0];
  let oldEndVnode = oldChildren[oldEndIndex];
  let newEndVnode = newChildren[newEndIndex];

  // 有任何一个不满足则停止 || 有一个为true就继续走
  // 双方有一方的头指针 大于尾部指针则停止循环
  while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
    if (isSameVnode(oldStartVnode, newStartVnode)) {
      // 说明两个是同一个元素 要比较属性和它的儿子
      patchVnode(oldStartVnode, newStartVnode);
      oldStartVnode = oldChildren[++oldStartIndex];
      newStartVnode = newChildren[++newStartIndex];
      // 比较开头节点
    } else if (isSameVnode(oldEndVnode, newEndVnode)) {
      // 说明两个是同一个元素 要比较属性和它的儿子
      patchVnode(oldEndVnode, newEndVnode);
      oldEndVnode = oldChildren[--oldEndIndex];
      newEndVnode = newChildren[--newEndIndex];
      // 比较结尾节点
    }
  }
  if (newStartIndex <= newEndIndex) {
    // 新的多了 多余的就插入进去
    for (let i = newStartIndex; i <= newEndIndex; i++) {
      const childEl = createElm(newChildren[i]);
      // 这里可能向后追加 还可能是向前追加
      let anchor = newChildren[newEndIndex + 1]
        ? newChildren[newEndIndex + 1].el
        : null; // 获取下一个元素
      el.insertBefore(childEl, anchor); // anchor为null的时候则会认为是appendChild
    }
  }
}

头移尾、尾头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
else if (isSameVnode(oldEndVnode, newEndVnode)) {
  // 说明两个是同一个元素 要比较属性和它的儿子
  patchVnode(oldEndVnode, newEndVnode);
  oldEndVnode = oldChildren[--oldEndIndex];
  newEndVnode = newChildren[--newEndIndex];
  // 比较结尾节点
} else if (isSameVnode(oldEndVnode, newStartVnode)) {
  patchVnode(oldEndVnode, newStartVnode);
  // insertBefore具备移动性 会将原来的元素移动走
  el.insertBefore(oldEndVnode.el, oldStartVnode.el); // 将老的尾巴移动到老的前面去
  oldEndVnode = oldChildren[--oldEndIndex];
  newStartVnode = newChildren[++newStartIndex];
  // 比较尾头节点
}

操作列表经常会有 push shift pop unshift reverse sort 这些方法(针对镇邪情况做一些优化)。以上四个条件对常见的 dom 操作进行了优化

暴力比对

1
2
3
4
5
6
7
8
function makeIndexByKey(children) {
  const map = {};
  children.forEach((child, index) => {
    map[child.key] = index;
  });
  return map;
}
const map = makeIndexByKey(oldChildren);

对所有的孩子元素进行编号

1
2
3
4
5
6
7
8
9
10
11
12
13
// 根据老的列表做一个映射关系 用新的去找 找到则移动 找不到则添加 最后多余的删除
// 给动态列表添加key的时候 要尽量避免使用索引 因为索引前后都是从0开始 可能会发生错误复用
const moveIndex = map[newStartVnode.key]; // 如果拿到则说明是要移动的索引
if (moveIndex !== undefined) {
  const moveVnode = oldChildren[moveIndex]; // 找到对应的虚拟节点 复用
  el.insertBefore(moveVnode.el, oldStartVnode.el);
  oldChildren[moveIndex] = undefined; // 表示这个节点已经移动走了
  patchVnode(moveVnode, newStartVnode); // 比对属性和子节点
} else {
  el.insertBefore(createElm(newStartVnode), oldStartVnode.el);
}
newStartVnode = newChildren[++newStartIndex];
// 乱序比对

3. 更新操作

1
2
3
4
5
6
7
8
9
10
11
12
Vue.prototype._update = function (vnode) {
  // 将虚拟节点变成真实节点
  // 将vnode渲染到el元素中
  const vm = this;
  const prevVnode = vm._vnode; // 上一次的vNode
  if (!prevVnode) {
    vm.$el = patch(vm.$el, vnode); // 可以初始化渲染,后续更新也走这个patch方法
  } else {
    vm.$el = patch(prevVnode, vnode);
  }
  vm._vnode = vnode; // 渲染完毕后重新更新vnode
};

八.生命周期合并

1.Mixin 原理

1
2
3
4
5
6
7
8
// gloablAPI.js
export default function initGlobalAPI(Vue) {
  Vue.options = {};
  Vue.mixin = function (options) {
    this.options = mergeOptions(this.options, options);
    return this;
  };
}

2.合并生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// gloablAPI.js
LIFECYCLE.forEach((hook) => {
  strats[hook] = function (p, c) {
    if (c) {
      // 如果儿子有 父亲有 让父亲和儿子拼在一起
      if (p) {
        return p.concat(c); // 最终八生命周期都合并在一起了
      } else {
        return [c]; // 儿子有父亲没有 则将儿子包装秤数组
      }
    } else {
      return p; // 如果儿子没有则用父亲即可
    }
  };
});

export function mergeOptions(parent, child) {
  const options = {};
  for (let key in parent) {
    // 循环老的
    mergeField(key);
  }
  for (let key in child) {
    if (!parent.hasOwnProperty(key)) {
      mergeField(key);
    }
  }
  function mergeField(key) {
    // 策略模式 减少if-else
    if (strats[key]) {
      options[key] = strats[key](parent[key], child[key]);
    } else {
      // 如果不在测量中则以儿子为主
      options[key] = child[key] || parent[key]; // 优先采用儿子 再采用父亲
    }
  }
  return options;
}

3.调用生命周期

1
2
3
4
5
6
7
8
9
// lifecycle.js
export function callHook(vm, hook) {
  const handlers = vm.$options[hook];
  if (handlers) {
    for (let i = 0; i < handlers.length; i++) {
      handlers[i].call(vm);
    }
  }
}

4.初始化流程中调用生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// init.js
Vue.prototype._init = function (options) {
  const vm = this;
  // vue中会判断如果是$开头的属性不会被变成响应式数据
  this.$options = mergeOptions(vm.constructor.options, options); // 所有后续的扩展方法都有一个$options选项可以获取用户的所有选项

  // 对于实例的数据源 props data methods computed watch
  // prop data
  callHook(vm, 'beforeCreate');
  initState(vm);
  callHook(vm, 'created');

  // 状态初始化完毕后需要进行页面挂载
  if (vm.$options.el) {
    // el属性和直接调用$mount是一样的
    vm.$mount(vm.$options.el);
  }
};

九.Watch & Computed

1.Watch 实现原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
const vm = new Vue({
  el: '#app',
  data: {
    name: 'pf',
    a: { a: 1 },
    arr: ['1', '2', '3'],
    age: 22,
    address: '广州',
  },
  methods: {
    handler(newVal, oldVal) {
      console.log(newVal, oldVal);
    },
  },
  watch: {
    name(newVal, oldVal) {
      console.log(newVal, oldVal);
    },
    a(newVal, oldVal) {
      console.log(newVal, oldVal);
    },
    arr: [
      (newVal, oldVal) => {
        console.log(newVal, oldVal);
      },
    ],
    age: [
      'handler',
      {
        handler: (newVal, oldVal) => {
          console.log(newVal, oldVal);
        },
        immediate: true,
      },
    ],
  },
});
vm.$watch('a.a', (newVal, oldVal) => {
  console.log(newVal, oldVal);
});
setTimeout(() => {
  vm.a.a = 22;
  vm.name.a = 'ricardopang';
  vm.a = { b: 2 };
  vm.arr.push('123');
  vm.age = 33;
}, 1000);

选项中如果有 watch 则对 watch 进行初始化

1
2
3
4
5
6
7
// state.js
export function initState(vm) {
  // ...
  if (options.watch) {
    initWatch(vm);
  }
}

选项中如果有 watch 则对 watch 进行初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// state.js
function initWatch(vm) {
  const watch = vm.$options.watch;
  for (let key in watch) {
    const handler = watch[key]; // 字符串 数组 函数
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatch(vm, key, handler[i]);
      }
    } else {
      // 对当前属性进行创建watcher 存放的回调是handler 取数据是从vm上获取
      createWatch(vm, key, handler);
    }
  }
}

function createWatch(vm, key, handler) {
  // 判断如果handler是一个字符串 可以采用实例上的方法
  let options;
  if (typeof handler === 'object' && handler !== null) {
    options = handler;
    handler = handler.handler;
  }
  if (typeof handler === 'string') {
    handler = vm[handler];
  }
  return vm.$watch(key, handler, options);
}

这里涉及了 watch 的三种写法,1.值是对象、2.值是数组、3.值是字符串 (如果是对象可以传入一些 watch 参数),最终会调用 vm.$watch 来实现

扩展 Vue 原型上的方法,都通过 mixin 的方式来进行添加

1
2
3
4
5
6
7
8
9
10
11
// state.js
export function stateMixin(Vue) {
  Vue.prototype.$nextTick = nextTick;
  Vue.prototype.$watch = function (exprOrFn, cb, options = {}) {
    options.user = true; // 标记为用户watcher
    const watcher = new Watcher(this, exprOrFn, options, cb);
    if (options.immediate) {
      cb.call(this, watcher.value);
    }
  };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// observer/watcher.js
class Watcher {
  // 不同组件有不同的watcher   目前只有一个 渲染根实例的
  constructor(vm, exprOrFn, options, cb) {
    // ...
    // 如果给的是一个字符串 需要去通过字符串取值
    if (typeof exprOrFn === 'string') {
      this.getter = function () {
        const path = exprOrFn.split('.'); // [a.b]
        return path.reduce((vm, current) => {
          vm = vm[current];
          return vm;
        }, vm);
      };
    } else {
      this.getter = exprOrFn; // getter意味着调用这个函数可以发生取值操作
    }

    // ...
    this.cb = cb;
    this.vm = vm;
    this.user = options.user; // 标识是否是用户自己的watcher

    this.value = this.get(); // 实现页面渲染
  }
  addDep(dep) {
    // 一个组件 对应着多个属性 重复的属性也不用记录
    let id = dep.id;
    if (!this.depsId.has(id)) {
      this.deps.push(dep);
      this.depsId.add(id);
      dep.addSub(this); // watcher已经记住了dep了而且去重了,此时让dep也记住watcher
    }
  }
  evaluate() {
    this.value = this.get(); // 获取到用户函数的返回值 并且还要标识为脏
    this.dirty = false;
  }
  get() {
    pushTarget(this); // 静态属性就是只有一份
    let value = this.getter.call(this.vm); // 会去vm上取值  vm._update(vm._render) 取name 和age
    popTarget(); // 渲染完毕后就清空
    return value;
  }
  run() {
    let oldValue = this.value;
    let newValue = this.get(); // 渲染的时候用的是最新的vm来渲染的
    this.value = newValue;
    if (this.user && this.cb) {
      this.cb.call(this.vm, newValue, oldValue);
    }
  }
}

还是借助 vue 响应式原理,默认在取值时将 watcher 存放到对应属性的 dep 中,当数据发生变化时通知对应的 watcher 重新执行

2.Computed 实现原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// state.js
if (options.computed) {
  initComputed(vm);
}

function initComputed(vm) {
  const computed = vm.$options.computed;
  const watchers = (vm._computedWatchers = {}); // 将计算属性watcher保存到vm上
  for (let key in computed) {
    let userDef = computed[key];
    // 监控计算属性中get的变化
    const fn = typeof userDef === 'function' ? userDef : userDef.get;
    // 如果直接new Watcher 默认就会执行fn 将属性和watcher对应起来
    watchers[key] = new Watcher(vm, fn, { lazy: true });
    defineComputed(vm, key, userDef);
  }
}

每个计算属性也都是一个watcher,计算属性需要表示 lazy:true,这样在初始化 watcher 时不会立即调用计算属性方法

1
2
3
4
5
6
7
8
9
// watcher.js
class Watcher {
  constructor(vm, exprOrFn, callback, options) {
    this.vm = vm;
    this.dirty = this.lazy;
    // ...
    this.value = this.lazy ? undefined : this.get(); // 调用get方法 会让渲染watcher执行
  }
}

默认计算属性需要保存一个 dirty 属性,用来实现缓存功能

1
2
3
4
5
6
7
8
9
// state.js
function defineComputed(target, key, userDef) {
  const setter = userDef.set || (() => {});
  // 每次取值都会执行 可以通过实例拿到对应的属性
  Object.defineProperty(target, key, {
    get: createComputedGetter(key),
    set: setter,
  });
}

创建缓存 getter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// state.js

// 计算属性根本不会收集依赖 只会让自己的依赖属性去收集依赖
function createComputedGetter(key) {
  return function () {
    const watcher = this._computedWatchers[key]; // 获取到对应属性的watcher
    if (watcher.dirty) {
      // 如果是脏的就去执行 用户传入的函数
      watcher.evaluate(); // 求值后 dirty变为了false 下次就不求值了
    }
    // 在求值的过程中 stack = [渲染watcher, 计算属性watcher]
    // 在evaluate执行完毕后 stack = [渲染watcher] => Dep.target = 渲染watcher
    if (Dep.target) {
      // 让计算属性watcher对应的两个dep记录watcher即可
      // 计算属性出栈后 还要渲染watcher 应该让计算属性watcher里面的属性 也去收集上一层watcher
      watcher.depend();
    }
    return watcher.value; // 最后返回的是watcher上的值
  };
}

watch.evaluate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// watcher.js
evaluate() {
  this.value = this.get(); // 获取到用户函数的返回值 并且还要标识为脏
  this.dirty = false;
}
update() {
  if (this.lazy) {
    // 如果是计算属性 依赖的值变化了 就标识计算属性是脏值了
    this.dirty = true;
  } else {
    queueWatcher(this); // 把当前的watcher 暂存起来
    // this.get(); // 重新渲染
  }
}

当依赖的属性变化时,会通知 watcher 调用 update 方法,此时我们将 dirty 置换为 true。这样再取值时会重新进行计算

1
2
3
4
5
6
7
8
9
10
11
12
13
// state.js
if (watcher.dirty) {
  // 如果是脏的就去执行 用户传入的函数
  watcher.evaluate(); // 求值后 dirty变为了false 下次就不求值了
}
// 在求值的过程中 stack = [渲染watcher, 计算属性watcher]
// 在evaluate执行完毕后 stack = [渲染watcher] => Dep.target = 渲染watcher
if (Dep.target) {
  // 让计算属性watcher对应的两个dep记录watcher即可
  // 计算属性出栈后 还要渲染watcher 应该让计算属性watcher里面的属性 也去收集上一层watcher
  watcher.depend();
}
return watcher.value; // 最后返回的是watcher上的值
1
2
3
4
5
6
7
8
9
// watcher.js
depend() {
  // watcher的depend 就是让watcher中dep去depend
  let i = this.deps.length;
  while (i--) {
    // 让dep记住渲染watcher
    this.deps[i].depend(); // 让计算属性watcher 也收集渲染watcher
  }
}

十.Vue 组件原理

1.全局组件的解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Vue.component内部会调用一个api Vue.extend() 产生一个组件的子类
Vue.component('outer-button', {
  // Vue.options.components = {}
  name: 'xxx',
  template: '<button>全局button</button>',
}); // new Sub().$mount('某个元素上') 每个组件在使用的时候都是通过类来new

Vue.component(
  'pf-button',
  Vue.extend({
    template: '<button>PF按钮</button>',
  })
);

// 函数式组件没有类 就是每次调用函数返回一个虚拟节点 所以函数式组件是没有watcher的
const vm = new Vue({
  el: '#app',
  // 局部组件(定义后只能在当前组件中使用)
  components: {
    'my-button': {
      beforeCreate() {
        console.log('beforeCreate 1');
      },
      components: {
        'my-ll': {
          template: '<div>我很帅</div>',
          beforeCreate() {
            console.log('beforeCreate 2');
          },
        },
      },
      template: '<button>我的按钮1111 <my-ll></my-ll></button>',
    },
  },
});

我们可以通过Vue.component注册全局组件,之后可以在模板中进行使用

1.1.Vue.component 方法

1
2
// index.js
initGlobalAPI(Vue);

Vue.component内部会调用Vue.extend方法,将定义挂载到Vue.options.components上。这也说明所有的全局组件最终都会挂载到这个变量上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// globalApi.js
export default function initGlobalAPI(Vue) {
  Vue.options = {};
  Vue.options._base = Vue;
  Vue.mixin = function (options) {
    this.options = mergeOptions(this.options, options);
    return this;
  };
  Vue.options.components = {}; // 放的是全局组件 全局的指令 Vue.otpions.directives
  Vue.component = function (id, definition) {
    // 如果definition已经是一个函数了 说明用户自己调用了Vue.extend
    definition =
      typeof definition === 'function' ? definition : Vue.extend(definition);
    Vue.options.components[id] = definition;
  };
}

1.2.Vue.extend 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 组件核心方法 可以手动创造组件进行挂载
Vue.extend = function (options) {
  // 就是实现根据用户的参数 返回一个构造而已
  function Sub(options = {}) {
    // 最终使用一个组件 就是new一个实例
    this._init(options); // 就是默认对子类进行初始化操作
  }
  // 子类继承父类原型方法
  Sub.prototype = Object.create(Vue.prototype); // Sub.prototype.__proto__ === Vue.prototype
  Sub.prototype.constructor = Sub;

  // 希望将用户传递的参数 和全局的Vue.options来合并
  Sub.options = mergeOptions(Vue.options, options); // 保存用户传递的选项
  return Sub;
};

extend方法就是创建出一个子类,继承于Vue,并返回这个类

1.3.属性合并

1
2
3
4
5
6
7
8
9
10
// globalAPI.js
function mergeField(key) {
  // 策略模式 减少if-else
  if (strats[key]) {
    options[key] = strats[key](parent[key], child[key]);
  } else {
    // 如果不在测量中则以儿子为主
    options[key] = child[key] || parent[key]; // 优先采用儿子 再采用父亲
  }
}

1.4.初始化合并

1
Sub.options = mergeOptions(Vue.options, options); // 保存用户传递的选项

2.组件的渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// vdom/index.js
const isReservedTag = (tag) => {
  return ['a', 'div', 'p', 'button', 'ul', 'li', 'span'].includes(tag);
};

// 创建元素vnode 等于render函数里面的 h=>h(App)
export function createElement(vm, tag, data = {}, ...children) {
  // 需要判断tag是元素还是组件
  if (isReservedTag(tag)) {
    return vnode(vm, tag, data, children, data.key, undefined);
  } else {
    // 创造组件的虚拟节点 组件需要找到组件的模版去进行渲染
    const Ctor = vm.$options.components[tag]; // 罪案构造函数
    // Ctor就是组件的定义 可能是一个Sub类 还有可能更是组件的obj选项
    return createComponentVnode(vm, tag, data, children, data.key, Ctor);
  }
}

在创建虚拟节点时我们要判断当前这个标签是否是组件,普通标签的虚拟节点和组件的虚拟节点有所不同

2.1.创建组件虚拟节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// vdom/index.js
// 创建元素vnode 等于render函数里面的 h=>h(App)
export function createElement(vm, tag, data = {}, ...children) {
  // 需要判断tag是元素还是组件
  if (isReservedTag(tag)) {
    return vnode(vm, tag, data, children, data.key, undefined);
  } else {
    // 创造组件的虚拟节点 组件需要找到组件的模版去进行渲染
    const Ctor = vm.$options.components[tag]; // 罪案构造函数
    // Ctor就是组件的定义 可能是一个Sub类 还有可能更是组件的obj选项
    return createComponentVnode(vm, tag, data, children, data.key, Ctor);
  }
}

const init = (vnode) => {
  // 组件的虚拟节点上有组件的实例 new Sub()._init()
  const child = (vnode.componentInstance = new vnode.componentOptions.Ctor({})); // 组件的children {} 放插槽属性
  child.$mount();
};

function createComponentVnode(vm, tag, key, data, children, Ctor) {
  if (typeof Ctor === 'object' && Ctor !== null) {
    Ctor = vm.$options._base.extend(Ctor); // 组件内部声明的components属性也会包装成类
  }
  data.hook = {
    // 稍后创造真实节点的时候 如果是组件则调用此init方法
    init,
  };
  return vnode(vm, tag, data, undefined, key, undefined, { Ctor, children });
}

2.2.创建组件的真实节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// // vdom/patch.js
export function patch(oldVnode, vnode) {
  if (!oldVnode) {
    // 这就是组件的挂载
    return createElm(vnode); // vm.$el 对应的就是组件渲染的结果了
  }
  // ...
}

// 调用组件初始化方法
function createComponent(vnode) {
  let i = vnode.data;
  if ((i = i.hook) && (i = i.init)) {
    i(vnode); // 初始化组件 找到init方法
  }
  if (vnode.componentInstance) {
    return true; // 说明是组件
  }
}

export function createElm(vnode) {
  const { tag, data, children, text } = vnode;
  if (typeof tag === 'string') {
    // 元素
    if (createComponent(vnode)) {
      return vnode.componentInstance.$el;
    }
    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;
}

调用init方法,进行组件的初始化

1
2
3
4
5
6
7
8
9
10
data.hook = {
  // 稍后创造真实节点的时候 如果是组件则调用此init方法
  init,
};

const init = (vnode) => {
  // 组件的虚拟节点上有组件的实例 new Sub()._init()
  const child = (vnode.componentInstance = new vnode.componentOptions.Ctor({})); // 组件的children {} 放插槽属性
  child.$mount();
};

Vue响应式原理

Vue2.0源码

源码地址