大家好,我是若川。持续组织了8个月源码共读活动,感兴趣的可以 点此加我微信ruochuan12 参与,每周大家一起学习200行左右的源码,共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列。另外:目前建有江西|湖南|湖北籍前端群,可加我微信进群。
之前的文章把响应式系统基本讲完了,没看过的同学可以看一下 vue.windliang.wang/。这篇文章主要是按照 Vue2 源码的目录格式和调用过程,把我们之前写的响应式系统移动进去。
html 中我们提供一个 id 为 root 的根 dom 。
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title></head><body><div id="root"></div><script src="bundle.js"></script></body>
</html>其中 bundle.js 就是我们打包好的测试代码,对应 ./VueLiang0/vueliang0.js ,代码如下:
import Vue from "./src/core/index";new Vue({el: "#root",data() {return {test: 1,name: "data:liang",};},watch: {test(newVal, oldVal) {console.log(newVal, oldVal);},},computed: {text() {return "computed:hello:" + this.name;},},methods: {hello() {return "调用methods:hello";},click() {this.test = 3;this.name = "wind";},},render() {const node = document.createElement("div");const dataNode = document.createElement("div");dataNode.innerText = this.test;node.append(dataNode);const computedNode = document.createElement("div");computedNode.innerText = this.text;node.append(computedNode);const methodsNode = document.createElement("div");methodsNode.innerText = this.hello();node.append(methodsNode);node.addEventListener("click", this.click);return node;},
});提供了 data 、watch 、computed、methods ,在 render 方法中正常情况的话应该是返回虚拟 dom ,这里我们直接生成一个真的 dom 返回。
代理
我们使用 data、methods 或者 computed 的时候,都是通过 this.xxx ,而不是 this.data.xxx 或者 this.methods.xxx ,是因为 Vue 帮我们把这些属性、方法都挂载到了 Vue 实例上。
挂载 methods
// VueLiang0/src/core/instance/state.js
function initMethods(vm, methods) {for (const key in methods) {vm[key] =typeof methods[key] !== "function" ? noop : bind(methods[key], vm);}
}挂载 computed
export function defineComputed(target, key, userDef) {...Object.defineProperty(target, key, sharedPropertyDefinition);
}挂载 data
function initData(vm) {let data = vm.$options.data;data = vm._data =typeof data === "function" ? getData(data, vm) : data || {};if (!isPlainObject(data)) {data = {};}// proxy data on instanceconst keys = Object.keys(data);const props = vm.$options.props;const methods = vm.$options.methods;let i = keys.length;while (i--) {const key = keys[i];// 检查 methods 是否有同名属性if (process.env.NODE_ENV !== "production") {if (methods && hasOwn(methods, key)) {console.warn(`Method "${key}" has already been defined as a data property.`,vm);}}// 检查 props 是否有同名属性if (props && hasOwn(props, key)) {process.env.NODE_ENV !== "production" &&console.warn(`The data property "${key}" is already declared as a prop. ` +`Use prop default value instead.`,vm);} else if (!isReserved(key)) { // 非内置属性proxy(vm, `_data`, key); // 代理}}observe(data); // 变为响应式数据
}为了保证 data 的对象值的稳定,我们的 data 属性其实是一个函数,返回一个对象,所以上边我们用 getData 方法先拿到对象。
export function getData(data, vm) {try {return data.call(vm, vm);} catch (e) {return {};}
}之后依次判断 data 属性是否和 methods 、computed 属性重名,非线上环境会打印警告,然后调用 isReserved 判断是否是内置属性。
/*** Check if a string starts with $ or _*/
export function isReserved(str) {const c = (str + "").charCodeAt(0);return c === 0x24 || c === 0x5f;
}最后调用 proxy 方法,将 data 属性挂在到  vm 对象中,相当于将 methods 、computed 的同名属性进行了覆盖。
export function proxy(target, sourceKey, key) {sharedPropertyDefinition.get = function proxyGetter() {return this[sourceKey][key];};sharedPropertyDefinition.set = function proxySetter(val) {this[sourceKey][key] = val;};Object.defineProperty(target, key, sharedPropertyDefinition);
}响应式
把各个属性初始化完成后,调用 mounted 方法,把我们的 dom 挂载到根节点中。
Vue.prototype._init = function (options) {const vm = this;vm.$options = options;vm._renderProxy = vm;initState(vm);if (vm.$options.el) {vm.$mount(vm.$options.el);}
};$mount 方法中把 el 对应的 dom 拿到,然后调用 mountComponent 方法进行挂载 dom 。
Vue.prototype.$mount = function (el) {el = el && document.querySelector(el);return mountComponent(this, el);
};mountComponent 方法中定义  updateComponent 方法和 Watcher 对象,这样当 updateComponent 中依赖的属性变化的时候,updateComponent 就会被自动调用。
export function mountComponent(vm, el) {vm.$el = el;let updateComponent;updateComponent = () => {vm._update(vm._render());};// we set this to vm._watcher inside the watcher's constructor// since the watcher's initial patch may call $forceUpdate (e.g. inside child// component's mounted hook), which relies on vm._watcher being already definednew Watcher(vm, updateComponent, noop /* isRenderWatcher */);return vm;
}_update 方法原本是进行虚拟 dom 的挂载,这里的话我们直接将 render 返回的 dom 进行挂载。
Vue.prototype._update = function (dom) {const vm = this;/*****这里仅仅是把 dom 更新,vue2 源码中这里会进行虚拟 dom 的处理 */if (vm.$el.children[0]) {vm.$el.removeChild(vm.$el.children[0]);}vm.$el.appendChild(dom);/*******************************/
};整体流程
入口文件代码如下:
import Vue from "./src/core/index";new Vue({el: "#root",...
});第一行代码 import Vue from "./src/core/index"; 的时候会进行一些初始化,src/core/index 代码如下:
// src/core/index
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'initGlobalAPI(Vue) // Vue 上挂载一些静态全局的方法export default Vue第一行 import Vue from './instance/index' 继续进行一些初始化,instance/index 代码如下:
// src/core/instance/index.js
import { initMixin } from "./init";
import { stateMixin } from "./state";
import { lifecycleMixin } from "./lifecycle";
import { renderMixin } from "./render";function Vue(options) {this._init(options);
}initMixin(Vue);
stateMixin(Vue);
lifecycleMixin(Vue);
renderMixin(Vue);export default Vue;initMixin 是在 Vue 挂载一个 _init 方法,也就是在 new Vue 的时候执行。
import { initState } from "./state";export function initMixin(Vue) {Vue.prototype._init = function (options) {const vm = this;vm.$options = options;vm._renderProxy = vm;initState(vm);if (vm.$options.el) {vm.$mount(vm.$options.el);}};
}_init 方法调用 initState  方法初始化 data 、watch 、computed、methods ,并且把他们变为响应式数据,还有上边讲到的把属性挂载到 Vue 实例上。
$mount 方法就是前边讲到的,把 render 返回的 dom 挂载到 el 节点上。
剩下的 stateMixin、lifecycleMixin 、renderMixin 是在  Vue.prototype  原型对象中挂载各种方法,这里不细说了。
所以整体过程就是下边的样子:

最开始的各种 Mixin 是在 Vue.prototype  原型对象上挂载需要的方法,initGlobalAPI 是直接在 Vue 上挂载方法,new Vue 就是传入 options 属性,接着调用 this.init 方法将 data 、watch 、computed、methods  这些进行初始化,最后调用 $mount 方法挂载 dom 。
最终效果
我们运行下程序,修改 webpack.config.js 的 entry 为我们写好的测试文件。
const path = require("path");
module.exports = {entry: "./VueLiang0/vueliang0.js",output: {path: path.resolve(__dirname, "./dist"),filename: "bundle.js",},devServer: {static: path.resolve(__dirname, "./dist"),},
};然后执行 npm run dev 。

可以看到 data 、computed 和 methods  都调用正常,接下来测试一下响应式,我们测试文件中添加了 click 事件。
import Vue from "./src/core/index";new Vue({el: "#root",data() {return {test: 1,name: "data:liang",};},watch: {test(newVal, oldVal) {console.log(newVal, oldVal);},},computed: {text() {return "computed:hello:" + this.name;},},methods: {hello() {return "调用methods:hello";},click() {this.test = 3;this.name = "wind";},},render() {const node = document.createElement("div");const dataNode = document.createElement("div");dataNode.innerText = this.test;node.append(dataNode);const computedNode = document.createElement("div");computedNode.innerText = this.text;node.append(computedNode);const methodsNode = document.createElement("div");methodsNode.innerText = this.hello();node.append(methodsNode);// click 事件node.addEventListener("click", this.click);return node;},
});点击的时候会更改 text 和 name 的值,看一下效果:

当我们点击的时候视图就自动进行了更新,简化的响应式系统就被我们实现了。
总
更详细代码的大家可以在 github 进行查看和调试。
https://github.com/wind-liang/vue2
现在我们的 render 函数是直接返回 dom ,当某个属性改变的时候整个 dom 树会全部重新生成,但更好的方式肯定是采用虚拟 dom ,进行局部更新。

················· 若川简介 ·················
你好,我是若川,毕业于江西高校。现在是一名前端开发“工程师”。写有《学习源码整体架构系列》20余篇,在知乎、掘金收获超百万阅读。
从2014年起,每年都会写一篇年度总结,已经坚持写了8年,点击查看年度总结。
同时,最近组织了源码共读活动,帮助4000+前端人学会看源码。公众号愿景:帮助5年内前端人走向前列。

扫码加我微信 ruochuan12、拉你进源码共读群
今日话题
目前建有江西|湖南|湖北 籍 前端群,想进群的可以加我微信 ruochuan12 进群。分享、收藏、点赞、在看我的文章就是对我最大的支持~