從零手寫 Vue 之響應式系統
之前的文章把響應式系統基本講完了,沒看過的同學可以看一下 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 />
<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 instance
const 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 defined
new 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 原型對象中掛載各種方法,這裏不細說了。
所以整體過程就是下邊的樣子:
image-20220529125250794
最開始的各種 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 。
image-20220529125906737
可以看到 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 的值,看一下效果:
Kapture 2022-05-29 at 13.01.11
當我們點擊的時候視圖就自動進行了更新,簡化的響應式系統就被我們實現了。
總
更詳細代碼的大家可以在 github 進行查看和調試。
https://github.com/wind-liang/vue2
現在我們的 render 函數是直接返回 dom ,當某個屬性改變的時候整個 dom 樹會全部重新生成,但更好的方式肯定是採用虛擬 dom ,進行局部更新。
接下來的幾篇文章就會開始虛擬 dom 的源碼解析了,歡迎繼續關注。
windliang 前端,生活,成長
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/zsRT9Yn_VtcV3yPZiSFGfg